菜单可正确被加载

This commit is contained in:
dengqichen 2024-12-03 18:17:47 +08:00
parent 893ab0e29f
commit c39db5a498
17 changed files with 1052 additions and 572 deletions

View File

@ -1,5 +1,6 @@
你是一名高级前端开发人员是ReactJS NextJS, JavaScript, TypeScript HTML CSS和现代UI/UX框架例如TailwindCSS, Shadcn, Radix的专家。你深思熟虑给出细致入微的答案并且善于推理。你细心地提供准确、真实、深思熟虑的答案是推理的天才。 你是一名高级前端开发人员是ReactJS NextJS, JavaScript, TypeScript HTML CSS和现代UI/UX框架例如TailwindCSS, Shadcn, Radix的专家。你深思熟虑给出细致入微的答案并且善于推理。你细心地提供准确、真实、深思熟虑的答案是推理的天才。
# 严格遵循的要求 # 严格遵循的要求
- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。 - 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
- 确认,然后写代码! - 确认,然后写代码!
- 始终编写正确、最佳实践、DRY原则不要重复自己、无错误、功能齐全且可工作的代码还应与下面代码实施指南中列出的规则保持一致。 - 始终编写正确、最佳实践、DRY原则不要重复自己、无错误、功能齐全且可工作的代码还应与下面代码实施指南中列出的规则保持一致。
@ -31,8 +32,8 @@ src/
├── components/ # 公共组件 ├── components/ # 公共组件
├── pages/ # 页面组件 ├── pages/ # 页面组件
│ └── System/ # 系统管理模块 │ └── System/ # 系统管理模块
├── layouts/ # 布局组件 ├── layouts/ # 布局组件
├── router/ # 路由配置 ├── router/index.ts # 路由配置
├── store/ # 状态管理 ├── store/ # 状态管理
├── services/ # API 服务 ├── services/ # API 服务
├── utils/ # 工具函数 ├── utils/ # 工具函数
@ -65,63 +66,38 @@ ModuleName/ # 模块结构
- 事件处理:`handle` 前缀 - 事件处理:`handle` 前缀
- 异步函数:动词开头(`fetchData` - 异步函数:动词开头(`fetchData`
3. 已定义了基础类型所以直接继承即可把Response query request 的定义都模块的types.ts文件中 3. 已定义了基础类型src\types\base下直接继承即可把Response query request 的定义都模块的types.ts文件中
```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 Response<T = any> {
code: number;
message: string;
data: T;
success: boolean;
}
```
## 3. 服务层规范 ## 3. 服务层规范
1. 方法定义: 1. 方法定义:
```typescript ```typescript
// 推荐写法 - 使用 http 工具 // 推荐写法 - 使用 request 工具
export const resetPassword = (id: number, password: string) => export const resetPassword = (id: number, password: string) =>
http.post<void>(`/api/v1/users/${id}/reset-password`, { password }); request.post<void>(`/api/v1/users/${id}/reset-password`, { password });
// 标准 CRUD 接口 // 标准 CRUD 接口
export const getList = (params?: Query) => export const getList = (params?: Query) =>
http.get<Response[]>('/api/v1/xxx', { params }); request.get<Response[]>('/api/v1/xxx', { params });
export const create = (data: Request) => export const create = (data: Request) =>
http.post<Response>('/api/v1/xxx', data); request.post<Response>('/api/v1/xxx', data);
export const update = (id: number, data: Request) => export const update = (id: number, data: Request) =>
http.put<Response>(`/api/v1/xxx/${id}`, data); request.put<Response>(`/api/v1/xxx/${id}`, data);
export const remove = (id: number) => export const remove = (id: number) =>
http.delete(`/api/v1/xxx/${id}`); request.delete(`/api/v1/xxx/${id}`);
export const batchRemove = (ids: number[]) => export const batchRemove = (ids: number[]) =>
http.post('/api/v1/xxx/batch-delete', { ids }); request.post('/api/v1/xxx/batch-delete', { ids });
export const exportData = (params?: Query) => export const exportData = (params?: Query) =>
http.download('/api/v1/xxx/export', undefined, { params }); request.download('/api/v1/xxx/export', undefined, { params });
``` ```
2. 规范要点: 2. 规范要点:
- 统一使用 `http` 工具(不要使用 request - 统一使用 src\utils\request.ts
- 使用泛型指定响应数据类型 - 使用泛型指定响应数据类型
- 错误处理在拦截器中统一处理 - 错误处理在拦截器中统一处理
- 使用模板字符串拼接路径 - 使用模板字符串拼接路径
@ -129,114 +105,14 @@ ModuleName/ # 模块结构
- 资源使用复数形式users, roles - 资源使用复数形式users, roles
- 特殊操作使用动词export, import - 特殊操作使用动词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);
}
};
```
4. 类型处理规则: 4. 类型处理规则:
- http 工具的泛型参数 T 表示业务数据类型 - request 工具的泛型参数 T 表示业务数据类型
- 响应拦截器负责从 Response<T> 中提取 data - 响应拦截器负责从 Response<T> 中提取 data
- 服务方法直接使用业务数据类型作为泛型参数 - 服务方法直接使用业务数据类型作为泛型参数
- 组件中可以直接使用返回的业务数据 - 组件中可以直接使用返回的业务数据
- TypeScript 类型系统能正确推断类型 - TypeScript 类型系统能正确推断类型
5. 最佳实践:
```typescript
// 服务层 - 使用业务数据类型
export const someService = () =>
http.get<SomeType>('/api/v1/xxx'); // 返回 Promise<SomeType>
// 组件层 - 直接使用业务数据
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. 组件开发:
@ -264,26 +140,4 @@ ModuleName/ # 模块结构
- 类名使用 kebab-case - 类名使用 kebab-case
- 避免内联样式 - 避免内联样式
- 响应式适配 - 响应式适配
- 支持暗色主题 - 支持暗色主题
2. 布局规范:
- 使用 BasicLayout
- 实现面包屑导航
- 菜单从后端获取
- 支持权限控制
## 7. 项目配置规范
1. 依赖版本:
- React: ^18.2.0
- TypeScript: ^5.3.3
- Ant Design: ^5.22.2
- Redux Toolkit: ^2.0.1
2. 开发规范:
- 启用 ESLint 和 Prettier
- 配置 Git Hooks
- 使用 .env 管理环境变量
- 使用 Vite 构建
- 配置路由级代码分割

