菜单可正确被加载
This commit is contained in:
parent
5282716028
commit
4742437fe7
@ -12,6 +12,7 @@
|
|||||||
- 如果你不知道答案,就说出来,而不是猜测。
|
- 如果你不知道答案,就说出来,而不是猜测。
|
||||||
- 可以提出合理化的建议,但是需要等待是否可以。
|
- 可以提出合理化的建议,但是需要等待是否可以。
|
||||||
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
|
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
|
||||||
|
- 不要随意修改现在的接口调用路径,如果不知道接口是什么,可以问。
|
||||||
#代码实现指南
|
#代码实现指南
|
||||||
在编写代码时遵循以下规则:
|
在编写代码时遵循以下规则:
|
||||||
- 尽可能使用早期返回,使代码更具可读性。
|
- 尽可能使用早期返回,使代码更具可读性。
|
||||||
@ -21,61 +22,50 @@
|
|||||||
- 在元素上实现可访问性特性。例如,一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
|
- 在元素上实现可访问性特性。例如,一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
|
||||||
— 使用const代替函数,例如:const toggle =() =>。另外,如果可能的话,定义一个类型。
|
— 使用const代替函数,例如:const toggle =() =>。另外,如果可能的话,定义一个类型。
|
||||||
|
|
||||||
## Deploy Ease Platform 前端开发规范
|
# Deploy Ease Platform 前端开发规范
|
||||||
|
|
||||||
## 1. 项目结构规范
|
## 1. 项目结构规范
|
||||||
|
|
||||||
1. 目录结构:
|
```
|
||||||
```
|
src/
|
||||||
src/
|
├── components/ # 公共组件
|
||||||
├── components/ # 可复用的公共组件
|
├── pages/ # 页面组件
|
||||||
├── pages/ # 页面组件
|
│ └── System/ # 系统管理模块
|
||||||
│ └── System/ # 系统管理模块
|
├── layouts/ # 布局组件
|
||||||
│ ├── User/ # 用户管理模块
|
├── router/ # 路由配置
|
||||||
│ └── Role/ # 角色管理模块
|
├── store/ # 状态管理
|
||||||
├── layouts/ # 布局组件
|
├── services/ # API 服务
|
||||||
├── router/ # 路由配置
|
├── utils/ # 工具函数
|
||||||
├── store/ # 状态管理
|
├── hooks/ # 自定义 Hooks
|
||||||
├── services/ # API 服务
|
└── types/ # TS 类型定义
|
||||||
├── utils/ # 工具函数
|
|
||||||
├── hooks/ # 自定义 Hooks
|
|
||||||
└── types/ # TypeScript 类型定义
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 模块结构:
|
ModuleName/ # 模块结构
|
||||||
```
|
├── components/ # 模块私有组件
|
||||||
ModuleName/ # 使用 PascalCase 命名
|
├── types/ # 类型定义
|
||||||
├── components/ # 模块私有组件
|
├── service.ts # API 服务
|
||||||
├── types/ # 模块内部类型定义
|
└── index.tsx # 模块入口
|
||||||
├── types.ts # 模块对外暴露的类型定义
|
```
|
||||||
├── service.ts # API 服务封装
|
|
||||||
└── index.tsx # 模块主入口
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 命名规范
|
## 2. 命名与类型规范
|
||||||
|
|
||||||
1. 文件命名:
|
1. 文件命名:
|
||||||
- 组件文件:使用 PascalCase(如 `UserProfile.tsx`)
|
- 组件:PascalCase(`UserProfile.tsx`)
|
||||||
- 工具函数文件:使用 camelCase(如 `formatDate.ts`)
|
- 工具函数:camelCase(`formatDate.ts`)
|
||||||
- 样式文件:与组件同名,使用 `.module.css`(如 `UserProfile.module.css`)
|
- 样式:组件同名(`UserProfile.module.css`)
|
||||||
- Redux Slice 文件:使用 `xxxSlice.ts` 命名
|
- Redux:`xxxSlice.ts`
|
||||||
- 类型定义文件:使用 `types.ts` 命名
|
- 类型:`types.ts`
|
||||||
- 服务文件:使用 `service.ts` 命名
|
- 服务:`service.ts`
|
||||||
|
|
||||||
2. 变量命名:
|
2. 变量命名:
|
||||||
- 使用有意义的英文名称
|
- 常量:UPPER_SNAKE_CASE
|
||||||
- 常量使用 UPPER_SNAKE_CASE
|
- 变量:camelCase
|
||||||
- 变量使用 camelCase
|
- 接口:以 `I` 开头,PascalCase
|
||||||
- 接口名使用 PascalCase,并以 `I` 开头
|
- 类型:以 `T` 开头,PascalCase
|
||||||
- 类型名使用 PascalCase,并以 `T` 开头
|
- 事件处理:`handle` 前缀
|
||||||
- 事件处理函数使用 `handle` 前缀(如 `handleClick`)
|
- 异步函数:动词开头(`fetchData`)
|
||||||
- 异步函数使用动词开头(如 `fetchData`、`loadUser`)
|
|
||||||
|
|
||||||
## 3. TypeScript 类型规范
|
3. 基础类型:
|
||||||
|
|
||||||
1. 基础类型定义:
|
|
||||||
```typescript
|
```typescript
|
||||||
// 基础响应类型
|
|
||||||
interface BaseResponse {
|
interface BaseResponse {
|
||||||
id: number;
|
id: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
@ -86,7 +76,6 @@
|
|||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基础查询类型
|
|
||||||
interface BaseQuery {
|
interface BaseQuery {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -94,191 +83,206 @@
|
|||||||
endTime?: string;
|
endTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页参数类型
|
interface Response<T = any> {
|
||||||
interface PageParams {
|
code: number;
|
||||||
pageNum: number;
|
message: string;
|
||||||
pageSize: number;
|
data: T;
|
||||||
sortField?: string;
|
success: boolean;
|
||||||
sortOrder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页响应类型
|
|
||||||
interface Page<T> {
|
|
||||||
content: T[];
|
|
||||||
number: number;
|
|
||||||
size: number;
|
|
||||||
totalElements: number;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 类型使用规范:
|
## 3. 服务层规范
|
||||||
- 优先使用 `type` 而不是 `interface`
|
|
||||||
- 必须明确声明函数参数和返回值类型
|
|
||||||
- 避免使用 `any`,优先使用 `unknown`
|
|
||||||
- 必须启用 TypeScript 严格模式
|
|
||||||
- 所有实体响应类型必须继承 BaseResponse
|
|
||||||
- 所有查询类型必须继承 BaseQuery
|
|
||||||
- 时间字段统一使用 ISO 字符串格式
|
|
||||||
- 版本号用于乐观锁控制
|
|
||||||
|
|
||||||
## 4. HTTP 请求规范
|
1. 方法定义:
|
||||||
|
```typescript
|
||||||
|
// 推荐写法 - 使用 http 工具
|
||||||
|
export const resetPassword = (id: number, password: string) =>
|
||||||
|
http.post<void>(`/api/v1/users/${id}/reset-password`, { password });
|
||||||
|
|
||||||
1. 请求工具:
|
// 标准 CRUD 接口
|
||||||
- 统一使用封装的 `request` 工具
|
export const getList = (params?: Query) =>
|
||||||
- 必须使用 `async/await` 语法,禁止使用 `.then()`
|
http.get<Response[]>('/api/v1/xxx', { params });
|
||||||
- 必须使用 `try/catch` 进行错误处理
|
|
||||||
- Promise 对象的类型必须显式声明
|
|
||||||
|
|
||||||
2. API 规范:
|
export const create = (data: Request) =>
|
||||||
- 所有后端接口必须使用统一前缀 `/api/v1/`
|
http.post<Response>('/api/v1/xxx', data);
|
||||||
- API 版本控制统一使用 v1
|
|
||||||
- 按照 RESTful 规范组织 API
|
export const update = (id: number, data: Request) =>
|
||||||
- 统一的响应格式:
|
http.put<Response>(`/api/v1/xxx/${id}`, data);
|
||||||
```typescript
|
|
||||||
interface Response<T = any> {
|
export const remove = (id: number) =>
|
||||||
code: number;
|
http.delete(`/api/v1/xxx/${id}`);
|
||||||
message: string;
|
|
||||||
data: T;
|
export const batchRemove = (ids: number[]) =>
|
||||||
success: boolean;
|
http.post('/api/v1/xxx/batch-delete', { ids });
|
||||||
|
|
||||||
|
export const exportData = (params?: Query) =>
|
||||||
|
http.download('/api/v1/xxx/export', undefined, { params });
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 规范要点:
|
||||||
|
- 统一使用 `http` 工具(不要使用 request)
|
||||||
|
- 使用泛型指定响应数据类型
|
||||||
|
- 错误处理在拦截器中统一处理
|
||||||
|
- 使用模板字符串拼接路径
|
||||||
|
- API 路径使用 `/api/v1/` 前缀
|
||||||
|
- 资源使用复数形式(users, roles)
|
||||||
|
- 特殊操作使用动词(export, import)
|
||||||
|
|
||||||
|
3. 响应数据处理:
|
||||||
|
```typescript
|
||||||
|
// 响应数据结构
|
||||||
|
interface Response<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// http 工具类型定义
|
||||||
|
const http = {
|
||||||
|
get: <T = any>(url: string, config?: RequestOptions) =>
|
||||||
|
request.get<any, T>(url, config), // 返回 T 类型,而不是 Response<T>
|
||||||
|
|
||||||
|
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||||
|
request.post<any, T>(url, data, config) // 返回 T 类型
|
||||||
|
};
|
||||||
|
|
||||||
|
// 服务层方法 - 直接使用业务数据类型
|
||||||
|
export const login = (data: LoginRequest) =>
|
||||||
|
http.post<LoginResponse>('/api/v1/user/login', data); // 返回 LoginResponse
|
||||||
|
|
||||||
|
// 组件中使用 - 直接获取业务数据
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const userData = await login(data); // userData 类型是 LoginResponse
|
||||||
|
console.log(userData.token); // 可以直接访问业务数据
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
}
|
}
|
||||||
```
|
};
|
||||||
|
```
|
||||||
|
|
||||||
3. 错误处理:
|
4. 类型处理规则:
|
||||||
- 统一的错误码定义:
|
- http 工具的泛型参数 T 表示业务数据类型
|
||||||
- 401:未授权,重新登录
|
- 响应拦截器负责从 Response<T> 中提取 data
|
||||||
- 403:拒绝访问
|
- 服务方法直接使用业务数据类型作为泛型参数
|
||||||
- 404:资源未找到
|
- 组件中可以直接使用返回的业务数据
|
||||||
- 500:服务器错误
|
- TypeScript 类型系统能正确推断类型
|
||||||
- 必须有完整的错误提示信息
|
|
||||||
- 必须有合理的错误降级策略
|
|
||||||
- 必须记录必要的错误日志
|
|
||||||
|
|
||||||
4. 请求配置:
|
5. 最佳实践:
|
||||||
- 超时时间:30秒
|
```typescript
|
||||||
- 重试次数:3次
|
// 服务层 - 使用业务数据类型
|
||||||
- 重试延迟:1秒
|
export const someService = () =>
|
||||||
- Token 使用 Bearer 方案
|
http.get<SomeType>('/api/v1/xxx'); // 返回 Promise<SomeType>
|
||||||
- Token 存储在 localStorage
|
|
||||||
- 文件上传下载使用专门的方法
|
// 组件层 - 直接使用业务数据
|
||||||
|
const handleOperation = async () => {
|
||||||
|
try {
|
||||||
|
const data = await someService(); // data 类型是 SomeType
|
||||||
|
console.log(data.someField); // TypeScript 能正确推断字段类型
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 特殊处理 - 文件上传
|
||||||
|
export const uploadFile = (file: File) =>
|
||||||
|
http.upload<UploadResponse>('/api/v1/upload', file);
|
||||||
|
|
||||||
|
// 特殊处理 - 文件下载
|
||||||
|
export const downloadFile = (fileId: string) =>
|
||||||
|
http.download('/api/v1/download', `file-${fileId}.pdf`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 错误处理规范
|
||||||
|
|
||||||
|
1. 统一处理:
|
||||||
|
```typescript
|
||||||
|
// 响应拦截器
|
||||||
|
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
||||||
|
const { data: result } = response;
|
||||||
|
if (result.success && result.code === 200) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
message.error(result.message || '操作失败');
|
||||||
|
return Promise.reject(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 错误拦截器
|
||||||
|
const errorHandler = (error: AxiosError) => {
|
||||||
|
const messages = {
|
||||||
|
401: '未授权,请重新登录',
|
||||||
|
403: '拒绝访问',
|
||||||
|
404: '资源未找到',
|
||||||
|
500: '服务器错误'
|
||||||
|
};
|
||||||
|
message.error(messages[error.response?.status] || '网络错误');
|
||||||
|
return Promise.reject(error);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 组件中使用:
|
||||||
|
```typescript
|
||||||
|
const handleSubmit = async (values: FormData) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await submitData(values);
|
||||||
|
message.success('操作成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## 5. React 开发规范
|
## 5. React 开发规范
|
||||||
|
|
||||||
1. 组件开发:
|
1. 组件开发:
|
||||||
- 使用函数组件和箭头函数定义
|
- 使用函数组件和箭头函数
|
||||||
- Props 类型必须明确定义
|
- Props 类型必须定义
|
||||||
- 必须提供默认值
|
- 必须提供默认值
|
||||||
- 必须有完整的 JSDoc 注释
|
- 使用 memo 优化
|
||||||
- 必须使用 memo 优化,除非确定不需要
|
- 复杂组件需拆分
|
||||||
- 复杂组件需要拆分为多个子组件
|
|
||||||
- 必须处理组件的所有状态和异常情况
|
|
||||||
|
|
||||||
2. Hooks 使用:
|
2. Hooks 使用:
|
||||||
- 自定义 hooks 必须以 `use` 开头
|
- 自定义 hooks 以 `use` 开头
|
||||||
- 必须使用 TypeScript 泛型
|
- 使用 TypeScript 泛型
|
||||||
- 必须处理加载状态和错误情况
|
- 使用 useCallback 和 useMemo
|
||||||
- 必须使用 useCallback 和 useMemo 优化性能
|
- 使用 Promise.all 处理并行请求
|
||||||
- 避免嵌套 `await`,使用 `Promise.all` 处理并行请求
|
|
||||||
|
|
||||||
3. 状态管理:
|
3. 状态管理:
|
||||||
- 使用 Redux Toolkit
|
- 使用 Redux Toolkit
|
||||||
- Slice 文件命名必须以 `Slice` 结尾
|
- 持久化数据存储在 localStorage
|
||||||
- Action 类型必须使用 `PayloadAction`
|
- Token、用户信息、菜单统一管理
|
||||||
- 必须定义清晰的状态接口
|
|
||||||
- 本地存储同步必须在 reducer 中处理
|
|
||||||
- 状态持久化:
|
|
||||||
- Token 统一存储在 localStorage
|
|
||||||
- 用户信息统一存储在 localStorage
|
|
||||||
- 清理时必须调用统一的 logout action
|
|
||||||
|
|
||||||
## 6. 表格组件规范
|
## 6. 样式与布局规范
|
||||||
|
|
||||||
1. 状态管理:
|
1. CSS 规范:
|
||||||
```typescript
|
- 使用 CSS Modules
|
||||||
interface TableState<T> {
|
|
||||||
list: T[];
|
|
||||||
pagination: {
|
|
||||||
current: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
loading: boolean;
|
|
||||||
selectedRows: T[];
|
|
||||||
selectedRowKeys: React.Key[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 功能实现:
|
|
||||||
- 必须实现标准的 CRUD 操作
|
|
||||||
- 必须处理分页、排序、筛选
|
|
||||||
- 必须处理选择行功能
|
|
||||||
- 必须提供重置功能
|
|
||||||
- 默认分页大小:10
|
|
||||||
- 页码从 1 开始
|
|
||||||
|
|
||||||
## 7. 样式开发规范
|
|
||||||
|
|
||||||
1. CSS 使用规范:
|
|
||||||
- 必须使用 CSS Modules
|
|
||||||
- 类名使用 kebab-case
|
- 类名使用 kebab-case
|
||||||
- 避免内联样式,除非是动态计算的值
|
- 避免内联样式
|
||||||
- 必须响应式适配
|
- 响应式适配
|
||||||
- 必须考虑暗色主题
|
- 支持暗色主题
|
||||||
|
|
||||||
2. 布局规范:
|
2. 布局规范:
|
||||||
- 统一使用 BasicLayout
|
- 使用 BasicLayout
|
||||||
- 必须实现面包屑导航
|
- 实现面包屑导航
|
||||||
- 必须处理响应式布局
|
- 菜单从后端获取
|
||||||
- 必须实现统一的错误页面
|
- 支持权限控制
|
||||||
- 导航菜单:
|
|
||||||
- 菜单数据必须从后端获取
|
|
||||||
- 必须支持多级菜单
|
|
||||||
- 必须支持菜单权限控制
|
|
||||||
- 必须支持菜单图标
|
|
||||||
|
|
||||||
## 8. 项目配置规范
|
## 7. 项目配置规范
|
||||||
|
|
||||||
1. 依赖管理:
|
1. 依赖版本:
|
||||||
- 核心依赖版本:
|
- React: ^18.2.0
|
||||||
- React: ^18.2.0
|
- TypeScript: ^5.3.3
|
||||||
- TypeScript: ^5.3.3
|
- Ant Design: ^5.22.2
|
||||||
- Ant Design: ^5.22.2
|
- Redux Toolkit: ^2.0.1
|
||||||
- Redux Toolkit: ^2.0.1
|
|
||||||
- 开发依赖必须锁定版本
|
|
||||||
- 生产依赖必须指定大版本
|
|
||||||
|
|
||||||
2. 开发规范:
|
2. 开发规范:
|
||||||
- 必须启用 ESLint
|
- 启用 ESLint 和 Prettier
|
||||||
- 必须使用 Prettier 格式化
|
- 配置 Git Hooks
|
||||||
- 必须配置 Git Hooks
|
- 使用 .env 管理环境变量
|
||||||
- commit message 遵循 Angular 规范
|
- 使用 Vite 构建
|
||||||
- 提交前必须进行 lint 和 type check
|
- 配置路由级代码分割
|
||||||
|
|
||||||
3. 环境配置:
|
|
||||||
- 使用 .env 文件管理环境变量
|
|
||||||
- 必须区分开发和生产环境
|
|
||||||
- 必须配置代理规则
|
|
||||||
- 必须处理跨域问题
|
|
||||||
|
|
||||||
4. 构建优化:
|
|
||||||
- 使用 Vite 作为构建工具
|
|
||||||
- 必须配置路径别名
|
|
||||||
- 必须优化构建性能
|
|
||||||
- 必须分离开发和生产配置
|
|
||||||
- 路由级别的代码分割
|
|
||||||
- 大型第三方库异步加载
|
|
||||||
|
|
||||||
5. 安全规范:
|
|
||||||
- 环境变量必须通过 .env 文件管理
|
|
||||||
- 禁止在代码中硬编码敏感信息
|
|
||||||
- 使用 ESLint security 插件
|
|
||||||
- 小心使用 dangerouslySetInnerHTML
|
|
||||||
|
|
||||||
6. 测试规范:
|
|
||||||
- 单元测试:
|
|
||||||
- 使用 Jest 和 React Testing Library
|
|
||||||
- 重要组件必须包含测试用例
|
|
||||||
- 测试文件以 .test.tsx 结尾
|
|
||||||
- E2E 测试:
|
|
||||||
- 使用 Cypress 进行端到端测试
|
|
||||||
- 覆盖关键业务流程
|
|
||||||
|
|
||||||
@ -15,8 +15,7 @@ const modulePath = path.join(__dirname, '../src/pages/System', moduleName);
|
|||||||
const directories = [
|
const directories = [
|
||||||
'',
|
'',
|
||||||
'components',
|
'components',
|
||||||
'hooks',
|
'types'
|
||||||
'styles'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 创建目录
|
// 创建目录
|
||||||
@ -30,7 +29,7 @@ directories.forEach(dir => {
|
|||||||
// 创建基础文件
|
// 创建基础文件
|
||||||
const files = {
|
const files = {
|
||||||
// 类型定义
|
// 类型定义
|
||||||
'types.ts': `import type { BaseResponse } from '@/types/base/response';
|
'types/index.ts': `import type { BaseResponse } from '@/types/base/response';
|
||||||
import type { BaseQuery } from '@/types/base/query';
|
import type { BaseQuery } from '@/types/base/query';
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
@ -52,59 +51,111 @@ export interface ${moduleName}Response extends BaseResponse {
|
|||||||
// API 服务
|
// API 服务
|
||||||
'service.ts': `import request from '@/utils/request';
|
'service.ts': `import request from '@/utils/request';
|
||||||
import type { ${moduleName}Response, ${moduleName}Request, ${moduleName}Query } from './types';
|
import type { ${moduleName}Response, ${moduleName}Request, ${moduleName}Query } from './types';
|
||||||
import type { Page } from '@/types/base/page';
|
|
||||||
|
|
||||||
const BASE_URL = '/api/v1/${moduleName.toLowerCase()}';
|
// 获取列表
|
||||||
|
export const getList = (params?: ${moduleName}Query) =>
|
||||||
// 获取列表(分页)
|
request.get<${moduleName}Response[]>('/api/v1/${moduleName.toLowerCase()}s', { params });
|
||||||
export const get${moduleName}s = async (params?: ${moduleName}Query) =>
|
|
||||||
request.get<Page<${moduleName}Response>>(\`\${BASE_URL}/page\`, { params });
|
|
||||||
|
|
||||||
// 创建
|
// 创建
|
||||||
export const create${moduleName} = async (data: ${moduleName}Request) =>
|
export const create = (data: ${moduleName}Request) =>
|
||||||
request.post<${moduleName}Response>(BASE_URL, data);
|
request.post<${moduleName}Response>('/api/v1/${moduleName.toLowerCase()}s', data);
|
||||||
|
|
||||||
// 更新
|
// 更新
|
||||||
export const update${moduleName} = async (id: number, data: ${moduleName}Request) =>
|
export const update = (id: number, data: ${moduleName}Request) =>
|
||||||
request.put<${moduleName}Response>(\`\${BASE_URL}/\${id}\`, data);
|
request.put<${moduleName}Response>(\`/api/v1/${moduleName.toLowerCase()}s/\${id}\`, data);
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
export const delete${moduleName} = async (id: number) =>
|
export const remove = (id: number) =>
|
||||||
request.delete(\`\${BASE_URL}/\${id}\`);
|
request.delete(\`/api/v1/${moduleName.toLowerCase()}s/\${id}\`);
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
export const batchRemove = (ids: number[]) =>
|
||||||
|
request.post('/api/v1/${moduleName.toLowerCase()}s/batch-delete', { ids });
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
export const exportData = (params?: ${moduleName}Query) =>
|
||||||
|
request.get('/api/v1/${moduleName.toLowerCase()}s/export', {
|
||||||
|
params,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 主页面组件
|
// 主页面组件
|
||||||
'index.tsx': `import React from 'react';
|
'index.tsx': `import React, { useState } from 'react';
|
||||||
import { Card } from 'antd';
|
import { Card, message } from 'antd';
|
||||||
import type { ${moduleName}Response } from './types';
|
import type { ${moduleName}Response } from './types';
|
||||||
import { use${moduleName}Data } from './hooks/use${moduleName}Data';
|
import * as service from './service';
|
||||||
import ${moduleName}Table from './components/${moduleName}Table';
|
import ${moduleName}Table from './components/${moduleName}Table';
|
||||||
import ${moduleName}Form from './components/${moduleName}Form';
|
import ${moduleName}Form from './components/${moduleName}Form';
|
||||||
import styles from './styles/index.module.css';
|
|
||||||
|
|
||||||
const ${moduleName}: React.FC = () => {
|
const ${moduleName}: React.FC = () => {
|
||||||
const {
|
const [loading, setLoading] = useState(false);
|
||||||
list,
|
const [list, setList] = useState<${moduleName}Response[]>([]);
|
||||||
pagination,
|
const [selectedRows, setSelectedRows] = useState<${moduleName}Response[]>([]);
|
||||||
loading,
|
|
||||||
selectedRows,
|
const loadData = async () => {
|
||||||
handleCreate,
|
setLoading(true);
|
||||||
handleUpdate,
|
try {
|
||||||
handleDelete,
|
const data = await service.getList();
|
||||||
handleSelectionChange
|
setList(data);
|
||||||
} = use${moduleName}Data();
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (values: ${moduleName}Request) => {
|
||||||
|
try {
|
||||||
|
await service.create(values);
|
||||||
|
message.success('创建成功');
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (id: number, values: ${moduleName}Request) => {
|
||||||
|
try {
|
||||||
|
await service.update(id, values);
|
||||||
|
message.success('更新成功');
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await service.remove(id);
|
||||||
|
message.success('删除成功');
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
try {
|
||||||
|
await service.batchRemove(selectedRows.map(row => row.id));
|
||||||
|
message.success('批量删除成功');
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="${moduleName} 管理" className={styles.container}>
|
<Card title="${moduleName} 管理">
|
||||||
<${moduleName}Form onSubmit={handleCreate} />
|
<${moduleName}Form onSubmit={handleCreate} />
|
||||||
<${moduleName}Table
|
<${moduleName}Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
pagination={pagination}
|
|
||||||
selectedRows={selectedRows}
|
selectedRows={selectedRows}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onBatchDelete={handleBatchDelete}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={setSelectedRows}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@ -117,28 +168,23 @@ export default ${moduleName};
|
|||||||
'components/${moduleName}Table.tsx': `import React from 'react';
|
'components/${moduleName}Table.tsx': `import React from 'react';
|
||||||
import { Table, Space, Button, Popconfirm } from 'antd';
|
import { Table, Space, Button, Popconfirm } from 'antd';
|
||||||
import type { ${moduleName}Response } from '../types';
|
import type { ${moduleName}Response } from '../types';
|
||||||
import styles from '../styles/table.module.css';
|
|
||||||
|
|
||||||
interface ${moduleName}TableProps {
|
interface ${moduleName}TableProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
dataSource: ${moduleName}Response[];
|
dataSource: ${moduleName}Response[];
|
||||||
pagination: {
|
|
||||||
current: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
selectedRows: ${moduleName}Response[];
|
selectedRows: ${moduleName}Response[];
|
||||||
onDelete: (id: number) => Promise<void>;
|
onDelete: (id: number) => Promise<void>;
|
||||||
onUpdate: (id: number, data: Partial<${moduleName}Response>) => Promise<void>;
|
onBatchDelete: () => Promise<void>;
|
||||||
onSelectionChange: (selectedRows: ${moduleName}Response[]) => void;
|
onUpdate: (id: number, data: ${moduleName}Response) => Promise<void>;
|
||||||
|
onSelectionChange: (rows: ${moduleName}Response[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ${moduleName}Table: React.FC<${moduleName}TableProps> = ({
|
const ${moduleName}Table: React.FC<${moduleName}TableProps> = ({
|
||||||
loading,
|
loading,
|
||||||
dataSource,
|
dataSource,
|
||||||
pagination,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onBatchDelete,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onSelectionChange
|
onSelectionChange
|
||||||
}) => {
|
}) => {
|
||||||
@ -166,18 +212,30 @@ const ${moduleName}Table: React.FC<${moduleName}TableProps> = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<>
|
||||||
className={styles.table}
|
{selectedRows.length > 0 && (
|
||||||
rowKey="id"
|
<div style={{ marginBottom: 16 }}>
|
||||||
loading={loading}
|
<Popconfirm
|
||||||
dataSource={dataSource}
|
title="确定要删除选中的项目吗?"
|
||||||
columns={columns}
|
onConfirm={onBatchDelete}
|
||||||
pagination={pagination}
|
>
|
||||||
rowSelection={{
|
<Button type="primary" danger>
|
||||||
selectedRowKeys: selectedRows.map(row => row.id),
|
批量删除
|
||||||
onChange: (_, rows) => onSelectionChange(rows),
|
</Button>
|
||||||
}}
|
</Popconfirm>
|
||||||
/>
|
</div>
|
||||||
|
)}
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: selectedRows.map(row => row.id),
|
||||||
|
onChange: (_, rows) => onSelectionChange(rows),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,7 +246,6 @@ export default ${moduleName}Table;
|
|||||||
'components/${moduleName}Form.tsx': `import React from 'react';
|
'components/${moduleName}Form.tsx': `import React from 'react';
|
||||||
import { Form, Input, Button } from 'antd';
|
import { Form, Input, Button } from 'antd';
|
||||||
import type { ${moduleName}Request } from '../types';
|
import type { ${moduleName}Request } from '../types';
|
||||||
import styles from '../styles/form.module.css';
|
|
||||||
|
|
||||||
interface ${moduleName}FormProps {
|
interface ${moduleName}FormProps {
|
||||||
initialValues?: Partial<${moduleName}Request>;
|
initialValues?: Partial<${moduleName}Request>;
|
||||||
@ -202,9 +259,13 @@ const ${moduleName}Form: React.FC<${moduleName}FormProps> = ({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const values = await form.validateFields();
|
try {
|
||||||
await onSubmit(values);
|
const values = await form.validateFields();
|
||||||
form.resetFields();
|
await onSubmit(values);
|
||||||
|
form.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单提交失败:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -212,7 +273,7 @@ const ${moduleName}Form: React.FC<${moduleName}FormProps> = ({
|
|||||||
form={form}
|
form={form}
|
||||||
layout="inline"
|
layout="inline"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
className={styles.form}
|
style={{ marginBottom: 16 }}
|
||||||
>
|
>
|
||||||
{/* TODO: 添加表单项 */}
|
{/* TODO: 添加表单项 */}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
@ -225,62 +286,6 @@ const ${moduleName}Form: React.FC<${moduleName}FormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ${moduleName}Form;
|
export default ${moduleName}Form;
|
||||||
`,
|
|
||||||
|
|
||||||
// 自定义 Hook
|
|
||||||
'hooks/use${moduleName}Data.ts': `import { useState, useCallback } from 'react';
|
|
||||||
import type { ${moduleName}Response, ${moduleName}Request } from '../types';
|
|
||||||
import { useTableData } from '@/hooks/useTableData';
|
|
||||||
import * as service from '../service';
|
|
||||||
|
|
||||||
export const use${moduleName}Data = () => {
|
|
||||||
const [selectedRows, setSelectedRows] = useState<${moduleName}Response[]>([]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list,
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
loadData,
|
|
||||||
handleCreate,
|
|
||||||
handleUpdate,
|
|
||||||
handleDelete
|
|
||||||
} = useTableData<${moduleName}Response>({
|
|
||||||
service: {
|
|
||||||
baseUrl: '/api/v1/${moduleName.toLowerCase()}'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelectionChange = useCallback((rows: ${moduleName}Response[]) => {
|
|
||||||
setSelectedRows(rows);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
list,
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
selectedRows,
|
|
||||||
handleCreate,
|
|
||||||
handleUpdate,
|
|
||||||
handleDelete,
|
|
||||||
handleSelectionChange
|
|
||||||
};
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
|
|
||||||
// 样式文件
|
|
||||||
'styles/index.module.css': `.container {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
'styles/table.module.css': `.table {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
'styles/form.module.css': `.form {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,11 +15,10 @@ import {logout, setMenus} from '../store/userSlice';
|
|||||||
import type {MenuProps} from 'antd';
|
import type {MenuProps} from 'antd';
|
||||||
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
||||||
import {getWeather} from '../services/weather';
|
import {getWeather} from '../services/weather';
|
||||||
import type {MenuResponse} from '@/pages/System/Menu/types';
|
|
||||||
import {MenuTypeEnum} from '@/pages/System/Menu/types';
|
|
||||||
import type {RootState} from '../store';
|
import type {RootState} from '../store';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types";
|
||||||
|
|
||||||
const {Header, Content, Sider} = Layout;
|
const {Header, Content, Sider} = Layout;
|
||||||
const {confirm} = Modal;
|
const {confirm} = Modal;
|
||||||
@ -27,28 +26,6 @@ const {confirm} = Modal;
|
|||||||
// 设置中文语言
|
// 设置中文语言
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// 添加清理空children的函数,同时过滤掉按钮类型菜单
|
|
||||||
const cleanEmptyChildren = (menus: MenuResponse[]): MenuResponse[] => {
|
|
||||||
// 先过滤掉按钮类型的菜单
|
|
||||||
return menus
|
|
||||||
.filter(menu => menu.type !== MenuTypeEnum.BUTTON)
|
|
||||||
.map(menu => {
|
|
||||||
const cleanedMenu = { ...menu };
|
|
||||||
|
|
||||||
if (cleanedMenu.children && cleanedMenu.children.length > 0) {
|
|
||||||
// 递归处理子菜单,同样需要过滤按钮
|
|
||||||
cleanedMenu.children = cleanEmptyChildren(cleanedMenu.children);
|
|
||||||
// 如果过滤后子菜单为空,则删除children属性
|
|
||||||
if (cleanedMenu.children.length === 0) {
|
|
||||||
delete cleanedMenu.children;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete cleanedMenu.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanedMenu;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const BasicLayout: React.FC = () => {
|
const BasicLayout: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -59,6 +36,7 @@ const BasicLayout: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||||
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
// 将天气获取逻辑提取为useCallback
|
// 将天气获取逻辑提取为useCallback
|
||||||
const fetchWeather = useCallback(async () => {
|
const fetchWeather = useCallback(async () => {
|
||||||
@ -76,7 +54,7 @@ const BasicLayout: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const menuData = await getCurrentUserMenus();
|
const menuData = await getCurrentUserMenus();
|
||||||
dispatch(setMenus(cleanEmptyChildren(menuData)));
|
dispatch(setMenus(menuData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取菜单数据失败');
|
message.error('获取菜单数据失败');
|
||||||
console.error('获取菜单数据失败:', error);
|
console.error('获取菜单数据失败:', error);
|
||||||
@ -158,15 +136,24 @@ const BasicLayout: React.FC = () => {
|
|||||||
// 将菜单数据转换为antd Menu需要的格式
|
// 将菜单数据转换为antd Menu需要的格式
|
||||||
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
||||||
return menuList
|
return menuList
|
||||||
?.filter(menu => menu.type !== MenuTypeEnum.BUTTON) // 过滤掉按钮类型的菜单
|
?.map(menu => {
|
||||||
?.map(menu => ({
|
// 确保path存在,否则使用id
|
||||||
key: menu.path || menu.id.toString(),
|
const key = menu.path || `menu-${menu.id}`;
|
||||||
icon: getIcon(menu.icon),
|
return {
|
||||||
label: menu.name,
|
key,
|
||||||
children: menu.children && menu.children.length > 0
|
icon: getIcon(menu.icon),
|
||||||
? getMenuItems(menu.children) // 递归处理子菜单
|
label: menu.name,
|
||||||
: undefined
|
children: menu.children && menu.children.length > 0
|
||||||
}));
|
? getMenuItems(menu.children)
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理菜单展开/收起
|
||||||
|
const handleOpenChange = (keys: string[]) => {
|
||||||
|
console.log('Menu open change:', keys);
|
||||||
|
setOpenKeys(keys);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -202,8 +189,13 @@ const BasicLayout: React.FC = () => {
|
|||||||
theme="dark"
|
theme="dark"
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
|
openKeys={openKeys}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
items={getMenuItems(menus)}
|
items={getMenuItems(menus)}
|
||||||
onClick={({key}) => navigate(key)}
|
onClick={({key}) => {
|
||||||
|
console.log('Menu click:', key);
|
||||||
|
navigate(key);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -239,22 +231,26 @@ const BasicLayout: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
|
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
|
||||||
<span style={{
|
<span style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
borderRadius: 4,
|
borderRadius: 4
|
||||||
'&:hover': {
|
}}
|
||||||
backgroundColor: 'rgba(0,0,0,0.025)'
|
onMouseEnter={(e) => {
|
||||||
}
|
e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.025)';
|
||||||
}}>
|
}}
|
||||||
<UserOutlined style={{fontSize: 16, marginRight: 8}}/>
|
onMouseLeave={(e) => {
|
||||||
<span>{userInfo?.nickname || userInfo?.username}</span>
|
e.currentTarget.style.backgroundColor = 'transparent';
|
||||||
<DownOutlined style={{fontSize: 12, marginLeft: 6}}/>
|
}}
|
||||||
</span>
|
>
|
||||||
|
<UserOutlined style={{fontSize: 16, marginRight: 8}}/>
|
||||||
|
<span>{userInfo?.nickname || userInfo?.username}</span>
|
||||||
|
<DownOutlined style={{fontSize: 12, marginLeft: 6}}/>
|
||||||
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import React, {useEffect, useState} from 'react';
|
|||||||
import {Form, Input, Button, message, Select} from 'antd';
|
import {Form, Input, Button, message, Select} from 'antd';
|
||||||
import {useNavigate} from 'react-router-dom';
|
import {useNavigate} from 'react-router-dom';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useDispatch} from 'react-redux';
|
||||||
import {login, getTenantList} from './service';
|
import {login} from './service';
|
||||||
|
import {getEnabledTenants} from '@/pages/System/Tenant/service';
|
||||||
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
|
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
|
||||||
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
||||||
import type { MenuResponse } from '@/pages/System/Menu/types';
|
import type { MenuResponse } from '@/pages/System/Menu/types';
|
||||||
import type { TenantResponse } from './types';
|
import type { TenantResponse } from '@/pages/System/Tenant/types';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
interface LoginForm {
|
interface LoginForm {
|
||||||
@ -25,7 +26,7 @@ const Login: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTenants = async () => {
|
const fetchTenants = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getTenantList();
|
const data = await getEnabledTenants();
|
||||||
setTenants(data);
|
setTenants(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取租户列表失败:', error);
|
console.error('获取租户列表失败:', error);
|
||||||
@ -35,44 +36,39 @@ const Login: React.FC = () => {
|
|||||||
fetchTenants();
|
fetchTenants();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 清理空children的函数,同时过滤掉按钮类型菜单
|
// 加载菜单数据
|
||||||
const cleanEmptyChildren = (menus: MenuResponse[]): MenuResponse[] => {
|
const loadMenuData = async () => {
|
||||||
return menus
|
try {
|
||||||
.filter(menu => menu.type !== 2) // 过滤掉按钮类型的菜单
|
const menuData = await getCurrentUserMenus();
|
||||||
.map(menu => {
|
if (menuData && menuData.length > 0) {
|
||||||
const cleanedMenu = { ...menu };
|
dispatch(setMenus(menuData));
|
||||||
|
}
|
||||||
if (cleanedMenu.children && cleanedMenu.children.length > 0) {
|
} catch (error) {
|
||||||
cleanedMenu.children = cleanEmptyChildren(cleanedMenu.children);
|
console.error('获取菜单数据失败:', error);
|
||||||
if (cleanedMenu.children.length === 0) {
|
}
|
||||||
delete cleanedMenu.children;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete cleanedMenu.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanedMenu;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: LoginForm) => {
|
const handleFinish = async (values: LoginForm) => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
|
||||||
// 1. 登录获取 token 和用户信息
|
// 1. 登录获取 token 和用户信息
|
||||||
const result = await login(values);
|
const loginData = await login(values);
|
||||||
dispatch(setToken(result.token));
|
dispatch(setToken(loginData.token));
|
||||||
dispatch(setUserInfo(result));
|
dispatch(setUserInfo({
|
||||||
|
id: loginData.id,
|
||||||
|
username: loginData.username,
|
||||||
|
nickname: loginData.nickname,
|
||||||
|
email: loginData.email,
|
||||||
|
phone: loginData.phone
|
||||||
|
}));
|
||||||
|
|
||||||
// 2. 获取菜单数据
|
// 2. 获取菜单数据
|
||||||
const menuData = await getCurrentUserMenus();
|
await loadMenuData();
|
||||||
dispatch(setMenus(cleanEmptyChildren(menuData)));
|
|
||||||
|
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
// 3. 所有数据都准备好后再跳转
|
navigate('/');
|
||||||
navigate('/dashboard', {replace: true});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
message.error('登录失败,请重试');
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -87,7 +83,7 @@ const Login: React.FC = () => {
|
|||||||
<Form<LoginForm>
|
<Form<LoginForm>
|
||||||
form={form}
|
form={form}
|
||||||
name="login"
|
name="login"
|
||||||
onFinish={onFinish}
|
onFinish={handleFinish}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,32 +1,21 @@
|
|||||||
import request from '@/utils/request';
|
import http from '@/utils/request';
|
||||||
|
import type { Response } from '@/utils/request';
|
||||||
import type { LoginRequest, LoginResponse } from './types';
|
import type { LoginRequest, LoginResponse } from './types';
|
||||||
import type { TenantResponse } from '../System/Tenant/types';
|
import type { MenuResponse } from '@/pages/System/Menu/types';
|
||||||
import type { MenuResponse } from '../System/Menu/types';
|
import type { TenantResponse } from '@/pages/System/Tenant/types';
|
||||||
|
|
||||||
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
// 登录
|
||||||
return request.post('/api/v1/user/login', data, {
|
export const login = (data: LoginRequest) =>
|
||||||
errorMessage: '登录失败,请检查用户名和密码'
|
http.post<LoginResponse>('/api/v1/user/login', data);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logout = async (): Promise<void> => {
|
// 退出登录
|
||||||
return request.post('/api/v1/user/logout', null, {
|
export const logout = () =>
|
||||||
errorMessage: '退出登录失败,请稍后重试'
|
http.post('/api/v1/user/logout');
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentUser = async (): Promise<LoginResponse> => {
|
// 获取当前用户菜单
|
||||||
return request.get('/api/v1/user/current');
|
export const getCurrentUserMenus = () =>
|
||||||
};
|
http.get<MenuResponse[]>('/api/v1/menu/current');
|
||||||
|
|
||||||
export const getUserMenus = async (): Promise<MenuResponse[]> => {
|
// 获取租户列表
|
||||||
return request.get('/api/v1/user/menus', {
|
export const getTenantList = () =>
|
||||||
errorMessage: '获取菜单失败,请刷新页面重试'
|
http.get<TenantResponse[]>('/api/v1/tenant/list');
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTenantList = async (): Promise<TenantResponse[]> => {
|
|
||||||
return request.get('/api/v1/tenant/list', {
|
|
||||||
errorMessage: '获取租户列表失败,请刷新重试'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './request';
|
|
||||||
export * from './response';
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export interface LoginRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
tenantId?: string;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
export interface LoginResponse {
|
|
||||||
id: number;
|
|
||||||
username: string;
|
|
||||||
nickname?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { BaseQuery } from '@/types/base/query';
|
|
||||||
|
|
||||||
export interface DepartmentQuery extends BaseQuery {
|
|
||||||
name?: string;
|
|
||||||
code?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { BaseRequest } from '@/types/base/request';
|
|
||||||
|
|
||||||
export interface DepartmentRequest extends BaseRequest {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
parentId?: number;
|
|
||||||
sort?: number;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { BaseResponse } from '@/types/base/response';
|
|
||||||
|
|
||||||
export interface DepartmentResponse extends BaseResponse {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
parentId?: number;
|
|
||||||
sort: number;
|
|
||||||
description?: string;
|
|
||||||
children?: DepartmentResponse[];
|
|
||||||
}
|
|
||||||
@ -13,9 +13,9 @@ export const getMenuTree = () =>
|
|||||||
request.get<MenuResponse[]>(`${BASE_URL}/tree`);
|
request.get<MenuResponse[]>(`${BASE_URL}/tree`);
|
||||||
|
|
||||||
// 获取当前用户菜单
|
// 获取当前用户菜单
|
||||||
export const getCurrentUserMenus = () => {
|
export const getCurrentUserMenus = () =>
|
||||||
return request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
request.get<MenuResponse[]>(`${BASE_URL}/current`);
|
||||||
}
|
|
||||||
|
|
||||||
// 创建菜单
|
// 创建菜单
|
||||||
export const createMenu = (data: MenuRequest) =>
|
export const createMenu = (data: MenuRequest) =>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { BaseResponse } from '@/types/base/response';
|
import {BaseQuery} from "@/types/base/query.ts";
|
||||||
|
import {BaseRequest} from "@/types/base/request.ts";
|
||||||
|
import {BaseResponse} from "@/types/base/response.ts";
|
||||||
|
|
||||||
export enum MenuTypeEnum {
|
export enum MenuTypeEnum {
|
||||||
DIRECTORY = 1, // 目录
|
DIRECTORY = 1, // 目录
|
||||||
@ -6,23 +8,12 @@ export enum MenuTypeEnum {
|
|||||||
BUTTON = 3 // 按钮
|
BUTTON = 3 // 按钮
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuTypeNames = {
|
export interface MenuQuery extends BaseQuery {
|
||||||
[MenuTypeEnum.DIRECTORY]: '目录',
|
|
||||||
[MenuTypeEnum.MENU]: '菜单',
|
|
||||||
[MenuTypeEnum.BUTTON]: '按钮'
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface MenuQuery {
|
|
||||||
pageNum?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
sortField?: string;
|
|
||||||
sortOrder?: 'ascend' | 'descend';
|
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: MenuTypeEnum;
|
type?: MenuTypeEnum;
|
||||||
enabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuRequest {
|
export interface MenuRequest extends BaseRequest {
|
||||||
name: string;
|
name: string;
|
||||||
type: MenuTypeEnum;
|
type: MenuTypeEnum;
|
||||||
parentId?: number;
|
parentId?: number;
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export * from './query';
|
|
||||||
export * from './request';
|
|
||||||
export * from './response';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { BaseQuery } from '@/types/base/query';
|
|
||||||
|
|
||||||
export interface MenuQuery extends BaseQuery {
|
|
||||||
name?: string;
|
|
||||||
path?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { BaseRequest } from '@/types/base/request';
|
|
||||||
|
|
||||||
export interface MenuRequest extends BaseRequest {
|
|
||||||
name: string;
|
|
||||||
path?: string;
|
|
||||||
icon?: string;
|
|
||||||
parentId?: number;
|
|
||||||
sort?: number;
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { BaseResponse } from '@/types/base/response';
|
|
||||||
|
|
||||||
export interface MenuResponse extends BaseResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
path?: string;
|
|
||||||
icon?: string;
|
|
||||||
parentId?: number;
|
|
||||||
sort: number;
|
|
||||||
enabled: boolean;
|
|
||||||
children?: MenuResponse[];
|
|
||||||
}
|
|
||||||
@ -3,31 +3,22 @@ import type { TenantResponse, TenantRequest, TenantQuery } from './types';
|
|||||||
|
|
||||||
export const getTenants = async (params?: TenantQuery) => {
|
export const getTenants = async (params?: TenantQuery) => {
|
||||||
return request.get('/api/v1/tenant', {
|
return request.get('/api/v1/tenant', {
|
||||||
params,
|
params
|
||||||
errorMessage: '获取租户列表失败,请刷新重试'
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTenant = async (data: TenantRequest) => {
|
export const createTenant = async (data: TenantRequest) => {
|
||||||
return request.post('/api/v1/tenant', data, {
|
return request.post('/api/v1/tenant', data);
|
||||||
errorMessage: '创建租户失败,请稍后重试'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTenant = async (id: number, data: TenantRequest) => {
|
export const updateTenant = async (id: number, data: TenantRequest) => {
|
||||||
return request.put(`/api/v1/tenant/${id}`, data, {
|
return request.put(`/api/v1/tenant/${id}`, data);
|
||||||
errorMessage: '更新租户失败,请稍后重试'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTenant = async (id: number) => {
|
export const deleteTenant = async (id: number) => {
|
||||||
return request.delete(`/api/v1/tenant/${id}`, {
|
return request.delete(`/api/v1/tenant/${id}`);
|
||||||
errorMessage: '删除租户失败,请稍后重试'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnabledTenants = async () => {
|
export const getEnabledTenants = async () => {
|
||||||
return request.get('/api/v1/tenant/enabled', {
|
return request.get('/api/v1/tenant/list');
|
||||||
errorMessage: '获取可用租户列表失败,请刷新重试'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from './query';
|
|
||||||
export * from './request';
|
|
||||||
export * from './response';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { BaseQuery } from '@/types/base/query';
|
|
||||||
|
|
||||||
export interface TenantQuery extends BaseQuery {
|
|
||||||
name?: string;
|
|
||||||
code?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { BaseRequest } from '@/types/base/request';
|
|
||||||
|
|
||||||
export interface TenantRequest extends BaseRequest {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
address?: string;
|
|
||||||
contactName?: string;
|
|
||||||
contactPhone?: string;
|
|
||||||
email?: string;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { BaseResponse } from '@/types/base/response';
|
|
||||||
|
|
||||||
export interface TenantResponse extends BaseResponse {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
address?: string;
|
|
||||||
contactName?: string;
|
|
||||||
contactPhone?: string;
|
|
||||||
email?: string;
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from './query';
|
|
||||||
export * from './request';
|
|
||||||
export * from './response';
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { BaseQuery } from '@/types/base/query';
|
|
||||||
|
|
||||||
export interface UserQuery extends BaseQuery {
|
|
||||||
username?: string;
|
|
||||||
nickname?: string;
|
|
||||||
email?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { BaseRequest } from '@/types/base/request';
|
|
||||||
|
|
||||||
export interface UserRequest extends BaseRequest {
|
|
||||||
username: string;
|
|
||||||
nickname?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
password?: string;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { BaseResponse } from '@/types/base/response';
|
|
||||||
|
|
||||||
export interface UserResponse extends BaseResponse {
|
|
||||||
username: string;
|
|
||||||
nickname?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
}
|
|
||||||
@ -18,7 +18,7 @@ interface UserState {
|
|||||||
const initialState: UserState = {
|
const initialState: UserState = {
|
||||||
token: localStorage.getItem('token'),
|
token: localStorage.getItem('token'),
|
||||||
userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null,
|
userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null,
|
||||||
menus: []
|
menus: localStorage.getItem('menus') ? JSON.parse(localStorage.getItem('menus')!) : []
|
||||||
};
|
};
|
||||||
|
|
||||||
const userSlice = createSlice({
|
const userSlice = createSlice({
|
||||||
@ -35,6 +35,9 @@ const userSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setMenus: (state, action: PayloadAction<MenuResponse[]>) => {
|
setMenus: (state, action: PayloadAction<MenuResponse[]>) => {
|
||||||
state.menus = action.payload;
|
state.menus = action.payload;
|
||||||
|
console.log(action.payload)
|
||||||
|
// console.log(JSON.stringify(action.payload))
|
||||||
|
// localStorage.setItem('menus', action.payload);
|
||||||
},
|
},
|
||||||
logout: (state) => {
|
logout: (state) => {
|
||||||
state.token = null;
|
state.token = null;
|
||||||
@ -43,6 +46,7 @@ const userSlice = createSlice({
|
|||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('tenantId');
|
localStorage.removeItem('tenantId');
|
||||||
localStorage.removeItem('userInfo');
|
localStorage.removeItem('userInfo');
|
||||||
|
localStorage.removeItem('menus');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export interface BaseRequest {
|
export interface BaseRequest {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
|||||||
if (result.success && result.code === 200) {
|
if (result.success && result.code === 200) {
|
||||||
return result.data;
|
return result.data;
|
||||||
} else {
|
} else {
|
||||||
if (result.message !== undefined) {
|
if (result.message != undefined) {
|
||||||
message.error(result.message || defaultErrorMessage);
|
message.error(result.message || defaultErrorMessage);
|
||||||
return Promise.reject(response);
|
return Promise.reject(response);
|
||||||
}
|
}
|
||||||
@ -104,18 +104,18 @@ const http = {
|
|||||||
request.get<any, T>(url, createRequestConfig(config)),
|
request.get<any, T>(url, createRequestConfig(config)),
|
||||||
|
|
||||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||||
request.post<any, Response<T>>(url, data, createRequestConfig(config)),
|
request.post<any, T>(url, data, createRequestConfig(config)),
|
||||||
|
|
||||||
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||||
request.put<any, Response<T>>(url, data, createRequestConfig(config)),
|
request.put<any, T>(url, data, createRequestConfig(config)),
|
||||||
|
|
||||||
delete: <T = any>(url: string, config?: RequestOptions) =>
|
delete: <T = any>(url: string, config?: RequestOptions) =>
|
||||||
request.delete<any, Response<T>>(url, createRequestConfig(config)),
|
request.delete<any, T>(url, createRequestConfig(config)),
|
||||||
|
|
||||||
upload: <T = any>(url: string, file: File, config?: RequestOptions) => {
|
upload: <T = any>(url: string, file: File, config?: RequestOptions) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return request.post<any, Response<T>>(url, formData, createRequestConfig({
|
return request.post<any, T>(url, formData, createRequestConfig({
|
||||||
headers: {'Content-Type': 'multipart/form-data'},
|
headers: {'Content-Type': 'multipart/form-data'},
|
||||||
...config
|
...config
|
||||||
}));
|
}));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user