84 changed files with 1567 additions and 1524 deletions
@ -0,0 +1,22 @@ |
|||
name: deploy |
|||
|
|||
on: |
|||
push: |
|||
branches: |
|||
- main |
|||
|
|||
jobs: |
|||
build-deploy: |
|||
runs-on: ubuntu-latest |
|||
|
|||
steps: |
|||
- uses: actions/checkout@v1 |
|||
- run: npm install |
|||
- run: npm run build |
|||
|
|||
- name: Deploy |
|||
uses: peaceiris/actions-gh-pages@v2.5.0 |
|||
env: |
|||
ACTIONS_DEPLOY_KEY: ${{secrets.ACTIONS_DEPLOY_KEY}} |
|||
PUBLISH_BRANCH: gh-pages |
|||
PUBLISH_DIR: dist |
|||
@ -1,48 +1,13 @@ |
|||
{ |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
// node环境调试当前激活编辑器ts/js代码 |
|||
{ |
|||
"type": "node", |
|||
"type": "chrome", |
|||
"request": "launch", |
|||
"name": "file", |
|||
"cwd": "${workspaceFolder}", |
|||
"program": "${file}", |
|||
// .vscode 目录又不认识了??? |
|||
"preLaunchTask": "tsc: 监视 - build/tsconfig.json", // cn |
|||
// "preLaunchTask": "tsc: watch - build/tsconfig.json", // en |
|||
"outFiles": ["${workspaceFolder}/compile/**/*.js"] |
|||
// "args": ["--experimental-modules", "--loader", "./loader.mjs"] |
|||
"name": "Launch Chrome", |
|||
"url": "http://localhost:3100", |
|||
"webRoot": "${workspaceFolder}/src", |
|||
"sourceMaps": true |
|||
}, |
|||
// 调试开发环境脚本 |
|||
{ |
|||
"type": "node", |
|||
"request": "launch", |
|||
"name": "dev", |
|||
// "stopOnEntry": true, |
|||
"cwd": "${workspaceFolder}", |
|||
"program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js", |
|||
"args": ["serve", "--open"] |
|||
}, |
|||
// 调试生产环境脚本 |
|||
{ |
|||
"type": "node", |
|||
"request": "launch", |
|||
"name": "build", |
|||
// "stopOnEntry": true, |
|||
"cwd": "${workspaceFolder}", |
|||
"program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js", |
|||
"args": ["build"] |
|||
}, |
|||
// 调试单元测试脚本 |
|||
{ |
|||
"type": "node", |
|||
"request": "launch", |
|||
"name": "test:unit", |
|||
// "stopOnEntry": true, |
|||
"cwd": "${workspaceFolder}", |
|||
"program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js", |
|||
"args": ["test:unit", "--detectOpenHandles"] |
|||
} |
|||
] |
|||
} |
|||
|
|||
@ -1,6 +0,0 @@ |
|||
import BreadcrumbLib from './src/Breadcrumb.vue'; |
|||
import BreadcrumbItemLib from './src/BreadcrumbItem.vue'; |
|||
import { withInstall } from '../util'; |
|||
|
|||
export const Breadcrumb = withInstall(BreadcrumbLib); |
|||
export const BreadcrumbItem = withInstall(BreadcrumbItemLib); |
|||
@ -1,96 +0,0 @@ |
|||
<template> |
|||
<div ref="breadcrumbRef" class="breadcrumb"> |
|||
<slot /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent, provide, ref } from 'vue'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'Breadcrumb', |
|||
props: { |
|||
separator: propTypes.string.def('/'), |
|||
separatorClass: propTypes.string, |
|||
}, |
|||
setup(props) { |
|||
const breadcrumbRef = ref<Nullable<HTMLElement>>(null); |
|||
|
|||
provide('breadcrumb', props); |
|||
|
|||
return { |
|||
breadcrumbRef, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@import (reference) '../../../design/index.less'; |
|||
|
|||
.breadcrumb { |
|||
.unselect(); |
|||
|
|||
height: @header-height; |
|||
padding-right: 20px; |
|||
font-size: 13px; |
|||
line-height: @header-height; |
|||
// line-height: 1; |
|||
|
|||
&::after, |
|||
&::before { |
|||
display: table; |
|||
content: ''; |
|||
} |
|||
|
|||
&::after { |
|||
clear: both; |
|||
} |
|||
|
|||
&__separator { |
|||
margin: 0 9px; |
|||
font-weight: 700; |
|||
color: @breadcrumb-item-normal-color; |
|||
|
|||
&[class*='icon'] { |
|||
margin: 0 6px; |
|||
font-weight: 400; |
|||
} |
|||
} |
|||
|
|||
&__item { |
|||
float: left; |
|||
} |
|||
|
|||
&__inner { |
|||
color: @breadcrumb-item-normal-color; |
|||
|
|||
&.is-link, |
|||
a { |
|||
font-weight: 500; |
|||
color: @text-color-base; |
|||
text-decoration: none; |
|||
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); |
|||
} |
|||
|
|||
a:hover, |
|||
&.is-link:hover { |
|||
color: @primary-color; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
&__item:last-child .breadcrumb__inner, |
|||
&__item:last-child &__inner a, |
|||
&__item:last-child &__inner a:hover, |
|||
&__item:last-child &__inner:hover { |
|||
font-weight: 400; |
|||
color: @breadcrumb-item-normal-color; |
|||
cursor: text; |
|||
} |
|||
|
|||
&__item:last-child &__separator { |
|||
display: none; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,57 +0,0 @@ |
|||
<template> |
|||
<span class="breadcrumb__item"> |
|||
<span ref="linkRef" :class="['breadcrumb__inner', to || isLink ? 'is-link' : '']"> |
|||
<slot /> |
|||
</span> |
|||
<i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass"></i> |
|||
<span v-else class="breadcrumb__separator">{{ separator }}</span> |
|||
</span> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent, inject, ref, onMounted, unref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { useEventListener } from '/@/hooks/event/useEventListener'; |
|||
|
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BreadcrumbItem', |
|||
props: { |
|||
to: propTypes.oneOfType([propTypes.string, propTypes.object]), |
|||
replace: propTypes.bool, |
|||
isLink: propTypes.bool, |
|||
}, |
|||
setup(props) { |
|||
const linkRef = ref<Nullable<HTMLElement>>(null); |
|||
|
|||
const parent = inject('breadcrumb') as { |
|||
separator: string; |
|||
separatorClass: string; |
|||
}; |
|||
|
|||
const { push, replace } = useRouter(); |
|||
|
|||
onMounted(() => { |
|||
const link = unref(linkRef); |
|||
if (!link) return; |
|||
useEventListener({ |
|||
el: link, |
|||
listener: () => { |
|||
const { to } = props; |
|||
if (!props.to) return; |
|||
props.replace ? replace(to) : push(to); |
|||
}, |
|||
name: 'click', |
|||
wait: 0, |
|||
}); |
|||
}); |
|||
|
|||
return { |
|||
linkRef, |
|||
separator: parent.separator && parent.separator, |
|||
separatorClass: parent.separatorClass && parent.separatorClass, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,18 +0,0 @@ |
|||
.breadcrumb-enter-active, |
|||
.breadcrumb-leave-active { |
|||
transition: all 0.24s; |
|||
} |
|||
|
|||
.breadcrumb-enter-from, |
|||
.breadcrumb-leave-active { |
|||
opacity: 0; |
|||
transform: translateX(16px); |
|||
} |
|||
|
|||
.breadcrumb-move { |
|||
transition: all 0.38s; |
|||
} |
|||
|
|||
.breadcrumb-leave-active { |
|||
position: absolute; |
|||
} |
|||
@ -1,72 +1,21 @@ |
|||
import { TabItem, tabStore } from '/@/store/modules/tab'; |
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { appStore } from '/@/store/modules/app'; |
|||
|
|||
type RouteFn = (tabItem: TabItem) => void; |
|||
|
|||
interface TabFn { |
|||
refreshPageFn: RouteFn; |
|||
closeAllFn: Fn; |
|||
closeLeftFn: RouteFn; |
|||
closeRightFn: RouteFn; |
|||
closeOtherFn: RouteFn; |
|||
closeCurrentFn: RouteFn; |
|||
} |
|||
|
|||
let refreshPage: RouteFn; |
|||
let closeAll: Fn; |
|||
let closeLeft: RouteFn; |
|||
let closeRight: RouteFn; |
|||
let closeOther: RouteFn; |
|||
let closeCurrent: RouteFn; |
|||
|
|||
export let isInitUseTab = false; |
|||
|
|||
export function useTabs() { |
|||
function initTabFn({ |
|||
refreshPageFn, |
|||
closeAllFn, |
|||
closeLeftFn, |
|||
closeRightFn, |
|||
closeOtherFn, |
|||
closeCurrentFn, |
|||
}: TabFn) { |
|||
if (isInitUseTab) return; |
|||
|
|||
refreshPageFn && (refreshPage = refreshPageFn); |
|||
closeAllFn && (closeAll = closeAllFn); |
|||
closeLeftFn && (closeLeft = closeLeftFn); |
|||
closeRightFn && (closeRight = closeRightFn); |
|||
closeOtherFn && (closeOther = closeOtherFn); |
|||
closeCurrentFn && (closeCurrent = closeCurrentFn); |
|||
isInitUseTab = true; |
|||
} |
|||
|
|||
function resetCache() { |
|||
const def = undefined as any; |
|||
refreshPage = def; |
|||
closeAll = def; |
|||
closeLeft = def; |
|||
closeRight = def; |
|||
closeOther = def; |
|||
closeCurrent = def; |
|||
} |
|||
|
|||
function canIUseFn(): boolean { |
|||
const { multiTabsSetting: { show } = {} } = appStore.getProjectConfig; |
|||
if (!show) { |
|||
throw new Error('当前未开启多标签页,请在设置中打开!'); |
|||
throw new Error('The multi-tab page is currently not open, please open it in the settings!'); |
|||
} |
|||
return !!show; |
|||
} |
|||
|
|||
return { |
|||
initTabFn, |
|||
refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab), |
|||
closeAll: () => canIUseFn() && closeAll(), |
|||
closeLeft: () => canIUseFn() && closeLeft(tabStore.getCurrentTab), |
|||
closeRight: () => canIUseFn() && closeRight(tabStore.getCurrentTab), |
|||
closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab), |
|||
closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab), |
|||
resetCache: () => canIUseFn() && resetCache(), |
|||
refreshPage: () => canIUseFn() && tabStore.commitRedoPage(), |
|||
closeAll: () => canIUseFn() && tabStore.closeAllTabAction(), |
|||
closeLeft: () => canIUseFn() && tabStore.closeLeftTabAction(tabStore.getCurrentTab), |
|||
closeRight: () => canIUseFn() && tabStore.closeRightTabAction(tabStore.getCurrentTab), |
|||
closeOther: () => canIUseFn() && tabStore.closeOtherTabAction(tabStore.getCurrentTab), |
|||
closeCurrent: () => canIUseFn() && tabStore.closeTabAction(tabStore.getCurrentTab), |
|||
}; |
|||
} |
|||
|
|||
@ -1,128 +0,0 @@ |
|||
import type { AppRouteRecordRaw } from '/@/router/types'; |
|||
import type { RouteLocationMatched } from 'vue-router'; |
|||
import type { PropType } from 'vue'; |
|||
|
|||
import { defineComponent, TransitionGroup, unref, watch, ref } from 'vue'; |
|||
import Icon from '/@/components/Icon'; |
|||
|
|||
import { Breadcrumb, BreadcrumbItem } from '/@/components/Breadcrumb'; |
|||
|
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { isBoolean } from '/@/utils/is'; |
|||
import { compile } from 'path-to-regexp'; |
|||
|
|||
import router from '/@/router'; |
|||
|
|||
import { PageEnum } from '/@/enums/pageEnum'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicBreadcrumb', |
|||
props: { |
|||
showIcon: { |
|||
type: Boolean as PropType<boolean>, |
|||
default: false, |
|||
}, |
|||
}, |
|||
setup(props) { |
|||
const itemList = ref<AppRouteRecordRaw[]>([]); |
|||
|
|||
const { currentRoute, push } = useRouter(); |
|||
const { t } = useI18n(); |
|||
watch( |
|||
() => currentRoute.value, |
|||
() => { |
|||
if (unref(currentRoute).name === 'Redirect') return; |
|||
getBreadcrumb(); |
|||
}, |
|||
{ immediate: true } |
|||
); |
|||
|
|||
function getBreadcrumb() { |
|||
const { matched } = unref(currentRoute); |
|||
const matchedList = matched.filter((item) => item.meta && item.meta.title).slice(1); |
|||
const firstItem = matchedList[0]; |
|||
const ret = getHomeRoute(firstItem); |
|||
if (!isBoolean(ret)) { |
|||
matchedList.unshift(ret); |
|||
} |
|||
itemList.value = ((matchedList as any) as AppRouteRecordRaw[]).filter( |
|||
(item) => item.meta && item.meta.title && !item.meta.hideBreadcrumb |
|||
); |
|||
} |
|||
|
|||
function getHomeRoute(firstItem: RouteLocationMatched) { |
|||
if (!firstItem || !firstItem.name) return false; |
|||
const routes = router.getRoutes(); |
|||
const homeRoute = routes.find((item) => item.path === PageEnum.BASE_HOME); |
|||
if (!homeRoute) return false; |
|||
if (homeRoute.name === firstItem.name) return false; |
|||
return homeRoute; |
|||
} |
|||
|
|||
function pathCompile(path: string) { |
|||
const { params } = unref(currentRoute); |
|||
const toPath = compile(path); |
|||
return toPath(params); |
|||
} |
|||
|
|||
function handleItemClick(item: AppRouteRecordRaw) { |
|||
const { redirect, path, meta } = item; |
|||
if (meta.disabledRedirect) return; |
|||
if (redirect) { |
|||
push(redirect as string); |
|||
return; |
|||
} |
|||
return push(pathCompile(path)); |
|||
} |
|||
|
|||
function renderItemContent(item: AppRouteRecordRaw) { |
|||
return ( |
|||
<> |
|||
{props.showIcon && item.meta.icon && item.meta.icon.trim() !== '' && ( |
|||
<Icon |
|||
icon={item.meta.icon} |
|||
class="icon mr-1 " |
|||
style={{ |
|||
marginBottom: '2px', |
|||
}} |
|||
/> |
|||
)} |
|||
{t(item.meta.title)} |
|||
</> |
|||
); |
|||
} |
|||
|
|||
function renderBreadcrumbItemList() { |
|||
return unref(itemList).map((item) => { |
|||
const isLink = |
|||
(!!item.redirect && !item.meta.disabledRedirect) || |
|||
!item.children || |
|||
item.children.length === 0; |
|||
|
|||
return ( |
|||
<BreadcrumbItem |
|||
key={item.path} |
|||
isLink={isLink} |
|||
onClick={handleItemClick.bind(null, item)} |
|||
> |
|||
{() => renderItemContent(item as AppRouteRecordRaw)} |
|||
</BreadcrumbItem> |
|||
); |
|||
}); |
|||
} |
|||
|
|||
function renderBreadcrumbDefault() { |
|||
return ( |
|||
<TransitionGroup name="breadcrumb">{() => renderBreadcrumbItemList()}</TransitionGroup> |
|||
); |
|||
} |
|||
|
|||
return () => ( |
|||
<Breadcrumb class={['layout-breadcrumb', unref(itemList).length === 0 ? 'hidden' : '']}> |
|||
{() => renderBreadcrumbDefault()} |
|||
</Breadcrumb> |
|||
); |
|||
}, |
|||
}); |
|||
@ -0,0 +1,79 @@ |
|||
<template> |
|||
<div class="layout-breadcrumb"> |
|||
<a-breadcrumb :routes="routes"> |
|||
<template #itemRender="{ route, routes }"> |
|||
<Icon :icon="route.meta.icon" v-if="showIcon && route.meta.icon" /> |
|||
<span v-if="routes.indexOf(route) === routes.length - 1"> |
|||
{{ t(route.meta.title) }} |
|||
</span> |
|||
<router-link v-else :to="route.path"> |
|||
{{ t(route.meta.title) }} |
|||
</router-link> |
|||
</template> |
|||
</a-breadcrumb> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { PropType } from 'vue'; |
|||
import { defineComponent, ref, toRaw, watchEffect } from 'vue'; |
|||
import { useI18n } from 'vue-i18n'; |
|||
|
|||
import type { RouteLocationMatched } from 'vue-router'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { filter } from '/@/utils/helper/treeHelper'; |
|||
import { REDIRECT_NAME } from '/@/router/constant'; |
|||
import Icon from '/@/components/Icon'; |
|||
|
|||
import { HomeOutlined } from '@ant-design/icons-vue'; |
|||
import { PageEnum } from '/@/enums/pageEnum'; |
|||
export default defineComponent({ |
|||
name: 'LayoutBreadcrumb', |
|||
components: { HomeOutlined, Icon }, |
|||
props: { |
|||
showIcon: { |
|||
type: Boolean as PropType<boolean>, |
|||
default: false, |
|||
}, |
|||
}, |
|||
setup() { |
|||
const routes = ref<RouteLocationMatched[]>([]); |
|||
const { currentRoute } = useRouter(); |
|||
|
|||
const { t } = useI18n(); |
|||
watchEffect(() => { |
|||
if (currentRoute.value.name === REDIRECT_NAME) { |
|||
return; |
|||
} |
|||
const matched = currentRoute.value.matched; |
|||
if (!matched || matched.length === 0) return; |
|||
|
|||
let breadcrumbList = filter(toRaw(matched), (item) => { |
|||
if (!item.meta) { |
|||
return false; |
|||
} |
|||
const { title, hideBreadcrumb } = item.meta; |
|||
if (!title || hideBreadcrumb) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}); |
|||
|
|||
const filterBreadcrumbList = breadcrumbList.filter( |
|||
(item) => item.path !== PageEnum.BASE_HOME |
|||
); |
|||
|
|||
if (filterBreadcrumbList.length === breadcrumbList.length) { |
|||
filterBreadcrumbList.unshift({ |
|||
path: PageEnum.BASE_HOME, |
|||
meta: { |
|||
title: t('layout.header.home'), |
|||
}, |
|||
}); |
|||
} |
|||
routes.value = filterBreadcrumbList; |
|||
}); |
|||
|
|||
return { routes, t }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,90 +0,0 @@ |
|||
import { DropMenu } from '/@/components/Dropdown/index'; |
|||
import { AppRouteRecordRaw } from '/@/router/types'; |
|||
import type { TabItem } from '/@/store/modules/tab'; |
|||
|
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
const { t } = useI18n(); |
|||
|
|||
export enum TabContentEnum { |
|||
TAB_TYPE, |
|||
EXTRA_TYPE, |
|||
} |
|||
|
|||
export interface TabContentProps { |
|||
tabItem: TabItem | AppRouteRecordRaw; |
|||
type?: TabContentEnum; |
|||
trigger?: Array<'click' | 'hover' | 'contextmenu'>; |
|||
} |
|||
|
|||
/** |
|||
* @description: 右键:下拉菜单文字 |
|||
*/ |
|||
export enum MenuEventEnum { |
|||
// 刷新
|
|||
REFRESH_PAGE, |
|||
// 关闭当前
|
|||
CLOSE_CURRENT, |
|||
// 关闭左侧
|
|||
CLOSE_LEFT, |
|||
// 关闭右侧
|
|||
CLOSE_RIGHT, |
|||
// 关闭其他
|
|||
CLOSE_OTHER, |
|||
// 关闭所有
|
|||
CLOSE_ALL, |
|||
// 放大
|
|||
SCALE, |
|||
} |
|||
|
|||
export function getActions() { |
|||
const REFRESH_PAGE: DropMenu = { |
|||
icon: 'ant-design:reload-outlined', |
|||
event: MenuEventEnum.REFRESH_PAGE, |
|||
text: t('layout.multipleTab.redo'), |
|||
disabled: false, |
|||
}; |
|||
const CLOSE_CURRENT: DropMenu = { |
|||
icon: 'ant-design:close-outlined', |
|||
event: MenuEventEnum.CLOSE_CURRENT, |
|||
text: t('layout.multipleTab.close'), |
|||
disabled: false, |
|||
divider: true, |
|||
}; |
|||
const CLOSE_LEFT: DropMenu = { |
|||
icon: 'ant-design:pic-left-outlined', |
|||
event: MenuEventEnum.CLOSE_LEFT, |
|||
text: t('layout.multipleTab.closeLeft'), |
|||
disabled: false, |
|||
divider: false, |
|||
}; |
|||
const CLOSE_RIGHT: DropMenu = { |
|||
icon: 'ant-design:pic-right-outlined', |
|||
event: MenuEventEnum.CLOSE_RIGHT, |
|||
text: t('layout.multipleTab.closeRight'), |
|||
disabled: false, |
|||
divider: true, |
|||
}; |
|||
const CLOSE_OTHER: DropMenu = { |
|||
icon: 'ant-design:pic-center-outlined', |
|||
event: MenuEventEnum.CLOSE_OTHER, |
|||
text: t('layout.multipleTab.closeOther'), |
|||
disabled: false, |
|||
}; |
|||
const CLOSE_ALL: DropMenu = { |
|||
icon: 'ant-design:line-outlined', |
|||
event: MenuEventEnum.CLOSE_ALL, |
|||
text: t('layout.multipleTab.closeAll'), |
|||
disabled: false, |
|||
}; |
|||
return [REFRESH_PAGE, CLOSE_CURRENT, CLOSE_LEFT, CLOSE_RIGHT, CLOSE_OTHER, CLOSE_ALL]; |
|||
} |
|||
|
|||
export function getScaleAction(text: string, isZoom = false) { |
|||
return { |
|||
icon: isZoom ? 'codicon:screen-normal' : 'codicon:screen-full', |
|||
event: MenuEventEnum.SCALE, |
|||
text: text, |
|||
disabled: false, |
|||
}; |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import type { DropMenu } from '/@/components/Dropdown/index'; |
|||
import type { RouteLocationNormalized } from 'vue-router'; |
|||
|
|||
export enum TabContentEnum { |
|||
TAB_TYPE, |
|||
EXTRA_TYPE, |
|||
} |
|||
|
|||
export type { DropMenu }; |
|||
|
|||
export interface TabContentProps { |
|||
tabItem: RouteLocationNormalized; |
|||
type?: TabContentEnum; |
|||
trigger?: ('click' | 'hover' | 'contextmenu')[]; |
|||
} |
|||
|
|||
/** |
|||
* @description: 右键:下拉菜单文字 |
|||
*/ |
|||
export enum MenuEventEnum { |
|||
// 刷新
|
|||
REFRESH_PAGE, |
|||
// 关闭当前
|
|||
CLOSE_CURRENT, |
|||
// 关闭左侧
|
|||
CLOSE_LEFT, |
|||
// 关闭右侧
|
|||
CLOSE_RIGHT, |
|||
// 关闭其他
|
|||
CLOSE_OTHER, |
|||
// 关闭所有
|
|||
CLOSE_ALL, |
|||
// 放大
|
|||
SCALE, |
|||
} |
|||
@ -1,79 +0,0 @@ |
|||
import type { FunctionalComponent } from 'vue'; |
|||
|
|||
import { computed, defineComponent, unref, Transition, KeepAlive } from 'vue'; |
|||
import { RouterView, RouteLocation } from 'vue-router'; |
|||
|
|||
import FrameLayout from '/@/layouts/iframe/index.vue'; |
|||
|
|||
import { useTransition } from './useTransition'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; |
|||
|
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; |
|||
|
|||
interface DefaultContext { |
|||
Component: FunctionalComponent; |
|||
route: RouteLocation; |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'PageLayout', |
|||
setup() { |
|||
const { getShowMenu } = useMenuSetting(); |
|||
|
|||
const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting(); |
|||
|
|||
const { getBasicTransition, getEnableTransition } = useTransitionSetting(); |
|||
|
|||
const { getMax } = useMultipleTabSetting(); |
|||
|
|||
const transitionEvent = useTransition(); |
|||
|
|||
const openCacheRef = computed(() => unref(getOpenKeepAlive) && unref(getShowMenu)); |
|||
|
|||
const getCacheTabsRef = computed(() => tabStore.getKeepAliveTabsState as string[]); |
|||
|
|||
return () => { |
|||
return ( |
|||
<div> |
|||
<RouterView> |
|||
{{ |
|||
default: ({ Component, route }: DefaultContext) => { |
|||
// No longer show animations that are already in the tab
|
|||
const cacheTabs = unref(getCacheTabsRef); |
|||
const isInCache = cacheTabs.includes(route.name as string); |
|||
const name = isInCache && route.meta.inTab ? 'fade-slide' : null; |
|||
|
|||
const renderComp = () => <Component key={route.fullPath} />; |
|||
|
|||
const PageContent = unref(openCacheRef) ? ( |
|||
<KeepAlive max={unref(getMax)} include={cacheTabs}> |
|||
{renderComp()} |
|||
</KeepAlive> |
|||
) : ( |
|||
renderComp() |
|||
); |
|||
|
|||
return unref(getEnableTransition) ? ( |
|||
<Transition |
|||
{...transitionEvent} |
|||
name={name || route.meta.transitionName || unref(getBasicTransition)} |
|||
mode="out-in" |
|||
appear={true} |
|||
> |
|||
{() => PageContent} |
|||
</Transition> |
|||
) : ( |
|||
PageContent |
|||
); |
|||
}, |
|||
}} |
|||
</RouterView> |
|||
{unref(getCanEmbedIFramePage) && <FrameLayout />} |
|||
</div> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<ParentLayout :isPage="true" /> |
|||
<FrameLayout v-if="getCanEmbedIFramePage" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
|
|||
import FrameLayout from '/@/layouts/iframe/index.vue'; |
|||
|
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
|
|||
import ParentLayout from '/@/layouts/parent/index.vue'; |
|||
export default defineComponent({ |
|||
components: { ParentLayout, FrameLayout }, |
|||
setup() { |
|||
const { getCanEmbedIFramePage } = useRootSetting(); |
|||
|
|||
return { getCanEmbedIFramePage }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,73 @@ |
|||
<!-- |
|||
* @Description: The reason is that tsx will report warnings under multi-level nesting. |
|||
--> |
|||
<template> |
|||
<div> |
|||
<router-view> |
|||
<template #default="{ Component, route }"> |
|||
<transition v-bind="transitionEvent" :name="getName(route)" mode="out-in" appear> |
|||
<keep-alive v-if="openCache" :include="getCaches"> |
|||
<component :max="getMax" :is="Component" :key="route.fullPath" /> |
|||
</keep-alive> |
|||
<component v-else :max="getMax" :is="Component" :key="route.fullPath" /> |
|||
</transition> |
|||
</template> |
|||
</router-view> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { computed, defineComponent, unref } from 'vue'; |
|||
import { RouteLocationNormalized } from 'vue-router'; |
|||
|
|||
import { useTransition } from './useTransition'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; |
|||
|
|||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; |
|||
import { useCache } from './useCache'; |
|||
|
|||
export default defineComponent({ |
|||
props: { |
|||
isPage: { |
|||
type: Boolean, |
|||
}, |
|||
}, |
|||
setup(props) { |
|||
const { getCaches } = useCache(props.isPage); |
|||
|
|||
const { getShowMenu } = useMenuSetting(); |
|||
|
|||
const { getOpenKeepAlive } = useRootSetting(); |
|||
|
|||
const { getBasicTransition, getEnableTransition } = useTransitionSetting(); |
|||
|
|||
const { getMax } = useMultipleTabSetting(); |
|||
|
|||
const transitionEvent = useTransition(); |
|||
|
|||
const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMenu)); |
|||
|
|||
function getName(route: RouteLocationNormalized) { |
|||
if (!unref(getEnableTransition)) { |
|||
return null; |
|||
} |
|||
const cacheTabs = unref(getCaches); |
|||
const isInCache = cacheTabs.includes(route.name as string); |
|||
const name = isInCache && route.meta.inTab ? 'fade-slide' : null; |
|||
|
|||
return name || route.meta.transitionName || unref(getBasicTransition); |
|||
} |
|||
|
|||
return { |
|||
getCaches, |
|||
getMax, |
|||
transitionEvent, |
|||
getBasicTransition, |
|||
getName, |
|||
openCache, |
|||
getEnableTransition, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,52 @@ |
|||
import { computed, ref, unref } from 'vue'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { tryTsxEmit } from '/@/utils/helper/vueHelper'; |
|||
import { tabStore, PAGE_LAYOUT_KEY } from '/@/store/modules/tab'; |
|||
|
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
const ParentLayoutName = 'ParentLayout'; |
|||
export function useCache(isPage: boolean) { |
|||
const name = ref(''); |
|||
const { currentRoute } = useRouter(); |
|||
|
|||
tryTsxEmit((instance: any) => { |
|||
const routeName = instance.ctx.$options.name; |
|||
|
|||
if (routeName && ![ParentLayoutName].includes(routeName)) { |
|||
name.value = routeName; |
|||
} else { |
|||
const matched = currentRoute.value.matched; |
|||
const len = matched.length; |
|||
if (len < 2) return; |
|||
name.value = matched[len - 2].name as string; |
|||
} |
|||
}); |
|||
const { getOpenKeepAlive } = useRootSetting(); |
|||
|
|||
const getCaches = computed((): string[] => { |
|||
if (!unref(getOpenKeepAlive)) { |
|||
return []; |
|||
} |
|||
const cached = tabStore.getCachedMapState; |
|||
|
|||
if (isPage) { |
|||
// page Layout
|
|||
// not parent layout
|
|||
return cached.get(PAGE_LAYOUT_KEY) || []; |
|||
} |
|||
|
|||
const cacheSet = new Set<string>(); |
|||
cacheSet.add(unref(name)); |
|||
|
|||
const list = cached.get(unref(name)); |
|||
if (!list) { |
|||
return Array.from(cacheSet); |
|||
} |
|||
list.forEach((item) => { |
|||
cacheSet.add(item); |
|||
}); |
|||
return Array.from(cacheSet); |
|||
}); |
|||
return { getCaches }; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export default { |
|||
level: 'Multi menu cache', |
|||
}; |
|||
@ -0,0 +1,3 @@ |
|||
export default { |
|||
level: '多级菜单缓存', |
|||
}; |
|||
@ -0,0 +1,5 @@ |
|||
// The content here is just for type approval. The actual file content is overwritten by transform
|
|||
// For specific coverage, see build/vite/plugin/transform/dynamic-import/index.ts
|
|||
export default function (name: string) { |
|||
return name as any; |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types'; |
|||
import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'; |
|||
|
|||
import { appStore } from '/@/store/modules/app'; |
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { getParentLayout, LAYOUT } from '/@/router/constant'; |
|||
import dynamicImport from './dynamicImport'; |
|||
import { cloneDeep } from 'lodash-es'; |
|||
|
|||
// 动态引入
|
|||
function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) { |
|||
if (!routes) return; |
|||
routes.forEach((item) => { |
|||
const { component, name } = item; |
|||
const { children } = item; |
|||
if (component) { |
|||
item.component = dynamicImport(component); |
|||
} else if (name) { |
|||
item.component = getParentLayout(name); |
|||
} |
|||
children && asyncImportRoute(children); |
|||
}); |
|||
} |
|||
|
|||
function getLayoutComp(comp: string) { |
|||
return comp === 'LAYOUT' ? LAYOUT : ''; |
|||
} |
|||
|
|||
// Turn background objects into routing objects
|
|||
export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModule[]): T[] { |
|||
routeList.forEach((route) => { |
|||
if (route.component) { |
|||
if ((route.component as string).toUpperCase() === 'LAYOUT') { |
|||
route.component = getLayoutComp(route.component); |
|||
} else { |
|||
route.children = [cloneDeep(route)]; |
|||
route.component = LAYOUT; |
|||
route.name = `${route.name}Parent`; |
|||
route.path = ''; |
|||
const meta = route.meta || {}; |
|||
meta.single = true; |
|||
meta.affix = false; |
|||
route.meta = meta; |
|||
} |
|||
} |
|||
route.children && asyncImportRoute(route.children); |
|||
}); |
|||
return (routeList as unknown) as T[]; |
|||
} |
|||
|
|||
/** |
|||
* Determine whether the tab has been opened |
|||
* @param toPath |
|||
*/ |
|||
export function getIsOpenTab(toPath: string) { |
|||
const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig; |
|||
|
|||
if (show && openKeepAlive) { |
|||
const tabList = tabStore.getTabsState; |
|||
return tabList.some((tab) => tab.path === toPath); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
export function getParams(data: any = {}) { |
|||
const { params = {} } = data; |
|||
let ret = ''; |
|||
Object.keys(params).forEach((key) => { |
|||
const p = params[key]; |
|||
ret += `/${p}`; |
|||
}); |
|||
return ret; |
|||
} |
|||
|
|||
// Return to the new routing structure, not affected by the original example
|
|||
export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized { |
|||
if (!route) return route; |
|||
const { matched, ...opt } = route; |
|||
return { |
|||
...opt, |
|||
matched: (matched |
|||
? matched.map((item) => ({ |
|||
meta: item.meta, |
|||
name: item.name, |
|||
path: item.path, |
|||
})) |
|||
: undefined) as RouteRecordNormalized[], |
|||
}; |
|||
} |
|||
@ -1,33 +1,20 @@ |
|||
import type { MenuModule } from '/@/router/types.d'; |
|||
|
|||
const menu: MenuModule[] = [ |
|||
{ |
|||
orderNo: 0, |
|||
menu: { |
|||
path: '/dashboard/welcome', |
|||
name: 'routes.dashboard.welcome', |
|||
}, |
|||
const menu: MenuModule = { |
|||
orderNo: 10, |
|||
menu: { |
|||
name: 'routes.dashboard.dashboard', |
|||
path: '/dashboard', |
|||
children: [ |
|||
{ |
|||
path: '/workbench', |
|||
name: 'routes.dashboard.workbench', |
|||
}, |
|||
{ |
|||
path: '/analysis', |
|||
name: 'routes.dashboard.analysis', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
orderNo: 10, |
|||
menu: { |
|||
name: 'routes.dashboard.dashboard', |
|||
path: '/dashboard', |
|||
children: [ |
|||
{ |
|||
path: '/workbench', |
|||
name: 'routes.dashboard.workbench', |
|||
}, |
|||
{ |
|||
path: '/analysis', |
|||
name: 'routes.dashboard.analysis', |
|||
}, |
|||
// {
|
|||
// path: '/welcome',
|
|||
// name: 'routes.dashboard.welcome',
|
|||
// },
|
|||
], |
|||
}, |
|||
}, |
|||
]; |
|||
}; |
|||
export default menu; |
|||
|
|||
@ -0,0 +1,39 @@ |
|||
import type { MenuModule } from '/@/router/types.d'; |
|||
|
|||
const menu: MenuModule = { |
|||
orderNo: 2000, |
|||
menu: { |
|||
name: 'routes.demo.level.level', |
|||
path: '/level', |
|||
tag: { |
|||
dot: true, |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: 'menu1', |
|||
name: 'Menu1', |
|||
children: [ |
|||
{ |
|||
path: 'menu1-1', |
|||
name: 'Menu1-1', |
|||
children: [ |
|||
{ |
|||
path: 'menu1-1-1', |
|||
name: 'Menu1-1-1', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'menu1-2', |
|||
name: 'Menu1-2', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'menu2', |
|||
name: 'Menu2', |
|||
}, |
|||
], |
|||
}, |
|||
}; |
|||
export default menu; |
|||
@ -0,0 +1,10 @@ |
|||
import type { MenuModule } from '/@/router/types.d'; |
|||
|
|||
const menu: MenuModule = { |
|||
orderNo: 0, |
|||
menu: { |
|||
path: '/home/welcome', |
|||
name: 'routes.dashboard.welcome', |
|||
}, |
|||
}; |
|||
export default menu; |
|||
@ -0,0 +1,63 @@ |
|||
import type { AppRouteModule } from '/@/router/types'; |
|||
|
|||
import { getParentLayout, LAYOUT } from '/@/router/constant'; |
|||
|
|||
const permission: AppRouteModule = { |
|||
path: '/level', |
|||
name: 'Level', |
|||
component: LAYOUT, |
|||
redirect: '/level/menu1/menu1-1', |
|||
meta: { |
|||
icon: 'carbon:user-role', |
|||
title: 'routes.demo.level.level', |
|||
}, |
|||
|
|||
children: [ |
|||
{ |
|||
path: 'menu1', |
|||
name: 'Menu1Demo', |
|||
component: getParentLayout('Menu1Demo'), |
|||
meta: { |
|||
title: 'Menu1', |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: 'menu1-1', |
|||
name: 'Menu11Demo', |
|||
component: getParentLayout('Menu11Demo'), |
|||
meta: { |
|||
title: 'Menu1-1', |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: 'menu1-1-1', |
|||
name: 'Menu111Demo', |
|||
component: () => import('/@/views/demo/level/Menu111.vue'), |
|||
meta: { |
|||
title: 'Menu111', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'menu1-2', |
|||
name: 'Menu12Demo', |
|||
component: () => import('/@/views/demo/level/Menu12.vue'), |
|||
meta: { |
|||
title: 'Menu1-2', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'menu2', |
|||
name: 'Menu2Demo', |
|||
component: () => import('/@/views/demo/level/Menu2.vue'), |
|||
meta: { |
|||
title: 'Menu2', |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
export default permission; |
|||
@ -0,0 +1,28 @@ |
|||
import type { AppRouteModule } from '/@/router/types'; |
|||
|
|||
import { LAYOUT } from '/@/router/constant'; |
|||
|
|||
const dashboard: AppRouteModule = { |
|||
path: '/home', |
|||
name: 'Home', |
|||
component: LAYOUT, |
|||
redirect: '/home/welcome', |
|||
meta: { |
|||
icon: 'ant-design:home-outlined', |
|||
title: 'routes.dashboard.welcome', |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: 'welcome', |
|||
name: 'Welcome', |
|||
component: () => import('/@/views/dashboard/welcome/index.vue'), |
|||
meta: { |
|||
title: 'routes.dashboard.welcome', |
|||
affix: true, |
|||
icon: 'ant-design:home-outlined', |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
export default dashboard; |
|||
@ -1,5 +0,0 @@ |
|||
// The content here is just for type approval. The actual file content is overwritten by transform
|
|||
export default function (id: string) { |
|||
const dynamicImportModule: any = id; |
|||
return dynamicImportModule; |
|||
} |
|||
@ -1,110 +0,0 @@ |
|||
import type { AppRouteModule, AppRouteRecordRaw, RouteModule } from '/@/router/types'; |
|||
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; |
|||
import { createRouter, createWebHashHistory } from 'vue-router'; |
|||
|
|||
import { appStore } from '/@/store/modules/app'; |
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { toRaw } from 'vue'; |
|||
import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant'; |
|||
// import { isDevMode } from '/@/utils/env';
|
|||
import dynamicImport from './dynamicImport'; |
|||
import { omit } from 'lodash-es'; |
|||
|
|||
let currentTo: RouteLocationNormalized | null = null; |
|||
|
|||
export function getCurrentTo() { |
|||
return currentTo; |
|||
} |
|||
|
|||
export function setCurrentTo(to: RouteLocationNormalized) { |
|||
currentTo = to; |
|||
} |
|||
// 转化路由模块
|
|||
// 将多级转成2层。keepAlive问题
|
|||
export function genRouteModule(moduleList: AppRouteModule[] | AppRouteRecordRaw[]) { |
|||
const ret: AppRouteRecordRaw[] = []; |
|||
for (const routeMod of moduleList) { |
|||
let routes: RouteRecordRaw[] = []; |
|||
let layout: AppRouteRecordRaw | undefined; |
|||
if (Reflect.has(routeMod, 'routes')) { |
|||
routes = (routeMod as RouteModule).routes as any; |
|||
layout = (routeMod as RouteModule).layout; |
|||
} else if (Reflect.has(routeMod, 'path')) { |
|||
layout = omit(routeMod, 'children') as any; |
|||
routes = (routeMod.children as RouteRecordRaw[]) || ([] as RouteRecordRaw[]); |
|||
} |
|||
|
|||
const router = createRouter({ routes, history: createWebHashHistory() }); |
|||
|
|||
const flatList = (toRaw(router.getRoutes()).filter( |
|||
(item) => item.children.length === 0 |
|||
) as unknown) as AppRouteRecordRaw[]; |
|||
flatList.forEach((item) => { |
|||
item.path = `${layout ? layout.path : ''}${item.path}`; |
|||
}); |
|||
if (layout) { |
|||
layout.children = flatList; |
|||
ret.push(layout); |
|||
} else { |
|||
ret.push(...flatList); |
|||
} |
|||
} |
|||
return ret as RouteRecordRaw[]; |
|||
} |
|||
|
|||
// 动态引入
|
|||
function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) { |
|||
if (!routes) return; |
|||
routes.forEach((item) => { |
|||
const { component } = item; |
|||
const { children } = item; |
|||
if (component) { |
|||
item.component = dynamicImport(component); |
|||
} |
|||
|
|||
children && asyncImportRoute(children); |
|||
}); |
|||
} |
|||
|
|||
function getLayoutComp(comp: string) { |
|||
return comp === 'PAGE_LAYOUT' ? PAGE_LAYOUT_COMPONENT : ''; |
|||
} |
|||
|
|||
// 将后台对象转成路由对象
|
|||
export function transformObjToRoute<T = any>(routeList: AppRouteModule[]): T[] { |
|||
routeList.forEach((route) => { |
|||
asyncImportRoute( |
|||
Reflect.has(route, 'routes') ? (route as RouteModule).routes : route.children || [] |
|||
); |
|||
if ((route as RouteModule).layout) { |
|||
(route as RouteModule).layout.component = getLayoutComp( |
|||
(route as RouteModule).layout.component |
|||
); |
|||
} else { |
|||
route.component = getLayoutComp(route.component); |
|||
(route as RouteModule).layout = omit(route, 'children') as any; |
|||
} |
|||
}); |
|||
return (routeList as unknown) as T[]; |
|||
} |
|||
|
|||
//
|
|||
export function getIsOpenTab(toPath: string) { |
|||
const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig; |
|||
|
|||
if (show && openKeepAlive) { |
|||
const tabList = tabStore.getTabsState; |
|||
return tabList.some((tab) => tab.path === toPath); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
export function getParams(data: any = {}) { |
|||
const { params = {} } = data; |
|||
let ret = ''; |
|||
Object.keys(params).forEach((key) => { |
|||
const p = params[key]; |
|||
ret += `/${p}`; |
|||
}); |
|||
return ret; |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<div class="p-5"> |
|||
多层级缓存-页面1-1-1 |
|||
<br /> |
|||
<input /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
export default defineComponent({ name: 'Menu111Demo' }); |
|||
</script> |
|||
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<div class="p-5"> |
|||
多层级缓存-页面1-2 |
|||
<br /> |
|||
<input /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
export default defineComponent({ name: 'Menu12Demo' }); |
|||
</script> |
|||
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<div class="p-5"> |
|||
多层级缓存-页面2 |
|||
<br /> |
|||
<input /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
export default defineComponent({ |
|||
name: 'Menu2Demo', |
|||
}); |
|||
</script> |
|||
Loading…
Reference in new issue