2023-12-10 14:53:27 +08:00

403 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
*
* @author Ray <https://github.com/XiaoDaiGua-Ray>
*
* @date 2022-11-03
*
* @workspace ray-template
*
* @remark 今天也是元气满满撸代码的一天
*/
/**
*
* 该文件为 menu 菜单 pinia store
*
* 说明:
* - BreadcrumbMenu、TagMenu、Menu 统一管理
* - BreadcrumbMenu、TagMenu、Menu 属性值重度依赖 vue-router routers, 所以需要按照该项目约定方法进行配置
*
* 缓存(sessionStorage):
* - breadcrumbOptions
* - menuKey
* - menuTagOptions
*/
import { NEllipsis } from 'naive-ui'
import { setStorage } from '@/utils/cache'
import { validRole, validMenuItemShow } from '@/router/helper/routerCopilot'
import {
parseAndFindMatchingNodes,
updateDocumentTitle,
hasMenuIcon,
getCatchMenuKey,
} from './helper'
import { useI18n } from '@/hooks/web'
import { getAppRawRoutes } from '@/router/appRouteModules'
import { throttle } from 'lodash-es'
import { useKeepAliveActions } from '@/store'
import type { AppRouteRecordRaw } from '@/router/type'
import type { AppMenuOption, MenuTagOptions } from '@/types/modules/app'
import type { MenuState } from '@/store/modules/menu/type'
import type { LocationQuery } from 'vue-router'
export const piniaMenuStore = defineStore(
'menu',
() => {
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const { setKeepAliveInclude } = useKeepAliveActions()
const menuState = reactive<MenuState>({
menuKey: getCatchMenuKey(), // 当前菜单 `key`
options: [], // 菜单列表
collapsed: false, // 是否折叠菜单
menuTagOptions: [], // tag 标签菜单
breadcrumbOptions: [], // 面包屑菜单
currentMenuOption: null, // 当前激活菜单项
})
const isSetupAppMenuLock = ref(true)
const isRootPathReg = new RegExp('/', 'g')
const resolveOption = (option: AppMenuOption) => {
const { meta } = option
/** 设置 label, i18nKey 优先级最高 */
const label = computed(() =>
meta?.i18nKey ? t(`${meta!.i18nKey}`) : meta?.noLocalTitle,
)
/** 拼装菜单项 */
const route = {
...option,
key: option.path,
label: () =>
h(NEllipsis, null, {
default: () => label.value,
}),
breadcrumbLabel: label.value,
/** 检查该菜单项是否展示 */
} as AppMenuOption
/** 合并 icon */
const attr: AppMenuOption = Object.assign({}, route, {
icon: hasMenuIcon(option),
})
if (option.path === getCatchMenuKey()) {
/** 设置标签页(初始化时执行设置一次, 避免含有平级路由模式情况时出现不能正确设置标签页的情况) */
setMenuTagOptionsWhenMenuValueChange(option.path, attr)
}
attr.show = validMenuItemShow(attr)
return attr
}
/**
*
* @param options menu options
* @param key target key
*
* @remark 获取完整菜单项
*/
const getCompleteRoutePath = (
options: AppMenuOption[],
key: string | number,
) => {
const ops = parseAndFindMatchingNodes(options, 'key', key)
return ops
}
/**
*
* 设置面包屑
*
* 如果识别到为平级模式, 则会自动追加一层面包屑
*/
const setBreadcrumbOptions = (
key: string | number,
option: AppMenuOption,
) => {
const { meta } = option as unknown as AppRouteRecordRaw
menuState.breadcrumbOptions = getCompleteRoutePath(menuState.options, key)
if (meta.sameLevel) {
nextTick().then(() => {
const fd = menuState.breadcrumbOptions.find((curr) => {
return curr.path === option.path
})
if (!fd) {
menuState.breadcrumbOptions.push(option as unknown as AppMenuOption)
}
})
}
}
/**
*
* @param options menu tag option(s)
* @param isAppend true: 追加操作(push), false: 覆盖操作
*/
const setMenuTagOptions = (
options: MenuTagOptions | MenuTagOptions[],
isAppend = true,
) => {
const isArray = Array.isArray(options)
const arr = isArray ? [...options] : [options]
isAppend
? menuState.menuTagOptions.push(...arr)
: (menuState.menuTagOptions = arr)
}
/** 当 url 地址发生变化触发 menuTagOptions 更新 */
const setMenuTagOptionsWhenMenuValueChange = (
key: string | number,
option: AppMenuOption,
) => {
const tag = menuState.menuTagOptions.find((curr) => curr.path === key)
if (!tag) {
menuState.menuTagOptions.push(option as MenuTagOptions)
}
}
/**
*
* @param key 菜单更新后的 key
* @param option 菜单当前 option 项
* @param query 路由参数
*
* @remark 修改 `menu key` 后的回调函数
* @remark 修改后, 缓存当前选择 key 并且存储标签页与跳转页面(router push 操作)
*
* 如果 windowOpen 存在, 则直接打开新窗口,不会更新当前菜单状态,也不会做其他的操作
* 如果 sameLevel 存在,则会追加一层面包屑,并不会触发菜单更新与标签页更新
*
* 在执行更新操作后会做一些缓存操作
*
* 该方法是整个模板的核心驱动: 菜单、标签页、面包屑、浏览器标题等等的更新方法
*
* @example
* changeMenuModelValue('/dashboard',{ dashboard option }) // 跳转页面至 dashboard并且更新菜单状态、标签页、面包屑、浏览器标题等等
* changeMenuModelValue('/dashboard', { dashboard option }, { id: 1 }) // 执行更新操作,并且传递参数
*/
const changeMenuModelValue = (
key: string | number,
option: AppMenuOption,
query?: LocationQuery,
) => {
const { meta, path } = option
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: _path,
query,
})
} else {
/** 根路由直接跳转 */
router.push({
path,
query,
})
}
/** 检查是否为根路由 */
const count = (path.match(isRootPathReg) || []).length
/** 更新缓存队列 */
setKeepAliveInclude(option as unknown as AppMenuOption)
/** 更新浏览器标题 */
updateDocumentTitle(option as unknown as AppMenuOption)
if (!meta.sameLevel || (meta.sameLevel && count === 1)) {
/** 更新标签菜单 */
setMenuTagOptionsWhenMenuValueChange(key, option)
/** 更新面包屑 */
setBreadcrumbOptions(key, option)
menuState.menuKey = key
/** 缓存菜单 key(sessionStorage) */
setStorage('menuKey', key)
} else {
setBreadcrumbOptions(menuState.menuKey || '', option)
}
menuState.currentMenuOption = option
}
}
/**
*
* @param path 路由地址
*
* @remark 监听路由地址变化更新菜单状态
* @remark 递归查找匹配项
*/
const updateMenuKeyWhenRouteUpdate = async (
path: string,
query: LocationQuery,
) => {
const [routePath] = path.split('?')
const count = (routePath.match(new RegExp('/', 'g')) || []).length // 如果获取长度为 1则视为根路由
let combinePath = routePath
if (count > 1) {
// 如果不是跟路径则取出最后一项字符
const splitPath = routePath.split('/').filter((curr) => curr)
combinePath = splitPath[splitPath.length - 1]
}
// 如果当前菜单 key 与路由地址相同,说明不是手动更新 url 则不会触发更新
if (combinePath === menuState.menuKey) {
return
}
const findMenuOption = router
.getRoutes()
.find((curr) =>
count > 1 ? path === curr.path : combinePath === curr.path,
)
if (findMenuOption) {
changeMenuModelValue(
count > 1 ? combinePath : path,
resolveOption(findMenuOption as unknown as AppMenuOption),
query,
)
}
}
/**
*
* @remark 初始化菜单列表, 并且按照权限过滤
* @remark 如果权限发生变动, 则会触发强制弹出页面并且重新登陆
*/
const setupAppMenu = () => {
return new Promise<void>((resolve) => {
const resolveRoutes = (routes: AppMenuOption[], index: number) => {
const catchArr: AppMenuOption[] = []
for (const curr of routes) {
if (curr.children?.length) {
curr.children = resolveRoutes(curr.children, index++)
} else if (!validRole(curr.meta)) {
/** 如果校验失败, 则不会添加进 menu options */
continue
}
catchArr.push(resolveOption(curr))
}
return catchArr
}
/** 缓存菜单列表 */
menuState.options = resolveRoutes(
getAppRawRoutes() as AppMenuOption[],
0,
)
resolve()
})
}
/**
*
* @param collapsed 折叠菜单开关
*/
const collapsedMenu = (collapsed: boolean) =>
(menuState.collapsed = collapsed)
/**
*
* @param idx 当前关闭标签索引
* @param length 裁剪标签页长度
*
* @returns 被关闭标签项
*/
const spliceMenTagOptions = (idx: number, length = 1) =>
menuState.menuTagOptions.splice(idx, length)
/**
*
* @remark 置空 menuTagOptions
*
* Q: 为什么不直接使用 spliceMenTagOptions 方法置空菜单标签?
* A: 因为直接将 menuTagOptions 指向新的地址会快一点
*/
const emptyMenuTagOptions = () => {
menuState.menuTagOptions = []
}
/**
*
* 初始化系统菜单列表
* 该方法仅执行一次
*/
const setupPiniaMenuStore = async () => {
if (!isSetupAppMenuLock.value) {
return
}
await setupAppMenu()
isSetupAppMenuLock.value = false
}
/** 监听路由变化并且更新路由菜单与菜单标签 */
watch(
() => route.fullPath,
async (ndata, odata) => {
await setupPiniaMenuStore()
if (ndata !== odata) {
await updateMenuKeyWhenRouteUpdate(ndata, route.query)
}
},
{
immediate: true,
},
)
return {
...toRefs(menuState),
changeMenuModelValue: throttle(changeMenuModelValue, 500),
setupAppMenu,
collapsedMenu,
spliceMenTagOptions,
emptyMenuTagOptions,
setMenuTagOptions,
}
},
{
persist: {
key: 'piniaMenuStore',
storage: window.sessionStorage,
paths: ['breadcrumbOptions', 'menuKey', 'menuTagOptions'],
},
},
)