feat(TagsView): Add TagsView component

feat(ContextMenu): Add ContextMenu component

feat(store): Add tagsView store
This commit is contained in:
kailong321200875 2022-01-16 17:55:20 +08:00
parent 4612e5544b
commit 349ac9d398
20 changed files with 900 additions and 164 deletions

View File

@ -84,6 +84,7 @@
"vite-plugin-purge-icons": "^0.7.0", "vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-style-import": "^1.4.1", "vite-plugin-style-import": "^1.4.1",
"vite-plugin-svg-icons": "^1.1.0", "vite-plugin-svg-icons": "^1.1.0",
"vite-plugin-vue-setup-extend": "^0.3.0",
"vite-plugin-windicss": "^1.6.2", "vite-plugin-windicss": "^1.6.2",
"vue-tsc": "^0.30.2", "vue-tsc": "^0.30.2",
"windicss": "^3.4.2", "windicss": "^3.4.2",

View File

@ -33,7 +33,6 @@ specifiers:
lodash-es: ^4.17.21 lodash-es: ^4.17.21
mockjs: ^1.1.0 mockjs: ^1.1.0
nprogress: ^0.2.0 nprogress: ^0.2.0
path-to-regexp: ^6.2.0
pinia: ^2.0.9 pinia: ^2.0.9
postcss: ^8.4.5 postcss: ^8.4.5
postcss-html: ^1.3.0 postcss-html: ^1.3.0
@ -54,6 +53,7 @@ specifiers:
vite-plugin-purge-icons: ^0.7.0 vite-plugin-purge-icons: ^0.7.0
vite-plugin-style-import: ^1.4.1 vite-plugin-style-import: ^1.4.1
vite-plugin-svg-icons: ^1.1.0 vite-plugin-svg-icons: ^1.1.0
vite-plugin-vue-setup-extend: ^0.3.0
vite-plugin-windicss: ^1.6.2 vite-plugin-windicss: ^1.6.2
vue: 3.2.26 vue: 3.2.26
vue-i18n: 9.1.9 vue-i18n: 9.1.9
@ -74,7 +74,6 @@ dependencies:
lodash-es: registry.nlark.com/lodash-es/4.17.21 lodash-es: registry.nlark.com/lodash-es/4.17.21
mockjs: registry.npmmirror.com/mockjs/1.1.0 mockjs: registry.npmmirror.com/mockjs/1.1.0
nprogress: registry.npmmirror.com/nprogress/0.2.0 nprogress: registry.npmmirror.com/nprogress/0.2.0
path-to-regexp: registry.npmmirror.com/path-to-regexp/6.2.0
pinia: registry.npmmirror.com/pinia/2.0.9_typescript@4.5.4+vue@3.2.26 pinia: registry.npmmirror.com/pinia/2.0.9_typescript@4.5.4+vue@3.2.26
qs: registry.npmmirror.com/qs/6.10.3 qs: registry.npmmirror.com/qs/6.10.3
vue: registry.npmmirror.com/vue/3.2.26 vue: registry.npmmirror.com/vue/3.2.26
@ -125,6 +124,7 @@ devDependencies:
vite-plugin-purge-icons: registry.nlark.com/vite-plugin-purge-icons/0.7.0_vite@2.7.10 vite-plugin-purge-icons: registry.nlark.com/vite-plugin-purge-icons/0.7.0_vite@2.7.10
vite-plugin-style-import: registry.npmmirror.com/vite-plugin-style-import/1.4.1_vite@2.7.10 vite-plugin-style-import: registry.npmmirror.com/vite-plugin-style-import/1.4.1_vite@2.7.10
vite-plugin-svg-icons: registry.npmmirror.com/vite-plugin-svg-icons/1.1.0_vite@2.7.10 vite-plugin-svg-icons: registry.npmmirror.com/vite-plugin-svg-icons/1.1.0_vite@2.7.10
vite-plugin-vue-setup-extend: registry.npmmirror.com/vite-plugin-vue-setup-extend/0.3.0_vite@2.7.10
vite-plugin-windicss: registry.npmmirror.com/vite-plugin-windicss/1.6.2_vite@2.7.10 vite-plugin-windicss: registry.npmmirror.com/vite-plugin-windicss/1.6.2_vite@2.7.10
vue-tsc: registry.npmmirror.com/vue-tsc/0.30.2_typescript@4.5.4 vue-tsc: registry.npmmirror.com/vue-tsc/0.30.2_typescript@4.5.4
windicss: registry.npmmirror.com/windicss/3.4.2 windicss: registry.npmmirror.com/windicss/3.4.2
@ -1732,7 +1732,7 @@ packages:
{ {
integrity: sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc=, integrity: sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc=,
registry: https://registry.npm.taobao.org/, registry: https://registry.npm.taobao.org/,
tarball: https://registry.nlark.com/eslint-utils/download/eslint-utils-2.1.0.tgz?cache=0&sync_timestamp=1631600361784&other_urls=https%3A%2F%2Fregistry.nlark.com%2Feslint-utils%2Fdownload%2Feslint-utils-2.1.0.tgz tarball: https://registry.nlark.com/eslint-utils/download/eslint-utils-2.1.0.tgz
} }
name: eslint-utils name: eslint-utils
version: 2.1.0 version: 2.1.0
@ -4948,7 +4948,7 @@ packages:
{ {
integrity: sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=, integrity: sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=,
registry: https://registry.npm.taobao.org/, registry: https://registry.npm.taobao.org/,
tarball: https://registry.nlark.com/semver/download/semver-5.7.1.tgz tarball: https://registry.nlark.com/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1631500167672&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz
} }
name: semver name: semver
version: 5.7.1 version: 5.7.1
@ -4960,7 +4960,7 @@ packages:
{ {
integrity: sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=, integrity: sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=,
registry: https://registry.npm.taobao.org/, registry: https://registry.npm.taobao.org/,
tarball: https://registry.nlark.com/semver/download/semver-6.3.0.tgz tarball: https://registry.nlark.com/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1631500167672&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz
} }
name: semver name: semver
version: 6.3.0 version: 6.3.0
@ -7909,7 +7909,6 @@ packages:
magic-string: registry.nlark.com/magic-string/0.25.7 magic-string: registry.nlark.com/magic-string/0.25.7
postcss: registry.npmmirror.com/postcss/8.4.5 postcss: registry.npmmirror.com/postcss/8.4.5
source-map: registry.nlark.com/source-map/0.6.1 source-map: registry.nlark.com/source-map/0.6.1
dev: false
registry.npmmirror.com/@vue/compiler-ssr/3.2.26: registry.npmmirror.com/@vue/compiler-ssr/3.2.26:
resolution: resolution:
@ -7923,7 +7922,6 @@ packages:
dependencies: dependencies:
'@vue/compiler-dom': registry.npmmirror.com/@vue/compiler-dom/3.2.26 '@vue/compiler-dom': registry.npmmirror.com/@vue/compiler-dom/3.2.26
'@vue/shared': registry.npmmirror.com/@vue/shared/3.2.26 '@vue/shared': registry.npmmirror.com/@vue/shared/3.2.26
dev: false
registry.npmmirror.com/@vue/devtools-api/6.0.0-beta.21.1: registry.npmmirror.com/@vue/devtools-api/6.0.0-beta.21.1:
resolution: resolution:
@ -7951,7 +7949,6 @@ packages:
'@vue/shared': registry.npmmirror.com/@vue/shared/3.2.26 '@vue/shared': registry.npmmirror.com/@vue/shared/3.2.26
estree-walker: registry.npmmirror.com/estree-walker/2.0.2 estree-walker: registry.npmmirror.com/estree-walker/2.0.2
magic-string: registry.nlark.com/magic-string/0.25.7 magic-string: registry.nlark.com/magic-string/0.25.7
dev: false
registry.npmmirror.com/@vue/reactivity/3.2.26: registry.npmmirror.com/@vue/reactivity/3.2.26:
resolution: resolution:
@ -9678,7 +9675,7 @@ packages:
{ {
integrity: sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=, integrity: sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=,
registry: https://registry.npm.taobao.org/, registry: https://registry.npm.taobao.org/,
tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz?cache=0&sync_timestamp=1636378650851&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-1.3.0.tgz tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz
} }
name: eslint-visitor-keys name: eslint-visitor-keys
version: 1.3.0 version: 1.3.0
@ -9690,7 +9687,7 @@ packages:
{ {
integrity: sha1-9lMoJZMFknOSyTjtROsKXJsr0wM=, integrity: sha1-9lMoJZMFknOSyTjtROsKXJsr0wM=,
registry: https://registry.npm.taobao.org/, registry: https://registry.npm.taobao.org/,
tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-2.1.0.tgz?cache=0&sync_timestamp=1636378650851&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-2.1.0.tgz tarball: https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-2.1.0.tgz
} }
name: eslint-visitor-keys name: eslint-visitor-keys
version: 2.1.0 version: 2.1.0
@ -11132,6 +11129,7 @@ packages:
} }
name: path-to-regexp name: path-to-regexp
version: 6.2.0 version: 6.2.0
dev: true
registry.npmmirror.com/picocolors/1.0.0: registry.npmmirror.com/picocolors/1.0.0:
resolution: resolution:
@ -12350,6 +12348,24 @@ packages:
- supports-color - supports-color
dev: true dev: true
registry.npmmirror.com/vite-plugin-vue-setup-extend/0.3.0_vite@2.7.10:
resolution:
{
integrity: sha512-9Nd7Bj4TftB2CoOAD2ZI4cHLW5zjKMF3LNihWbrnAPx3nuGBn33tM9SVUGBVjBB6uv1mGAPavwKCTU0xAD8qhw==,
registry: https://registry.npm.taobao.org/,
tarball: https://registry.npmmirror.com/vite-plugin-vue-setup-extend/download/vite-plugin-vue-setup-extend-0.3.0.tgz
}
id: registry.npmmirror.com/vite-plugin-vue-setup-extend/0.3.0
name: vite-plugin-vue-setup-extend
version: 0.3.0
peerDependencies:
vite: '>=2.0.0'
dependencies:
'@vue/compiler-sfc': registry.npmmirror.com/@vue/compiler-sfc/3.2.26
magic-string: registry.nlark.com/magic-string/0.25.7
vite: registry.npmmirror.com/vite/2.7.10_less@4.1.2
dev: true
registry.npmmirror.com/vite-plugin-windicss/1.6.2_vite@2.7.10: registry.npmmirror.com/vite-plugin-windicss/1.6.2_vite@2.7.10:
resolution: resolution:
{ {

View File

@ -0,0 +1,3 @@
import ContextMenu from './src/ContextMenu.vue'
export { ContextMenu }

View File

@ -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>

View File

@ -1,5 +1,5 @@
<script lang="tsx"> <script lang="tsx">
import { computed, defineComponent } from 'vue' import { computed, defineComponent, unref } from 'vue'
import { ElMenu, ElScrollbar } from 'element-plus' import { ElMenu, ElScrollbar } from 'element-plus'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
@ -33,7 +33,7 @@ export default defineComponent({
const collapse = computed(() => appStore.getCollapse) const collapse = computed(() => appStore.getCollapse)
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { meta, path } = currentRoute.value const { meta, path } = unref(currentRoute)
// if set path, the sidebar will highlight the path you set // if set path, the sidebar will highlight the path you set
if (meta.activeMenu) { if (meta.activeMenu) {
return meta.activeMenu as string return meta.activeMenu as string
@ -62,9 +62,9 @@ export default defineComponent({
> >
<ElScrollbar> <ElScrollbar>
<ElMenu <ElMenu
defaultActive={activeMenu.value} defaultActive={unref(activeMenu)}
mode={menuMode.value} mode={unref(menuMode)}
collapse={collapse.value} collapse={unref(collapse)}
backgroundColor="var(--left-menu-bg-color)" backgroundColor="var(--left-menu-bg-color)"
textColor="var(--left-menu-text-color)" textColor="var(--left-menu-text-color)"
activeTextColor="var(--left-menu-text-active-color)" activeTextColor="var(--left-menu-text-active-color)"
@ -72,7 +72,7 @@ export default defineComponent({
> >
{{ {{
default: () => { default: () => {
const { renderMenuItem } = useRenderMenuItem(routers.value, menuMode.value) const { renderMenuItem } = useRenderMenuItem(unref(routers), unref(menuMode))
return renderMenuItem() return renderMenuItem()
} }
}} }}

View File

@ -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> <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> </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>

View File

@ -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
}

View File

@ -1,6 +1,5 @@
<script lang="tsx"> <script lang="tsx">
import { computed, defineComponent, KeepAlive } from 'vue' import { computed, defineComponent } from 'vue'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { Menu } from '@/components/Menu' import { Menu } from '@/components/Menu'
import { Collapse } from '@/components/Collapse' import { Collapse } from '@/components/Collapse'
@ -10,12 +9,7 @@ import { UserInfo } from '@/components/UserInfo'
import { Screenfull } from '@/components/Screenfull' import { Screenfull } from '@/components/Screenfull'
import { Breadcrumb } from '@/components/Breadcrumb' import { Breadcrumb } from '@/components/Breadcrumb'
import { TagsView } from '@/components/TagsView' import { TagsView } from '@/components/TagsView'
import AppView from './components/AppView.vue'
const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews
})
const appStore = useAppStore() const appStore = useAppStore()
@ -71,18 +65,10 @@ export default defineComponent({
<UserInfo class="header__tigger"></UserInfo> <UserInfo class="header__tigger"></UserInfo>
</div> </div>
</div> </div>
<div class="v-app-right__tags relative"> <div class="v-app-right__tags-view relative">
<TagsView></TagsView> <TagsView></TagsView>
</div> </div>
<router-view> <AppView></AppView>
{{
default: ({ Component, route }) => (
<KeepAlive include={getCaches.value}>
<Component is={Component} key={route.fullPath}></Component>
</KeepAlive>
)
}}
</router-view>
</div> </div>
</section> </section>
) )
@ -111,7 +97,7 @@ export default defineComponent({
transition: left var(--transition-time-02); transition: left var(--transition-time-02);
&__tool, &__tool,
&__tags { &__tags-view {
&::after { &::after {
position: absolute; position: absolute;
bottom: 0; bottom: 0;

View File

@ -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>

View File

@ -11,7 +11,13 @@ export default {
reminder: 'Reminder', reminder: 'Reminder',
loginOutMessage: 'Exit the system', loginOutMessage: 'Exit the system',
ok: 'OK', ok: 'OK',
cancel: 'Cancel' cancel: 'Cancel',
reload: 'Reload current',
closeTab: 'Close current',
closeTheLeftTab: 'Close left',
closeTheRightTab: 'Close right',
closeOther: 'Close other',
closeAll: 'Close all'
}, },
size: { size: {
default: 'Default', default: 'Default',

View File

@ -11,7 +11,13 @@ export default {
reminder: '温馨提示', reminder: '温馨提示',
loginOutMessage: '是否退出本系统?', loginOutMessage: '是否退出本系统?',
ok: '确定', ok: '确定',
cancel: '取消' cancel: '取消',
reload: '重新加载',
closeTab: '关闭标签页',
closeTheLeftTab: '关闭左侧标签页',
closeTheRightTab: '关闭右侧标签页',
closeOther: '关闭其他标签页',
closeAll: '关闭全部标签页'
}, },
size: { size: {
default: '默认', default: '默认',

View File

@ -20,7 +20,8 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
} }
], ],
meta: { meta: {
hidden: true hidden: true,
noTagsView: true
} }
}, },
{ {
@ -107,7 +108,8 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
name: 'Icons', name: 'Icons',
meta: { meta: {
title: '图标', title: '图标',
icon: 'carbon:skill-level-advanced' icon: 'carbon:skill-level-advanced',
affix: true
} }
} }
] ]

View File

@ -1,8 +1,9 @@
// import router from '@/router' import router from '@/router'
import type { RouteLocationNormalizedLoaded } from 'vue-router' import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { getRawRoute } from '@/utils/routerHelper' import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { store } from '../index' import { store } from '../index'
import { findIndex } from '@/utils'
export interface TagsViewState { export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[] visitedViews: RouteLocationNormalizedLoaded[]
@ -24,18 +25,24 @@ export const useTagsViewStore = defineStore({
} }
}, },
actions: { actions: {
ADD_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void { // 新增缓存和tag
if (this.visitedViews.some((v: RouteLocationNormalizedLoaded) => v.path === view.path)) return addView(view: RouteLocationNormalizedLoaded): void {
this.addVisitedView(view)
this.addCachedView()
},
// 新增tag
addVisitedView(view: RouteLocationNormalizedLoaded) {
if (this.visitedViews.some((v) => v.path === view.path)) return
if (view.meta?.noTagsView) return if (view.meta?.noTagsView) return
this.visitedViews.push( this.visitedViews.push(
Object.assign({}, view, { Object.assign({}, view, {
title: view.meta.title || 'no-name' title: view.meta?.title || 'no-name'
}) })
) )
}, },
SET_CACHED_VIEW(): void { // 新增缓存
addCachedView() {
const cacheMap: Set<string> = new Set() const cacheMap: Set<string> = new Set()
for (const v of this.visitedViews) { for (const v of this.visitedViews) {
const item = getRawRoute(v) const item = getRawRoute(v)
const needCache = !item.meta?.noCache const needCache = !item.meta?.noCache
@ -45,9 +52,17 @@ export const useTagsViewStore = defineStore({
const name = item.name as string const name = item.name as string
cacheMap.add(name) cacheMap.add(name)
} }
if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString())
return
this.cachedViews = cacheMap this.cachedViews = cacheMap
}, },
DEL_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void { // 删除某个
delView(view: RouteLocationNormalizedLoaded) {
this.delVisitedView(view)
this.addCachedView()
},
// 删除tag
delVisitedView(view: RouteLocationNormalizedLoaded) {
for (const [i, v] of this.visitedViews.entries()) { for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) { if (v.path === view.path) {
this.visitedViews.splice(i, 1) this.visitedViews.splice(i, 1)
@ -55,117 +70,60 @@ export const useTagsViewStore = defineStore({
} }
} }
}, },
DEL_CACHED_VIEW(): void { // 删除缓存
// const route = router.currentRoute.value delCachedView() {
// for (const [key, value] of this.cachedViews) { const route = router.currentRoute.value
// const index = value.findIndex((item: string) => item === (route.name as string)) const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
// if (index === -1) { if (index > -1) {
// continue this.cachedViews.delete(this.getCachedViews[index])
// } }
// if (value.length === 1) {
// this.cachedViews.delete(key)
// continue
// }
// value.splice(index, 1)
// this.cachedViews.set(key, value)
// }
}, },
DEL_OTHERS_VISITED_VIEWS(view: RouteLocationNormalizedLoaded): void { // 删除所有缓存和tag
this.visitedViews = this.visitedViews.filter((v) => { delAllViews() {
return v.meta.affix || v.path === view.path this.delAllVisitedViews()
}) this.addCachedView()
}, },
DEL_ALL_VISITED_VIEWS(): void { // 删除所有tag
// keep affix tags delAllVisitedViews() {
const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = affixTags this.visitedViews = affixTags
}, },
UPDATE_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void { // 删除其他
for (let v of this.visitedViews) { delOthersViews(view: RouteLocationNormalizedLoaded) {
if (v.path === view.path) { this.delOthersVisitedViews(view)
v = Object.assign(v, view)
break
}
}
},
addView(view: RouteLocationNormalizedLoaded): void {
this.addVisitedView(view)
this.addCachedView() this.addCachedView()
}, },
addVisitedView(view: RouteLocationNormalizedLoaded): void { // 删除其他tag
this.ADD_VISITED_VIEW(view) delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
this.visitedViews = this.visitedViews.filter((v) => {
return v?.meta?.affix || v.path === view.path
})
}, },
addCachedView(): void { // 删除左侧
this.SET_CACHED_VIEW() delLeftViews(view: RouteLocationNormalizedLoaded) {
}, const index = findIndex<RouteLocationNormalizedLoaded>(
delView(view: RouteLocationNormalizedLoaded): Promise<unknown> { this.visitedViews,
return new Promise((resolve) => { (v) => v.path === view.path
this.delVisitedView(view) )
this.SET_CACHED_VIEW() if (index > -1) {
resolve({ this.visitedViews = this.visitedViews.filter((v, i) => {
visitedViews: [...this.visitedViews], return v?.meta?.affix || v.path === view.path || i > index
cachedViews: [...this.cachedViews]
}) })
}) this.addCachedView()
}
}, },
delVisitedView(view: RouteLocationNormalizedLoaded): Promise<unknown> { // 删除右侧
return new Promise((resolve) => { delRightViews(view: RouteLocationNormalizedLoaded) {
this.DEL_VISITED_VIEW(view) const index = findIndex<RouteLocationNormalizedLoaded>(
resolve([...this.visitedViews]) this.visitedViews,
}) (v) => v.path === view.path
}, )
delCachedView(): Promise<unknown> { if (index > -1) {
return new Promise((resolve) => { this.visitedViews = this.visitedViews.filter((v, i) => {
this.DEL_CACHED_VIEW() return v?.meta?.affix || v.path === view.path || i < index
resolve([...this.cachedViews])
})
},
delOthersViews(view: RouteLocationNormalizedLoaded): Promise<unknown> {
return new Promise((resolve) => {
this.delOthersVisitedViews(view)
this.SET_CACHED_VIEW()
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
}) })
}) this.addCachedView()
}, }
delOthersVisitedViews(view: RouteLocationNormalizedLoaded): Promise<unknown> {
return new Promise((resolve) => {
this.DEL_OTHERS_VISITED_VIEWS(view)
resolve([...this.visitedViews])
})
},
delOthersCachedViews(): Promise<unknown> {
return new Promise((resolve) => {
this.SET_CACHED_VIEW()
resolve([...this.cachedViews])
})
},
delAllViews(): Promise<unknown> {
return new Promise((resolve) => {
this.delAllVisitedViews()
this.SET_CACHED_VIEW()
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delAllVisitedViews(): Promise<unknown> {
return new Promise((resolve) => {
this.DEL_ALL_VISITED_VIEWS()
resolve([...this.visitedViews])
})
},
delAllCachedViews(): Promise<unknown> {
return new Promise((resolve) => {
this.SET_CACHED_VIEW()
resolve([...this.cachedViews])
})
},
updateVisitedView(view: RouteLocationNormalizedLoaded): void {
this.UPDATE_VISITED_VIEW(view)
} }
} }
}) })

289
src/utils/domUtils.ts Normal file
View File

@ -0,0 +1,289 @@
import { isServer } from './is'
const ieVersion = isServer ? 0 : Number((document as any).documentMode)
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
const MOZ_HACK_REGEXP = /^moz([A-Z])/
export interface ViewportOffsetResult {
left: number
top: number
right: number
bottom: number
rightIncludeBody: number
bottomIncludeBody: number
}
/* istanbul ignore next */
const trim = function (string: string) {
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
}
/* istanbul ignore next */
const camelCase = function (name: string) {
return name
.replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
return offset ? letter.toUpperCase() : letter
})
.replace(MOZ_HACK_REGEXP, 'Moz$1')
}
/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
if (!el || !cls) return false
if (cls.indexOf(' ') !== -1) {
throw new Error('className should not contain space.')
}
if (el.classList) {
return el.classList.contains(cls)
} else {
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
}
}
/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
if (!el) return
let curClass = el.className
const classes = (cls || '').split(' ')
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) continue
if (el.classList) {
el.classList.add(clsName)
} else if (!hasClass(el, clsName)) {
curClass += ' ' + clsName
}
}
if (!el.classList) {
el.className = curClass
}
}
/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
if (!el || !cls) return
const classes = cls.split(' ')
let curClass = ' ' + el.className + ' '
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) continue
if (el.classList) {
el.classList.remove(clsName)
} else if (hasClass(el, clsName)) {
curClass = curClass.replace(' ' + clsName + ' ', ' ')
}
}
if (!el.classList) {
el.className = trim(curClass)
}
}
export function getBoundingClientRect(element: Element): DOMRect | number {
if (!element || !element.getBoundingClientRect) {
return 0
}
return element.getBoundingClientRect()
}
/**
* lefttop偏移
* left
* top:元素最顶端距离文档顶端的距离
* right:元素最右侧距离文档右侧的距离
* bottom
* rightIncludeBody
* bottomIncludeBody
*
* @description:
*/
export function getViewportOffset(element: Element): ViewportOffsetResult {
const doc = document.documentElement
const docScrollLeft = doc.scrollLeft
const docScrollTop = doc.scrollTop
const docClientLeft = doc.clientLeft
const docClientTop = doc.clientTop
const pageXOffset = window.pageXOffset
const pageYOffset = window.pageYOffset
const box = getBoundingClientRect(element)
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
const offsetLeft = retLeft + pageXOffset
const offsetTop = rectTop + pageYOffset
const left = offsetLeft - scrollLeft
const top = offsetTop - scrollTop
const clientWidth = window.document.documentElement.clientWidth
const clientHeight = window.document.documentElement.clientHeight
return {
left: left,
top: top,
right: clientWidth - rectWidth - left,
bottom: clientHeight - rectHeight - top,
rightIncludeBody: clientWidth - left,
bottomIncludeBody: clientHeight - top
}
}
/* istanbul ignore next */
export const on = function (
element: HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject
): void {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const off = function (
element: HTMLElement | Document | Window,
event: string,
handler: any
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) {
if (fn) {
// @ts-ignore
fn.apply(this, args)
}
off(el, event, listener)
}
on(el, event, listener)
}
/* istanbul ignore next */
export const getStyle =
ieVersion < 9
? function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'styleFloat'
}
try {
switch (styleName) {
case 'opacity':
try {
return element.filters.item('alpha').opacity / 100
} catch (e) {
return 1.0
}
default:
return element.style[styleName] || element.currentStyle
? element.currentStyle[styleName]
: null
}
} catch (e) {
return element.style[styleName]
}
}
: function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
const computed = (document as any).defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
if (!element || !styleName) return
if (typeof styleName === 'object') {
for (const prop in styleName) {
if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
setStyle(element, prop, styleName[prop])
}
}
} else {
styleName = camelCase(styleName)
if (styleName === 'opacity' && ieVersion < 9) {
element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
} else {
element.style[styleName] = value
}
}
}
/* istanbul ignore next */
export const isScroll = (el: Element, vertical: any) => {
if (isServer) return
const determinedDirection = vertical !== null || vertical !== undefined
const overflow = determinedDirection
? vertical
? getStyle(el, 'overflow-y')
: getStyle(el, 'overflow-x')
: getStyle(el, 'overflow')
return overflow.match(/(scroll|auto)/)
}
/* istanbul ignore next */
export const getScrollContainer = (el: Element, vertical?: any) => {
if (isServer) return
let parent: any = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) {
return window
}
if (isScroll(parent, vertical)) {
return parent
}
parent = parent.parentNode
}
return parent
}
/* istanbul ignore next */
export const isInContainer = (el: Element, container: any) => {
if (isServer || !el || !container) return false
const elRect = el.getBoundingClientRect()
let containerRect
if ([window, document, document.documentElement, null, undefined].includes(container)) {
containerRect = {
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
left: 0
}
} else {
containerRect = container.getBoundingClientRect()
}
return (
elRect.top < containerRect.bottom &&
elRect.bottom > containerRect.top &&
elRect.right > containerRect.left &&
elRect.left < containerRect.right
)
}

