feat(TagsView): Add TagsView component
feat(ContextMenu): Add ContextMenu component feat(store): Add tagsView storemaster
parent
4612e5544b
commit
349ac9d398
@ -0,0 +1,3 @@
|
||||
import ContextMenu from './src/ContextMenu.vue'
|
||||
|
||||
export { ContextMenu }
|
||||
@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { PropType } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
schema: {
|
||||
type: Array as PropType<contextMenuSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
trigger: {
|
||||
type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
|
||||
default: 'contextmenu'
|
||||
}
|
||||
})
|
||||
|
||||
const command = (item: contextMenuSchema) => {
|
||||
item.command && item.command(item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown
|
||||
:trigger="trigger"
|
||||
placement="bottom-start"
|
||||
@command="command"
|
||||
popper-class="v-context-menu-popper"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="(item, index) in schema"
|
||||
:key="`dropdown${index}`"
|
||||
:divided="item.divided"
|
||||
:disabled="item.disabled"
|
||||
:command="item"
|
||||
>
|
||||
<Icon :icon="item.icon" /> {{ t(item.label) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.v-context-menu-popper {
|
||||
min-width: 150px;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,356 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { filterAffixTags } from './helper'
|
||||
import { ContextMenu } from '@/components/ContextMenu'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { currentRoute, push, replace } = useRouter()
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const visitedViews = computed(() => tagsViewStore.getVisitedViews)
|
||||
|
||||
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
|
||||
|
||||
// 初始化tag
|
||||
const initTags = () => {
|
||||
affixTagArr.value = filterAffixTags(unref(routers))
|
||||
for (const tag of unref(affixTagArr)) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTag = ref<RouteLocationNormalizedLoaded>()
|
||||
|
||||
// 新增tag
|
||||
const addTags = () => {
|
||||
const { name } = unref(currentRoute)
|
||||
if (name) {
|
||||
selectedTag.value = unref(currentRoute)
|
||||
tagsViewStore.addView(unref(currentRoute))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 关闭选中的tag
|
||||
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
|
||||
if (view?.meta?.affix) return
|
||||
tagsViewStore.delView(view)
|
||||
if (isActive(view)) {
|
||||
toLastView()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
const closeAllTags = () => {
|
||||
tagsViewStore.delAllViews()
|
||||
toLastView()
|
||||
}
|
||||
|
||||
// 关闭其他
|
||||
const closeOthersTags = () => {
|
||||
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
|
||||
if (!view) return
|
||||
tagsViewStore.delCachedView()
|
||||
const { fullPath } = view
|
||||
await nextTick()
|
||||
replace({
|
||||
path: '/redirect' + fullPath
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭左侧
|
||||
const closeLeftTags = () => {
|
||||
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
const closeRightTags = () => {
|
||||
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
const toLastView = () => {
|
||||
const visitedViews = tagsViewStore.getVisitedViews
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
push(latestView)
|
||||
} else {
|
||||
if (
|
||||
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
|
||||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
|
||||
) {
|
||||
addTags()
|
||||
return
|
||||
}
|
||||
// You can set another route
|
||||
push(permissionStore.getAddRouters[0].path)
|
||||
}
|
||||
}
|
||||
|
||||
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
|
||||
return route.path === unref(currentRoute).path
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
() => {
|
||||
addTags()
|
||||
// moveToCurrentTag()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[var(--tags-view-height)]">tagsView</div>
|
||||
<div class="v-tags-view h-[var(--tags-view-height)] flex w-full">
|
||||
<span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
|
||||
<Icon icon="ant-design:left-outlined" color="#333" />
|
||||
</span>
|
||||
<div class="overflow-hidden flex-1">
|
||||
<ElScrollbar>
|
||||
<div class="flex h-[var(--tags-view-height)]">
|
||||
<ContextMenu
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ant-design:sync-outlined',
|
||||
label: t('common.reload'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
refreshSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:close-outlined',
|
||||
label: t('common.closeTab'),
|
||||
command: () => {
|
||||
closeSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:vertical-right-outlined',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[0].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:vertical-left-outlined',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:tag-outlined',
|
||||
label: t('common.closeOther'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:line-outlined',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
v-for="item in visitedViews"
|
||||
:key="item.fullPath"
|
||||
:class="[
|
||||
'v-tags-view__item',
|
||||
{
|
||||
'v-tags-view__item--affix': item?.meta?.affix,
|
||||
'is-active': isActive(item)
|
||||
}
|
||||
]"
|
||||
>
|
||||
<router-link :to="{ ...item }" custom #default="{ navigate }">
|
||||
<div @click="navigate" class="h-full">
|
||||
{{ t(item?.meta?.title as string) }}
|
||||
<Icon
|
||||
class="v-tags-view__item--close"
|
||||
color="#333"
|
||||
icon="ant-design:close-outlined"
|
||||
:size="12"
|
||||
@click.prevent.stop="closeSelectedTag(item)"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
<span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
|
||||
<Icon icon="ant-design:right-outlined" color="#333" />
|
||||
</span>
|
||||
<span
|
||||
class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer"
|
||||
@click="refreshSelectedTag(selectedTag)"
|
||||
>
|
||||
<Icon icon="ant-design:reload-outlined" color="#333" />
|
||||
</span>
|
||||
<ContextMenu
|
||||
trigger="click"
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ant-design:sync-outlined',
|
||||
label: t('common.reload'),
|
||||
command: () => {
|
||||
refreshSelectedTag(selectedTag)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:close-outlined',
|
||||
label: t('common.closeTab')
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:vertical-right-outlined',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:vertical-left-outlined',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:tag-outlined',
|
||||
label: t('common.closeOther'),
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:line-outlined',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
>
|
||||
<span
|
||||
class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer block"
|
||||
>
|
||||
<Icon icon="ant-design:down-outlined" color="#333" />
|
||||
</span>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-tags-view';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__tool {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
:deep(span) {
|
||||
color: var(--el-color-black) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--top-tool-border-color);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__item + &__item {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
height: calc(~'100% - 4px');
|
||||
padding: 0 15px;
|
||||
font-size: 12px;
|
||||
line-height: calc(~'var( - -tags-view-height) - 4px');
|
||||
cursor: pointer;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&--close {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 5px;
|
||||
display: none;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
&:not(.@{prefix-cls}__item--affix):hover {
|
||||
.@{prefix-cls}__item--close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item:not(.@{prefix-cls}__item--affix) {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
&__item:not(.is-active) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__item.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
.@{prefix-cls}__item--close {
|
||||
:deep(span) {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
|
||||
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
|
||||
let tags: RouteLocationNormalizedLoaded[] = []
|
||||
routes.forEach((route) => {
|
||||
const meta = route.meta as RouteMeta
|
||||
const tagPath = pathResolve(parentPath, route.path)
|
||||
if (meta?.affix) {
|
||||
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tags
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const getCaches = computed((): string[] => {
|
||||
return tagsViewStore.getCachedViews
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<keep-alive :include="getCaches">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</template>
|
||||
</router-view>
|
||||
</template>
|
||||
@ -1,5 +1,10 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts" name="Menu111">
|
||||
import { onMounted } from 'vue'
|
||||
onMounted(() => {
|
||||
console.log('????')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Menu111 <input type="text" /></div>
|
||||
<div class="h-[100000px]">Menu111 <input type="text" /></div>
|
||||
</template>
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
declare type contextMenuSchema = {
|
||||
disabled?: boolean
|
||||
divided?: boolean
|
||||
icon?: string
|
||||
label: string
|
||||
command?: (item: contextMenuSchema) => viod
|
||||
}
|
||||
Loading…
Reference in New Issue