diff --git a/frontend/.cursorrules b/frontend/.cursorrules index 0178c52b..ad0fd472 100644 --- a/frontend/.cursorrules +++ b/frontend/.cursorrules @@ -1,5 +1,6 @@ 你是一名高级前端开发人员,是ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS和现代UI/UX框架(例如,TailwindCSS, Shadcn, Radix)的专家。你深思熟虑,给出细致入微的答案,并且善于推理。你细心地提供准确、真实、深思熟虑的答案,是推理的天才。 # 严格遵循的要求 +- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记 - 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。 - 确认,然后写代码! - 始终编写正确、最佳实践、DRY原则(不要重复自己)、无错误、功能齐全且可工作的代码,还应与下面代码实施指南中列出的规则保持一致。 @@ -31,8 +32,8 @@ src/ ├── components/ # 公共组件 ├── pages/ # 页面组件 │ └── System/ # 系统管理模块 -├── layouts/ # 布局组件 -├── router/ # 路由配置 +├── layouts/ # 布局组件 +├── 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 { - 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(`/api/v1/users/${id}/reset-password`, { password }); + request.post(`/api/v1/users/${id}/reset-password`, { password }); // 标准 CRUD 接口 export const getList = (params?: Query) => - http.get('/api/v1/xxx', { params }); + request.get('/api/v1/xxx', { params }); export const create = (data: Request) => - http.post('/api/v1/xxx', data); + request.post('/api/v1/xxx', data); export const update = (id: number, data: Request) => - http.put(`/api/v1/xxx/${id}`, data); + request.put(`/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 { - code: number; - message: string; - data: T; - success: boolean; - } - - // http 工具类型定义 - const http = { - get: (url: string, config?: RequestOptions) => - request.get(url, config), // 返回 T 类型,而不是 Response - - post: (url: string, data?: any, config?: RequestOptions) => - request.post(url, data, config) // 返回 T 类型 - }; - - // 服务层方法 - 直接使用业务数据类型 - export const login = (data: LoginRequest) => - http.post('/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 中提取 data - 服务方法直接使用业务数据类型作为泛型参数 - 组件中可以直接使用返回的业务数据 - TypeScript 类型系统能正确推断类型 -5. 最佳实践: - ```typescript - // 服务层 - 使用业务数据类型 - export const someService = () => - http.get('/api/v1/xxx'); // 返回 Promise - - // 组件层 - 直接使用业务数据 - 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('/api/v1/upload', file); - - // 特殊处理 - 文件下载 - export const downloadFile = (fileId: string) => - http.download('/api/v1/download', `file-${fileId}.pdf`); - ``` - -## 4. 错误处理规范 - -1. 统一处理: - ```typescript - // 响应拦截器 - const responseHandler = (response: AxiosResponse>) => { - 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. 组件开发: @@ -264,26 +140,4 @@ ModuleName/ # 模块结构 - 类名使用 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 构建 - - 配置路由级代码分割 - \ No newline at end of file + - 支持暗色主题 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9c2554ef..dc7f0469 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index c0227221..6ae3b8f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/scripts/create-module.js b/frontend/scripts/create-module.js deleted file mode 100644 index c416ba3f..00000000 --- a/frontend/scripts/create-module.js +++ /dev/null @@ -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 ( - - <${moduleName}Form onSubmit={handleCreate} /> - <${moduleName}Table - loading={loading} - dataSource={list} - selectedRows={selectedRows} - onDelete={handleDelete} - onBatchDelete={handleBatchDelete} - onUpdate={handleUpdate} - onSelectionChange={setSelectedRows} - /> - - ); -}; - -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; - onBatchDelete: () => Promise; - onUpdate: (id: number, data: ${moduleName}Response) => Promise; - 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) => ( - - - onDelete(record.id)} - > - - - - ), - }, - ]; - - return ( - <> - {selectedRows.length > 0 && ( -
- - - -
- )} - 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; -} - -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 ( -
- {/* TODO: 添加表单项 */} - - - - - ); -}; - -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: ( - }> - <${moduleName} /> - - ) -} - -并添加懒加载导入: -const ${moduleName} = lazy(() => import('../pages/System/${moduleName}')); -`); \ No newline at end of file diff --git a/frontend/scripts/generate-page.ts b/frontend/scripts/generate-page.ts new file mode 100644 index 00000000..db42691b --- /dev/null +++ b/frontend/scripts/generate-page.ts @@ -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([]); + 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) => ( + + + + + ), + }, + ]; + + return ( + <> + + + + + }> +
+ + setQuery(prev => ({ ...prev, name: e.target.value }))} + /> + + +
setSelectedRowKeys(keys as number[]), + }} + /> + + + setEditModalVisible(false)} + > +
+ + + + {/* TODO: 添加其他表单项 */} + +
+ + ); +}; + +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); \ No newline at end of file diff --git a/frontend/src/pages/System/Menu/service.ts b/frontend/src/pages/System/Menu/service.ts index 1aadda9f..7cec6030 100644 --- a/frontend/src/pages/System/Menu/service.ts +++ b/frontend/src/pages/System/Menu/service.ts @@ -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]; }; // 创建菜单 diff --git a/frontend/src/pages/X6Test/index.tsx b/frontend/src/pages/X6Test/index.tsx new file mode 100644 index 00000000..9641ed6a --- /dev/null +++ b/frontend/src/pages/X6Test/index.tsx @@ -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(null); + const graphRef = useRef({ 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 ( + +
+ + ); +}; + +export default X6TestPage; \ No newline at end of file diff --git a/frontend/src/pages/X6Test/types.ts b/frontend/src/pages/X6Test/types.ts new file mode 100644 index 00000000..03d2da6a --- /dev/null +++ b/frontend/src/pages/X6Test/types.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/src/pages/flow-designer/components/FlowGraph.tsx b/frontend/src/pages/flow-designer/components/FlowGraph.tsx new file mode 100644 index 00000000..4fdd57e2 --- /dev/null +++ b/frontend/src/pages/flow-designer/components/FlowGraph.tsx @@ -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 ( +
+ {data.label || 'Node'} +
+ ); + }, +}); + +interface FlowGraphProps { + onNodeSelected?: (nodeId: string) => void; + onEdgeSelected?: (edgeId: string) => void; +} + +const FlowGraph: React.FC = ({ onNodeSelected, onEdgeSelected }) => { + const containerRef = useRef(null); + const graphRef = useRef(); + + 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 ( +
+ ); +}; + +export default FlowGraph; \ No newline at end of file diff --git a/frontend/src/pages/flow-designer/index.tsx b/frontend/src/pages/flow-designer/index.tsx new file mode 100644 index 00000000..61a97840 --- /dev/null +++ b/frontend/src/pages/flow-designer/index.tsx @@ -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(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 ( +
+ + + + + + } + > + console.log('选中连线:', edgeId)} + /> + + + setDrawerVisible(false)} + open={drawerVisible} + width={400} + extra={ + + + + + } + > +
+ + + + +