холст фигуры меняю
This commit is contained in:
@@ -15,6 +15,8 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import { ElementLayout, ElementType, FormLayoutSettings, TemplateElement } from '../../types/template';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
interface VisualLayoutEditorProps {
|
||||
elements: TemplateElement[];
|
||||
@@ -65,7 +67,6 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
|
||||
gridSize,
|
||||
}) => {
|
||||
const layout = element.layout || { x: 50, y: 50, width: 300, height: 80, zIndex: 1 };
|
||||
const elementColor = ELEMENT_COLORS[element.type];
|
||||
|
||||
const handleDragStop = useCallback((_e: any, d: { x: number; y: number }) => {
|
||||
// Вызываем onUpdate только если позиция действительно изменилась,
|
||||
@@ -95,6 +96,57 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
|
||||
onEdit?.();
|
||||
}, [onEdit]);
|
||||
|
||||
/*
|
||||
* Render a lightweight, disabled preview of the element so that the user can
|
||||
* see how the real control looks directly on the canvas instead of a plain
|
||||
* colored square. Keeping the elements disabled and wrapped with
|
||||
* `pointer-events-none` guarantees that they do not interfere with the drag
|
||||
* behaviour of Rnd and keeps rendering costs minimal.
|
||||
*/
|
||||
const previewControl = useMemo(() => {
|
||||
switch (element.type) {
|
||||
case 'text':
|
||||
return <Input readOnly placeholder={element.placeholder || 'Введите текст'} className="pointer-events-none w-full bg-white" />;
|
||||
case 'textarea':
|
||||
return <Textarea readOnly rows={3} placeholder={element.placeholder || 'Введите текст'} className="pointer-events-none w-full resize-none bg-white" />;
|
||||
case 'number':
|
||||
return <Input type="number" readOnly placeholder={element.placeholder || '0'} className="pointer-events-none w-full bg-white" />;
|
||||
case 'date':
|
||||
return <Input type="date" readOnly className="pointer-events-none w-full bg-white" />;
|
||||
case 'select':
|
||||
return (
|
||||
<div className="pointer-events-none w-full">
|
||||
<div className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-white px-3 py-2 text-sm">
|
||||
<span className="text-gray-500">
|
||||
{element.placeholder || 'Выберите значение'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'radio':
|
||||
return (
|
||||
<div className="space-y-2 pointer-events-none">
|
||||
{(element.options?.length ? element.options : [{ label: 'Вариант 1' }]).map((opt, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 rounded-full bg-white" />
|
||||
<span className="text-sm text-gray-900">{opt.label || opt.value || `Вариант ${idx + 1}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="flex items-center gap-2 pointer-events-none">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 rounded bg-white" />
|
||||
<span className="text-sm text-gray-900">{element.placeholder || element.label || 'Отметить'}</span>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [element]);
|
||||
|
||||
return (
|
||||
<Rnd
|
||||
dragGrid={[gridSize, gridSize]}
|
||||
@@ -107,47 +159,73 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
|
||||
}}
|
||||
onDragStop={handleDragStop}
|
||||
onResizeStop={handleResizeStop}
|
||||
minWidth={150}
|
||||
minHeight={40}
|
||||
minWidth={120}
|
||||
minHeight={32}
|
||||
bounds="parent"
|
||||
className={`transition-shadow duration-200 group ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2 z-20' : 'z-10'}`}
|
||||
className={`group ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2 z-20' : 'z-10'}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className={`h-full rounded-lg border-2 p-3 cursor-move ${elementColor} ${isSelected ? 'shadow-xl' : 'shadow-md'} transition-all duration-200`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className={`h-full cursor-move ${
|
||||
isSelected
|
||||
? 'bg-blue-50/20 border border-blue-200/50 rounded-md p-2'
|
||||
: 'bg-transparent border border-transparent rounded-md hover:bg-gray-50/10 p-1'
|
||||
}`}>
|
||||
{/* Label and controls (only visible when selected or hovered) */}
|
||||
<div className={`flex items-center justify-between mb-1 ${
|
||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{ELEMENT_ICONS[element.type]}
|
||||
<span className="font-medium text-sm truncate">{element.label || `${element.type} элемент`}</span>
|
||||
<span className="font-medium text-xs text-gray-600 truncate">
|
||||
{element.label || element.name || `${element.type} элемент`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity hover:bg-blue-100"
|
||||
className="h-4 w-4 cursor-pointer hover:bg-blue-100"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
<Edit className="h-3 w-3 text-blue-600" />
|
||||
<Edit className="h-2.5 w-2.5 text-blue-600" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-100"
|
||||
className="h-4 w-4 cursor-pointer hover:bg-red-100"
|
||||
onClick={handleDeleteClick}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-red-600" />
|
||||
<Trash2 className="h-2.5 w-2.5 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs opacity-75 space-y-1">
|
||||
{element.required && <div className="text-red-700 font-medium">• Обязательное</div>}
|
||||
|
||||
{/* Real element preview */}
|
||||
{previewControl && (
|
||||
<div className="space-y-1 flex-1">
|
||||
{element.label && (
|
||||
<label className="block text-sm font-medium text-gray-900 pointer-events-none">
|
||||
{element.label}
|
||||
{element.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{previewControl}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional info (only visible on selection to preserve visual clarity & perf) */}
|
||||
{isSelected && (
|
||||
<div className="text-xs opacity-75 space-y-1 mt-1">
|
||||
{element.targetCells && element.targetCells.length > 0 && (
|
||||
<div className="font-mono truncate">
|
||||
{element.targetCells.map((cell, i) => <span key={i} className="mr-1.5 p-1 bg-black/5 rounded-sm">{cell.sheet}!{cell.cell}</span>)}
|
||||
{element.targetCells.map((cell, i) => (
|
||||
<span key={i} className="mr-1 px-1 py-0.5 bg-black/5 rounded text-xs">{cell.sheet}!{cell.cell}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Rnd>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user