表单设计器
This commit is contained in:
parent
ef5415f412
commit
374a74e4ab
@ -18,7 +18,7 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { Button, Space, message, Modal } from 'antd';
|
import { Button, Space, message, Modal } from 'antd';
|
||||||
import {
|
import {
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
@ -41,7 +41,7 @@ import { arrayMove } from '@dnd-kit/sortable';
|
|||||||
import ComponentPanel from './components/ComponentPanel';
|
import ComponentPanel from './components/ComponentPanel';
|
||||||
import DesignCanvas from './components/DesignCanvas';
|
import DesignCanvas from './components/DesignCanvas';
|
||||||
import PropertyPanel from './components/PropertyPanel';
|
import PropertyPanel from './components/PropertyPanel';
|
||||||
import FormPreview from './components/FormPreview';
|
import FormPreview, { type FormPreviewRef } from './components/FormPreview';
|
||||||
import { COMPONENT_LIST } from './config';
|
import { COMPONENT_LIST } from './config';
|
||||||
import type { FieldConfig, FormConfig, FormSchema } from './types';
|
import type { FieldConfig, FormConfig, FormSchema } from './types';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
@ -63,6 +63,9 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
showToolbar = true,
|
showToolbar = true,
|
||||||
extraActions
|
extraActions
|
||||||
}) => {
|
}) => {
|
||||||
|
// 表单预览 ref
|
||||||
|
const formPreviewRef = useRef<FormPreviewRef>(null);
|
||||||
|
|
||||||
// 内部状态(非受控模式使用)
|
// 内部状态(非受控模式使用)
|
||||||
const [internalFields, setInternalFields] = useState<FieldConfig[]>([]);
|
const [internalFields, setInternalFields] = useState<FieldConfig[]>([]);
|
||||||
const [internalFormConfig, setInternalFormConfig] = useState<FormConfig>({
|
const [internalFormConfig, setInternalFormConfig] = useState<FormConfig>({
|
||||||
@ -208,10 +211,10 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
if (over.id === 'canvas') {
|
if (over.id === 'canvas') {
|
||||||
newFields.push(newField);
|
newFields.push(newField);
|
||||||
} else {
|
} else {
|
||||||
// 如果拖到某个字段上,插入到该字段位置
|
// 如果拖到某个字段上,插入到该字段之后
|
||||||
const overIndex = newFields.findIndex((f) => f.id === over.id);
|
const overIndex = newFields.findIndex((f) => f.id === over.id);
|
||||||
if (overIndex >= 0) {
|
if (overIndex >= 0) {
|
||||||
newFields.splice(overIndex, 0, newField);
|
newFields.splice(overIndex + 1, 0, newField); // 插入到目标字段之后
|
||||||
} else {
|
} else {
|
||||||
newFields.push(newField);
|
newFields.push(newField);
|
||||||
}
|
}
|
||||||
@ -685,9 +688,26 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
open={previewVisible}
|
open={previewVisible}
|
||||||
onCancel={() => setPreviewVisible(false)}
|
onCancel={() => setPreviewVisible(false)}
|
||||||
width={formConfig.formWidth || 600}
|
width={formConfig.formWidth || 600}
|
||||||
footer={null}
|
bodyStyle={{
|
||||||
|
maxHeight: 'calc(100vh - 200px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={() => formPreviewRef.current?.submit()}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => formPreviewRef.current?.reset()}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FormPreview fields={fields} formConfig={formConfig} />
|
<FormPreview ref={formPreviewRef} fields={fields} formConfig={formConfig} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -38,8 +38,8 @@ const DraggableComponent: React.FC<{ component: ComponentMeta }> = ({ component
|
|||||||
{...attributes}
|
{...attributes}
|
||||||
className="form-designer-component-item"
|
className="form-designer-component-item"
|
||||||
>
|
>
|
||||||
<Icon style={{ marginRight: 8, fontSize: 16 }} />
|
<Icon style={{ marginRight: 6, fontSize: 14, flexShrink: 0 }} />
|
||||||
<Text>{component.label}</Text>
|
<Text style={{ fontSize: 12 }}>{component.label}</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -57,30 +57,32 @@ const ComponentPanel: React.FC = () => {
|
|||||||
<Text strong>组件列表</Text>
|
<Text strong>组件列表</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Collapse
|
<div className="form-designer-component-panel-body">
|
||||||
defaultActiveKey={['基础字段', '高级字段', '布局字段']}
|
<Collapse
|
||||||
ghost
|
defaultActiveKey={['基础字段', '高级字段', '布局字段']}
|
||||||
bordered={false}
|
ghost
|
||||||
items={categoryOrder
|
bordered={false}
|
||||||
.map((category) => {
|
items={categoryOrder
|
||||||
const components = componentsByCategory[category];
|
.map((category) => {
|
||||||
if (!components || components.length === 0) return null;
|
const components = componentsByCategory[category];
|
||||||
|
if (!components || components.length === 0) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: category,
|
key: category,
|
||||||
label: category,
|
label: category,
|
||||||
children: (
|
children: (
|
||||||
<div className="form-designer-component-list">
|
<div className="form-designer-component-list">
|
||||||
{components.map((component) => (
|
{components.map((component) => (
|
||||||
<DraggableComponent key={component.type} component={component} />
|
<DraggableComponent key={component.type} component={component} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean) as any[]
|
.filter(Boolean) as any[]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
* 预览设计好的表单效果
|
* 预览设计好的表单效果
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } 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 type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from '../types';
|
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from '../types';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
@ -16,7 +16,12 @@ interface FormPreviewProps {
|
|||||||
formConfig: FormConfig;
|
formConfig: FormConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
export interface FormPreviewRef {
|
||||||
|
submit: () => Promise<void>;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, formConfig }, ref) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||||
@ -42,6 +47,12 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
|||||||
message.info('表单已重置');
|
message.info('表单已重置');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
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 [];
|
||||||
@ -307,21 +318,6 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{renderFields(fields)}
|
{renderFields(fields)}
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
style={{ marginTop: 32, marginBottom: 0 }}
|
|
||||||
labelCol={{ span: 0 }}
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8 }}>
|
|
||||||
<Button type="primary" onClick={handleSubmit}>
|
|
||||||
提交
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{Object.keys(formData).length > 0 && (
|
{Object.keys(formData).length > 0 && (
|
||||||
@ -332,7 +328,9 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
FormPreview.displayName = 'FormPreview';
|
||||||
|
|
||||||
export default FormPreview;
|
export default FormPreview;
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,13 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-designer-component-panel-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-designer-component-list {
|
.form-designer-component-list {
|
||||||
@ -56,13 +63,19 @@
|
|||||||
.form-designer-component-item {
|
.form-designer-component-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px;
|
justify-content: center;
|
||||||
|
padding: 8px 6px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-designer-component-item:hover {
|
.form-designer-component-item:hover {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user