mock数据,AI对话,全部应用
This commit is contained in:
109
urbanLifelineWeb/temp/deviceUtils.ts
Normal file
109
urbanLifelineWeb/temp/deviceUtils.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/**
|
||||
* 设备类型枚举
|
||||
*/
|
||||
export enum DeviceType {
|
||||
MOBILE = 'mobile', // h5移动端
|
||||
DESKTOP = 'desktop' // web桌面端
|
||||
}
|
||||
|
||||
/**
|
||||
* 屏幕尺寸断点
|
||||
*/
|
||||
export const BREAKPOINTS = {
|
||||
mobile: 768, // 小于768px为移动端(h5)
|
||||
desktop: 768 // 大于等于768px为桌面端(web)
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测当前设备类型
|
||||
*/
|
||||
export function getDeviceType(): DeviceType {
|
||||
const width = window.innerWidth;
|
||||
|
||||
if (width < BREAKPOINTS.mobile) {
|
||||
return DeviceType.MOBILE; // h5移动端
|
||||
} else {
|
||||
return DeviceType.DESKTOP; // web桌面端
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为移动端
|
||||
*/
|
||||
export function isMobile(): boolean {
|
||||
return getDeviceType() === DeviceType.MOBILE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为桌面端
|
||||
*/
|
||||
export function isDesktop(): boolean {
|
||||
return getDeviceType() === DeviceType.DESKTOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式设备类型 Hook
|
||||
*/
|
||||
export function useDevice() {
|
||||
const deviceType = ref<DeviceType>(getDeviceType());
|
||||
const isMobileDevice = ref(isMobile());
|
||||
const isDesktopDevice = ref(isDesktop());
|
||||
|
||||
const updateDeviceType = () => {
|
||||
deviceType.value = getDeviceType();
|
||||
isMobileDevice.value = isMobile();
|
||||
isDesktopDevice.value = isDesktop();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateDeviceType);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateDeviceType);
|
||||
});
|
||||
|
||||
return {
|
||||
deviceType,
|
||||
isMobileDevice,
|
||||
isDesktopDevice
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备类型获取对应的组件路径
|
||||
*/
|
||||
export function getComponentPath(basePath: string, deviceType?: DeviceType): string {
|
||||
const currentDeviceType = deviceType || getDeviceType();
|
||||
|
||||
// 如果是移动端(h5),尝试加载移动端版本
|
||||
if (currentDeviceType === DeviceType.MOBILE) {
|
||||
const mobilePath = basePath.replace('.vue', '.mobile.vue');
|
||||
return mobilePath;
|
||||
}
|
||||
|
||||
// 默认返回桌面版本(web)
|
||||
return basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态导入组件,支持回退机制
|
||||
*/
|
||||
export async function importResponsiveComponent(basePath: string) {
|
||||
const deviceType = getDeviceType();
|
||||
|
||||
// 尝试加载设备特定的组件
|
||||
if (deviceType === DeviceType.MOBILE) {
|
||||
try {
|
||||
const mobilePath = basePath.replace('.vue', '.mobile.vue');
|
||||
return await import(/* @vite-ignore */ mobilePath);
|
||||
} catch {
|
||||
// 移动端组件不存在,回退到默认组件
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认组件(桌面端/web)
|
||||
return await import(/* @vite-ignore */ basePath);
|
||||
}
|
||||
586
urbanLifelineWeb/temp/route-generator.ts
Normal file
586
urbanLifelineWeb/temp/route-generator.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
/**
|
||||
* @description 动态路由生成器
|
||||
* @author yslg
|
||||
* @since 2025-10-07
|
||||
*/
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { routes } from '@/router';
|
||||
import { getResponsiveLayout, createResponsiveRoute, type RouteAdapter } from './routeAdapter';
|
||||
|
||||
// 预注册所有视图组件,构建时由 Vite 解析并生成按需加载的 chunk
|
||||
const VIEW_MODULES = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
/**
|
||||
* 布局组件映射 - 使用响应式布局适配
|
||||
*/
|
||||
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
||||
// 基础布局(旧版,带侧边栏)
|
||||
'BasicLayout': getResponsiveLayout('BasicLayout'),
|
||||
// 导航布局(新版,顶部导航+动态侧边栏)
|
||||
'NavigationLayout': getResponsiveLayout('NavigationLayout'),
|
||||
// 侧边栏布局(管理后台专用,顶层SIDEBAR菜单)
|
||||
'SidebarLayout': getResponsiveLayout('SidebarLayout'),
|
||||
// 空白布局
|
||||
'BlankLayout': getResponsiveLayout('BlankLayout'),
|
||||
// 页面布局
|
||||
'PageLayout': getResponsiveLayout('PageLayout'),
|
||||
// 路由占位组件(用于没有组件的子路由)
|
||||
'RoutePlaceholder': () => import('@/layouts/RoutePlaceholder.vue'),
|
||||
// 用户中心布局(有共用区域,避免重复查询)
|
||||
'UserCenterLayout': () => import('@/views/user/user-center/UserCenterLayout.vue'),
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据菜单生成路由配置
|
||||
* @param menus 用户菜单列表
|
||||
* @returns Vue Router路由配置数组
|
||||
*/
|
||||
export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
const pageRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
// 构建菜单树
|
||||
const menuTree = buildMenuTree(menus);
|
||||
|
||||
// 生成路由
|
||||
menuTree.forEach(menu => {
|
||||
const route = generateRouteFromMenu(menu);
|
||||
|
||||
if (route) {
|
||||
routes.push(route);
|
||||
|
||||
// 递归提取所有 PAGE 类型的子菜单
|
||||
extractPageChildren(route, pageRoutes);
|
||||
}
|
||||
});
|
||||
|
||||
// 将 PAGE 类型的路由添加到路由列表
|
||||
routes.push(...pageRoutes);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取路由中的 PAGE 类型子菜单
|
||||
*/
|
||||
function extractPageChildren(route: any, pageRoutes: RouteRecordRaw[]) {
|
||||
// 检查当前路由是否有 PAGE 类型的子菜单
|
||||
if (route.meta?.pageChildren && Array.isArray(route.meta.pageChildren)) {
|
||||
route.meta.pageChildren.forEach((pageMenu: SysMenu) => {
|
||||
const pageRoute = generateRouteFromMenu(pageMenu, true); // 作为顶层路由生成
|
||||
if (pageRoute) {
|
||||
|
||||
pageRoutes.push(pageRoute);
|
||||
} else {
|
||||
console.error(`[路由生成] 生成独立PAGE路由失败: ${pageMenu.name}`);
|
||||
}
|
||||
});
|
||||
// 清理临时数据
|
||||
delete route.meta.pageChildren;
|
||||
}
|
||||
|
||||
// 递归检查子路由
|
||||
if (route.children && Array.isArray(route.children)) {
|
||||
route.children.forEach((childRoute: any) => {
|
||||
extractPageChildren(childRoute, pageRoutes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单个菜单生成路由
|
||||
* @param menu 菜单对象
|
||||
* @param isTopLevel 是否是顶层菜单
|
||||
* @returns 路由配置
|
||||
*/
|
||||
function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw | null {
|
||||
// 跳过按钮类型
|
||||
if (menu.type === MenuType.BUTTON) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
|
||||
if (menu.component === '__STATIC_ROUTE__') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const route: any = {
|
||||
path: menu.url || `/${menu.menuID}`,
|
||||
name: menu.menuID,
|
||||
meta: {
|
||||
title: menu.name,
|
||||
icon: menu.icon,
|
||||
menuId: menu.menuID,
|
||||
parentId: menu.parentID,
|
||||
orderNum: menu.orderNum,
|
||||
type: menu.type,
|
||||
hideInMenu: false,
|
||||
requiresAuth: true,
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否指定了布局(只有顶层菜单才使用布局)
|
||||
const layout = isTopLevel ? (menu as any).layout : null;
|
||||
const hasChildren = menu.children && menu.children.length > 0;
|
||||
|
||||
// 检查 component 是否是布局组件(优先检查 LAYOUT_MAP,再检查名称)
|
||||
const isComponentLayout = menu.component && (
|
||||
LAYOUT_MAP[menu.component] ||
|
||||
(typeof menu.component === 'string' && menu.component.includes('Layout'))
|
||||
);
|
||||
|
||||
// 确定路由组件
|
||||
if (layout && LAYOUT_MAP[layout]) {
|
||||
// 如果指定了布局,使用指定的布局
|
||||
route.component = getComponent(layout);
|
||||
} else if (isComponentLayout && hasChildren && isTopLevel && menu.component) {
|
||||
// 如果 component 是布局组件且有子菜单,使用该布局组件作为父路由组件
|
||||
// 这样可以确保布局只渲染一次,共用区域的数据也只查询一次
|
||||
route.component = getComponent(menu.component);
|
||||
} else if (hasChildren && isTopLevel) {
|
||||
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
|
||||
if (menu.type === MenuType.NAVIGATION) {
|
||||
route.component = getComponent('NavigationLayout');
|
||||
} else if (menu.type === MenuType.SIDEBAR && !menu.parentID) {
|
||||
// 顶层SIDEBAR菜单(管理后台)默认使用SidebarLayout
|
||||
route.component = getComponent('SidebarLayout');
|
||||
} else {
|
||||
route.component = getComponent('BasicLayout');
|
||||
}
|
||||
} else {
|
||||
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
||||
if (menu.component) {
|
||||
route.component = getComponent(menu.component);
|
||||
} else {
|
||||
// 非顶层菜单没有组件时,使用路由占位组件(不影响布局样式)
|
||||
route.component = getComponent('RoutePlaceholder');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子路由
|
||||
if (layout && LAYOUT_MAP[layout] && menu.component && isTopLevel) {
|
||||
// 如果指定了布局,将页面组件作为子路由
|
||||
route.children = [{
|
||||
path: '',
|
||||
name: `${menu.menuID}_page`,
|
||||
component: getComponent(menu.component),
|
||||
meta: route.meta
|
||||
}];
|
||||
|
||||
// 如果还有其他子菜单,继续添加
|
||||
if (hasChildren) {
|
||||
const pageChildren: SysMenu[] = [];
|
||||
const normalChildren: SysMenu[] = [];
|
||||
|
||||
menu.children!.forEach(child => {
|
||||
if (child.type === MenuType.PAGE) {
|
||||
pageChildren.push(child);
|
||||
} else {
|
||||
normalChildren.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加普通子菜单
|
||||
normalChildren.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, false);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
|
||||
// PAGE 类型的菜单保存到 meta
|
||||
if (pageChildren.length > 0) {
|
||||
route.meta.pageChildren = pageChildren;
|
||||
}
|
||||
}
|
||||
} else if (hasChildren) {
|
||||
// 处理有子菜单的情况
|
||||
route.children = [];
|
||||
|
||||
// 分离 PAGE 类型的子菜单和普通子菜单
|
||||
const pageChildren: SysMenu[] = [];
|
||||
const normalChildren: SysMenu[] = [];
|
||||
|
||||
menu.children!.forEach(child => {
|
||||
if (child.type === MenuType.PAGE) {
|
||||
// PAGE 类型的菜单作为独立路由,不作为子路由
|
||||
pageChildren.push(child);
|
||||
} else {
|
||||
normalChildren.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
// 当前菜单指定了页面组件时,如果存在“普通子菜单”(非 PAGE 类型)
|
||||
// 则需要创建一个默认子路由来承载当前菜单的页面组件,
|
||||
// 这样父级既能作为分组,又能渲染自己的页面。
|
||||
// 如果只有 PAGE 类型子菜单,则直接使用当前路由组件,而不再包一层 `_page`,
|
||||
// 避免多出一层嵌套导致 matched 结构过深。
|
||||
if (menu.component && !isComponentLayout && normalChildren.length > 0) {
|
||||
route.children!.push({
|
||||
path: '',
|
||||
name: `${menu.menuID}_page`,
|
||||
component: getComponent(menu.component),
|
||||
meta: {
|
||||
...route.meta,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 只将普通子菜单加入 children
|
||||
normalChildren.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, false);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
|
||||
// PAGE 类型的菜单需要在外层单独处理(不管是哪一层的菜单)
|
||||
if (pageChildren.length > 0) {
|
||||
// 将 PAGE 类型的子菜单保存到路由的 meta 中,稍后在外层生成
|
||||
route.meta.pageChildren = pageChildren;
|
||||
}
|
||||
|
||||
// 如果没有设置重定向,自动重定向到第一个有URL的子菜单
|
||||
if (!route.redirect && route.children.length > 0) {
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(normalChildren);
|
||||
if (firstChildWithUrl?.url) {
|
||||
route.redirect = firstChildWithUrl.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个有URL的菜单
|
||||
* @param menus 菜单列表
|
||||
* @returns 第一个有URL的菜单
|
||||
*/
|
||||
function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
|
||||
for (const menu of menus) {
|
||||
if (menu.type !== MenuType.BUTTON) {
|
||||
if (menu.url) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findFirstMenuWithUrl(menu.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件名称获取组件(支持响应式组件)
|
||||
* @param componentName 组件名称/路径
|
||||
* @returns 组件异步加载函数
|
||||
*/
|
||||
function getComponent(componentName: string) {
|
||||
// 1. 若是布局组件,直接返回预定义映射
|
||||
if (LAYOUT_MAP[componentName]) {
|
||||
return LAYOUT_MAP[componentName];
|
||||
}
|
||||
|
||||
// 2. 将后台给的 component 字段转换为 ../views/**.vue 形式的 key
|
||||
let componentPath = componentName;
|
||||
|
||||
// 如果不是以 @/ 开头,则认为是相对 views 根目录的路径,例如 "user/home/HomeView"
|
||||
if (!componentPath.startsWith('@/')) {
|
||||
if (!componentPath.startsWith('/')) {
|
||||
componentPath = '/' + componentPath;
|
||||
}
|
||||
componentPath = '@/views' + componentPath; // => '@/views/user/home/HomeView'
|
||||
}
|
||||
|
||||
// 将别名 @/ 转为相对于当前文件的路径,必须与 import.meta.glob 中的模式一致
|
||||
const originalPath = componentPath.replace(/^@\//, '../'); // => '../views/user/home/HomeView'
|
||||
|
||||
// 补全 .vue 后缀
|
||||
if (!originalPath.endsWith('.vue')) {
|
||||
componentPath = originalPath + '.vue';
|
||||
} else {
|
||||
componentPath = originalPath;
|
||||
}
|
||||
|
||||
// 3. 检查是否有移动端版本
|
||||
const mobileComponentPath = componentPath.replace('.vue', '.mobile.vue');
|
||||
|
||||
// 从 VIEW_MODULES 中查找对应的 loader
|
||||
const originalLoader = VIEW_MODULES[componentPath];
|
||||
const mobileLoader = VIEW_MODULES[mobileComponentPath];
|
||||
|
||||
if (!originalLoader) {
|
||||
console.error('[路由生成] 未找到组件模块', {
|
||||
原始组件名: componentName,
|
||||
期望路径: componentPath,
|
||||
可用模块: Object.keys(VIEW_MODULES)
|
||||
});
|
||||
// 找不到时退回到 404 组件
|
||||
return () => import('@/views/public/error/404.vue');
|
||||
}
|
||||
|
||||
// 4. 如果有移动端版本,创建响应式路由适配器
|
||||
if (mobileLoader) {
|
||||
const adapter: RouteAdapter = {
|
||||
original: originalLoader as () => Promise<any>,
|
||||
mobile: mobileLoader as () => Promise<any>
|
||||
};
|
||||
return createResponsiveRoute(adapter);
|
||||
}
|
||||
|
||||
// 5. 没有移动端版本,直接返回原始组件
|
||||
return originalLoader as () => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将静态路由转换为菜单项
|
||||
* @param routes 静态路由数组
|
||||
* @returns 菜单项数组
|
||||
*/
|
||||
function convertRoutesToMenus(routes: RouteRecordRaw[]): SysMenu[] {
|
||||
const menus: SysMenu[] = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
// 处理有子路由的情况(现在静态路由都有布局组件)
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children.forEach(child => {
|
||||
// 只处理有 meta.menuType 的子路由
|
||||
if (child.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
menuID: child.name as string || child.path.replace(/\//g, '-'),
|
||||
parentID: '0',
|
||||
name: child.meta.title as string || child.name as string,
|
||||
url: route.path, // 使用父路由的路径
|
||||
type: child.meta.menuType as MenuType,
|
||||
orderNum: (child.meta.orderNum as number) || -1,
|
||||
// 标记为静态路由,避免重复生成路由
|
||||
component: '__STATIC_ROUTE__', // 特殊标记
|
||||
};
|
||||
|
||||
menus.push(menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 处理没有子路由的情况(兼容性保留)
|
||||
else if (route.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
menuID: route.name as string || route.path.replace(/\//g, '-'),
|
||||
parentID: '0',
|
||||
name: route.meta.title as string || route.name as string,
|
||||
url: route.path,
|
||||
type: route.meta.menuType as MenuType,
|
||||
orderNum: (route.meta.orderNum as number) || -1,
|
||||
// 标记为静态路由,避免重复生成路由
|
||||
component: '__STATIC_ROUTE__', // 特殊标记
|
||||
};
|
||||
|
||||
menus.push(menu);
|
||||
}
|
||||
});
|
||||
|
||||
return menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树结构
|
||||
* @param menus 菜单列表
|
||||
* @returns 菜单树
|
||||
*/
|
||||
export function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||||
// 将静态路由转换为菜单项
|
||||
const staticMenus = convertRoutesToMenus(routes);
|
||||
|
||||
// 合并动态菜单和静态菜单
|
||||
const allMenus = [...staticMenus, ...menus];
|
||||
|
||||
if (allMenus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const menuMap = new Map<string, SysMenu>();
|
||||
const rootMenus: SysMenu[] = [];
|
||||
const maxDepth = allMenus.length; // 最多遍历len层
|
||||
|
||||
// 创建菜单映射
|
||||
allMenus.forEach(menu => {
|
||||
if (menu.menuID) {
|
||||
menuMap.set(menu.menuID, { ...menu, children: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 循环构建树结构,最多遍历maxDepth次
|
||||
for (let depth = 0; depth < maxDepth; depth++) {
|
||||
let hasChanges = false;
|
||||
|
||||
allMenus.forEach(menu => {
|
||||
if (!menu.menuID) return;
|
||||
|
||||
const menuNode = menuMap.get(menu.menuID);
|
||||
if (!menuNode) return;
|
||||
|
||||
// 如果节点已经在树中,跳过
|
||||
if (isNodeInTree(menuNode, rootMenus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!menu.parentID || menu.parentID === '0' || menu.parentID === '') {
|
||||
// 根菜单
|
||||
if (!isNodeInTree(menuNode, rootMenus)) {
|
||||
rootMenus.push(menuNode);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else {
|
||||
// 子菜单
|
||||
const parent = menuMap.get(menu.parentID);
|
||||
if (parent && isNodeInTree(parent, rootMenus)) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
if (!parent.children.includes(menuNode)) {
|
||||
parent.children.push(menuNode);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有变化,说明树构建完成
|
||||
if (!hasChanges) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 按orderNum排序
|
||||
const sortMenus = (menus: SysMenu[]): SysMenu[] => {
|
||||
return menus
|
||||
.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0))
|
||||
.map(menu => ({
|
||||
...menu,
|
||||
children: menu.children ? sortMenus(menu.children) : []
|
||||
}));
|
||||
};
|
||||
|
||||
return sortMenus(rootMenus);
|
||||
}
|
||||
|
||||
// 检查节点是否已经在树中
|
||||
function isNodeInTree(node: SysMenu, tree: SysMenu[]): boolean {
|
||||
for (const treeNode of tree) {
|
||||
if (treeNode.menuID === node.menuID) {
|
||||
return true;
|
||||
}
|
||||
if (treeNode.children && isNodeInTree(node, treeNode.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限过滤菜单
|
||||
* @param menus 菜单列表
|
||||
* @param permissions 用户权限列表
|
||||
* @returns 过滤后的菜单列表
|
||||
*/
|
||||
export function filterMenusByPermissions(
|
||||
menus: SysMenu[],
|
||||
permissions: string[]
|
||||
): SysMenu[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return menus
|
||||
.filter(() => {
|
||||
// 如果菜单没有设置权限要求,则默认显示
|
||||
// 这里可以根据实际业务需求调整权限检查逻辑
|
||||
return true; // 暂时返回true,后续可以根据菜单的权限字段进行过滤
|
||||
})
|
||||
.map(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
return {
|
||||
...menu,
|
||||
children: filterMenusByPermissions(menu.children, permissions)
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找路由路径对应的菜单
|
||||
* @param menus 菜单树
|
||||
* @param path 路由路径
|
||||
* @returns 匹配的菜单
|
||||
*/
|
||||
export function findMenuByPath(menus: SysMenu[], path: string): SysMenu | null {
|
||||
for (const menu of menus) {
|
||||
if (menu.url === path) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuByPath(menu.children, path);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单路径数组(面包屑导航用)
|
||||
* @param menus 菜单树
|
||||
* @param targetMenuId 目标菜单ID
|
||||
* @returns 菜单路径数组
|
||||
*/
|
||||
export function getMenuPath(menus: SysMenu[], targetMenuId: string): SysMenu[] {
|
||||
const path: SysMenu[] = [];
|
||||
|
||||
function findPath(menuList: SysMenu[]): boolean {
|
||||
for (const menu of menuList) {
|
||||
path.push(menu);
|
||||
|
||||
if (menu.menuID === targetMenuId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
if (findPath(menu.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
path.pop();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
findPath(menus);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第一个可访问的菜单URL(用于登录后跳转)
|
||||
* @param menus 菜单树
|
||||
* @returns 第一个可访问的菜单URL,如果没有则返回 null
|
||||
*/
|
||||
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
|
||||
if (!menus || menus.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "/home";
|
||||
}
|
||||
261
urbanLifelineWeb/temp/routeAdapter.ts
Normal file
261
urbanLifelineWeb/temp/routeAdapter.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
|
||||
import { getDeviceType, DeviceType } from './deviceUtils';
|
||||
|
||||
/**
|
||||
* 路由适配器接口
|
||||
*/
|
||||
export interface RouteAdapter {
|
||||
original: () => Promise<any>; // web桌面端
|
||||
mobile?: () => Promise<any>; // h5移动端
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端路由映射表
|
||||
*/
|
||||
export const MOBILE_ROUTES_MAP: Record<string, string> = {
|
||||
// User Views
|
||||
'/home': '@/views/user/home/HomeView.mobile.vue',
|
||||
'/resource-center': '@/views/user/resource-center/ResourceCenterView.mobile.vue',
|
||||
'/resource-hot': '@/views/user/resource-center/HotResourceView.mobile.vue',
|
||||
'/search': '@/views/user/resource-center/SearchView.mobile.vue',
|
||||
'/course-center': '@/views/user/study-plan/CourseCenterView.mobile.vue',
|
||||
'/course-detail': '@/views/user/study-plan/CourseDetailView.mobile.vue',
|
||||
'/course-study': '@/views/user/study-plan/CourseStudyView.mobile.vue',
|
||||
'/study-tasks': '@/views/user/study-plan/StudyTasksView.mobile.vue',
|
||||
'/learning-task-detail': '@/views/user/study-plan/LearningTaskDetailView.mobile.vue',
|
||||
'/user-center': '@/views/user/user-center/UserCenterLayout.mobile.vue',
|
||||
'/personal-info': '@/views/user/user-center/profile/PersonalInfoView.mobile.vue',
|
||||
'/account-settings': '@/views/user/user-center/profile/AccountSettingsView.mobile.vue',
|
||||
'/my-achievements': '@/views/user/user-center/MyAchievementsView.mobile.vue',
|
||||
'/my-favorites': '@/views/user/user-center/MyFavoritesView.mobile.vue',
|
||||
'/learning-records': '@/views/user/user-center/LearningRecordsView.mobile.vue',
|
||||
'/my-messages': '@/views/user/message/MyMessageListView.mobile.vue',
|
||||
'/message-detail': '@/views/user/message/MyMessageDetailView.mobile.vue',
|
||||
|
||||
// Public Views
|
||||
'/login': '@/views/public/login/Login.mobile.vue',
|
||||
'/register': '@/views/public/login/Register.mobile.vue',
|
||||
'/forgot-password': '@/views/public/login/ForgotPassword.mobile.vue',
|
||||
'/article-show': '@/views/public/article/ArticleShowView.mobile.vue',
|
||||
'/article-add': '@/views/public/article/ArticleAddView.mobile.vue'
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout映射表
|
||||
*/
|
||||
export const LAYOUT_MAP: Record<string, Record<DeviceType, string>> = {
|
||||
'NavigationLayout': {
|
||||
[DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端
|
||||
[DeviceType.DESKTOP]: '@/layouts/NavigationLayout.vue' // web桌面端
|
||||
},
|
||||
'SidebarLayout': {
|
||||
[DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端
|
||||
[DeviceType.DESKTOP]: '@/layouts/SidebarLayout.vue' // web桌面端
|
||||
},
|
||||
'BasicLayout': {
|
||||
[DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端
|
||||
[DeviceType.DESKTOP]: '@/layouts/BasicLayout.vue' // web桌面端
|
||||
},
|
||||
'BlankLayout': {
|
||||
[DeviceType.MOBILE]: '@/layouts/BlankLayout.vue', // h5移动端
|
||||
[DeviceType.DESKTOP]: '@/layouts/BlankLayout.vue' // web桌面端
|
||||
},
|
||||
'PageLayout': {
|
||||
[DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端
|
||||
[DeviceType.DESKTOP]: '@/layouts/PageLayout.vue' // web桌面端
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建响应式路由组件
|
||||
*/
|
||||
export function createResponsiveRoute(adapter: RouteAdapter): () => Promise<any> {
|
||||
return async () => {
|
||||
const deviceType = getDeviceType();
|
||||
|
||||
try {
|
||||
// 尝试加载设备特定的组件
|
||||
if (deviceType === DeviceType.MOBILE && adapter.mobile) {
|
||||
return await adapter.mobile();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load device-specific component for ${deviceType}, falling back to original:`, error);
|
||||
}
|
||||
|
||||
// 回退到原始组件(桌面端/web)
|
||||
return await adapter.original();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应式Layout组件
|
||||
*/
|
||||
export function getResponsiveLayout(layoutName: string): () => Promise<any> {
|
||||
const deviceType = getDeviceType();
|
||||
const layoutMap = LAYOUT_MAP[layoutName];
|
||||
|
||||
if (!layoutMap) {
|
||||
console.warn(`Unknown layout: ${layoutName}, using original`);
|
||||
// 使用具体的导入路径
|
||||
switch (layoutName) {
|
||||
case 'BlankLayout':
|
||||
return () => import('@/layouts/BlankLayout.vue');
|
||||
case 'NavigationLayout':
|
||||
return () => import('@/layouts/NavigationLayout.vue');
|
||||
case 'SidebarLayout':
|
||||
return () => import('@/layouts/SidebarLayout.vue');
|
||||
case 'BasicLayout':
|
||||
return () => import('@/layouts/BasicLayout.vue');
|
||||
case 'PageLayout':
|
||||
return () => import('@/layouts/PageLayout.vue');
|
||||
default:
|
||||
throw new Error(`Unknown layout: ${layoutName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const targetLayout = layoutMap[deviceType];
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
// 使用具体的导入路径而不是动态路径
|
||||
switch (targetLayout) {
|
||||
case '@/layouts/BlankLayout.vue':
|
||||
return await import('@/layouts/BlankLayout.vue');
|
||||
case '@/layouts/NavigationLayout.vue':
|
||||
return await import('@/layouts/NavigationLayout.vue');
|
||||
case '@/layouts/SidebarLayout.vue':
|
||||
return await import('@/layouts/SidebarLayout.vue');
|
||||
case '@/layouts/BasicLayout.vue':
|
||||
return await import('@/layouts/BasicLayout.vue');
|
||||
case '@/layouts/MobileLayout.vue':
|
||||
return await import('@/layouts/MobileLayout.vue');
|
||||
case '@/layouts/PageLayout.vue':
|
||||
return await import('@/layouts/PageLayout.vue');
|
||||
default:
|
||||
throw new Error(`Unknown layout path: ${targetLayout}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load responsive layout ${targetLayout}, falling back to original:`, error);
|
||||
// 回退到原始layout
|
||||
switch (layoutName) {
|
||||
case 'BlankLayout':
|
||||
return await import('@/layouts/BlankLayout.vue');
|
||||
case 'NavigationLayout':
|
||||
return await import('@/layouts/NavigationLayout.vue');
|
||||
case 'SidebarLayout':
|
||||
return await import('@/layouts/SidebarLayout.vue');
|
||||
case 'BasicLayout':
|
||||
return await import('@/layouts/BasicLayout.vue');
|
||||
case 'PageLayout':
|
||||
return await import('@/layouts/PageLayout.vue');
|
||||
default:
|
||||
throw new Error(`Unknown layout: ${layoutName}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自适应路由配置
|
||||
*/
|
||||
export function createAdaptiveRoute(
|
||||
path: string,
|
||||
originalComponent: string,
|
||||
layoutName?: string,
|
||||
meta?: any
|
||||
): RouteRecordRaw {
|
||||
// 创建具体的导入函数而不是使用动态路径
|
||||
const getOriginalComponent = () => {
|
||||
switch (originalComponent) {
|
||||
case '@/views/public/login/Login.vue':
|
||||
return import('@/views/public/login/Login.vue');
|
||||
case '@/views/public/login/Register.vue':
|
||||
return import('@/views/public/login/Register.vue');
|
||||
case '@/views/public/login/ForgotPassword.vue':
|
||||
return import('@/views/public/login/ForgotPassword.vue');
|
||||
default:
|
||||
throw new Error(`Unknown component: ${originalComponent}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getMobileComponent = (): (() => Promise<any>) | null => {
|
||||
const mobilePath = MOBILE_ROUTES_MAP[path];
|
||||
if (!mobilePath) return null;
|
||||
|
||||
switch (mobilePath) {
|
||||
case '@/views/public/login/Login.mobile.vue':
|
||||
return () => import('@/views/public/login/Login.mobile.vue');
|
||||
case '@/views/public/login/Register.mobile.vue':
|
||||
return () => import('@/views/public/login/Register.mobile.vue');
|
||||
case '@/views/public/login/ForgotPassword.mobile.vue':
|
||||
return () => import('@/views/public/login/ForgotPassword.mobile.vue');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const adapter: RouteAdapter = {
|
||||
original: getOriginalComponent
|
||||
};
|
||||
|
||||
// 检查是否有移动端版本
|
||||
const mobileImportFunction = getMobileComponent();
|
||||
if (mobileImportFunction) {
|
||||
adapter.mobile = mobileImportFunction;
|
||||
}
|
||||
|
||||
// 如果指定了Layout,应用响应式Layout
|
||||
if (layoutName) {
|
||||
const route: RouteRecordRaw = {
|
||||
path,
|
||||
component: getResponsiveLayout(layoutName),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: createResponsiveRoute(adapter),
|
||||
meta
|
||||
}
|
||||
],
|
||||
meta
|
||||
};
|
||||
return route;
|
||||
}
|
||||
|
||||
const route: RouteRecordRaw = {
|
||||
path,
|
||||
component: createResponsiveRoute(adapter),
|
||||
meta
|
||||
};
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听屏幕尺寸变化,重新加载路由
|
||||
*/
|
||||
export function setupRouteWatcher(router: any) {
|
||||
let currentDeviceType = getDeviceType();
|
||||
|
||||
const handleResize = () => {
|
||||
const newDeviceType = getDeviceType();
|
||||
if (newDeviceType !== currentDeviceType) {
|
||||
currentDeviceType = newDeviceType;
|
||||
// 重新加载当前路由以应用新的组件
|
||||
const currentRoute = router.currentRoute.value;
|
||||
router.replace({
|
||||
...currentRoute,
|
||||
query: {
|
||||
...currentRoute.query,
|
||||
_refresh: Date.now() // 强制重新加载
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user