表单设计器

This commit is contained in:
dengqichen 2025-10-23 21:54:46 +08:00
parent f880c5f59d
commit 0dabb6aef7
5 changed files with 168 additions and 65 deletions

View File

@ -49,6 +49,9 @@ const DraggableComponent: React.FC<{ component: ComponentMeta }> = ({ component
const ComponentPanel: React.FC = () => {
const componentsByCategory = getComponentsByCategory();
// 定义分类显示顺序
const categoryOrder = ['布局字段', '基础字段', '高级字段'];
return (
<div className="form-designer-component-panel">
<div className="form-designer-component-panel-header">
@ -60,15 +63,20 @@ const ComponentPanel: React.FC = () => {
ghost
bordered={false}
>
{Object.entries(componentsByCategory).map(([category, components]) => (
<Panel header={category} key={category}>
<div className="form-designer-component-list">
{components.map((component) => (
<DraggableComponent key={component.type} component={component} />
))}
</div>
</Panel>
))}
{categoryOrder.map((category) => {
const components = componentsByCategory[category];
if (!components || components.length === 0) return null;
return (
<Panel header={category} key={category}>
<div className="form-designer-component-list">
{components.map((component) => (
<DraggableComponent key={component.type} component={component} />
))}
</div>
</Panel>
);
})}
</Collapse>
</div>
);

View File

@ -28,14 +28,22 @@ const GridField: React.FC<GridFieldProps> = ({
labelAlign = 'right',
}) => {
const columns = field.columns || 2;
const colSpan = 24 / columns;
const children = field.children || Array(columns).fill([]);
// 使用自定义列宽度或平均分配
const getColSpan = (colIndex: number) => {
if (field.columnSpans && field.columnSpans.length > colIndex) {
return field.columnSpans[colIndex];
}
return 24 / columns; // 平均分配
};
return (
<div style={{ padding: '8px 0', width: '100%' }}>
<Row gutter={field.gutter || 16}>
{children.map((columnFields, colIndex) => {
const dropId = `grid-${field.id}-col-${colIndex}`;
const colSpan = getColSpan(colIndex);
return (
<GridColumn

View File

@ -45,23 +45,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
const handleFieldUpdate = (changedValues: any) => {
if (selectedField) {
let updatedField = { ...selectedField, ...changedValues };
// 特殊处理:栅格列数变化时,调整 children 数组
if (selectedField.type === 'grid' && changedValues.columns !== undefined) {
const newColumns = changedValues.columns;
const oldChildren = selectedField.children || [];
const newChildren: FieldConfig[][] = [];
// 保留旧数据,调整数组长度
for (let i = 0; i < newColumns; i++) {
newChildren[i] = oldChildren[i] || [];
}
updatedField = { ...updatedField, children: newChildren };
}
onFieldChange(updatedField);
onFieldChange({ ...selectedField, ...changedValues });
}
};
@ -111,15 +95,23 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
onValuesChange={handleFieldUpdate}
style={{ padding: 16 }}
>
<Form.Item label="字段标签" name="label" rules={[{ required: true }]}>
<Input placeholder="请输入字段标签" />
</Form.Item>
{/* 布局组件不显示字段标签和字段名称 */}
{selectedField.type !== 'divider' && selectedField.type !== 'grid' && selectedField.type !== 'text' && (
<>
<Form.Item label="字段标签" name="label" rules={[{ required: true }]}>
<Input placeholder="请输入字段标签" />
</Form.Item>
<Form.Item label="字段名称" name="name" rules={[{ required: true }]}>
<Input placeholder="请输入字段名称(英文)" />
</Form.Item>
<Form.Item label="字段名称" name="name" rules={[{ required: true }]}>
<Input placeholder="请输入字段名称(英文)" />
</Form.Item>
</>
)}
{selectedField.type !== 'divider' && (
{/* 只有实际表单字段才显示这些属性 */}
{selectedField.type !== 'divider' &&
selectedField.type !== 'grid' &&
selectedField.type !== 'text' && (
<>
<Form.Item label="占位提示" name="placeholder">
<Input placeholder="请输入占位提示" />
@ -166,9 +158,102 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
{selectedField.type === 'grid' && (
<>
<Form.Item label="列数" name="columns">
<InputNumber min={1} max={24} style={{ width: '100%' }} />
</Form.Item>
<Divider style={{ margin: '16px 0' }} />
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong></Text>
<Button
type="dashed"
size="small"
icon={<PlusOutlined />}
onClick={() => {
// 如果没有配置 columnSpans先初始化为当前列数的平均分配
const currentSpans = selectedField.columnSpans || (() => {
const cols = selectedField.columns || 2;
const avgSpan = Math.floor(24 / cols);
return Array(cols).fill(avgSpan);
})();
const total = currentSpans.reduce((a, b) => a + b, 0);
if (total >= 24) {
return;
}
const newSpan = Math.min(12, 24 - total);
const newSpans = [...currentSpans, newSpan];
const currentChildren = selectedField.children || [];
const newChildren = [...currentChildren, []];
onFieldChange({
...selectedField,
columnSpans: newSpans,
columns: newSpans.length,
children: newChildren,
});
}}
disabled={
selectedField.columnSpans &&
selectedField.columnSpans.reduce((a: number, b: number) => a + b, 0) >= 24
}
>
</Button>
</div>
<Space direction="vertical" style={{ width: '100%' }} size="small">
{(selectedField.columnSpans || [12, 12]).map((span, index) => (
<Space key={index} style={{ display: 'flex', width: '100%' }} align="center">
<Text style={{ minWidth: 20 }}>{index + 1}.</Text>
<InputNumber
min={1}
max={24}
value={span}
onChange={(value) => {
if (value) {
const currentSpans = selectedField.columnSpans || [12, 12];
const newSpans = [...currentSpans];
newSpans[index] = value;
onFieldChange({
...selectedField,
columnSpans: newSpans,
});
}
}}
style={{ flex: 1 }}
addonAfter="/24"
/>
<Button
icon={<DeleteOutlined />}
danger
onClick={() => {
const currentSpans = selectedField.columnSpans || [12, 12];
const newSpans = currentSpans.filter((_, i) => i !== index);
const newChildren = (selectedField.children || [[], []]).filter((_, i) => i !== index);
// 如果删除后没有列了重置为默认的2列配置
if (newSpans.length === 0) {
onFieldChange({
...selectedField,
columnSpans: [12, 12],
columns: 2,
children: [[], []],
});
} else {
onFieldChange({
...selectedField,
columnSpans: newSpans,
columns: newSpans.length,
children: newChildren,
});
}
}}
/>
</Space>
))}
</Space>
<div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
{(selectedField.columnSpans || [12, 12]).reduce((a: number, b: number) => a + b, 0)} / 24
</div>
<Divider style={{ margin: '16px 0' }} />
<Form.Item label="间距" name="gutter">
<InputNumber min={0} max={48} style={{ width: '100%' }} addonAfter="px" />
</Form.Item>

View File

@ -34,6 +34,26 @@ export interface ComponentMeta {
// 组件列表配置
export const COMPONENT_LIST: ComponentMeta[] = [
// 布局字段
{
type: 'grid',
label: '栅格布局',
icon: BorderOutlined,
category: '布局字段',
defaultConfig: {
columns: 2,
columnSpans: [12, 12], // 默认两列各占12格
gutter: 16,
children: [[], []], // 初始化两列空数组
},
},
{
type: 'divider',
label: '分割线',
icon: MinusOutlined,
category: '布局字段',
defaultConfig: {},
},
// 基础字段
{
type: 'input',
@ -131,6 +151,15 @@ export const COMPONENT_LIST: ComponentMeta[] = [
placeholder: '请选择时间',
},
},
{
type: 'text',
label: '文字',
icon: FileTextOutlined,
category: '基础字段',
defaultConfig: {
content: '这是一段文字',
},
},
// 高级字段
{
type: 'switch',
@ -180,34 +209,6 @@ export const COMPONENT_LIST: ComponentMeta[] = [
] as any,
},
},
// 布局字段
{
type: 'text',
label: '文字',
icon: FileTextOutlined,
category: '布局字段',
defaultConfig: {
content: '这是一段文字',
},
},
{
type: 'grid',
label: '栅格布局',
icon: BorderOutlined,
category: '布局字段',
defaultConfig: {
columns: 2,
gutter: 16,
children: [[], []], // 初始化两列空数组
},
},
{
type: 'divider',
label: '分割线',
icon: MinusOutlined,
category: '布局字段',
defaultConfig: {},
},
];
// 根据分类分组组件

View File

@ -49,6 +49,7 @@ export interface FieldConfig {
span?: number; // 栅格占位格数(该字段在栅格中占几列)
content?: string; // 文本内容(用于 text 组件)
columns?: number; // 栅格列数(用于 grid 组件)
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2]总和不超过24
gutter?: number; // 栅格间距(用于 grid 组件)
children?: FieldConfig[][]; // 子字段(用于容器组件,二维数组,每个子数组代表一列)
}