mock数据,AI对话,全部应用
This commit is contained in:
195
urbanLifelineWeb/packages/shared/EXPOSES.md
Normal file
195
urbanLifelineWeb/packages/shared/EXPOSES.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Shared 模块导出规范
|
||||
|
||||
## 导出路径组织
|
||||
|
||||
为了保持代码的清晰性和可维护性,shared 包的模块导出已按照功能分类组织。
|
||||
|
||||
### 组件模块 (Components)
|
||||
|
||||
```typescript
|
||||
// 整体导出
|
||||
import { FileUpload, DynamicFormItem } from 'shared/components'
|
||||
|
||||
// 单独导入(推荐)
|
||||
import FileUpload from 'shared/components/FileUpload'
|
||||
import DynamicFormItem from 'shared/components/DynamicFormItem'
|
||||
```
|
||||
|
||||
**可用组件:**
|
||||
- `shared/components/FileUpload` - 文件上传组件(支持 cover/dialog/content 三种模式)
|
||||
- `shared/components/DynamicFormItem` - 动态表单项组件
|
||||
|
||||
---
|
||||
|
||||
### API 模块
|
||||
|
||||
```typescript
|
||||
// 整体导出
|
||||
import { api, TokenManager } from 'shared/api'
|
||||
|
||||
// 单独导入(推荐)
|
||||
import { authAPI } from 'shared/api/auth'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
```
|
||||
|
||||
**可用 API:**
|
||||
- `shared/api` - API 基础模块和 TokenManager
|
||||
- `shared/api/auth` - 认证相关 API
|
||||
- `shared/api/file` - 文件管理 API
|
||||
|
||||
---
|
||||
|
||||
### Utils 工具模块
|
||||
|
||||
```typescript
|
||||
// 整体导出
|
||||
import { formatFileSize, isImageFile } from 'shared/utils'
|
||||
|
||||
// 单独导入
|
||||
import { getDeviceType, isMobile } from 'shared/utils/device'
|
||||
import {
|
||||
generateSimpleRoutes,
|
||||
loadViewsFromStorage,
|
||||
buildMenuTree
|
||||
} from 'shared/utils/route'
|
||||
import { formatFileSize, isImageFile } from 'shared/utils/file'
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `shared/utils` - 通用工具函数集合
|
||||
- `shared/utils/device` - 设备检测工具
|
||||
- `shared/utils/route` - 路由生成和菜单构建工具
|
||||
- `generateSimpleRoutes()` - 生成简化路由(适合直接添加到 router)
|
||||
- `loadViewsFromStorage()` - 从 localStorage 加载视图数据
|
||||
- `buildMenuTree()` - 构建菜单树结构
|
||||
- `generateRoutes()` - 生成完整路由配置
|
||||
- 更多工具函数...
|
||||
- `shared/utils/file` - 文件处理工具
|
||||
|
||||
---
|
||||
|
||||
### Types 类型模块
|
||||
|
||||
```typescript
|
||||
// 整体导出
|
||||
import type { LoginParam, TbSysViewDTO } from 'shared/types'
|
||||
|
||||
// 单独导入
|
||||
import type { LoginParam, LoginDomain } from 'shared/types/auth'
|
||||
import type { TbSysFileDTO } from 'shared/types/file'
|
||||
import type { SysUserVO, SysConfigVO } from 'shared/types/sys'
|
||||
```
|
||||
|
||||
**可用类型:**
|
||||
- `shared/types` - 所有类型的统一导出
|
||||
- `shared/types/base` - 基础类型(BaseVO, BaseDTO 等)
|
||||
- `shared/types/auth` - 认证相关类型
|
||||
- `shared/types/file` - 文件相关类型
|
||||
- `shared/types/sys` - 系统相关类型
|
||||
|
||||
---
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
为了保持向后兼容,旧的导入路径仍然可用(但不推荐):
|
||||
|
||||
```typescript
|
||||
// ❌ 旧路径(不推荐,但仍可用)
|
||||
import FileUpload from 'shared/FileUpload'
|
||||
import { authAPI } from 'shared/authAPI'
|
||||
import { fileAPI } from 'shared/fileAPI'
|
||||
|
||||
// ✅ 新路径(推荐)
|
||||
import FileUpload from 'shared/components/FileUpload'
|
||||
import { authAPI } from 'shared/api/auth'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用明确的路径
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:路径清晰,便于理解
|
||||
import FileUpload from 'shared/components/FileUpload'
|
||||
import { authAPI } from 'shared/api/auth'
|
||||
|
||||
// ❌ 不推荐:路径模糊
|
||||
import FileUpload from 'shared/FileUpload'
|
||||
```
|
||||
|
||||
### 2. 按需导入
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:只导入需要的模块
|
||||
import { formatFileSize } from 'shared/utils/file'
|
||||
|
||||
// ❌ 不推荐:导入整个模块
|
||||
import * as utils from 'shared/utils'
|
||||
```
|
||||
|
||||
### 3. 使用类型导入
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确标识类型导入
|
||||
import type { LoginParam } from 'shared/types/auth'
|
||||
import { authAPI } from 'shared/api/auth'
|
||||
|
||||
// ❌ 不推荐:混合导入
|
||||
import { LoginParam, authAPI } from 'shared/types'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 添加新模块
|
||||
|
||||
如果需要添加新的导出模块,请按照以下步骤:
|
||||
|
||||
1. **在 vite.config.ts 中添加导出**
|
||||
```typescript
|
||||
exposes: {
|
||||
'./components/YourComponent': './src/components/yourcomponent/YourComponent.vue'
|
||||
}
|
||||
```
|
||||
|
||||
2. **在 shared.d.ts 中添加类型声明**
|
||||
```typescript
|
||||
declare module 'shared/components/YourComponent' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const YourComponent: DefineComponent<{}, {}, any>
|
||||
export default YourComponent
|
||||
}
|
||||
```
|
||||
|
||||
3. **更新此文档**
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: TypeScript 报错找不到模块?
|
||||
A: 尝试以下方法:
|
||||
1. 重启 TypeScript 语言服务器(VS Code: `Ctrl+Shift+P` → Restart TS Server)
|
||||
2. 确认 `shared.d.ts` 文件已正确更新
|
||||
3. 检查 shared 包是否已正确构建
|
||||
|
||||
### Q: 运行时找不到模块?
|
||||
A: 确保:
|
||||
1. shared 包已启动(`npm run dev` 在 shared 目录)
|
||||
2. `vite.config.ts` 中的 exposes 配置正确
|
||||
3. Module Federation 配置正确加载
|
||||
|
||||
### Q: 如何查看所有可用模块?
|
||||
A: 查看 `packages/shared/vite.config.ts` 的 `exposes` 配置
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-12-12
|
||||
- ✅ 重新组织导出路径,使用清晰的分类前缀
|
||||
- ✅ 添加向后兼容性支持
|
||||
- ✅ 更新类型声明文件
|
||||
- ✅ 添加更多常用模块导出
|
||||
287
urbanLifelineWeb/packages/shared/ROUTE_REFACTOR.md
Normal file
287
urbanLifelineWeb/packages/shared/ROUTE_REFACTOR.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 路由生成逻辑重构说明
|
||||
|
||||
## 重构目标
|
||||
|
||||
将 Platform 中的通用路由生成逻辑提取到 shared 包中,使其他 web 服务也可以复用。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 职责划分
|
||||
|
||||
**Shared 包(shared/utils/route)**
|
||||
- ✅ 提供通用的路由生成方法
|
||||
- ✅ 提供视图树构建方法
|
||||
- ✅ 提供 localStorage 数据加载方法
|
||||
- ✅ 不依赖特定的 router 实例
|
||||
- ✅ 不依赖特定的组件加载方式
|
||||
|
||||
**Platform 包(platform/src/router/dynamicRoute.ts)**
|
||||
- ✅ 提供 Platform 特定的布局组件映射
|
||||
- ✅ 提供 Platform 特定的组件加载器
|
||||
- ✅ 调用 shared 中的通用方法
|
||||
- ✅ 将生成的路由添加到 Platform 的 router 实例
|
||||
|
||||
## 核心方法
|
||||
|
||||
### Shared 包新增方法
|
||||
|
||||
#### 1. `generateSimpleRoutes()`
|
||||
```typescript
|
||||
export function generateSimpleRoutes(
|
||||
views: TbSysViewDTO[],
|
||||
config: RouteGeneratorConfig,
|
||||
options?: GenerateSimpleRoutesOptions
|
||||
): RouteRecordRaw[]
|
||||
```
|
||||
|
||||
**功能**:生成简化的路由配置,适合直接添加到 router
|
||||
|
||||
**参数**:
|
||||
- `views` - 视图列表
|
||||
- `config` - 路由生成器配置(布局映射、组件加载器等)
|
||||
- `options` - 可选配置
|
||||
- `asRootChildren` - 是否作为根路由的子路由
|
||||
- `iframePlaceholder` - iframe 类型视图的占位组件
|
||||
- `verbose` - 是否启用详细日志
|
||||
|
||||
**返回**:路由配置数组
|
||||
|
||||
#### 2. `loadViewsFromStorage()`
|
||||
```typescript
|
||||
export function loadViewsFromStorage(
|
||||
storageKey?: string,
|
||||
viewsPath?: string
|
||||
): TbSysViewDTO[] | null
|
||||
```
|
||||
|
||||
**功能**:从 localStorage 加载视图数据
|
||||
|
||||
**参数**:
|
||||
- `storageKey` - localStorage 的 key(默认:'loginDomain')
|
||||
- `viewsPath` - 视图数据在对象中的路径(默认:'userViews',支持嵌套如 'user.views')
|
||||
|
||||
**返回**:视图列表,如果不存在返回 null
|
||||
|
||||
### Platform 包简化后的方法
|
||||
|
||||
#### 1. `addDynamicRoutes()`
|
||||
```typescript
|
||||
export function addDynamicRoutes(views: TbSysViewDTO[]) {
|
||||
// 使用 shared 中的通用方法生成路由
|
||||
const routes = generateSimpleRoutes(views, routeConfig, routeOptions)
|
||||
|
||||
// 将生成的路由添加到 Platform 的 router
|
||||
routes.forEach(route => {
|
||||
router.addRoute('Root', route)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `loadRoutesFromStorage()`
|
||||
```typescript
|
||||
export function loadRoutesFromStorage(): boolean {
|
||||
// 使用 shared 中的通用方法加载视图数据
|
||||
const views = loadViewsFromStorage('loginDomain', 'userViews')
|
||||
|
||||
if (views) {
|
||||
addDynamicRoutes(views)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 在 Platform 中使用
|
||||
|
||||
```typescript
|
||||
import { loadRoutesFromStorage, addDynamicRoutes } from '@/router/dynamicRoute'
|
||||
|
||||
// 从 localStorage 加载并添加路由
|
||||
loadRoutesFromStorage()
|
||||
|
||||
// 或者手动传入视图数据
|
||||
const views = [...] // 从 API 获取
|
||||
addDynamicRoutes(views)
|
||||
```
|
||||
|
||||
### 在其他 Web 服务中使用
|
||||
|
||||
```typescript
|
||||
import {
|
||||
generateSimpleRoutes,
|
||||
loadViewsFromStorage,
|
||||
type RouteGeneratorConfig
|
||||
} from 'shared/utils/route'
|
||||
import router from './router'
|
||||
|
||||
// 1. 配置路由生成器
|
||||
const config: RouteGeneratorConfig = {
|
||||
layoutMap: {
|
||||
'MyLayout': () => import('./layouts/MyLayout.vue')
|
||||
},
|
||||
viewLoader: (path) => {
|
||||
// 自定义组件加载逻辑
|
||||
return () => import(`./views/${path}.vue`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 加载视图数据
|
||||
const views = loadViewsFromStorage()
|
||||
|
||||
// 3. 生成路由
|
||||
if (views) {
|
||||
const routes = generateSimpleRoutes(views, config, {
|
||||
asRootChildren: true,
|
||||
verbose: true
|
||||
})
|
||||
|
||||
// 4. 添加到 router
|
||||
routes.forEach(route => {
|
||||
router.addRoute('Root', route)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### RouteGeneratorConfig
|
||||
|
||||
```typescript
|
||||
interface RouteGeneratorConfig {
|
||||
/** 布局组件映射表 */
|
||||
layoutMap: Record<string, () => Promise<any>>
|
||||
|
||||
/** 视图组件加载器 */
|
||||
viewLoader: (componentPath: string) => (() => Promise<any>) | null
|
||||
|
||||
/** 静态路由列表(可选) */
|
||||
staticRoutes?: RouteRecordRaw[]
|
||||
|
||||
/** 404 组件(可选) */
|
||||
notFoundComponent?: () => Promise<any>
|
||||
}
|
||||
```
|
||||
|
||||
### GenerateSimpleRoutesOptions
|
||||
|
||||
```typescript
|
||||
interface GenerateSimpleRoutesOptions {
|
||||
/** 是否作为根路由的子路由(路径去掉前导 /) */
|
||||
asRootChildren?: boolean
|
||||
|
||||
/** iframe 类型视图的占位组件 */
|
||||
iframePlaceholder?: () => Promise<any>
|
||||
|
||||
/** 是否启用详细日志 */
|
||||
verbose?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
### 1. 代码复用
|
||||
- ✅ 通用逻辑只需维护一份
|
||||
- ✅ 其他 web 服务可以直接使用
|
||||
- ✅ 减少重复代码
|
||||
|
||||
### 2. 职责清晰
|
||||
- ✅ Shared 负责通用逻辑
|
||||
- ✅ 各个服务负责特定配置
|
||||
- ✅ 易于理解和维护
|
||||
|
||||
### 3. 灵活性
|
||||
- ✅ 通过配置注入实现定制化
|
||||
- ✅ 支持多种使用方式
|
||||
- ✅ 易于扩展
|
||||
|
||||
### 4. 可测试性
|
||||
- ✅ 通用方法独立测试
|
||||
- ✅ 配置化便于 mock
|
||||
- ✅ 减少耦合
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果其他服务想要使用 shared 中的路由生成逻辑:
|
||||
|
||||
### 步骤 1:准备配置
|
||||
|
||||
```typescript
|
||||
// 1. 准备布局组件映射
|
||||
const layoutMap = {
|
||||
'MainLayout': () => import('./layouts/MainLayout.vue'),
|
||||
'BlankLayout': () => import('./layouts/BlankLayout.vue')
|
||||
}
|
||||
|
||||
// 2. 准备组件加载器
|
||||
const VIEW_MODULES = import.meta.glob('./views/**/*.vue')
|
||||
const viewLoader = (path: string) => {
|
||||
const fullPath = `./views/${path}.vue`
|
||||
return VIEW_MODULES[fullPath] || null
|
||||
}
|
||||
|
||||
// 3. 组装配置
|
||||
const config: RouteGeneratorConfig = {
|
||||
layoutMap,
|
||||
viewLoader
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:加载视图数据
|
||||
|
||||
```typescript
|
||||
import { loadViewsFromStorage } from 'shared/utils/route'
|
||||
|
||||
// 从 localStorage 加载
|
||||
const views = loadViewsFromStorage('loginDomain', 'userViews')
|
||||
|
||||
// 或从 API 加载
|
||||
// const views = await api.getUserViews()
|
||||
```
|
||||
|
||||
### 步骤 3:生成并添加路由
|
||||
|
||||
```typescript
|
||||
import { generateSimpleRoutes } from 'shared/utils/route'
|
||||
|
||||
if (views) {
|
||||
const routes = generateSimpleRoutes(views, config, {
|
||||
asRootChildren: true,
|
||||
verbose: process.env.NODE_ENV === 'development'
|
||||
})
|
||||
|
||||
routes.forEach(route => {
|
||||
router.addRoute('YourRootRouteName', route)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **组件加载器**:每个服务的组件路径可能不同,需要自行实现 `viewLoader`
|
||||
2. **布局组件**:需要提供服务特定的布局组件映射
|
||||
3. **路由实例**:需要自行将生成的路由添加到服务的 router 实例
|
||||
4. **iframe 支持**:如果需要支持 iframe 视图,需要提供占位组件
|
||||
|
||||
## 文件变更
|
||||
|
||||
### 新增文件
|
||||
- `shared/src/utils/route/route-generator.ts` - 新增通用方法
|
||||
|
||||
### 修改文件
|
||||
- `shared/src/utils/route/index.ts` - 新增导出
|
||||
- `shared/vite.config.ts` - 新增导出配置
|
||||
- `shared/EXPOSES.md` - 更新文档
|
||||
- `platform/src/router/dynamicRoute.ts` - 简化代码
|
||||
- `platform/src/types/shared.d.ts` - 新增类型声明
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-12-12
|
||||
- ✅ 将路由生成通用逻辑提取到 shared
|
||||
- ✅ 新增 `generateSimpleRoutes` 方法
|
||||
- ✅ 新增 `loadViewsFromStorage` 方法
|
||||
- ✅ 简化 Platform 的 dynamicRoute.ts
|
||||
- ✅ 更新文档和类型声明
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Shared 模块静态文件服务器
|
||||
* 提供构建后的 ES Module 文件供其他应用使用
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. npm run build:esm # 先构建
|
||||
* 2. node server.js # 启动服务器
|
||||
*/
|
||||
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const app = express()
|
||||
const PORT = 5000
|
||||
|
||||
// 启用 CORS
|
||||
app.use(cors())
|
||||
|
||||
// 静态文件服务:/shared/* -> dist/esm/*
|
||||
app.use('/shared', express.static(path.join(__dirname, 'dist/esm'), {
|
||||
setHeaders: (res, filepath) => {
|
||||
// 设置正确的 MIME 类型
|
||||
if (filepath.endsWith('.js')) {
|
||||
res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
|
||||
}
|
||||
// 允许跨域
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||
}
|
||||
}))
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
port: PORT,
|
||||
modules: ['components', 'utils', 'api', 'composables', 'types']
|
||||
})
|
||||
})
|
||||
|
||||
// 模块列表
|
||||
app.get('/modules', (req, res) => {
|
||||
res.json({
|
||||
modules: {
|
||||
components: '/shared/components.js',
|
||||
utils: '/shared/utils.js',
|
||||
api: '/shared/api.js',
|
||||
composables: '/shared/composables.js',
|
||||
types: '/shared/types.js'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 Shared 模块服务器已启动!`)
|
||||
console.log(``)
|
||||
console.log(`📦 提供以下模块:`)
|
||||
console.log(` - http://localhost:${PORT}/shared/components.js`)
|
||||
console.log(` - http://localhost:${PORT}/shared/utils.js`)
|
||||
console.log(` - http://localhost:${PORT}/shared/api.js`)
|
||||
console.log(` - http://localhost:${PORT}/shared/composables.js`)
|
||||
console.log(` - http://localhost:${PORT}/shared/types.js`)
|
||||
console.log(``)
|
||||
console.log(`🔍 健康检查:http://localhost:${PORT}/health`)
|
||||
console.log(`📋 模块列表:http://localhost:${PORT}/modules`)
|
||||
console.log(``)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from '@/api/index'
|
||||
import type { LoginParam, LoginDomain } from '@/types'
|
||||
import type { LoginParam, LoginDomain, ResultDomain } from '@/types'
|
||||
|
||||
|
||||
/**
|
||||
* 认证 API
|
||||
@@ -14,16 +15,18 @@ export const authAPI = {
|
||||
* @param loginParam 登录参数
|
||||
* @returns 登录结果(包含 token 和用户信息)
|
||||
*/
|
||||
login(loginParam: LoginParam) {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/login`, loginParam)
|
||||
async login(loginParam: LoginParam): Promise<ResultDomain<LoginDomain>> {
|
||||
const response = await api.post<LoginDomain>(`${this.baseUrl}/login`, loginParam)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* @returns 登出结果
|
||||
*/
|
||||
logout() {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/logout`)
|
||||
async logout(): Promise<ResultDomain<null>> {
|
||||
const response = await api.post<null>(`${this.baseUrl}/logout`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -31,16 +34,18 @@ export const authAPI = {
|
||||
* @param loginParam 登录参数(包含验证码类型)
|
||||
* @returns 验证码结果
|
||||
*/
|
||||
getCaptcha(loginParam: LoginParam) {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/captcha`, loginParam)
|
||||
async getCaptcha(loginParam: LoginParam): Promise<ResultDomain<any>> {
|
||||
const response = await api.post<any>(`${this.baseUrl}/captcha`, loginParam)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
* @returns 新的登录信息
|
||||
*/
|
||||
refreshToken() {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/refresh`)
|
||||
async refreshToken(): Promise<ResultDomain<LoginDomain>> {
|
||||
const response = await api.post<LoginDomain>(`${this.baseUrl}/refresh`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -48,8 +53,9 @@ export const authAPI = {
|
||||
* @param email 邮箱地址
|
||||
* @returns 发送结果
|
||||
*/
|
||||
sendEmailCode(email: string) {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/send-email-code`, { email })
|
||||
async sendEmailCode(email: string): Promise<ResultDomain<null>> {
|
||||
const response = await api.post<null>(`${this.baseUrl}/send-email-code`, { email })
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -57,8 +63,9 @@ export const authAPI = {
|
||||
* @param phone 手机号
|
||||
* @returns 发送结果
|
||||
*/
|
||||
sendSmsCode(phone: string) {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/send-sms-code`, { phone })
|
||||
async sendSmsCode(phone: string): Promise<ResultDomain<null>> {
|
||||
const response = await api.post<null>(`${this.baseUrl}/send-sms-code`, { phone })
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -66,7 +73,7 @@ export const authAPI = {
|
||||
* @param registerData 注册数据
|
||||
* @returns 注册结果(成功后自动登录,返回 token)
|
||||
*/
|
||||
register(registerData: {
|
||||
async register(registerData: {
|
||||
registerType: 'username' | 'phone' | 'email'
|
||||
username?: string
|
||||
phone?: string
|
||||
@@ -78,15 +85,17 @@ export const authAPI = {
|
||||
smsSessionId?: string
|
||||
emailSessionId?: string
|
||||
studentId?: string
|
||||
}) {
|
||||
return api.post<LoginDomain>(`${this.baseUrl}/register`, registerData)
|
||||
}): Promise<ResultDomain<LoginDomain>> {
|
||||
const response = await api.post<LoginDomain>(`${this.baseUrl}/register`, registerData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* @returns 健康状态
|
||||
*/
|
||||
health() {
|
||||
return api.get<string>(`${this.baseUrl}/health`)
|
||||
async health(): Promise<ResultDomain<string>> {
|
||||
const response = await api.get<string>(`${this.baseUrl}/health`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,52 @@ export interface LoginParam {
|
||||
}
|
||||
|
||||
// LoginDomain - 登录信息
|
||||
import type {
|
||||
TbSysUserDTO,
|
||||
TbSysUserInfoDTO,
|
||||
TbSysUserRoleDTO
|
||||
} from '@/types/sys/user'
|
||||
|
||||
import type {
|
||||
TbSysDeptDTO,
|
||||
TbSysPermissionDTO,
|
||||
TbSysViewDTO
|
||||
} from '@/types/sys/permission'
|
||||
|
||||
/**
|
||||
* 登录返回的领域对象
|
||||
*/
|
||||
export interface LoginDomain {
|
||||
/** 用户ID */
|
||||
userId?: string
|
||||
/** 用户名 */
|
||||
username?: string
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
/** 手机 */
|
||||
phone?: string
|
||||
/** 访问令牌 */
|
||||
accessToken?: string
|
||||
/** 刷新令牌 */
|
||||
refreshToken?: string
|
||||
/** 令牌类型 */
|
||||
tokenType?: string
|
||||
/** 过期时间(秒) */
|
||||
expiresIn?: number
|
||||
/** 用户权限列表 */
|
||||
permissions?: string[]
|
||||
/** 用户角色列表 */
|
||||
roles?: string[]
|
||||
/** 用户基本信息 */
|
||||
user?: TbSysUserDTO
|
||||
|
||||
/** 用户详细信息 */
|
||||
userInfo?: TbSysUserInfoDTO
|
||||
|
||||
/** 用户角色列表 */
|
||||
userRoles?: TbSysUserRoleDTO[]
|
||||
|
||||
/** 用户部门列表 */
|
||||
userDepts?: TbSysDeptDTO[]
|
||||
|
||||
/** 用户权限列表 */
|
||||
userPermissions?: TbSysPermissionDTO[]
|
||||
|
||||
/** 用户视图列表(视图即菜单,用于生成路由和侧边栏) */
|
||||
userViews?: TbSysViewDTO[]
|
||||
|
||||
/** 访问令牌 */
|
||||
token?: string
|
||||
|
||||
/** 令牌过期时间 */
|
||||
tokenExpireTime?: string | Date
|
||||
|
||||
/** 登录时间 */
|
||||
loginTime?: string
|
||||
|
||||
/** IP地址 */
|
||||
ipAddress?: string
|
||||
|
||||
/** 登录类型 */
|
||||
loginType?: string
|
||||
}
|
||||
|
||||
10
urbanLifelineWeb/packages/shared/src/types/enums.ts
Normal file
10
urbanLifelineWeb/packages/shared/src/types/enums.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 菜单类型枚举
|
||||
*/
|
||||
export enum MenuType {
|
||||
NAVIGATION = 'navigation', // 导航菜单
|
||||
SIDEBAR = 'sidebar', // 侧边栏菜单
|
||||
MENU = 'menu', // 普通菜单
|
||||
PAGE = 'page', // 页面
|
||||
BUTTON = 'button' // 按钮
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from "./response"
|
||||
export * from "./page"
|
||||
export * from "./base"
|
||||
export * from "./sys"
|
||||
export * from "./enums"
|
||||
|
||||
// 服务 types
|
||||
export * from "./auth"
|
||||
|
||||
@@ -120,16 +120,22 @@ export interface TbSysViewDTO extends BaseDTO {
|
||||
url?: string;
|
||||
/** 组件 */
|
||||
component?: string;
|
||||
/** iframe URL */
|
||||
iframeUrl?: string;
|
||||
/** 图标 */
|
||||
icon?: string;
|
||||
/** 类型 */
|
||||
type?: number;
|
||||
/** 视图类型 route\iframe*/
|
||||
viewType?: string;
|
||||
/** 布局 */
|
||||
layout?: string;
|
||||
/** 排序 */
|
||||
orderNum?: number;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 子视图列表(用于构建树形结构) */
|
||||
children?: TbSysViewDTO[];
|
||||
}
|
||||
|
||||
// TbSysPermissionDTO - 系统权限DTO
|
||||
|
||||
109
urbanLifelineWeb/packages/shared/src/utils/device.ts
Normal file
109
urbanLifelineWeb/packages/shared/src/utils/device.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)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Utils 统一导出
|
||||
*/
|
||||
|
||||
export * from './file'
|
||||
export * from './crypto'
|
||||
export * from './device'
|
||||
export * from './file'
|
||||
export * from './route'
|
||||
|
||||
19
urbanLifelineWeb/packages/shared/src/utils/route/index.ts
Normal file
19
urbanLifelineWeb/packages/shared/src/utils/route/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// 导出所有路由生成相关的函数和类型
|
||||
export * from './route-generator'
|
||||
|
||||
// 显式导出新增的函数和类型
|
||||
export type {
|
||||
RouteGeneratorConfig,
|
||||
GenerateSimpleRoutesOptions
|
||||
} from './route-generator'
|
||||
|
||||
export {
|
||||
generateRoutes,
|
||||
generateSimpleRoutes,
|
||||
buildMenuTree,
|
||||
filterMenusByPermissions,
|
||||
findMenuByPath,
|
||||
getMenuPath,
|
||||
getFirstAccessibleMenuUrl,
|
||||
loadViewsFromStorage
|
||||
} from './route-generator'
|
||||
@@ -0,0 +1,789 @@
|
||||
/**
|
||||
* @description 动态路由生成器工具类
|
||||
* @author yslg
|
||||
* @since 2025-12-12
|
||||
*
|
||||
* 说明:此文件提供路由生成的通用方法,各个 web 服务可以使用这些方法生成自己的路由
|
||||
*/
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { TbSysViewDTO } from '@/types'
|
||||
|
||||
// 为了代码可读性,创建类型别名
|
||||
type SysMenu = TbSysViewDTO
|
||||
|
||||
// 视图类型常量(对应后端的 type 字段)
|
||||
const ViewType = {
|
||||
MENU: 1, // 菜单
|
||||
PAGE: 2, // 页面
|
||||
BUTTON: 3 // 按钮
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 路由生成器配置
|
||||
*/
|
||||
export interface RouteGeneratorConfig {
|
||||
/**
|
||||
* 布局组件映射表
|
||||
* key: 布局名称,value: 组件加载函数
|
||||
*/
|
||||
layoutMap: Record<string, () => Promise<any>>
|
||||
|
||||
/**
|
||||
* 视图组件加载器
|
||||
* 用于动态加载视图组件
|
||||
*/
|
||||
viewLoader: (componentPath: string) => Promise<any> | null
|
||||
|
||||
/**
|
||||
* 静态路由列表(可选)
|
||||
* 用于将静态路由转换为菜单项
|
||||
*/
|
||||
staticRoutes?: RouteRecordRaw[]
|
||||
|
||||
/**
|
||||
* 404 组件路径(可选)
|
||||
*/
|
||||
notFoundComponent?: () => Promise<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单生成路由配置
|
||||
* @param menus 用户菜单列表
|
||||
* @param config 路由生成器配置
|
||||
* @returns Vue Router路由配置数组
|
||||
*/
|
||||
export function generateRoutes(
|
||||
menus: SysMenu[],
|
||||
config: RouteGeneratorConfig
|
||||
): RouteRecordRaw[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = []
|
||||
const pageRoutes: RouteRecordRaw[] = []
|
||||
|
||||
// 构建菜单树
|
||||
const menuTree = buildMenuTree(menus, config.staticRoutes)
|
||||
|
||||
// 生成路由
|
||||
menuTree.forEach(menu => {
|
||||
const route = generateRouteFromMenu(menu, config, true)
|
||||
|
||||
if (route) {
|
||||
routes.push(route)
|
||||
|
||||
// 递归提取所有 PAGE 类型的子菜单
|
||||
extractPageChildren(route, pageRoutes, config)
|
||||
}
|
||||
})
|
||||
|
||||
// 将 PAGE 类型的路由添加到路由列表
|
||||
routes.push(...pageRoutes)
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取路由中的 PAGE 类型子菜单
|
||||
*/
|
||||
function extractPageChildren(
|
||||
route: any,
|
||||
pageRoutes: RouteRecordRaw[],
|
||||
config: RouteGeneratorConfig
|
||||
) {
|
||||
// 检查当前路由是否有 PAGE 类型的子菜单
|
||||
if (route.meta?.pageChildren && Array.isArray(route.meta.pageChildren)) {
|
||||
route.meta.pageChildren.forEach((pageMenu: SysMenu) => {
|
||||
const pageRoute = generateRouteFromMenu(pageMenu, config, 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, config)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单个菜单生成路由
|
||||
* @param menu 菜单对象
|
||||
* @param config 路由生成器配置
|
||||
* @param isTopLevel 是否是顶层菜单
|
||||
* @returns 路由配置
|
||||
*/
|
||||
function generateRouteFromMenu(
|
||||
menu: SysMenu,
|
||||
config: RouteGeneratorConfig,
|
||||
isTopLevel = true
|
||||
): RouteRecordRaw | null {
|
||||
// 跳过按钮类型
|
||||
if (menu.type === ViewType.BUTTON) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
|
||||
if (menu.component === '__STATIC_ROUTE__') {
|
||||
return null
|
||||
}
|
||||
|
||||
const route: any = {
|
||||
path: menu.url || `/${menu.viewId}`,
|
||||
name: menu.viewId,
|
||||
meta: {
|
||||
title: menu.name,
|
||||
icon: menu.icon,
|
||||
menuId: menu.viewId,
|
||||
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 是否是布局组件
|
||||
const isComponentLayout = menu.component && (
|
||||
config.layoutMap[menu.component] ||
|
||||
(typeof menu.component === 'string' && menu.component.includes('Layout'))
|
||||
)
|
||||
|
||||
// 确定路由组件
|
||||
if (layout && config.layoutMap[layout]) {
|
||||
// 如果指定了布局,使用指定的布局
|
||||
route.component = config.layoutMap[layout]
|
||||
} else if (isComponentLayout && hasChildren && isTopLevel && menu.component) {
|
||||
// 如果 component 是布局组件且有子菜单,使用该布局组件作为父路由组件
|
||||
route.component = config.layoutMap[menu.component]
|
||||
} else if (hasChildren && isTopLevel) {
|
||||
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
|
||||
if (menu.type === ViewType.MENU && !menu.parentId) {
|
||||
route.component = config.layoutMap['SidebarLayout']
|
||||
} else {
|
||||
route.component = config.layoutMap['BasicLayout']
|
||||
}
|
||||
} else {
|
||||
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
||||
if (menu.component) {
|
||||
const component = config.viewLoader(menu.component)
|
||||
if (component) {
|
||||
route.component = component
|
||||
} else {
|
||||
// 组件加载失败,使用 404
|
||||
route.component = config.notFoundComponent || (() => Promise.resolve({ default: { template: '<div>404</div>' } }))
|
||||
}
|
||||
} else {
|
||||
// 使用路由占位组件
|
||||
route.component = () => Promise.resolve({
|
||||
default: {
|
||||
template: '<router-view />'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子路由
|
||||
if (layout && config.layoutMap[layout] && menu.component && isTopLevel) {
|
||||
// 如果指定了布局,将页面组件作为子路由
|
||||
const component = config.viewLoader(menu.component)
|
||||
route.children = [{
|
||||
path: '',
|
||||
name: `${menu.viewId}_page`,
|
||||
component: component || route.component,
|
||||
meta: route.meta
|
||||
}]
|
||||
|
||||
// 如果还有其他子菜单,继续添加
|
||||
if (hasChildren) {
|
||||
const pageChildren: SysMenu[] = []
|
||||
const normalChildren: SysMenu[] = []
|
||||
|
||||
menu.children!.forEach((child: SysMenu) => {
|
||||
if (child.type === ViewType.PAGE) {
|
||||
pageChildren.push(child)
|
||||
} else {
|
||||
normalChildren.push(child)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加普通子菜单
|
||||
normalChildren.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, config, 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: SysMenu) => {
|
||||
if (child.type === ViewType.PAGE) {
|
||||
pageChildren.push(child)
|
||||
} else {
|
||||
normalChildren.push(child)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果当前菜单有组件且有普通子菜单,创建默认子路由
|
||||
if (menu.component && !isComponentLayout && normalChildren.length > 0) {
|
||||
const component = config.viewLoader(menu.component)
|
||||
route.children!.push({
|
||||
path: '',
|
||||
name: `${menu.viewId}_page`,
|
||||
component: component || route.component,
|
||||
meta: {
|
||||
...route.meta,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 只将普通子菜单加入 children
|
||||
normalChildren.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, config, false)
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute)
|
||||
}
|
||||
})
|
||||
|
||||
// PAGE 类型的菜单保存到 meta
|
||||
if (pageChildren.length > 0) {
|
||||
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的菜单
|
||||
*/
|
||||
function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
|
||||
for (const menu of menus) {
|
||||
if (menu.type !== ViewType.BUTTON) {
|
||||
if (menu.url) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findFirstMenuWithUrl(menu.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 将静态路由转换为菜单项
|
||||
*/
|
||||
function convertRoutesToMenus(routes: RouteRecordRaw[]): SysMenu[] {
|
||||
const menus: SysMenu[] = []
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children.forEach(child => {
|
||||
if (child.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
viewId: 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 number,
|
||||
orderNum: (child.meta.orderNum as number) || -1,
|
||||
component: '__STATIC_ROUTE__',
|
||||
}
|
||||
menus.push(menu)
|
||||
}
|
||||
})
|
||||
} else if (route.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
viewId: 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 number,
|
||||
orderNum: (route.meta.orderNum as number) || -1,
|
||||
component: '__STATIC_ROUTE__',
|
||||
}
|
||||
menus.push(menu)
|
||||
}
|
||||
})
|
||||
|
||||
return menus
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树结构
|
||||
* @param menus 菜单列表
|
||||
* @param staticRoutes 静态路由列表
|
||||
* @returns 菜单树
|
||||
*/
|
||||
export function buildMenuTree(
|
||||
menus: SysMenu[],
|
||||
staticRoutes?: RouteRecordRaw[]
|
||||
): SysMenu[] {
|
||||
// 将静态路由转换为菜单项
|
||||
const staticMenus = staticRoutes ? convertRoutesToMenus(staticRoutes) : []
|
||||
|
||||
// 合并动态菜单和静态菜单
|
||||
const allMenus = [...staticMenus, ...menus]
|
||||
|
||||
if (allMenus.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const menuMap = new Map<string, SysMenu>()
|
||||
const rootMenus: SysMenu[] = []
|
||||
const maxDepth = allMenus.length
|
||||
|
||||
// 创建菜单映射
|
||||
allMenus.forEach(menu => {
|
||||
if (menu.viewId) {
|
||||
menuMap.set(menu.viewId, { ...menu, children: [] })
|
||||
}
|
||||
})
|
||||
|
||||
// 循环构建树结构
|
||||
for (let depth = 0; depth < maxDepth; depth++) {
|
||||
let hasChanges = false
|
||||
|
||||
allMenus.forEach(menu => {
|
||||
if (!menu.viewId) return
|
||||
|
||||
const menuNode = menuMap.get(menu.viewId)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
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.viewId === node.viewId) {
|
||||
return true
|
||||
}
|
||||
if (treeNode.children && isNodeInTree(node, treeNode.children)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限过滤菜单
|
||||
*/
|
||||
export function filterMenusByPermissions(
|
||||
menus: SysMenu[],
|
||||
permissions: string[]
|
||||
): SysMenu[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return menus
|
||||
.filter(() => true) // 暂时返回true,后续可根据实际需求过滤
|
||||
.map(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
return {
|
||||
...menu,
|
||||
children: filterMenusByPermissions(menu.children, permissions)
|
||||
}
|
||||
}
|
||||
return menu
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找路由路径对应的菜单
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单路径数组(面包屑导航用)
|
||||
*/
|
||||
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.viewId === 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(用于登录后跳转)
|
||||
*/
|
||||
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
|
||||
if (!menus || menus.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const firstMenu = findFirstMenuWithUrl(menus)
|
||||
return firstMenu?.url || '/home'
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 LocalStorage 加载用户视图数据
|
||||
* @param storageKey localStorage 的 key,默认为 'loginDomain'
|
||||
* @param viewsPath 视图数据在对象中的路径,默认为 'userViews'
|
||||
* @returns 视图列表,如果不存在返回 null
|
||||
*/
|
||||
export function loadViewsFromStorage(
|
||||
storageKey: string = 'loginDomain',
|
||||
viewsPath: string = 'userViews'
|
||||
): TbSysViewDTO[] | null {
|
||||
try {
|
||||
const dataStr = localStorage.getItem(storageKey)
|
||||
|
||||
if (!dataStr) {
|
||||
console.log(`[路由工具] LocalStorage 中没有 ${storageKey}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = JSON.parse(dataStr)
|
||||
|
||||
// 支持嵌套路径,如 'user.views'
|
||||
const paths = viewsPath.split('.')
|
||||
let views = data
|
||||
|
||||
for (const path of paths) {
|
||||
views = views?.[path]
|
||||
}
|
||||
|
||||
if (views && Array.isArray(views) && views.length > 0) {
|
||||
console.log(`[路由工具] 从 LocalStorage 加载视图,数量: ${views.length}`)
|
||||
return views
|
||||
}
|
||||
|
||||
console.log(`[路由工具] ${storageKey} 中没有 ${viewsPath} 或数据为空`)
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('[路由工具] 从 LocalStorage 加载视图失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简化的路由配置(用于直接添加到 router)
|
||||
* 相比 generateRoutes,这个方法生成的路由更适合动态添加到现有路由树
|
||||
*
|
||||
* @param views 视图列表
|
||||
* @param config 路由生成器配置
|
||||
* @param options 额外选项
|
||||
* @returns 路由配置数组
|
||||
*/
|
||||
export interface GenerateSimpleRoutesOptions {
|
||||
/**
|
||||
* 是否作为根路由的子路由(路径去掉前导 /)
|
||||
*/
|
||||
asRootChildren?: boolean
|
||||
|
||||
/**
|
||||
* iframe 类型视图的占位组件
|
||||
*/
|
||||
iframePlaceholder?: () => Promise<any>
|
||||
|
||||
/**
|
||||
* 是否启用详细日志
|
||||
*/
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export function generateSimpleRoutes(
|
||||
views: TbSysViewDTO[],
|
||||
config: RouteGeneratorConfig,
|
||||
options: GenerateSimpleRoutesOptions = {}
|
||||
): RouteRecordRaw[] {
|
||||
const {
|
||||
asRootChildren = false,
|
||||
iframePlaceholder,
|
||||
verbose = false
|
||||
} = options
|
||||
|
||||
if (!views || views.length === 0) {
|
||||
if (verbose) console.warn('[路由生成] 视图列表为空')
|
||||
return []
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log('[路由生成] 开始生成路由,视图数量:', views.length)
|
||||
}
|
||||
|
||||
// 构建视图树
|
||||
const viewTree = buildMenuTree(views)
|
||||
|
||||
if (verbose) {
|
||||
console.log('[路由生成] 构建视图树,根节点数量:', viewTree.length)
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = []
|
||||
|
||||
// 遍历根节点,生成路由
|
||||
viewTree.forEach(view => {
|
||||
const route = generateSimpleRoute(view, config, {
|
||||
asRootChild: asRootChildren,
|
||||
iframePlaceholder,
|
||||
verbose
|
||||
})
|
||||
|
||||
if (route) {
|
||||
routes.push(route)
|
||||
if (verbose) {
|
||||
console.log('[路由生成] 已生成路由:', {
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
hasComponent: !!route.component,
|
||||
childrenCount: route.children?.length || 0
|
||||
})
|
||||
}
|
||||
} else if (verbose) {
|
||||
console.warn('[路由生成] 跳过无效视图:', view.name)
|
||||
}
|
||||
})
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单个视图生成简化路由
|
||||
*/
|
||||
function generateSimpleRoute(
|
||||
view: TbSysViewDTO,
|
||||
config: RouteGeneratorConfig,
|
||||
options: {
|
||||
asRootChild?: boolean
|
||||
iframePlaceholder?: () => Promise<any>
|
||||
verbose?: boolean
|
||||
} = {}
|
||||
): RouteRecordRaw | null {
|
||||
const { asRootChild = false, iframePlaceholder, verbose = false } = options
|
||||
|
||||
// 验证必要字段
|
||||
if (!view.viewId) {
|
||||
if (verbose) console.error('[路由生成] 视图缺少 viewId:', view)
|
||||
return null
|
||||
}
|
||||
|
||||
// 判断是否是 iframe 类型
|
||||
const isIframe = (view as any).viewType === 'iframe' || !!(view as any).iframeUrl
|
||||
|
||||
// 处理路径和组件
|
||||
let routePath = view.url || `/${view.viewId}`
|
||||
let component: any
|
||||
|
||||
if (isIframe) {
|
||||
// iframe 类型:使用占位组件
|
||||
component = iframePlaceholder || (() => Promise.resolve({
|
||||
default: {
|
||||
template: '<div class="iframe-placeholder"></div>'
|
||||
}
|
||||
}))
|
||||
} else if (view.component) {
|
||||
// route 类型:加载实际组件
|
||||
component = config.viewLoader(view.component)
|
||||
if (!component) {
|
||||
if (verbose) console.warn('[路由生成] 组件加载失败:', view.component)
|
||||
}
|
||||
}
|
||||
|
||||
// 根路径的子路由去掉前导斜杠
|
||||
if (asRootChild && routePath.startsWith('/')) {
|
||||
routePath = routePath.substring(1)
|
||||
}
|
||||
|
||||
const hasChildren = view.children && view.children.length > 0
|
||||
|
||||
if (verbose) {
|
||||
console.log('[路由生成] 视图信息:', {
|
||||
viewId: view.viewId,
|
||||
name: view.name,
|
||||
url: view.url,
|
||||
component: view.component,
|
||||
isIframe,
|
||||
hasChildren,
|
||||
childrenCount: view.children?.length || 0
|
||||
})
|
||||
}
|
||||
|
||||
const route: any = {
|
||||
path: routePath,
|
||||
name: view.viewId,
|
||||
meta: {
|
||||
title: view.name || view.viewId,
|
||||
icon: view.icon,
|
||||
menuId: view.viewId,
|
||||
orderNum: view.orderNum,
|
||||
requiresAuth: true,
|
||||
isIframe,
|
||||
iframeUrl: (view as any).iframeUrl
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 component 和 children 的情况处理
|
||||
if (component && hasChildren) {
|
||||
// 有组件且有子视图:组件作为空路径子路由
|
||||
route.component = component
|
||||
route.children = [
|
||||
{
|
||||
path: '',
|
||||
name: `${view.viewId}_page`,
|
||||
component: component,
|
||||
meta: route.meta
|
||||
}
|
||||
]
|
||||
|
||||
// 添加其他子路由
|
||||
view.children!.forEach(childView => {
|
||||
const childRoute = generateSimpleRoute(childView, config, {
|
||||
asRootChild: false,
|
||||
iframePlaceholder,
|
||||
verbose
|
||||
})
|
||||
if (childRoute) {
|
||||
route.children.push(childRoute)
|
||||
}
|
||||
})
|
||||
} else if (component && !hasChildren) {
|
||||
// 只有组件,没有子视图
|
||||
route.component = component
|
||||
} else if (!component && hasChildren) {
|
||||
// 没有组件,只有子视图(路由容器)
|
||||
route.component = () => Promise.resolve({
|
||||
default: {
|
||||
template: '<router-view />'
|
||||
}
|
||||
})
|
||||
route.children = []
|
||||
|
||||
// 添加子路由
|
||||
view.children!.forEach(childView => {
|
||||
const childRoute = generateSimpleRoute(childView, config, {
|
||||
asRootChild: false,
|
||||
iframePlaceholder,
|
||||
verbose
|
||||
})
|
||||
if (childRoute) {
|
||||
route.children.push(childRoute)
|
||||
}
|
||||
})
|
||||
|
||||
// 重定向到第一个子路由
|
||||
if (route.children.length > 0) {
|
||||
const firstChild = route.children[0]
|
||||
route.redirect = firstChild.path
|
||||
}
|
||||
} else {
|
||||
// 既没有组件也没有子视图
|
||||
if (verbose) {
|
||||
console.warn('[路由生成] 视图既无组件也无子视图:', view.name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
@@ -32,23 +32,29 @@ export default defineConfig({
|
||||
filename: 'remoteEntry.js',
|
||||
// 暴露的模块
|
||||
exposes: {
|
||||
// 通用组件
|
||||
'./FileUpload': './src/components/fileupload/FileUpload.vue',
|
||||
'./DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
|
||||
// ========== 组件模块 ==========
|
||||
'./components': './src/components/index.ts',
|
||||
'./components/FileUpload': './src/components/fileupload/FileUpload.vue',
|
||||
'./components/DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
|
||||
|
||||
// API 模块
|
||||
// ========== API 模块 ==========
|
||||
'./api': './src/api/index.ts',
|
||||
'./authAPI': './src/api/auth/auth.ts',
|
||||
'./fileAPI': './src/api/file/file.ts',
|
||||
'./api/auth': './src/api/auth/auth.ts',
|
||||
'./api/file': './src/api/file/file.ts',
|
||||
|
||||
// Utils 模块
|
||||
// ========== Utils 工具模块 ==========
|
||||
'./utils': './src/utils/index.ts',
|
||||
'./utils/device': './src/utils/device.ts',
|
||||
'./utils/route': './src/utils/route/index.ts',
|
||||
'./utils/route/generator': './src/utils/route/route-generator.ts',
|
||||
'./utils/file': './src/utils/file.ts',
|
||||
|
||||
// Types 模块
|
||||
// ========== Types 类型模块 ==========
|
||||
'./types': './src/types/index.ts',
|
||||
|
||||
// 整体导出
|
||||
'./components': './src/components/index.ts'
|
||||
'./types/base': './src/types/base/index.ts',
|
||||
'./types/auth': './src/types/auth/index.ts',
|
||||
'./types/file': './src/types/file/index.ts',
|
||||
'./types/sys': './src/types/sys/index.ts'
|
||||
},
|
||||
// 共享依赖(重要:避免重复加载)
|
||||
shared: {
|
||||
|
||||
Reference in New Issue
Block a user