修改菜单逻辑.

This commit is contained in:
lijiahang
2023-08-02 17:08:40 +08:00
parent 4322f01354
commit 7915198be4
16 changed files with 230 additions and 94 deletions

View File

@@ -30,6 +30,9 @@
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
/**
* 跳转
*/
const goto = (item: RouteRecordRaw) => {
// 打开外链
if (regexUrl.test(item.path)) {
@@ -48,6 +51,7 @@
name: item.name,
});
};
const findMenuOpenKeys = (target: string) => {
const result: string[] = [];
let isFind = false;
@@ -69,9 +73,14 @@
});
return result;
};
/**
* 监听路由 设置打开的 key
*/
listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
// TODO
const { activeMenu, hideInMenu } = newRoute.meta;
if (!hideInMenu || activeMenu) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);
@@ -84,11 +93,14 @@
];
}
}, true);
// 展开菜单
const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop')
appStore.updateSettings({ menuCollapse: val });
};
// 渲染菜单
const renderSubMenu = () => {
function travel(_route: RouteRecordRaw[], nodes = []) {
if (_route) {
@@ -103,7 +115,8 @@
key={element?.name}
v-slots={{
icon,
title: () => h(compile(t(element?.meta?.locale || ''))),
// 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),
title: () => h(compile(element?.meta?.locale || '')),
}}
>
{travel(element?.children)}
@@ -114,7 +127,7 @@
v-slots={{ icon }}
onClick={() => goto(element)}
>
{t(element?.meta?.locale || '')}
{element?.meta?.locale || ''}
</a-menu-item>
);
nodes.push(node as never);
@@ -158,5 +171,14 @@
font-size: 18px;
}
}
.arco-menu-icon {
margin-right: 10px !important;
}
.arco-menu-indent-list {
width: 28px;
display: inline-block;
}
}
</style>

View File

@@ -4,7 +4,7 @@
<div class="tab-bar-box">
<div class="tab-bar-scroll">
<div class="tags-wrap">
<tab-item
<TabItem
v-for="(tag, index) in tagList"
:key="tag.fullPath"
:index="index"
@@ -19,14 +19,11 @@
</template>
<script lang="ts" setup>
import { ref, computed, watch, onUnmounted } from 'vue';
import { computed, onUnmounted, ref, watch } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import {
listenerRouteChange,
removeRouteListener,
} from '@/utils/route-listener';
import { listenerRouteChange, removeRouteListener, } from '@/utils/route-listener';
import { useAppStore, useTabBarStore } from '@/store';
import tabItem from './tab-item.vue';
import TabItem from './tab-item.vue';
const appStore = useAppStore();
const tabBarStore = useTabBarStore();
@@ -45,11 +42,19 @@
affixRef.value.updatePosition();
}
);
/**
* 监听路由变化
*/
// TODO
listenerRouteChange((route: RouteLocationNormalized) => {
console.log(route);
console.log(!route.meta.noAffix);
if (
!route.meta.noAffix &&
!route.meta.noAffix && // todo 改一下
!tagList.value.some((tag) => tag.fullPath === route.fullPath)
) {
console.log('updateTabList', route);
tabBarStore.updateTabList(route);
}
}, true);
@@ -66,7 +71,7 @@
.tab-bar-box {
display: flex;
padding: 0 0 0 20px;
padding: 0 0 0 6px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);

View File

