增加审批组件
This commit is contained in:
parent
2f4ff4b6bd
commit
ef3e34b85d
361
frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md
Normal file
361
frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
# FormPreview 使用指南
|
||||||
|
|
||||||
|
## 📖 概述
|
||||||
|
|
||||||
|
`FormPreview` 是一个独立的表单预览组件,从 `FormDesigner` 中提取出来,可在任何地方使用。它支持完整的表单交互、验证和联动规则。
|
||||||
|
|
||||||
|
## ✨ 特点
|
||||||
|
|
||||||
|
- ✅ **完全独立**:可在任何页面使用,不依赖 FormDesigner
|
||||||
|
- ✅ **支持所有字段类型**:input, select, textarea, date, grid, cascader 等
|
||||||
|
- ✅ **支持验证规则**:required, pattern, min/max, email, phone 等
|
||||||
|
- ✅ **支持联动规则**:显示/隐藏、禁用/启用、必填控制、值联动
|
||||||
|
- ✅ **支持动态数据源**:API 数据源、预定义数据源
|
||||||
|
- ✅ **非受控组件**:内部管理状态,无冲突
|
||||||
|
- ✅ **简单易用**:只需传入 `fields` 和 `formConfig`
|
||||||
|
|
||||||
|
## 📦 导入
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FormPreview, FormPreviewRef } from '@/components/FormDesigner';
|
||||||
|
import type { FieldConfig, FormConfig } from '@/components/FormDesigner';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 基础用法
|
||||||
|
|
||||||
|
### 1. 最简单的使用
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { FormPreview, FormPreviewRef } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const previewRef = useRef<FormPreviewRef>(null);
|
||||||
|
|
||||||
|
// 从后端获取表单定义
|
||||||
|
const formSchema = await getFormDefinitionById(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormPreview
|
||||||
|
ref={previewRef}
|
||||||
|
fields={formSchema.fields}
|
||||||
|
formConfig={formSchema.formConfig}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 Modal 中使用
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from 'antd';
|
||||||
|
import { FormPreview } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
const FormPreviewModal = ({ open, onClose, formDefinition }) => {
|
||||||
|
if (!formDefinition) return null;
|
||||||
|
|
||||||
|
const { schema } = formDefinition;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`预览表单:${formDefinition.name}`}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={null}
|
||||||
|
width={schema.formConfig.formWidth || 600}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
maxHeight: '70vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: 0,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormPreview
|
||||||
|
fields={schema.fields}
|
||||||
|
formConfig={schema.formConfig}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用 Ref 方法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { FormPreview, FormPreviewRef } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const previewRef = useRef<FormPreviewRef>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 触发表单验证和提交
|
||||||
|
await previewRef.current?.submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
// 重置表单
|
||||||
|
previewRef.current?.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormPreview
|
||||||
|
ref={previewRef}
|
||||||
|
fields={fields}
|
||||||
|
formConfig={formConfig}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Button type="primary" onClick={handleSubmit}>提交</Button>
|
||||||
|
<Button onClick={handleReset} style={{ marginLeft: 8 }}>重置</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Props 说明
|
||||||
|
|
||||||
|
### FormPreviewProps
|
||||||
|
|
||||||
|
| 属性 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `fields` | `FieldConfig[]` | ✅ | 字段列表 |
|
||||||
|
| `formConfig` | `FormConfig` | ✅ | 表单配置 |
|
||||||
|
|
||||||
|
### FormPreviewRef 方法
|
||||||
|
|
||||||
|
| 方法 | 参数 | 返回值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `submit()` | 无 | `Promise<void>` | 提交表单(触发验证) |
|
||||||
|
| `reset()` | 无 | `void` | 重置表单 |
|
||||||
|
|
||||||
|
## 🔧 数据格式
|
||||||
|
|
||||||
|
### FormConfig 示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const formConfig: FormConfig = {
|
||||||
|
title: "部署申请表单", // 表单标题(可选)
|
||||||
|
formWidth: 600, // 表单弹窗宽度,单位px
|
||||||
|
labelAlign: "right", // 标签对齐:left | right | top
|
||||||
|
size: "middle" // 表单尺寸:small | middle | large
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields 示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fields: FieldConfig[] = [
|
||||||
|
// 基础输入框
|
||||||
|
{
|
||||||
|
id: "field_1",
|
||||||
|
type: "input",
|
||||||
|
label: "应用名称",
|
||||||
|
name: "appName",
|
||||||
|
placeholder: "请输入应用名称",
|
||||||
|
required: true,
|
||||||
|
validationRules: [
|
||||||
|
{ type: "required", message: "应用名称不能为空" },
|
||||||
|
{ type: "maxLength", value: 50, message: "不能超过50个字符" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下拉选择(静态选项)
|
||||||
|
{
|
||||||
|
id: "field_2",
|
||||||
|
type: "select",
|
||||||
|
label: "环境选择",
|
||||||
|
name: "environment",
|
||||||
|
required: true,
|
||||||
|
dataSourceType: "static",
|
||||||
|
options: [
|
||||||
|
{ label: "开发环境", value: "dev" },
|
||||||
|
{ label: "测试环境", value: "test" },
|
||||||
|
{ label: "生产环境", value: "prod" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下拉选择(API 数据源)
|
||||||
|
{
|
||||||
|
id: "field_3",
|
||||||
|
type: "select",
|
||||||
|
label: "Jenkins服务器",
|
||||||
|
name: "jenkinsServer",
|
||||||
|
required: true,
|
||||||
|
dataSourceType: "api",
|
||||||
|
apiDataSource: {
|
||||||
|
url: "/api/v1/jenkins-servers/list",
|
||||||
|
method: "GET",
|
||||||
|
params: { enabled: true },
|
||||||
|
labelField: "name",
|
||||||
|
valueField: "id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下拉选择(预定义数据源)
|
||||||
|
{
|
||||||
|
id: "field_4",
|
||||||
|
type: "select",
|
||||||
|
label: "项目组",
|
||||||
|
name: "projectGroup",
|
||||||
|
required: true,
|
||||||
|
dataSourceType: "predefined",
|
||||||
|
predefinedDataSource: {
|
||||||
|
sourceType: "PROJECT_GROUPS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 栅格布局
|
||||||
|
{
|
||||||
|
id: "field_5",
|
||||||
|
type: "grid",
|
||||||
|
label: "基础信息",
|
||||||
|
name: "grid_basic",
|
||||||
|
columns: 2,
|
||||||
|
columnSpans: [12, 12],
|
||||||
|
gutter: 16,
|
||||||
|
children: [
|
||||||
|
// 第一列
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "field_6",
|
||||||
|
type: "input",
|
||||||
|
label: "创建人",
|
||||||
|
name: "creator",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// 第二列
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "field_7",
|
||||||
|
type: "date",
|
||||||
|
label: "创建日期",
|
||||||
|
name: "createDate",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌟 实际应用场景
|
||||||
|
|
||||||
|
### 场景1:工作流设计页面预览启动表单
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/pages/Workflow/Design/components/FormPreviewModal.tsx
|
||||||
|
import { FormPreview } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
const FormPreviewModal = ({ formDefinition, open, onClose }) => {
|
||||||
|
const { schema } = formDefinition;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onCancel={onClose} width={schema.formConfig.formWidth}>
|
||||||
|
<FormPreview
|
||||||
|
fields={schema.fields}
|
||||||
|
formConfig={schema.formConfig}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2:表单设计器内部预览
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/FormDesigner/Designer.tsx
|
||||||
|
const FormDesigner = () => {
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
const [fields, setFields] = useState<FieldConfig[]>([]);
|
||||||
|
const [formConfig, setFormConfig] = useState<FormConfig>({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setPreviewVisible(true)}>预览</Button>
|
||||||
|
|
||||||
|
<Modal open={previewVisible} onCancel={() => setPreviewVisible(false)}>
|
||||||
|
<FormPreview fields={fields} formConfig={formConfig} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3:独立的表单预览页面
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/pages/Form/Preview/index.tsx
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { FormPreview } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
const FormPreviewPage = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [formSchema, setFormSchema] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadForm = async () => {
|
||||||
|
const data = await getFormDefinitionById(id);
|
||||||
|
setFormSchema(data.schema);
|
||||||
|
};
|
||||||
|
loadForm();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (!formSchema) return <div>加载中...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<h1>表单预览</h1>
|
||||||
|
<FormPreview
|
||||||
|
fields={formSchema.fields}
|
||||||
|
formConfig={formSchema.formConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 重要说明
|
||||||
|
|
||||||
|
### 1. 非受控组件
|
||||||
|
|
||||||
|
`FormPreview` 是**非受控组件**,内部通过 `Ant Design Form` 管理状态:
|
||||||
|
|
||||||
|
- ✅ **不要**传递 `value` 和 `onChange`(会导致冲突)
|
||||||
|
- ✅ **不要**尝试从外部控制字段值
|
||||||
|
- ✅ 使用 `ref` 方法获取表单值或提交表单
|
||||||
|
|
||||||
|
### 2. 数据格式统一
|
||||||
|
|
||||||
|
无论在哪里使用,数据格式都是一致的:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<FormPreview
|
||||||
|
fields={formSchema.fields} // FieldConfig[]
|
||||||
|
formConfig={formSchema.formConfig} // FormConfig
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 与 FormRenderer 的区别
|
||||||
|
|
||||||
|
| 特性 | FormPreview | FormRenderer |
|
||||||
|
|------|-------------|--------------|
|
||||||
|
| 推荐使用 | ✅ 是 | ❌ 已废弃 |
|
||||||
|
| 受控模式 | ❌ 非受控 | ⚠️ 部分受控(有问题) |
|
||||||
|
| 下拉框可选中 | ✅ 正常 | ❌ 有bug |
|
||||||
|
| 使用场景 | 预览、交互 | 已废弃 |
|
||||||
|
|
||||||
|
**建议**:所有新代码都使用 `FormPreview`,不要再使用 `FormRenderer`。
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- [FormDesigner 文档](./README.md)
|
||||||
|
- [字段类型定义](./types.ts)
|
||||||
|
- [工作流扩展字段](./extensions/workflow/README.md)
|
||||||
|
|
||||||
508
frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md
Normal file
508
frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md
Normal file
@ -0,0 +1,508 @@
|
|||||||
|
# FormRenderer 生命周期钩子指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`FormRenderer` 提供了完整的表单生命周期钩子,允许你在表单提交的各个阶段插入自定义逻辑。
|
||||||
|
|
||||||
|
## 钩子执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点击提交
|
||||||
|
↓
|
||||||
|
1. 表单验证 (Form.validateFields)
|
||||||
|
↓ (验证失败)
|
||||||
|
onError ← 触发
|
||||||
|
↓ (验证通过)
|
||||||
|
2. beforeSubmit 钩子
|
||||||
|
↓ (返回 false 或抛出错误)
|
||||||
|
onError ← 触发 (可选)
|
||||||
|
↓ (返回修改后的数据或继续)
|
||||||
|
3. onSubmit 钩子 (执行实际提交)
|
||||||
|
↓ (提交失败)
|
||||||
|
onError ← 触发
|
||||||
|
↓ (提交成功)
|
||||||
|
4. afterSubmit 钩子
|
||||||
|
↓
|
||||||
|
完成
|
||||||
|
```
|
||||||
|
|
||||||
|
## 钩子详解
|
||||||
|
|
||||||
|
### 1. beforeSubmit
|
||||||
|
|
||||||
|
**触发时机**: 表单验证通过后,实际提交前
|
||||||
|
|
||||||
|
**作用**:
|
||||||
|
- 预处理表单数据(格式化、转换等)
|
||||||
|
- 添加额外字段(如时间戳、用户 ID)
|
||||||
|
- 条件判断,取消提交
|
||||||
|
|
||||||
|
**签名**:
|
||||||
|
```typescript
|
||||||
|
beforeSubmit?: (values: Record<string, any>) =>
|
||||||
|
Record<string, any> | false | Promise<Record<string, any> | false>
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
- `Record<string, any>` - 修改后的数据,将传递给 `onSubmit`
|
||||||
|
- `false` - 取消提交,不执行 `onSubmit` 和 `afterSubmit`
|
||||||
|
- 抛出异常 - 触发 `onError`,中断提交流程
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```tsx
|
||||||
|
const handleBeforeSubmit = async (values) => {
|
||||||
|
// 数据预处理
|
||||||
|
return {
|
||||||
|
...values,
|
||||||
|
email: values.email.toLowerCase(),
|
||||||
|
submitTime: new Date().toISOString(),
|
||||||
|
userId: getCurrentUserId(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 条件取消提交
|
||||||
|
const handleBeforeSubmitWithCondition = async (values) => {
|
||||||
|
if (!values.agreeToTerms) {
|
||||||
|
message.warning('请先同意服务条款');
|
||||||
|
return false; // 取消提交
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
<FormRenderer
|
||||||
|
schema={schema}
|
||||||
|
beforeSubmit={handleBeforeSubmit}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. onSubmit
|
||||||
|
|
||||||
|
**触发时机**: `beforeSubmit` 执行完成后(如果有)
|
||||||
|
|
||||||
|
**作用**:
|
||||||
|
- 执行实际的提交逻辑
|
||||||
|
- 调用后端 API
|
||||||
|
- 返回提交结果
|
||||||
|
|
||||||
|
**签名**:
|
||||||
|
```typescript
|
||||||
|
onSubmit?: (values: Record<string, any>) => void | Promise<any>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `values` - 如果有 `beforeSubmit`,则是其返回的数据;否则是原始表单数据
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
- 返回的结果会传递给 `afterSubmit`
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```tsx
|
||||||
|
const handleSubmit = async (data) => {
|
||||||
|
try {
|
||||||
|
const response = await api.createUser(data);
|
||||||
|
message.success('用户创建成功!');
|
||||||
|
return response; // 返回结果给 afterSubmit
|
||||||
|
} catch (error) {
|
||||||
|
// 错误会被捕获,触发 onError
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
<FormRenderer
|
||||||
|
schema={schema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. afterSubmit
|
||||||
|
|
||||||
|
**触发时机**: `onSubmit` 成功完成后
|
||||||
|
|
||||||
|
**作用**:
|
||||||
|
- 提交成功后的后续操作
|
||||||
|
- 页面跳转
|
||||||
|
- 刷新列表
|
||||||
|
- 显示成功提示
|
||||||
|
|
||||||
|
**签名**:
|
||||||
|
```typescript
|
||||||
|
afterSubmit?: (response?: any) => void | Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `response` - `onSubmit` 的返回值
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- `afterSubmit` 的错误不会中断提交流程,只会记录日志
|
||||||
|
- 即使 `afterSubmit` 抛出异常,提交仍然被视为成功
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```tsx
|
||||||
|
const handleAfterSubmit = async (response) => {
|
||||||
|
// 跳转到详情页
|
||||||
|
if (response?.id) {
|
||||||
|
navigate(`/users/${response.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
await refreshUserList();
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
setModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
<FormRenderer
|
||||||
|
schema={schema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
afterSubmit={handleAfterSubmit}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. onError
|
||||||
|
|
||||||
|
**触发时机**:
|
||||||
|
- 表单验证失败
|
||||||
|
- `beforeSubmit` 抛出异常
|
||||||
|
- `onSubmit` 抛出异常
|
||||||
|
|
||||||
|
**作用**:
|
||||||
|
- 统一的错误处理
|
||||||
|
- 错误上报
|
||||||
|
- 自定义错误提示
|
||||||
|
|
||||||
|
**签名**:
|
||||||
|
```typescript
|
||||||
|
onError?: (error: any) => void | Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `error` - 错误对象(可能是验证错误、网络错误等)
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```tsx
|
||||||
|
const handleError = async (error) => {
|
||||||
|
// 错误上报
|
||||||
|
reportError({
|
||||||
|
type: 'form_submit_error',
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义错误提示
|
||||||
|
if (error.code === 'DUPLICATE_EMAIL') {
|
||||||
|
message.error('该邮箱已被注册,请使用其他邮箱');
|
||||||
|
} else {
|
||||||
|
message.error(`提交失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
<FormRenderer
|
||||||
|
schema={schema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const UserCreateForm: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 1️⃣ beforeSubmit: 数据预处理
|
||||||
|
const handleBeforeSubmit = async (values: Record<string, any>) => {
|
||||||
|
console.log('📝 原始表单数据:', values);
|
||||||
|
|
||||||
|
// 数据格式化
|
||||||
|
const processedValues = {
|
||||||
|
...values,
|
||||||
|
email: values.email?.toLowerCase(),
|
||||||
|
phone: values.phone?.replace(/\s/g, ''),
|
||||||
|
submitTime: new Date().toISOString(),
|
||||||
|
submittedBy: getCurrentUserId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 条件验证
|
||||||
|
if (values.age < 18) {
|
||||||
|
message.warning('用户年龄必须大于18岁');
|
||||||
|
return false; // 取消提交
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 处理后的数据:', processedValues);
|
||||||
|
return processedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2️⃣ onSubmit: 执行提交
|
||||||
|
const handleSubmit = async (data: Record<string, any>) => {
|
||||||
|
console.log('📤 提交到服务器:', data);
|
||||||
|
|
||||||
|
// 调用 API
|
||||||
|
const response = await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('✅ 服务器响应:', result);
|
||||||
|
message.success('用户创建成功!');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3️⃣ afterSubmit: 提交成功后的处理
|
||||||
|
const handleAfterSubmit = async (response: any) => {
|
||||||
|
console.log('🎉 提交完成,开始后续处理:', response);
|
||||||
|
|
||||||
|
// 延迟跳转
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// 跳转到用户详情页
|
||||||
|
navigate(`/users/${response.data.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4️⃣ onError: 错误处理
|
||||||
|
const handleError = async (error: any) => {
|
||||||
|
console.error('❌ 提交出错:', error);
|
||||||
|
|
||||||
|
// 上报错误
|
||||||
|
await reportError({
|
||||||
|
type: 'user_create_error',
|
||||||
|
message: error.message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义错误提示
|
||||||
|
if (error.message.includes('duplicate')) {
|
||||||
|
message.error('该邮箱已被注册');
|
||||||
|
} else if (error.message.includes('network')) {
|
||||||
|
message.error('网络错误,请稍后重试');
|
||||||
|
} else {
|
||||||
|
message.error(`创建失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormRenderer
|
||||||
|
schema={formSchema}
|
||||||
|
beforeSubmit={handleBeforeSubmit}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
afterSubmit={handleAfterSubmit}
|
||||||
|
onError={handleError}
|
||||||
|
showSubmit
|
||||||
|
showCancel
|
||||||
|
submitText="创建用户"
|
||||||
|
cancelText="取消"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见场景
|
||||||
|
|
||||||
|
### 场景1: 添加额外字段
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleBeforeSubmit = async (values) => {
|
||||||
|
return {
|
||||||
|
...values,
|
||||||
|
createdBy: getCurrentUser().id,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
source: 'web',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2: 数据格式转换
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleBeforeSubmit = async (values) => {
|
||||||
|
return {
|
||||||
|
...values,
|
||||||
|
// 日期对象转 ISO 字符串
|
||||||
|
startDate: values.startDate?.toISOString(),
|
||||||
|
endDate: values.endDate?.toISOString(),
|
||||||
|
// 数组转逗号分隔字符串
|
||||||
|
tags: values.tags?.join(','),
|
||||||
|
// 金额转分(后端存储)
|
||||||
|
amount: Math.round(values.amount * 100),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3: 二次确认
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleBeforeSubmit = async (values) => {
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认提交',
|
||||||
|
content: `确定要创建用户 "${values.username}" 吗?`,
|
||||||
|
onOk: () => resolve(true),
|
||||||
|
onCancel: () => resolve(false),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return confirmed ? values : false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4: 提交后跳转或刷新
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleAfterSubmit = async (response) => {
|
||||||
|
// 方式1: 跳转到列表页
|
||||||
|
navigate('/users');
|
||||||
|
|
||||||
|
// 方式2: 跳转到详情页
|
||||||
|
navigate(`/users/${response.id}`);
|
||||||
|
|
||||||
|
// 方式3: 刷新当前页面数据
|
||||||
|
await refreshData();
|
||||||
|
|
||||||
|
// 方式4: 关闭弹窗
|
||||||
|
setModalVisible(false);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景5: 错误分类处理
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleError = async (error) => {
|
||||||
|
if (error.code === 400) {
|
||||||
|
message.error('请求参数错误,请检查输入');
|
||||||
|
} else if (error.code === 401) {
|
||||||
|
message.error('登录已过期,请重新登录');
|
||||||
|
navigate('/login');
|
||||||
|
} else if (error.code === 403) {
|
||||||
|
message.error('没有权限执行此操作');
|
||||||
|
} else if (error.code === 409) {
|
||||||
|
message.error('数据冲突,请刷新后重试');
|
||||||
|
} else if (error.code >= 500) {
|
||||||
|
message.error('服务器错误,请联系管理员');
|
||||||
|
} else {
|
||||||
|
message.error(`提交失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### ✅ 推荐
|
||||||
|
|
||||||
|
1. **明确职责分离**
|
||||||
|
```tsx
|
||||||
|
// ✅ beforeSubmit 只做数据处理
|
||||||
|
beforeSubmit: (values) => ({ ...values, email: values.email.toLowerCase() })
|
||||||
|
|
||||||
|
// ✅ onSubmit 只做网络请求
|
||||||
|
onSubmit: (data) => api.createUser(data)
|
||||||
|
|
||||||
|
// ✅ afterSubmit 只做后续操作
|
||||||
|
afterSubmit: () => navigate('/success')
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用 async/await**
|
||||||
|
```tsx
|
||||||
|
// ✅ 清晰的异步处理
|
||||||
|
const handleSubmit = async (data) => {
|
||||||
|
const response = await api.createUser(data);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **统一错误处理**
|
||||||
|
```tsx
|
||||||
|
// ✅ 在 onError 中统一处理
|
||||||
|
onError: (error) => {
|
||||||
|
reportError(error);
|
||||||
|
showErrorMessage(error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 避免
|
||||||
|
|
||||||
|
1. **不要在 beforeSubmit 中做网络请求**
|
||||||
|
```tsx
|
||||||
|
// ❌ 不推荐
|
||||||
|
beforeSubmit: async (values) => {
|
||||||
|
await api.checkDuplicate(values.email); // 应该在 onSubmit 中做
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **不要在 afterSubmit 中处理错误**
|
||||||
|
```tsx
|
||||||
|
// ❌ 错误应该在 onError 中处理
|
||||||
|
afterSubmit: (response) => {
|
||||||
|
if (!response.success) {
|
||||||
|
message.error('失败'); // 应该在 onError 中做
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **避免循环依赖**
|
||||||
|
```tsx
|
||||||
|
// ❌ 不要在钩子中再次触发提交
|
||||||
|
afterSubmit: () => {
|
||||||
|
form.submit(); // 会导致无限循环
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const handleBeforeSubmit = async (values) => {
|
||||||
|
console.group('🎣 beforeSubmit');
|
||||||
|
console.log('原始数据:', values);
|
||||||
|
const processed = processData(values);
|
||||||
|
console.log('处理后:', processed);
|
||||||
|
console.groupEnd();
|
||||||
|
return processed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data) => {
|
||||||
|
console.group('📤 onSubmit');
|
||||||
|
console.log('提交数据:', data);
|
||||||
|
try {
|
||||||
|
const response = await api.submit(data);
|
||||||
|
console.log('响应:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('错误:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAfterSubmit = async (response) => {
|
||||||
|
console.log('🎉 afterSubmit:', response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = async (error) => {
|
||||||
|
console.error('❌ onError:', error);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
生命周期钩子让你能够在表单提交的各个阶段插入自定义逻辑:
|
||||||
|
|
||||||
|
- **beforeSubmit**: 预处理数据,可取消提交
|
||||||
|
- **onSubmit**: 执行实际提交
|
||||||
|
- **afterSubmit**: 提交成功后的处理
|
||||||
|
- **onError**: 统一的错误处理
|
||||||
|
|
||||||
|
合理使用这些钩子,可以构建健壮、灵活的表单提交流程。
|
||||||
|
|
||||||
@ -1,13 +1,41 @@
|
|||||||
/**
|
/**
|
||||||
* 表单渲染器组件(运行时)
|
* 表单渲染器组件(运行时)
|
||||||
* 根据 Schema 渲染可交互的表单
|
* 用于真实业务场景的表单渲染和提交
|
||||||
|
*
|
||||||
|
* 特点:
|
||||||
|
* - ✅ 支持所有表单字段类型
|
||||||
|
* - ✅ 支持验证规则和联动规则
|
||||||
|
* - ✅ 支持动态数据源(API、预定义)
|
||||||
|
* - ✅ 非受控组件,内部管理状态
|
||||||
|
* - ✅ 支持生命周期钩子(后续补充)
|
||||||
|
* - ✅ 用于生产环境的真实表单提交
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* // 使用示例
|
||||||
|
* import { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
*
|
||||||
|
* const MyComponent = () => {
|
||||||
|
* const formSchema = await getFormDefinitionById(id);
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <FormRenderer
|
||||||
|
* fields={formSchema.fields}
|
||||||
|
* formConfig={formSchema.formConfig}
|
||||||
|
* onSubmit={async (values) => {
|
||||||
|
* await api.submitForm(values);
|
||||||
|
* }}
|
||||||
|
* />
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { Form, Button, message } from 'antd';
|
import { Form, message } from 'antd';
|
||||||
import type { Rule } from 'antd/es/form';
|
import type { Rule } from 'antd/es/form';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { FieldConfig, FormSchema, ValidationRule, LinkageRule } from './types';
|
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule, FormSchema } from './types';
|
||||||
import type { ComponentMeta } from './config';
|
import type { ComponentMeta } from './config';
|
||||||
import { COMPONENT_LIST } from './config';
|
import { COMPONENT_LIST } from './config';
|
||||||
import { ComponentsContext } from './Designer';
|
import { ComponentsContext } from './Designer';
|
||||||
@ -15,136 +43,179 @@ import FieldRenderer from './components/FieldRenderer';
|
|||||||
import GridFieldPreview from './components/GridFieldPreview';
|
import GridFieldPreview from './components/GridFieldPreview';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单渲染器组件的 Props
|
||||||
|
*/
|
||||||
export interface FormRendererProps {
|
export interface FormRendererProps {
|
||||||
schema: FormSchema; // 表单 Schema
|
/** 表单 Schema(包含 fields 和 formConfig) */
|
||||||
extraComponents?: ComponentMeta[]; // 🆕 扩展组件(如工作流字段)
|
schema?: FormSchema;
|
||||||
value?: Record<string, any>; // 表单值(受控)
|
/** 字段列表(与 schema 二选一) */
|
||||||
onChange?: (values: Record<string, any>) => void; // 值变化回调
|
fields?: FieldConfig[];
|
||||||
onSubmit?: (values: Record<string, any>) => void | Promise<any>; // 提交回调
|
/** 表单配置(与 schema 二选一) */
|
||||||
onCancel?: () => void; // 取消回调
|
formConfig?: FormConfig;
|
||||||
readonly?: boolean; // 只读模式
|
/** 🆕 扩展组件(如工作流字段) */
|
||||||
showSubmit?: boolean; // 是否显示提交按钮
|
extraComponents?: ComponentMeta[];
|
||||||
showCancel?: boolean; // 是否显示取消按钮
|
/** 表单初始值(仅用于初始化,不要作为受控属性使用) */
|
||||||
submitText?: string; // 提交按钮文本
|
value?: Record<string, any>;
|
||||||
cancelText?: string; // 取消按钮文本
|
/** 值变化回调(父组件可监听,但不应将值传回 value) */
|
||||||
// 🎣 生命周期钩子
|
onChange?: (values: Record<string, any>) => void;
|
||||||
beforeSubmit?: (values: Record<string, any>) => Record<string, any> | false | Promise<Record<string, any> | false>; // 提交前钩子
|
/** 提交回调 */
|
||||||
afterSubmit?: (response?: any) => void | Promise<void>; // 提交成功后钩子
|
onSubmit?: (values: Record<string, any>) => void | Promise<any>;
|
||||||
onError?: (error: any) => void | Promise<void>; // 提交失败钩子
|
/** 取消回调 */
|
||||||
|
onCancel?: () => void;
|
||||||
|
/** 只读模式 */
|
||||||
|
readonly?: boolean;
|
||||||
|
/** 是否显示提交按钮 */
|
||||||
|
showSubmit?: boolean;
|
||||||
|
/** 是否显示取消按钮 */
|
||||||
|
showCancel?: boolean;
|
||||||
|
/** 提交按钮文本 */
|
||||||
|
submitText?: string;
|
||||||
|
/** 取消按钮文本 */
|
||||||
|
cancelText?: string;
|
||||||
|
// 🎣 生命周期钩子(后续实现)
|
||||||
|
beforeSubmit?: (values: Record<string, any>) => Record<string, any> | false | Promise<Record<string, any> | false>;
|
||||||
|
afterSubmit?: (response?: any) => void | Promise<void>;
|
||||||
|
onError?: (error: any) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormRenderer: React.FC<FormRendererProps> = ({
|
/**
|
||||||
schema,
|
* 表单渲染器组件暴露的方法
|
||||||
extraComponents = [],
|
*/
|
||||||
value,
|
export interface FormRendererRef {
|
||||||
onChange,
|
/** 提交表单(触发验证) */
|
||||||
onSubmit,
|
submit: () => Promise<void>;
|
||||||
onCancel,
|
/** 重置表单 */
|
||||||
readonly = false,
|
reset: () => void;
|
||||||
showSubmit = true,
|
}
|
||||||
showCancel = false,
|
|
||||||
submitText = '提交',
|
const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref) => {
|
||||||
cancelText = '取消',
|
const {
|
||||||
beforeSubmit,
|
schema,
|
||||||
afterSubmit,
|
fields: propFields,
|
||||||
onError
|
formConfig: propFormConfig,
|
||||||
}) => {
|
extraComponents = [],
|
||||||
const { fields, formConfig } = schema;
|
onSubmit,
|
||||||
const [form] = Form.useForm();
|
showSubmit = false,
|
||||||
|
showCancel = false,
|
||||||
|
submitText = '提交',
|
||||||
|
cancelText = '取消',
|
||||||
|
onCancel,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// 🔌 合并核心组件和扩展组件
|
// 🔌 合并核心组件和扩展组件
|
||||||
const allComponents = useMemo(() => {
|
const allComponents = React.useMemo(() => {
|
||||||
return [...COMPONENT_LIST, ...extraComponents];
|
return [...COMPONENT_LIST, ...extraComponents];
|
||||||
}, [extraComponents]);
|
}, [extraComponents]);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>(value || {});
|
|
||||||
|
// 从 schema 或直接从 props 获取 fields 和 formConfig
|
||||||
|
const fields = schema?.fields || propFields || [];
|
||||||
|
const formConfig = schema?.formConfig || propFormConfig || {
|
||||||
|
labelAlign: 'right',
|
||||||
|
size: 'middle',
|
||||||
|
};
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}>>({});
|
}>>({});
|
||||||
|
|
||||||
// 同步外部值到表单
|
|
||||||
useEffect(() => {
|
|
||||||
if (value) {
|
|
||||||
form.setFieldsValue(value);
|
|
||||||
setFormData(value);
|
|
||||||
}
|
|
||||||
}, [value, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (readonly) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1️⃣ 表单验证
|
// 1. 表单验证
|
||||||
let values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
console.log('📋 表单验证通过:', values);
|
||||||
|
setFormData(values);
|
||||||
|
|
||||||
// 2️⃣ beforeSubmit 钩子 - 数据转换或阻止提交
|
// 2. beforeSubmit 钩子 - 允许修改提交数据或中断提交
|
||||||
if (beforeSubmit) {
|
let finalValues = values;
|
||||||
const result = await beforeSubmit(values);
|
if (props.beforeSubmit) {
|
||||||
if (result === false) {
|
try {
|
||||||
// 返回 false 阻止提交
|
const result = await props.beforeSubmit(values);
|
||||||
|
if (result === false) {
|
||||||
|
console.log('⚠️ beforeSubmit 返回 false,提交已取消');
|
||||||
|
message.warning('提交已取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result && typeof result === 'object') {
|
||||||
|
finalValues = result;
|
||||||
|
console.log('🔄 beforeSubmit 修改了提交数据:', finalValues);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ beforeSubmit 钩子执行失败:', error);
|
||||||
|
message.error('提交前处理失败');
|
||||||
|
if (props.onError) {
|
||||||
|
await props.onError(error);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result && typeof result === 'object') {
|
|
||||||
// 使用转换后的数据
|
|
||||||
values = result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormData(values);
|
// 3. 执行提交
|
||||||
|
let submitResult: any;
|
||||||
// 3️⃣ 提交
|
|
||||||
let response: any;
|
|
||||||
if (onSubmit) {
|
if (onSubmit) {
|
||||||
response = await onSubmit(values);
|
try {
|
||||||
|
submitResult = await onSubmit(finalValues);
|
||||||
|
console.log('✅ 表单提交成功:', submitResult);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 表单提交失败:', error);
|
||||||
|
// 提交失败时触发 onError 钩子
|
||||||
|
if (props.onError) {
|
||||||
|
await props.onError(error);
|
||||||
|
} else {
|
||||||
|
message.error('提交失败,请稍后重试');
|
||||||
|
}
|
||||||
|
throw error; // 重新抛出,不执行 afterSubmit
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('表单提交数据:', values);
|
|
||||||
message.success('表单提交成功!');
|
message.success('表单提交成功!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4️⃣ afterSubmit 钩子 - 提交成功后处理
|
// 4. afterSubmit 钩子 - 提交成功后的处理
|
||||||
if (afterSubmit) {
|
if (props.afterSubmit) {
|
||||||
try {
|
try {
|
||||||
await afterSubmit(response);
|
await props.afterSubmit(submitResult);
|
||||||
} catch (err) {
|
console.log('✅ afterSubmit 钩子执行完成');
|
||||||
console.error('afterSubmit error:', err);
|
} catch (error) {
|
||||||
|
console.error('⚠️ afterSubmit 钩子执行失败:', error);
|
||||||
|
// afterSubmit 失败不影响整体提交流程,只记录日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 5️⃣ onError 钩子 - 错误处理
|
// 验证失败
|
||||||
if (onError) {
|
console.error('❌ 表单验证失败:', error);
|
||||||
try {
|
message.error('请填写必填项');
|
||||||
await onError(error);
|
|
||||||
} catch (err) {
|
// 触发 onError 钩子(验证失败也算错误)
|
||||||
console.error('onError hook error:', err);
|
if (props.onError) {
|
||||||
}
|
await props.onError(error);
|
||||||
} else {
|
|
||||||
message.error('请填写必填项');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
if (readonly) return;
|
|
||||||
|
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
const resetValues = {};
|
setFormData({});
|
||||||
setFormData(resetValues);
|
|
||||||
|
|
||||||
if (onChange) {
|
|
||||||
onChange(resetValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
message.info('表单已重置');
|
message.info('表单已重置');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听表单值变化
|
||||||
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
|
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
|
||||||
setFormData(allValues);
|
setFormData(allValues);
|
||||||
if (onChange) {
|
if (props.onChange) {
|
||||||
onChange(allValues);
|
props.onChange(allValues);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
submit: handleSubmit,
|
||||||
|
reset: handleReset,
|
||||||
|
}));
|
||||||
|
|
||||||
// 将验证规则转换为Ant Design的Rule数组
|
// 将验证规则转换为Ant Design的Rule数组
|
||||||
const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => {
|
const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => {
|
||||||
if (!validationRules || validationRules.length === 0) return [];
|
if (!validationRules || validationRules.length === 0) return [];
|
||||||
@ -153,14 +224,11 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
const antdRule: Rule = {
|
const antdRule: Rule = {
|
||||||
type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined,
|
type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined,
|
||||||
message: rule.message || `请输入正确的${rule.type}`,
|
message: rule.message || `请输入正确的${rule.type}`,
|
||||||
// 注意:不设置 trigger,或者不仅仅设置为 blur,这样提交时也会触发验证
|
|
||||||
// 如果用户指定了 trigger,我们也在提交时触发
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (rule.type) {
|
switch (rule.type) {
|
||||||
case 'required':
|
case 'required':
|
||||||
antdRule.required = true;
|
antdRule.required = true;
|
||||||
// 必填验证始终在提交时触发,不管用户选择了什么 trigger
|
|
||||||
break;
|
break;
|
||||||
case 'pattern':
|
case 'pattern':
|
||||||
if (rule.value) {
|
if (rule.value) {
|
||||||
@ -243,7 +311,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
allFields.forEach(field => {
|
allFields.forEach(field => {
|
||||||
if (field.linkageRules && field.linkageRules.length > 0) {
|
if (field.linkageRules && field.linkageRules.length > 0) {
|
||||||
field.linkageRules.forEach((rule: LinkageRule) => {
|
field.linkageRules.forEach((rule: LinkageRule) => {
|
||||||
// 检查所有条件是否都满足
|
|
||||||
const allConditionsMet = rule.conditions.every(condition =>
|
const allConditionsMet = rule.conditions.every(condition =>
|
||||||
evaluateCondition(condition, formData)
|
evaluateCondition(condition, formData)
|
||||||
);
|
);
|
||||||
@ -264,7 +331,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
newFieldStates[field.name].required = rule.action;
|
newFieldStates[field.name].required = rule.action;
|
||||||
break;
|
break;
|
||||||
case 'value':
|
case 'value':
|
||||||
// 值联动需要直接设置表单值
|
|
||||||
form.setFieldValue(field.name, rule.action);
|
form.setFieldValue(field.name, rule.action);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -283,7 +349,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||||
fieldList.forEach(field => {
|
fieldList.forEach(field => {
|
||||||
if (field.defaultValue !== undefined && field.name) {
|
if (field.defaultValue !== undefined && field.name) {
|
||||||
// 🔧 日期字段需要转换为 dayjs 对象
|
|
||||||
if (field.type === 'date' && typeof field.defaultValue === 'string') {
|
if (field.type === 'date' && typeof field.defaultValue === 'string') {
|
||||||
defaultValues[field.name] = dayjs(field.defaultValue);
|
defaultValues[field.name] = dayjs(field.defaultValue);
|
||||||
} else {
|
} else {
|
||||||
@ -303,10 +368,9 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
setFormData(prev => ({ ...prev, ...defaultValues }));
|
setFormData(prev => ({ ...prev, ...defaultValues }));
|
||||||
}, [fields, form]);
|
}, [fields, form]);
|
||||||
|
|
||||||
// 递归渲染字段(包括栅格布局内的字段)
|
// 递归渲染字段
|
||||||
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
||||||
return fieldList.map((field) => {
|
return fieldList.map((field) => {
|
||||||
// 布局组件:文本、分割线
|
|
||||||
if (field.type === 'text' || field.type === 'divider') {
|
if (field.type === 'text' || field.type === 'divider') {
|
||||||
return (
|
return (
|
||||||
<div key={field.id}>
|
<div key={field.id}>
|
||||||
@ -315,43 +379,28 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item
|
|
||||||
if (field.type === 'grid') {
|
if (field.type === 'grid') {
|
||||||
return (
|
return (
|
||||||
<GridFieldPreview
|
<GridFieldPreview
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
formData={formData}
|
|
||||||
onFieldChange={(name, value) => setFormData(prev => ({ ...prev, [name]: value }))}
|
|
||||||
formConfig={formConfig}
|
formConfig={formConfig}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字段状态(联动规则影响)
|
|
||||||
const fieldState = fieldStates[field.name] || {};
|
const fieldState = fieldStates[field.name] || {};
|
||||||
const isVisible = fieldState.visible !== false; // 默认显示
|
const isVisible = fieldState.visible !== false;
|
||||||
const isDisabled = fieldState.disabled || field.disabled || false;
|
const isDisabled = fieldState.disabled || field.disabled || false;
|
||||||
const isRequired = fieldState.required !== undefined ? fieldState.required : field.required;
|
const isRequired = fieldState.required !== undefined ? fieldState.required : field.required;
|
||||||
|
|
||||||
// 如果字段被隐藏,不渲染
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并基础验证规则和自定义验证规则
|
|
||||||
const customRules = convertValidationRules(field.validationRules);
|
const customRules = convertValidationRules(field.validationRules);
|
||||||
|
|
||||||
// 🐛 调试:打印验证规则
|
|
||||||
if (field.validationRules && field.validationRules.length > 0) {
|
|
||||||
console.log(`📋 字段 "${field.label}" 的验证规则:`, field.validationRules);
|
|
||||||
console.log(`✅ 转换后的规则:`, customRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查自定义验证规则中是否已经包含必填验证
|
|
||||||
const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required');
|
const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required');
|
||||||
|
|
||||||
// 基础规则:只有在没有自定义必填验证时,才使用字段属性中的"是否必填"
|
|
||||||
const baseRules: Rule[] = [];
|
const baseRules: Rule[] = [];
|
||||||
if (isRequired && !hasRequiredRule) {
|
if (isRequired && !hasRequiredRule) {
|
||||||
baseRules.push({
|
baseRules.push({
|
||||||
@ -362,12 +411,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
|
|
||||||
const allRules = [...baseRules, ...customRules];
|
const allRules = [...baseRules, ...customRules];
|
||||||
|
|
||||||
// 🐛 调试:打印最终规则
|
|
||||||
if (allRules.length > 0) {
|
|
||||||
console.log(`🎯 字段 "${field.label}" 最终验证规则:`, allRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通表单组件使用 Form.Item 包裹
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key={field.id}
|
key={field.id}
|
||||||
@ -378,8 +421,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
>
|
>
|
||||||
<FieldRenderer
|
<FieldRenderer
|
||||||
field={{ ...field, disabled: isDisabled }}
|
field={{ ...field, disabled: isDisabled }}
|
||||||
value={formData[field.name]}
|
|
||||||
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
|
|
||||||
isPreview={true}
|
isPreview={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -403,52 +444,70 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
<ComponentsContext.Provider value={allComponents}>
|
<ComponentsContext.Provider value={allComponents}>
|
||||||
<div style={{ padding: '24px 40px' }}>
|
<div style={{ padding: '24px 40px' }}>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout={formLayout}
|
layout={formLayout}
|
||||||
size={formConfig.size}
|
size={formConfig.size}
|
||||||
colon={true}
|
colon={true}
|
||||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
||||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
||||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
||||||
onValuesChange={handleValuesChange}
|
onValuesChange={handleValuesChange}
|
||||||
disabled={readonly}
|
>
|
||||||
>
|
{formConfig.title && (
|
||||||
{formConfig.title && (
|
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{renderFields(fields)}
|
{renderFields(fields)}
|
||||||
|
|
||||||
{!readonly && (showSubmit || showCancel) && (
|
{(showSubmit || showCancel) && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
style={{ marginTop: 32, marginBottom: 0 }}
|
style={{ marginTop: 32, marginBottom: 0 }}
|
||||||
labelCol={{ span: 0 }}
|
wrapperCol={{ offset: labelColSpan || 0, span: wrapperColSpan || 24 }}
|
||||||
wrapperCol={{ span: 24 }}
|
>
|
||||||
>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8 }}>
|
{showSubmit && (
|
||||||
{showSubmit && (
|
<button
|
||||||
<Button type="primary" onClick={handleSubmit}>
|
type="button"
|
||||||
{submitText}
|
onClick={handleSubmit}
|
||||||
</Button>
|
style={{
|
||||||
)}
|
padding: '4px 15px',
|
||||||
{showCancel && (
|
fontSize: '14px',
|
||||||
<Button onClick={onCancel || handleReset}>
|
borderRadius: '6px',
|
||||||
{cancelText}
|
border: 'none',
|
||||||
</Button>
|
background: '#1677ff',
|
||||||
)}
|
color: 'white',
|
||||||
{!showCancel && (
|
cursor: 'pointer',
|
||||||
<Button onClick={handleReset}>
|
}}
|
||||||
重置
|
>
|
||||||
</Button>
|
{submitText}
|
||||||
)}
|
</button>
|
||||||
</div>
|
)}
|
||||||
</Form.Item>
|
{showCancel && (
|
||||||
)}
|
<button
|
||||||
</Form>
|
type="button"
|
||||||
|
onClick={onCancel || handleReset}
|
||||||
|
style={{
|
||||||
|
padding: '4px 15px',
|
||||||
|
fontSize: '14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
background: 'white',
|
||||||
|
color: 'rgba(0, 0, 0, 0.88)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</ComponentsContext.Provider>
|
</ComponentsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
FormRenderer.displayName = 'FormRenderer';
|
||||||
|
|
||||||
export default FormRenderer;
|
export default FormRenderer;
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
*
|
*
|
||||||
* 提供三个核心组件:
|
* 提供三个核心组件:
|
||||||
* 1. FormDesigner - 设计时组件,用于可视化设计表单
|
* 1. FormDesigner - 设计时组件,用于可视化设计表单
|
||||||
* 2. FormPreview - 预览组件,用于预览和交互表单(推荐)
|
* 2. FormPreview - 预览组件,用于预览和交互表单(推荐用于纯预览场景)
|
||||||
* 3. FormRenderer - 运行时组件,用于渲染和提交表单(已废弃,建议使用 FormPreview)
|
* 3. FormRenderer - 运行时组件,用于真实业务场景的表单渲染和提交(推荐用于生产环境)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as FormDesigner } from './Designer';
|
export { default as FormDesigner } from './Designer';
|
||||||
@ -14,7 +14,7 @@ export { default as FormPreview } from './Preview';
|
|||||||
export type { FormPreviewProps, FormPreviewRef } from './Preview';
|
export type { FormPreviewProps, FormPreviewRef } from './Preview';
|
||||||
|
|
||||||
export { default as FormRenderer } from './Renderer';
|
export { default as FormRenderer } from './Renderer';
|
||||||
export type { FormRendererProps } from './Renderer';
|
export type { FormRendererProps, FormRendererRef } from './Renderer';
|
||||||
|
|
||||||
// 导出类型定义
|
// 导出类型定义
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
138
frontend/src/domain/dataSource/presets/README.md
Normal file
138
frontend/src/domain/dataSource/presets/README.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# 数据源预设配置
|
||||||
|
|
||||||
|
本目录包含所有预定义的数据源配置。
|
||||||
|
|
||||||
|
## 已注册的数据源
|
||||||
|
|
||||||
|
### 部署相关 (deploy.ts)
|
||||||
|
|
||||||
|
#### 1. ENVIRONMENTS - 环境列表
|
||||||
|
```typescript
|
||||||
|
import { DataSourceType, loadDataSource } from '@/domain/dataSource';
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
const options = await loadDataSource(DataSourceType.ENVIRONMENTS);
|
||||||
|
```
|
||||||
|
|
||||||
|
**接口:** `GET /api/v1/environments/list?enabled=true`
|
||||||
|
|
||||||
|
**返回格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
label: string; // 环境名称
|
||||||
|
value: number; // 环境 ID
|
||||||
|
code: string; // 环境代码
|
||||||
|
type: string; // 环境类型
|
||||||
|
description: string;// 描述
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. PROJECT_GROUPS - 项目组列表
|
||||||
|
```typescript
|
||||||
|
const options = await loadDataSource(DataSourceType.PROJECT_GROUPS);
|
||||||
|
```
|
||||||
|
|
||||||
|
**接口:** `GET /api/v1/project-groups/list?enabled=true`
|
||||||
|
|
||||||
|
**返回格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
label: string; // 项目组名称
|
||||||
|
value: number; // 项目组 ID
|
||||||
|
code: string; // 项目组代码
|
||||||
|
description: string;// 描述
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. APPLICATIONS - 应用列表
|
||||||
|
```typescript
|
||||||
|
const options = await loadDataSource(DataSourceType.APPLICATIONS);
|
||||||
|
```
|
||||||
|
|
||||||
|
**接口:** `GET /api/v1/applications/list?enabled=true`
|
||||||
|
|
||||||
|
**返回格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
label: string; // 应用名称
|
||||||
|
value: number; // 应用 ID
|
||||||
|
code: string; // 应用代码
|
||||||
|
projectGroupId: number; // 所属项目组 ID
|
||||||
|
description: string; // 描述
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 外部系统 (jenkins.ts, k8s.ts, git.ts, docker.ts)
|
||||||
|
|
||||||
|
- **JENKINS_SERVERS** - Jenkins 服务器列表
|
||||||
|
- **K8S_CLUSTERS** - K8s 集群列表
|
||||||
|
- **GIT_REPOSITORIES** - Git 仓库列表
|
||||||
|
- **DOCKER_REGISTRIES** - Docker 镜像仓库列表
|
||||||
|
|
||||||
|
### 通知相关 (notification.ts)
|
||||||
|
|
||||||
|
- **NOTIFICATION_CHANNEL_TYPES** - 通知渠道类型
|
||||||
|
- **NOTIFICATION_CHANNELS** - 通知渠道列表
|
||||||
|
|
||||||
|
### 用户相关 (user.ts)
|
||||||
|
|
||||||
|
- **USERS** - 用户列表
|
||||||
|
- **ROLES** - 角色列表
|
||||||
|
- **DEPARTMENTS** - 部门列表
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 在 React 组件中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { DataSourceType, loadDataSource } from '@/domain/dataSource';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [environments, setEnvironments] = useState([]);
|
||||||
|
const [projectGroups, setProjectGroups] = useState([]);
|
||||||
|
const [applications, setApplications] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 加载环境列表
|
||||||
|
loadDataSource(DataSourceType.ENVIRONMENTS).then(setEnvironments);
|
||||||
|
|
||||||
|
// 加载项目组列表
|
||||||
|
loadDataSource(DataSourceType.PROJECT_GROUPS).then(setProjectGroups);
|
||||||
|
|
||||||
|
// 加载应用列表
|
||||||
|
loadDataSource(DataSourceType.APPLICATIONS).then(setApplications);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select>
|
||||||
|
{environments.map(env => (
|
||||||
|
<option key={env.value} value={env.value}>
|
||||||
|
{env.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在表单设计器中使用
|
||||||
|
|
||||||
|
这些数据源可以直接在表单设计器的下拉框、单选框等组件中使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: '选择环境',
|
||||||
|
dataSource: DataSourceType.ENVIRONMENTS
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新数据源
|
||||||
|
|
||||||
|
1. 在 `types.ts` 中添加枚举类型
|
||||||
|
2. 在 `presets/` 目录创建配置文件
|
||||||
|
3. 在 `DataSourceRegistry.ts` 中注册
|
||||||
|
|
||||||
|
详见主 README.md。
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
|
|
||||||
// ==================== 示例 2: 表单渲染器(静态) ====================
|
// ==================== 示例 2: 表单渲染器(静态) ====================
|
||||||
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
|
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
|
||||||
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
|
|
||||||
|
|
||||||
const handleStaticFormSubmit = async (data: Record<string, any>) => {
|
const handleStaticFormSubmit = async (data: Record<string, any>) => {
|
||||||
console.log('📤 静态表单提交数据:', data);
|
console.log('📤 静态表单提交数据:', data);
|
||||||
@ -145,7 +144,6 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 示例 5: 复杂表单(带栅格布局) ====================
|
// ==================== 示例 5: 复杂表单(带栅格布局) ====================
|
||||||
const [complexFormData, setComplexFormData] = useState<Record<string, any>>({});
|
|
||||||
|
|
||||||
const complexFormSchema: FormSchema = {
|
const complexFormSchema: FormSchema = {
|
||||||
version: '1.0',
|
version: '1.0',
|
||||||
@ -210,6 +208,98 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
message.success('表单提交成功!');
|
message.success('表单提交成功!');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== 示例 6: 带生命周期钩子的表单 ====================
|
||||||
|
const lifecycleFormSchema: FormSchema = {
|
||||||
|
version: '1.0',
|
||||||
|
formConfig: {
|
||||||
|
labelAlign: 'right',
|
||||||
|
size: 'middle',
|
||||||
|
formWidth: 600
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: 'lc_field_1',
|
||||||
|
type: 'input',
|
||||||
|
label: '用户名',
|
||||||
|
name: 'username',
|
||||||
|
placeholder: '请输入用户名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lc_field_2',
|
||||||
|
type: 'input',
|
||||||
|
label: '邮箱',
|
||||||
|
name: 'email',
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
required: true,
|
||||||
|
validationRules: [
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lc_field_3',
|
||||||
|
type: 'select',
|
||||||
|
label: '用户角色',
|
||||||
|
name: 'role',
|
||||||
|
placeholder: '请选择角色',
|
||||||
|
required: true,
|
||||||
|
dataSourceType: 'static',
|
||||||
|
options: [
|
||||||
|
{ label: '管理员', value: 'admin' },
|
||||||
|
{ label: '普通用户', value: 'user' },
|
||||||
|
{ label: '访客', value: 'guest' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLifecycleBeforeSubmit = async (values: Record<string, any>) => {
|
||||||
|
console.log('🎣 beforeSubmit 钩子被调用:', values);
|
||||||
|
|
||||||
|
// 模拟数据处理
|
||||||
|
const processedValues = {
|
||||||
|
...values,
|
||||||
|
email: values.email?.toLowerCase(), // 邮箱转小写
|
||||||
|
submitTime: new Date().toISOString(), // 添加提交时间
|
||||||
|
};
|
||||||
|
|
||||||
|
message.info('正在处理表单数据...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
console.log('✅ beforeSubmit 处理完成,返回修改后的数据:', processedValues);
|
||||||
|
return processedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLifecycleSubmit = async (data: Record<string, any>) => {
|
||||||
|
console.log('📤 提交到服务器:', data);
|
||||||
|
|
||||||
|
// 模拟 API 调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const response = { id: Math.random(), success: true, data };
|
||||||
|
console.log('✅ 服务器响应:', response);
|
||||||
|
message.success('用户创建成功!');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLifecycleAfterSubmit = async (response: any) => {
|
||||||
|
console.log('🎣 afterSubmit 钩子被调用,服务器响应:', response);
|
||||||
|
|
||||||
|
// 模拟后续操作(如跳转、刷新列表等)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
message.success(`用户 ID: ${response.id.toFixed(0)} 已成功创建`);
|
||||||
|
console.log('✅ afterSubmit 完成,可以执行跳转或刷新操作');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLifecycleError = async (error: any) => {
|
||||||
|
console.error('🎣 onError 钩子被调用:', error);
|
||||||
|
|
||||||
|
// 可以在这里做错误上报、记录等
|
||||||
|
message.error(`操作失败: ${error.message || '未知错误'}`);
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== Tab 配置 ====================
|
// ==================== Tab 配置 ====================
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
@ -295,12 +385,18 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。
|
根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。
|
||||||
</p>
|
</p>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Alert
|
||||||
|
message="✅ 正确用法"
|
||||||
|
description="FormRenderer 现在是非受控组件,不需要传递 value 和 onChange。表单内部自动管理状态,onSubmit 会返回完整数据。"
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
{previewSchema ? (
|
{previewSchema ? (
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
schema={previewSchema}
|
schema={previewSchema}
|
||||||
value={staticFormData}
|
|
||||||
onChange={setStaticFormData}
|
|
||||||
onSubmit={handleStaticFormSubmit}
|
onSubmit={handleStaticFormSubmit}
|
||||||
|
showSubmit
|
||||||
showCancel
|
showCancel
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -327,6 +423,13 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
showIcon
|
showIcon
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
<Alert
|
||||||
|
message="✅ 下拉框可选中"
|
||||||
|
description="FormRenderer 现已修复,下拉框可以正常选中。不需要传递 value 和 onChange,表单内部自动管理状态。"
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
{workflowPreviewSchema ? (
|
{workflowPreviewSchema ? (
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
@ -336,6 +439,7 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
console.log('🔄 工作流表单提交数据:', data);
|
console.log('🔄 工作流表单提交数据:', data);
|
||||||
message.success('工作流表单提交成功!');
|
message.success('工作流表单提交成功!');
|
||||||
}}
|
}}
|
||||||
|
showSubmit
|
||||||
showCancel
|
showCancel
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -427,14 +531,83 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
schema={complexFormSchema}
|
schema={complexFormSchema}
|
||||||
value={complexFormData}
|
|
||||||
onChange={setComplexFormData}
|
|
||||||
onSubmit={handleComplexFormSubmit}
|
onSubmit={handleComplexFormSubmit}
|
||||||
|
showSubmit
|
||||||
showCancel
|
showCancel
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '6',
|
||||||
|
label: '🎣 示例 6: 生命周期钩子',
|
||||||
|
children: (
|
||||||
|
<Card>
|
||||||
|
<h2>生命周期钩子(beforeSubmit / afterSubmit / onError)</h2>
|
||||||
|
<p style={{ marginBottom: 16, color: '#666' }}>
|
||||||
|
演示表单提交的完整生命周期钩子,支持数据预处理、提交后处理和错误处理。
|
||||||
|
</p>
|
||||||
|
<Alert
|
||||||
|
message="🎣 生命周期钩子"
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<p><strong>beforeSubmit</strong>: 提交前处理,可修改数据或取消提交(返回 false)</p>
|
||||||
|
<p><strong>onSubmit</strong>: 执行实际提交操作</p>
|
||||||
|
<p><strong>afterSubmit</strong>: 提交成功后的处理(跳转、刷新等)</p>
|
||||||
|
<p><strong>onError</strong>: 错误处理(验证失败、提交失败等)</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<div style={{
|
||||||
|
padding: 12,
|
||||||
|
background: '#f5f5f5',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 16,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12
|
||||||
|
}}>
|
||||||
|
<strong>使用方式:</strong><br />
|
||||||
|
{`<FormRenderer`}<br />
|
||||||
|
{` schema={schema}`}<br />
|
||||||
|
{` beforeSubmit={async (values) => {`}<br />
|
||||||
|
{` // 数据预处理`}<br />
|
||||||
|
{` return modifiedValues;`}<br />
|
||||||
|
{` }}`}<br />
|
||||||
|
{` onSubmit={async (data) => {`}<br />
|
||||||
|
{` // 提交到服务器`}<br />
|
||||||
|
{` return response;`}<br />
|
||||||
|
{` }}`}<br />
|
||||||
|
{` afterSubmit={async (response) => {`}<br />
|
||||||
|
{` // 提交后处理`}<br />
|
||||||
|
{` }}`}<br />
|
||||||
|
{` onError={async (error) => {`}<br />
|
||||||
|
{` // 错误处理`}<br />
|
||||||
|
{` }}`}<br />
|
||||||
|
{`/>`}
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<FormRenderer
|
||||||
|
schema={lifecycleFormSchema}
|
||||||
|
beforeSubmit={handleLifecycleBeforeSubmit}
|
||||||
|
onSubmit={handleLifecycleSubmit}
|
||||||
|
afterSubmit={handleLifecycleAfterSubmit}
|
||||||
|
onError={handleLifecycleError}
|
||||||
|
showSubmit
|
||||||
|
showCancel
|
||||||
|
/>
|
||||||
|
<Alert
|
||||||
|
message="📝 提示"
|
||||||
|
description="打开浏览器控制台查看完整的钩子执行流程,包括 beforeSubmit、onSubmit、afterSubmit 的详细日志输出。"
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,128 @@
|
|||||||
|
# FormPreviewModal 组件
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
表单预览弹窗组件,用于在工作流设计页面预览与工作流关联的启动表单。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 自动读取工作流关联的表单定义数据
|
||||||
|
- ✅ 使用 FormRenderer 组件渲染表单
|
||||||
|
- ✅ 只读预览模式(不显示提交/取消按钮)
|
||||||
|
- ✅ 显示表单基本信息(名称、描述、标识、版本、状态)
|
||||||
|
- ✅ 响应式弹窗尺寸(max-w-5xl)
|
||||||
|
- ✅ 支持滚动查看长表单
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 1. 在工作流设计页面自动加载
|
||||||
|
|
||||||
|
当进入工作流设计页面时,如果工作流关联了启动表单(`formDefinitionId`),系统会自动:
|
||||||
|
|
||||||
|
1. 调用 `/api/v1/form-definitions/{id}` 接口获取表单定义数据
|
||||||
|
2. 将数据存储在 `formDefinition` 状态中
|
||||||
|
3. 在工具栏显示"预览表单"按钮(只有关联表单时才显示)
|
||||||
|
|
||||||
|
### 2. 点击预览按钮
|
||||||
|
|
||||||
|
用户点击"预览表单"按钮后,会打开弹窗显示表单预览。
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 加载工作流时自动查询表单数据
|
||||||
|
if (data.definition?.formDefinitionId) {
|
||||||
|
const formDef = await getFormDefinitionById(data.definition.formDefinitionId);
|
||||||
|
setFormDefinition(formDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 点击预览按钮
|
||||||
|
const handlePreviewForm = () => {
|
||||||
|
if (!formDefinition) {
|
||||||
|
message.warning('当前工作流未关联启动表单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFormPreviewVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 渲染预览弹窗
|
||||||
|
<FormPreviewModal
|
||||||
|
open={formPreviewVisible}
|
||||||
|
onClose={() => setFormPreviewVisible(false)}
|
||||||
|
formDefinition={formDefinition}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
| 属性 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `open` | `boolean` | 是 | 控制弹窗显示/隐藏 |
|
||||||
|
| `onClose` | `() => void` | 是 | 关闭弹窗的回调函数 |
|
||||||
|
| `formDefinition` | `FormDefinitionResponse \| null` | 是 | 表单定义数据 |
|
||||||
|
|
||||||
|
## 表单定义数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FormDefinitionResponse {
|
||||||
|
id: number;
|
||||||
|
name: string; // 表单名称
|
||||||
|
key: string; // 表单标识
|
||||||
|
formVersion: number; // 表单版本号
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
description?: string; // 表单描述
|
||||||
|
schema: FormSchema; // 表单Schema(包含fields和formConfig)
|
||||||
|
status: 'DRAFT' | 'PUBLISHED' | 'DISABLED'; // 表单状态
|
||||||
|
isTemplate: boolean; // 是否为模板
|
||||||
|
// ... 其他基础字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## FormRenderer 配置
|
||||||
|
|
||||||
|
预览模式下的 FormRenderer 配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<FormRenderer
|
||||||
|
schema={formDefinition.schema} // 表单Schema
|
||||||
|
readonly={false} // 可交互(但不提交)
|
||||||
|
showSubmit={false} // 不显示提交按钮
|
||||||
|
showCancel={false} // 不显示取消按钮
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI 展示
|
||||||
|
|
||||||
|
弹窗包含以下部分:
|
||||||
|
|
||||||
|
1. **标题栏**:显示表单名称和描述
|
||||||
|
2. **表单内容区**:使用 FormRenderer 渲染的表单
|
||||||
|
3. **底部信息栏**:显示表单标识、版本、状态等元数据
|
||||||
|
|
||||||
|
## 样式特性
|
||||||
|
|
||||||
|
- 弹窗最大宽度:`max-w-5xl`(适配大表单)
|
||||||
|
- 弹窗最大高度:`max-h-[90vh]`(适配屏幕)
|
||||||
|
- 内容区可滚动:`overflow-y-auto`
|
||||||
|
- 状态颜色:
|
||||||
|
- 已发布:绿色(`text-green-600`)
|
||||||
|
- 草稿:黄色(`text-yellow-600`)
|
||||||
|
- 其他:灰色(`text-gray-600`)
|
||||||
|
|
||||||
|
## 未来扩展
|
||||||
|
|
||||||
|
可能的功能扩展方向:
|
||||||
|
|
||||||
|
- [ ] 支持表单数据填写和本地保存
|
||||||
|
- [ ] 支持导出表单数据为 JSON
|
||||||
|
- [ ] 支持全屏预览模式
|
||||||
|
- [ ] 支持打印表单
|
||||||
|
- [ ] 支持分享表单预览链接
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 只有在工作流关联了启动表单时,"预览表单"按钮才会显示
|
||||||
|
2. 表单数据在进入页面时自动加载,无需手动刷新
|
||||||
|
3. 预览模式下表单是可交互的,但不会实际提交数据
|
||||||
|
4. 如果表单定义数据加载失败,会在控制台输出错误信息
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user