View File

@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@antv/x6": "^2.18.1",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"antd": "^5.22.2", "antd": "^5.22.2",
"axios": "^1.6.2", "axios": "^1.6.2",
@ -134,6 +135,33 @@
"react": ">=16.9.0" "react": ">=16.9.0"
} }
}, },
"node_modules/@antv/x6": {
"version": "2.18.1",
"resolved": "https://registry.npmmirror.com/@antv/x6/-/x6-2.18.1.tgz",
"integrity": "sha512-FkWdbLOpN9J7dfJ+kiBxzowSx2N6syBily13NMVdMs+wqC6Eo5sLXWCZjQHateTFWgFw7ZGi2y9o3Pmdov1sXw==",
"license": "MIT",
"dependencies": {
"@antv/x6-common": "^2.0.16",
"@antv/x6-geometry": "^2.0.5",
"utility-types": "^3.10.0"
}
},
"node_modules/@antv/x6-common": {
"version": "2.0.17",
"resolved": "https://registry.npmmirror.com/@antv/x6-common/-/x6-common-2.0.17.tgz",
"integrity": "sha512-37g7vmRkNdYzZPdwjaMSZEGv/MMH0S4r70/Jwoab1mioycmuIBN73iyziX8m56BvJSDucZ3J/6DU07otWqzS6A==",
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.15",
"utility-types": "^3.10.0"
}
},
"node_modules/@antv/x6-geometry": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/@antv/x6-geometry/-/x6-geometry-2.0.5.tgz",
"integrity": "sha512-MId6riEQkxphBpVeTcL4ZNXL4lScyvDEPLyIafvWMcWNTGK0jgkK7N20XSzqt8ltJb0mGUso5s56mrk8ysHu2A==",
"license": "MIT"
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz",
@ -2904,6 +2932,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -4244,6 +4278,15 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/utility-types": {
"version": "3.11.0",
"resolved": "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz",
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.11", "version": "5.4.11",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.11.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.11.tgz",

