deploy-ease-platform/frontend/src/components/FormDesigner/components/GridField.tsx
2025-10-24 10:22:34 +08:00

294 lines
8.4 KiB
TypeScript

/**
* 栅格布局字段组件
* 支持拖入子组件
*/
import React from 'react';
import { useDroppable } from '@dnd-kit/core';
import { Row, Col, Button, Space, Tooltip } from 'antd';
import { DeleteOutlined, CopyOutlined, PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import type { FieldConfig } from '../types';
import FieldRenderer from './FieldRenderer';
interface GridFieldProps {
field: FieldConfig;
isPreview?: boolean; // 是否为预览模式
selectedFieldId?: string; // 当前选中的字段ID
onSelectField?: (fieldId: string) => void;
onCopyField?: (fieldId: string) => void;
onPasteToGrid?: (gridId: string, colIndex: number) => void;
onDeleteField?: (fieldId: string) => void;
labelAlign?: 'left' | 'right' | 'top'; // 标签对齐方式
hasClipboard?: boolean;
duplicateFieldIds?: Set<string>; // 冲突的字段ID集合
}
const GridField: React.FC<GridFieldProps> = ({
field,
isPreview = false,
selectedFieldId,
onSelectField,
onCopyField,
onPasteToGrid,
onDeleteField,
labelAlign = 'right',
hasClipboard = false,
duplicateFieldIds = new Set(),
}) => {
const columns = field.columns || 2;
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
key={colIndex}
dropId={dropId}
span={colSpan}
fields={columnFields}
isPreview={isPreview}
selectedFieldId={selectedFieldId}
onSelectField={onSelectField}
onCopyField={onCopyField}
onPasteToGrid={onPasteToGrid}
onDeleteField={onDeleteField}
labelAlign={labelAlign}
gridId={field.id}
colIndex={colIndex}
hasClipboard={hasClipboard}
duplicateFieldIds={duplicateFieldIds}
/>
);
})}
</Row>
</div>
);
};
// 栅格列组件
interface GridColumnProps {
dropId: string;
span: number;
fields: FieldConfig[];
isPreview?: boolean;
selectedFieldId?: string;
onSelectField?: (fieldId: string) => void;
onCopyField?: (fieldId: string) => void;
onPasteToGrid?: (gridId: string, colIndex: number) => void;
onDeleteField?: (fieldId: string) => void;
labelAlign?: 'left' | 'right' | 'top';
gridId?: string;
colIndex?: number;
hasClipboard?: boolean;
duplicateFieldIds?: Set<string>; // 冲突的字段ID集合
}
const GridColumn: React.FC<GridColumnProps> = ({
dropId,
span,
fields,
isPreview = false,
selectedFieldId,
onSelectField,
onCopyField,
onPasteToGrid,
onDeleteField,
labelAlign = 'right',
gridId,
colIndex,
hasClipboard = false,
duplicateFieldIds = new Set(),
}) => {
const { setNodeRef, isOver } = useDroppable({
id: dropId,
data: {
accept: 'field',
gridId,
colIndex,
},
});
// 预览模式下的样式
if (isPreview) {
return (
<Col span={span}>
<div>
{fields.map((childField) => (
<div key={childField.id} style={{ marginBottom: 8 }}>
<FieldRenderer
field={childField}
isPreview={isPreview}
labelAlign={labelAlign}
duplicateFieldIds={duplicateFieldIds}
/>
</div>
))}
</div>
</Col>
);
}
// 设计模式下的样式
return (
<Col span={span}>
<div
ref={setNodeRef}
style={{
minHeight: fields.length === 0 ? 80 : 'auto',
padding: 12,
border: `2px dashed ${isOver ? '#1890ff' : '#d9d9d9'}`,
borderRadius: 6,
background: isOver ? '#f0f5ff' : '#fafafa',
transition: 'all 0.3s ease',
position: 'relative',
}}
>
{/* 栅格占位标签和粘贴按钮 */}
<div
style={{
position: 'absolute',
top: 4,
right: 4,
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
{hasClipboard && gridId !== undefined && colIndex !== undefined && (
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onPasteToGrid?.(gridId, colIndex);
}}
style={{
fontSize: 10,
padding: '0 4px',
height: 18,
}}
/>
)}
<div
style={{
fontSize: 10,
color: '#bfbfbf',
background: '#fff',
padding: '2px 6px',
borderRadius: 3,
border: '1px solid #e8e8e8',
}}
>
{span}/24
</div>
</div>
{fields.length === 0 ? (
<div
style={{
textAlign: 'center',
color: '#8c8c8c',
fontSize: 12,
padding: '20px 0',
}}
>
</div>
) : (
<div>
{fields.map((childField) => (
<div
key={childField.id}
style={{
marginBottom: 8,
padding: 8,
border: duplicateFieldIds.has(childField.id)
? '2px solid #ff4d4f' // 冲突:红色边框
: selectedFieldId === childField.id
? '2px solid #1890ff' // 选中:蓝色边框
: '1px solid #e8e8e8', // 正常:灰色边框
borderRadius: 4,
background: duplicateFieldIds.has(childField.id) ? '#fff1f0' : '#fff', // 冲突:浅红色背景
position: 'relative',
cursor: 'pointer',
}}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
onSelectField?.(childField.id);
}}
>
{/* 冲突警告图标 */}
{duplicateFieldIds.has(childField.id) && (
<Tooltip title="字段名称与其他字段重复">
<ExclamationCircleOutlined
style={{
position: 'absolute',
top: 4,
left: 4,
color: '#ff4d4f',
fontSize: 16,
zIndex: 10,
}}
/>
</Tooltip>
)}
<FieldRenderer field={childField} isPreview={isPreview} labelAlign={labelAlign} />
{/* 字段操作按钮 */}
<div
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 10, // 确保按钮在最上层
}}
>
<Space size="small">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onCopyField?.(childField.id);
}}
/>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDeleteField?.(childField.id);
}}
/>
</Space>
</div>
</div>
))}
</div>
)}
</div>
</Col>
);
};
export default GridField;