• 10 Vue3 UI Framework Tabs 组件


    标签页是非常常用的组件,接下来我们来制作一个简单的 Tabs 组件

    返回阅读列表点击 这里

    需求分析

    我们先做一个简单的需求分析

    1. 可以选择标签页排列的方向
    2. 选中的标签页应当有下划线高亮显示
    3. 切换选中时,下划线应当有动画效果
    4. 应当允许更换颜色

    那么可以整理出以下参数表格

    参数 含义 类型 可选值 默认值
    direction 方向 string row / column row
    selected 默认选中 string 子项的 name 必填
    color 颜色 string 任意合法颜色值 #d3c8f5

    通过为子项设置 name 属性,来指定默认值

    骨架

    本体

    通过需求分析我们可以得到如下骨架:

    <template>
      <div
        class="jeremy-tabs"
        :style="{ '--color': color }"
        ref="container"
        :direction="direction"
      >
        <div class="jeremy-tabs-titles">
          <button
            v-for="(title, index) in titles"
            :key="index"
            class="jeremy-tabs-title"
            :class="{ selected: names[index] === selected }"
            @click="select(index)"
            :ref="
              (el) => {
                if (names[index] === selected) {
                  selectedItem = el;
                }
              }
            "
          >
            {{ title }}
          </button>
          <div class="jeremy-tabs-indicator" ref="indicator"></div>
        </div>
        <div class="jeremy-tabs-divider"></div>
        <div class="jeremy-tabs-content">
          <component :is="content" :key="selected" />
        </div>
      </div>
    </template>
    

    注意

    这里我们用一个 div 来充当下划线,再使用一个新的 component 来显示用户输入的内容

    我们还需要为标签页创建子组件,即 Tab 组件

    子组件

    通过之前的分析,可以得出子组件 Tab 的骨架如下:

    <template>
        <div>
            <slot></slot>
        </div>
    </template>
    

    另外,我们还需要定义一个参数,也就是标签的标题,所以还应该有如下声明与导出:

    declare const props: {
      title: string;
    };
    
    export default {
      install: function (Vue) {
        Vue.component(this.name, this);
      },
      name: "JeremyTab",
      props: {
        title: {
          type: String,
          default: "标签页",
        },
      },
    };
    

    功能

    首先,我们先在 TypeScript 中声明:

    declare const props: {
        direction?: "row" | "column";
        selected: String;
        color: String;
    };
    declare const context: SetupContext;
    

    其次,再在 export default 中,写入我们的参数:

    export default {
      name: "JeremyTabs",
      props: {
        direction: {
          type: String,
          default: "row",
        },
        selected: {
          type: String,
          required: true,
        },
        color: {
          type: String,
          default: "#8c6fef",
        },
      },
    };
    

    再次,再补全 setup 方法:

      setup(props, context) {
        if (!["row", "column"].includes(props.direction)) {
          throw new Error("错误的方向");
        }
        const container = ref<HTMLDivElement>(null);
        const selectedItem = ref<HTMLButtonElement>(null);
        const indicator = ref<HTMLDivElement>(null);
        const slots = context.slots.default();
        slots.forEach((slot) => {
          if (slot.type !== JeremyTab) {
            throw new Error("一级子标签必须是 JeremyTab");
          }
          if (!slot.props) {
            throw new Error("存在 JeremyTab 属性列为空");
          }
          if (!("title" in slot.props)) {
            throw new Error("JeremyTab 缺少属性 title");
          }
          if (!("name" in slot.props)) {
            throw new Error("JeremyTab 缺少属性 name");
          }
        });
        const titles = slots.map((slot) => slot.props.title);
        const names = slots.map((slot) => slot.props.name);
        if (!names.includes(props.selected)) {
          throw new Error("指定了不存在的 selected 值");
        }
        const content = computed(() =>
          slots.find((slot) => slot.props.name === props.selected)
        );
        onMounted(() => {
          watchEffect(
            () => {
              if (props.direction === "row") {
                const { height } = selectedItem.value.getBoundingClientRect();
                indicator.value.style.top = height + "px";
                const { width } = selectedItem.value.getBoundingClientRect();
                indicator.value.style.width = width + "px";
                const left1 = container.value.getBoundingClientRect().left;
                const left2 = selectedItem.value.getBoundingClientRect().left;
                const left = left2 - left1;
                indicator.value.style.left = left + "px";
              } else {
                const { height } = selectedItem.value.getBoundingClientRect();
                indicator.value.style.height = height + "px";
                const { width } = selectedItem.value.getBoundingClientRect();
                indicator.value.style.left = width + "px";
                const top1 = container.value.getBoundingClientRect().top;
                const top2 = selectedItem.value.getBoundingClientRect().top;
                const top = top2 - top1;
                indicator.value.style.top = top + "px";
              }
            },
            { flush: "post" }
          );
        });
        const select = (index) => {
          context.emit("update:selected", names[index]);
        };
    
        return {
          container,
          selectedItem,
          indicator,
          slots,
          titles,
          names,
          content,
          select,
        };
      },
    

    样式表

    最后,再补全样式表

    $theme-color: var(--color);
    .jeremy-tabs {
      display: flex;
      flex-direction: column;
      position: relative;
      &-titles {
        display: flex;
      }
      &-title {
        padding: 4px 6px;
        border: none;
        cursor: pointer;
        outline: none;
        background: white;
        &:focus {
          outline: none;
        }
        &:hover {
          color: $theme-color;
        }
        &.selected {
          color: $theme-color;
        }
      }
      &-indicator {
        position: absolute;
        transition: all 250ms;
        border: 1px solid $theme-color;
      }
      &-divider {
        border: 1px solid rgb(184, 184, 184);
      }
      &-content {
        padding: 8px 4px;
      }
    }
    .jeremy-tabs[direction="column"] {
      flex-direction: row;
      > .jeremy-tabs-titles {
        flex-direction: column;
      }
      > .jeremy-tabs-content {
        padding: 2px 10px;
      }
    }
    

    测试

    JeremyTabs 组件引入到测试文档,查看一下运行效果

    tabs

    项目地址

    GitHub: https://github.com/JeremyWu917/jeremy-ui

    官网地址

    JeremyUI: https://ui.jeremywu.top

    感谢阅读 ☕

  • 相关阅读:
    centos 7 安装mqtt 修改用户名和密码
    5-STM32物联网开发WIFI(ESP8266)+GPRS(Air202)系统方案升级篇(,远程升级GPRS内部程序)
    ESP8266开发综合篇第十四节(LUA)-8266作为TCP服务器,Android客户端连接,显示温湿度,控制继电器
    ESP8266开发综合篇第一节(LUA)-下载和刷固件
    springMvc源码学习之:spirngMvc的拦截器使用
    Java Web学习(1): 客户端请求、服务器响应及其HTTP状态码
    HttpServletRequest常用获取URL的方法
    java多线程:并发包中的信号量和计数栓的编程模型
    springMvc源码学习之:利用springMVC随时随地获取HttpServletRequest等对象
    springMvc源码学习之:spirngMvc的参数注入的问题
  • 原文地址:https://www.cnblogs.com/jeremywucnblog/p/15709478.html
Copyright © 2020-2023  润新知