既然有需求,那么就手撸一个非常规侧边按钮组组件吧

前言

自从前两天发了我人生之中第一篇文章,底下的评论以及赞与收藏让我倍感荣幸与惶恐,思索许久还是决定把前段时间写的一个非常规侧边按钮组组件分享一下,当然了,也希望各位看官老爷轻喷。好咯,话不多说,直接上代码;

截图

效果展示

目录结构

使用

代码部分

YSideBtnsContent>index.vue

vue2 复制代码
<template>
  <!-- <transition name="el-fade-in-linear"> -->
  <div
    class="y-btn-content-item"
    :style="{ height: `calc( 100vh - ${$parent.top} - 20px)`, width }"
    v-show="isActiveToShowContent"
  >
    <slot></slot>
  </div>
  <!-- </transition> -->
</template>
<script>
export default {
  name: "YSideBtnsContent",
  props: {
    name: String, // 每个下方内容区都有自己的name名字
    isShow: {
      default: true,
    },
    disabled: {
      // 是否禁用这一项
      type: Boolean,
      default: false, // 默认不禁用
    },
    width: {
      type: String,
      default: () => "100px",
    },
  },
  computed: {
    // 控制根据高亮的tab显示对应标签页内容
    isActiveToShowContent() {
      let activeName = this.$parent.value; // 比如当前高亮的是 sunwukong
      let currentName = this.name; // this.name的值有很多个,有:sunwukong、zhubajie、shaheshang...
      // 谁等于,就显示谁
      return this.isShow && activeName === currentName;
    },
  },
};

</script>
<style lang="scss" scoped>
.y-btn-content-item {
  // padding: 12px;
  height: 100%;
  background-color: #48484c;
  [data-theme="theme1"] & {
    background: #e8effa;
  }
}
</style>

YSideBtns>index.vue

vue2 复制代码
<script>
import BtnNav from "./components/BtnNav";
export default {
  name: "YSideBtns",
  components: { BtnNav },
  props: {
    value: null, // v-model接参
    beforeLeave: {
      // 切换标签之前的钩子,若返回 false 或者返回 Promise 且被 reject,则阻止切换
      type: Function,
      default: () => {
        return true; // 默认为true,始终允许切换tab
      },
    },
    top: {
      type: String,
      default: () => "0",
    },
    width: {
      type: String,
      default: () => "76px",
    },
  },
  data() {
    return {
      btnItemArr: [],
      activeName: this.value,
    };
  },
  watch: {
    value(n) {
      this.setCurrentName(n);
    },
  },
  mounted() {
    /**
     * 计算收集btns页内容信息,将需要用到的信息存在btnItemArr数组中
     * 并传递给sideBtn组件,sideBtn组件根据btnItemArr信息去v-for渲染有哪些
     * */
    this.calcSideBtnItemInstances();
    /**
     * 将组件挂载到BODY上
     */
    // this.$nextTick(() => {
    //   const body = document.querySelector('body')
    //   const YSideBtns = document.querySelector('body>.SideBtns-Box')
    //   YSideBtns
    //     ? () => {
    //         if (body.append) {
    //           body.append(this.$el)
    //         } else {
    //           body.appendChild(this.$el)
    //         }
    //       }
    //     : null
    // })
  },
  methods: {
    /**
     * @description 重点方法,获取使用的地方的Y-SideBtns标签中间的内容并存储
     */
    calcSideBtnItemInstances() {
      if (this.$slots.default) {
        // 收集Y-SideBtns标签中间的插槽内容数组
        let slotBtnItemArr = this.$slots.default.filter((v) => v.tag); // console.log("slotBtnItemArr", slotBtnItemArr);
        // 然后把这些数据交给sideBtn动态渲染
        this.btnItemArr = slotBtnItemArr.map((item) => {
          return item.componentInstance; // 只保留componentInstance组件实例即可,可以理解为组件的this
        });
      } else {
        this.btnItemArr = []; // 没传递就置为空,当然需要规范使用组件,规范传递相关参数
      }
    },
    /**
     * @description 点击btn的回调
     */
    handleBtnClick(btnItem) {
      // this.$emit("btnClick", btnItem, btnItem.name); // 通知父元素点击的是谁,是哪个SideBtn
      let newBtnName = btnItem.name; // 获取传出来的最新的name名字
      this.setCurrentName(newBtnName); // 执行更新方法
    },
    async setCurrentName(newBtnName) {
      let oldBtnName = this.activeName; // 要更新了,所以当下的就变成旧的了
      let res = await this.beforeLeave(newBtnName, oldBtnName);
      if (res) {
        this.$emit("input", newBtnName); // 更新父组件的v-model绑定的值
        this.$emit("btnClick", newBtnName); // 通知父元素点击的是谁,是哪个SideBtn
        this.activeName = newBtnName; // 自身也更新一下
      } else {
        this.$message.warning("您没有权限!");
      }
    },
  },
  render() {
    // 准备参数,以便把参数传递给btn-nav组件
    const btnsData = {
      props: {
        btnItemArr: this.btnItemArr, // 内容区相关信息数组
        activeName: this.activeName, // 当前高亮的是哪一项
        onBtnClick: this.handleBtnClick, // 点击某一tab项的回调
      },
    };
    // <transition name="el-zoom-in-center"> v-show={this.activeName}
    // </transition>
    return (
      <div class="SideBtns-Box" style={{ top: this.top, width: this.width }}>
        <BtnNav {...btnsData}></BtnNav>
        <div class="my-btn-content-item-box">{this.$slots.default}</div>
      </div>
    );
  },
};
</script>
<style lang="scss" scoped>
.SideBtns-Box {
  display: flex;
  justify-content: space-around;
  position: fixed;
  right: 0;
  z-index: 999;
  transition: width 0.2s;
}
</style>

