From 0a7241636547ef0c3d576e131491f507e7327b7d Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Fri, 12 Dec 2025 18:17:38 +0800 Subject: [PATCH] =?UTF-8?q?mock=E6=95=B0=E6=8D=AE=EF=BC=8CAI=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=EF=BC=8C=E5=85=A8=E9=83=A8=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packages/platform/ROUTE_GUIDE.md | 382 +++++++++ .../packages/platform/src/api/ai/agent.ts | 56 ++ .../packages/platform/src/api/ai/chat.ts | 35 + .../packages/platform/src/api/ai/index.ts | 2 + .../packages/platform/src/api/index.ts | 1 + .../layouts/SidebarLayout/SidebarLayout.vue | 143 ++-- .../platform/src/router/dynamicRoute.ts | 147 ++++ .../packages/platform/src/router/index.ts | 54 +- .../packages/platform/src/types/shared.d.ts | 126 ++- .../platform/src/views/common/IframeView.vue | 90 ++ .../public/Agents/AgentPlatformView.scss | 68 ++ .../views/public/Agents/AgentPlatformView.vue | 256 ++++++ .../src/views/public/Agents/README.md | 259 ++++++ .../components/AgentCard/AgentCard.scss | 69 ++ .../Agents/components/AgentCard/AgentCard.vue | 43 + .../components/AgentEdit/AgentEdit.scss | 88 ++ .../Agents/components/AgentEdit/AgentEdit.vue | 252 ++++++ .../src/views/public/Chat/AIChatView.scss | 386 +++++++++ .../src/views/public/Chat/AIChatView.vue | 427 ++++++++++ .../platform/src/views/public/Chat/README.md | 176 ++++ .../components/ChatDefault/ChatDefault.scss | 82 ++ .../components/ChatDefault/ChatDefault.vue | 109 +++ .../components/ChatHistory/ChatHistory.scss | 0 .../components/ChatHistory/ChatHistory.vue | 0 .../src/views/public/{ => Login}/Login.vue | 31 +- urbanLifelineWeb/packages/shared/EXPOSES.md | 195 +++++ .../packages/shared/ROUTE_REFACTOR.md | 287 +++++++ urbanLifelineWeb/packages/shared/server.js | 73 -- .../packages/shared/src/api/auth/auth.ts | 45 +- .../packages/shared/src/types/auth/auth.ts | 67 +- .../packages/shared/src/types/enums.ts | 10 + .../packages/shared/src/types/index.ts | 1 + .../shared/src/types/sys/permission.ts | 6 + .../packages/shared/src/utils/device.ts | 109 +++ .../packages/shared/src/utils/index.ts | 5 +- .../packages/shared/src/utils/route/index.ts | 19 + .../shared/src/utils/route/route-generator.ts | 789 ++++++++++++++++++ .../packages/shared/vite.config.ts | 28 +- urbanLifelineWeb/temp/deviceUtils.ts | 109 +++ urbanLifelineWeb/temp/route-generator.ts | 586 +++++++++++++ urbanLifelineWeb/temp/routeAdapter.ts | 261 ++++++ 41 files changed, 5667 insertions(+), 205 deletions(-) create mode 100644 urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md create mode 100644 urbanLifelineWeb/packages/platform/src/api/ai/agent.ts create mode 100644 urbanLifelineWeb/packages/platform/src/api/ai/chat.ts create mode 100644 urbanLifelineWeb/packages/platform/src/api/ai/index.ts create mode 100644 urbanLifelineWeb/packages/platform/src/api/index.ts create mode 100644 urbanLifelineWeb/packages/platform/src/router/dynamicRoute.ts create mode 100644 urbanLifelineWeb/packages/platform/src/views/common/IframeView.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/AgentPlatformView.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/AgentPlatformView.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/README.md create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/components/AgentCard/AgentCard.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/components/AgentCard/AgentCard.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/components/AgentEdit/AgentEdit.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Agents/components/AgentEdit/AgentEdit.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/AIChatView.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/AIChatView.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/README.md create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/components/ChatDefault/ChatDefault.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/components/ChatDefault/ChatDefault.vue create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/components/ChatHistory/ChatHistory.scss create mode 100644 urbanLifelineWeb/packages/platform/src/views/public/Chat/components/ChatHistory/ChatHistory.vue rename urbanLifelineWeb/packages/platform/src/views/public/{ => Login}/Login.vue (83%) create mode 100644 urbanLifelineWeb/packages/shared/EXPOSES.md create mode 100644 urbanLifelineWeb/packages/shared/ROUTE_REFACTOR.md delete mode 100644 urbanLifelineWeb/packages/shared/server.js create mode 100644 urbanLifelineWeb/packages/shared/src/types/enums.ts create mode 100644 urbanLifelineWeb/packages/shared/src/utils/device.ts create mode 100644 urbanLifelineWeb/packages/shared/src/utils/route/index.ts create mode 100644 urbanLifelineWeb/packages/shared/src/utils/route/route-generator.ts create mode 100644 urbanLifelineWeb/temp/deviceUtils.ts create mode 100644 urbanLifelineWeb/temp/route-generator.ts create mode 100644 urbanLifelineWeb/temp/routeAdapter.ts diff --git a/urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md b/urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md new file mode 100644 index 00000000..e05408c6 --- /dev/null +++ b/urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md @@ -0,0 +1,382 @@ +# Platform 路由集成指南 + +## 快速开始 + +### TL;DR + +1. **shared 提供**:路由生成工具、菜单处理、设备检测 +2. **platform 定义**:布局组件映射、视图加载器 +3. **platform 生成**:调用 `generateRoutes()` 生成自己的路由 + +```typescript +// 1. 导入工具 +import { generateRoutes, type RouteGeneratorConfig } from 'shared/utils/route' +import type { SysMenu } from 'shared/types' + +// 2. 配置生成器 +const config: RouteGeneratorConfig = { + layoutMap: { /* 布局映射 */ }, + viewLoader: (path) => { /* 组件加载 */ } +} + +// 3. 生成路由 +const routes = generateRoutes(menus, config) + +// 4. 添加到路由 +routes.forEach(route => router.addRoute(route)) +``` + +## 架构说明 + +Platform 使用 shared 提供的路由生成工具来动态生成路由。架构如下: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ shared │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ utils/route/route-generator.ts │ │ +│ │ - generateRoutes() 路由生成 │ │ +│ │ - buildMenuTree() 菜单树构建 │ │ +│ │ - filterMenusByPermissions() 权限过滤 │ │ +│ │ - findMenuByPath() 路径查找 │ │ +│ │ - getMenuPath() 面包屑路径 │ │ +│ │ - getFirstAccessibleMenuUrl() 首页跳转 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ utils/device.ts │ │ +│ │ - getDeviceType() 设备类型检测 │ │ +│ │ - isMobile() 移动端判断 │ │ +│ │ - useDevice() 响应式 Hook │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ types/sys/menu.ts & types/enums.ts │ │ +│ │ - SysMenu 菜单接口 │ │ +│ │ - MenuType 菜单类型枚举 │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────┐ + │ Module Federation (远程模块) │ + │ - shared/utils/route │ + │ - shared/utils/device │ + │ - shared/types │ + └─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ platform │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ src/router/index.ts │ │ +│ │ 1. 定义布局组件映射 (platformLayoutMap) │ │ +│ │ 2. 定义视图组件加载器 (viewLoader) │ │ +│ │ 3. 调用 generateRoutes() 生成动态路由 │ │ +│ │ 4. 添加到 Vue Router │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ src/layouts/ │ │ +│ │ - SidebarLayout.vue 侧边栏布局 │ │ +│ │ - NavigationLayout.vue 导航布局(如需) │ │ +│ │ - BasicLayout.vue 基础布局(如需) │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ src/views/ │ │ +│ │ - 各种页面组件 │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 使用步骤 + +### 1. 在 router/index.ts 中配置路由生成器 + +```typescript +import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' +import { generateRoutes, type RouteGeneratorConfig } from 'shared/utils/route' +import type { SysMenu } from 'shared/types' +import { SidebarLayout } from '../layouts' + +// 1. 定义布局组件映射 +const platformLayoutMap: Record Promise> = { + 'SidebarLayout': () => Promise.resolve({ default: SidebarLayout }), + 'NavigationLayout': () => Promise.resolve({ default: SidebarLayout }), // 可复用或自定义 + 'BasicLayout': () => Promise.resolve({ default: SidebarLayout }), +} + +// 2. 定义视图组件加载器 +const VIEW_MODULES = import.meta.glob('../views/**/*.vue') + +function viewLoader(componentPath: string): (() => Promise) | null { + // 将后台路径转换为实际路径 + let path = componentPath + if (!path.startsWith('../')) { + if (!path.startsWith('/')) { + path = '/' + path + } + path = '../views' + path + } + if (!path.endsWith('.vue')) { + path += '.vue' + } + + const loader = VIEW_MODULES[path] + return loader ? (loader as () => Promise) : null +} + +// 3. 创建路由生成器配置 +const routeConfig: RouteGeneratorConfig = { + layoutMap: platformLayoutMap, + viewLoader, + staticRoutes: routes, // 可选:静态路由 + notFoundComponent: () => import('../views/public/404.vue') // 可选 +} + +// 4. 动态添加路由的函数 +export function addDynamicRoutes(menus: SysMenu[]) { + const dynamicRoutes = generateRoutes(menus, routeConfig) + + dynamicRoutes.forEach(route => { + router.addRoute(route) + }) + + console.log('✅ 动态路由已添加', dynamicRoutes.length, '个') +} +``` + +### 2. 在应用初始化时调用 + +```typescript +// main.ts 或登录成功后 +import { addDynamicRoutes } from './router' +import { menuAPI } from 'shared/api' + +// 获取用户菜单 +const response = await menuAPI.getUserMenus() +if (response.success && response.data) { + // 添加动态路由 + addDynamicRoutes(response.data) + + // 跳转到首页或指定页面 + router.push('/home') +} +``` + +## 菜单数据格式 + +```typescript +interface SysMenu { + menuID: string // 菜单ID,作为路由 name + parentID?: string // 父菜单ID,'0' 表示根菜单 + name: string // 菜单名称 + url?: string // 路由路径,如 '/user/profile' + type: MenuType // 菜单类型 + icon?: string // 图标 + component?: string // 组件路径,如 'user/profile/ProfileView' + layout?: string // 布局名称,如 'SidebarLayout' + orderNum?: number // 排序号 + permission?: string // 权限标识 + hidden?: boolean // 是否隐藏 + children?: SysMenu[] // 子菜单 +} + +enum MenuType { + NAVIGATION = 'navigation', // 导航菜单(顶部导航) + SIDEBAR = 'sidebar', // 侧边栏菜单 + MENU = 'menu', // 普通菜单 + PAGE = 'page', // 页面(独立路由) + BUTTON = 'button' // 按钮(不生成路由) +} +``` + +## 示例菜单数据 + +```typescript +const menus: SysMenu[] = [ + { + menuID: 'user-center', + parentID: '0', + name: '用户中心', + url: '/user', + type: MenuType.NAVIGATION, + icon: 'User', + layout: 'SidebarLayout', + orderNum: 1, + children: [ + { + menuID: 'user-profile', + parentID: 'user-center', + name: '个人信息', + url: '/user/profile', + type: MenuType.MENU, + component: 'user/profile/ProfileView', + orderNum: 1 + }, + { + menuID: 'user-settings', + parentID: 'user-center', + name: '账号设置', + url: '/user/settings', + type: MenuType.MENU, + component: 'user/settings/SettingsView', + orderNum: 2 + } + ] + } +] +``` + +## 布局组件要求 + +布局组件必须包含 `` 用于渲染子路由: + +```vue + +``` + +## 响应式布局(可选) + +如果需要移动端适配,可以使用 shared 的设备检测工具: + +```typescript +import { getDeviceType, DeviceType } from 'shared/utils/device' + +const deviceType = getDeviceType() +if (deviceType === DeviceType.MOBILE) { + // 移动端逻辑 +} +``` + +## 工具方法说明 + +所有工具方法从 `shared/utils/route` 导入: + +```typescript +import { + generateRoutes, + buildMenuTree, + filterMenusByPermissions, + findMenuByPath, + getMenuPath, + getFirstAccessibleMenuUrl, + type RouteGeneratorConfig +} from 'shared/utils/route' + +import type { SysMenu, MenuType } from 'shared/types' +``` + +### generateRoutes(menus, config) +根据菜单生成路由配置数组 + +**参数:** +- `menus: SysMenu[]` - 菜单列表 +- `config: RouteGeneratorConfig` - 路由生成器配置 + +**返回:** `RouteRecordRaw[]` + +### buildMenuTree(menus, staticRoutes?) +将扁平菜单列表转换为树形结构 + +**参数:** +- `menus: SysMenu[]` - 菜单列表 +- `staticRoutes?: RouteRecordRaw[]` - 静态路由(可选) + +**返回:** `SysMenu[]` + +### filterMenusByPermissions(menus, permissions) +根据权限过滤菜单 + +**参数:** +- `menus: SysMenu[]` - 菜单列表 +- `permissions: string[]` - 权限列表 + +**返回:** `SysMenu[]` + +### findMenuByPath(menus, path) +根据路径查找菜单项 + +**参数:** +- `menus: SysMenu[]` - 菜单列表 +- `path: string` - 路由路径 + +**返回:** `SysMenu | null` + +### getMenuPath(menus, targetMenuId) +获取菜单路径数组(用于面包屑导航) + +**参数:** +- `menus: SysMenu[]` - 菜单列表 +- `targetMenuId: string` - 目标菜单ID + +**返回:** `SysMenu[]` + +### getFirstAccessibleMenuUrl(menus) +获取第一个可访问的菜单URL(用于登录后跳转) + +**参数:** +- `menus: SysMenu[]` - 菜单列表 + +**返回:** `string | null` + +## 注意事项 + +1. **shared 服务必须先启动**:因为使用 Module Federation,platform 依赖 shared 的远程模块 +2. **布局组件必须包含 router-view**:否则子路由无法渲染 +3. **组件路径映射**:确保 `viewLoader` 能正确加载组件 +4. **静态路由优先**:如果菜单标记为 `__STATIC_ROUTE__`,不会重复生成路由 +5. **路由守卫**:记得在 `router.beforeEach` 中添加权限检查 + +## 调试技巧 + +1. **查看生成的路由**: +```typescript +console.log('所有路由:', router.getRoutes()) +console.log('路由数量:', router.getRoutes().length) +``` + +2. **查看菜单树结构**: +```typescript +import { buildMenuTree } from 'shared/utils/route' + +const tree = buildMenuTree(menus) +console.log('菜单树:', JSON.stringify(tree, null, 2)) +``` + +3. **查看当前路由信息**: +```typescript +console.log('当前路由:', router.currentRoute.value) +console.log('路由路径:', router.currentRoute.value.path) +console.log('路由参数:', router.currentRoute.value.params) +console.log('路由元数据:', router.currentRoute.value.meta) +``` + +4. **测试菜单查找**: +```typescript +import { findMenuByPath, getMenuPath } from 'shared/utils/route' + +// 根据路径查找菜单 +const menu = findMenuByPath(menus, '/user/profile') +console.log('找到的菜单:', menu) + +// 获取面包屑路径 +const breadcrumb = getMenuPath(menus, 'user-profile') +console.log('面包屑:', breadcrumb.map(m => m.name).join(' > ')) +``` + +5. **检查视图组件加载**: +```typescript +// 在 viewLoader 中添加日志 +function viewLoader(componentPath: string) { + console.log('尝试加载组件:', componentPath) + const path = /* 转换逻辑 */ + const loader = VIEW_MODULES[path] + console.log('找到的加载器:', loader ? '✅' : '❌') + return loader ? (loader as () => Promise) : null +} +``` diff --git a/urbanLifelineWeb/packages/platform/src/api/ai/agent.ts b/urbanLifelineWeb/packages/platform/src/api/ai/agent.ts new file mode 100644 index 00000000..0c3534ed --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/api/ai/agent.ts @@ -0,0 +1,56 @@ +import { api } from 'shared/api' +import type { ResultDomain } from 'shared/types' + + +/** + * 认证 API + * 通过 Gateway (8180) 访问 Auth Service (8181) + * 路由规则:/urban-lifeline/auth/** → auth-service/urban-lifeline/auth/** + */ +export const agentAPI = { + baseUrl: "/urban-lifeline/xxx",//随后端更新 + + /** + * 获取智能体列表 + * @returns 智能体列表 + */ + async agentList(): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, + + /** + * 新增智能体 + * @param agent 智能体信息 + * @returns 智能体信息 + */ + async addAgent(agent: any): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, + + /** + * 更新智能体 + * @param agent 智能体信息 + * @returns 智能体信息 + */ + async updateAgent(agent: any): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, + + /** + * 删除智能体 + * @param agentId 智能体ID + * @returns 智能体信息 + */ + async deleteAgent(agentId: string): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, + +} \ No newline at end of file diff --git a/urbanLifelineWeb/packages/platform/src/api/ai/chat.ts b/urbanLifelineWeb/packages/platform/src/api/ai/chat.ts new file mode 100644 index 00000000..0890204c --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/api/ai/chat.ts @@ -0,0 +1,35 @@ +import { api } from 'shared/api' +import type { ResultDomain } from 'shared/types' + + +/** + * 认证 API + * 通过 Gateway (8180) 访问 Auth Service (8181) + * 路由规则:/urban-lifeline/auth/** → auth-service/urban-lifeline/auth/** + */ +export const chatAPI = { + baseUrl: "/urban-lifeline/xxx",//随后端更新 + + /** + * 根据agentId获取聊天会话id + * @param agentId agentId + * @returns 发送结果 + */ + async chatHistory(agentId: string): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, + + /** + * 根据agentId和conversationId获取聊天记录 + * @param agentId agentId + * @param conversationId conversationId + * @returns 发送结果 + */ + async chatConversation(agentId: string, conversationId: string): Promise> { + // const response = await api.post(`${this.baseUrl}/send-sms-code`, { phone }) + // return response.data + return []; + }, +} \ No newline at end of file diff --git a/urbanLifelineWeb/packages/platform/src/api/ai/index.ts b/urbanLifelineWeb/packages/platform/src/api/ai/index.ts new file mode 100644 index 00000000..2ba168f1 --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/api/ai/index.ts @@ -0,0 +1,2 @@ +export * from './agent' +export * from './chat' diff --git a/urbanLifelineWeb/packages/platform/src/api/index.ts b/urbanLifelineWeb/packages/platform/src/api/index.ts new file mode 100644 index 00000000..a726f637 --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/api/index.ts @@ -0,0 +1 @@ +export * from './ai' \ No newline at end of file diff --git a/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue b/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue index ce24499a..61409b69 100644 --- a/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue +++ b/urbanLifelineWeb/packages/platform/src/layouts/SidebarLayout/SidebarLayout.vue @@ -113,8 +113,7 @@ interface MenuItem { key: string label: string icon: string - path?: string - iframeUrl?: string + url?: string type: 'route' | 'iframe' } @@ -126,57 +125,90 @@ const collapsed = ref(false) const activeMenu = ref('home') const iframeLoading = ref(false) const iframeRef = ref() -const userName = ref('管理员') -// 菜单配置 -const menuItems: MenuItem[] = [ - { - key: 'home', - label: '工作台', - icon: 'Grid', - path: '/home', - type: 'route' - }, - { - key: 'bidding', - label: '招标助手', - icon: 'Document', - iframeUrl: 'http://localhost:5002', - type: 'iframe' - }, - { - key: 'service', - label: '泰豪小电', - icon: 'Service', - iframeUrl: 'http://localhost:5003', - type: 'iframe' - }, - { - key: 'workflow', - label: '智能体编排', - icon: 'Connection', - iframeUrl: 'http://localhost:3000', - type: 'iframe' - }, - { - key: 'chat', - label: 'AI助手', - icon: 'ChatDotRound', - path: '/chat', - type: 'route' +// 从 LocalStorage 获取用户名 +function getUserName(): string { + try { + const loginDomainStr = localStorage.getItem('loginDomain') + if (loginDomainStr) { + const loginDomain = JSON.parse(loginDomainStr) + return loginDomain.user?.username || loginDomain.userInfo?.username || '管理员' + } + } catch (error) { + console.error('❌ 获取用户名失败:', error) } -] + return '管理员' +} + +const userName = ref(getUserName()) + +/** + * 从 LocalStorage 加载菜单 + */ +function loadMenuFromStorage(): MenuItem[] { + try { + const loginDomainStr = localStorage.getItem('loginDomain') + if (!loginDomainStr) { + console.warn('⚠️ 未找到 loginDomain') + return [] + } + + const loginDomain = JSON.parse(loginDomainStr) + const userViews = loginDomain.userViews || [] + + console.log('📋 加载用户视图:', userViews) + + // 过滤出 SidebarLayout 的顶级菜单(没有 parentId) + const sidebarViews = userViews.filter((view: any) => + view.layout === 'SidebarLayout' && + !view.parentId && + view.type === 1 // type 1 是侧边栏菜单 + ) + + // 按 orderNum 排序 + sidebarViews.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0)) + + // 转换为 MenuItem 格式 + const menuItems: MenuItem[] = sidebarViews.map((view: any) => { + // 根据 viewType 或 iframeUrl 判断是 route 还是 iframe + const isIframe = view.viewType === 'iframe' || !!view.iframeUrl + + // 确定菜单的路由路径 + let menuUrl = view.url + if (isIframe && view.url && (view.url.startsWith('http://') || view.url.startsWith('https://'))) { + // iframe 类型且 url 是外部链接,使用 viewId 作为路由路径 + menuUrl = `/${view.viewId}` + } + + return { + key: view.viewId || view.name, + label: view.name, + icon: view.icon || 'Grid', + url: menuUrl, + type: isIframe ? 'iframe' : 'route' + } + }) + + console.log('✅ 侧边栏菜单:', menuItems) + return menuItems + } catch (error) { + console.error('❌ 加载菜单失败:', error) + return [] + } +} + +// 菜单配置(从 LocalStorage 加载) +const menuItems = ref(loadMenuFromStorage()) // 当前菜单项 const currentMenuItem = computed(() => { - return menuItems.find(item => item.key === activeMenu.value) + return menuItems.value.find(item => item.key === activeMenu.value) }) -// 当前 iframe URL +// 当前 iframe URL(从路由 meta 读取) const currentIframeUrl = computed(() => { - return currentMenuItem.value?.type === 'iframe' - ? currentMenuItem.value.iframeUrl - : null + const meta = route.meta as any + return meta?.iframeUrl || null }) // 切换侧边栏 @@ -188,11 +220,12 @@ const toggleSidebar = () => { const handleMenuClick = (item: MenuItem) => { activeMenu.value = item.key - if (item.type === 'route' && item.path) { - router.push(item.path) - } else if (item.type === 'iframe') { - iframeLoading.value = true - // iframe 模式不需要路由跳转 + // 所有菜单都通过路由跳转 + if (item.url) { + router.push(item.url) + if (item.type === 'iframe') { + iframeLoading.value = true + } } } @@ -224,6 +257,7 @@ const handleUserCommand = (command: string) => { router.push('/settings') break case 'logout': + localStorage.clear() ElMessage.success('退出成功') router.push('/login') break @@ -234,9 +268,16 @@ const handleUserCommand = (command: string) => { watch( () => route.path, (newPath) => { - const menuItem = menuItems.find(item => item.path === newPath) + // 查找匹配的菜单项(route 或 iframe 类型) + const menuItem = menuItems.value.find((item: MenuItem) => item.url === newPath) if (menuItem) { activeMenu.value = menuItem.key + } else { + // 如果路径不匹配,尝试通过 route.name 匹配 viewId + const menuByName = menuItems.value.find((item: MenuItem) => item.key === route.name) + if (menuByName) { + activeMenu.value = menuByName.key + } } }, { immediate: true } diff --git a/urbanLifelineWeb/packages/platform/src/router/dynamicRoute.ts b/urbanLifelineWeb/packages/platform/src/router/dynamicRoute.ts new file mode 100644 index 00000000..31334a39 --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/router/dynamicRoute.ts @@ -0,0 +1,147 @@ +/** + * 动态路由生成模块(Platform 特定) + * + * 职责: + * 1. 提供 Platform 特定的布局和组件配置 + * 2. 调用 shared 中的通用路由生成方法 + * 3. 将生成的路由添加到 Platform 的 router 实例 + */ + +/// + +import { + generateSimpleRoutes, + loadViewsFromStorage, + type RouteGeneratorConfig, + type GenerateSimpleRoutesOptions +} from 'shared/utils/route' +import type { TbSysViewDTO } from 'shared/types' +import type { RouteRecordRaw } from 'vue-router' +import router from './index' +import { SidebarLayout } from '../layouts' + +// Platform 布局组件映射 +const platformLayoutMap: Record Promise> = { + 'SidebarLayout': () => Promise.resolve({ default: SidebarLayout }), + 'NavigationLayout': () => Promise.resolve({ default: SidebarLayout }), + 'BasicLayout': () => Promise.resolve({ default: SidebarLayout }) +} + +// 视图组件加载器 +const VIEW_MODULES = import.meta.glob<{ default: any }>('../views/**/*.vue') + +/** + * 视图组件加载函数 + * @param componentPath 组件路径 + */ +function viewLoader(componentPath: string): (() => Promise) | null { + // 将后台路径转换为实际路径 + let path = componentPath + + // 如果不是以 ../ 开头,则认为是相对 views 目录的路径 + if (!path.startsWith('../')) { + if (!path.startsWith('/')) { + path = '/' + path + } + path = '../views' + path + } + + // 补全 .vue 后缀 + if (!path.endsWith('.vue')) { + path += '.vue' + } + + const loader = VIEW_MODULES[path] + + if (!loader) { + console.warn(`[路由生成] 未找到组件: ${componentPath},期望路径: ${path}`) + return null + } + + return loader as () => Promise +} + +// Platform 路由生成器配置 +const routeConfig: RouteGeneratorConfig = { + layoutMap: platformLayoutMap, + viewLoader, + notFoundComponent: () => Promise.resolve({ + default: { + template: '

