diff --git a/frontend/.cursorrules b/frontend/.cursorrules new file mode 100644 index 00000000..9cee8632 --- /dev/null +++ b/frontend/.cursorrules @@ -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 { + 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 { + 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 { + 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 进行端到端测试 + - 覆盖关键业务流程 + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f061e3e9..9c2554ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@ant-design/icons": "^5.2.6", "@reduxjs/toolkit": "^2.0.1", - "antd": "^5.12.2", + "antd": "^5.22.2", "axios": "^1.6.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/package.json b/frontend/package.json index b0e3dda4..c0227221 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "dependencies": { "@ant-design/icons": "^5.2.6", "@reduxjs/toolkit": "^2.0.1", - "antd": "^5.12.2", + "antd": "^5.22.2", "axios": "^1.6.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/scripts/create-module.js b/frontend/scripts/create-module.js new file mode 100644 index 00000000..d47b0d96 --- /dev/null +++ b/frontend/scripts/create-module.js @@ -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>(\`\${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 ( + + <${moduleName}Form onSubmit={handleCreate} /> + <${moduleName}Table + loading={loading} + dataSource={list} + pagination={pagination} + selectedRows={selectedRows} + onDelete={handleDelete} + onUpdate={handleUpdate} + onSelectionChange={handleSelectionChange} + /> + + ); +}; + +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; + onUpdate: (id: number, data: Partial<${moduleName}Response>) => Promise; + 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) => ( + + + onDelete(record.id)} + > + + + + ), + }, + ]; + + return ( + 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; +} + +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 ( +
+ {/* TODO: 添加表单项 */} + + + + + ); +}; + +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: ( + }> + <${moduleName} /> + + ) +} + +并添加懒加载导入: +const ${moduleName} = lazy(() => import('../pages/System/${moduleName}')); +`); \ No newline at end of file diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx index 14ceed47..d58db9d9 100644 --- a/frontend/src/pages/Login/index.tsx +++ b/frontend/src/pages/Login/index.tsx @@ -3,15 +3,24 @@ import {Form, Input, Button, message, Select} from 'antd'; import {useNavigate} from 'react-router-dom'; import {useDispatch} from 'react-redux'; 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'; +interface LoginForm { + tenantId: string; + username: string; + password: string; +} + const Login: React.FC = () => { const navigate = useNavigate(); const dispatch = useDispatch(); - const [form] = Form.useForm(); + const [form] = Form.useForm(); const [loading, setLoading] = useState(false); - const [tenants, setTenants] = useState([]); + const [tenants, setTenants] = useState([]); useEffect(() => { const fetchTenants = async () => { @@ -26,16 +35,44 @@ const Login: React.FC = () => { 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 { setLoading(true); + // 1. 登录获取 token 和用户信息 const result = await login(values); dispatch(setToken(result.token)); dispatch(setUserInfo(result)); + + // 2. 获取菜单数据 + const menuData = await getCurrentUserMenus(); + dispatch(setMenus(cleanEmptyChildren(menuData))); + message.success('登录成功'); + // 3. 所有数据都准备好后再跳转 navigate('/dashboard', {replace: true}); } catch (error) { console.error('登录失败:', error); + message.error('登录失败,请重试'); } finally { setLoading(false); } @@ -45,10 +82,9 @@ const Login: React.FC = () => {
- QC-NAS -

QC-NAS

+

管理系统

-
form={form} name="login" onFinish={onFinish} diff --git a/frontend/src/pages/Login/types.ts b/frontend/src/pages/Login/types.ts index 74d2ccb0..248d0a0e 100644 --- a/frontend/src/pages/Login/types.ts +++ b/frontend/src/pages/Login/types.ts @@ -1,14 +1,35 @@ -export interface LoginRequest { - username: string; - password: string; - tenantId?: string; +import type { BaseResponse } from '@/types/base/response'; + +// 租户响应类型 +export interface TenantResponse extends BaseResponse { + code: string; + name: string; + address?: string; + contactName?: string; + contactPhone?: string; + email?: string; } -export interface LoginResponse { - id: number; - username: string; - nickname?: string; - email?: string; - phone?: string; - token: string; +// 登录请求参数 +export interface LoginRequest { + tenantId: string; + username: string; + password: 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[]; } \ No newline at end of file diff --git a/frontend/src/pages/System/User/components/RoleModal.module.css b/frontend/src/pages/System/User/components/RoleModal.module.css new file mode 100644 index 00000000..37e86487 --- /dev/null +++ b/frontend/src/pages/System/User/components/RoleModal.module.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/pages/System/User/types.ts b/frontend/src/pages/System/User/types.ts index ec12ff97..7473b6b9 100644 --- a/frontend/src/pages/System/User/types.ts +++ b/frontend/src/pages/System/User/types.ts @@ -1,17 +1,15 @@ 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; - pageSize?: number; - sortField?: string; - sortOrder?: 'ascend' | 'descend'; +// 用户查询参数 +export interface UserQuery extends BaseQuery { username?: string; nickname?: string; email?: string; enabled?: boolean; } +// 用户请求参数 export interface UserRequest { username: string; nickname?: string; @@ -33,6 +31,7 @@ export interface Role { enabled: boolean; } +// 用户响应类型 export interface UserResponse extends BaseResponse { username: string; nickname?: string; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 8afb3ab9..8f2fb229 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -23,6 +23,7 @@ const LoadingComponent = () => (
); +// 路由守卫 const PrivateRoute = ({ children }: { children: React.ReactNode }) => { const token = useSelector((state: RootState) => state.user.token); @@ -61,6 +62,18 @@ const router = createBrowserRouter([ { path: 'system', children: [ + { + path: '', + element: + }, + { + path: 'dashboard', + element: ( + }> + + + ) + }, { path: 'tenant', element: ( @@ -118,6 +131,10 @@ const router = createBrowserRouter([ ) } ] + }, + { + path: '*', + element: } ] } diff --git a/frontend/src/store/userSlice.ts b/frontend/src/store/userSlice.ts index 4f61aaac..845127df 100644 --- a/frontend/src/store/userSlice.ts +++ b/frontend/src/store/userSlice.ts @@ -17,7 +17,7 @@ interface UserState { const initialState: UserState = { token: localStorage.getItem('token'), - userInfo: null, + userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null, menus: [] };