View File

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@antv/x6": "^2.18.1",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"antd": "^5.22.2", "antd": "^5.22.2",
"axios": "^1.6.2", "axios": "^1.6.2",

View File

@ -1,316 +0,0 @@
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',
'types'
];
// 创建目录
directories.forEach(dir => {
const dirPath = path.join(modulePath, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
});
// 创建基础文件
const files = {
// 类型定义
'types/index.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';
// 获取列表
export const getList = (params?: ${moduleName}Query) =>
request.get<${moduleName}Response[]>('/api/v1/${moduleName.toLowerCase()}s', { params });
// 创建
export const create = (data: ${moduleName}Request) =>
request.post<${moduleName}Response>('/api/v1/${moduleName.toLowerCase()}s', data);
// 更新
export const update = (id: number, data: ${moduleName}Request) =>
request.put<${moduleName}Response>(\`/api/v1/${moduleName.toLowerCase()}s/\${id}\`, data);
// 删除
export const remove = (id: number) =>
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, { useState } from 'react';
import { Card, message } from 'antd';
import type { ${moduleName}Response } from './types';
import * as service from './service';
import ${moduleName}Table from './components/${moduleName}Table';
import ${moduleName}Form from './components/${moduleName}Form';
const ${moduleName}: React.FC = () => {
const [loading, setLoading] = useState(false);
const [list, setList] = useState<${moduleName}Response[]>([]);
const [selectedRows, setSelectedRows] = useState<${moduleName}Response[]>([]);
const loadData = async () => {
setLoading(true);
try {
const data = await service.getList();
setList(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 (
<Card title="${moduleName} 管理">
<${moduleName}Form onSubmit={handleCreate} />
<${moduleName}Table
loading={loading}
dataSource={list}
selectedRows={selectedRows}
onDelete={handleDelete}
onBatchDelete={handleBatchDelete}
onUpdate={handleUpdate}
onSelectionChange={setSelectedRows}
/>
</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';
interface ${moduleName}TableProps {
loading: boolean;
dataSource: ${moduleName}Response[];
selectedRows: ${moduleName}Response[];
onDelete: (id: number) => Promise<void>;
onBatchDelete: () => Promise<void>;
onUpdate: (id: number, data: ${moduleName}Response) => Promise<void>;
onSelectionChange: (rows: ${moduleName}Response[]) => void;
}
const ${moduleName}Table: React.FC<${moduleName}TableProps> = ({
loading,
dataSource,
selectedRows,
onDelete,
onBatchDelete,
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 (
<>
{selectedRows.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Popconfirm
title="确定要删除选中的项目吗?"
onConfirm={onBatchDelete}
>
<Button type="primary" danger>
批量删除
</Button>
</Popconfirm>
</div>
)}
<Table
rowKey="id"
loading={loading}
dataSource={dataSource}
columns={columns}
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';
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 () => {
try {
const values = await form.validateFields();
await onSubmit(values);
form.resetFields();
} catch (error) {
console.error('表单提交失败:', error);
}
};
return (
<Form
form={form}
layout="inline"
initialValues={initialValues}
style={{ marginBottom: 16 }}
>
{/* TODO: 添加表单项 */}
<Form.Item>
<Button type="primary" onClick={handleSubmit}>
提交
</Button>
</Form.Item>
</Form>
);
};
export default ${moduleName}Form;
`
};
// 创建文件
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

@ -0,0 +1,272 @@
import fs from 'fs';
import path from 'path';
interface GenerateOptions {
name: string; // 模块名称,如 user, role 等
description: string; // 中文描述,如 "用户管理"
baseUrl: string; // API基础路径如 "/api/v1/users"
}
// 生成 types.ts
function generateTypes(options: GenerateOptions): string {
const { name } = options;
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
return `import { BaseResponse, BaseQuery } from '@/types/base';
export interface ${typeName}Response extends BaseResponse {
name: string;
// TODO: 添加其他字段
}
export interface ${typeName}Query extends BaseQuery {
name?: string;
// TODO: 添加其他查询字段
}
export interface ${typeName}Request {
name: string;
// TODO: 添加其他请求字段
}
`;
}
// 生成 service.ts
function generateService(options: GenerateOptions): string {
const { name, baseUrl } = options;
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
return `import { http } from '@/utils/http';
import type { ${typeName}Response, ${typeName}Query, ${typeName}Request } from './types';
// 获取列表
export const getList = (params?: ${typeName}Query) =>
http.get<${typeName}Response[]>('${baseUrl}', { params });
// 获取详情
export const getDetail = (id: number) =>
http.get<${typeName}Response>(\`${baseUrl}/\${id}\`);
// 创建
export const create = (data: ${typeName}Request) =>
http.post<${typeName}Response>('${baseUrl}', data);
// 更新
export const update = (id: number, data: ${typeName}Request) =>
http.put<${typeName}Response>(\`${baseUrl}/\${id}\`, data);
// 删除
export const remove = (id: number) =>
http.delete(\`${baseUrl}/\${id}\`);
// 批量删除
export const batchRemove = (ids: number[]) =>
http.post('${baseUrl}/batch-delete', { ids });
`;
}
// 生成页面组件
function generatePage(options: GenerateOptions): string {
const { name, description } = options;
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
return `import { useState } from 'react';
import { Card, Table, Button, Space, Modal, message, Form, Input } from 'antd';
import type { ${typeName}Response, ${typeName}Query, ${typeName}Request } from './types';
import * as service from './service';
import { useRequest } from 'ahooks';
const ${typeName}Page = () => {
const [query, setQuery] = useState<${typeName}Query>({});
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editingRecord, setEditingRecord] = useState<${typeName}Response | null>(null);
const [form] = Form.useForm();
// 获取列表数据
const { data, loading, refresh } = useRequest(() => service.getList(query), {
refreshDeps: [query]
});
// 删除操作
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?',
onOk: async () => {
try {
await service.remove(id);
message.success('删除成功');
refresh();
} catch (error) {
message.error('删除失败');
}
}
});
};
// 批量删除
const handleBatchDelete = () => {
if (!selectedRowKeys.length) {
message.warning('请选择要删除的记录');
return;
}
Modal.confirm({
title: '确认删除',
content: \`确定要删除这\${selectedRowKeys.length}条记录吗?\`,
onOk: async () => {
try {
await service.batchRemove(selectedRowKeys);
message.success('删除成功');
setSelectedRowKeys([]);
refresh();
} catch (error) {
message.error('删除失败');
}
}
});
};
// 打开编辑弹窗
const handleEdit = async (id: number) => {
try {
const detail = await service.getDetail(id);
setEditingRecord(detail);
form.setFieldsValue(detail);
setEditModalVisible(true);
} catch (error) {
message.error('获取详情失败');
}
};
// 打开新增弹窗
const handleAdd = () => {
setEditingRecord(null);
form.resetFields();
setEditModalVisible(true);
};
// 保存表单
const handleSave = async () => {
try {
const values = await form.validateFields();
if (editingRecord) {
await service.update(editingRecord.id, values);
message.success('更新成功');
} else {
await service.create(values);
message.success('创建成功');
}
setEditModalVisible(false);
refresh();
} catch (error) {
message.error('操作失败');
}
};
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
title: '操作',
key: 'action',
render: (_, record: ${typeName}Response) => (
<Space>
<Button type="link" onClick={() => handleEdit(record.id)}></Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}></Button>
</Space>
),
},
];
return (
<>
<Card title="${description}" extra={
<Space>
<Button type="primary" onClick={handleAdd}></Button>
<Button danger onClick={handleBatchDelete}></Button>
</Space>
}>
<Form layout="inline" style={{ marginBottom: 16 }}>
<Form.Item label="名称" name="name">
<Input placeholder="请输入名称"
onChange={e => setQuery(prev => ({ ...prev, name: e.target.value }))}
/>
</Form.Item>
</Form>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as number[]),
}}
/>
</Card>
<Modal
title={editingRecord ? '编辑' : '新增'}
open={editModalVisible}
onOk={handleSave}
onCancel={() => setEditModalVisible(false)}
>
<Form form={form} layout="vertical">
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="请输入名称" />
</Form.Item>
{/* TODO: 添加其他表单项 */}
</Form>
</Modal>
</>
);
};
export default ${typeName}Page;
`;
}
// 生成文件
function generateFiles(options: GenerateOptions) {
const { name } = options;
const basePath = path.join(process.cwd(), 'src/pages', name);
// 创建目录
if (!fs.existsSync(basePath)) {
fs.mkdirSync(basePath, { recursive: true });
}
// 生成文件
fs.writeFileSync(path.join(basePath, 'types.ts'), generateTypes(options));
fs.writeFileSync(path.join(basePath, 'service.ts'), generateService(options));
fs.writeFileSync(path.join(basePath, 'index.tsx'), generatePage(options));
console.log(`Successfully generated files for module "${name}" in ${basePath}`);
}
// 使用示例
const options: GenerateOptions = {
name: process.argv[2],
description: process.argv[3] || '管理',
baseUrl: process.argv[4] || `/api/v1/${process.argv[2]}`,
};
if (!options.name) {
console.error('Please provide a module name!');
process.exit(1);
}
generateFiles(options);

View File

@ -21,7 +21,6 @@ export const getCurrentUserMenus = async () => {
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateTime: new Date().toISOString(), updateTime: new Date().toISOString(),
version: 0, version: 0,
deleted: false,
name: "首页", name: "首页",
path: "/dashboard", path: "/dashboard",
component: "/Dashboard/index", component: "/Dashboard/index",
@ -35,6 +34,25 @@ export const getCurrentUserMenus = async () => {
updateBy: "system" updateBy: "system"
}; };
// 添加X6测试菜单
const x6Test: MenuResponse = {
id: -1,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 0,
name: "X6测试",
path: "/x6-test",
component: "/X6Test/index",
icon: "experiment",
type: 2,
parentId: 0,
sort: 1,
hidden: false,
enabled: true,
createBy: "system",
updateBy: "system"
};
// 处理组件路径格式 // 处理组件路径格式
const processMenu = (menu: MenuResponse): MenuResponse => { const processMenu = (menu: MenuResponse): MenuResponse => {
const processed = { ...menu }; const processed = { ...menu };
@ -53,7 +71,7 @@ export const getCurrentUserMenus = async () => {
}; };
const processedMenus = menus.map(processMenu); const processedMenus = menus.map(processMenu);
return [dashboard, ...processedMenus]; return [dashboard, x6Test, ...processedMenus];
}; };
// 创建菜单 // 创建菜单

View File

@ -0,0 +1,102 @@
import React, { useEffect, useRef } from 'react';
import { Graph } from '@antv/x6';
import { Card } from 'antd';
import { IX6GraphRef } from './types';
const X6TestPage: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<IX6GraphRef>({ graph: null });
useEffect(() => {
if (containerRef.current) {
// 初始化画布
const graph = new Graph({
container: containerRef.current,
width: 800,
height: 600,
grid: true,
background: {
color: '#F5F5F5',
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
},
});
// 保存graph实例
graphRef.current.graph = graph;
// 创建示例节点
const rect1 = graph.addNode({
x: 100,
y: 100,
width: 100,
height: 40,
label: '节点 1',
attrs: {
body: {
fill: '#fff',
stroke: '#1890ff',
strokeWidth: 1,
},
label: {
fill: '#000',
},
},
});
const rect2 = graph.addNode({
x: 300,
y: 100,
width: 100,
height: 40,
label: '节点 2',
attrs: {
body: {
fill: '#fff',
stroke: '#1890ff',
strokeWidth: 1,
},
label: {
fill: '#000',
},
},
});
// 创建连线
graph.addEdge({
source: rect1,
target: rect2,
attrs: {
line: {
stroke: '#1890ff',
strokeWidth: 1,
},
},
});
// 清理函数
return () => {
graph.dispose();
};
}
}, []);
return (
<Card title="X6 图形编辑器测试">
<div
ref={containerRef}
style={{
width: '100%',
height: '600px',
border: '1px solid #ddd'
}}
/>
</Card>
);
};
export default X6TestPage;

