增加cursorrules

This commit is contained in:
dengqichen 2024-12-02 14:10:09 +08:00
parent 7121e83fbb
commit 2dcb8bd115
10 changed files with 729 additions and 27 deletions

284
frontend/.cursorrules Normal file
View 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 进行端到端测试
- 覆盖关键业务流程

View File

@ -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",

View File

@ -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",

View 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}'));
`);

View File

@ -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<LoginForm>();
const [loading, setLoading] = useState(false);
const [tenants, setTenants] = useState([]);
const [tenants, setTenants] = useState<TenantResponse[]>([]);
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 = () => {
<div className={styles.loginContainer}>
<div className={styles.loginBox}>
<div className={styles.logo}>
<img src="/logo.png" alt="QC-NAS" />
<h1>QC-NAS</h1>
<h1></h1>
</div>
<Form
<Form<LoginForm>
form={form}
name="login"
onFinish={onFinish}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;

View File

@ -23,6 +23,7 @@ const LoadingComponent = () => (
</div>
);
// 路由守卫
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: <Navigate to="/system/dashboard" />
},
{
path: 'dashboard',
element: (
<Suspense fallback={<LoadingComponent />}>
<Dashboard />
</Suspense>
)
},
{
path: 'tenant',
element: (
@ -118,6 +131,10 @@ const router = createBrowserRouter([
)
}
]
},
{
path: '*',
element: <Navigate to="/dashboard" />
}
]
}

View File

@ -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: []
};