537 lines
18 KiB
JavaScript
537 lines
18 KiB
JavaScript
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,
|
||
};
|