404 - 页面未找到

' + } + }) +} + +// Platform 路由生成选项 +const routeOptions: GenerateSimpleRoutesOptions = { + asRootChildren: true, // 作为 Root 路由的子路由 + iframePlaceholder: () => Promise.resolve({ + default: { + template: '
' + } + }), + verbose: true // 启用详细日志 +} + +/** + * 添加动态路由(Platform 特定) + * @param views 视图列表(用作菜单) + */ +export function addDynamicRoutes(views: TbSysViewDTO[]) { + if (!views || views.length === 0) { + console.warn('[Platform 路由] 视图列表为空') + return + } + + console.log('[Platform 路由] 开始生成路由,视图数量:', views.length) + + try { + // 使用 shared 中的通用方法生成路由 + const routes = generateSimpleRoutes(views, routeConfig, routeOptions) + + // 将生成的路由添加到 Platform 的 router + routes.forEach(route => { + router.addRoute('Root', route) + console.log('[Platform 路由] 已添加路由:', { + path: route.path, + name: route.name, + hasComponent: !!route.component, + childrenCount: route.children?.length || 0 + }) + }) + + console.log('✅ Platform 动态路由添加完成') + console.log('所有路由:', router.getRoutes().map(r => ({ path: r.path, name: r.name }))) + } catch (error) { + console.error('❌ Platform 动态路由生成失败:', error) + throw error + } +} + +// ============================================ +// 以下为 Platform 特有的辅助函数 +// 通用的路由生成逻辑已迁移到 shared/utils/route +// ============================================ + +/** + * 从 LocalStorage 获取菜单并生成路由(Platform 特定) + * + * 使用 shared 中的通用 loadViewsFromStorage 方法 + */ +export function loadRoutesFromStorage(): boolean { + try { + // 使用 shared 中的通用方法加载视图数据 + const views = loadViewsFromStorage('loginDomain', 'userViews') + + if (views) { + // 使用 Platform 的 addDynamicRoutes 添加路由 + addDynamicRoutes(views) + return true + } + + return false + } catch (error) { + console.error('[Platform 路由] 从 LocalStorage 加载路由失败:', error) + return false + } +} diff --git a/urbanLifelineWeb/packages/platform/src/router/index.ts b/urbanLifelineWeb/packages/platform/src/router/index.ts index 627f8462..487af0a9 100644 --- a/urbanLifelineWeb/packages/platform/src/router/index.ts +++ b/urbanLifelineWeb/packages/platform/src/router/index.ts @@ -1,29 +1,23 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { SidebarLayout } from '../layouts' import { TokenManager } from 'shared/api' +import { loadRoutesFromStorage } from './dynamicRoute' const routes: RouteRecordRaw[] = [ { path: '/', - redirect: '/home' + name: 'Root', + component: SidebarLayout, + children: [] }, { path: '/login', name: 'Login', - component: () => import('../views/public/Login.vue'), + component: () => import('@/views/public/Login/Login.vue'), meta: { title: '登录', requiresAuth: false // 不需要登录 } - }, - { - path: '/home', - name: 'Home', - component: SidebarLayout, - meta: { - title: '首页', - requiresAuth: true // 需要登录 - } } ] @@ -32,6 +26,9 @@ const router = createRouter({ routes }) +// 标记动态路由是否已加载 +let dynamicRoutesLoaded = false + // 路由守卫 router.beforeEach((to, from, next) => { // 设置页面标题 @@ -49,12 +46,37 @@ router.beforeEach((to, from, next) => { path: '/login', query: { redirect: to.fullPath } // 保存原始路径 }) - } else if (to.path === '/login' && hasToken) { - // 已登录但访问登录页,跳转到首页 - next('/home') - } else { - next() + return } + + if (to.path === '/login' && hasToken) { + // 已登录但访问登录页,跳转到首页 + next('/') + return + } + + // 如果已登录且动态路由未加载,先加载动态路由 + if (hasToken && !dynamicRoutesLoaded) { + dynamicRoutesLoaded = true + const loaded = loadRoutesFromStorage() + + if (loaded && to.path !== '/') { + // 动态路由已加载,重新导航到目标路由 + next({ ...to, replace: true }) + return + } + } + + next() }) +// 导出动态路由生成函数 +export { addDynamicRoutes, loadRoutesFromStorage } from './dynamicRoute' + +// 重置动态路由加载状态(用于登录后重新加载) +export function resetDynamicRoutes() { + dynamicRoutesLoaded = false +} + +// 导出路由和辅助函数 export default router diff --git a/urbanLifelineWeb/packages/platform/src/types/shared.d.ts b/urbanLifelineWeb/packages/platform/src/types/shared.d.ts index 327fe324..6bec6970 100644 --- a/urbanLifelineWeb/packages/platform/src/types/shared.d.ts +++ b/urbanLifelineWeb/packages/platform/src/types/shared.d.ts @@ -3,7 +3,39 @@ * 用于 TypeScript 识别远程模块 */ +// ========== 组件模块 ========== +declare module 'shared/components' { + export const FileUpload: any + export const DynamicFormItem: any +} +declare module 'shared/components/FileUpload' { + import { DefineComponent } from 'vue' + const FileUpload: DefineComponent<{}, {}, any> + export default FileUpload +} + +declare module 'shared/components/DynamicFormItem' { + import { DefineComponent } from 'vue' + const DynamicFormItem: DefineComponent<{}, {}, any> + export default DynamicFormItem +} + +// ========== API 模块 ========== +declare module 'shared/api' { + export const api: any + export const TokenManager: any +} + +declare module 'shared/api/auth' { + export const authAPI: any +} + +declare module 'shared/api/file' { + export const fileAPI: any +} + +// 保留旧的导出路径(向后兼容) declare module 'shared/FileUpload' { import { DefineComponent } from 'vue' const FileUpload: DefineComponent<{}, {}, any> @@ -16,11 +48,6 @@ declare module 'shared/DynamicFormItem' { export default DynamicFormItem } -declare module 'shared/api' { - export const api: any - export const TokenManager: any -} - declare module 'shared/authAPI' { export const authAPI: any } @@ -40,15 +67,98 @@ declare module 'shared/utils' { } declare module 'shared/types' { + import { RouteRecordRaw } from 'vue-router' + export type LoginParam = any export type LoginDomain = any export type SysUserVO = any export type TbSysFileDTO = any export type SysConfigVO = any export type ResultDomain = any + + // 视图类型(用于路由和菜单) + export interface TbSysViewDTO { + viewId?: string + name?: string + parentId?: string + url?: string + component?: string + iframeUrl?: string + icon?: string + type?: number + layout?: string + orderNum?: number + description?: string + children?: TbSysViewDTO[] + } } -declare module 'shared/components' { - export const FileUpload: any - export const DynamicFormItem: any +declare module 'shared/utils/route' { + import { RouteRecordRaw } from 'vue-router' + import type { TbSysViewDTO } from 'shared/types' + + export interface RouteGeneratorConfig { + layoutMap: Record Promise> + viewLoader: (componentPath: string) => (() => Promise) | null + staticRoutes?: RouteRecordRaw[] + notFoundComponent?: () => Promise + } + + export interface GenerateSimpleRoutesOptions { + asRootChildren?: boolean + iframePlaceholder?: () => Promise + verbose?: boolean + } + + export function generateRoutes( + views: TbSysViewDTO[], + config: RouteGeneratorConfig + ): RouteRecordRaw[] + + export function generateSimpleRoutes( + views: TbSysViewDTO[], + config: RouteGeneratorConfig, + options?: GenerateSimpleRoutesOptions + ): RouteRecordRaw[] + + export function buildMenuTree( + views: TbSysViewDTO[], + staticRoutes?: RouteRecordRaw[] + ): TbSysViewDTO[] + + export function filterMenusByPermissions( + views: TbSysViewDTO[], + permissions: string[] + ): TbSysViewDTO[] + + export function findMenuByPath( + views: TbSysViewDTO[], + path: string + ): TbSysViewDTO | null + + export function getMenuPath( + views: TbSysViewDTO[], + targetViewId: string + ): TbSysViewDTO[] + + export function getFirstAccessibleMenuUrl( + views: TbSysViewDTO[] + ): string | null + + export function loadViewsFromStorage( + storageKey?: string, + viewsPath?: string + ): TbSysViewDTO[] | null +} + +declare module 'shared/utils/device' { + export enum DeviceType { + MOBILE = 'mobile', + DESKTOP = 'desktop' + } + + export function getDeviceType(): DeviceType + export function isMobile(): boolean + export function isDesktop(): boolean + export function useDevice(): any } diff --git a/urbanLifelineWeb/packages/platform/src/views/common/IframeView.vue b/urbanLifelineWeb/packages/platform/src/views/common/IframeView.vue new file mode 100644 index 00000000..1e35c572 --- /dev/null +++ b/urbanLifelineWeb/packages/platform/src/views/common/IframeView.vue @@ -0,0 +1,90 @@ +