You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

367 lines
14 KiB

import { defineStore } from 'pinia';
import router, { constantRoutes, dynamicRoutes } from '@/router';
import store from '@/store';
import { getRouters } from '@/api/menu';
import auth from '@/plugins/auth';
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue';
import ParentView from '@/components/ParentView/index.vue';
import InnerLink from '@/layout/components/InnerLink/index.vue';
import { ref, computed } from 'vue';
import { createCustomNameComponent } from '@/utils/createCustomNameComponent';
import { isHttp } from '@/utils/validate';
import { useUserStore } from '@/store/modules/user';
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue');
/** 系统分类选项(顺序:首页、执法监督码、后台管理) */
export interface SystemCodeOption {
label: string;
value: string;
icon: string;
/** 当设置了 iframeUrl 时,切换到该选项会以全屏 iframe 展示,而非加载菜单路由 */
iframeUrl?: string;
/** 当设置了 fullscreenRoute 时,切换到该选项会以全屏 Vue 路由展示 */
fullscreenRoute?: string;
/** 切换到该系统后默认跳转的路由路径 */
defaultRoute?: string;
}
/** 404 兜底路由名称,方便动态移除和重新添加 */
export const CATCH_ALL_ROUTE_NAME = 'CatchAll404';
//http://10.132.108.3:85/loginByUserId
export const systemCodeOptions: SystemCodeOption[] = [
{ label: '首页', value: 'planindex', icon: 'HomeFilled', fullscreenRoute: '/home' },
{ label: '统计分析', value: 'supervisionCode', icon: 'Document', defaultRoute: '/statistics/administrative/enterprise-enforcement' },
{ label: '后台管理', value: 'authService', icon: 'Setting', defaultRoute: '/system-setting/menu' }
];
export const usePermissionStore = defineStore('permission', () => {
/**
* 根据基础 iframeUrl 动态拼接登录凭据参数
*/
const buildIframeUrl = (baseUrl: string): string => {
if (!baseUrl) return '';
// 当配置了 Same-origin 占位符时,替换为浏览器当前的 origin(协议+主机+端口)
let resolvedUrl = baseUrl;
if (baseUrl.includes('Same-origin')) {
resolvedUrl = baseUrl.replace('Same-origin', window.location.origin);
}
const userStore = useUserStore();
const params = new URLSearchParams();
if (userStore.v1SystemCredential) {
params.set('v1_system_credential', userStore.v1SystemCredential);
}
const separator = resolvedUrl.includes('?') ? '&' : '?';
return resolvedUrl + separator + params.toString();
};
const routes = ref<RouteRecordRaw[]>([]);
const addRoutes = ref<RouteRecordRaw[]>([]);
const defaultRoutes = ref<RouteRecordRaw[]>([]);
const topbarRouters = ref<RouteRecordRaw[]>([]);
const sidebarRouters = ref<RouteRecordRaw[]>([]);
/** 缓存后端返回的全量路由原始数据(未经组件转换) */
const allRouteDataCache = ref<any[]>([]);
/** sessionStorage key */
const SYSTEM_CODE_KEY = 'currentSystemCode';
/** 当前选中的系统编码(从 sessionStorage 恢复,默认 planindex) */
const savedCode = sessionStorage.getItem(SYSTEM_CODE_KEY) || 'planindex';
const savedOption = systemCodeOptions.find((o) => o.value === savedCode);
const currentSystemCode = ref<string>(savedCode);
/** 当前是否处于 iframe 全屏模式 */
const iframeMode = ref<boolean>(!!(savedOption?.iframeUrl || savedOption?.fullscreenRoute));
/** 当前 iframe 地址 */
const iframeUrl = ref<string>(savedOption?.iframeUrl ? buildIframeUrl(savedOption.iframeUrl) : '');
/** 当前全屏 Vue 路由地址 */
const fullscreenRoute = ref<string>(savedOption?.fullscreenRoute || '');
const getRoutes = (): RouteRecordRaw[] => {
return routes.value as RouteRecordRaw[];
};
const getDefaultRoutes = (): RouteRecordRaw[] => {
return defaultRoutes.value as RouteRecordRaw[];
};
const getSidebarRoutes = (): RouteRecordRaw[] => {
return sidebarRouters.value as RouteRecordRaw[];
};
const getTopbarRoutes = (): RouteRecordRaw[] => {
return topbarRouters.value as RouteRecordRaw[];
};
const setRoutes = (newRoutes: RouteRecordRaw[]): void => {
addRoutes.value = newRoutes;
routes.value = constantRoutes.concat(newRoutes);
};
const setDefaultRoutes = (routes: RouteRecordRaw[]): void => {
defaultRoutes.value = constantRoutes.concat(routes);
};
const setTopbarRoutes = (routes: RouteRecordRaw[]): void => {
topbarRouters.value = routes;
};
const setSidebarRouters = (routes: RouteRecordRaw[]): void => {
sidebarRouters.value = routes;
};
const generateRoutes = async (systemCode?: string): Promise<RouteRecordRaw[]> => {
const code = systemCode || currentSystemCode.value;
// 仅首次加载时请求后端,后续从缓存取
if (allRouteDataCache.value.length === 0) {
const res = await getRouters();
allRouteDataCache.value = res.data as any[];
console.log('[路由调试] 后端返回全量路由数据 =', JSON.parse(JSON.stringify(allRouteDataCache.value)));
}
// 按当前 systemCode 过滤出匹配的顶层路由
const filteredData = allRouteDataCache.value.filter((route: any) => route.systemCode === code);
console.log('[路由调试] systemCode =', code, ', 过滤后路由数据 =', JSON.parse(JSON.stringify(filteredData)));
const sdata = JSON.parse(JSON.stringify(filteredData));
const rdata = JSON.parse(JSON.stringify(filteredData));
const defaultData = JSON.parse(JSON.stringify(filteredData));
const sidebarRoutes = filterAsyncRouter(sdata);
const rewriteRoutes = filterAsyncRouter(rdata, undefined, true);
const defaultRoutes = filterAsyncRouter(defaultData);
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
asyncRoutes.forEach((route) => {
router.addRoute(route);
});
setRoutes(rewriteRoutes);
setSidebarRouters(constantRoutes.concat(sidebarRoutes));
setDefaultRoutes(sidebarRoutes);
setTopbarRoutes(defaultRoutes);
// 路由name重复检查
duplicateRouteChecker(asyncRoutes, sidebarRoutes);
return new Promise<RouteRecordRaw[]>((resolve) => resolve(rewriteRoutes));
};
/**
* 遍历后台传来的路由字符串,转换为组件对象
* @param asyncRouterMap 后台传来的路由字符串
* @param lastRouter 上一级路由
* @param type 是否是重写路由
*/
const filterAsyncRouter = (asyncRouterMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw, type = false): RouteRecordRaw[] => {
return asyncRouterMap.filter((route) => {
if (type && route.children) {
route.children = filterChildren(route.children, undefined);
}
// Layout ParentView 组件特殊处理
if (route.component?.toString() === 'Layout') {
route.component = Layout;
} else if (route.component?.toString() === 'ParentView') {
route.component = ParentView;
} else if (route.component?.toString() === 'InnerLink') {
route.component = InnerLink;
} else {
route.component = loadView(route.component, route.name as string);
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type);
} else {
delete route.children;
delete route.redirect;
}
return true;
});
};
const filterChildren = (childrenMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw): RouteRecordRaw[] => {
let children: RouteRecordRaw[] = [];
childrenMap.forEach((el) => {
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path;
if (el.children && el.children.length && el.component?.toString() === 'ParentView') {
children = children.concat(filterChildren(el.children, el));
} else {
children.push(el);
}
});
return children;
};
/**
* 切换菜单组:更新 systemCode,重新加载路由并刷新页面
*/
const switchSystemCode = async (systemCode: string) => {
currentSystemCode.value = systemCode;
sessionStorage.setItem(SYSTEM_CODE_KEY, systemCode);
const option = systemCodeOptions.find((o) => o.value === systemCode);
// 判断是否为全屏模式(iframe 或 Vue 路由)
if (option?.iframeUrl || option?.fullscreenRoute) {
iframeMode.value = true;
iframeUrl.value = option.iframeUrl ? buildIframeUrl(option.iframeUrl) : '';
fullscreenRoute.value = option.fullscreenRoute || '';
setSidebarRouters([]);
if (option.fullscreenRoute) {
await router.push(option.fullscreenRoute);
}
return;
}
iframeMode.value = false;
iframeUrl.value = '';
fullscreenRoute.value = '';
// ====== 彻底清理所有动态路由,保证路由表干净 ======
// 1. 收集所有静态路由名称(包括子路由)
const constantNames = new Set<string | symbol>();
const collectNames = (routeList: RouteRecordRaw[]) => {
routeList.forEach((r) => {
if (r.name) constantNames.add(r.name);
if (r.children) collectNames(r.children);
});
};
collectNames(constantRoutes);
// 2. 移除所有非静态路由(包括之前的动态业务路由和404兜底路由)
router.getRoutes().forEach((route) => {
if (route.name && !constantNames.has(route.name)) {
router.removeRoute(route.name);
}
});
// ====== 重新生成并注册路由 ======
const accessRoutes = await generateRoutes(systemCode);
accessRoutes.forEach((route) => {
if (!isHttp(route.path)) {
router.addRoute(route);
}
});
// 重新添加404兜底路由,确保放在所有动态路由之后
router.addRoute({ name: CATCH_ALL_ROUTE_NAME, path: '/:pathMatch(.*)*', component: () => import('@/views/error/404.vue') });
console.log('[路由调试] switchSystemCode 完成, 当前路由列表 =', router.getRoutes().map((r) => ({ name: r.name, path: r.path })));
// 跳转到默认路由
if (option?.defaultRoute) {
await router.push(option.defaultRoute);
}
};
/**
* 清理路由缓存并重置系统码到首页,用于切换账号登录时
*/
const cleanRouteCache = () => {
allRouteDataCache.value = [];
routes.value = [];
addRoutes.value = [];
defaultRoutes.value = [];
topbarRouters.value = [];
sidebarRouters.value = [];
// 重置到首页
currentSystemCode.value = 'planindex';
sessionStorage.setItem(SYSTEM_CODE_KEY, 'planindex');
const defaultOption = systemCodeOptions.find((o) => o.value === 'planindex');
iframeMode.value = !!(defaultOption?.iframeUrl || defaultOption?.fullscreenRoute);
iframeUrl.value = defaultOption?.iframeUrl ? buildIframeUrl(defaultOption.iframeUrl) : '';
fullscreenRoute.value = defaultOption?.fullscreenRoute || '';
};
/** 根据后端返回的路由数据,动态过滤可见的系统分类选项(首页始终显示) */
const visibleSystemCodeOptions = computed(() => {
return systemCodeOptions.filter((option) => {
// 首页(有 iframeUrl 或 fullscreenRoute 的)始终显示
if (option.iframeUrl || option.fullscreenRoute) return true;
// 其他选项:只有当后端路由中存在匹配的 systemCode 时才显示
return allRouteDataCache.value.some((route: any) => route.systemCode === option.value);
});
});
return {
routes,
topbarRouters,
sidebarRouters,
defaultRoutes,
currentSystemCode,
iframeMode,
iframeUrl,
fullscreenRoute,
visibleSystemCodeOptions,
getRoutes,
getDefaultRoutes,
getSidebarRoutes,
getTopbarRoutes,
setRoutes,
generateRoutes,
setSidebarRouters,
switchSystemCode,
cleanRouteCache
};
});
// 动态路由遍历,验证是否具备权限
export const filterDynamicRoutes = (routes: RouteRecordRaw[]) => {
const res: RouteRecordRaw[] = [];
routes.forEach((route) => {
if (route.permissions) {
if (auth.hasPermiOr(route.permissions)) {
res.push(route);
}
} else if (route.roles) {
if (auth.hasRoleOr(route.roles)) {
res.push(route);
}
}
});
return res;
};
export const loadView = (view: any, name: string) => {
let res;
const allDirs: string[] = [];
for (const path in modules) {
const viewsIndex = path.indexOf('/views/');
let dir = path.substring(viewsIndex + 7);
dir = dir.substring(0, dir.lastIndexOf('.vue'));
allDirs.push(dir);
if (dir === view) {
res = createCustomNameComponent(modules[path], { name });
console.log('[路由调试] loadView 匹配成功: view =', view, ', name =', name);
return res;
}
}
console.warn('[路由调试] loadView 未匹配到组件! view =', JSON.stringify(view), ', name =', name, '\n glob扫描到的路径:', allDirs);
return res;
};
// 非setup
export const usePermissionStoreHook = () => {
return usePermissionStore(store);
};
interface Route {
name?: string | symbol;
path: string;
children?: Route[];
}
/**
* 检查路由name是否重复
* @param localRoutes 本地路由
* @param routes 动态路由
*/
function duplicateRouteChecker(localRoutes: Route[], routes: Route[]) {
// 展平
function flatRoutes(routes: Route[]) {
const res: Route[] = [];
routes.forEach((route) => {
if (route.children) {
res.push(...flatRoutes(route.children));
} else {
res.push(route);
}
});
return res;
}
const allRoutes = flatRoutes([...localRoutes, ...routes]);
const nameList: string[] = [];
allRoutes.forEach((route) => {
const name = route.name.toString();
if (name && nameList.includes(name)) {
const message = `路由名称: [${name}] 重复, 会造成 404`;
console.error(message);
ElNotification({
title: '路由名称重复',
message,
type: 'error'
});
return;
}
nameList.push(route.name.toString());
});
}