修复弹窗边距问题

This commit is contained in:
dengqichen 2025-11-01 11:46:50 +08:00
parent d4e4beb3a7
commit 9900602244
18 changed files with 137 additions and 1042 deletions

View File

@ -1,159 +0,0 @@
你是一名高级前端开发人员是ReactJS NextJS, JavaScript, TypeScript HTML CSS和现代UI/UX框架例如TailwindCSS, Shadcn, Radix的专家。你深思熟虑给出细致入微的答案并且善于推理。你细心地提供准确、真实、深思熟虑的答案是推理的天才。
# 严格遵循的要求
- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
- 确认,然后写代码!
- 始终编写正确、最佳实践、DRY原则不要重复自己、无错误、功能齐全且可工作的代码还应与下面代码实施指南中列出的规则保持一致。
- 专注于简单易读的代码,而不是高性能。
- 完全实现所有要求的功能。
- 不要留下待办事项、占位符或缺失的部分。
- 确保代码完整!彻底确认。
- 包括所有必需的导入的包,并确保关键组件的正确命名。
- 如果你认为可能没有正确答案,你就说出来。
- 如果你不知道答案,就说出来,而不是猜测。
- 可以提出合理化的建议,但是需要等待是否可以。
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
- 不要随意修改现在的接口调用路径,如果不知道接口是什么,可以问。
#代码实现指南
在编写代码时遵循以下规则:
- 尽可能使用早期返回,使代码更具可读性。
- 总是使用顺风类样式HTML元素避免使用CSS或标签。
- 尽可能在类标记中使用“ class: ”而不是第三操作符。
- 使用描述性变量名和函数/const名。此外事件函数应该以“handle”前缀命名就像onClick的“handleClick”和onKeyDown的“handleKeyDown”。
- 在元素上实现可访问性特性。例如一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
— 使用const代替函数例如const toggle =() =>。另外,如果可能的话,定义一个类型。
# Deploy Ease Platform 前端开发规范
## 1. 项目结构规范
```
src/
├── components/ # 公共组件
├── pages/ # 页面组件
│ └── System/ # 系统管理模块
├── layouts/ # 布局组件
├── router/index.ts # 路由配置
├── store/ # 状态管理
├── services/ # API 服务
├── utils/ # 工具函数
├── hooks/ # 自定义 Hooks
└── types/ # TS 类型定义
│ └── pages.ts # 分页基础类
ModuleName/ # 模块结构
├── components/ # 模块私有组件
├── type.ts # 类型定义
├── service.ts # API 服务
└── index.tsx # 模块入口
```
## 2. 命名与类型规范
1. 文件命名:
- 组件PascalCase`UserProfile.tsx`
- 工具函数camelCase`formatDate.ts`
- 样式:组件同名(`UserProfile.module.css`
- Redux`xxxSlice.ts`
- 类型:`types.ts`
- 服务:`service.ts`
2. 变量命名:
- 常量UPPER_SNAKE_CASE
- 变量camelCase
- 接口:以 `I` 开头PascalCase
- 类型:以 `T` 开头PascalCase
- 事件处理:`handle` 前缀
- 异步函数:动词开头(`fetchData`
3. 已定义了基础类型src\types\base下直接继承即可把Response query request 的定义都模块的types.ts文件中
## 3. 服务层规范
1. 方法定义:
```typescript
// 推荐写法 - 使用 request 工具
export const resetPassword = (id: number, password: string) =>
request.post<void>(`/api/v1/users/${id}/reset-password`, { password });
// 标准 CRUD 接口
export const getList = (params?: Query) =>
request.get<Page<Response>>('/api/v1/xxx/page', { params }); // 列表接口统一使用 /page 后缀
export const create = (data: Request) =>
request.post<Response>('/api/v1/xxx', data);
export const update = (id: number, data: Request) =>
request.put<Response>(`/api/v1/xxx/${id}`, data);
export const remove = (id: number) =>
request.delete(`/api/v1/xxx/${id}`);
export const batchRemove = (ids: number[]) =>
request.post('/api/v1/xxx/batch-delete', { ids });
export const exportData = (params?: Query) =>
request.download('/api/v1/xxx/export', undefined, { params });
```
2. 规范要点:
- 统一使用 src\utils\request.ts
- 使用泛型指定响应数据类型
- 错误处理在拦截器中统一处理
- 使用模板字符串拼接路径
- API 路径使用 `/api/v1/` 前缀
- 资源使用复数形式users, roles
- 特殊操作使用动词export, import
3. 列表数据获取规范:
- API 路径必须以 `/page` 结尾,例如:`/api/v1/workflow-definitions/page`
- 服务层方法定义示例:
```typescript
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
request.get<Page<WorkflowDefinitionResponse>>(`${DEFINITION_URL}/page`, { params });
```
- 组件中获取列表数据:
```typescript
const response = await getDefinitions();
if (response) {
setList(response.content); // 使用 response.content 获取列表数据
}
```
- 分页数据结构统一使用 `Page<T>` 类型
- 列表数据必须位于返回结果的 `content` 字段中
## 4. 类型处理规则:
- request 工具的泛型参数 T 表示业务数据类型
- 响应拦截器负责从 Response<T> 中提取 data
- 服务方法直接使用业务数据类型作为泛型参数
- 组件中可以直接使用返回的业务数据
- TypeScript 类型系统能正确推断类型
## 5. React 开发规范
1. 组件开发:
- 使用函数组件和箭头函数
- Props 类型必须定义
- 必须提供默认值
- 使用 memo 优化
- 复杂组件需拆分
2. Hooks 使用:
- 自定义 hooks 以 `use` 开头
- 使用 TypeScript 泛型
- 使用 useCallback 和 useMemo
- 使用 Promise.all 处理并行请求
3. 状态管理:
- 使用 Redux Toolkit
- 持久化数据存储在 localStorage
- Token、用户信息、菜单统一管理
## 6. 样式与布局规范
1. CSS 规范:
- 使用 CSS Modules
- 类名使用 kebab-case
- 避免内联样式
- 响应式适配
- 支持暗色主题

View File

@ -1,159 +0,0 @@
你是一名高级前端开发人员是ReactJS NextJS, JavaScript, TypeScript HTML CSS和现代UI/UX框架例如TailwindCSS, Shadcn, Radix的专家。你深思熟虑给出细致入微的答案并且善于推理。你细心地提供准确、真实、深思熟虑的答案是推理的天才。
# 严格遵循的要求
- 不要随意删除代码,请先详细浏览下现在所有的代码,再进行询问修改,得到确认再修改。重点切记
- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。
- 确认,然后写代码!
- 始终编写正确、最佳实践、DRY原则不要重复自己、无错误、功能齐全且可工作的代码还应与下面代码实施指南中列出的规则保持一致。
- 专注于简单易读的代码,而不是高性能。
- 完全实现所有要求的功能。
- 不要留下待办事项、占位符或缺失的部分。
- 确保代码完整!彻底确认。
- 包括所有必需的导入的包,并确保关键组件的正确命名。
- 如果你认为可能没有正确答案,你就说出来。
- 如果你不知道答案,就说出来,而不是猜测。
- 可以提出合理化的建议,但是需要等待是否可以。
- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。
- 不要随意修改现在的接口调用路径,如果不知道接口是什么,可以问。
#代码实现指南
在编写代码时遵循以下规则:
- 尽可能使用早期返回,使代码更具可读性。
- 总是使用顺风类样式HTML元素避免使用CSS或标签。
- 尽可能在类标记中使用“ class: ”而不是第三操作符。
- 使用描述性变量名和函数/const名。此外事件函数应该以“handle”前缀命名就像onClick的“handleClick”和onKeyDown的“handleKeyDown”。
- 在元素上实现可访问性特性。例如一个标签应该有tabindex= " 0 "、aria-label、on:click和on:keydown以及类似的属性。
— 使用const代替函数例如const toggle =() =>。另外,如果可能的话,定义一个类型。
# Deploy Ease Platform 前端开发规范
## 1. 项目结构规范
```
src/
├── components/ # 公共组件
├── pages/ # 页面组件
│ └── System/ # 系统管理模块
├── layouts/ # 布局组件
├── router/index.ts # 路由配置
├── store/ # 状态管理
├── services/ # API 服务
├── utils/ # 工具函数
├── hooks/ # 自定义 Hooks
└── types/ # TS 类型定义
│ └── pages.ts # 分页基础类
ModuleName/ # 模块结构
├── components/ # 模块私有组件
├── type.ts # 类型定义
├── service.ts # API 服务
└── index.tsx # 模块入口
```
## 2. 命名与类型规范
1. 文件命名:
- 组件PascalCase`UserProfile.tsx`
- 工具函数camelCase`formatDate.ts`
- 样式:组件同名(`UserProfile.module.css`
- Redux`xxxSlice.ts`
- 类型:`types.ts`
- 服务:`service.ts`
2. 变量命名:
- 常量UPPER_SNAKE_CASE
- 变量camelCase
- 接口:以 `I` 开头PascalCase
- 类型:以 `T` 开头PascalCase
- 事件处理:`handle` 前缀
- 异步函数:动词开头(`fetchData`
3. 要使用和继承定义了基础类型src\types\base.ts
## 3. 服务层规范
1. 方法定义:
```typescript
// 推荐写法 - 使用 request 工具
export const resetPassword = (id: number, password: string) =>
request.post<void>(`/api/v1/users/${id}/reset-password`, { password });
// 标准 CRUD 接口
export const getList = (params?: Query) =>
request.get<Page<Response>>('/api/v1/xxx/page', { params }); // 列表接口统一使用 /page 后缀
export const create = (data: Request) =>
request.post<Response>('/api/v1/xxx', data);
export const update = (id: number, data: Request) =>
request.put<Response>(`/api/v1/xxx/${id}`, data);
export const remove = (id: number) =>
request.delete(`/api/v1/xxx/${id}`);
export const batchRemove = (ids: number[]) =>
request.post('/api/v1/xxx/batch-delete', { ids });
export const exportData = (params?: Query) =>
request.download('/api/v1/xxx/export', undefined, { params });
```
2. 规范要点:
- 统一使用 src\utils\request.ts
- 使用泛型指定响应数据类型
- 错误处理在拦截器中统一处理
- 使用模板字符串拼接路径
- API 路径使用 `/api/v1/` 前缀
- 资源使用复数形式users, roles
- 特殊操作使用动词export, import
3. 列表数据获取规范:
- API 路径必须以 `/page` 结尾,例如:`/api/v1/workflow-definitions/page`
- 服务层方法定义示例:
```typescript
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
request.get<Page<WorkflowDefinitionResponse>>(`${DEFINITION_URL}/page`, { params });
```
- 组件中获取列表数据:
```typescript
const response = await getDefinitions();
if (response) {
setList(response.content); // 使用 response.content 获取列表数据
}
```
- 分页数据结构统一使用 `Page<T>` 类型
- 列表数据必须位于返回结果的 `content` 字段中
## 4. 类型处理规则:
- request 工具的泛型参数 T 表示业务数据类型
- 响应拦截器负责从 Response<T> 中提取 data
- 服务方法直接使用业务数据类型作为泛型参数
- 组件中可以直接使用返回的业务数据
- TypeScript 类型系统能正确推断类型
## 5. React 开发规范
1. 组件开发:
- 使用函数组件和箭头函数
- Props 类型必须定义
- 必须提供默认值
- 使用 memo 优化
- 复杂组件需拆分
2. Hooks 使用:
- 自定义 hooks 以 `use` 开头
- 使用 TypeScript 泛型
- 使用 useCallback 和 useMemo
- 使用 Promise.all 处理并行请求
3. 状态管理:
- 使用 Redux Toolkit
- 持久化数据存储在 localStorage
- Token、用户信息、菜单统一管理
## 6. 样式与布局规范
1. CSS 规范:
- 使用 CSS Modules
- 类名使用 kebab-case
- 避免内联样式
- 响应式适配
- 支持暗色主题

View File

@ -1,659 +0,0 @@
# API 接口文档
## 1. 通用说明
### 1.1 接口规范
- 基础路径: `/api/v1`
- 请求方式: REST风格
- 数据格式: JSON
- 字符编码: UTF-8
- 时间格式: ISO8601 (YYYY-MM-DDTHH:mm:ss.SSSZ)
### 1.2 通用响应格式
```json
{
"code": 0, // 响应码0表示成功非0表示失败
"message": "成功", // 响应消息
"data": { // 响应数据
// 具体数据结构
}
}
```
### 1.3 通用查询参数
分页查询参数:
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段,可选
"keyword": "", // 关键字搜索,可选
}
```
### 1.4 通用错误码
| 错误码 | 说明 | 处理建议 |
|--------|------|----------|
| 0 | 成功 | - |
| 1000 | 系统内部错误 | 联系管理员 |
| 1001 | 数据库操作失败 | 重试或联系管理员 |
| 1002 | 并发操作冲突 | 刷新后重试 |
| 2000 | 参数验证失败 | 检查参数 |
| 2001 | 数据不存在 | 检查参数 |
| 2002 | 数据已存在 | 检查参数 |
## 2. 工作流定义<E5AE9A><E4B989>
### 2.1 创建工作流定义
**接口说明**:创建新的工作流定义
**请求路径**POST /workflow-definitions
**请求参数**
```json
{
"code": "string", // 工作流编码,必填,唯一
"name": "string", // 工作流名称,必填
"description": "string", // 描述,可选
"nodeConfig": { // 节点配置,必填
"nodes": [{
"id": "string", // 节点ID
"type": "string", // 节点类型
"name": "string", // 节点名称
"config": { // 节点配置
// 具体配置项根据节点类型定义
}
}]
},
"transitionConfig": { // 流转配置,必填
"transitions": [{
"from": "string", // 来源节点ID
"to": "string", // 目标节点ID
"condition": "string" // 流转条件
}]
},
"formDefinition": { // 表单定义,必填
// 表单配置项
},
"graphDefinition": { // 图形定义,必填
// 图形布局配置
}
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"id": "long", // 工作流定义ID
"code": "string", // 工作流编码
"name": "string", // 工作流名称
"description": "string", // 描述
"version": 1, // 版本号
"status": "DRAFT", // 状态DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用
"enabled": true, // 是否启用
"nodeConfig": {}, // 节点配置
"transitionConfig": {}, // 流转配置
"formDefinition": {}, // 表单定义
"graphDefinition": {}, // 图形定义
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}
}
```
### 2.2 更新工作流定义
**接口说明**:更新工作流定义,仅草稿状态可更新
**请求路径**PUT /workflow-definitions/{id}
**路径参数**
- id: 工作流定义ID
**请求参数**:同创建接口
**响应数据**:同创建接口
### 2.3 发布工作流定义
**接口说明**:发布工作流定义,使其可被使用
**请求路径**POST /workflow-definitions/{id}/publish
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.4 禁用工作流定义
**接口说明**:禁用工作流定义,禁止新建实例
**请求路径**POST /workflow-definitions/{id}/disable
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.5 启用工作流定义
**接口说明**启用已<E794A8><E5B7B2>用的工作流定义
**请求路径**POST /workflow-definitions/{id}/enable
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.6 创建新版本
**接口说明**:基于现有工作流定义创建新版本
**请求路径**POST /workflow-definitions/{id}/versions
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.7 查询工作流定义列表
**接口说明**:分页查询工作流定义列表
**请求路径**GET /workflow-definitions
**请求参数**
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段
"keyword": "", // 关键字搜索
"status": "DRAFT", // 状态过滤
"enabled": true // 是否启用
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"content": [{ // 列表数据
// 工作流定义数据,同创建接口
}],
"totalElements": 100, // 总记录数
"totalPages": 10, // 总页数
"size": 10, // 每页大小
"number": 1 // 当前页码
}
}
```
## 3. 工作流实例接口
### 3.1 创建工作流实例
**接口说明**:创建工作流实例
**请求路径**POST /workflow-instances
**请求参数**
```json
{
"definitionId": "long", // 工作流定义ID必填
"businessKey": "string", // 业务标识,可选
"variables": { // 初始变量,可选
"key": "value"
}
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"id": "long", // 实例ID
"definitionId": "long", // 定义ID
"businessKey": "string",// 业务标识
"status": "CREATED", // 状态CREATED-已创建、RUNNING-运行中、SUSPENDED-已暂停、COMPLETED-已完成、TERMINATED-已终止
"startTime": "string", // 开始时间
"endTime": "string", // 结束时间
"variables": {}, // 变量
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}
}
```
### 3.2 启动工作流实例
**接口说明**:启动工作流实例
**请求路径**POST /workflow-instances/{id}/start
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.3 暂停工作流实例
**接口说明**:暂停运行中的工作流实例
**请求路径**POST /workflow-instances/{id}/suspend
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.4 恢复工作流实例
**接口说明**:恢复已暂停的工作流实例
**请求路径**POST /workflow-instances/{id}/resume
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.5 终止工作流实例
**接口说明**:强制终止工作流实例
**请求路径**POST /workflow-instances/{id}/terminate
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.6 查询工作流实例列表
**接口说明**:分页查询工作流实例列表
**请求路径**GET /workflow-instances
**请求参数**
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段
"keyword": "", // 关键字搜索
"status": "RUNNING", // 状态过滤
"definitionId": "long", // 定义ID过滤
"businessKey": "string" // 业务标识过滤
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"content": [{ // 列表数据
// 工作流实例数据,同创建接口
}],
"totalElements": 100, // 总记录数
"totalPages": 10, // 总页数
"size": 10, // 每页大小
"number": 1 // 当前页码
}
}
```
## 4. 节点实例接口
### 4.1 查询节点实例列表
**接口说明**:查询工作流实例的节点列表
**请求路径**GET /workflow-instances/{instanceId}/nodes
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"status": "RUNNING" // 状态过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 节点实例ID
"workflowInstanceId": "long", // 工作流实例ID
"nodeId": "string", // 节点定义ID
"nodeName": "string", // 节点名称
"nodeType": "string", // 节点类型
"status": "string", // 状态CREATED、RUNNING、COMPLETED、FAILED
"startTime": "string", // 开始时间
"endTime": "string", // 结束时间
"output": "string", // 输出结果
"error": "string", // 错误信息
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}]
}
```
## 5. 工作流变量接口
### 5.1 设置变量
**接口说明**:设置工作流实例变量
**请求路径**POST /workflow-instances/{instanceId}/variables
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"name": "string", // 变量名,必填
"value": "object", // 变量值<EFBC8C><E5BF85>
"scope": "GLOBAL" // 作用域GLOBAL-全局、NODE-节点可选默认GLOBAL
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功"
}
```
### 5.2 获取变量
**接口说明**:获取工作流实例变量
**请求路径**GET /workflow-instances/{instanceId}/variables
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"scope": "GLOBAL" // 作用域过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"variableName": "variableValue"
}
}
```
## 6. 工作流日志接口
### 6.1 查询日志列表
**接口说明**:查询工作流实例日志
**请求路径**GET /workflow-instances/{instanceId}/logs
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"nodeId": "string", // 节点ID过滤可选
"level": "INFO", // 日志级别过滤DEBUG、INFO、WARN、ERROR可选
"startTime": "string", // 开始时间过滤,可选
"endTime": "string" // 结束时间过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 日志ID
"workflowInstanceId": "long", // 工作流实例ID
"nodeId": "string", // 节点ID
"level": "string", // 日志级别
"content": "string", // 日志内容
"detail": "string", // 详细信息
"createTime": "string" // 创建时间
}]
}
```
## 7. 节点类型接口
### 7.1 查询节点类型列表
**接口说明**:查询可用的节点类型列表
**请求路径**GET /node-types
**请求参数**
```json
{
"enabled": true, // 是否启用过滤,可选
"category": "TASK" // 类型分类过滤TASK、EVENT、GATEWAY可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 节点类型ID
"code": "string", // 节点类型编码
"name": "string", // 节点类型名称
"category": "string", // 分类
"description": "string", // 描述
"enabled": true, // 是否启用
"icon": "string", // 图标
"color": "string", // 颜色
"executors": [{ // 执行器列表仅TASK类型
"code": "string", // 执行器编码
"name": "string", // 执行器名称
"description": "string", // 描述
"configSchema": {} // 配置模式
}],
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}]
}
```
### 7.2 获取节点执行器列表
**接口说明**:获取指定节点类型支持的执行器列表
**请求路径**GET /node-types/{code}/executors
**路径参数**
- code: 节点类型编码
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"code": "string", // 执行器编码
"name": "string", // 执行器名称
"description": "string", // 描述
"configSchema": { // 配置模式
"type": "object",
"properties": {
// 具体配置项定义
},
"required": [] // 必填项
}
}]
}
```
## 8. 错误码说明
### 8.1 系统错误 (1xxx)
- 1000: 系统内部错误
- 1001: 数据库操作失败
- 1002: 并发操作冲突
### 8.2 通用业务错误 (2xxx)
- 2000: 参数验证失败
- 2001: 数据不存在
- 2002: 数据已存在
### 8.3 工作流定义错误 (3xxx)
- 3000: 工作流定义不存在
- 3001: 工作流编码已存在
- 3002: 工作流定义非草稿状态
- 3003: 工作流定义未发布
- 3004: 工作流定义已禁用
- 3005: 节点配置无效
- 3006: 流转配置无效
- 3007: 表单配置无效
### 8.4 工作流实例错误 (4xxx)
- 4000: 工作流实例不存在
- 4001: 工作流实例状态无效
- 4002: 工作流实例已终止
- 4003: 节点实例不存在
- 4004: 变量类型无效
## 9. 数据结构说明
### 9.1 工作流状态
```typescript
enum WorkflowStatus {
DRAFT = "DRAFT", // 草稿
PUBLISHED = "PUBLISHED", // 已发布
DISABLED = "DISABLED" // 已禁用
}
```
### 9.2 实例状态
```typescript
enum InstanceStatus {
CREATED = "CREATED", // 已创建
RUNNING = "RUNNING", // 运行中
SUSPENDED = "SUSPENDED", // 已暂停
COMPLETED = "COMPLETED", // 已完成
TERMINATED = "TERMINATED"// 已终止
}
```
### 9.3 节点状态
```typescript
enum NodeStatus {
CREATED = "CREATED", // 已创建
RUNNING = "RUNNING", // 运行中
COMPLETED = "COMPLETED", // 已完成
FAILED = "FAILED" // 失败
}
```
### 9.4 日志级别
```typescript
enum LogLevel {
DEBUG = "DEBUG",
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR"
}
```
### 9.5 变量作用域
```typescript
enum VariableScope {
GLOBAL = "GLOBAL", // 全局变量
NODE = "NODE" // 节点变量
}
```
## 10. 最佳实践
### 10.1 工作流设计
1. 工作流定义创建流程:
- 创建草稿
- 配置节点和流转
- 验证配置
- 发布使用
2. 节点类型选择:
- 根据业务场景选择合适的节点类型
- 配置合适的执行器
- 设置必要的变量
### 10.2 实例管理
1. 实例生命周期管理:
- 创建 -> 启动 -> 运行 -> 完成
- 必要时可暂停或终止
- 记录关键节点日志
2. 变量使用:
- 合理使用变量作用域
- 及时清理无用变量
- 注意变量类型匹配
### 10.3 错误处理
1. 异常处理:
- 捕获所有可能的错误码
- 提供友好的错误提示
- 记录详细的错误日志
2. 重试机制:
- 对非致命错误进行重试
- 设置合理的重试间隔
- 限制最大重试次数
### 10.4 性能优化
1. 查询优化:
- 合理使用分页
- 避免大量数据查询
- 使用合适的查询条件
2. 缓存使用:
- 缓存常用数据
- 及时更新缓存
- 避免缓存穿透

