菜单可正确被加载
This commit is contained in:
parent
893ab0e29f
commit
c39db5a498
@ -1,5 +1,6 @@
|
||||
你是一名高级前端开发人员,是ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS和现代UI/UX框架(例如,TailwindCSS, Shadcn, Radix)的专家。你深思熟虑,给出细致入微的答案,并且善于推理。你细心地提供准确、真实、深思熟虑的答案,是推理的天才。
|
||||
# 严格遵循的要求
|
||||
- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
|
||||
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
|
||||
- 确认,然后写代码!
|
||||
- 始终编写正确、最佳实践、DRY原则(不要重复自己)、无错误、功能齐全且可工作的代码,还应与下面代码实施指南中列出的规则保持一致。
|
||||
@ -32,7 +33,7 @@ src/
|
||||
├── pages/ # 页面组件
|
||||
│ └── System/ # 系统管理模块
|
||||
├── layouts/ # 布局组件
|
||||
├── router/ # 路由配置
|
||||
├── router/index.ts # 路由配置
|
||||
├── store/ # 状态管理
|
||||
├── services/ # API 服务
|
||||
├── utils/ # 工具函数
|
||||
@ -65,63 +66,38 @@ ModuleName/ # 模块结构
|
||||
- 事件处理:`handle` 前缀
|
||||
- 异步函数:动词开头(`fetchData`)
|
||||
|
||||
3. 已定义了基础类型,所以直接继承即可,把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. 已定义了基础类型src\types\base下直接继承即可,把Response query request 的定义都模块的types.ts文件中:
|
||||
|
||||
## 3. 服务层规范
|
||||
|
||||
1. 方法定义:
|
||||
```typescript
|
||||
// 推荐写法 - 使用 http 工具
|
||||
// 推荐写法 - 使用 request 工具
|
||||
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 接口
|
||||
export const getList = (params?: Query) =>
|
||||
http.get<Response[]>('/api/v1/xxx', { params });
|
||||
request.get<Response[]>('/api/v1/xxx', { params });
|
||||
|
||||
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) =>
|
||||
http.put<Response>(`/api/v1/xxx/${id}`, data);
|
||||
request.put<Response>(`/api/v1/xxx/${id}`, data);
|
||||
|
||||
export const remove = (id: number) =>
|
||||
http.delete(`/api/v1/xxx/${id}`);
|
||||
request.delete(`/api/v1/xxx/${id}`);
|
||||
|
||||
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) =>
|
||||
http.download('/api/v1/xxx/export', undefined, { params });
|
||||
request.download('/api/v1/xxx/export', undefined, { params });
|
||||
```
|
||||
|
||||
2. 规范要点:
|
||||
- 统一使用 `http` 工具(不要使用 request)
|
||||
- 统一使用 src\utils\request.ts
|
||||
- 使用泛型指定响应数据类型
|
||||
- 错误处理在拦截器中统一处理
|
||||
- 使用模板字符串拼接路径
|
||||
@ -129,114 +105,14 @@ ModuleName/ # 模块结构
|
||||
- 资源使用复数形式(users, roles)
|
||||
- 特殊操作使用动词(export, import)
|
||||
|
||||
3. 响应数据处理:
|
||||
```typescript
|
||||
// 响应数据结构
|
||||
interface Response<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// http 工具类型定义
|
||||
const http = {
|
||||
get: <T = any>(url: string, config?: RequestOptions) =>
|
||||
request.get<any, T>(url, config), // 返回 T 类型,而不是 Response<T>
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.post<any, T>(url, data, config) // 返回 T 类型
|
||||
};
|
||||
|
||||
// 服务层方法 - 直接使用业务数据类型
|
||||
export const login = (data: LoginRequest) =>
|
||||
http.post<LoginResponse>('/api/v1/user/login', data); // 返回 LoginResponse
|
||||
|
||||
// 组件中使用 - 直接获取业务数据
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const userData = await login(data); // userData 类型是 LoginResponse
|
||||
console.log(userData.token); // 可以直接访问业务数据
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
4. 类型处理规则:
|
||||
- http 工具的泛型参数 T 表示业务数据类型
|
||||
- request 工具的泛型参数 T 表示业务数据类型
|
||||
- 响应拦截器负责从 Response<T> 中提取 data
|
||||
- 服务方法直接使用业务数据类型作为泛型参数
|
||||
- 组件中可以直接使用返回的业务数据
|
||||
- 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 开发规范
|
||||
|
||||
1. 组件开发:
|
||||
@ -265,25 +141,3 @@ ModuleName/ # 模块结构
|
||||
- 避免内联样式
|
||||
- 响应式适配
|
||||
- 支持暗色主题
|
||||
|
||||
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 构建
|
||||
- 配置路由级代码分割
|
||||
|
||||
43
frontend/package-lock.json
generated
43
frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"antd": "^5.22.2",
|
||||
"axios": "^1.6.2",
|
||||
@ -134,6 +135,33 @@
|
||||
"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": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
@ -2904,6 +2932,12 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.11.tgz",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"antd": "^5.22.2",
|
||||
"axios": "^1.6.2",
|
||||
|
||||
@ -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}'));
|
||||
`);
|
||||
272
frontend/scripts/generate-page.ts
Normal file
272
frontend/scripts/generate-page.ts
Normal 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);
|
||||
@ -21,7 +21,6 @@ export const getCurrentUserMenus = async () => {
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
version: 0,
|
||||
deleted: false,
|
||||
name: "首页",
|
||||
path: "/dashboard",
|
||||
component: "/Dashboard/index",
|
||||
@ -35,6 +34,25 @@ export const getCurrentUserMenus = async () => {
|
||||
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 processed = { ...menu };
|
||||
@ -53,7 +71,7 @@ export const getCurrentUserMenus = async () => {
|
||||
};
|
||||
|
||||
const processedMenus = menus.map(processMenu);
|
||||
return [dashboard, ...processedMenus];
|
||||
return [dashboard, x6Test, ...processedMenus];
|
||||
};
|
||||
|
||||
// 创建菜单
|
||||
|
||||
102
frontend/src/pages/X6Test/index.tsx
Normal file
102
frontend/src/pages/X6Test/index.tsx
Normal 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;
|
||||
19
frontend/src/pages/X6Test/types.ts
Normal file
19
frontend/src/pages/X6Test/types.ts
Normal 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;
|
||||
}
|
||||
212
frontend/src/pages/flow-designer/components/FlowGraph.tsx
Normal file
212
frontend/src/pages/flow-designer/components/FlowGraph.tsx
Normal 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;
|
||||
88
frontend/src/pages/flow-designer/index.tsx
Normal file
88
frontend/src/pages/flow-designer/index.tsx
Normal 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;
|
||||
24
frontend/src/pages/flow-designer/service.ts
Normal file
24
frontend/src/pages/flow-designer/service.ts
Normal 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}`);
|
||||
41
frontend/src/pages/flow-designer/types.ts
Normal file
41
frontend/src/pages/flow-designer/types.ts
Normal 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;
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
import { createBrowserRouter, Navigate, RouteObject } from 'react-router-dom';
|
||||
import {createBrowserRouter, Navigate} from 'react-router-dom';
|
||||
import {lazy, Suspense} from 'react';
|
||||
import {Spin} from 'antd';
|
||||
import Login from '../pages/Login';
|
||||
import BasicLayout from '../layouts/BasicLayout';
|
||||
import {useSelector} from 'react-redux';
|
||||
import {RootState} from '../store';
|
||||
import { MenuResponse } from '@/pages/System/Menu/types';
|
||||
|
||||
// 加载中组件
|
||||
const LoadingComponent = () => (
|
||||
@ -32,6 +31,7 @@ const Role = lazy(() => import('../pages/System/Role'));
|
||||
const Menu = lazy(() => import('../pages/System/Menu'));
|
||||
const Department = lazy(() => import('../pages/System/Department'));
|
||||
const External = lazy(() => import('../pages/System/External'));
|
||||
const X6Test = lazy(() => import('../pages/X6Test'));
|
||||
|
||||
// 创建路由
|
||||
const router = createBrowserRouter([
|
||||
@ -100,6 +100,14 @@ const router = createBrowserRouter([
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'x6-test',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<X6Test/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/dashboard"/>
|
||||
|
||||
16
frontend/src/types/base.ts
Normal file
16
frontend/src/types/base.ts
Normal 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
1
frontend/src/types/x6.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
||||
94
frontend/src/utils/http.ts
Normal file
94
frontend/src/utils/http.ts
Normal 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();
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
@ -10,13 +11,15 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["@antv/x6"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user