将 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 {
display: inline-block;
margin-right: 4px;
margin-bottom: 2px;
width: 6px;
height: 6px;
border-radius: 50%;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
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';
export default function usePermission() {
const appStore = useAppStore();
const menuStore = useMenuStore();
const userStore = useUserStore();
return {
/**
@@ -15,7 +15,7 @@ export default function usePermission() {
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;
while (menuConfig.length && !exist) {
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 localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
import localeSettings from './zh-CN/settings';
export default {
'navbar.action.locale': '切换为中文',
...localeSettings,
...localeMessageBox,
...localeLogin,
...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 NProgress from 'nprogress';
import { useAppStore } from '@/store';
import { useMenuStore } from '@/store';
import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants';
import usePermission from '@/hooks/permission';
export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore();
const menuStore = useMenuStore();
// 未加载菜单 并且 不在白名单内 则加载菜单
if (
!appStore.menuFetched &&
!menuStore.menuFetched &&
!WHITE_ROUTER_LIST.find((el) => el.name === to.name)
) {
// 加载菜单
await appStore.fetchMenuConfig();
await menuStore.fetchMenu();
}
// 检测是否可以访问
const permission = usePermission();
const access = permission.accessRouter(to);
// 刚进入页面时 重定向的 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;
}

View File

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

View File

@@ -1,15 +1,10 @@
import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue';
import type { RouteRecordNormalized } from 'vue-router';
import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/user/auth';
import { AppState } from './types';
import router from '@/router';
const useAppStore = defineStore('app', {
export default defineStore('app', {
state: (): AppState => ({
...defaultSettings,
menuFetched: false,
}),
getters: {
@@ -19,23 +14,16 @@ const useAppStore = defineStore('app', {
appDevice(state: AppState) {
return state.device;
},
appAsyncMenus(state: AppState): RouteRecordNormalized[] {
return state.serverMenu as unknown as RouteRecordNormalized[];
},
},
actions: {
/**
* 更新配置
*/
// 更新配置
updateSettings(partial: Partial<AppState>) {
// @ts-ignore-next-line
this.$patch(partial);
this.$patch(partial as object);
console.log(partial);
},
/**
* 修改颜色主题
*/
// 修改颜色主题
toggleTheme(dark: boolean) {
if (dark) {
this.theme = 'dark';
@@ -46,90 +34,14 @@ const useAppStore = defineStore('app', {
}
},
/**
* 切换设备
*/
// 切换设备
toggleDevice(device: string) {
this.device = device;
},
/**
* 切换菜单状态
*/
// 切换菜单状态
toggleMenu(value: boolean) {
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 {
theme: string;
colorWeak: boolean;
@@ -9,13 +7,10 @@ export interface AppState {
hideMenu: boolean;
menuCollapse: boolean;
footer: boolean;
themeColor: string;
menuWidth: number;
globalSettings: boolean;
device: string;
tabBar: boolean;
serverMenu: RouteRecordNormalized[];
menuFetched: boolean;
[key: string]: unknown;
}

View File

@@ -3,7 +3,7 @@ import { CacheState } from './types';
export type CacheType = 'menus' | 'roles' | 'tags' | 'hostKeys' | 'hostIdentities'
const useCacheStore = defineStore('cache', {
export default defineStore('cache', {
state: (): CacheState => ({
menus: [],
roles: [],
@@ -15,13 +15,9 @@ const useCacheStore = defineStore('cache', {
getters: {},
actions: {
/**
* 设置
*/
// 设置
set(name: CacheType, value: any) {
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 { TabBarState, TagProps } from './types';
const useTabBarStore = defineStore('tabBar', {
export default defineStore('tabBar', {
state: (): TabBarState => ({
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
tagList: [DEFAULT_TAB],
@@ -19,9 +19,7 @@ const useTabBarStore = defineStore('tabBar', {
},
actions: {
/**
* 添加 tab
*/
// 添加 tab
addTab(tag: TagProps, ignoreCache: boolean) {
this.tagList.push(tag);
if (!ignoreCache) {
@@ -29,31 +27,23 @@ const useTabBarStore = defineStore('tabBar', {
}
},
/**
* 移除 tab
*/
// 移除 tab
deleteTab(idx: number, tag: TagProps) {
this.tagList.splice(idx, 1);
this.cacheTabList.delete(tag.name);
},
/**
* 添加缓存
*/
// 添加缓存
addCache(name: string) {
if (isString(name) && name !== '') this.cacheTabList.add(name);
},
/**
* 删除缓存
*/
// 删除缓存
deleteCache(tag: TagProps) {
this.cacheTabList.delete(tag.name);
},
/**
* 重设缓存
*/
// 重设缓存
freshTabList(tags: TagProps[]) {
this.tagList = tags;
this.cacheTabList.clear();
@@ -63,9 +53,7 @@ const useTabBarStore = defineStore('tabBar', {
.forEach((x) => this.cacheTabList.add(x));
},
/**
* 重设 tab
*/
// 重设 tab
resetTabList() {
this.tagList = [DEFAULT_TAB];
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 { removeRouteListener } from '@/utils/route-listener';
import { UserState } from './types';
import useAppStore from '../app';
import { useMenuStore, useTabBarStore } from '@/store';
const useUserStore = defineStore('user', {
export default defineStore('user', {
state: (): UserState => ({
id: undefined,
username: undefined,
@@ -23,16 +23,12 @@ const useUserStore = defineStore('user', {
},
actions: {
/**
* 设置用户信息
*/
// 设置用户信息
setInfo(partial: Partial<UserState>) {
this.$patch(partial);
},
/**
* 获取用户信息
*/
// 获取用户信息
async info() {
const { data } = await getUserPermission();
this.setInfo({
@@ -45,9 +41,7 @@ const useUserStore = defineStore('user', {
});
},
/**
* 登录
*/
// 登录
async login(loginForm: LoginRequest) {
try {
const loginRequest: LoginRequest = {
@@ -64,9 +58,7 @@ const useUserStore = defineStore('user', {
}
},
/**
* 登出
*/
// 登出
async logout() {
try {
await userLogout();
@@ -77,19 +69,18 @@ const useUserStore = defineStore('user', {
}
},
/**
* 登出回调
*/
// 登出回调
logoutCallBack() {
this.$reset();
clearToken();
// 移除路由监听器
removeRouteListener();
// 清空菜单
const appStore = useAppStore();
appStore.clearMenu();
const menuStore = useMenuStore();
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-form-item>
<!-- 用户状态 -->
<a-form-item field="status" label="用户状态 " label-col-flex="50px">
<a-form-item field="status" label="用户状态" label-col-flex="50px">
<a-select
v-model="formModel.status"
:options="toOptions(UserStatusEnum)"