表单设计器
This commit is contained in:
parent
f880c5f59d
commit
0dabb6aef7
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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: {},
|
||||
},
|
||||
];
|
||||
|
||||
// 根据分类分组组件
|
||||
|
||||
@ -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[][]; // 子字段(用于容器组件,二维数组,每个子数组代表一列)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user