View File

@ -196,8 +196,8 @@ const BasicLayout: React.FC = () => {
</Space> </Space>
<Dropdown menu={{ items: userMenuItems }} trigger={['hover']}> <Dropdown menu={{ items: userMenuItems }} trigger={['hover']}>
<Space className="cursor-pointer"> <Space className="cursor-pointer">
<UserOutlined /> <UserOutlined className="text-sm" />
<span>{userInfo?.nickname || userInfo?.username}</span> <span className="text-sm">{userInfo?.nickname || userInfo?.username}</span>
</Space> </Space>
</Dropdown> </Dropdown>
</Space> </Space>

View File

@ -34,7 +34,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600"> <DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" /> <AlertCircle className="h-5 w-5" />
@ -48,7 +48,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
)} )}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground"> <div className="px-6 py-4 space-y-2 text-sm text-muted-foreground">
<p> <p>
<span className="font-medium">:</span> <code className="text-sm">{record.code}</code> <span className="font-medium">:</span> <code className="text-sm">{record.code}</code>
</p> </p>

View File

@ -181,7 +181,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
<DialogHeader> <DialogHeader>
<DialogTitle>{record ? '编辑部门' : '新增部门'}</DialogTitle> <DialogTitle>{record ? '编辑部门' : '新增部门'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="parentId"></Label> <Label htmlFor="parentId"></Label>
<Select <Select

View File

@ -46,7 +46,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600"> <DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" /> <AlertCircle className="h-5 w-5" />
@ -60,7 +60,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
)} )}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground"> <div className="px-6 py-4 space-y-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium">:</span> <span className="font-medium">:</span>
<Badge variant={typeInfo.variant}>{typeInfo.text}</Badge> <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>

