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