This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export interface MenuAccessIdentity {
|
||||
currentRole: string | null;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export function hasRoleAccess(roles: string[], currentRole: string | null): boolean {
|
||||
return roles.length === 0 || currentRole === 'SYSTEM_ADMIN' || roles.includes(currentRole || '');
|
||||
}
|
||||
|
||||
export function hasPermissionAccess(
|
||||
currentRole: string | null,
|
||||
permissions: string[],
|
||||
required: string | string[],
|
||||
): boolean {
|
||||
if (currentRole === 'SYSTEM_ADMIN') return true;
|
||||
return Array.isArray(required)
|
||||
? required.some((permission) => permissions.includes(permission))
|
||||
: permissions.includes(required);
|
||||
}
|
||||
|
||||
export function canAccess(route: RouteRecordRaw, user: MenuAccessIdentity): boolean {
|
||||
const meta = route.meta || {};
|
||||
const roles = (meta.roleCodes || []) as string[];
|
||||
const permission = meta.permission as string | string[] | undefined;
|
||||
if (!hasRoleAccess(roles, user.currentRole)) return false;
|
||||
if (permission && !hasPermissionAccess(user.currentRole, user.permissions, permission)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function childLocation(parentPath: string, childPath: string): string {
|
||||
if (childPath.startsWith('/')) return childPath;
|
||||
if (!childPath) return parentPath;
|
||||
return `${parentPath}/${childPath}`.replace(/\/{2,}/g, '/');
|
||||
}
|
||||
|
||||
export function filterMenu(routes: RouteRecordRaw[], user: MenuAccessIdentity): RouteRecordRaw[] {
|
||||
return routes
|
||||
.map((route) => {
|
||||
const children = route.children ? filterMenu(route.children, user) : [];
|
||||
const selfAllowed = canAccess(route, user);
|
||||
if (route.children && children.length === 0) return null;
|
||||
if (!route.children && !selfAllowed) return null;
|
||||
const copy: RouteRecordRaw = {
|
||||
...route,
|
||||
meta: route.meta ? { ...route.meta } : undefined,
|
||||
...(route.children ? { children } : {}),
|
||||
} as RouteRecordRaw;
|
||||
if (route.children) {
|
||||
const firstChild = children[0];
|
||||
if (firstChild) copy.redirect = childLocation(route.path, firstChild.path);
|
||||
}
|
||||
return copy;
|
||||
})
|
||||
.filter((route): route is RouteRecordRaw => route !== null);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import type { NotificationItem } from '@/types/interface';
|
||||
|
||||
const getMsgData = (): NotificationItem[] => [];
|
||||
|
||||
type MsgDataType = ReturnType<typeof getMsgData>;
|
||||
|
||||
export const useNotificationStore = defineStore('notification', {
|
||||
state: () => ({
|
||||
msgData: getMsgData(),
|
||||
}),
|
||||
getters: {
|
||||
unreadMsg: (state) => state.msgData.filter((item: NotificationItem) => item.status),
|
||||
readMsg: (state) => state.msgData.filter((item: NotificationItem) => !item.status),
|
||||
},
|
||||
actions: {
|
||||
setMsgData(data: MsgDataType) {
|
||||
this.msgData = data;
|
||||
},
|
||||
refreshMsgData() {
|
||||
this.msgData = getMsgData();
|
||||
},
|
||||
},
|
||||
persist: false,
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// 前端 roles 控制菜单权限 通过登录后的角色对菜单就行过滤处理
|
||||
// 如果需要前端 roles 控制菜单权限 请使用此文件代码替换 permission.ts 的内容
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { defineStore } from 'pinia';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import router, { allRoutes } from '@/router';
|
||||
import { store } from '@/store';
|
||||
|
||||
function filterPermissionsRouters(routes: Array<RouteRecordRaw>, roles: Array<unknown>) {
|
||||
const res: Array<RouteRecordRaw> = [];
|
||||
const removeRoutes: Array<RouteRecordRaw> = [];
|
||||
routes.forEach((route) => {
|
||||
const children: Array<RouteRecordRaw> = [];
|
||||
route.children?.forEach((childRouter) => {
|
||||
const roleCode = childRouter.meta?.roleCode || childRouter.name;
|
||||
if (roles.includes(roleCode)) {
|
||||
children.push(childRouter);
|
||||
} else {
|
||||
removeRoutes.push(childRouter);
|
||||
}
|
||||
});
|
||||
if (children.length > 0) {
|
||||
route.children = children;
|
||||
res.push(route);
|
||||
}
|
||||
});
|
||||
return { accessedRouters: res, removeRoutes };
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: () => ({
|
||||
whiteListRouters: ['/login'],
|
||||
routers: [] as Array<RouteRecordRaw>,
|
||||
removeRoutes: [] as Array<RouteRecordRaw>,
|
||||
}),
|
||||
actions: {
|
||||
async initRoutes(roles: Array<unknown>) {
|
||||
let accessedRouters = [];
|
||||
|
||||
let removeRoutes: Array<RouteRecordRaw> = [];
|
||||
// special token
|
||||
if (roles.includes('all')) {
|
||||
accessedRouters = cloneDeep(allRoutes);
|
||||
} else {
|
||||
const res = filterPermissionsRouters(allRoutes, roles);
|
||||
accessedRouters = res.accessedRouters;
|
||||
removeRoutes = res.removeRoutes;
|
||||
}
|
||||
|
||||
this.routers = accessedRouters;
|
||||
this.removeRoutes = removeRoutes;
|
||||
|
||||
removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
if (router.hasRoute(item.name!)) {
|
||||
router.removeRoute(item.name!);
|
||||
}
|
||||
});
|
||||
},
|
||||
async restore() {
|
||||
this.removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function getPermissionStore() {
|
||||
return usePermissionStore(store);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { financeRoutes } from '@/router/modules/finance';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
import { filterMenu } from './menu-access';
|
||||
import { useTabsRouterStore } from './tabs-router';
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: () => ({
|
||||
whiteListRouters: ['/login', '/role-select'],
|
||||
routers: [] as RouteRecordRaw[],
|
||||
asyncRoutes: [] as RouteRecordRaw[],
|
||||
}),
|
||||
actions: {
|
||||
initRoutes() {
|
||||
const user = useUserStore();
|
||||
if (this.routers.length > 0) return this.routers;
|
||||
this.routers = filterMenu(financeRoutes, user);
|
||||
this.asyncRoutes = this.routers;
|
||||
return this.routers;
|
||||
},
|
||||
resetForIdentity() {
|
||||
this.routers = [];
|
||||
this.asyncRoutes = [];
|
||||
useTabsRouterStore().removeTabRouterList();
|
||||
},
|
||||
restoreRoutes() {
|
||||
this.resetForIdentity();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function getPermissionStore() {
|
||||
return usePermissionStore();
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import keys from 'lodash/keys';
|
||||
import { defineStore } from 'pinia';
|
||||
import { Color } from 'tvision-color';
|
||||
|
||||
import type { TColorSeries } from '@/config/color';
|
||||
import { DARK_CHART_COLORS, LIGHT_CHART_COLORS } from '@/config/color';
|
||||
import STYLE_CONFIG from '@/config/style';
|
||||
import { store } from '@/store';
|
||||
import type { ModeType } from '@/types/interface';
|
||||
import { generateColorMap, insertThemeStylesheet } from '@/utils/color';
|
||||
|
||||
const state: Record<string, any> = {
|
||||
...STYLE_CONFIG,
|
||||
showSettingPanel: false,
|
||||
colorList: {} as TColorSeries,
|
||||
chartColors: LIGHT_CHART_COLORS,
|
||||
};
|
||||
|
||||
export type TState = typeof state;
|
||||
export type TStateKey = keyof typeof state;
|
||||
|
||||
export const useSettingStore = defineStore('setting', {
|
||||
state: () => state,
|
||||
getters: {
|
||||
showSidebar: (state) => state.layout !== 'top',
|
||||
showSidebarLogo: (state) => state.layout === 'side',
|
||||
showHeaderLogo: (state) => state.layout !== 'side',
|
||||
displayMode: (state): ModeType => {
|
||||
if (state.mode === 'auto') {
|
||||
const media = window.matchMedia('(prefers-color-scheme:dark)');
|
||||
if (media.matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
return state.mode as ModeType;
|
||||
},
|
||||
displaySideMode: (state): ModeType => {
|
||||
return state.sideMode as ModeType;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async changeMode(mode: ModeType | 'auto') {
|
||||
let theme = mode;
|
||||
|
||||
if (mode === 'auto') {
|
||||
theme = this.getMediaColor();
|
||||
}
|
||||
const isDarkMode = theme === 'dark';
|
||||
|
||||
document.documentElement.setAttribute('theme-mode', isDarkMode ? 'dark' : '');
|
||||
|
||||
this.chartColors = isDarkMode ? DARK_CHART_COLORS : LIGHT_CHART_COLORS;
|
||||
},
|
||||
async changeSideMode(mode: ModeType) {
|
||||
const isDarkMode = mode === 'dark';
|
||||
|
||||
document.documentElement.setAttribute('side-mode', isDarkMode ? 'dark' : '');
|
||||
},
|
||||
getMediaColor() {
|
||||
const media = window.matchMedia('(prefers-color-scheme:dark)');
|
||||
|
||||
if (media.matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
},
|
||||
changeBrandTheme(brandTheme: string) {
|
||||
const mode = this.displayMode;
|
||||
// 以主题色加显示模式作为键
|
||||
const colorKey = `${brandTheme}[${mode}]`;
|
||||
let colorMap = this.colorList[colorKey];
|
||||
// 如果不存在色阶,就需要计算
|
||||
if (colorMap === undefined) {
|
||||
const [{ colors: newPalette, primary: brandColorIndex }] = Color.getColorGradations({
|
||||
colors: [brandTheme],
|
||||
step: 10,
|
||||
remainInput: false, // 是否保留输入 不保留会矫正不合适的主题色
|
||||
});
|
||||
colorMap = generateColorMap(brandTheme, newPalette, mode, brandColorIndex);
|
||||
this.colorList[colorKey] = colorMap;
|
||||
}
|
||||
// TODO 需要解决不停切换时有反复插入 style 的问题
|
||||
insertThemeStylesheet(brandTheme, colorMap, mode);
|
||||
document.documentElement.setAttribute('theme-color', brandTheme);
|
||||
},
|
||||
updateConfig(payload: Partial<TState>) {
|
||||
for (const key in payload) {
|
||||
if (payload[key as TStateKey] !== undefined) {
|
||||
this[key as TStateKey] = payload[key as TStateKey];
|
||||
}
|
||||
if (key === 'mode') {
|
||||
this.changeMode(payload[key] as ModeType);
|
||||
}
|
||||
if (key === 'sideMode') {
|
||||
this.changeSideMode(payload[key] as ModeType);
|
||||
}
|
||||
if (key === 'brandTheme') {
|
||||
this.changeBrandTheme(payload[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
pick: [...keys(STYLE_CONFIG), 'colorList', 'chartColors'],
|
||||
},
|
||||
});
|
||||
|
||||
export function getSettingStore() {
|
||||
return useSettingStore(store);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { store } from '@/store';
|
||||
import type { TRouterInfo, TTabRouterType } from '@/types/interface';
|
||||
|
||||
const homeRoute: Array<TRouterInfo> = [
|
||||
{
|
||||
path: '/role-select',
|
||||
routeIdx: 0,
|
||||
title: { zh_CN: '选择身份', en_US: 'Select role' },
|
||||
name: 'RoleSelect',
|
||||
isHome: true,
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
tabRouterList: homeRoute,
|
||||
isRefreshing: false,
|
||||
};
|
||||
|
||||
// 不需要做多标签tabs页缓存的列表 值为每个页面对应的name 如 DashboardDetail
|
||||
// const ignoreCacheRoutes = ['DashboardDetail'];
|
||||
const ignoreCacheRoutes = ['login'];
|
||||
|
||||
export const useTabsRouterStore = defineStore('tabsRouter', {
|
||||
state: () => state,
|
||||
getters: {
|
||||
tabRouters: (state: TTabRouterType) => state.tabRouterList,
|
||||
refreshing: (state: TTabRouterType) => state.isRefreshing,
|
||||
},
|
||||
actions: {
|
||||
// 处理刷新
|
||||
toggleTabRouterAlive(routeIdx: number) {
|
||||
this.isRefreshing = !this.isRefreshing;
|
||||
this.tabRouters[routeIdx].isAlive = !this.tabRouters[routeIdx].isAlive;
|
||||
},
|
||||
// 处理新增
|
||||
appendTabRouterList(newRoute: TRouterInfo) {
|
||||
// 不要将判断条件newRoute.meta.keepAlive !== false修改为newRoute.meta.keepAlive,starter默认开启保活,所以meta.keepAlive未定义时也需要进行保活,只有显式说明false才禁用保活。
|
||||
const needAlive = !ignoreCacheRoutes.includes(newRoute.name as string) && newRoute.meta?.keepAlive !== false;
|
||||
if (!this.tabRouters.some((route: TRouterInfo) => route.path === newRoute.path)) {
|
||||
this.tabRouterList = this.tabRouterList.concat({ ...newRoute, isAlive: needAlive });
|
||||
}
|
||||
},
|
||||
// 处理关闭当前
|
||||
subtractCurrentTabRouter(newRoute: TRouterInfo) {
|
||||
const { routeIdx } = newRoute;
|
||||
if (routeIdx === undefined) return;
|
||||
this.tabRouterList = this.tabRouterList.slice(0, routeIdx).concat(this.tabRouterList.slice(routeIdx + 1));
|
||||
},
|
||||
// 处理关闭右侧
|
||||
subtractTabRouterBehind(newRoute: TRouterInfo) {
|
||||
const { routeIdx } = newRoute;
|
||||
if (routeIdx === undefined) return;
|
||||
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
|
||||
let tabRouterList: Array<TRouterInfo> = this.tabRouterList.slice(0, routeIdx + 1);
|
||||
if (routeIdx < homeIdx) {
|
||||
tabRouterList = tabRouterList.concat(homeRoute);
|
||||
}
|
||||
this.tabRouterList = tabRouterList;
|
||||
},
|
||||
// 处理关闭左侧
|
||||
subtractTabRouterAhead(newRoute: TRouterInfo) {
|
||||
const { routeIdx } = newRoute;
|
||||
if (routeIdx === undefined) return;
|
||||
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
|
||||
let tabRouterList: Array<TRouterInfo> = this.tabRouterList.slice(routeIdx);
|
||||
if (routeIdx > homeIdx) {
|
||||
tabRouterList = homeRoute.concat(tabRouterList);
|
||||
}
|
||||
this.tabRouterList = tabRouterList;
|
||||
},
|
||||
// 处理关闭其他
|
||||
subtractTabRouterOther(newRoute: TRouterInfo) {
|
||||
const { routeIdx } = newRoute;
|
||||
if (routeIdx === undefined) return;
|
||||
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
|
||||
this.tabRouterList = routeIdx === homeIdx ? homeRoute : homeRoute.concat([this.tabRouterList?.[routeIdx]]);
|
||||
},
|
||||
removeTabRouterList() {
|
||||
this.tabRouterList = [];
|
||||
},
|
||||
initTabRouterList(newRoutes: TRouterInfo[]) {
|
||||
newRoutes?.forEach((route: TRouterInfo) => this.appendTabRouterList(route));
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
});
|
||||
|
||||
export function getTabsRouterStore() {
|
||||
return useTabsRouterStore(store);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import type { RoleView, SessionView, UserView } from '@/api/auth';
|
||||
import * as authApi from '@/api/auth';
|
||||
import { usePermissionStore } from '@/store';
|
||||
|
||||
import { hasPermissionAccess } from './menu-access';
|
||||
|
||||
const emptyUser: UserView = {
|
||||
publicId: '',
|
||||
username: '',
|
||||
displayName: '',
|
||||
departmentName: '',
|
||||
mustChangePassword: false,
|
||||
};
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
authenticated: false,
|
||||
user: { ...emptyUser },
|
||||
roles: [] as RoleView[],
|
||||
currentRole: null as string | null,
|
||||
permissions: [] as string[],
|
||||
sessionExpiresAt: null as string | null,
|
||||
absoluteSessionExpiresAt: null as string | null,
|
||||
loading: false,
|
||||
}),
|
||||
getters: {
|
||||
displayName: (state) => state.user.displayName || state.user.username,
|
||||
roleName: (state) => state.roles.find((role) => role.code === state.currentRole)?.name || '',
|
||||
hasRole: (state) => (roleCode: string) => state.roles.some((role) => role.code === roleCode),
|
||||
hasPermission: (state) => (permission: string) =>
|
||||
hasPermissionAccess(state.currentRole, state.permissions, permission),
|
||||
},
|
||||
actions: {
|
||||
applySession(session: SessionView) {
|
||||
this.authenticated = session.authenticated;
|
||||
this.user = session.user || { ...emptyUser };
|
||||
this.roles = session.roles || [];
|
||||
this.currentRole = session.activeRole || null;
|
||||
this.permissions = session.permissions || [];
|
||||
this.sessionExpiresAt = session.sessionExpiresAt || null;
|
||||
this.absoluteSessionExpiresAt = session.absoluteSessionExpiresAt || null;
|
||||
},
|
||||
async login(payload: authApi.LoginRequest) {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.applySession(await authApi.login(payload));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async restoreSession() {
|
||||
if (this.authenticated) return this.user;
|
||||
const session = await authApi.getSession();
|
||||
this.applySession(session);
|
||||
if (!this.authenticated) throw new Error('Session is not authenticated');
|
||||
return this.user;
|
||||
},
|
||||
async refreshProfile() {
|
||||
this.user = await authApi.getProfile();
|
||||
return this.user;
|
||||
},
|
||||
async selectRole(roleCode: string) {
|
||||
const session = await authApi.selectRole(roleCode);
|
||||
this.applySession(session);
|
||||
const permissionStore = usePermissionStore();
|
||||
permissionStore.resetForIdentity();
|
||||
permissionStore.initRoutes();
|
||||
},
|
||||
async updatePassword(payload: authApi.PasswordChangeRequest) {
|
||||
this.applySession(await authApi.changePassword(payload));
|
||||
},
|
||||
async logout() {
|
||||
if (this.authenticated) {
|
||||
try {
|
||||
await authApi.logout();
|
||||
} finally {
|
||||
this.clearSession();
|
||||
}
|
||||
} else {
|
||||
this.clearSession();
|
||||
}
|
||||
},
|
||||
clearSession() {
|
||||
this.authenticated = false;
|
||||
this.user = { ...emptyUser };
|
||||
this.roles = [];
|
||||
this.currentRole = null;
|
||||
this.permissions = [];
|
||||
this.sessionExpiresAt = null;
|
||||
this.absoluteSessionExpiresAt = null;
|
||||
},
|
||||
},
|
||||
persist: false,
|
||||
});
|
||||
Reference in New Issue
Block a user