YSideBtns>components>BtnNav.vue

vue2 复制代码
<script>
export default {
  name: "BtnNav",
  props: {
    // 源自于内容区的数组数据,非常重要
    btnItemArr: {
      type: Array,
      default: () => [],
    },
    // 当前激活的名字
    activeName: {
      type: String,
      default: "",
    },
    // 接收点击选项卡函数,在点击tab选项卡的时候,通过此函数传递出去
    onBtnClick: {
      type: Function,
    },
  },
  methods: {
    /**
     * @description 点击按钮的回调
     */
    changeActiveName(btnItem) {
      // 自己点自己就不让执行了
      if (btnItem.name === this.activeName) {
        return;
      }
      // 如果包含禁用项disabled属性(即处于禁用状态),也不让执行
      if (btnItem.disabled) {
        return;
      }
      this.onBtnClick(btnItem);
    },
  },
  render() {
    let topHtmlTemplate = [];
    let activeHtmlTemplate = [];
    let btmHtmlTemplate = [];
    let isTop = true;
    const getBtnHtml = (btn, idx) => {
      return (
        <div
          onClick={() => {
            this.changeActiveName(btn);
          }}
          class="my-tab-nav-item-box-btn flex_c_m "
          key={idx}
        >
          {this.activeName === btn.name ? (
            // <img src={btn.$attrs.activeImg} />
            <span class={" iconfont " + btn.$attrs.activeIcon}></span>
          ) : (
            // <img src={btn.$attrs.deActiveImg} />
            <span class={" iconfont " + btn.$attrs.deActiveIcon}></span>
          )}
        </div>
      );
    };
    this.btnItemArr.forEach((btn, idx) => {
      if (isTop) {
        if (btn.name === this.activeName) {
          isTop = false;
          activeHtmlTemplate.push(getBtnHtml(btn, idx));
        } else {
          topHtmlTemplate.push(getBtnHtml(btn, idx));
        }
      } else {
        btmHtmlTemplate.push(getBtnHtml(btn, idx));
      }
    });
    return (
      <div class={"my-tab-nav-item-box " + (this.activeName ? "isOpen" : "")}>
        {topHtmlTemplate.length != 0 ? (
          <div class="my-tab-nav-item-box-deactive-container ">
            {topHtmlTemplate.map((html) => {
              return html;
            })}
          </div>
        ) : null}
        {activeHtmlTemplate.length != 0 ? (
          <div class="my-tab-nav-item-box-active-container flex_c_m ">
            {activeHtmlTemplate.map((html) => {
              return html;
            })}
          </div>
        ) : null}
        {btmHtmlTemplate.length != 0 ? (
          <div class="my-tab-nav-item-box-deactive-container ">
            {btmHtmlTemplate.map((html) => {
              return html;
            })}
          </div>
        ) : null}
      </div>
    );
  },
};
</script>
<style lang="scss" scoped>
.my-tab-nav-item-box {
  text-align: right;
  font-size: 0;
  span {
    @include font_level1_color();
    font-size: 22px;
    cursor: pointer;
  }
  .my-tab-nav-item-box-deactive-container {
    overflow: hidden;
    display: inline-block;
    border-top-left-radius: 1rem;
    border-bottom-left-radius: 1rem;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    background: rgba(41, 42, 45, 0.9);
    [data-theme="theme1"] & {
      background: rgba(0, 113, 255, 0.05);
    }
    .my-tab-nav-item-box-btn {
      &:hover {
        background: $theme-highlight-tab-color;
        span {
          @include font_color();
        }
      }
    }
  }
  .my-tab-nav-item-box-active-container {
    height: 110px;
    width: 92px;
    overflow: hidden;
    // padding: 1rem 1rem 1rem 1.5rem;
    border-top-left-radius: 1rem;
    border-bottom-left-radius: 1rem;
    background-color: #48484c;
    [data-theme="theme1"] & {
      background: #e8effa;
    }
    span {
      @include font_color();
    }
  }
  .my-tab-nav-item-box-btn {
    // padding: 1rem 0;
    height: 92px;
    width: 76px;
    border-bottom: 1px solid transparent;
    position: relative;
    &:not(:last-of-type) {
      &::after {
        content: "";
        position: absolute;
        width: 30px;
        height: 1px;
        // background: $text-input-color2;
        @include bg_sub_color6();
        bottom: 0;
        left: calc(50% - 15px);
      }
    }
  }
}
.my-tab-nav-item-box.isOpen {
  span {
    color: $text-level1-color1;
  }
  .my-tab-nav-item-box-deactive-container {
    background: rgba(41, 42, 45, 0.9);
    [data-theme="theme1"] & {
      background: rgba(0, 0, 0, 0.3);
    }
  }
}
</style>
相关推荐
甜兒.20 分钟前
鸿蒙小技巧
前端·华为·typescript·harmonyos
Jiaberrr4 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy4 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白4 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、4 小时前
Web Worker 简单使用
前端
web_learning_3214 小时前
信息收集常用指令
前端·搜索引擎
tabzzz4 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百5 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao5 小时前
自动化测试常用函数
前端·css·html5
码爸5 小时前
flink doris批量sink
java·前端·flink