@@ -2,20 +2,17 @@
<a-dropdown
trigger="contextMenu"
:popup-max-height="false"
@select="actionSelect"
>
@select="actionSelect">
<span
class="arco-tag arco-tag-size-medium arco-tag-checked"
:class="{ 'link-activated': itemData.fullPath === $route.fullPath }"
@click="goto(itemData)"
>
@click="goto(itemData)">
<span class="tag-link">
{{ $t(itemData.title) }}
{{ itemData.title }}
</span>
<span
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
@click.stop="tagClose(itemData, index)"
>
@click.stop="tagClose(itemData, index)">
<icon-close />
</span>
</span>
@@ -27,8 +24,7 @@
<a-doption
class="sperate-line"
:disabled="disabledCurrent"
:value="Eaction.current"
>
:value="Eaction.current">
<icon-close />
<span>关闭当前标签页</span>
</a-doption>
@@ -39,8 +35,7 @@
<a-doption
class="sperate-line"
:disabled="disabledRight"
:value="Eaction.right"
>
:value="Eaction.right">
<icon-to-right />
<span>关闭右侧标签页</span>
</a-doption>
@@ -113,6 +108,9 @@
return props.index === tagList.value.length - 1;
});
/**
* 关闭 tag
*/
const tagClose = (tag: TagProps, idx: number) => {
tabBarStore.deleteTab(idx, tag);
if (props.itemData.fullPath === route.fullPath) {
@@ -124,12 +122,15 @@
const findCurrentRouteIndex = () => {
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
};
const actionSelect = async (value: any) => {
const { itemData, index } = props;
const copyTagList = [...tagList.value];
if (value === Eaction.current) {
// 关闭当前
tagClose(itemData, index);
} else if (value === Eaction.left) {
// 关闭左侧
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(1, props.index - 1);
@@ -138,6 +139,7 @@
router.push({ name: itemData.name });
}
} else if (value === Eaction.right) {
// 关闭右侧
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(props.index + 1);
@@ -146,14 +148,15 @@
router.push({ name: itemData.name });
}
} else if (value === Eaction.others) {
// 关闭其他
const filterList = tagList.value.filter((el, idx) => {
return idx === 0 || idx === props.index;
});
tabBarStore.freshTabList(filterList);
router.push({ name: itemData.name });
} else if (value === Eaction.reload) {
// 重新加载
tabBarStore.deleteCache(itemData);
console.log(route.fullPath);
await router.push({
name: REDIRECT_ROUTE_NAME,
params: {
@@ -162,6 +165,7 @@
});
tabBarStore.addCache(itemData.name);
} else {
// 关闭全部
tabBarStore.resetTabList();
router.push({ name: DEFAULT_ROUTE_NAME });
}

View File

@@ -4,26 +4,46 @@ import { useUserStore } from '@/store';
export default function usePermission() {
const userStore = useUserStore();
return {
// TODO test
/**
* 是否可访问路由
*/
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
return (
!route.meta?.requiresAuth ||
!route.meta?.permission ||
userStore.permission?.includes(route.meta?.permission)
);
// route.name
// TODO
},
findFirstPermissionRoute(_routers: any, permission: string) {
const cloneRouters = [..._routers];
while (cloneRouters.length) {
const firstElement = cloneRouters.shift();
if (firstElement?.meta?.permission === permission) {
return { name: firstElement.name };
}
if (firstElement?.children) {
cloneRouters.push(...firstElement.children);
}
}
return null;
/**
* 是否有权限
*/
hasPermission(permission: string) {
return userStore.permission?.includes('*') ||
userStore.permission?.includes(permission);
},
/**
* 是否有权限
*/
hasAnyPermission(permission: string[]) {
return userStore.permission?.includes('*') ||
permission.map(s => userStore.permission?.includes(s))
.filter(Boolean).length > 0;
},
/**
* 是否有角色
*/
hasRole(role: string) {
return userStore.roles?.includes('admin') ||
userStore.roles?.includes(role);
},
/**
* 是否有角色
*/
hasAnyRole(role: string[]) {
return userStore.roles?.includes('*') ||
role.map(s => userStore.roles?.includes(s))
.filter(Boolean).length > 0;
}
};
}

View File

@@ -1,3 +1,6 @@
import { RouteLocationNormalized } from 'vue-router';
import { TagProps } from '@/store/modules/tab-bar/types';
export const REDIRECT_ROUTE_NAME = 'redirect';
export const LOGIN_ROUTE_NAME = 'login';
@@ -10,15 +13,6 @@ export const DEFAULT_ROUTE_NAME = 'workplace';
export const DEFAULT_ROUTE_FULL_PATH = '/dashboard/workplace';
/**
* 默认 tab 页面
*/
export const DEFAULT_TAB = {
title: 'menu.dashboard.workplace',
name: DEFAULT_ROUTE_NAME,
fullPath: DEFAULT_ROUTE_FULL_PATH,
};
/**
* 路由白名单
*/
@@ -28,3 +22,29 @@ export const WHITE_ROUTER_LIST = [
{ name: FORBIDDEN_ROUTER_NAME, children: [] },
{ name: REDIRECT_ROUTE_NAME, children: [] },
];
/**
* 默认 tab 页面
*/
export const DEFAULT_TAB = {
title: '工作台',
name: DEFAULT_ROUTE_NAME,
fullPath: DEFAULT_ROUTE_FULL_PATH,
};
/**
* router 转 tag
*/
// TODO 获取后端meta
export const routerToTag = (route: RouteLocationNormalized): TagProps => {
console.log(route);
// TODO 还是得需要 name 和 meta 的映射
const { name, meta, fullPath, query } = route;
return {
title: meta.locale || 'me',
name: String(name),
fullPath,
query,
ignoreCache: meta.ignoreCache,
};
};

View File

@@ -1,23 +1,21 @@
import type { Router, RouteRecordNormalized } from 'vue-router';
import NProgress from 'nprogress';
import usePermission from '@/hooks/permission';
import { useAppStore } from '@/store';
import { WHITE_ROUTER_LIST, NOT_FOUND_ROUTER_NAME, FORBIDDEN_ROUTER_NAME } from '../constants';
import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants';
export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore();
const permission = usePermission();
// 未加载菜单 并且 未从白名单中找到 to.name
// 未加载菜单 并且 不在白名单内 则加载菜单
if (
!appStore.appAsyncMenus.length &&
!appStore.menuFetched &&
!WHITE_ROUTER_LIST.find((el) => el.name === to.name)
) {
// 加载菜单
await appStore.fetchMenuConfig();
}
// 检查路由是否存在
// 检查路由是否存在于授权路由中
const menuConfig = [...appStore.appAsyncMenus, ...WHITE_ROUTER_LIST];
let exist = false;
while (menuConfig.length && !exist) {
@@ -30,14 +28,9 @@ export default function setupPermissionGuard(router: Router) {
);
}
}
// 检查是否有权限
const permissionsAllow = permission.accessRouter(to);
if (!exist) {
// 页面不存在
next({ name: NOT_FOUND_ROUTER_NAME });
} else if (!permissionsAllow) {
// 无权限
next({ name: FORBIDDEN_ROUTER_NAME });
} else {
// 正常跳转
next();

View File

@@ -43,6 +43,7 @@ export const REDIRECT_ROUTER: RouteRecordRaw = {
component: () => import('@/views/redirect/index.vue'),
meta: {
hideInMenu: true,
noAffix: true
},
},
],

View File

@@ -3,11 +3,12 @@ import { AppRouteRecordRaw } from '../types';
const DASHBOARD: AppRouteRecordRaw = {
name: 'dashboard',
path: '/dashboard',
component: DEFAULT_LAYOUT,
children: [
{
path: '/dashboard/workplace',
name: 'workplace',
path: '/dashboard/workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
},
],

View File

@@ -3,6 +3,7 @@ import { AppRouteRecordRaw } from '../types';
const USER: AppRouteRecordRaw = {
name: 'user',
path: '/user',
component: DEFAULT_LAYOUT,
children: [
{
@@ -14,6 +15,9 @@ const USER: AppRouteRecordRaw = {
path: '/user/userChild2',
name: 'userChild2',
component: () => import('@/views/user/child2/index.vue'),
meta: {
noAffix: true
}
},
],
};

View File

@@ -1,22 +1,25 @@
import 'vue-router';
/**
* 前端覆盖后端
*/
declare module 'vue-router' {
interface RouteMeta {
// 后端赋值
// 图标
icon?: string;
// 后端赋值
// 名称
locale?: string;
// 后端赋值
// 排序
order?: number;
// 后端赋值 是否隐藏菜单
// 是否隐藏菜单
hideInMenu?: boolean;
// 后端赋值 是否隐藏子菜单
// 是否隐藏子菜单
hideChildrenInMenu?: boolean;
// 后端赋值 是否添加到 tab
// 是否添加到 tab
noAffix?: boolean;
// 前端赋值 是否忽略缓存
// 是否忽略缓存
ignoreCache?: boolean;
// 不赋值
// 是否活跃
activeMenu?: string;
}
}

View File

@@ -2,11 +2,15 @@ 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';
import { getMenuList } from '@/api/user/auth';
import { AppState } from './types';
import router from '@/router';
const useAppStore = defineStore('app', {
state: (): AppState => ({ ...defaultSettings }),
state: (): AppState => ({
...defaultSettings,
menuFetched: false,
}),
getters: {
appCurrentSetting(state: AppState): AppState {
@@ -62,7 +66,54 @@ const useAppStore = defineStore('app', {
async fetchMenuConfig() {
try {
const { data } = await getMenuList();
this.serverMenu = data;
// @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: '加载菜单失败',
@@ -76,6 +127,7 @@ const useAppStore = defineStore('app', {
*/
clearMenu() {
this.serverMenu = [];
this.menuFetched = false;
},
},
});

View File

@@ -1,6 +1,6 @@
import type { RouteLocationNormalized } from 'vue-router';
import { defineStore } from 'pinia';
import { DEFAULT_TAB, DEFAULT_ROUTE_NAME, BAN_TAB_LIST, routerToTag } from '@/router/constants';
import { DEFAULT_TAB, DEFAULT_ROUTE_NAME, routerToTag } from '@/router/constants';
import { isString } from '@/utils/is';
import { TabBarState, TagProps } from './types';
@@ -24,7 +24,6 @@ const useTabBarStore = defineStore('tabBar', {
* 添加 tab
*/
updateTabList(route: RouteLocationNormalized) {
if (BAN_TAB_LIST.includes(route.name as string)) return;
this.tagList.push(routerToTag(route));
if (!route.meta.ignoreCache) {
this.cacheTabList.add(route.name as string);

View File

@@ -1,6 +1,5 @@
import { defineStore } from 'pinia';
import { getUserInfo } from '@/api/user';
import { login as userLogin, LoginRequest, logout as userLogout, } from '@/api/user/auth';
import { getUserPermission, login as userLogin, LoginRequest, logout as userLogout } from '@/api/user/auth';
import { clearToken, setToken } from '@/utils/auth';
import { md5 } from '@/utils';
import { removeRouteListener } from '@/utils/route-listener';
@@ -13,8 +12,8 @@ const useUserStore = defineStore('user', {
username: undefined,
nickname: undefined,
avatar: undefined,
permission: undefined,
roles: undefined,
permission: undefined,
}),
getters: {
@@ -35,13 +34,14 @@ const useUserStore = defineStore('user', {
* 获取用户信息
*/
async info() {
const res = await getUserInfo();
const { data } = await getUserPermission();
this.setInfo({
id: 1,
username: 'admin',
nickname: '管理员',
permission: ['*'],
roles: ['admin'],
id: data.user.id,
username: data.user.username,
nickname: data.user.nickname,
avatar: data.user.avatar,
roles: data.roles,
permission: data.permissions,
});
},
@@ -54,7 +54,9 @@ const useUserStore = defineStore('user', {
username: loginForm.username,
password: md5(loginForm.password),
};
// 执行登陆
const res = await userLogin(loginRequest);
// 设置登陆 token
setToken(res.data.token);
} catch (err) {
clearToken();
@@ -69,6 +71,7 @@ const useUserStore = defineStore('user', {
try {
await userLogout();
} finally {
// 登出回调
this.logoutCallBack();
}
},

View File

@@ -1,7 +1,3 @@
/**
* Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management
* 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
*/
import mitt, { Handler } from 'mitt';
import type { RouteLocationNormalized } from 'vue-router';
@@ -13,9 +9,14 @@ let latestRoute: RouteLocationNormalized;
export function setRouteEmitter(to: RouteLocationNormalized) {
emitter.emit(key, to);
// TODO 这里寻找
latestRoute = to;
console.log('change', to);
}
/**
* 添加路由跳转监听器
*/
export function listenerRouteChange(
handler: (route: RouteLocationNormalized) => void,
immediate = true
@@ -26,6 +27,9 @@ export function listenerRouteChange(
}
}
/**
* 移除路由跳转监听器
*/
export function removeRouteListener() {
emitter.off(key);
}

View File

@@ -81,7 +81,9 @@
if (!errors) {
setLoading(true);
try {
// 执行登陆
await userStore.login(values as LoginData);
// 跳转路由
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: (redirect as string) || 'workplace',

View File

@@ -2,13 +2,16 @@
<div>
<p>UserChild 2</p>
<h1 v-permission="['admin']">123</h1>
<button @click="red">red</button>
</div>
</template>
<script>
export default {
name: 'UserChild2',
};
<script lang="ts" setup>
import router from '@/router';
function red() {
router.push({ name: 'workplace' });
}
</script>
<style scoped>