mirror of
https://github.com/XiaoDaiGua-Ray/ray-template.git
synced 2025-04-06 03:57:49 +08:00
v3.1.1,补充面包屑和修复一些小问题
This commit is contained in:
parent
13db02c6c3
commit
5676356af5
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
||||
# CHANGE LOG
|
||||
|
||||
## 3.1.1
|
||||
|
||||
### Fixes
|
||||
|
||||
- 修复国际化语言包模块合并处理不能正常合并问题
|
||||
- 修复国际化切换时,面包屑、标签页不能正常切换
|
||||
|
||||
### Feats
|
||||
|
||||
- 新增面包屑
|
||||
- 支持国际化语言包分包管理(但是,依旧是合并到一个文件中,所以需要注意 key 的管理)
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "Scroll Reveal",
|
||||
"Axios": "Axios Request",
|
||||
"Table": "Table",
|
||||
"MultiMenu": "MultiMenu",
|
||||
"Doc": "Doc",
|
||||
"DocLocal": "Doc (China)"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "Scroll Reveal",
|
||||
"Axios": "Axios Request",
|
||||
"Table": "Table",
|
||||
"MultiMenu": "MultiMenu",
|
||||
"Doc": "Doc",
|
||||
"DocLocal": "Doc (China)"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "滚动动画",
|
||||
"Axios": "请求",
|
||||
"Table": "表格",
|
||||
"MultiMenu": "多级菜单",
|
||||
"Doc": "文档",
|
||||
"DocLocal": "文档 (国内地址)"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "Scroll Reveal",
|
||||
"Axios": "Axios Request",
|
||||
"Table": "Table",
|
||||
"MultiMenu": "MultiMenu",
|
||||
"Doc": "Doc",
|
||||
"DocLocal": "Doc (China)"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "滚动动画",
|
||||
"Axios": "请求",
|
||||
"Table": "表格",
|
||||
"MultiMenu": "多级菜单",
|
||||
"Doc": "文档",
|
||||
"DocLocal": "文档 (国内地址)"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@
|
||||
"scrollReveal": "滚动动画",
|
||||
"Axios": "请求",
|
||||
"Table": "表格",
|
||||
"MultiMenu": "多级菜单",
|
||||
"Doc": "文档",
|
||||
"DocLocal": "文档 (国内地址)"
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ray-template",
|
||||
"private": true,
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@ -28,6 +28,7 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { naiveLocales } from './language'
|
||||
import { getCache } from '@use-utils/cache'
|
||||
import { forIn, merge } from 'lodash-es'
|
||||
|
||||
export { naiveLocales, localOptions } from './language'
|
||||
|
||||
@ -51,10 +52,16 @@ export const getMatchLanguageModule = () => {
|
||||
})
|
||||
|
||||
const moduleKeys = Object.keys(modules)
|
||||
moduleKeys.forEach((curr) => {
|
||||
const k = curr.match(reg)?.[1] as string
|
||||
|
||||
msg[k] = Object.assign({}, JSON.parse(modules[curr]))
|
||||
moduleKeys.forEach((curr) => {
|
||||
const k = curr.match(reg)?.[1] as string // 当前语言包类型(zh-CN, en-US...)
|
||||
const content = JSON.parse(modules[curr]) // 当前语言包内容
|
||||
|
||||
msg[k] = merge({}, msg[k])
|
||||
|
||||
forIn(content, (value, ckey) => {
|
||||
msg[k][ckey] = merge(msg[k][ckey], value)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
@ -19,9 +19,11 @@ const MenuTag = defineComponent({
|
||||
name: 'MenuTag',
|
||||
setup() {
|
||||
const menuStore = useMenu()
|
||||
const { menuTagOptions, menuKey } = storeToRefs(menuStore)
|
||||
const { menuKey } = storeToRefs(menuStore)
|
||||
const { menuModelValueChange, spliceMenTagOptions } = menuStore
|
||||
|
||||
const modelMenuTagOptions = computed(() => menuStore.menuTagOptions)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param idx 索引
|
||||
@ -32,7 +34,7 @@ const MenuTag = defineComponent({
|
||||
spliceMenTagOptions(idx)
|
||||
|
||||
if (menuKey.value !== '/dashboard') {
|
||||
const options = menuTagOptions.value as MenuOption[]
|
||||
const options = modelMenuTagOptions.value
|
||||
const length = options.length
|
||||
|
||||
const tag = options[length - 1]
|
||||
@ -50,7 +52,7 @@ const MenuTag = defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
menuTagOptions,
|
||||
modelMenuTagOptions,
|
||||
menuModelValueChange,
|
||||
handleCloseTag,
|
||||
menuKey,
|
||||
@ -61,10 +63,10 @@ const MenuTag = defineComponent({
|
||||
return (
|
||||
<NScrollbar class="menu-tag" xScrollable>
|
||||
<NSpace class="menu-tag-sapce" wrap={false} align="center">
|
||||
{this.menuTagOptions.map((curr, idx) => (
|
||||
{this.modelMenuTagOptions.map((curr, idx) => (
|
||||
<NTag
|
||||
closable={
|
||||
curr.key !== '/dashboard' && this.menuTagOptions.length > 1
|
||||
curr.key !== '/dashboard' && this.modelMenuTagOptions.length > 1
|
||||
}
|
||||
onClose={() => this.handleCloseTag(idx)}
|
||||
type={curr.key === this.menuKey ? 'success' : 'info'}
|
||||
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-03
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 顶部面包屑
|
||||
*
|
||||
* 如果下拉菜单条目小于一条, 则不会触发下拉菜单
|
||||
*
|
||||
* 添加 <span> 标签, 避免 Runtime directive used on component... 警告
|
||||
*/
|
||||
|
||||
import { NDropdown, NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
|
||||
|
||||
import { useMenu } from '@/store'
|
||||
|
||||
import type { DropdownOption } from 'naive-ui'
|
||||
|
||||
const Breadcrumb = defineComponent({
|
||||
name: 'Breadcrumb',
|
||||
setup() {
|
||||
const menuStore = useMenu()
|
||||
|
||||
const { menuModelValueChange } = menuStore
|
||||
const modelBreadcrumbOptions = computed(() => menuStore.breadcrumbOptions)
|
||||
|
||||
const handleDropdownSelect = (
|
||||
key: string | number,
|
||||
option: DropdownOption,
|
||||
) => {
|
||||
menuModelValueChange(key, option)
|
||||
}
|
||||
|
||||
return {
|
||||
modelBreadcrumbOptions,
|
||||
handleDropdownSelect,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NBreadcrumb>
|
||||
{this.modelBreadcrumbOptions.map((curr) => (
|
||||
<NBreadcrumbItem key={curr.key}>
|
||||
<NDropdown
|
||||
labelField="breadcrumbLabel"
|
||||
options={
|
||||
curr.children && curr.children?.length > 1 ? curr.children : []
|
||||
}
|
||||
onSelect={this.handleDropdownSelect.bind(this)}
|
||||
>
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
{curr.label && typeof curr.label === 'function'
|
||||
? curr.label()
|
||||
: curr.breadcrumbLabel}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
</NDropdown>
|
||||
</NBreadcrumbItem>
|
||||
))}
|
||||
</NBreadcrumb>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default Breadcrumb
|
@ -38,8 +38,12 @@ const SettingDrawer = defineComponent({
|
||||
const settingStore = useSetting()
|
||||
|
||||
const { changePrimaryColor, changeSwitcher } = settingStore
|
||||
const { themeValue, primaryColorOverride, menuTagSwitch } =
|
||||
storeToRefs(settingStore)
|
||||
const {
|
||||
themeValue,
|
||||
primaryColorOverride,
|
||||
menuTagSwitch,
|
||||
breadcrumbSwitch,
|
||||
} = storeToRefs(settingStore)
|
||||
|
||||
const modelShow = computed({
|
||||
get: () => props.show,
|
||||
@ -61,6 +65,7 @@ const SettingDrawer = defineComponent({
|
||||
primaryColorOverride,
|
||||
menuTagSwitch,
|
||||
changeSwitcher,
|
||||
breadcrumbSwitch,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
@ -135,6 +140,14 @@ const SettingDrawer = defineComponent({
|
||||
}
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="显示面包屑">
|
||||
<NSwitch
|
||||
v-model:value={this.breadcrumbSwitch}
|
||||
onUpdateValue={(bool: boolean) =>
|
||||
this.changeSwitcher(bool, 'breadcrumbSwitch')
|
||||
}
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NSpace>
|
||||
</NDrawerContent>
|
||||
|
@ -15,6 +15,7 @@ import { NLayoutHeader, NSpace, NTooltip, NDropdown, NTag } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
import RayTooltipIcon from '@/components/RayTooltipIcon/index'
|
||||
import SettingDrawer from './components/SettingDrawer/index'
|
||||
import Breadcrumb from './components/Breadcrumb/index'
|
||||
|
||||
import { useSetting } from '@/store'
|
||||
import { localOptions } from '@/language/index'
|
||||
@ -39,7 +40,7 @@ const SiderBar = defineComponent({
|
||||
|
||||
const { t } = useI18n()
|
||||
const { updateLocale, changeSwitcher } = settingStore
|
||||
const modelDrawerPlacement = ref(settingStore.drawerPlacement)
|
||||
const { drawerPlacement, breadcrumbSwitch } = storeToRefs(settingStore)
|
||||
const showSettings = ref(false)
|
||||
const person = getCache('person')
|
||||
const spaceItemStyle = {
|
||||
@ -127,12 +128,13 @@ const SiderBar = defineComponent({
|
||||
rightTooltipIconOptions,
|
||||
t,
|
||||
handleIconClick,
|
||||
modelDrawerPlacement,
|
||||
showSettings,
|
||||
updateLocale,
|
||||
handlePersonSelect,
|
||||
person,
|
||||
spaceItemStyle,
|
||||
drawerPlacement,
|
||||
breadcrumbSwitch,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
@ -159,6 +161,7 @@ const SiderBar = defineComponent({
|
||||
}}
|
||||
</NTooltip>
|
||||
))}
|
||||
{this.breadcrumbSwitch ? <Breadcrumb /> : ''}
|
||||
</NSpace>
|
||||
<NSpace align="center" itemStyle={this.spaceItemStyle}>
|
||||
{this.rightTooltipIconOptions.map((curr) => (
|
||||
@ -203,7 +206,7 @@ const SiderBar = defineComponent({
|
||||
</NSpace>
|
||||
<SettingDrawer
|
||||
v-model:show={this.showSettings}
|
||||
placement={this.modelDrawerPlacement}
|
||||
placement={this.drawerPlacement}
|
||||
/>
|
||||
</NLayoutHeader>
|
||||
)
|
||||
|
@ -4,10 +4,11 @@ import { constantRoutes } from './routes'
|
||||
import { permissionRouter as _permissionRouter } from './permission'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes,
|
||||
routes: constantRoutes as unknown as RouteRecordRaw[],
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
|
@ -6,6 +6,7 @@ import scrollReveal from './scroll-reveal'
|
||||
import axios from './axios'
|
||||
import table from './table'
|
||||
import doc from './doc'
|
||||
import multiMenu from './multi-menu'
|
||||
import docLocal from './doc-local'
|
||||
|
||||
const routes = [
|
||||
@ -15,6 +16,7 @@ const routes = [
|
||||
axios,
|
||||
scrollReveal,
|
||||
error,
|
||||
multiMenu,
|
||||
doc,
|
||||
docLocal,
|
||||
reyl,
|
||||
|
40
src/router/modules/multi-menu.ts
Normal file
40
src/router/modules/multi-menu.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export default {
|
||||
path: '/multi-menu',
|
||||
name: 'multi-menu',
|
||||
component: () => import('@/views/multi-menu/index'),
|
||||
meta: {
|
||||
i18nKey: 'MultiMenu',
|
||||
icon: 'table',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'multi-menu-one',
|
||||
name: 'multi-menu-one',
|
||||
component: () => import('@/views/multi-menu/views/multi-menu-one/index'),
|
||||
meta: {
|
||||
noLocalTitle: '多级菜单-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'multi-menu-two',
|
||||
name: 'multi-menu-two',
|
||||
component: () => import('@/views/multi-menu/views/multi-menu-two/index'),
|
||||
meta: {
|
||||
noLocalTitle: '多级菜单-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'sub-menu',
|
||||
name: 'sub-menu',
|
||||
component: () =>
|
||||
import(
|
||||
'@/views/multi-menu/views/multi-menu-two/views/sub-menu/index'
|
||||
),
|
||||
meta: {
|
||||
noLocalTitle: '多级菜单-2-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
23
src/router/remark.md
Normal file
23
src/router/remark.md
Normal file
@ -0,0 +1,23 @@
|
||||
- 类型
|
||||
|
||||
```ts
|
||||
interface RouteMeta {
|
||||
i18nKey: string
|
||||
icon?: string
|
||||
windowOpen?: string
|
||||
role?: string[]
|
||||
hidden?: boolean
|
||||
noLocalTitle?: string | number
|
||||
}
|
||||
```
|
||||
|
||||
- 说明
|
||||
|
||||
```
|
||||
i18nKey: i18n 国际化 key, 会优先使用该字段
|
||||
icon: icon 图标, 用于 Menu 菜单(依赖 RayIcon 组件实现)
|
||||
windowOpen: 超链接打开
|
||||
role: 权限表
|
||||
hidden: 是否显示
|
||||
noLocalTitle: 不使用国际化渲染 Menu Titile
|
||||
```
|
@ -3,7 +3,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import type { App } from 'vue'
|
||||
|
||||
export { useSetting } from './modules/setting' // import { useSetting } from '@/store' 即可使用
|
||||
export { useMenu } from './modules/menu'
|
||||
export { useMenu } from './modules/menu/index'
|
||||
export { useSignin } from './modules/signin'
|
||||
|
||||
const store = createPinia()
|
||||
|
@ -1,165 +0,0 @@
|
||||
import { NEllipsis } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
import { getCache, setCache } from '@/utils/cache'
|
||||
import { validRole } from '@/router/basic'
|
||||
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
|
||||
export const useMenu = defineStore('menu', () => {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const cacheMenuKey =
|
||||
getCache('menuKey') === 'no' ? '/dashboard' : getCache('menuKey')
|
||||
|
||||
const menuState = reactive({
|
||||
menuKey: cacheMenuKey as string | null, // 当前菜单 `key`
|
||||
options: [] as IMenuOptions[], // 菜单列表
|
||||
collapsed: false, // 是否折叠菜单
|
||||
menuTagOptions: [] as TagMenuOptions[],
|
||||
})
|
||||
|
||||
const handleMenuTagOptions = (item: IMenuOptions) => {
|
||||
if (item.path !== menuState.menuKey) {
|
||||
const tag = menuState.menuTagOptions.find(
|
||||
(curr) => curr.path === item.path,
|
||||
)
|
||||
|
||||
if (!tag) {
|
||||
menuState.menuTagOptions.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key 菜单更新后的 `key`
|
||||
* @param item 菜单当前 `item`
|
||||
*
|
||||
* 修改 `menu key` 后的回调函数
|
||||
*/
|
||||
const menuModelValueChange = (key: string, item: MenuOption) => {
|
||||
const meta = item.meta as RouteMeta
|
||||
|
||||
if (meta.windowOpen) {
|
||||
window.open(meta.windowOpen)
|
||||
} else {
|
||||
handleMenuTagOptions(item as unknown as TagMenuOptions)
|
||||
|
||||
menuState.menuKey = key
|
||||
|
||||
router.push(`${item.path}`)
|
||||
|
||||
setCache('menuKey', key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path 路由地址
|
||||
*
|
||||
* 监听路由地址变化更新菜单状态
|
||||
*/
|
||||
const updateMenuKeyWhenRouteUpdate = (path: string) => {
|
||||
const matchMenuItem = (options: MenuOption[]) => {
|
||||
for (const i of options) {
|
||||
if (i?.children?.length) {
|
||||
matchMenuItem(i.children)
|
||||
}
|
||||
|
||||
if (path === i.path) {
|
||||
menuModelValueChange(i.path, i)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchMenuItem(menuState.options as MenuOption[])
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 初始化菜单列表, 并且按照权限过滤
|
||||
* @remark 如果权限发生变动, 则会触发强制弹出页面并且重新登陆
|
||||
*/
|
||||
const setupAppRoutes = () => {
|
||||
const layout = router.getRoutes().find((route) => route.name === 'layout')
|
||||
|
||||
const resolveRoutes = (routes: IMenuOptions[], index: number) => {
|
||||
return routes.map((curr) => {
|
||||
if (curr.children?.length) {
|
||||
curr.children = resolveRoutes(curr.children, index++)
|
||||
}
|
||||
|
||||
const { meta } = curr
|
||||
|
||||
const route = {
|
||||
...curr,
|
||||
key: curr.path,
|
||||
label: () =>
|
||||
h(NEllipsis, null, {
|
||||
default: () => t(`GlobalMenuOptions.${meta!.i18nKey}`),
|
||||
}),
|
||||
}
|
||||
|
||||
const expandIcon = {
|
||||
icon: () =>
|
||||
h(
|
||||
RayIcon,
|
||||
{
|
||||
name: meta!.icon as string,
|
||||
size: 20,
|
||||
},
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
const attr: IMenuOptions = meta?.icon
|
||||
? Object.assign({}, route, expandIcon)
|
||||
: route
|
||||
|
||||
if (curr.path === cacheMenuKey) {
|
||||
menuState.menuTagOptions.push(attr)
|
||||
}
|
||||
|
||||
attr.show = validRole(curr)
|
||||
|
||||
return attr
|
||||
})
|
||||
}
|
||||
|
||||
menuState.options = resolveRoutes(layout?.children as IMenuOptions[], 0)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param collapsed 折叠菜单开关
|
||||
*/
|
||||
const collapsedMenu = (collapsed: boolean) =>
|
||||
(menuState.collapsed = collapsed)
|
||||
|
||||
const spliceMenTagOptions = (idx: number) =>
|
||||
menuState.menuTagOptions.splice(idx, 1)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newData) => {
|
||||
updateMenuKeyWhenRouteUpdate(newData)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(menuState),
|
||||
menuModelValueChange,
|
||||
setupAppRoutes,
|
||||
collapsedMenu,
|
||||
spliceMenTagOptions,
|
||||
}
|
||||
})
|
110
src/store/modules/menu/helper.ts
Normal file
110
src/store/modules/menu/helper.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-03
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
/** 本方法感谢 <https://yunkuangao.me/> 的支持 */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node 当前节点
|
||||
* @param key 动态字段
|
||||
* @param value 匹配值
|
||||
*
|
||||
* @remark 检查是否为所需项
|
||||
*/
|
||||
const check = (
|
||||
node: IMenuOptions,
|
||||
key: string | number,
|
||||
value: string | number,
|
||||
) => {
|
||||
return node[key] === value || node.key === value
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options 节点数组
|
||||
* @param key 动态字段
|
||||
* @param value 匹配值
|
||||
*
|
||||
* @remark 匹配所有节点
|
||||
*/
|
||||
const process = (
|
||||
options: IMenuOptions,
|
||||
key: string | number,
|
||||
value: string | number,
|
||||
) => {
|
||||
const temp: IMenuOptions[] = []
|
||||
|
||||
// 检查当前节点是否匹配值
|
||||
if (check(options, key, value)) {
|
||||
temp.push(options)
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
// 遍历子节点
|
||||
if (options.children && options.children.length > 0) {
|
||||
for (const it of options.children) {
|
||||
// 子节点递归调用
|
||||
const innerTemp = process(it, key, value)
|
||||
|
||||
// 如果子节点匹配到了,则将当前节点加入数组
|
||||
if (innerTemp.length > 0) {
|
||||
temp.push(options, ...innerTemp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options 节点数组
|
||||
* @param key 动态字段
|
||||
* @param value 匹配值
|
||||
*/
|
||||
export const parse = (
|
||||
options: IMenuOptions[],
|
||||
key: string | number,
|
||||
value: string | number,
|
||||
) => {
|
||||
const temp = []
|
||||
|
||||
for (const it of options) {
|
||||
const innerTemp = process(it, key, value)
|
||||
|
||||
if (innerTemp.length > 0) {
|
||||
temp.push(...innerTemp)
|
||||
}
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item menu options
|
||||
*
|
||||
* @remark 查找当前菜单项
|
||||
*/
|
||||
export const matchMenuOption = (
|
||||
item: IMenuOptions,
|
||||
key: MenuKey,
|
||||
menuTagOptions: TagMenuOptions[],
|
||||
) => {
|
||||
if (item.path !== key) {
|
||||
const tag = menuTagOptions.find((curr) => curr.path === item.path)
|
||||
|
||||
if (!tag) {
|
||||
menuTagOptions.push(item)
|
||||
}
|
||||
}
|
||||
}
|
218
src/store/modules/menu/index.ts
Normal file
218
src/store/modules/menu/index.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
*
|
||||
* @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, 所以需要按照该项目约定方法进行配置
|
||||
*/
|
||||
|
||||
import { NEllipsis } from 'naive-ui'
|
||||
import RayIcon from '@/components/RayIcon/index'
|
||||
|
||||
import { getCache, setCache } from '@/utils/cache'
|
||||
import { validRole } from '@/router/basic'
|
||||
import { parse, matchMenuOption } from './helper'
|
||||
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
|
||||
export const useMenu = defineStore(
|
||||
'menu',
|
||||
() => {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const cacheMenuKey =
|
||||
getCache('menuKey') === 'no' ? '/dashboard' : getCache('menuKey')
|
||||
|
||||
const menuState = reactive({
|
||||
menuKey: cacheMenuKey as MenuKey, // 当前菜单 `key`
|
||||
options: [] as IMenuOptions[], // 菜单列表
|
||||
collapsed: false, // 是否折叠菜单
|
||||
menuTagOptions: [] as TagMenuOptions[], // tag 标签菜单
|
||||
breadcrumbOptions: [] as IMenuOptions[], // 面包屑菜单
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key 菜单更新后的 `key`
|
||||
* @param item 菜单当前 `item`
|
||||
*
|
||||
* 修改 `menu key` 后的回调函数
|
||||
*/
|
||||
const menuModelValueChange = (key: string | number, item: MenuOption) => {
|
||||
const meta = item.meta as RouteMeta
|
||||
|
||||
if (meta.windowOpen) {
|
||||
window.open(meta.windowOpen)
|
||||
} else {
|
||||
// 防止重复点击做重复操作处理
|
||||
if (menuState.menuKey !== key) {
|
||||
matchMenuOption(
|
||||
item as unknown as TagMenuOptions,
|
||||
menuState.menuKey,
|
||||
menuState.menuTagOptions,
|
||||
)
|
||||
|
||||
menuState.breadcrumbOptions = parse(menuState.options, 'key', key) // 获取面包屑
|
||||
|
||||
if (key[0] !== '/') {
|
||||
const p = menuState.breadcrumbOptions
|
||||
.map((curr) => curr.key)
|
||||
.join('/')
|
||||
|
||||
router.push(p)
|
||||
} else {
|
||||
router.push(item.path as string)
|
||||
}
|
||||
|
||||
menuState.menuKey = key
|
||||
setCache('menuKey', key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path 路由地址
|
||||
*
|
||||
* 监听路由地址变化更新菜单状态
|
||||
*/
|
||||
const updateMenuKeyWhenRouteUpdate = (path: string) => {
|
||||
const matchMenuItem = (options: MenuOption[]) => {
|
||||
for (const i of options) {
|
||||
if (i?.children?.length) {
|
||||
matchMenuItem(i.children)
|
||||
}
|
||||
|
||||
if (path === i.path) {
|
||||
menuModelValueChange(i.path, i)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchMenuItem(menuState.options as MenuOption[])
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @remark 初始化菜单列表, 并且按照权限过滤
|
||||
* @remark 如果权限发生变动, 则会触发强制弹出页面并且重新登陆
|
||||
*/
|
||||
const setupAppRoutes = () => {
|
||||
const layout = router.getRoutes().find((route) => route.name === 'layout')
|
||||
|
||||
const resolveRoutes = (routes: IMenuOptions[], index: number) => {
|
||||
return routes.map((curr) => {
|
||||
if (curr.children?.length) {
|
||||
curr.children = resolveRoutes(curr.children, index++)
|
||||
}
|
||||
|
||||
const { meta } = curr
|
||||
const label = computed(() =>
|
||||
meta?.i18nKey
|
||||
? t(`GlobalMenuOptions.${meta!.i18nKey}`)
|
||||
: meta?.noLocalTitle,
|
||||
)
|
||||
|
||||
/** 拼装菜单项 */
|
||||
const route = {
|
||||
...curr,
|
||||
key: curr.path,
|
||||
label: () =>
|
||||
h(NEllipsis, null, {
|
||||
default: () => label.value,
|
||||
}),
|
||||
breadcrumbLabel: label.value,
|
||||
} as IMenuOptions
|
||||
|
||||
/** 是否有 icon */
|
||||
const expandIcon = {
|
||||
icon: () =>
|
||||
h(
|
||||
RayIcon,
|
||||
{
|
||||
name: meta!.icon as string,
|
||||
size: 20,
|
||||
},
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
const attr: IMenuOptions = meta?.icon
|
||||
? Object.assign({}, route, expandIcon)
|
||||
: route
|
||||
|
||||
if (curr.path === cacheMenuKey) {
|
||||
menuState.menuTagOptions.push(attr)
|
||||
}
|
||||
|
||||
attr.show = validRole(curr)
|
||||
|
||||
return attr
|
||||
})
|
||||
}
|
||||
|
||||
menuState.options = resolveRoutes(layout?.children as IMenuOptions[], 0)
|
||||
|
||||
/** 初始化后渲染面包屑 */
|
||||
nextTick(() => {
|
||||
menuState.breadcrumbOptions = parse(
|
||||
menuState.options,
|
||||
'key',
|
||||
menuState.menuKey as string,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param collapsed 折叠菜单开关
|
||||
*/
|
||||
const collapsedMenu = (collapsed: boolean) =>
|
||||
(menuState.collapsed = collapsed)
|
||||
|
||||
const spliceMenTagOptions = (idx: number) =>
|
||||
menuState.menuTagOptions.splice(idx, 1)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newData) => {
|
||||
updateMenuKeyWhenRouteUpdate(newData)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(menuState),
|
||||
menuModelValueChange,
|
||||
setupAppRoutes,
|
||||
collapsedMenu,
|
||||
spliceMenTagOptions,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'piniaMenuStore',
|
||||
storage: window.sessionStorage,
|
||||
paths: ['breadcrumbOptions', 'menuKey'],
|
||||
},
|
||||
},
|
||||
)
|
@ -1,4 +1,5 @@
|
||||
import { naiveLocales, getDefaultNaiveLocal } from '@/language/index'
|
||||
import { setCache } from '@use-utils/cache'
|
||||
|
||||
export const useSetting = defineStore(
|
||||
'setting',
|
||||
@ -15,6 +16,7 @@ export const useSetting = defineStore(
|
||||
menuTagSwitch: true, // 多标签页开关
|
||||
naiveLocal: getDefaultNaiveLocal(), // `naive ui` 语言包
|
||||
spinSwitch: false, // 全屏加载
|
||||
breadcrumbSwitch: true, // 面包屑开关
|
||||
})
|
||||
const { locale } = useI18n()
|
||||
|
||||
@ -22,6 +24,8 @@ export const useSetting = defineStore(
|
||||
// TODO: 修改语言
|
||||
locale.value = key
|
||||
settingState.naiveLocal = naiveLocales(key)
|
||||
|
||||
setCache('localeLanguage', key, 'localStorage')
|
||||
}
|
||||
|
||||
const changePrimaryColor = (value: string) => {
|
||||
|
4
src/types/store.d.ts
vendored
4
src/types/store.d.ts
vendored
@ -13,7 +13,11 @@ declare global {
|
||||
show?: boolean
|
||||
children?: IMenuOptions[]
|
||||
meta?: RouteMeta
|
||||
breadcrumbLabel?: string
|
||||
noLocalTitle?: string | number
|
||||
}
|
||||
|
||||
declare interface TagMenuOptions extends IMenuOptions {}
|
||||
|
||||
declare type MenuKey = null | string | number
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
NLayoutHeader,
|
||||
NSpace,
|
||||
NInput,
|
||||
NButton,
|
||||
} from 'naive-ui'
|
||||
import { onAxiosTest } from '@use-api/test'
|
||||
|
||||
@ -41,9 +42,13 @@ const Axios = defineComponent({
|
||||
]
|
||||
|
||||
const handleInputCityValue = async (value: string) => {
|
||||
const cb = await onAxiosTest(value)
|
||||
try {
|
||||
const cb = await onAxiosTest(value)
|
||||
|
||||
state.weatherData = cb.data as unknown as IUnknownObjectKey[]
|
||||
state.weatherData = cb.data as unknown as IUnknownObjectKey[]
|
||||
} catch (e) {
|
||||
window.$message.error('请求已被取消')
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
@ -62,20 +67,23 @@ const Axios = defineComponent({
|
||||
<NLayout>
|
||||
<NLayoutHeader bordered>
|
||||
<NCard title="请求函数">
|
||||
基于 axios 封装, 能够自动取消连续请求, 避免重复渲染造成问题.
|
||||
基于 axios 封装,能够自动取消连续请求,避免重复渲染造成问题
|
||||
<p>
|
||||
打开控制台 => 网络 => 使用低速3g网络 =>
|
||||
查看控制台被取消的请求
|
||||
</p>
|
||||
</NCard>
|
||||
</NLayoutHeader>
|
||||
<NLayoutHeader bordered>
|
||||
<NSpace
|
||||
class="axios-header__btn"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<NSpace class="axios-header__btn" align="center">
|
||||
<NInput
|
||||
v-model:value={this.inputCityValue}
|
||||
onInput={this.handleInputCityValue.bind(this)}
|
||||
placeholder="请输入城市"
|
||||
/>
|
||||
<NButton onClick={this.handleInputCityValue.bind(this, '')}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NLayoutHeader>
|
||||
<NLayoutContent>
|
||||
|
24
src/views/multi-menu/index.tsx
Normal file
24
src/views/multi-menu/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-01
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const MultiMenu = defineComponent({
|
||||
name: 'MultiMenu',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return <RouterView />
|
||||
},
|
||||
})
|
||||
|
||||
export default MultiMenu
|
22
src/views/multi-menu/views/multi-menu-one/index.tsx
Normal file
22
src/views/multi-menu/views/multi-menu-one/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-01
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
const MultiMenuOne = defineComponent({
|
||||
name: 'MultiMenuOne',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return <div>多级菜单-1</div>
|
||||
},
|
||||
})
|
||||
|
||||
export default MultiMenuOne
|
24
src/views/multi-menu/views/multi-menu-two/index.tsx
Normal file
24
src/views/multi-menu/views/multi-menu-two/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-01
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const MultiMenuTwo = defineComponent({
|
||||
name: 'MultiMenuTwo',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return <RouterView />
|
||||
},
|
||||
})
|
||||
|
||||
export default MultiMenuTwo
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
*
|
||||
* @author Ray <https://github.com/XiaoDaiGua-Ray>
|
||||
*
|
||||
* @date 2023-03-01
|
||||
*
|
||||
* @workspace ray-template
|
||||
*
|
||||
* @remark 今天也是元气满满撸代码的一天
|
||||
*/
|
||||
|
||||
const SubMenu = defineComponent({
|
||||
name: 'SubMenu',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return <div>多级菜单-2-1</div>
|
||||
},
|
||||
})
|
||||
|
||||
export default SubMenu
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -17,6 +17,7 @@ declare module 'vue-router' {
|
||||
windowOpen?: string
|
||||
role?: string[]
|
||||
hidden?: boolean
|
||||
noLocalTitle?: string | number
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user