View File

@ -0,0 +1,19 @@
import { Graph } from '@antv/x6';
export interface IX6Node {
id: string;
label: string;
x: number;
y: number;
}
export interface IX6Edge {
id: string;
source: string;
target: string;
label?: string;
}
export interface IX6GraphRef {
graph: Graph | null;
}

View File

@ -0,0 +1,212 @@
import React, { useEffect, useRef } from 'react';
import { Graph } from '@antv/x6';
import { register } from '@antv/x6-react-shape';
import { Selection } from '@antv/x6-plugin-selection';
import { Keyboard } from '@antv/x6-plugin-keyboard';
import { Snapline } from '@antv/x6-plugin-snapline';
import { Transform } from '@antv/x6-plugin-transform';
import { History } from '@antv/x6-plugin-history';
import { Export } from '@antv/x6-plugin-export';
// 注册自定义节点
register({
shape: 'custom-node',
width: 100,
height: 40,
component: ({ node }) => {
const data = node.getData() || {};
return (
<div className="custom-node" style={{
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#fff'
}}>
<span>{data.label || 'Node'}</span>
</div>
);
},
});
interface FlowGraphProps {
onNodeSelected?: (nodeId: string) => void;
onEdgeSelected?: (edgeId: string) => void;
}
const FlowGraph: React.FC<FlowGraphProps> = ({ onNodeSelected, onEdgeSelected }) => {
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<Graph>();
useEffect(() => {
if (!containerRef.current) return;
const graph = new Graph({
container: containerRef.current,
width: 800,
height: 600,
grid: {
size: 10,
visible: true,
type: 'dot',
args: {
color: '#ccc',
thickness: 1,
},
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
connector: 'smooth',
connectionPoint: 'boundary',
createEdge() {
return this.createEdge({
shape: 'edge',
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 1,
targetMarker: {
name: 'classic',
size: 8,
},
},
},
router: {
name: 'manhattan',
},
});
},
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
padding: 4,
attrs: {
strokeWidth: 4,
stroke: '#52c41a',
},
},
},
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
},
interacting: {
magnetConnectable: true,
nodeMovable: true,
},
});
// 注册插件
graph.use(
new Selection({
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true,
})
);
graph.use(
new Keyboard({
enabled: true,
})
);
graph.use(new Snapline());
graph.use(new Transform());
graph.use(new History());
graph.use(new Export());
// 监听事件
graph.on('node:click', ({ node }) => {
onNodeSelected?.(node.id);
});
graph.on('edge:click', ({ edge }) => {
onEdgeSelected?.(edge.id);
});
// 快捷键
graph.bindKey(['meta+c', 'ctrl+c'], () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.copy(cells);
}
return false;
});
graph.bindKey(['meta+v', 'ctrl+v'], () => {
if (!graph.isClipboardEmpty()) {
const cells = graph.paste({ offset: 32 });
graph.cleanSelection();
graph.select(cells);
}
return false;
});
graph.bindKey(['meta+z', 'ctrl+z'], () => {
if (graph.canUndo()) {
graph.undo();
}
return false;
});
graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
if (graph.canRedo()) {
graph.redo();
}
return false;
});
graph.bindKey('delete', () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
}
});
graphRef.current = graph;
// 添加示例节点
graph.addNode({
shape: 'custom-node',
x: 100,
y: 100,
data: { label: '开始' },
});
graph.addNode({
shape: 'custom-node',
x: 300,
y: 100,
data: { label: '处理' },
});
graph.addNode({
shape: 'custom-node',
x: 500,
y: 100,
data: { label: '结束' },
});
return () => {
graph.dispose();
};
}, [onNodeSelected, onEdgeSelected]);
return (
<div
ref={containerRef}
style={{
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#fff'
}}
/>
);
};
export default FlowGraph;

