将 menu 从 appstore 提出.

This commit is contained in:
lijiahangmax
2023-09-25 22:05:48 +08:00
parent 80e5e241fa
commit 99d6d16a04
23 changed files with 190 additions and 287 deletions

View File

@@ -98,6 +98,7 @@ body {
.circle { .circle {
display: inline-block; display: inline-block;
margin-right: 4px; margin-right: 4px;
margin-bottom: 2px;
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;

View File

@@ -2,7 +2,7 @@
<div class="block"> <div class="block">
<h5 class="title">{{ title }}</h5> <h5 class="title">{{ title }}</h5>
<div v-for="option in options" :key="option.name" class="switch-wrapper"> <div v-for="option in options" :key="option.name" class="switch-wrapper">
<span>{{ $t(option.name) }}</span> <span>{{ option.name }}</span>
<form-wrapper <form-wrapper
:type="option.type || 'switch'" :type="option.type || 'switch'"
:name="option.key" :name="option.key"

View File

@@ -1,18 +1,15 @@
<template> <template>
<a-input-number <a-input-number v-if="type === 'number'"
v-if="type === 'number'" :style="{ width: '80px' }"
:style="{ width: '80px' }" size="small"
size="small" :default-value="defaultValue as number"
:default-value="defaultValue as number" @change="handleChange"
@change="handleChange" hide-button
hide-button
/>
<a-switch
v-else
:default-checked="defaultValue"
size="small"
@change="handleChange"
/> />
<a-switch v-else
:default-checked="defaultValue"
size="small"
@change="handleChange" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -6,35 +6,27 @@
</template> </template>
</a-button> </a-button>
</div> </div>
<a-drawer <a-drawer title="偏好设置"
:width="300" :width="300"
unmount-on-close unmount-on-close
:visible="visible" :visible="visible"
:cancel-text="$t('settings.close')" ok-text="保存"
:ok-text="$t('settings.copySettings')" cancel-text="关闭"
@ok="copySettings" @ok="saveConfig"
@cancel="cancel" @cancel="cancel">
> <Block :options="contentOpts" title="内容区域" />
<template #title> {{ $t('settings.title') }}</template> <Block :options="othersOpts" title="其他设置" />
<Block :options="contentOpts" :title="$t('settings.content')" />
<Block :options="othersOpts" :title="$t('settings.otherSettings')" />
<a-alert>{{ $t('settings.alertContent') }}</a-alert>
</a-drawer> </a-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import Block from './block.vue'; import Block from './block.vue';
const emit = defineEmits(['cancel']); const emit = defineEmits(['cancel']);
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n();
const { copy } = useClipboard();
const visible = computed(() => appStore.globalSettings); const visible = computed(() => appStore.globalSettings);
/** /**
@@ -42,33 +34,32 @@
*/ */
const contentOpts = computed(() => [ const contentOpts = computed(() => [
{ {
name: 'settings.navbar', name: '导航栏',
key: 'navbar', key: 'navbar',
defaultVal: appStore.navbar defaultVal: appStore.navbar
}, },
{ {
name: 'settings.menu', name: '菜单栏',
key: 'menu', key: 'menu',
defaultVal: appStore.menu, defaultVal: appStore.menu,
}, },
{ {
name: 'settings.topMenu', name: '顶部菜单栏',
key: 'topMenu', key: 'topMenu',
defaultVal: appStore.topMenu, defaultVal: appStore.topMenu,
}, },
{ {
name: 'settings.footer', name: '底部',
key: 'footer', key: 'footer',
defaultVal: defaultVal: appStore.footer
appStore.footer
}, },
{ {
name: 'settings.tabBar', name: '多页签',
key: 'tabBar', key: 'tabBar',
defaultVal: appStore.tabBar defaultVal: appStore.tabBar
}, },
{ {
name: 'settings.menuWidth', name: '菜单宽度 (px)',
key: 'menuWidth', key: 'menuWidth',
defaultVal: appStore.menuWidth, defaultVal: appStore.menuWidth,
type: 'number', type: 'number',
@@ -80,7 +71,7 @@
*/ */
const othersOpts = computed(() => [ const othersOpts = computed(() => [
{ {
name: 'settings.colorWeak', name: '色弱模式',
key: 'colorWeak', key: 'colorWeak',
defaultVal: appStore.colorWeak, defaultVal: appStore.colorWeak,
}, },
@@ -97,10 +88,7 @@
/** /**
* 复制配置 * 复制配置
*/ */
const copySettings = async () => { const saveConfig = async () => {
const text = JSON.stringify(appStore.$state, null, 2);
await copy(text);
Message.success(t('settings.copySettings.message'));
}; };
/** /**

View File

@@ -1,12 +1,12 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router'; import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
import { useAppStore } from '@/store'; import { useMenuStore } from '@/store';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
export default function useMenuTree() { export default function useMenuTree() {
const appStore = useAppStore(); const menuStore = useMenuStore();
const appRoute = computed(() => { const appRoute = computed(() => {
return appStore.appAsyncMenus; return menuStore.appMenus;
}); });
const menuTree = computed(() => { const menuTree = computed(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[]; const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];

View File

@@ -6,17 +6,15 @@
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span> <span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
</template> </template>
<a-result v-if="!renderList.length" status="404"> <a-result v-if="!renderList.length" status="404">
<template #subtitle> {{ $t('messageBox.noContent') }}</template> <template #subtitle>暂无内容</template>
</a-result> </a-result>
<List <List :render-list="renderList"
:render-list="renderList" :unread-count="unreadCount"
:unread-count="unreadCount" @item-click="handleItemClick" />
@item-click="handleItemClick"
/>
</a-tab-pane> </a-tab-pane>
<template #extra> <template #extra>
<a-button type="text" @click="emptyList"> <a-button type="text" @click="emptyList">
{{ $t('messageBox.tab.button') }} 清空
</a-button> </a-button>
</template> </template>
</a-tabs> </a-tabs>
@@ -25,7 +23,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, toRefs, computed } from 'vue'; import { ref, reactive, toRefs, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { import {
queryMessageList, queryMessageList,
setMessageStatus, setMessageStatus,
@@ -43,7 +40,6 @@
const { loading, setLoading } = useLoading(true); const { loading, setLoading } = useLoading(true);
const messageType = ref('message'); const messageType = ref('message');
const { t } = useI18n();
const messageData = reactive<{ const messageData = reactive<{
renderList: MessageRecord[]; renderList: MessageRecord[];
messageList: MessageRecord[]; messageList: MessageRecord[];
@@ -55,15 +51,15 @@
const tabList: TabItem[] = [ const tabList: TabItem[] = [
{ {
key: 'message', key: 'message',
title: t('messageBox.tab.title.message'), title: '消息',
}, },
{ {
key: 'notice', key: 'notice',
title: t('messageBox.tab.title.notice'), title: '通知',
}, },
{ {
key: 'todo', key: 'todo',
title: t('messageBox.tab.title.todo'), title: '待办',
}, },
]; ];

View File

@@ -57,10 +57,10 @@
:class="{ 'add-border-top': renderList.length < showMax }" :class="{ 'add-border-top': renderList.length < showMax }"
> >
<div class="footer-wrap"> <div class="footer-wrap">
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link> <a-link @click="allRead">全部已读</a-link>
</div> </div>
<div class="footer-wrap"> <div class="footer-wrap">
<a-link>{{ $t('messageBox.viewMore') }}</a-link> <a-link>查看更多</a-link>
</div> </div>
</a-space> </a-space>
</template> </template>

View File

@@ -1,12 +0,0 @@
export default {
'messageBox.tab.title.message': '消息',
'messageBox.tab.title.notice': '通知',
'messageBox.tab.title.todo': '待办',
'messageBox.tab.button': '清空',
'messageBox.allRead': '全部已读',
'messageBox.viewMore': '查看更多',
'messageBox.noContent': '暂无内容',
'messageBox.userCenter': '用户中心',
'messageBox.userSettings': '用户设置',
'messageBox.logout': '登出登录',
};

View File

@@ -31,7 +31,7 @@
<ul class="right-side"> <ul class="right-side">
<!-- 搜索 --> <!-- 搜索 -->
<li v-if="false"> <li v-if="false">
<a-tooltip :content="$t('settings.search')"> <a-tooltip content="搜索">
<a-button class="nav-btn" type="outline" shape="circle"> <a-button class="nav-btn" type="outline" shape="circle">
<template #icon> <template #icon>
<icon-search /> <icon-search />
@@ -41,7 +41,7 @@
</li> </li>
<!-- 切换语言 --> <!-- 切换语言 -->
<li v-if="false"> <li v-if="false">
<a-tooltip :content="$t('settings.language')"> <a-tooltip content="语言">
<a-button <a-button
class="nav-btn" class="nav-btn"
type="outline" type="outline"
@@ -69,8 +69,8 @@
<!-- 暗色模式 --> <!-- 暗色模式 -->
<li> <li>
<a-tooltip :content="theme === 'light' <a-tooltip :content="theme === 'light'
? $t('settings.navbar.theme.toDark') ? '点击切换为暗黑模式'
: $t('settings.navbar.theme.toLight')"> : '点击切换为亮色模式'">
<a-button <a-button
class="nav-btn" class="nav-btn"
type="outline" type="outline"
@@ -85,7 +85,7 @@
</li> </li>
<!-- 消息列表 --> <!-- 消息列表 -->
<li v-if="false"> <li v-if="false">
<a-tooltip :content="$t('settings.navbar.alerts')"> <a-tooltip content="消息通知">
<div class="message-box-trigger"> <div class="message-box-trigger">
<a-badge :count="9" dot> <a-badge :count="9" dot>
<a-button <a-button
@@ -112,8 +112,8 @@
<!-- 全屏模式 --> <!-- 全屏模式 -->
<li> <li>
<a-tooltip :content="isFullscreen <a-tooltip :content="isFullscreen
? $t('settings.navbar.screen.toExit') ? '点击退出全屏模式'
: $t('settings.navbar.screen.toFull')"> : '点击切换全屏模式'">
<a-button <a-button
class="nav-btn" class="nav-btn"
type="outline" type="outline"
@@ -126,9 +126,9 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</li> </li>
<!-- 页面配 --> <!-- 偏好设 -->
<li v-if="false"> <li>
<a-tooltip :content="$t('settings.title')"> <a-tooltip content="偏好设置">
<a-button <a-button
class="nav-btn" class="nav-btn"
type="outline" type="outline"
@@ -154,27 +154,21 @@
<a-doption> <a-doption>
<a-space @click="$router.push({ name: 'Info' })"> <a-space @click="$router.push({ name: 'Info' })">
<icon-user /> <icon-user />
<span> <span>用户中心</span>
{{ $t('messageBox.userCenter') }}
</span>
</a-space> </a-space>
</a-doption> </a-doption>
<!-- 用户设置 --> <!-- 用户设置 -->
<a-doption> <a-doption>
<a-space @click="$router.push({ name: 'Setting' })"> <a-space @click="$router.push({ name: 'Setting' })">
<icon-settings /> <icon-settings />
<span> <span>用户设置</span>
{{ $t('messageBox.userSettings') }}
</span>
</a-space> </a-space>
</a-doption> </a-doption>
<!-- 退出登录 --> <!-- 退出登录 -->
<a-doption> <a-doption>
<a-space @click="handleLogout"> <a-space @click="handleLogout">
<icon-export /> <icon-export />
<span> <span>退出登录</span>
{{ $t('messageBox.logout') }}
</span>
</a-space> </a-space>
</a-doption> </a-doption>
</template> </template>

View File

@@ -7,10 +7,8 @@
"hideMenu": false, "hideMenu": false,
"menuCollapse": false, "menuCollapse": false,
"footer": true, "footer": true,
"themeColor": "#165DFF",
"menuWidth": 220, "menuWidth": 220,
"globalSettings": false, "globalSettings": false,
"device": "desktop", "device": "desktop",
"tabBar": true, "tabBar": true
"serverMenu": []
} }

View File

@@ -1,9 +1,9 @@
import { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router'; import { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import { useAppStore, useUserStore } from '@/store'; import { useMenuStore, useUserStore } from '@/store';
import { STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants'; import { STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants';
export default function usePermission() { export default function usePermission() {
const appStore = useAppStore(); const menuStore = useMenuStore();
const userStore = useUserStore(); const userStore = useUserStore();
return { return {
/** /**
@@ -15,7 +15,7 @@ export default function usePermission() {
return false; return false;
} }
// 检查路由是否存在于授权路由中 // 检查路由是否存在于授权路由中
const menuConfig = [...appStore.appAsyncMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST]; const menuConfig = [...menuStore.appMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST];
let exist = false; let exist = false;
while (menuConfig.length && !exist) { while (menuConfig.length && !exist) {
const element = menuConfig.shift(); const element = menuConfig.shift();

View File

@@ -1,14 +1,8 @@
import localeMessageBox from '@/components/message-box/locale/zh-CN';
import localeLogin from '@/views/login/locale/zh-CN'; import localeLogin from '@/views/login/locale/zh-CN';
import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'; import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
import localeSettings from './zh-CN/settings';
export default { export default {
'navbar.action.locale': '切换为中文', 'navbar.action.locale': '切换为中文',
...localeSettings,
...localeMessageBox,
...localeLogin, ...localeLogin,
...localeWorkplace, ...localeWorkplace,
}; };

View File

@@ -1,28 +0,0 @@
export default {
'settings.title': '页面配置',
'settings.themeColor': '主题色',
'settings.content': '内容区域',
'settings.search': '搜索',
'settings.language': '语言',
'settings.navbar': '导航栏',
'settings.menuWidth': '菜单宽度 (px)',
'settings.navbar.theme.toLight': '点击切换为亮色模式',
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
'settings.navbar.screen.toFull': '点击切换全屏模式',
'settings.navbar.screen.toExit': '点击退出全屏模式',
'settings.navbar.alerts': '消息通知',
'settings.menu': '菜单栏',
'settings.topMenu': '顶部菜单栏',
'settings.tabBar': '多页签',
'settings.footer': '底部',
'settings.otherSettings': '其他设置',
'settings.colorWeak': '色弱模式',
'settings.alertContent':
'配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
'settings.copySettings': '复制配置',
'settings.copySettings.message':
'复制成功,请粘贴到 src/settings.json 文件中',
'settings.close': '关闭',
'settings.color.tooltip':
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
};

View File

@@ -1,25 +1,25 @@
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import { useAppStore } from '@/store'; import { useMenuStore } from '@/store';
import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants'; import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants';
import usePermission from '@/hooks/permission'; import usePermission from '@/hooks/permission';
export default function setupPermissionGuard(router: Router) { export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const appStore = useAppStore(); const menuStore = useMenuStore();
// 未加载菜单 并且 不在白名单内 则加载菜单 // 未加载菜单 并且 不在白名单内 则加载菜单
if ( if (
!appStore.menuFetched && !menuStore.menuFetched &&
!WHITE_ROUTER_LIST.find((el) => el.name === to.name) !WHITE_ROUTER_LIST.find((el) => el.name === to.name)
) { ) {
// 加载菜单 // 加载菜单
await appStore.fetchMenuConfig(); await menuStore.fetchMenu();
} }
// 检测是否可以访问 // 检测是否可以访问
const permission = usePermission(); const permission = usePermission();
const access = permission.accessRouter(to); const access = permission.accessRouter(to);
// 刚进入页面时 重定向的 meta 是空的 // 刚进入页面时 重定向的 meta 是空的
if (access && to.meta.locale === undefined && appStore.menuFetched) { if (access && to.meta.locale === undefined && menuStore.menuFetched) {
to.meta = to.matched[to.matched.length - 1].meta; to.meta = to.matched[to.matched.length - 1].meta;
} }

View File

@@ -1,5 +1,6 @@
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import useAppStore from './modules/app'; import useAppStore from './modules/app';
import useMenuStore from './modules/menu';
import useUserStore from './modules/user'; import useUserStore from './modules/user';
import useTabBarStore from './modules/tab-bar'; import useTabBarStore from './modules/tab-bar';
import useCacheStore from './modules/cache'; import useCacheStore from './modules/cache';
@@ -8,6 +9,7 @@ const pinia = createPinia();
export { export {
useAppStore, useAppStore,
useMenuStore,
useUserStore, useUserStore,
useTabBarStore, useTabBarStore,
useCacheStore useCacheStore

View File

@@ -1,15 +1,10 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue';
import type { RouteRecordNormalized } from 'vue-router';
import defaultSettings from '@/config/settings.json'; import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/user/auth';
import { AppState } from './types'; import { AppState } from './types';
import router from '@/router';
const useAppStore = defineStore('app', { export default defineStore('app', {
state: (): AppState => ({ state: (): AppState => ({
...defaultSettings, ...defaultSettings,
menuFetched: false,
}), }),
getters: { getters: {
@@ -19,23 +14,16 @@ const useAppStore = defineStore('app', {
appDevice(state: AppState) { appDevice(state: AppState) {
return state.device; return state.device;
}, },
appAsyncMenus(state: AppState): RouteRecordNormalized[] {
return state.serverMenu as unknown as RouteRecordNormalized[];
},
}, },
actions: { actions: {
/** // 更新配置
* 更新配置
*/
updateSettings(partial: Partial<AppState>) { updateSettings(partial: Partial<AppState>) {
// @ts-ignore-next-line this.$patch(partial as object);
this.$patch(partial); console.log(partial);
}, },
/** // 修改颜色主题
* 修改颜色主题
*/
toggleTheme(dark: boolean) { toggleTheme(dark: boolean) {
if (dark) { if (dark) {
this.theme = 'dark'; this.theme = 'dark';
@@ -46,90 +34,14 @@ const useAppStore = defineStore('app', {
} }
}, },
/** // 切换设备
* 切换设备
*/
toggleDevice(device: string) { toggleDevice(device: string) {
this.device = device; this.device = device;
}, },
/** // 切换菜单状态
* 切换菜单状态
*/
toggleMenu(value: boolean) { toggleMenu(value: boolean) {
this.hideMenu = value; this.hideMenu = value;
}, },
/**
* 加载菜单
*/
async fetchMenuConfig() {
try {
const { data } = await getMenuList();
// @ts-ignore
this.serverMenu = (data as Array<any>).map(s => {
// 转换
const convert = (item: any) => {
// 设置路由属性
const meta = {
locale: item.name,
icon: item.icon,
order: item.sort,
hideInMenu: item.visible === 0,
hideChildrenInMenu: item.visible === 0,
noAffix: item.visible === 0,
ignoreCache: item.cache === 0,
};
// 获取 router
const route = router.getRoutes().find(r => {
return r.name === item.component;
});
// 设置 router meta
if (route) {
// 路由配置覆盖菜单配置
route.meta = { ...meta, ...route.meta };
}
// 返回
return {
name: item.component,
path: item.path,
meta: meta,
children: undefined as unknown
};
};
// 构建父目录
const menu = convert(s);
// 构建子目录
if (s.children) {
menu.children = (s.children as Array<any>).map(convert);
}
return menu;
});
// 是否已加载过
this.menuFetched = true;
// 未配置菜单
if (this.serverMenu.length === 0) {
Notification.error({
content: '该用户未配置菜单, 请先联系管理员配置',
closable: true,
});
}
} catch (error) {
Notification.error({
content: '加载菜单失败',
closable: true,
});
}
},
/**
* 清空菜单
*/
clearMenu() {
this.serverMenu = [];
this.menuFetched = false;
},
}, },
}); });
export default useAppStore;

View File

@@ -1,5 +1,3 @@
import type { RouteRecordNormalized } from 'vue-router';
export interface AppState { export interface AppState {
theme: string; theme: string;
colorWeak: boolean; colorWeak: boolean;
@@ -9,13 +7,10 @@ export interface AppState {
hideMenu: boolean; hideMenu: boolean;
menuCollapse: boolean; menuCollapse: boolean;
footer: boolean; footer: boolean;
themeColor: string;
menuWidth: number; menuWidth: number;
globalSettings: boolean; globalSettings: boolean;
device: string; device: string;
tabBar: boolean; tabBar: boolean;
serverMenu: RouteRecordNormalized[];
menuFetched: boolean;
[key: string]: unknown; [key: string]: unknown;
} }

View File

@@ -3,7 +3,7 @@ import { CacheState } from './types';
export type CacheType = 'menus' | 'roles' | 'tags' | 'hostKeys' | 'hostIdentities' export type CacheType = 'menus' | 'roles' | 'tags' | 'hostKeys' | 'hostIdentities'
const useCacheStore = defineStore('cache', { export default defineStore('cache', {
state: (): CacheState => ({ state: (): CacheState => ({
menus: [], menus: [],
roles: [], roles: [],
@@ -15,13 +15,9 @@ const useCacheStore = defineStore('cache', {
getters: {}, getters: {},
actions: { actions: {
/** // 设置
* 设置
*/
set(name: CacheType, value: any) { set(name: CacheType, value: any) {
this[name] = value; this[name] = value;
} }
}, },
}); });
export default useCacheStore;

View File

@@ -0,0 +1,87 @@
import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue';
import type { RouteRecordNormalized } from 'vue-router';
import { getMenuList } from '@/api/user/auth';
import { MenuState } from './types';
import router from '@/router';
export default defineStore('menu', {
state: (): MenuState => ({
serverMenus: [],
menuFetched: false,
}),
getters: {
appMenus(state: MenuState): RouteRecordNormalized[] {
return state.serverMenus as unknown as RouteRecordNormalized[];
},
},
actions: {
// 加载菜单
async fetchMenu() {
try {
const { data } = await getMenuList();
// @ts-ignore
this.serverMenus = (data as Array<any>).map(s => {
// 转换
const convert = (item: any) => {
// 设置路由属性
const meta = {
locale: item.name,
icon: item.icon,
order: item.sort,
hideInMenu: item.visible === 0,
hideChildrenInMenu: item.visible === 0,
noAffix: item.visible === 0,
ignoreCache: item.cache === 0,
};
// 获取 router
const route = router.getRoutes().find(r => {
return r.name === item.component;
});
// 设置 router meta
if (route) {
// 路由配置覆盖菜单配置
route.meta = { ...meta, ...route.meta };
}
// 返回
return {
name: item.component,
path: item.path,
meta: meta,
children: undefined as unknown
};
};
// 构建父目录
const menu = convert(s);
// 构建子目录
if (s.children) {
menu.children = (s.children as Array<any>).map(convert);
}
return menu;
});
// 是否已加载过
this.menuFetched = true;
// 未配置菜单
if (this.serverMenus.length === 0) {
Notification.error({
content: '该用户未配置菜单, 请先联系管理员配置',
closable: true,
});
}
} catch (error) {
Notification.error({
content: '加载菜单失败',
closable: true,
});
}
},
// 清空菜单
clearMenu() {
this.serverMenus = [];
this.menuFetched = false;
},
},
});

View File

@@ -0,0 +1,6 @@
import type { RouteRecordNormalized } from 'vue-router';
export interface MenuState {
serverMenus: RouteRecordNormalized[];
menuFetched: boolean;
}

View File

@@ -3,7 +3,7 @@ import { DEFAULT_ROUTE_NAME, DEFAULT_TAB } from '@/router/constants';
import { isString } from '@/utils/is'; import { isString } from '@/utils/is';
import { TabBarState, TagProps } from './types'; import { TabBarState, TagProps } from './types';
const useTabBarStore = defineStore('tabBar', { export default defineStore('tabBar', {
state: (): TabBarState => ({ state: (): TabBarState => ({
cacheTabList: new Set([DEFAULT_ROUTE_NAME]), cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
tagList: [DEFAULT_TAB], tagList: [DEFAULT_TAB],
@@ -19,9 +19,7 @@ const useTabBarStore = defineStore('tabBar', {
}, },
actions: { actions: {
/** // 添加 tab
* 添加 tab
*/
addTab(tag: TagProps, ignoreCache: boolean) { addTab(tag: TagProps, ignoreCache: boolean) {
this.tagList.push(tag); this.tagList.push(tag);
if (!ignoreCache) { if (!ignoreCache) {
@@ -29,31 +27,23 @@ const useTabBarStore = defineStore('tabBar', {
} }
}, },
/** // 移除 tab
* 移除 tab
*/
deleteTab(idx: number, tag: TagProps) { deleteTab(idx: number, tag: TagProps) {
this.tagList.splice(idx, 1); this.tagList.splice(idx, 1);
this.cacheTabList.delete(tag.name); this.cacheTabList.delete(tag.name);
}, },
/** // 添加缓存
* 添加缓存
*/
addCache(name: string) { addCache(name: string) {
if (isString(name) && name !== '') this.cacheTabList.add(name); if (isString(name) && name !== '') this.cacheTabList.add(name);
}, },
/** // 删除缓存
* 删除缓存
*/
deleteCache(tag: TagProps) { deleteCache(tag: TagProps) {
this.cacheTabList.delete(tag.name); this.cacheTabList.delete(tag.name);
}, },
/** // 重设缓存
* 重设缓存
*/
freshTabList(tags: TagProps[]) { freshTabList(tags: TagProps[]) {
this.tagList = tags; this.tagList = tags;
this.cacheTabList.clear(); this.cacheTabList.clear();
@@ -63,9 +53,7 @@ const useTabBarStore = defineStore('tabBar', {
.forEach((x) => this.cacheTabList.add(x)); .forEach((x) => this.cacheTabList.add(x));
}, },
/** // 重设 tab
* 重设 tab
*/
resetTabList() { resetTabList() {
this.tagList = [DEFAULT_TAB]; this.tagList = [DEFAULT_TAB];
this.cacheTabList.clear(); this.cacheTabList.clear();
@@ -73,5 +61,3 @@ const useTabBarStore = defineStore('tabBar', {
}, },
}, },
}); });
export default useTabBarStore;

View File

@@ -4,9 +4,9 @@ import { clearToken, setToken } from '@/utils/auth';
import { md5 } from '@/utils'; import { md5 } from '@/utils';
import { removeRouteListener } from '@/utils/route-listener'; import { removeRouteListener } from '@/utils/route-listener';
import { UserState } from './types'; import { UserState } from './types';
import useAppStore from '../app'; import { useMenuStore, useTabBarStore } from '@/store';
const useUserStore = defineStore('user', { export default defineStore('user', {
state: (): UserState => ({ state: (): UserState => ({
id: undefined, id: undefined,
username: undefined, username: undefined,
@@ -23,16 +23,12 @@ const useUserStore = defineStore('user', {
}, },
actions: { actions: {
/** // 设置用户信息
* 设置用户信息
*/
setInfo(partial: Partial<UserState>) { setInfo(partial: Partial<UserState>) {
this.$patch(partial); this.$patch(partial);
}, },
/** // 获取用户信息
* 获取用户信息
*/
async info() { async info() {
const { data } = await getUserPermission(); const { data } = await getUserPermission();
this.setInfo({ this.setInfo({
@@ -45,9 +41,7 @@ const useUserStore = defineStore('user', {
}); });
}, },
/** // 登录
* 登录
*/
async login(loginForm: LoginRequest) { async login(loginForm: LoginRequest) {
try { try {
const loginRequest: LoginRequest = { const loginRequest: LoginRequest = {
@@ -64,9 +58,7 @@ const useUserStore = defineStore('user', {
} }
}, },
/** // 登出
* 登出
*/
async logout() { async logout() {
try { try {
await userLogout(); await userLogout();
@@ -77,19 +69,18 @@ const useUserStore = defineStore('user', {
} }
}, },
/** // 登出回调
* 登出回调
*/
logoutCallBack() { logoutCallBack() {
this.$reset(); this.$reset();
clearToken(); clearToken();
// 移除路由监听器 // 移除路由监听器
removeRouteListener(); removeRouteListener();
// 清空菜单 // 清空菜单
const appStore = useAppStore(); const menuStore = useMenuStore();
appStore.clearMenu(); menuStore.clearMenu();
// 清除 tabs
const tabBarStore = useTabBarStore();
tabBarStore.resetTabList();
}, },
}, },
}); });
export default useUserStore;

View File

@@ -21,7 +21,7 @@
<a-input v-model="formModel.nickname" placeholder="请输入花名" allow-clear /> <a-input v-model="formModel.nickname" placeholder="请输入花名" allow-clear />
</a-form-item> </a-form-item>
<!-- 用户状态 --> <!-- 用户状态 -->
<a-form-item field="status" label="用户状态 " label-col-flex="50px"> <a-form-item field="status" label="用户状态" label-col-flex="50px">
<a-select <a-select
v-model="formModel.status" v-model="formModel.status"
:options="toOptions(UserStatusEnum)" :options="toOptions(UserStatusEnum)"