美文网首页
Vue3 & TypeScript ---- tagsView

Vue3 & TypeScript ---- tagsView

作者: 牛会骑自行车 | 来源:发表于2023-05-03 14:11 被阅读0次
    效果图: contextMenu tags较多时有滚动条
    点:
    • 数据(tags(标签列表)、activeTag(当前标签)、以及更改这俩数据的方法、关闭一个或多个标签的方法)都放在了pinia(仓库)里,因为很多东西需要和pinia中的permission联动并且在不同地方都会使用,全部放仓库里管理起来更加方便~
      1. activeTag的更新:初始时赋值一次;刷新页面时在Router.beforeEach中用to.name或其他路由唯一值与之相匹配;
      2. tags的数据来源与左侧经过筛选后的路由保持一致,所以从pinia的permission中获取;
      3. tags的滚动:写一个可以获取到滚动容器的scrollLeft及可以获取到tagView视图区宽度及每个tagItem的offsetWidth与offsetLeft的布局即可。尽量自己写!不要使用el-scrollbar!!除非你想效仿mall-admin-master~
    • contextMenu(右键小菜单儿)
      1. 我选择将它设定在tagItem区域以鼠标落点为参照的固定定位。不要放在每一个tagItem(标签单项)中相对定位或者使用el-dropdown(组件库中下拉菜单)!!
      2. 根据tagItem的位置和是否为固定标签来判断contextMenu有哪几项操作;
    • 需要一丢丢计算的是点击靠左或靠右tagItem时需要将它之前或之后的tag也全部露出来,代码中的方法为autoScroll 思考时的图图.jpg

      不好意思。。昨晚因为工作的烦心事基本就没怎么睡觉。我知道这张图乱得一屁。。。。。不管你信不信哈哈哈哈我真的是通过画嚓这张图想明白的~

    还有刚刚着重强调的两点!我使用了感叹号儿的地儿!!一定不要尝试哇。。因为之前用Vue2时没用element-ui,都是自己写的。这次就想说,要不用一下子人家的组件吧?结果我了个妈耶~(除非你打算借鉴mall-admin https://gitee.com/youlaiorg/mall-admin 那当我没说嘿嘿)
    使用el-scrollbar没法子获取滚动容器(一长条儿的那个)的宽度;使用el-dropdown没法子获取tagItem的offsetLeft哈哈哈哈哈哈哈我谆的栓Q儿了我

    代码:(还存有报错的地儿可能是没粘全仓库里的数据,你可以先自己写个假的试试~
    TagView.vue 👇
    <script lang="ts" setup>
    import { RouteLocationNormalized } from 'vue-router';
    import Router from '@/router'
    import type { RouteType, TagType } from '@/store/types'
    
    import useStore from '@/store'
    
    import commonStyle from '@/assets/css/variables.module.scss'
    
    const { permission, tag, app } = useStore();
    
    // #region ---- tagsData
    ///////////////////////////////// fake data
    // const tagList = ref<TagType[]>([]);
    // for (let i = 0; i < 20; i++) {
    //     const item = {
    //         name: '哈哈哈' + i + '哈哈哈',
    //         meta: { title: i % 2 === 0 ? '哈哈哈 ' + (i + 1) + ' 哈哈哈' : '哈 ' + (i + 1), url: '哈哈哈' + i + '哈哈哈' }
    //     }
    //     tagList.value.push(item);
    // }
    const tags = computed(() => {
        return tag.state.tags;
    })
    // ---- 右键菜单对应tag
    const currentTag = ref<TagType>({
        name: "",
        meta: {}
    });
    // ---- 当前路由对应tag
    const activeTag = computed(() => {
        return tag.state.activeTag;
    })
    
    // #endregion
    
    // #region ---- tagsFunction
    
    const handleTag = (item: TagType, index: number) => {
        // 路由跳转、设置当前高亮
        Router.push(item.meta.url);
        app.state.activeRoute = item.meta.url;
        // 自动滚动
        autoScroll(item, index);
    }
    // ---- 滚动
    // #region ---- autoScroll
    const { proxy } = getCurrentInstance() as any;
    const refScrollWrap = ref();
    const refScrollCon = ref();
    
    const viewScrollLeft = ref(0);
    const handleScroll = (e: any) => {
        viewScrollLeft.value = e.target.scrollLeft;
    }
    const autoScroll = (item: TagType, index: number) => {
        // 容器
        const containerEl = refScrollWrap.value;
        // 可视区域
        const viewEl = refScrollWrap.value;
        // 当前DOM
        const currentEl = proxy.$refs[item.name][0];
        // 前一个元素DOM = index > 0 ? 前一个元素 : 当前元素
        const prevEl = index > 0 ? proxy.$refs[tags.value[index - 1].name][0] : currentEl;
        // 后一个元素DOM = index < tag列表最后一个元素index - 1 ? 后一个元素 : 当前元素
        const nextEl = index < tags.value.length - 1 ? proxy.$refs[tags.value[index + 1].name][0] : currentEl;
        // 缝儿 = firstTag || lastTag ? 14 : 5; 此处俩值根据公共样式中的缝儿宽决定 (commonStyle中的sideTagSeam和tagItemSeam与后面style中的tag-item中的样式保持一致昂。。)
        const seamWidth = index === 0 || index === tags.value.length - 1 ? parseInt(commonStyle.sideTagSeam) : parseInt(commonStyle.tagItemSeam);
    
        if (viewScrollLeft.value > prevEl.offsetLeft + seamWidth) {
            viewScrollLeft.value = prevEl.offsetLeft - seamWidth;
            containerEl.scrollLeft = viewScrollLeft.value;
        }
    
        if ((nextEl.offsetLeft + nextEl.offsetWidth + seamWidth) > (viewScrollLeft.value + containerEl.offsetWidth)) {
            viewScrollLeft.value = nextEl.offsetLeft + nextEl.offsetWidth + seamWidth - viewEl.offsetWidth;
            containerEl.scrollLeft = viewScrollLeft.value;
        }
    }
    
    // #endregion
    
    // #region ---- contextMemu
    const menu = reactive({
        left: "0",
        top: "0",
        show: false,
    })
    
    // ---- 打开右键菜单
    const handleContextMenu = ({ x, y }: any, item: TagType) => {
        menu.left = x + 'px';
        menu.top = y + 'px';
        menu.show = true;
    
        currentTag.value = item;
        // 点击页面其他地方 菜单消失
        window.addEventListener('click', menuDissapear);
        window.addEventListener('contextmenu', menuDissapear);
    }
    // ---- 菜单消失
    const menuDissapear = () => {
        if (menu.show) {
            menu.show = false;
            window.removeEventListener('click', menuDissapear);
            window.removeEventListener('contextmenu', menuDissapear);
        }
    }
    // ---- 菜单功能 👇
    // ---- 公共方法:当前Tag和当前路由是否匹配
    const isMatch = (tag: TagType | RouteType | RouteLocationNormalized) => {
        const currentRoute = Router.currentRoute.value;
        return currentRoute.name === tag.name;
    }
    // 右击小菜单儿
    // ---- 刷新
    const handleRefresh = () => {
        menuDissapear();
        Router.push({
            path: '/404',
            query: {
                redirect: currentTag.value.meta.url
            }
        })
    }
    // ---- 关闭
    const closeTag = (item: RouteType | RouteLocationNormalized | TagType) => {
        // 判断是否为当前项:是 - 路由跳转前一项;不是 - 只splice当前tag,路由无处理
        const index = tag.state.tags.findIndex((i: any) => i.name === item.name);
    
        if (isMatch(item)) {
            const beforeTag = tag.state.tags[index - 1];
            Router.push(beforeTag.meta.url);
        }
        tag.closeTag(item);
        menuDissapear();
    }
    // ---- 关闭右侧
    const showMenuItemCloseRight = computed(() => {
        const currentIndex = tags.value.findIndex((item: TagType) => item.name === currentTag.value.name);
        return currentIndex < tags.value.length - 1;
    })
    const handleCloseRight = () => {
        // 当前Tag索引
        const currentIndex = tags.value.findIndex((item: TagType) => item.name === currentTag.value.name);
        tags.value.map((item: TagType, index: number) => {
            if (index > currentIndex) tag.closeTag(item);
            if (!isMatch(currentTag.value)) Router.push(currentTag.value.meta.url);
        })
        menuDissapear();
    }
    // ---- 关闭其他:tags超过一个!fixedTag时显示
    const showMenuItemCloseOthers = computed(() => {
        const notFixedList = tags.value.filter((item: TagType) => !item.meta.fixedTag);
        return notFixedList.length > 1;
    })
    const handleCloseOthers = () => {
        tags.value.map((item: TagType) => {
            if (item.name === currentTag.value.name || item.meta.fixedTag) return;
            tag.closeTag(item);
    
            if (!isMatch(currentTag.value)) {
                Router.push(currentTag.value.meta.url);
            }
        })
        menuDissapear();
    }
    // ---- 关闭所有
    const showMenuItemCloseAll = computed(() => {
        const hasNotConstance = tags.value.findIndex((item: TagType) => !item.meta.fixedTag) !== -1;
        return hasNotConstance;
    })
    const handleCloseAll = () => {
        tag.closeAllTags();
        Router.push(tags.value[0].meta.url);
    }
    
    // #endregion
    
    // #region ---- 生命周期
    
    onMounted(() => {
        initTags();
    })
    // 初始化tagList数据
    const initTags = () => {
        // tags
        const tags = permission.state.constanceSideBarRoutes.filter((item: any) => item.meta.fixedTag);
        // currentTag
        const activeTag = Router.currentRoute.value;
        const isActiveTag = tags.findIndex((item: any) => item.name === activeTag.name) !== -1;
        if (!isActiveTag) tags.push(Router.currentRoute.value);
        tag.state.tags = tags;
    
        tag.state.activeTag = activeTag;
    }
    
    // #endregion
    
    </script>
    
    <template>
        <div class="tag-wrap" @scroll="handleScroll" ref="refScrollWrap">
            <div class="tag-container" ref="refScrollCon">
                <div v-for="(item, index) in tags" :key="item.name" class="tag-item" :class="{
                        fixed: item.meta.fixedTag,
                        active: activeTag.name === item.name
                    }" :style="{ color: item.color }" @mouseenter="item.color = commonStyle.$primary"
                    @mouseleave="item.color = '#222'" @click.native="handleTag(item, index)" :ref="item.name"
                    @contextmenu.stop.prevent="handleContextMenu($event, item)">
                    <span>{{ item.meta.title }}</span>
                    <span v-if="!item.meta.fixedTag" class="tag-close" @click.stop="closeTag(item)">×</span>
                </div>
            </div>
    
            <div class="contextmenu" :style="{ left: menu.left, top: menu.top }" v-show="menu.show">
                <div class="menu-item" @click="handleRefresh" @contextmenu.prevent="handleRefresh">刷新</div>
                <div class="menu-item" v-if="!currentTag.meta.fixedTag" @click="closeTag(currentTag)"
                    @contextmenu.prevent="closeTag(currentTag)">关闭</div>
                <div class="menu-item" v-if="showMenuItemCloseRight" @click="handleCloseRight"
                    @contextmenu.prevent="handleCloseRight">关闭右侧</div>
                <div class="menu-item" v-if="showMenuItemCloseOthers" @click="handleCloseOthers"
                    @contextmenu.prevent="handleCloseOthers">关闭其他</div>
                <div class="menu-item" v-if="showMenuItemCloseAll" @click="handleCloseAll"
                    @contextmenu.prevent="handleCloseAll">关闭所有</div>
            </div>
        </div>
    </template>
    
    <style lang="scss" scoped>
    .tag-wrap {
        overflow-x: scroll;
        border-bottom: 1px solid #d8dce5;
    }
    
    ::-webkit-scrollbar {
        height: 4px;
    }
    
    ::-webkit-scrollbar-thumb {
        background: $shadow_color;
    }
    
    .tag-container {
        display: inline-block;
        height: 36px;
    
        padding-top: 8px;
    
        user-select: none;
        white-space: nowrap;
    
        position: relative; // for take item's offsetLeft value
    }
    
    .contextmenu {
        position: fixed;
        left: 0;
        top: 0;
    
        padding: 4px 0;
        border-radius: 4px;
    
        background: #fff;
    
        z-index: 4;
    
        box-shadow: 0 1px 3px 0 $shadow_color, 0 0 3px 0 $shadow_color;
    
        .menu-item {
            padding: 4px 16px;
            text-align: center;
            font-size: 12px;
            color: #222;
            cursor: pointer;
    
            &:hover {
                background: $shadow_color;
            }
        }
    }
    
    .tag-item {
        display: inline-block;
        border: 1px solid $primary_border_color;
    
        height: 26px;
        line-height: 24px;
        font-size: 12px;
        color: #222;
    
        padding: 0 24px 0 6px;
        margin-right: $tagItemSeam;
        border-radius: 4px;
    
        position: relative;
        cursor: pointer;
    
        &:hover {
            color: $primary;
        }
    
        &:nth-child(1) {
            margin-left: 14px;
        }
    
        &:nth-last-child(1) {
            margin-right: 14px;
        }
    
        .tag-close {
            position: absolute;
            top: 50%;
            right: 6px;
            transform: translateY(-50%);
    
            cursor: pointer;
    
            width: 15px;
            height: 15px;
            text-align: center;
            line-height: 13px;
    
            border-radius: 50%;
            font-size: 15px;
    
            &:hover {
                color: $primary;
                background: $shadow_color;
            }
        }
    
    }
    
    
    .tag-item.fixed {
        padding-right: 6px;
    }
    
    .tag-item.active {
        border-color: $primary;
        background: $primary;
        color: #fff !important;
    
        .tag-close {
            &:hover {
                background: #fff;
            }
        }
    }
    </style>
    
    pinia中的tags.ts代码 👇
    import { defineStore } from 'pinia'
    import type { RouteLocationNormalized } from 'vue-router'
    import type { TagStateType, RouteType, TagType } from '../types'
    import { defaultRoute } from '@/utils/dict'
    
    const useTagsStore = defineStore('tag', () => {
        const state = reactive<TagStateType>({
            tags: [],
            activeTag: defaultRoute,
        })
    
    
        function setActiveTag(tag: any) {
            state.activeTag = tag;
        }
    
        function tagSetting(tag: RouteLocationNormalized) {
            const already = state.tags.findIndex((item: any) => item.name === tag.name) !== -1;
            // 列表中暂无该项 && 在左侧菜单中该项不隐藏 && 推
            !already && !tag.meta.hidden && state.tags.push(tag);
            setActiveTag(tag);
        }
    
        function closeTag(tag: RouteType | RouteLocationNormalized | TagType) {
            const index = state.tags.findIndex((item: any) => item.name === tag.name);
            state.tags.splice(index, 1);
        }
    
        function closeAllTags() {
            state.tags = state.tags.filter((tag: TagType) => {
                return !!tag.meta.fixedTag;
            })
        }
    
    
        return {
            state,
    
            setActiveTag,
            tagSetting,
            closeTag,
            closeAllTags,
        }
    })
    
    export default useTagsStore;
    

    tada~~一个功能健全的tagsView就完成啦~

    相关文章

      网友评论

          本文标题:Vue3 & TypeScript ---- tagsView

          本文链接:https://www.haomeiwen.com/subject/ssnzjdtx.html