View File

@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { Card, Space, Button, Drawer, Form, Input, Select } from 'antd';
import FlowGraph from './components/FlowGraph';
const FlowDesigner: React.FC = () => {
const [selectedNode, setSelectedNode] = useState<string | null>(null);
const [drawerVisible, setDrawerVisible] = useState(false);
const [form] = Form.useForm();
const handleNodeSelected = (nodeId: string) => {
setSelectedNode(nodeId);
setDrawerVisible(true);
};
const handleSave = async () => {
try {
const values = await form.validateFields();
console.log('保存节点属性:', values);
setDrawerVisible(false);
} catch (error) {
console.error('表单验证失败:', error);
}
};
return (
<div style={{ padding: '24px' }}>
<Card
title="流程设计器"
extra={
<Space>
<Button></Button>
<Button></Button>
<Button type="primary"></Button>
</Space>
}
>
<FlowGraph
onNodeSelected={handleNodeSelected}
onEdgeSelected={(edgeId) => console.log('选中连线:', edgeId)}
/>
</Card>
<Drawer
title="节点属性"
placement="right"
onClose={() => setDrawerVisible(false)}
open={drawerVisible}
width={400}
extra={
<Space>
<Button onClick={() => setDrawerVisible(false)}></Button>
<Button type="primary" onClick={handleSave}>
</Button>
</Space>
}
>
<Form form={form} layout="vertical">
<Form.Item
label="节点名称"
name="name"
rules={[{ required: true, message: '请输入节点名称' }]}
>
<Input placeholder="请输入节点名称" />
</Form.Item>
<Form.Item
label="节点类型"
name="type"
rules={[{ required: true, message: '请选择节点类型' }]}
>
<Select
options={[
{ label: '开始节点', value: 'start' },
{ label: '处理节点', value: 'process' },
{ label: '结束节点', value: 'end' },
]}
/>
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={4} placeholder="请输入节点描述" />
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default FlowDesigner;

View File

@ -0,0 +1,24 @@
import { http } from '@/utils/http';
import type { FlowResponse, FlowQuery, FlowRequest } from './types';
const baseUrl = '/api/v1/flows';
// 获取流程列表
export const getList = (params?: FlowQuery) =>
http.get<FlowResponse[]>(baseUrl, { params });
// 获取流程详情
export const getDetail = (id: number) =>
http.get<FlowResponse>(`${baseUrl}/${id}`);
// 创建流程
export const create = (data: FlowRequest) =>
http.post<FlowResponse>(baseUrl, data);
// 更新流程
export const update = (id: number, data: FlowRequest) =>
http.put<FlowResponse>(`${baseUrl}/${id}`, data);
// 删除流程
export const remove = (id: number) =>
http.delete(`${baseUrl}/${id}`);

View File

@ -0,0 +1,41 @@
import { BaseResponse, BaseQuery } from '@/types/base';
export interface Node {
id: string;
shape: string;
x: number;
y: number;
width: number;
height: number;
label: string;
data?: Record<string, any>;
}
export interface Edge {
id: string;
source: string;
target: string;
label?: string;
data?: Record<string, any>;
}
export interface FlowData {
nodes: Node[];
edges: Edge[];
}
export interface FlowResponse extends BaseResponse {
name: string;
description?: string;
flowData: FlowData;
}
export interface FlowQuery extends BaseQuery {
name?: string;
}
export interface FlowRequest {
name: string;
description?: string;
flowData: FlowData;
}

View File

@ -1,28 +1,27 @@
import { createBrowserRouter, Navigate, RouteObject } from 'react-router-dom'; import {createBrowserRouter, Navigate} from 'react-router-dom';
import { lazy, Suspense } from 'react'; import {lazy, Suspense} from 'react';
import { Spin } from 'antd'; import {Spin} from 'antd';
import Login from '../pages/Login'; import Login from '../pages/Login';
import BasicLayout from '../layouts/BasicLayout'; import BasicLayout from '../layouts/BasicLayout';
import { useSelector } from 'react-redux'; import {useSelector} from 'react-redux';
import { RootState } from '../store'; import {RootState} from '../store';
import { MenuResponse } from '@/pages/System/Menu/types';
// 加载中组件 // 加载中组件
const LoadingComponent = () => ( const LoadingComponent = () => (
<div style={{ padding: 24, textAlign: 'center' }}> <div style={{padding: 24, textAlign: 'center'}}>
<Spin size="large" /> <Spin size="large"/>
</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);
if (!token) { if (!token) {
return <Navigate to="/login" />; return <Navigate to="/login"/>;
} }
return <>{children}</>; return <>{children}</>;
}; };
// 懒加载组件 // 懒加载组件
@ -32,84 +31,93 @@ const Role = lazy(() => import('../pages/System/Role'));
const Menu = lazy(() => import('../pages/System/Menu')); const Menu = lazy(() => import('../pages/System/Menu'));
const Department = lazy(() => import('../pages/System/Department')); const Department = lazy(() => import('../pages/System/Department'));
const External = lazy(() => import('../pages/System/External')); const External = lazy(() => import('../pages/System/External'));
const X6Test = lazy(() => import('../pages/X6Test'));
// 创建路由 // 创建路由
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: '/', path: '/',
element: (
<PrivateRoute>
<BasicLayout />
</PrivateRoute>
),
children: [
{
path: '',
element: <Navigate to="/dashboard" replace />
},
{
path: 'dashboard',
element: ( element: (
<Suspense fallback={<LoadingComponent />}> <PrivateRoute>
<Dashboard /> <BasicLayout/>
</Suspense> </PrivateRoute>
) ),
},
{
path: 'system',
children: [ children: [
{ {
path: 'user', path: '',
element: ( element: <Navigate to="/dashboard" replace/>
<Suspense fallback={<LoadingComponent />}> },
<User /> {
</Suspense> path: 'dashboard',
) element: (
}, <Suspense fallback={<LoadingComponent/>}>
{ <Dashboard/>
path: 'role', </Suspense>
element: ( )
<Suspense fallback={<LoadingComponent />}> },
<Role /> {
</Suspense> path: 'system',
) children: [
}, {
{ path: 'user',
path: 'menu', element: (
element: ( <Suspense fallback={<LoadingComponent/>}>
<Suspense fallback={<LoadingComponent />}> <User/>
<Menu /> </Suspense>
</Suspense> )
) },
}, {
{ path: 'role',
path: 'department', element: (
element: ( <Suspense fallback={<LoadingComponent/>}>
<Suspense fallback={<LoadingComponent />}> <Role/>
<Department /> </Suspense>
</Suspense> )
) },
}, {
{ path: 'menu',
path: 'external', element: (
element: ( <Suspense fallback={<LoadingComponent/>}>
<Suspense fallback={<LoadingComponent />}> <Menu/>
<External /> </Suspense>
</Suspense> )
) },
} {
path: 'department',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Department/>
</Suspense>
)
},
{
path: 'external',
element: (
<Suspense fallback={<LoadingComponent/>}>
<External/>
</Suspense>
)
}
]
},
{
path: 'x6-test',
element: (
<Suspense fallback={<LoadingComponent/>}>
<X6Test/>
</Suspense>
)
},
{
path: '*',
element: <Navigate to="/dashboard"/>
}
] ]
}, },
{ {
path: '*', path: '/login',
element: <Navigate to="/dashboard" /> element: <Login/>
} }
]
},
{
path: '/login',
element: <Login />
}
]); ]);
export default router; export default router;

