feat(TagsView): Add TagsView component
feat(ContextMenu): Add ContextMenu component feat(store): Add tagsView store
This commit is contained in:
parent
4612e5544b
commit
349ac9d398
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,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()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
@ -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',
|
||||||
|
|
|
@ -11,7 +11,13 @@ export default {
|
||||||
reminder: '温馨提示',
|
reminder: '温馨提示',
|
||||||
loginOutMessage: '是否退出本系统?',
|
loginOutMessage: '是否退出本系统?',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
cancel: '取消'
|
cancel: '取消',
|
||||||
|
reload: '重新加载',
|
||||||
|
closeTab: '关闭标签页',
|
||||||
|
closeTheLeftTab: '关闭左侧标签页',
|
||||||
|
closeTheRightTab: '关闭右侧标签页',
|
||||||
|
closeOther: '关闭其他标签页',
|
||||||
|
closeAll: '关闭全部标签页'
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: '默认',
|
default: '默认',
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前元素的left、top偏移
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
declare type contextMenuSchema = {
|
||||||
|
disabled?: boolean
|
||||||
|
divided?: boolean
|
||||||
|
icon?: string
|
||||||
|
label: string
|
||||||
|
command?: (item: contextMenuSchema) => viod
|
||||||
|
}
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in New Issue