增加审批组件
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 { Form, Button, message } from 'antd';
|
||||
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||
import { Form, message } from 'antd';
|
||||
import type { Rule } from 'antd/es/form';
|
||||
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 { COMPONENT_LIST } from './config';
|
||||
import { ComponentsContext } from './Designer';
|
||||
@ -15,136 +43,179 @@ import FieldRenderer from './components/FieldRenderer';
|
||||
import GridFieldPreview from './components/GridFieldPreview';
|
||||
import './styles.css';
|
||||
|
||||
/**
|
||||
* 表单渲染器组件的 Props
|
||||
*/
|
||||
export interface FormRendererProps {
|
||||
schema: FormSchema; // 表单 Schema
|
||||
extraComponents?: ComponentMeta[]; // 🆕 扩展组件(如工作流字段)
|
||||
value?: Record<string, any>; // 表单值(受控)
|
||||
onChange?: (values: Record<string, any>) => void; // 值变化回调
|
||||
onSubmit?: (values: Record<string, any>) => void | Promise<any>; // 提交回调
|
||||
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>; // 提交失败钩子
|
||||
/** 表单 Schema(包含 fields 和 formConfig) */
|
||||
schema?: FormSchema;
|
||||
/** 字段列表(与 schema 二选一) */
|
||||
fields?: FieldConfig[];
|
||||
/** 表单配置(与 schema 二选一) */
|
||||
formConfig?: FormConfig;
|
||||
/** 🆕 扩展组件(如工作流字段) */
|
||||
extraComponents?: ComponentMeta[];
|
||||
/** 表单初始值(仅用于初始化,不要作为受控属性使用) */
|
||||
value?: Record<string, any>;
|
||||
/** 值变化回调(父组件可监听,但不应将值传回 value) */
|
||||
onChange?: (values: Record<string, any>) => void;
|
||||
/** 提交回调 */
|
||||
onSubmit?: (values: Record<string, any>) => void | Promise<any>;
|
||||
/** 取消回调 */
|
||||
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,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
readonly = false,
|
||||
showSubmit = true,
|
||||
showCancel = false,
|
||||
submitText = '提交',
|
||||
cancelText = '取消',
|
||||
beforeSubmit,
|
||||
afterSubmit,
|
||||
onError
|
||||
}) => {
|
||||
const { fields, formConfig } = schema;
|
||||
const [form] = Form.useForm();
|
||||
/**
|
||||
* 表单渲染器组件暴露的方法
|
||||
*/
|
||||
export interface FormRendererRef {
|
||||
/** 提交表单(触发验证) */
|
||||
submit: () => Promise<void>;
|
||||
/** 重置表单 */
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref) => {
|
||||
const {
|
||||
schema,
|
||||
fields: propFields,
|
||||
formConfig: propFormConfig,
|
||||
extraComponents = [],
|
||||
onSubmit,
|
||||
showSubmit = false,
|
||||
showCancel = false,
|
||||
submitText = '提交',
|
||||
cancelText = '取消',
|
||||
onCancel,
|
||||
} = props;
|
||||
|
||||
// 🔌 合并核心组件和扩展组件
|
||||
const allComponents = useMemo(() => {
|
||||
const allComponents = React.useMemo(() => {
|
||||
return [...COMPONENT_LIST, ...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, {
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}>>({});
|
||||
|
||||
// 同步外部值到表单
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
form.setFieldsValue(value);
|
||||
setFormData(value);
|
||||
}
|
||||
}, [value, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (readonly) return;
|
||||
|
||||
try {
|
||||
// 1️⃣ 表单验证
|
||||
let values = await form.validateFields();
|
||||
// 1. 表单验证
|
||||
const values = await form.validateFields();
|
||||
console.log('📋 表单验证通过:', values);
|
||||
setFormData(values);
|
||||
|
||||
// 2️⃣ beforeSubmit 钩子 - 数据转换或阻止提交
|
||||
if (beforeSubmit) {
|
||||
const result = await beforeSubmit(values);
|
||||
if (result === false) {
|
||||
// 返回 false 阻止提交
|
||||
// 2. beforeSubmit 钩子 - 允许修改提交数据或中断提交
|
||||
let finalValues = values;
|
||||
if (props.beforeSubmit) {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
if (result && typeof result === 'object') {
|
||||
// 使用转换后的数据
|
||||
values = result;
|
||||
}
|
||||
}
|
||||
|
||||
setFormData(values);
|
||||
|
||||
// 3️⃣ 提交
|
||||
let response: any;
|
||||
// 3. 执行提交
|
||||
let submitResult: any;
|
||||
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 {
|
||||
console.log('表单提交数据:', values);
|
||||
message.success('表单提交成功!');
|
||||
}
|
||||
|
||||
// 4️⃣ afterSubmit 钩子 - 提交成功后处理
|
||||
if (afterSubmit) {
|
||||
// 4. afterSubmit 钩子 - 提交成功后的处理
|
||||
if (props.afterSubmit) {
|
||||
try {
|
||||
await afterSubmit(response);
|
||||
} catch (err) {
|
||||
console.error('afterSubmit error:', err);
|
||||
await props.afterSubmit(submitResult);
|
||||
console.log('✅ afterSubmit 钩子执行完成');
|
||||
} catch (error) {
|
||||
console.error('⚠️ afterSubmit 钩子执行失败:', error);
|
||||
// afterSubmit 失败不影响整体提交流程,只记录日志
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 5️⃣ onError 钩子 - 错误处理
|
||||
if (onError) {
|
||||
try {
|
||||
await onError(error);
|
||||
} catch (err) {
|
||||
console.error('onError hook error:', err);
|
||||
}
|
||||
} else {
|
||||
message.error('请填写必填项');
|
||||
// 验证失败
|
||||
console.error('❌ 表单验证失败:', error);
|
||||
message.error('请填写必填项');
|
||||
|
||||
// 触发 onError 钩子(验证失败也算错误)
|
||||
if (props.onError) {
|
||||
await props.onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (readonly) return;
|
||||
|
||||
form.resetFields();
|
||||
const resetValues = {};
|
||||
setFormData(resetValues);
|
||||
|
||||
if (onChange) {
|
||||
onChange(resetValues);
|
||||
}
|
||||
|
||||
setFormData({});
|
||||
message.info('表单已重置');
|
||||
};
|
||||
|
||||
// 监听表单值变化
|
||||
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
|
||||
setFormData(allValues);
|
||||
if (onChange) {
|
||||
onChange(allValues);
|
||||
if (props.onChange) {
|
||||
props.onChange(allValues);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit: handleSubmit,
|
||||
reset: handleReset,
|
||||
}));
|
||||
|
||||
// 将验证规则转换为Ant Design的Rule数组
|
||||
const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => {
|
||||
if (!validationRules || validationRules.length === 0) return [];
|
||||
@ -153,14 +224,11 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
const antdRule: Rule = {
|
||||
type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined,
|
||||
message: rule.message || `请输入正确的${rule.type}`,
|
||||
// 注意:不设置 trigger,或者不仅仅设置为 blur,这样提交时也会触发验证
|
||||
// 如果用户指定了 trigger,我们也在提交时触发
|
||||
};
|
||||
|
||||
switch (rule.type) {
|
||||
case 'required':
|
||||
antdRule.required = true;
|
||||
// 必填验证始终在提交时触发,不管用户选择了什么 trigger
|
||||
break;
|
||||
case 'pattern':
|
||||
if (rule.value) {
|
||||
@ -243,7 +311,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
allFields.forEach(field => {
|
||||
if (field.linkageRules && field.linkageRules.length > 0) {
|
||||
field.linkageRules.forEach((rule: LinkageRule) => {
|
||||
// 检查所有条件是否都满足
|
||||
const allConditionsMet = rule.conditions.every(condition =>
|
||||
evaluateCondition(condition, formData)
|
||||
);
|
||||
@ -264,7 +331,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
newFieldStates[field.name].required = rule.action;
|
||||
break;
|
||||
case 'value':
|
||||
// 值联动需要直接设置表单值
|
||||
form.setFieldValue(field.name, rule.action);
|
||||
break;
|
||||
}
|
||||
@ -283,7 +349,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||
fieldList.forEach(field => {
|
||||
if (field.defaultValue !== undefined && field.name) {
|
||||
// 🔧 日期字段需要转换为 dayjs 对象
|
||||
if (field.type === 'date' && typeof field.defaultValue === 'string') {
|
||||
defaultValues[field.name] = dayjs(field.defaultValue);
|
||||
} else {
|
||||
@ -303,10 +368,9 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
setFormData(prev => ({ ...prev, ...defaultValues }));
|
||||
}, [fields, form]);
|
||||
|
||||
// 递归渲染字段(包括栅格布局内的字段)
|
||||
// 递归渲染字段
|
||||
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
||||
return fieldList.map((field) => {
|
||||
// 布局组件:文本、分割线
|
||||
if (field.type === 'text' || field.type === 'divider') {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
@ -315,43 +379,28 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item
|
||||
if (field.type === 'grid') {
|
||||
return (
|
||||
<GridFieldPreview
|
||||
key={field.id}
|
||||
field={field}
|
||||
formData={formData}
|
||||
onFieldChange={(name, value) => setFormData(prev => ({ ...prev, [name]: value }))}
|
||||
formConfig={formConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 获取字段状态(联动规则影响)
|
||||
const fieldState = fieldStates[field.name] || {};
|
||||
const isVisible = fieldState.visible !== false; // 默认显示
|
||||
const isVisible = fieldState.visible !== false;
|
||||
const isDisabled = fieldState.disabled || field.disabled || false;
|
||||
const isRequired = fieldState.required !== undefined ? fieldState.required : field.required;
|
||||
|
||||
// 如果字段被隐藏,不渲染
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 合并基础验证规则和自定义验证规则
|
||||
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 baseRules: Rule[] = [];
|
||||
if (isRequired && !hasRequiredRule) {
|
||||
baseRules.push({
|
||||
@ -362,12 +411,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
|
||||
const allRules = [...baseRules, ...customRules];
|
||||
|
||||
// 🐛 调试:打印最终规则
|
||||
if (allRules.length > 0) {
|
||||
console.log(`🎯 字段 "${field.label}" 最终验证规则:`, allRules);
|
||||
}
|
||||
|
||||
// 普通表单组件使用 Form.Item 包裹
|
||||
return (
|
||||
<Form.Item
|
||||
key={field.id}
|
||||
@ -378,8 +421,6 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
>
|
||||
<FieldRenderer
|
||||
field={{ ...field, disabled: isDisabled }}
|
||||
value={formData[field.name]}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
|
||||
isPreview={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
@ -403,52 +444,70 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
<ComponentsContext.Provider value={allComponents}>
|
||||
<div style={{ padding: '24px 40px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout={formLayout}
|
||||
size={formConfig.size}
|
||||
colon={true}
|
||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
||||
onValuesChange={handleValuesChange}
|
||||
disabled={readonly}
|
||||
>
|
||||
{formConfig.title && (
|
||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||
)}
|
||||
form={form}
|
||||
layout={formLayout}
|
||||
size={formConfig.size}
|
||||
colon={true}
|
||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
{formConfig.title && (
|
||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||
)}
|
||||
|
||||
{renderFields(fields)}
|
||||
{renderFields(fields)}
|
||||
|
||||
{!readonly && (showSubmit || showCancel) && (
|
||||
<Form.Item
|
||||
style={{ marginTop: 32, marginBottom: 0 }}
|
||||
labelCol={{ span: 0 }}
|
||||
wrapperCol={{ span: 24 }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8 }}>
|
||||
{showSubmit && (
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
{submitText}
|
||||
</Button>
|
||||
)}
|
||||
{showCancel && (
|
||||
<Button onClick={onCancel || handleReset}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{!showCancel && (
|
||||
<Button onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
{(showSubmit || showCancel) && (
|
||||
<Form.Item
|
||||
style={{ marginTop: 32, marginBottom: 0 }}
|
||||
wrapperCol={{ offset: labelColSpan || 0, span: wrapperColSpan || 24 }}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{showSubmit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
padding: '4px 15px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: '#1677ff',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{submitText}
|
||||
</button>
|
||||
)}
|
||||
{showCancel && (
|
||||
<button
|
||||
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>
|
||||
</ComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
FormRenderer.displayName = 'FormRenderer';
|
||||
|
||||
export default FormRenderer;
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
*
|
||||
* 提供三个核心组件:
|
||||
* 1. FormDesigner - 设计时组件,用于可视化设计表单
|
||||
* 2. FormPreview - 预览组件,用于预览和交互表单(推荐)
|
||||
* 3. FormRenderer - 运行时组件,用于渲染和提交表单(已废弃,建议使用 FormPreview)
|
||||
* 2. FormPreview - 预览组件,用于预览和交互表单(推荐用于纯预览场景)
|
||||
* 3. FormRenderer - 运行时组件,用于真实业务场景的表单渲染和提交(推荐用于生产环境)
|
||||
*/
|
||||
|
||||
export { default as FormDesigner } from './Designer';
|
||||
@ -14,7 +14,7 @@ export { default as FormPreview } from './Preview';
|
||||
export type { FormPreviewProps, FormPreviewRef } from './Preview';
|
||||
|
||||
export { default as FormRenderer } from './Renderer';
|
||||
export type { FormRendererProps } from './Renderer';
|
||||
export type { FormRendererProps, FormRendererRef } from './Renderer';
|
||||
|
||||
// 导出类型定义
|
||||
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: 表单渲染器(静态) ====================
|
||||
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
|
||||
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
|
||||
|
||||
const handleStaticFormSubmit = async (data: Record<string, any>) => {
|
||||
console.log('📤 静态表单提交数据:', data);
|
||||
@ -145,7 +144,6 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// ==================== 示例 5: 复杂表单(带栅格布局) ====================
|
||||
const [complexFormData, setComplexFormData] = useState<Record<string, any>>({});
|
||||
|
||||
const complexFormSchema: FormSchema = {
|
||||
version: '1.0',
|
||||
@ -210,6 +208,98 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
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 配置 ====================
|
||||
const tabItems = [
|
||||
{
|
||||
@ -295,12 +385,18 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。
|
||||
</p>
|
||||
<Divider />
|
||||
<Alert
|
||||
message="✅ 正确用法"
|
||||
description="FormRenderer 现在是非受控组件,不需要传递 value 和 onChange。表单内部自动管理状态,onSubmit 会返回完整数据。"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
{previewSchema ? (
|
||||
<FormRenderer
|
||||
schema={previewSchema}
|
||||
value={staticFormData}
|
||||
onChange={setStaticFormData}
|
||||
onSubmit={handleStaticFormSubmit}
|
||||
showSubmit
|
||||
showCancel
|
||||
/>
|
||||
) : (
|
||||
@ -327,6 +423,13 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Alert
|
||||
message="✅ 下拉框可选中"
|
||||
description="FormRenderer 现已修复,下拉框可以正常选中。不需要传递 value 和 onChange,表单内部自动管理状态。"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Divider />
|
||||
{workflowPreviewSchema ? (
|
||||
<FormRenderer
|
||||
@ -336,6 +439,7 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
console.log('🔄 工作流表单提交数据:', data);
|
||||
message.success('工作流表单提交成功!');
|
||||
}}
|
||||
showSubmit
|
||||
showCancel
|
||||
/>
|
||||
) : (
|
||||
@ -427,14 +531,83 @@ const FormDesignerExamplesPage: React.FC = () => {
|
||||
<Divider />
|
||||
<FormRenderer
|
||||
schema={complexFormSchema}
|
||||
value={complexFormData}
|
||||
onChange={setComplexFormData}
|
||||
onSubmit={handleComplexFormSubmit}
|
||||
showSubmit
|
||||
showCancel
|
||||
/>
|
||||
</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 (
|
||||
|
||||
@ -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