View File

@ -0,0 +1,16 @@
export interface BaseResponse {
id: number;
createTime: string;
updateTime: string;
createBy?: string;
updateBy?: string;
enabled: boolean;
version: number;
}
export interface BaseQuery {
keyword?: string;
enabled?: boolean;
startTime?: string;
endTime?: string;
}

1
frontend/src/types/x6.d.ts vendored Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,94 @@
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { message } from 'antd';
interface Response<T = any> {
code: number;
message: string;
data: T;
success: boolean;
}
class Http {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse<Response>) => {
const { data: res } = response;
if (res.success) {
return res.data;
}
message.error(res.message);
return Promise.reject(new Error(res.message));
},
(error) => {
if (error.response?.status === 401) {
// 处理未授权
localStorage.removeItem('token');
window.location.href = '/login';
}
message.error(error.response?.data?.message || '网络错误');
return Promise.reject(error);
}
);
}
get<T = any>(url: string, config?: AxiosRequestConfig) {
return this.instance.get<Response<T>, T>(url, config);
}
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
return this.instance.post<Response<T>, T>(url, data, config);
}
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
return this.instance.put<Response<T>, T>(url, data, config);
}
delete<T = any>(url: string, config?: AxiosRequestConfig) {
return this.instance.delete<Response<T>, T>(url, config);
}
download(url: string, filename?: string, config?: AxiosRequestConfig) {
return this.instance
.get(url, { ...config, responseType: 'blob' })
.then((response) => {
const blob = new Blob([response]);
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename || 'download';
link.click();
window.URL.revokeObjectURL(link.href);
});
}
}
export const http = new Http();

View File

@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ESNext"], "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
@ -10,13 +11,15 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
} },
"types": ["@antv/x6"]
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]