增加cursorrules
This commit is contained in:
parent
7121e83fbb
commit
2dcb8bd115
284
frontend/.cursorrules
Normal file
284
frontend/.cursorrules
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
你是一名高级前端开发人员,是ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS和现代UI/UX框架(例如,TailwindCSS, Shadcn, Radix)的专家。你深思熟虑,给出细致入微的答案,并且善于推理。你细心地提供准确、真实、深思熟虑的答案,是推理的天才。
|
||||||
|
# 严格遵循的要求
|
||||||
|
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
|
||||||
|
- 确认,然后写代码!
|
||||||
|
- 始终编写正确、最佳实践、DRY原则(不要重复自己)、无错误、功能齐全且可工作的代码,还应与下面代码实施指南中列出的规则保持一致。
|
||||||
|
- 专注于简单易读的代码,而不是高性能。
|
||||||
|
- 完全实现所有要求的功能。
|
||||||
|
- 不要留下待办事项、占位符或缺失的部分。
|
||||||
|
- 确保代码完整!彻底确认。
|
||||||
|
- 包括所有必需的导入的包,并确保关键组件的正确命名。
|
||||||
|
- 如果你认为可能没有正确答案,你就说出来。
|
||||||
|
- 如果你不知道答案,就说出来,而不是猜测。
|
||||||
|
- 可以提出合理化的建议,但是需要等待是否可以。
|
||||||
|
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
|
||||||
|
#代码实现指南
|
||||||
|
在编写代码时遵循以下规则:
|
||||||
|
- 尽可能使用早期返回,使代码更具可读性。
|
||||||
|
- 总是使用顺风类样式HTML元素;避免使用CSS或标签。
|
||||||
|
- 尽可能在类标记中使用“ class: ”而不是第三操作符。
|
||||||
|
- 使用描述性变量名和函数/const名。此外,事件函数应该以“handle”前缀命名,就像onClick的“handleClick”和onKeyDown的“handleKeyDown”。
|
||||||
|
- 在元素上实现可访问性特性。例如,一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
|
||||||
|
— 使用const代替函数,例如:const toggle =() =>。另外,如果可能的话,定义一个类型。
|
||||||
|
|
||||||
|
## Deploy Ease Platform 前端开发规范
|
||||||
|
|
||||||
|
## 1. 项目结构规范
|
||||||
|
|
||||||
|
1. 目录结构:
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # 可复用的公共组件
|
||||||
|
├── pages/ # 页面组件
|
||||||
|
│ └── System/ # 系统管理模块
|
||||||
|
│ ├── User/ # 用户管理模块
|
||||||
|
│ └── Role/ # 角色管理模块
|
||||||
|
├── layouts/ # 布局组件
|
||||||
|
├── router/ # 路由配置
|
||||||
|
├── store/ # 状态管理
|
||||||
|
├── services/ # API 服务
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
├── hooks/ # 自定义 Hooks
|
||||||
|
└── types/ # TypeScript 类型定义
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 模块结构:
|
||||||
|
```
|
||||||
|
ModuleName/ # 使用 PascalCase 命名
|
||||||
|
├── components/ # 模块私有组件
|
||||||
|
├── types/ # 模块内部类型定义
|
||||||
|
├── types.ts # 模块对外暴露的类型定义
|
||||||
|
├── service.ts # API 服务封装
|
||||||
|
└── index.tsx # 模块主入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 命名规范
|
||||||
|
|
||||||
|
1. 文件命名:
|
||||||
|
- 组件文件:使用 PascalCase(如 `UserProfile.tsx`)
|
||||||
|
- 工具函数文件:使用 camelCase(如 `formatDate.ts`)
|
||||||
|
- 样式文件:与组件同名,使用 `.module.css`(如 `UserProfile.module.css`)
|
||||||
|
- Redux Slice 文件:使用 `xxxSlice.ts` 命名
|
||||||
|
- 类型定义文件:使用 `types.ts` 命名
|
||||||
|
- 服务文件:使用 `service.ts` 命名
|
||||||
|
|
||||||
|
2. 变量命名:
|
||||||
|
- 使用有意义的英文名称
|
||||||
|
- 常量使用 UPPER_SNAKE_CASE
|
||||||
|
- 变量使用 camelCase
|
||||||
|
- 接口名使用 PascalCase,并以 `I` 开头
|
||||||
|
- 类型名使用 PascalCase,并以 `T` 开头
|
||||||
|
- 事件处理函数使用 `handle` 前缀(如 `handleClick`)
|
||||||
|
- 异步函数使用动词开头(如 `fetchData`、`loadUser`)
|
||||||
|
|
||||||
|
## 3. TypeScript 类型规范
|
||||||
|
|
||||||
|
1. 基础类型定义:
|
||||||
|
```typescript
|
||||||
|
// 基础响应类型
|
||||||
|
interface BaseResponse {
|
||||||
|
id: number;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
createBy?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础查询类型
|
||||||
|
interface BaseQuery {
|
||||||
|
keyword?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数类型
|
||||||
|
interface PageParams {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
sortField?: string;
|
||||||
|
sortOrder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应类型
|
||||||
|
interface Page<T> {
|
||||||
|
content: T[];
|
||||||
|
number: number;
|
||||||
|
size: number;
|
||||||
|
totalElements: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 类型使用规范:
|
||||||
|
- 优先使用 `type` 而不是 `interface`
|
||||||
|
- 必须明确声明函数参数和返回值类型
|
||||||
|
- 避免使用 `any`,优先使用 `unknown`
|
||||||
|
- 必须启用 TypeScript 严格模式
|
||||||
|
- 所有实体响应类型必须继承 BaseResponse
|
||||||
|
- 所有查询类型必须继承 BaseQuery
|
||||||
|
- 时间字段统一使用 ISO 字符串格式
|
||||||
|
- 版本号用于乐观锁控制
|
||||||
|
|
||||||
|
## 4. HTTP 请求规范
|
||||||
|
|
||||||
|
1. 请求工具:
|
||||||
|
- 统一使用封装的 `request` 工具
|
||||||
|
- 必须使用 `async/await` 语法,禁止使用 `.then()`
|
||||||
|
- 必须使用 `try/catch` 进行错误处理
|
||||||
|
- Promise 对象的类型必须显式声明
|
||||||
|
|
||||||
|
2. API 规范:
|
||||||
|
- 所有后端接口必须使用统一前缀 `/api/v1/`
|
||||||
|
- API 版本控制统一使用 v1
|
||||||
|
- 按照 RESTful 规范组织 API
|
||||||
|
- 统一的响应格式:
|
||||||
|
```typescript
|
||||||
|
interface Response<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 错误处理:
|
||||||
|
- 统一的错误码定义:
|
||||||
|
- 401:未授权,重新登录
|
||||||
|
- 403:拒绝访问
|
||||||
|
- 404:资源未找到
|
||||||
|
- 500:服务器错误
|
||||||
|
- 必须有完整的错误提示信息
|
||||||
|
- 必须有合理的错误降级策略
|
||||||
|
- 必须记录必要的错误日志
|
||||||
|
|
||||||
|
4. 请求配置:
|
||||||
|
- 超时时间:30秒
|
||||||
|
- 重试次数:3次
|
||||||
|
- 重试延迟:1秒
|
||||||
|
- Token 使用 Bearer 方案
|
||||||
|
- Token 存储在 localStorage
|
||||||
|
- 文件上传下载使用专门的方法
|
||||||
|
|
||||||
|
## 5. React 开发规范
|
||||||
|
|
||||||
|
1. 组件开发:
|
||||||
|
- 使用函数组件和箭头函数定义
|
||||||
|
- Props 类型必须明确定义
|
||||||
|
- 必须提供默认值
|
||||||
|
- 必须有完整的 JSDoc 注释
|
||||||
|
- 必须使用 memo 优化,除非确定不需要
|
||||||
|
- 复杂组件需要拆分为多个子组件
|
||||||
|
- 必须处理组件的所有状态和异常情况
|
||||||
|
|
||||||
|
2. Hooks 使用:
|
||||||
|
- 自定义 hooks 必须以 `use` 开头
|
||||||
|
- 必须使用 TypeScript 泛型
|
||||||
|
- 必须处理加载状态和错误情况
|
||||||
|
- 必须使用 useCallback 和 useMemo 优化性能
|
||||||
|
- 避免嵌套 `await`,使用 `Promise.all` 处理并行请求
|
||||||
|
|
||||||
|
3. 状态管理:
|
||||||
|
- 使用 Redux Toolkit
|
||||||
|
- Slice 文件命名必须以 `Slice` 结尾
|
||||||
|
- Action 类型必须使用 `PayloadAction`
|
||||||
|
- 必须定义清晰的状态接口
|
||||||
|
- 本地存储同步必须在 reducer 中处理
|
||||||
|
- 状态持久化:
|
||||||
|
- Token 统一存储在 localStorage
|
||||||
|
- 用户信息统一存储在 localStorage
|
||||||
|
- 清理时必须调用统一的 logout action
|
||||||
|
|
||||||
|
## 6. 表格组件规范
|
||||||
|
|
||||||
|
1. 状态管理:
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
- 避免内联样式,除非是动态计算的值
|
||||||
|
- 必须响应式适配
|
||||||
|
- 必须考虑暗色主题
|
||||||
|
|
||||||
|
2. 布局规范:
|
||||||
|
- 统一使用 BasicLayout
|
||||||
|
- 必须实现面包屑导航
|
||||||
|
- 必须处理响应式布局
|
||||||
|
- 必须实现统一的错误页面
|
||||||
|
- 导航菜单:
|
||||||
|
- 菜单数据必须从后端获取
|
||||||
|
- 必须支持多级菜单
|
||||||
|
- 必须支持菜单权限控制
|
||||||
|
- 必须支持菜单图标
|
||||||
|
|
||||||
|
## 8. 项目配置规范
|
||||||
|
|
||||||
|
1. 依赖管理:
|
||||||
|
- 核心依赖版本:
|
||||||
|
- React: ^18.2.0
|
||||||
|
- TypeScript: ^5.3.3
|
||||||
|
- Ant Design: ^5.22.2
|
||||||
|
- Redux Toolkit: ^2.0.1
|
||||||
|
- 开发依赖必须锁定版本
|
||||||
|
- 生产依赖必须指定大版本
|
||||||
|
|
||||||
|
2. 开发规范:
|
||||||
|
- 必须启用 ESLint
|
||||||
|
- 必须使用 Prettier 格式化
|
||||||
|
- 必须配置 Git Hooks
|
||||||
|
- commit message 遵循 Angular 规范
|
||||||
|
- 提交前必须进行 lint 和 type check
|
||||||
|
|
||||||
|
3. 环境配置:
|
||||||
|
- 使用 .env 文件管理环境变量
|
||||||
|
- 必须区分开发和生产环境
|
||||||
|
- 必须配置代理规则
|
||||||
|
- 必须处理跨域问题
|
||||||
|
|
||||||
|
4. 构建优化:
|
||||||
|
- 使用 Vite 作为构建工具
|
||||||
|
- 必须配置路径别名
|
||||||
|
- 必须优化构建性能
|
||||||
|
- 必须分离开发和生产配置
|
||||||
|
- 路由级别的代码分割
|
||||||
|
- 大型第三方库异步加载
|
||||||
|
|
||||||
|
5. 安全规范:
|
||||||
|
- 环境变量必须通过 .env 文件管理
|
||||||
|
- 禁止在代码中硬编码敏感信息
|
||||||
|
- 使用 ESLint security 插件
|
||||||
|
- 小心使用 dangerouslySetInnerHTML
|
||||||
|
|
||||||
|
6. 测试规范:
|
||||||
|
- 单元测试:
|
||||||
|
- 使用 Jest 和 React Testing Library
|
||||||
|
- 重要组件必须包含测试用例
|
||||||
|
- 测试文件以 .test.tsx 结尾
|
||||||
|
- E2E 测试:
|
||||||
|
- 使用 Cypress 进行端到端测试
|
||||||
|
- 覆盖关键业务流程
|
||||||
|
|
||||||
2
frontend/package-lock.json
generated
2
frontend/package-lock.json
generated
@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"antd": "^5.12.2",
|
"antd": "^5.22.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"antd": "^5.12.2",
|
"antd": "^5.22.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
311
frontend/scripts/create-module.js
Normal file
311
frontend/scripts/create-module.js
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 模块名称(首字母大写)
|
||||||
|
const moduleName = process.argv[2];
|
||||||
|
if (!moduleName) {
|
||||||
|
console.error('请提供模块名称');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块路径
|
||||||
|
const modulePath = path.join(__dirname, '../src/pages/System', moduleName);
|
||||||
|
|
||||||
|
// 创建目录结构
|
||||||
|
const directories = [
|
||||||
|
'',
|
||||||
|
'components',
|
||||||
|
'hooks',
|
||||||
|
'styles'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
directories.forEach(dir => {
|
||||||
|
const dirPath = path.join(modulePath, dir);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建基础文件
|
||||||
|
const files = {
|
||||||
|
// 类型定义
|
||||||
|
'types.ts': `import type { BaseResponse } from '@/types/base/response';
|
||||||
|
import type { BaseQuery } from '@/types/base/query';
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
export interface ${moduleName}Query extends BaseQuery {
|
||||||
|
// TODO: 添加查询参数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求参数
|
||||||
|
export interface ${moduleName}Request {
|
||||||
|
// TODO: 添加请求参数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应类型
|
||||||
|
export interface ${moduleName}Response extends BaseResponse {
|
||||||
|
// TODO: 添加响应字段
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
// API 服务
|
||||||
|
'service.ts': `import request from '@/utils/request';
|
||||||
|
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 get${moduleName}s = async (params?: ${moduleName}Query) =>
|
||||||
|
request.get<Page<${moduleName}Response>>(\`\${BASE_URL}/page\`, { params });
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
export const create${moduleName} = async (data: ${moduleName}Request) =>
|
||||||
|
request.post<${moduleName}Response>(BASE_URL, data);
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
export const update${moduleName} = async (id: number, data: ${moduleName}Request) =>
|
||||||
|
request.put<${moduleName}Response>(\`\${BASE_URL}/\${id}\`, data);
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
export const delete${moduleName} = async (id: number) =>
|
||||||
|
request.delete(\`\${BASE_URL}/\${id}\`);
|
||||||
|
`,
|
||||||
|
|
||||||
|
// 主页面组件
|
||||||
|
'index.tsx': `import React from 'react';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import type { ${moduleName}Response } from './types';
|
||||||
|
import { use${moduleName}Data } from './hooks/use${moduleName}Data';
|
||||||
|
import ${moduleName}Table from './components/${moduleName}Table';
|
||||||
|
import ${moduleName}Form from './components/${moduleName}Form';
|
||||||
|
import styles from './styles/index.module.css';
|
||||||
|
|
||||||
|
const ${moduleName}: React.FC = () => {
|
||||||
|
const {
|
||||||
|
list,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
selectedRows,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
handleDelete,
|
||||||
|
handleSelectionChange
|
||||||
|
} = use${moduleName}Data();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="${moduleName} 管理" className={styles.container}>
|
||||||
|
<${moduleName}Form onSubmit={handleCreate} />
|
||||||
|
<${moduleName}Table
|
||||||
|
loading={loading}
|
||||||
|
dataSource={list}
|
||||||
|
pagination={pagination}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ${moduleName};
|
||||||
|
`,
|
||||||
|
|
||||||
|
// 表格组件
|
||||||
|
'components/${moduleName}Table.tsx': `import React from 'react';
|
||||||
|
import { Table, Space, Button, Popconfirm } from 'antd';
|
||||||
|
import type { ${moduleName}Response } from '../types';
|
||||||
|
import styles from '../styles/table.module.css';
|
||||||
|
|
||||||
|
interface ${moduleName}TableProps {
|
||||||
|
loading: boolean;
|
||||||
|
dataSource: ${moduleName}Response[];
|
||||||
|
pagination: {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
selectedRows: ${moduleName}Response[];
|
||||||
|
onDelete: (id: number) => Promise<void>;
|
||||||
|
onUpdate: (id: number, data: Partial<${moduleName}Response>) => Promise<void>;
|
||||||
|
onSelectionChange: (selectedRows: ${moduleName}Response[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ${moduleName}Table: React.FC<${moduleName}TableProps> = ({
|
||||||
|
loading,
|
||||||
|
dataSource,
|
||||||
|
pagination,
|
||||||
|
selectedRows,
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
onSelectionChange
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
// TODO: 添加列定义
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record: ${moduleName}Response) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Button type="link" onClick={() => onUpdate(record.id, record)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除吗?"
|
||||||
|
onConfirm={() => onDelete(record.id)}
|
||||||
|
>
|
||||||
|
<Button type="link" danger>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
className={styles.table}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={columns}
|
||||||
|
pagination={pagination}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: selectedRows.map(row => row.id),
|
||||||
|
onChange: (_, rows) => onSelectionChange(rows),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ${moduleName}Table;
|
||||||
|
`,
|
||||||
|
|
||||||
|
// 表单组件
|
||||||
|
'components/${moduleName}Form.tsx': `import React from 'react';
|
||||||
|
import { Form, Input, Button } from 'antd';
|
||||||
|
import type { ${moduleName}Request } from '../types';
|
||||||
|
import styles from '../styles/form.module.css';
|
||||||
|
|
||||||
|
interface ${moduleName}FormProps {
|
||||||
|
initialValues?: Partial<${moduleName}Request>;
|
||||||
|
onSubmit: (values: ${moduleName}Request) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ${moduleName}Form: React.FC<${moduleName}FormProps> = ({
|
||||||
|
initialValues,
|
||||||
|
onSubmit
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
await onSubmit(values);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="inline"
|
||||||
|
initialValues={initialValues}
|
||||||
|
className={styles.form}
|
||||||
|
>
|
||||||
|
{/* TODO: 添加表单项 */}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" onClick={handleSubmit}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建文件
|
||||||
|
Object.entries(files).forEach(([filename, content]) => {
|
||||||
|
const filePath = path.join(modulePath, filename);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`模块 ${moduleName} 创建成功!`);
|
||||||
|
console.log(`
|
||||||
|
请确保在路由配置文件 src/router/index.tsx 中添加以下路由:
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'system/${moduleName.toLowerCase()}',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
|
<${moduleName} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
并添加懒加载导入:
|
||||||
|
const ${moduleName} = lazy(() => import('../pages/System/${moduleName}'));
|
||||||
|
`);
|
||||||
@ -3,15 +3,24 @@ 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, getTenantList} from './service';
|
||||||
import {setToken, setUserInfo} from '../../store/userSlice';
|
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
|
||||||
|
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
||||||
|
import type { MenuResponse } from '@/pages/System/Menu/types';
|
||||||
|
import type { TenantResponse } from './types';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
interface LoginForm {
|
||||||
|
tenantId: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm<LoginForm>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tenants, setTenants] = useState([]);
|
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTenants = async () => {
|
const fetchTenants = async () => {
|
||||||
@ -26,16 +35,44 @@ const Login: React.FC = () => {
|
|||||||
fetchTenants();
|
fetchTenants();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFinish = async (values) => {
|
// 清理空children的函数,同时过滤掉按钮类型菜单
|
||||||
|
const cleanEmptyChildren = (menus: MenuResponse[]): MenuResponse[] => {
|
||||||
|
return menus
|
||||||
|
.filter(menu => menu.type !== 2) // 过滤掉按钮类型的菜单
|
||||||
|
.map(menu => {
|
||||||
|
const cleanedMenu = { ...menu };
|
||||||
|
|
||||||
|
if (cleanedMenu.children && cleanedMenu.children.length > 0) {
|
||||||
|
cleanedMenu.children = cleanEmptyChildren(cleanedMenu.children);
|
||||||
|
if (cleanedMenu.children.length === 0) {
|
||||||
|
delete cleanedMenu.children;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete cleanedMenu.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedMenu;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = async (values: LoginForm) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
// 1. 登录获取 token 和用户信息
|
||||||
const result = await login(values);
|
const result = await login(values);
|
||||||
dispatch(setToken(result.token));
|
dispatch(setToken(result.token));
|
||||||
dispatch(setUserInfo(result));
|
dispatch(setUserInfo(result));
|
||||||
|
|
||||||
|
// 2. 获取菜单数据
|
||||||
|
const menuData = await getCurrentUserMenus();
|
||||||
|
dispatch(setMenus(cleanEmptyChildren(menuData)));
|
||||||
|
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
|
// 3. 所有数据都准备好后再跳转
|
||||||
navigate('/dashboard', {replace: true});
|
navigate('/dashboard', {replace: true});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
|
message.error('登录失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -45,10 +82,9 @@ const Login: React.FC = () => {
|
|||||||
<div className={styles.loginContainer}>
|
<div className={styles.loginContainer}>
|
||||||
<div className={styles.loginBox}>
|
<div className={styles.loginBox}>
|
||||||
<div className={styles.logo}>
|
<div className={styles.logo}>
|
||||||
<img src="/logo.png" alt="QC-NAS" />
|
<h1>管理系统</h1>
|
||||||
<h1>QC-NAS</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form<LoginForm>
|
||||||
form={form}
|
form={form}
|
||||||
name="login"
|
name="login"
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
|||||||
@ -1,14 +1,35 @@
|
|||||||
export interface LoginRequest {
|
import type { BaseResponse } from '@/types/base/response';
|
||||||
username: string;
|
|
||||||
password: string;
|
// 租户响应类型
|
||||||
tenantId?: string;
|
export interface TenantResponse extends BaseResponse {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
address?: string;
|
||||||
|
contactName?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
// 登录请求参数
|
||||||
id: number;
|
export interface LoginRequest {
|
||||||
username: string;
|
tenantId: string;
|
||||||
nickname?: string;
|
username: string;
|
||||||
email?: string;
|
password: string;
|
||||||
phone?: string;
|
}
|
||||||
token: string;
|
|
||||||
|
// 登录响应类型
|
||||||
|
export interface LoginResponse extends BaseResponse {
|
||||||
|
token: string;
|
||||||
|
username: string;
|
||||||
|
nickname?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
roles: {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
}[];
|
||||||
|
permissions: string[];
|
||||||
}
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
.role-modal {
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
@ -1,17 +1,15 @@
|
|||||||
import type { BaseResponse } from '@/types/base/response';
|
import type { BaseResponse } from '@/types/base/response';
|
||||||
import type { Page } from '@/types/base/page';
|
import type { BaseQuery } from '@/types/base/query';
|
||||||
|
|
||||||
export interface UserQuery {
|
// 用户查询参数
|
||||||
pageNum?: number;
|
export interface UserQuery extends BaseQuery {
|
||||||
pageSize?: number;
|
|
||||||
sortField?: string;
|
|
||||||
sortOrder?: 'ascend' | 'descend';
|
|
||||||
username?: string;
|
username?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户请求参数
|
||||||
export interface UserRequest {
|
export interface UserRequest {
|
||||||
username: string;
|
username: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
@ -33,6 +31,7 @@ export interface Role {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户响应类型
|
||||||
export interface UserResponse extends BaseResponse {
|
export interface UserResponse extends BaseResponse {
|
||||||
username: string;
|
username: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const LoadingComponent = () => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
|
const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
const token = useSelector((state: RootState) => state.user.token);
|
const token = useSelector((state: RootState) => state.user.token);
|
||||||
|
|
||||||
@ -61,6 +62,18 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: 'system',
|
path: 'system',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
element: <Navigate to="/system/dashboard" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
|
<Dashboard />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'tenant',
|
path: 'tenant',
|
||||||
element: (
|
element: (
|
||||||
@ -118,6 +131,10 @@ const router = createBrowserRouter([
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <Navigate to="/dashboard" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ interface UserState {
|
|||||||
|
|
||||||
const initialState: UserState = {
|
const initialState: UserState = {
|
||||||
token: localStorage.getItem('token'),
|
token: localStorage.getItem('token'),
|
||||||
userInfo: null,
|
userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null,
|
||||||
menus: []
|
menus: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user