View File

@ -100,7 +100,7 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
<TagIcon className="h-5 w-5" /> <TagIcon className="h-5 w-5" />
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="px-6 py-4">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <Loader2 className="h-6 w-6 animate-spin mr-2" />

View File

@ -32,7 +32,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[450px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600"> <DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" /> <AlertCircle className="h-5 w-5" />
@ -42,7 +42,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground"> <div className="px-6 py-4 space-y-2 text-sm text-muted-foreground">
<p> <p>
<span className="font-medium">:</span> {record.code} <span className="font-medium">:</span> {record.code}
</p> </p>

View File

@ -103,7 +103,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
<DialogHeader> <DialogHeader>
<DialogTitle>{record ? '编辑角色' : '新建角色'}</DialogTitle> <DialogTitle>{record ? '编辑角色' : '新建角色'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="code"> *</Label> <Label htmlFor="code"> *</Label>
<Input <Input

View File

@ -1,16 +1,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { Loader2, KeyRound, Folder, Key } from 'lucide-react'; import { Loader2, Folder, Key, Search, Shield } from 'lucide-react';
import { getPermissionTree } from '../service'; import { getPermissionTree } from '../service';
interface PermissionDialogProps { interface PermissionDialogProps {
@ -52,6 +52,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [menuTree, setMenuTree] = useState<MenuItem[]>([]); const [menuTree, setMenuTree] = useState<MenuItem[]>([]);
const [checkedPermissionIds, setCheckedPermissionIds] = useState<number[]>([]); const [checkedPermissionIds, setCheckedPermissionIds] = useState<number[]>([]);
const [searchText, setSearchText] = useState('');
useEffect(() => { useEffect(() => {
const loadPermissionTree = async () => { const loadPermissionTree = async () => {
@ -135,6 +136,45 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
} }
}; };
// 筛选菜单树(根据搜索文本)
const filteredMenuTree = useMemo(() => {
if (!searchText.trim()) return menuTree;
const filterMenu = (menu: MenuItem): MenuItem | null => {
const matchesName = menu.name.toLowerCase().includes(searchText.toLowerCase());
const matchingPermissions = menu.permissions?.filter(p =>
p.name.toLowerCase().includes(searchText.toLowerCase())
) || [];
const matchingChildren = menu.permissionChildren
?.map(child => filterMenu(child))
.filter(Boolean) as MenuItem[] || [];
if (matchesName || matchingPermissions.length > 0 || matchingChildren.length > 0) {
return {
...menu,
permissions: matchingPermissions,
permissionChildren: matchingChildren
};
}
return null;
};
return menuTree.map(menu => filterMenu(menu)).filter(Boolean) as MenuItem[];
}, [menuTree, searchText]);
// 统计信息
const statistics = useMemo(() => {
const countPermissions = (menus: MenuItem[]): number => {
return menus.reduce((sum, menu) => {
return sum + (menu.permissions?.length || 0) + countPermissions(menu.permissionChildren || []);
}, 0);
};
const total = countPermissions(menuTree);
const selected = checkedPermissionIds.length;
const percentage = total > 0 ? Math.round((selected / total) * 100) : 0;
return { total, selected, percentage };
}, [menuTree, checkedPermissionIds]);
// 渲染菜单项 // 渲染菜单项
const renderMenuItem = (menu: MenuItem, level: number = 0): React.ReactNode => { const renderMenuItem = (menu: MenuItem, level: number = 0): React.ReactNode => {
const hasChildren = menu.permissionChildren && menu.permissionChildren.length > 0; const hasChildren = menu.permissionChildren && menu.permissionChildren.length > 0;
@ -147,7 +187,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
} }
return ( return (
<div key={menu.id} className={level > 0 ? 'ml-4' : ''}> <div key={menu.id} className={level > 0 ? 'ml-4' : 'mb-2'}>
{hasChildren ? ( {hasChildren ? (
<AccordionItem value={`menu-${menu.id}`} className="border-b-0"> <AccordionItem value={`menu-${menu.id}`} className="border-b-0">
<AccordionTrigger className="py-2 hover:no-underline hover:bg-muted/50 px-2 rounded"> <AccordionTrigger className="py-2 hover:no-underline hover:bg-muted/50 px-2 rounded">
@ -163,16 +203,16 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
onCheckedChange={() => handleToggleMenu(menu)} onCheckedChange={() => handleToggleMenu(menu)}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
<Folder className="h-4 w-4 text-blue-500" /> <Folder className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium">{menu.name}</span> <span className="text-sm font-medium">{menu.name}</span>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="pb-0 pt-2"> <AccordionContent className="pb-0 pt-2">
{/* 渲染当前菜单的权限 */} {/* 渲染当前菜单的权限 */}
{hasPermissions && ( {hasPermissions && (
<div className="space-y-2 mb-2 ml-8"> <div className="grid grid-cols-4 gap-x-3 gap-y-1 mb-2 ml-8">
{menu.permissions.map(permission => ( {menu.permissions.map(permission => (
<div key={permission.id} className="flex items-center gap-2 py-1"> <div key={permission.id} className="flex items-center gap-1.5 py-1.5 px-2 rounded hover:bg-muted/50">
<Checkbox <Checkbox
id={`permission-${permission.id}`} id={`permission-${permission.id}`}
checked={checkedPermissionIds.includes(permission.id)} checked={checkedPermissionIds.includes(permission.id)}
@ -180,10 +220,10 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
/> />
<label <label
htmlFor={`permission-${permission.id}`} htmlFor={`permission-${permission.id}`}
className="text-sm cursor-pointer flex items-center gap-2" className="text-sm cursor-pointer flex items-center gap-1.5 flex-1"
> >
<Key className="h-3 w-3 text-muted-foreground" /> <Key className="h-3 w-3 text-muted-foreground flex-shrink-0" />
{permission.name} <span className="truncate">{permission.name}</span>
</label> </label>
</div> </div>
))} ))}
@ -209,13 +249,13 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
}} }}
onCheckedChange={() => handleToggleMenu(menu)} onCheckedChange={() => handleToggleMenu(menu)}
/> />
<Folder className="h-4 w-4 text-blue-500" /> <Folder className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium">{menu.name}</span> <span className="text-sm font-medium">{menu.name}</span>
</div> </div>
{hasPermissions && ( {hasPermissions && (
<div className="space-y-2 ml-8"> <div className="grid grid-cols-4 gap-x-3 gap-y-1 ml-8">
{menu.permissions.map(permission => ( {menu.permissions.map(permission => (
<div key={permission.id} className="flex items-center gap-2 py-1"> <div key={permission.id} className="flex items-center gap-1.5 py-1.5 px-2 rounded hover:bg-muted/50">
<Checkbox <Checkbox
id={`permission-${permission.id}`} id={`permission-${permission.id}`}
checked={checkedPermissionIds.includes(permission.id)} checked={checkedPermissionIds.includes(permission.id)}
@ -223,10 +263,10 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
/> />
<label <label
htmlFor={`permission-${permission.id}`} htmlFor={`permission-${permission.id}`}
className="text-sm cursor-pointer flex items-center gap-2" className="text-sm cursor-pointer flex items-center gap-1.5 flex-1"
> >
<Key className="h-3 w-3 text-muted-foreground" /> <Key className="h-3 w-3 text-muted-foreground flex-shrink-0" />
{permission.name} <span className="truncate">{permission.name}</span>
</label> </label>
</div> </div>
))} ))}
@ -252,43 +292,75 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[80vh]"> <DialogContent className="sm:max-w-[700px] max-h-[85vh] overflow-hidden flex flex-col p-0">
<DialogHeader> <DialogHeader className="px-6 pt-6 pb-4 border-b">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center justify-between pr-8">
<KeyRound className="h-5 w-5" /> <span></span>
{!loading && (
<span className="text-sm font-normal text-muted-foreground">
<span className="font-semibold text-foreground">{statistics.selected}</span> / {statistics.total}
</span>
)}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="overflow-y-auto max-h-[500px] pr-2">
{loading ? ( {/* 搜索框 */}
<div className="flex items-center justify-center py-12"> {!loading && menuTree.length > 0 && (
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <div className="px-6 py-3 border-b bg-muted/30">
<span className="text-sm text-muted-foreground">...</span> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索权限名称或菜单名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="pl-10"
/>
</div> </div>
) : menuTree.length > 0 ? ( </div>
)}
{/* 权限树 */}
<div className="overflow-y-auto flex-1 px-6 py-4">
{loading ? (
<div className="flex flex-col items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground mb-3" />
<span className="text-sm text-muted-foreground">...</span>
</div>
) : filteredMenuTree.length > 0 ? (
<Accordion type="multiple" className="w-full"> <Accordion type="multiple" className="w-full">
{menuTree.map(menu => renderMenuItem(menu))} {filteredMenuTree.map(menu => renderMenuItem(menu))}
</Accordion> </Accordion>
) : searchText ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Search className="h-12 w-12 mb-3 opacity-30" />
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
) : ( ) : (
<div className="flex items-center justify-center py-12 text-muted-foreground"> <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Shield className="h-12 w-12 mb-3 opacity-30" />
<p className="text-sm"></p>
</div> </div>
)} )}
</div> </div>
<DialogFooter>
<div className="flex items-center justify-between w-full"> <div className="px-6 py-4 border-t bg-muted/30 flex justify-end gap-2">
<span className="text-sm text-muted-foreground"> <Button
{checkedPermissionIds.length} type="button"
</span> variant="outline"
<div className="flex gap-2"> onClick={() => onOpenChange(false)}
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}> disabled={submitting}
>
</Button>
<Button onClick={handleSubmit} disabled={submitting || loading}> </Button>
{submitting ? '保存中...' : '确定'} <Button
</Button> type="button"
</div> onClick={handleSubmit}
</div> disabled={submitting || loading}
</DialogFooter> >
{submitting ? '保存中...' : '确定'}
</Button>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -153,7 +153,7 @@ const TagDialog: React.FC<TagDialogProps> = ({
<Settings className="h-5 w-5" /> <Settings className="h-5 w-5" />
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="px-6 py-4">
<div className="mb-4"> <div className="mb-4">
<Button onClick={handleAdd}> <Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
@ -239,7 +239,7 @@ const TagDialog: React.FC<TagDialogProps> = ({
<DialogHeader> <DialogHeader>
<DialogTitle>{editingTag ? '编辑标签' : '新建标签'}</DialogTitle> <DialogTitle>{editingTag ? '编辑标签' : '新建标签'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name"> *</Label> <Label htmlFor="name"> *</Label>
<Input <Input

View File

@ -60,7 +60,7 @@ const AssignRolesDialog: React.FC<AssignRolesDialogProps> = ({
<Users className="h-5 w-5" /> <Users className="h-5 w-5" />
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
"<span className="font-semibold text-foreground">{record.username}</span>" "<span className="font-semibold text-foreground">{record.username}</span>"
</div> </div>

View File

@ -31,7 +31,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[450px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600"> <DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" /> <AlertCircle className="h-5 w-5" />
@ -41,7 +41,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground"> <div className="px-6 py-4 space-y-2 text-sm text-muted-foreground">
{record.nickname && ( {record.nickname && (
<p> <p>
<span className="font-medium">:</span> {record.nickname} <span className="font-medium">:</span> {record.nickname}

View File

@ -142,7 +142,7 @@ const EditModal: React.FC<EditModalProps> = ({
<DialogHeader> <DialogHeader>
<DialogTitle>{record ? '编辑用户' : '新增用户'}</DialogTitle> <DialogTitle>{record ? '编辑用户' : '新增用户'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="username"> *</Label> <Label htmlFor="username"> *</Label>
<Input <Input

View File

@ -68,7 +68,7 @@ const ResetPasswordDialog: React.FC<ResetPasswordDialogProps> = ({
<KeyRound className="h-5 w-5" /> <KeyRound className="h-5 w-5" />
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 px-6 py-4">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
"<span className="font-semibold text-foreground">{record.username}</span>" "<span className="font-semibold text-foreground">{record.username}</span>"
</div> </div>

View File

@ -52,7 +52,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600"> <DialogTitle className="flex items-center gap-2 text-red-600">
<AlertCircle className="h-5 w-5" /> <AlertCircle className="h-5 w-5" />
@ -62,7 +62,7 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 text-sm text-muted-foreground"> <div className="px-6 py-4 space-y-2 text-sm text-muted-foreground">
<p> <p>
<span className="font-medium">:</span>{' '} <span className="font-medium">:</span>{' '}
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"> <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">

View File

@ -199,7 +199,7 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
<DialogHeader> <DialogHeader>
<DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle> <DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-6 py-4"> <div className="grid gap-6 px-6 py-4">
{/* 流程分类 */} {/* 流程分类 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="categoryId"> <Label htmlFor="categoryId">