This commit is contained in:
ray_wuhao 2023-06-30 17:52:06 +08:00
parent d736fc9b03
commit 9041f61f2b
19 changed files with 355 additions and 134 deletions

View File

@ -5,7 +5,7 @@
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"]
}

View File

@ -1,5 +1,12 @@
# CHANGE LOG
## 4.0.2
### Feats
- 新增平级路由配置router meta配置项sameLevel 允许你将子路由标记为平级模式,跳转时不会出发菜单、标签页更新,仅会更新面包屑
- 修改路由菜单显示、隐藏逻辑,现在仅会针对权限的验证匹配选择是否加入菜单列表中
## 4.0.1
### Feats

View File

@ -22,7 +22,7 @@ const LayoutMenu = defineComponent({
const modelMenuKey = computed({
get: () => {
nextTick().then(() => {
menuRef.value?.showOption?.(menuStore.menuKey as string)
showMenuOption()
})
return menuStore.menuKey
@ -44,6 +44,14 @@ const LayoutMenu = defineComponent({
}
}
const showMenuOption = () => {
const key = modelMenuKey.value as string
nextTick().then(() => {
menuRef.value?.showOption?.(key)
})
}
return {
modelMenuKey,
changeMenuModelValue,

View File

@ -22,7 +22,12 @@ import { NDropdown, NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
import { useMenu } from '@/store'
import type { DropdownOption } from 'naive-ui'
import type { DropdownOption, MenuOption } from 'naive-ui'
import type {
AppMenuOption,
MenuTagOptions,
AppMenuKey,
} from '@/types/modules/app'
const Breadcrumb = defineComponent({
name: 'RBreadcrumb',
@ -30,7 +35,8 @@ const Breadcrumb = defineComponent({
const menuStore = useMenu()
const { changeMenuModelValue } = menuStore
const modelBreadcrumbOptions = computed(() => menuStore.breadcrumbOptions)
const { breadcrumbOptions } = storeToRefs(menuStore)
const modelBreadcrumbOptions = computed(() => breadcrumbOptions.value)
const handleDropdownSelect = (
key: string | number,
@ -39,16 +45,26 @@ const Breadcrumb = defineComponent({
changeMenuModelValue(key, option)
}
const handleBreadcrumbItemClick = (option: AppMenuOption) => {
if (!option.children?.length) {
changeMenuModelValue(option.key, option as unknown as MenuOption)
}
}
return {
modelBreadcrumbOptions,
handleDropdownSelect,
handleBreadcrumbItemClick,
}
},
render() {
return (
<NBreadcrumb>
{this.modelBreadcrumbOptions.map((curr) => (
<NBreadcrumbItem key={curr.key}>
<NBreadcrumbItem
key={curr.key}
onClick={this.handleBreadcrumbItemClick.bind(this, curr)}
>
<NDropdown
labelField="breadcrumbLabel"
options={

View File

@ -72,6 +72,7 @@ const GlobalSeach = defineComponent({
if ((_e.ctrlKey || _e.metaKey) && _e.key === 'k') {
modelShow.value = true
console.log(modelMenuOptions.value)
}
}

View File

@ -18,7 +18,7 @@ import MenuTag from './components/MenuTag/index'
import ContentWrapper from '@/layout/default/ContentWrapper'
import FooterWrapper from '@/layout/default/FooterWrapper'
import { useSetting, useMenu } from '@/store'
import { useSetting } from '@/store'
import { LAYOUT_CONTENT_REF } from '@/appConfig/routerConfig'
import { layoutHeaderCssVars } from '@/layout/layoutResize'
import useAppLockScreen from '@/components/AppComponents/AppLockScreen/appLockVar'
@ -38,11 +38,6 @@ const Layout = defineComponent({
layoutSiderBarRef,
layoutMenuTagRef,
])
const { setupAppMenu } = useMenu()
nextTick().then(() => {
setupAppMenu()
})
return {
windowHeight,

View File

@ -15,5 +15,6 @@
"Office_Presentation": "Presentation",
"Office_Spreadsheet": "Spreadsheet",
"CalculatePrecision": "Precision",
"Directive": "Directive"
"Directive": "Directive",
"RouterDemo": "Same Level Router Demo"
}

View File

@ -15,5 +15,6 @@
"Office_Presentation": "演示",
"Office_Spreadsheet": "表格",
"CalculatePrecision": "数字精度",
"Directive": "指令"
"Directive": "指令",
"RouterDemo": "平层路由详情"
}

View File

@ -99,6 +99,7 @@ interface RouteMeta {
noLocalTitle?: string | number
ignoreAutoResetScroll?: boolean
keepAlive?: boolean
sameLevel?: boolean
}
```
@ -114,4 +115,5 @@ hidden: 是否显示
noLocalTitle: 不使用国际化渲染 Menu Titile
ignoreAutoResetScroll: 该页面内容区域自动初始化滚动条位置
keepAlive: 是否缓存该页面(需要配置 APP_KEEP_ALIVE setupKeepAlive 属性为 true 启用才有效)
sameLevel: 是否标记该路由为平级模式
```

View File

@ -32,6 +32,7 @@ import type {
RouteLocationNormalized,
} from 'vue-router'
import type { AppMenuOption } from '@/types/modules/app'
import type { AppRouteMeta } from '@/router/type'
export const permissionRouter = (router: Router) => {
const { beforeEach } = router
@ -39,9 +40,10 @@ export const permissionRouter = (router: Router) => {
beforeEach((to, from, next) => {
const token = getCache<string>(APP_CATCH_KEY.token)
const route = getCache<string>('menuKey') || ROOT_ROUTE.path
const { meta } = to
if (token !== null) {
if (validMenuItemShow(to as unknown as AppMenuOption)) {
if (validRole(meta as AppRouteMeta)) {
if (to.path === '/' || from.path === '/login') {
if (route !== 'no') {
next(route)

View File

@ -66,31 +66,10 @@ export const validRole = (meta: AppRouteMeta) => {
* , 使 validRole
*/
export const validMenuItemShow = (option: AppMenuOption) => {
const { meta, name } = option
const hidden =
meta?.hidden === undefined || meta?.hidden === false ? false : meta?.hidden
const { meta = {} } = option
const { hidden } = meta
// 如果是超级管理员(预设为 admin), 则根据其菜单栏(hidden)字段判断是否显示
if (validRole(meta)) {
return true && !hidden
} else {
// 如果为基础路由, 不进行鉴权则根据其菜单栏(hidden)字段判断是否显示
if (WHITE_ROUTES.includes(name)) {
return true && !hidden
}
// 如果 role 为 undefind 或者空数组, 则认为该路由不做权限过滤
if (!meta?.role || !meta.role?.length) {
return true && !hidden
}
// 判断权限是否匹配和菜单栏(hidden)字段判断是否显示
if (meta?.role && meta.role.length) {
return validRole(meta) && !hidden
}
return true && !hidden
}
return hidden === undefined || hidden === false ? true : false
}
/**

View File

@ -11,7 +11,6 @@ const axios: AppRouteRecordRaw = {
icon: 'axios',
order: 3,
keepAlive: true,
hidden: false,
},
}

View File

@ -0,0 +1,37 @@
import { t } from '@/locales/useI18n'
import { LAYOUT } from '@/router/constant/index'
import type { AppRouteRecordRaw } from '@/router/type'
const routerDemo: AppRouteRecordRaw = {
path: '/router-demo',
name: 'RouterDemoRoot',
component: LAYOUT,
meta: {
i18nKey: t('menu.RouterDemo'),
icon: 'other',
order: 3,
},
children: [
{
path: 'router-demo-home',
name: 'RouterDemoHome',
component: () => import('@/views/router-demo/router-demo-home/index'),
meta: {
noLocalTitle: '人员信息',
},
},
{
path: 'router-demo-detail',
name: 'RouterDemoDetail',
component: () => import('@/views/router-demo/router-demo-detail/index'),
meta: {
noLocalTitle: '信息详情',
hidden: true,
sameLevel: true,
},
},
],
}
export default routerDemo

View File

@ -18,10 +18,10 @@ export default () => [
component: Layout,
children: expandRoutes(getAppRawRoutes()),
},
{
path: '/:catchAll(.*)',
name: 'errorPage',
component: Layout,
redirect: '/error',
},
// {
// path: '/:catchAll(.*)',
// name: 'errorPage',
// component: Layout,
// redirect: '/error',
// },
]

View File

@ -19,6 +19,7 @@ export interface AppRouteMeta {
ignoreAutoResetScroll?: boolean
order?: number
keepAlive?: boolean
sameLevel?: boolean
}
// @ts-ignore

View File

@ -25,22 +25,26 @@
import { NEllipsis } from 'naive-ui'
import { getCache, setCache } from '@/utils/cache'
import { validMenuItemShow } from '@/router/helper/routerCopilot'
import { validMenuItemShow, validRole } from '@/router/helper/routerCopilot'
import {
parseAndFindMatchingNodes,
matchMenuOption,
updateDocumentTitle,
hasMenuIcon,
getCatchMenuKey,
} from './helper'
import { useI18n } from '@/locales/useI18n'
import { getAppRawRoutes } from '@/router/routeModules'
import { expandRoutes } from '@/router/helper/expandRoutes'
import { useKeepAlive } from '@/store'
import { useVueRouter } from '@/router/helper/useVueRouter'
import type { MenuOption } from 'naive-ui'
import type { AppRouteMeta, AppRouteRecordRaw } from '@/router/type'
import type { AppMenuOption, MenuTagOptions } from '@/types/modules/app'
import type {
AppMenuOption,
MenuTagOptions,
AppMenuKey,
} from '@/types/modules/app'
import type { MenuState } from '@/store/modules/menu/type'
export const useMenu = defineStore(
@ -58,6 +62,7 @@ export const useMenu = defineStore(
menuTagOptions: [], // tag 标签菜单
breadcrumbOptions: [], // 面包屑菜单
})
const isSetupAppMenuLock = ref(true)
/**
*
@ -77,80 +82,28 @@ export const useMenu = defineStore(
/**
*
* @param key key
* @param option option
*
*
* @remark `menu key`
* @remark , key (router push )
* ,
*/
const changeMenuModelValue = (key: string | number, option: MenuOption) => {
const { meta, path } = option as unknown as AppRouteRecordRaw
const setBreadcrumbOptions = (key: string | number, option: MenuOption) => {
const { meta } = option as unknown as AppRouteRecordRaw
if (meta.windowOpen) {
window.open(meta.windowOpen)
} else {
// 防止重复点击做重复操作处理
if (menuState.menuKey !== key) {
matchMenuOption(
option as unknown as MenuTagOptions,
menuState.menuKey,
menuState.menuTagOptions,
)
updateDocumentTitle(option as unknown as AppMenuOption)
setKeepAliveInclude(option as unknown as AppMenuOption)
menuState.breadcrumbOptions = getCompleteRoutePath(menuState.options, key)
menuState.breadcrumbOptions = parseAndFindMatchingNodes(
menuState.options,
'key',
key,
) // 获取面包屑
if (meta.sameLevel) {
nextTick().then(() => {
const fd = menuState.breadcrumbOptions.find((curr) => {
return curr.path === option.path
})
/** 是否为根路由 */
if (!String(key).startsWith('/')) {
/** 如果不是根路由, 则拼接完整路由并跳转 */
const _path = getCompleteRoutePath(menuState.options, key)
.map((curr) => curr.key)
.join('/')
router.push(_path)
} else {
/** 根路由直接跳转 */
router.push(path)
if (!fd) {
menuState.breadcrumbOptions.push(option as unknown as AppMenuOption)
}
menuState.menuKey = key
/** 缓存菜单 key(sessionStorage) */
setCache('menuKey', key)
}
})
}
}
/**
*
* @param path
*
* @remark
* @remark
*/
const updateMenuKeyWhenRouteUpdate = (path: string) => {
const matchMenuItem = (options: MenuOption[]) => {
for (const i of options) {
if (i?.children?.length) {
matchMenuItem(i.children)
}
if (path === i.path) {
changeMenuModelValue(i.path, i)
break
}
}
}
matchMenuItem(menuState.options as MenuOption[])
}
/**
*
* @param optins menu tag option(s)
@ -168,6 +121,98 @@ export const useMenu = defineStore(
: (menuState.menuTagOptions = arr)
}
/** 当 url 地址发生变化触发 menuTagOptions 更新 */
const setMenuTagOptionsWhenMenuValueChange = (
key: string | number,
option: MenuOption,
) => {
const tag = menuState.menuTagOptions.find((curr) => curr.path === key)
if (!tag) {
menuState.menuTagOptions.push(option as unknown as MenuTagOptions)
}
}
/**
*
* @param key key
* @param option option
*
* @remark `menu key`
* @remark , key (router push )
*/
const changeMenuModelValue = (key: string | number, option: MenuOption) => {
const { meta, path } = option as unknown as AppRouteRecordRaw
if (meta.windowOpen) {
window.open(meta.windowOpen)
} else {
/**
*
* key `/` , ,
* key `/`,
*
* , key key
*/
if (!String(key).startsWith('/')) {
/** 如果不是根路由, 则拼接完整路由并跳转 */
const _path = getCompleteRoutePath(menuState.options, key)
.map((curr) => curr.key)
.join('/')
router.push(_path)
} else {
/** 根路由直接跳转 */
router.push(path)
}
/** 检查是否为根路由 */
const count = (path.match(new RegExp('/', 'g')) || []).length
/** 更新浏览器标题 */
updateDocumentTitle(option as unknown as AppMenuOption)
/** 更新缓存队列 */
setKeepAliveInclude(option as unknown as AppMenuOption)
if (!meta.sameLevel || (meta.sameLevel && count === 1)) {
/** 更新标签菜单 */
setMenuTagOptionsWhenMenuValueChange(key, option)
/** 更新面包屑 */
setBreadcrumbOptions(key, option)
menuState.menuKey = key
/** 缓存菜单 key(sessionStorage) */
setCache('menuKey', key)
} else {
setBreadcrumbOptions(menuState.menuKey || '', option)
}
}
}
/**
*
* @param path
*
* @remark
* @remark
*/
const updateMenuKeyWhenRouteUpdate = (path: string) => {
const appRawRoutes = expandRoutes(getAppRawRoutes())
const count = (path.match(new RegExp('/', 'g')) || []).length
const fd = appRawRoutes.find((curr) => curr.path === path)
let combinePath = path
if (count > 1) {
const splitPath = path.split('/').filter((curr) => curr)
combinePath = splitPath[splitPath.length - 1]
}
if (fd) {
changeMenuModelValue(combinePath, fd as unknown as MenuOption)
}
}
/**
*
* @remark ,
@ -191,6 +236,9 @@ export const useMenu = defineStore(
default: () => label.value,
}),
breadcrumbLabel: label.value,
/** 检查该菜单项是否展示 */
show:
meta.hidden === false || meta.hidden === undefined ? true : false,
} as AppMenuOption
/** 合并 icon */
const attr: AppMenuOption = Object.assign({}, route, {
@ -198,15 +246,14 @@ export const useMenu = defineStore(
})
if (option.path === getCatchMenuKey()) {
/** 设置菜单标签 */
setMenuTagOptions(attr)
/** 设置浏览器标题 */
updateDocumentTitle(attr)
setMenuTagOptionsWhenMenuValueChange(
option.path,
attr as unknown as MenuOption,
)
}
/** 检查该菜单项是否展示 */
attr.show = validMenuItemShow(option)
return attr
}
@ -214,9 +261,9 @@ export const useMenu = defineStore(
const catchArr: AppMenuOption[] = []
for (const curr of routes) {
if (curr.children?.length && validMenuItemShow(curr)) {
if (curr.children?.length) {
curr.children = resolveRoutes(curr.children, index++)
} else if (!validMenuItemShow(curr)) {
} else if (!validRole(curr.meta)) {
/** 如果校验失败, 则不会添加进 menu options */
continue
}
@ -234,15 +281,6 @@ export const useMenu = defineStore(
)
resolve()
/** 初始化后渲染面包屑 */
nextTick(() => {
menuState.breadcrumbOptions = parseAndFindMatchingNodes(
menuState.options,
'key',
menuState.menuKey as string,
)
})
})
}
@ -274,11 +312,28 @@ export const useMenu = defineStore(
menuState.menuTagOptions = []
}
/**
*
*
*
*/
const setupPiniaMenuStore = async () => {
if (isSetupAppMenuLock.value) {
await setupAppMenu()
isSetupAppMenuLock.value = false
}
}
/** 监听路由变化并且更新路由菜单与菜单标签 */
watch(
() => route.fullPath,
(newData) => {
updateMenuKeyWhenRouteUpdate(newData)
async (newData) => {
const reg = /^([^?]+)/
const match = newData.match(reg)?.[1]
await setupPiniaMenuStore()
updateMenuKeyWhenRouteUpdate(match || '')
},
{
immediate: true,

View File

@ -0,0 +1,26 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-30
*
* @workspace ray-template
*
* @remark
*/
import { NCard, NSpace } from 'naive-ui'
const RouterDemoDetail = defineComponent({
name: 'RouterDemoDetail',
render() {
return (
<NSpace wrapItem={false}>
<NCard title="平层路由详情页面"></NCard>
</NSpace>
)
},
})
export default RouterDemoDetail

View File

@ -0,0 +1,94 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2023-06-30
*
* @workspace ray-template
*
* @remark
*/
import { NSpace, NDataTable, NButton } from 'naive-ui'
import { useVueRouter } from '@/router/helper/useVueRouter'
import type { DataTableColumns } from 'naive-ui'
export interface RowData {
key: string | number
name: string
phone: string
address: string
}
const RouterDemoHome = defineComponent({
name: 'RouterDemoHome',
setup() {
const { router } = useVueRouter()
const columns: DataTableColumns<RowData> = [
{
title: '姓名',
key: 'name',
},
{
title: '地址',
key: 'address',
},
{
title: '联系方式',
key: 'phone',
},
{
title: '操作',
key: '',
render: (row) => {
return (
<NSpace align="center">
<NButton
type="info"
text
size="tiny"
onClick={() => {
router.push({
path: '/router-demo/router-demo-detail',
query: {
row: JSON.stringify(row),
},
})
}}
>
</NButton>
</NSpace>
)
},
},
]
const dataSource: RowData[] = []
for (let i = 0; i < 10; i++) {
dataSource.push({
name: '张三',
address: 'New York No. 1 Lake Park',
phone: '010-121212',
key: i,
})
}
return {
dataSource,
columns,
}
},
render() {
return (
<NSpace wrapItem={false}>
<NDataTable columns={this.columns} data={this.dataSource} />
</NSpace>
)
},
})
export default RouterDemoHome

View File

@ -43,9 +43,6 @@
"src/**/*.tsx",
"src/**/*.ts",
"src/**/*.vue",
"src/*.ts",
"src/*.vue",
"src/*",
"components.d.ts",
"auto-imports.d.ts",
"src/types/global.d.ts"