feat: deliver first finance preview with setup wizard
Release / release (push) Failing after 18s

This commit is contained in:
Qiufeng
2026-08-17 14:08:40 +08:00
parent 47403fd337
commit c8b0151769
558 changed files with 140774 additions and 3 deletions
+57
View File
@@ -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);
}
+37
View File
@@ -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();
}
+111
View File
@@ -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);
}
+92
View File
@@ -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);
}
+96
View File
@@ -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,
});