View File

@ -38,3 +38,24 @@ export const underlineToHump = (str: string): string => {
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val) dom.style.setProperty(prop, val)
} }
/**
*
* @param {Array} ary
* @param {Functon} fn
*/
// eslint-disable-next-line
export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
if (ary.findIndex) {
return ary.findIndex(fn)
}
let index = -1
ary.some((item: T, i: number, ary: Array<T>) => {
const ret: T = fn(item, i, ary)
if (ret) {
index = i
return ret
}
})
return index
}

View File

@ -1,5 +1,10 @@
<script setup lang="ts"></script> <script setup lang="ts" name="Menu111">
import { onMounted } from 'vue'
onMounted(() => {
console.log('????')
})
</script>
<template> <template>
<div>Menu111 <input type="text" /></div> <div class="h-[100000px]">Menu111 <input type="text" /></div>
</template> </template>

View File

@ -2,7 +2,6 @@
import { LoginForm } from './components' import { LoginForm } from './components'
import { ThemeSwitch } from '@/components/ThemeSwitch' import { ThemeSwitch } from '@/components/ThemeSwitch'
import { LocaleDropdown } from '@/components/LocaleDropdown' import { LocaleDropdown } from '@/components/LocaleDropdown'
import { useDesign } from '@/hooks/web/useDesign'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { underlineToHump } from '@/utils' import { underlineToHump } from '@/utils'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
@ -10,22 +9,14 @@ import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useI18n() const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('login')
</script> </script>
<template> <template>
<div <div
:class="prefixCls" class="v-login h-[100%] relative overflow-hidden <xl:bg-v-dark <sm:px-10px <xl:px-10px <md:px-10px"
class="h-[100%] relative overflow-hidden <xl:bg-v-dark <sm:px-10px <xl:px-10px <md:px-10px"
> >
<div class="relative h-full flex mx-auto"> <div class="relative h-full flex mx-auto">
<div <div class="v-login__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px <xl:hidden">
:class="`${prefixCls}__left`"
class="flex-1 bg-gray-500 bg-opacity-20 relative p-30px <xl:hidden"
>
<div class="flex items-center relative text-white"> <div class="flex items-center relative text-white">
<img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" /> <img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>

