294 lines
8.4 KiB
TypeScript
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;
|
|
|