холст фигуры меняю

This commit is contained in:
2025-07-16 19:07:20 +03:00
parent 6933fab3bb
commit dfb4d377d0

View File

@@ -15,6 +15,8 @@ import React, { useCallback, useMemo, useState } from 'react';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { ElementLayout, ElementType, FormLayoutSettings, TemplateElement } from '../../types/template'; import { ElementLayout, ElementType, FormLayoutSettings, TemplateElement } from '../../types/template';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Textarea } from '../ui/textarea';
interface VisualLayoutEditorProps { interface VisualLayoutEditorProps {
elements: TemplateElement[]; elements: TemplateElement[];
@@ -65,7 +67,6 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
gridSize, gridSize,
}) => { }) => {
const layout = element.layout || { x: 50, y: 50, width: 300, height: 80, zIndex: 1 }; 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 }) => { const handleDragStop = useCallback((_e: any, d: { x: number; y: number }) => {
// Вызываем onUpdate только если позиция действительно изменилась, // Вызываем onUpdate только если позиция действительно изменилась,
@@ -95,6 +96,57 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
onEdit?.(); onEdit?.();
}, [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 ( return (
<Rnd <Rnd
dragGrid={[gridSize, gridSize]} dragGrid={[gridSize, gridSize]}
@@ -107,47 +159,73 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(({
}} }}
onDragStop={handleDragStop} onDragStop={handleDragStop}
onResizeStop={handleResizeStop} onResizeStop={handleResizeStop}
minWidth={150} minWidth={120}
minHeight={40} minHeight={32}
bounds="parent" 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} 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={`h-full cursor-move ${
<div className="flex items-center justify-between mb-2"> 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"> <div className="flex items-center gap-2">
{ELEMENT_ICONS[element.type]} <span className="font-medium text-xs text-gray-600 truncate">
<span className="font-medium text-sm truncate">{element.label || `${element.type} элемент`}</span> {element.label || element.name || `${element.type} элемент`}
</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{onEdit && ( {onEdit && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" 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} onClick={handleEditClick}
> >
<Edit className="h-3 w-3 text-blue-600" /> <Edit className="h-2.5 w-2.5 text-blue-600" />
</Button> </Button>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" 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} onClick={handleDeleteClick}
> >
<Trash2 className="h-3 w-3 text-red-600" /> <Trash2 className="h-2.5 w-2.5 text-red-600" />
</Button> </Button>
</div> </div>
</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 && ( {element.targetCells && element.targetCells.length > 0 && (
<div className="font-mono truncate"> <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> </div>
)}
</div> </div>
</Rnd> </Rnd>
); );