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

前言

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

截图

效果展示

目录结构

使用

代码部分

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>
相关推荐
前端郭德纲1 小时前
浅谈React的虚拟DOM
前端·javascript·react.js
2401_879103682 小时前
24.11.10 css
前端·css
ComPDFKit3 小时前
使用 PDF API 合并 PDF 文件
前端·javascript·macos
yqcoder3 小时前
react 中 memo 模块作用
前端·javascript·react.js
优雅永不过时·4 小时前
Three.js 原生 实现 react-three-fiber drei 的 磨砂反射的效果
前端·javascript·react.js·webgl·threejs·three
神夜大侠6 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱6 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号7 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72937 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲7 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6