Vue3 & TypeScript ---- tagsView

2023-05-03  本文已影响0人  牛会骑自行车
效果图: contextMenu tags较多时有滚动条
点:

还有刚刚着重强调的两点!我使用了感叹号儿的地儿!!一定不要尝试哇。。因为之前用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就完成啦~

上一篇下一篇

猜你喜欢

热点阅读