View File

@ -129,11 +129,10 @@ const signIn = async () => {
await permissionStore.generateRoutes().catch(() => {}) await permissionStore.generateRoutes().catch(() => {})
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
console.log(route)
addRoute(route as RouteRecordRaw) // 访 addRoute(route as RouteRecordRaw) // 访
}) })
permissionStore.setIsAddRouters(true) permissionStore.setIsAddRouters(true)
push({ path: redirect.value || '/level' }) push({ path: redirect.value || permissionStore.addRouters[0].path })
} }
} }
} }

7
types/componentType/contextMenu.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare type contextMenuSchema = {
disabled?: boolean
divided?: boolean
icon?: string
label: string
command?: (item: contextMenuSchema) => viod
}

View File

@ -10,6 +10,7 @@ import StyleImport, { ElementPlusResolve } from 'vite-plugin-style-import'
import ViteSvgIcons from 'vite-plugin-svg-icons' import ViteSvgIcons from 'vite-plugin-svg-icons'
import PurgeIcons from 'vite-plugin-purge-icons' import PurgeIcons from 'vite-plugin-purge-icons'
import { viteMockServe } from 'vite-plugin-mock' import { viteMockServe } from 'vite-plugin-mock'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
const root = process.cwd() const root = process.cwd()
@ -68,7 +69,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
setupProdMockServer() setupProdMockServer()
` `
}) }),
VueSetupExtend()
], ],
css: { css: {