kboss/f/web-kboss/src/store/modules/permission.js
2026-06-16 11:50:02 +08:00

537 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { asyncRoutes, constantRoutes } from "@/router";
// 用浏览器 UA 判断当前是不是手机端,后面会按 PC / 手机过滤菜单。
const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
// 项目里用到的固定角色名,集中放这里,避免代码里到处写字符串。
const CUSTOMER_ROLE = '客户';
const OPERATION_ROLE = '运营';
const FINANCE_ROLE = '财务';
// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。
const SPECIAL_ORDER_USER = 'ZhipuHZ';
// 超级管理员只放行这个一级菜单。
const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator';
// 所有登录用户都能访问的公共路由,不依赖后端 auths 和角色。hidden 路由不会显示在菜单里。
const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/tokenUsage', '/modelExperience', '/modelDetail', '/modelApiDocument'];
// 运营角色需要额外补出来的菜单。
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/operationReport'];
// 财务角色需要额外补出来的菜单。
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
// 普通客户账号默认要补出来的基础菜单。
const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement'];
// 客户角色额外能看到的一级菜单。
const CUSTOMER_EXTRA_ROUTE_PATHS = [
'/containerInstance',
'/unsubscribeManagement',
'/informationPerfect',
'/rechargeManagement',
'/invoiceManagement',
'/workOrderManagement'
];
// 这些菜单只允许客户角色看到,非客户就算后端给了权限也不展示。
const CUSTOMER_ONLY_ROUTE_PATHS = [
'/overview',
...CUSTOMER_EXTRA_ROUTE_PATHS
];
// 客户登录后必须能看到的入口菜单,不完全依赖后端 auths 返回。
const CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS = ['/overview'];
// 订单管理里只给 SPECIAL_ORDER_USER 看的子菜单 path。
const ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER = ['HistoricalOrders', 'orderDetails'];
const isMobile = MOBILE_UA_REGEXP.test(window.navigator.userAgent);
// 把角色统一整理成数组,兼容 undefined、数组、逗号字符串这几种写法。
function normalizeRoles(roles) {
if (!roles) {
return [];
}
if (Array.isArray(roles)) {
return roles;
}
if (typeof roles === 'string') {
return roles.split(',').filter(Boolean);
}
return [];
}
// 从 sessionStorage 里取 roles取不到或格式坏了就当成没有角色。
function getSessionRoles() {
try {
return JSON.parse(sessionStorage.getItem('roles') || '[]');
} catch (error) {
console.warn('读取 roles 失败:', error);
return [];
}
}
// 汇总当前用户的所有角色来源接口参数、vuex、sessionStorage、新旧字段。
function getCurrentRoles(params, rootState) {
return [
...normalizeRoles(params.roles),
...normalizeRoles(rootState.user.roles),
...normalizeRoles(getSessionRoles()),
...normalizeRoles(sessionStorage.getItem('jueseNew'))
];
}
// 判断当前用户是不是客户角色。
function isCustomer(userRoles = []) {
return userRoles.includes(CUSTOMER_ROLE);
}
// 把布尔值转成更好读的设备类型,后面的判断都用 pc / mobile。
function getDeviceType(isMobileDevice) {
return isMobileDevice ? 'mobile' : 'pc';
}
// 判断路由 meta.roles 是否满足。没写 roles 的路由默认所有角色都能继续往下判断。
function hasRouteRole(route, userRoles = []) {
const routeRoles = route.meta?.roles;
if (!routeRoles || routeRoles.length === 0) {
return true;
}
return routeRoles.some(role => userRoles.includes(role));
}
// 根据设备过滤路由手机只要手机路由PC 不要手机专用路由。
function isRouteAllowedByDevice(route, deviceType) {
if (deviceType === 'mobile') {
return route.meta?.isMobile || route.meta?.isMobile === true;
}
return route.meta?.isMobile !== true;
}
// 在一组路由里按 path 找某个路由。
function findRouteByPath(routes, path) {
return routes.find(route => route.path === path);
}
// 后端 auths 里的 path 要和路由 meta.fullPath 对上,对上才算有权限。
function routeHasPermission(route, permissions) {
return permissions.some(permission => permission.path === route.meta?.fullPath);
}
// 客户专属菜单要再卡一层客户角色,防止非客户误展示。
function canShowCustomerOnlyRoute(route, userRoles) {
return !CUSTOMER_ONLY_ROUTE_PATHS.includes(route.path) || isCustomer(userRoles);
}
// 把所有动态路由的 fullPath 收集出来。后端返回 path 为空时,表示拥有全部权限。
function getAllRoutePermissions(routes) {
const permissions = [];
routes.forEach(route => {
if (route.meta?.fullPath) {
permissions.push({ path: route.meta.fullPath });
}
if (route.children) {
permissions.push(...getAllRoutePermissions(route.children));
}
});
return permissions;
}
// 复制路由对象。这里不能用 JSON 深拷贝,因为路由里的 component 是函数,会被 JSON 丢掉。
function cloneRoute(route) {
const clonedRoute = { ...route };
if (route.meta) {
clonedRoute.meta = { ...route.meta };
}
if (route.children) {
clonedRoute.children = route.children.map(cloneRoute);
}
return clonedRoute;
}
// 根据 path 列表批量找到对应路由,没找到的自动过滤掉。
function getRoutesByPath(routes, paths) {
return paths
.map(path => findRouteByPath(routes, path))
.filter(Boolean);
}
// 判断这个一级路由是不是已经加过了,避免菜单重复出现。
function shouldAppendRoute(accessedRoutes, route) {
return !accessedRoutes.some(item => item.path === route.path);
}
// 把缺少的路由补到最终菜单里,补之前会先去重并复制一份。
function appendMissingRoutes(accessedRoutes, routesToAppend) {
routesToAppend.forEach(route => {
if (shouldAppendRoute(accessedRoutes, route)) {
accessedRoutes.push(cloneRoute(route));
}
});
return accessedRoutes;
}
// 如果是手机访问,额外把根路径导到 H5 首页,并注册 H5 首页菜单。
if (isMobile) {
console.log("检测到移动设备,添加移动端路由");
constantRoutes.unshift({
path: '/',
redirect: '/h5HomePage',
hidden: true
});
constantRoutes.push({
path: '/h5HomePage',
name: 'H5HomePage',
title: 'H5首页',
component: () => import('@/views/H5/index.vue'),
hidden: true,
redirect: "/h5HomePage/index",
meta: { isMobile: true },
children: [
{
path: "index",
title: 'H5首页',
component: () => import('@/views/H5/official/index.vue'),
meta: {
title: "H5首页",
fullPath: "/h5HomePage/index",
isMobile: true
},
},
{
path: "cloud",
title: '云',
component: () => import('@/views/H5/cloud/index.vue'),
meta: {
title: "云",
fullPath: "/h5HomePage/cloud",
isMobile: true
},
},
{
path: "calculate",
title: '算',
component: () => import('@/views/H5/calculate/index.vue'),
meta: {
title: "算",
fullPath: "/h5HomePage/calculate",
isMobile: true
},
},
{
path: "net",
title: '网',
component: () => import('@/views/H5/net/index.vue'),
meta: {
title: "网",
fullPath: "/h5HomePage/net",
isMobile: true
},
},
{
path: "use",
title: '用',
component: () => import('@/views/H5/use/index.vue'),
meta: {
title: "用",
fullPath: "/h5HomePage/use",
isMobile: true
},
},
]
});
}
// 核心过滤函数:拿后端权限、角色和设备类型,一层层筛出最终可访问路由。
function filterAsyncRoutes(routes, permissions, userRoles = [], deviceType = 'pc') {
const res = [];
routes.forEach(route => {
// 先复制一份,避免直接改原始 asyncRoutes。
const tmpRoute = cloneRoute(route);
// 第一步:角色不符合,或者客户专属菜单但当前用户不是客户,直接跳过。
if (!hasRouteRole(tmpRoute, userRoles) || !canShowCustomerOnlyRoute(route, userRoles)) {
return;
}
// 第二步:设备不符合也跳过,比如 PC 端不展示 H5 专用路由。
if (!isRouteAllowedByDevice(route, deviceType)) {
return;
}
// 第三步:看后端 auths 里有没有当前路由的 fullPath。
const hasPermission = routeHasPermission(route, permissions);
// 第四步:客户首页入口特殊处理,客户登录后默认展示。
const isAlwaysVisibleCustomerRoute =
CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS.includes(route.path) && isCustomer(userRoles);
// 有权限,或者是客户默认入口,就把这个路由放进最终菜单。
if (hasPermission || isAlwaysVisibleCustomerRoute) {
res.push(tmpRoute);
} else if (tmpRoute.children) {
// 父级没权限时继续看子级。只要子级有权限,父级也要保留,否则子菜单没地方挂。
const filteredChildren = filterAsyncRoutes(tmpRoute.children, permissions, userRoles, deviceType);
if (filteredChildren.length > 0) {
tmpRoute.children = filteredChildren;
res.push(tmpRoute);
}
}
});
return res;
}
// 给普通用户和客户补充固定菜单:订单、资源,以及客户专属的工单/充值/发票等。
function addUserRoutes(routes, userType, orgType, userRoles = [], deviceType = 'pc') {
console.log("addUserRoutes - userType:", userType, "orgType:", orgType, "userRoles:", userRoles);
const userRoutes = [];
// orgType 为 2 或 3 时也按客户账号处理。
const isUserAccount = userType === 'user' || orgType == 2 || orgType == 3;
if (isUserAccount) {
// 普通客户账号默认补订单管理和资源管理。
const baseUserRoutes = getRoutesByPath(routes, BASE_USER_ROUTE_PATHS)
.filter(route => isRouteAllowedByDevice(route, deviceType));
console.log("添加基础用户菜单路由:", baseUserRoutes.map(route => route.path));
userRoutes.push(...baseUserRoutes);
}
if (isCustomer(userRoles)) {
// 只有客户角色才补客户专属菜单。
const customerRoutes = getRoutesByPath(routes, CUSTOMER_EXTRA_ROUTE_PATHS)
.filter(route => isRouteAllowedByDevice(route, deviceType));
console.log("添加客户菜单路由:", customerRoutes.map(route => route.path));
userRoutes.push(...customerRoutes);
}
return userRoutes;
}
// 运营角色额外补模型管理菜单,目前只在 PC 端展示。
function addOperationRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
if (!userRoles.includes(OPERATION_ROLE) || deviceType !== 'pc') {
return accessedRoutes;
}
return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, OPERATION_EXTRA_ROUTE_PATHS));
}
// 财务角色额外补财务菜单,目前只在 PC 端展示。
function addFinanceRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
if (!userRoles.includes(FINANCE_ROLE) || deviceType !== 'pc') {
return accessedRoutes;
}
return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, FINANCE_EXTRA_ROUTE_PATHS));
}
// token市集是公共菜单所有登录用户都要能看到。
function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') {
const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS)
.filter(route => isRouteAllowedByDevice(route, deviceType));
return appendMissingRoutes(accessedRoutes, commonRoutes);
}
// 订单管理有两个特殊子菜单,只有 SPECIAL_ORDER_USER 能看到,其他用户过滤掉。
function filterOrderChildrenByUser(routes, username) {
if (username === SPECIAL_ORDER_USER) {
console.log(`用户 ${username}${SPECIAL_ORDER_USER},保留所有订单子路由`);
return routes;
}
return routes.map(route => {
const nextRoute = cloneRoute(route);
// 找到订单管理后,移除特殊用户专属的子菜单。
if (nextRoute.path === '/orderManagement' && nextRoute.children) {
console.log(`用户 ${username} 不是 ${SPECIAL_ORDER_USER},过滤订单管理子路由`);
nextRoute.children = nextRoute.children.filter(child =>
!ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER.includes(child.path)
);
console.log('过滤后订单子路由:', nextRoute.children.map(child => child.path));
}
if (nextRoute.children) {
// 子路由里如果还有订单管理,也继续递归处理。
nextRoute.children = filterOrderChildrenByUser(nextRoute.children, username);
}
return nextRoute;
});
}
// 整理后端权限列表。如果包含空 path就按“拥有全部动态路由权限”处理。
function getPermissionList(auths = []) {
const permissions = JSON.parse(JSON.stringify(auths));
const permissionPaths = permissions.map(item => item.path);
if (permissionPaths.includes('')) {
return getAllRoutePermissions(asyncRoutes);
}
return permissions;
}
// 根据后端 auths 生成第一版可访问路由。没有 auths 就不展示动态菜单。
function getAccessedRoutesByPermission(auths, userRoles, deviceType) {
if (!auths.length) {
return [];
}
const permissions = getPermissionList(auths);
return filterAsyncRoutes(asyncRoutes, permissions, userRoles, deviceType);
}
// 判断是不是超级管理员账号:用户名包含 admin并且不是客户组织。
function isSuperAdminUser(username, orgType) {
return username && username.includes('admin') && orgType != 2 && orgType != 3;
}
// 超级管理员只拿超级管理员菜单;手机端不展示这个菜单。
function getSuperAdminRoutes(deviceType) {
if (deviceType !== 'pc') {
return [];
}
return getRoutesByPath(asyncRoutes, [SUPER_ADMIN_ROUTE_PATH]).map(cloneRoute);
}
// 在已有权限菜单基础上,补充用户类型/客户角色需要固定展示的菜单。
function addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType) {
const userSpecificRoutes = addUserRoutes(asyncRoutes, userType, orgType, userRoles, deviceType);
return appendMissingRoutes(accessedRoutes, userSpecificRoutes);
}
const state = {
routes: [],
addRoutes: [],
users: [],
isMobile: isMobile // 保存设备类型状态
};
const mutations = {
SET_ROUTES: (state, routes) => {
console.log("MUTATION SET_ROUTES - received routes:", routes);
// addRoutes 只保存动态生成的菜单,方便 router.addRoutes 使用。
state.addRoutes = routes;
sessionStorage.setItem("routes", JSON.stringify(routes));
// routes 是侧边栏最终读取的数据:基础路由 + 动态权限路由。
state.routes = constantRoutes.concat(routes);
console.log("MUTATION SET_ROUTES - final state.routes:", state.routes);
},
RESET_ROUTES: (state) => {
// 退出登录或切换账号时,必须清掉内存里的旧菜单,否则不刷新页面会继续显示上个角色的菜单。
state.routes = [];
state.addRoutes = [];
sessionStorage.removeItem("routes");
},
SETUSERS: (state, user) => {
state.users = user;
},
SET_DEVICE_TYPE: (state, isMobile) => {
state.isMobile = isMobile;
}
};
const actions = {
/**
* 生成动态路由
*
* 根据用户类型、组织类型和权限列表生成对应的动态路由配置
* 包含管理员和普通用户的不同路由生成逻辑
*
* @param {Object} context - Vuex上下文对象
* @param {Function} context.commit - 提交mutation的方法
* @param {Object} context.rootState - 根模块的状态
* @param {Object} params - 参数对象
* @param {string} [params.userType] - 用户类型
* @param {number} [params.orgType] - 组织类型
* @param {Array} [params.auths] - 权限列表
* @param {Object} [params.user] - 用户信息对象
* @returns {Promise<Array>} 解析后的动态路由数组
*/
generateRoutes({ commit, rootState, state }, params) {
console.log("ACTION generateRoutes - params:", params);
return new Promise((resolve) => {
// 1. 先拿到用户基础信息,优先用传进来的参数,没有就从 sessionStorage / vuex 兜底。
const userType = params.userType || sessionStorage.getItem('userType') || '';
const orgType = params.orgType || parseInt(sessionStorage.getItem('orgType')) || 0;
const username = params.user || rootState.user.user || '';
const userRoles = getCurrentRoles(params, rootState);
const deviceType = getDeviceType(state.isMobile);
const auths = params.auths ? JSON.parse(JSON.stringify(params.auths)) : [];
// 2. 判断是不是超级管理员,超级管理员走单独菜单逻辑。
const isSuperAdmin = isSuperAdminUser(params.user, orgType);
console.log("用户角色:", userRoles);
console.log("当前用户名:", username, `检查是否是${SPECIAL_ORDER_USER}:`, username === SPECIAL_ORDER_USER);
console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
console.log("ACTION generateRoutes - auths:", auths);
// 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。
let accessedRoutes = isSuperAdmin
? getSuperAdminRoutes(deviceType)
: getAccessedRoutesByPermission(auths, userRoles, deviceType);
// 4. token市集是公共入口所有登录用户都补上。
accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType);
if (!isSuperAdmin) {
// 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。
console.log("为用户添加特定路由");
accessedRoutes = addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType);
console.log("添加用户特定路由后的accessedRoutes:", accessedRoutes);
}
// 6. 运营角色额外补模型管理。
accessedRoutes = addOperationRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
// 6.1 财务角色额外补财务菜单。
accessedRoutes = addFinanceRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
// 7. 最后处理订单管理里的特殊子菜单权限。
accessedRoutes = filterOrderChildrenByUser(accessedRoutes, username);
console.log("ACTION generateRoutes - 最终 calculated accessedRoutes:", accessedRoutes);
// 8. 保存到 vuex 和 sessionStorage侧边栏会读取 state.permission.routes。
commit("SET_ROUTES", accessedRoutes);
resolve(accessedRoutes);
});
},
};
export default {
namespaced: true,
state,
mutations,
actions,
};