diff --git a/frontend/package.json b/frontend/package.json index 7b8a04e2..e31a5495 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,9 @@ "dependencies": { "@ant-design/icons": "^5.2.6", "@ant-design/pro-components": "^2.8.2", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.9.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.12", diff --git a/frontend/src/pages/FormDesigner/components/ComponentPanel.tsx b/frontend/src/pages/FormDesigner/components/ComponentPanel.tsx new file mode 100644 index 00000000..64ea9461 --- /dev/null +++ b/frontend/src/pages/FormDesigner/components/ComponentPanel.tsx @@ -0,0 +1,78 @@ +/** + * 左侧组件面板 + * 显示可拖拽的表单组件列表 + */ + +import React from 'react'; +import { useDraggable } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import { Collapse, Typography } from 'antd'; +import type { ComponentMeta } from '../config'; +import { getComponentsByCategory } from '../config'; +import '../styles.css'; + +const { Panel } = Collapse; +const { Text } = Typography; + +// 可拖拽组件项 +const DraggableComponent: React.FC<{ component: ComponentMeta }> = ({ component }) => { + const { attributes, listeners, setNodeRef, transform } = useDraggable({ + id: `new-${component.type}`, + data: { + type: component.type, + isNew: true, + }, + }); + + const style = { + transform: CSS.Translate.toString(transform), + cursor: 'move', + }; + + const Icon = component.icon; + + return ( +
+ + {component.label} +
+ ); +}; + +// 组件面板 +const ComponentPanel: React.FC = () => { + const componentsByCategory = getComponentsByCategory(); + + return ( +
+
+ 组件列表 +
+ + + {Object.entries(componentsByCategory).map(([category, components]) => ( + +
+ {components.map((component) => ( + + ))} +
+
+ ))} +
+
+ ); +}; + +export default ComponentPanel; + diff --git a/frontend/src/pages/FormDesigner/components/DesignCanvas.tsx b/frontend/src/pages/FormDesigner/components/DesignCanvas.tsx new file mode 100644 index 00000000..082d7005 --- /dev/null +++ b/frontend/src/pages/FormDesigner/components/DesignCanvas.tsx @@ -0,0 +1,80 @@ +/** + * 中间设计画布 + * 可拖放字段的区域 + */ + +import React from 'react'; +import { useDroppable } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { Empty, Typography } from 'antd'; +import FieldItem from './FieldItem'; +import type { FieldConfig } from '../types'; +import '../styles.css'; + +const { Text } = Typography; + +interface DesignCanvasProps { + fields: FieldConfig[]; + selectedFieldId?: string; + onSelectField: (fieldId: string) => void; + onDeleteField: (fieldId: string) => void; + labelAlign?: 'left' | 'right' | 'top'; +} + +const DesignCanvas: React.FC = ({ + fields, + selectedFieldId, + onSelectField, + onDeleteField, + labelAlign = 'right', +}) => { + const { setNodeRef } = useDroppable({ + id: 'canvas', + }); + + return ( +
+
+ 表单设计 +
+ +
+ {fields.length === 0 ? ( +
+ +
+ ) : ( + f.id)} + strategy={verticalListSortingStrategy} + > +
+ {fields.map((field) => ( + onSelectField(field.id)} + onDelete={() => onDeleteField(field.id)} + selectedFieldId={selectedFieldId} + onSelectField={onSelectField} + onDeleteField={onDeleteField} + labelAlign={labelAlign} + /> + ))} +
+
+ )} +
+
+ ); +}; + +export default DesignCanvas; + diff --git a/frontend/src/pages/FormDesigner/components/FieldItem.tsx b/frontend/src/pages/FormDesigner/components/FieldItem.tsx new file mode 100644 index 00000000..8647eff0 --- /dev/null +++ b/frontend/src/pages/FormDesigner/components/FieldItem.tsx @@ -0,0 +1,102 @@ +/** + * 画布中的字段项 + * 支持拖拽排序、选中、删除 + */ + +import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Button, Space } from 'antd'; +import { HolderOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'; +import type { FieldConfig } from '../types'; +import FieldRenderer from './FieldRenderer'; +import '../styles.css'; + +interface FieldItemProps { + field: FieldConfig; + isSelected: boolean; + onSelect: () => void; + onDelete: () => void; + selectedFieldId?: string; // 传递给栅格内部字段 + onSelectField?: (fieldId: string) => void; + onDeleteField?: (fieldId: string) => void; + labelAlign?: 'left' | 'right' | 'top'; // 标签对齐方式 +} + +const FieldItem: React.FC = ({ + field, + isSelected, + onSelect, + onDelete, + selectedFieldId, + onSelectField, + onDeleteField, + labelAlign = 'right', +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: field.id, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
+ +
+ +
+ +
+ +
+ +
+
+ ); +}; + +export default FieldItem; + diff --git a/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx b/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx new file mode 100644 index 00000000..fd6379ec --- /dev/null +++ b/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx @@ -0,0 +1,264 @@ +/** + * 字段渲染器 + * 根据字段类型渲染对应的表单组件 + */ + +import React from 'react'; +import { + Form, + Input, + InputNumber, + Select, + Radio, + Checkbox, + DatePicker, + TimePicker, + Switch, + Slider, + Rate, + Upload, + Cascader, + Divider, + Button, + Typography, +} from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import type { FieldConfig } from '../types'; +import GridField from './GridField'; + +const { Text } = Typography; + +const { TextArea } = Input; + +interface FieldRendererProps { + field: FieldConfig; + value?: any; + onChange?: (value: any) => void; + isPreview?: boolean; // 是否为预览模式 + selectedFieldId?: string; // 当前选中的字段ID + onSelectField?: (fieldId: string) => void; + onDeleteField?: (fieldId: string) => void; + labelAlign?: 'left' | 'right' | 'top'; // 标签对齐方式 +} + +const FieldRenderer: React.FC = ({ + field, + value, + onChange, + isPreview = false, + selectedFieldId, + onSelectField, + onDeleteField, + labelAlign = 'right', +}) => { + // 布局组件特殊处理 + if (field.type === 'divider') { + return {field.label}; + } + + if (field.type === 'text') { + return ( +
+ {field.content || '这是一段文字'} +
+ ); + } + + if (field.type === 'grid') { + return ( + + ); + } + + const renderField = () => { + switch (field.type) { + case 'input': + return ( + onChange?.(e.target.value)} + /> + ); + + case 'textarea': + return ( +