增加审批组件

This commit is contained in:
dengqichen 2025-10-25 00:20:19 +08:00
parent 2f4ff4b6bd
commit ef3e34b85d
7 changed files with 1542 additions and 175 deletions

View 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)

View 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**: 统一的错误处理
合理使用这些钩子,可以构建健壮、灵活的表单提交流程。

View File

@ -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;

View File

@ -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 {

View 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。

View File

@ -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 (

View File

@ -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. 如果表单定义数据加载失败,会在控制台输出错误信息