переименования папок
This commit is contained in:
970
src/component/TemplateManager/ElementConstructor.tsx
Normal file
970
src/component/TemplateManager/ElementConstructor.tsx
Normal file
@@ -0,0 +1,970 @@
|
||||
import {
|
||||
getAvailableElementTypes,
|
||||
getElementDefinition,
|
||||
} from '@/lib/element-registry'
|
||||
import {
|
||||
autoArrangeElements,
|
||||
findFreePosition,
|
||||
generateDefaultLayout,
|
||||
} from '@/lib/utils'
|
||||
import {
|
||||
CellTarget,
|
||||
ElementType,
|
||||
FormLayoutSettings,
|
||||
Template,
|
||||
TemplateElement,
|
||||
} from '@/type/template'
|
||||
import {
|
||||
Calendar,
|
||||
Droplets,
|
||||
Edit3,
|
||||
Gauge,
|
||||
Plus,
|
||||
Radio,
|
||||
Thermometer,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Checkbox } from '../ui/checkbox'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select'
|
||||
import { Textarea } from '../ui/textarea'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
interface ElementFormProps {
|
||||
formData: Partial<TemplateElement>
|
||||
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
|
||||
}
|
||||
|
||||
const CellTargetEditor: React.FC<{
|
||||
targets: CellTarget[]
|
||||
onChange: (targets: CellTarget[]) => void
|
||||
}> = ({ targets, onChange }) => {
|
||||
const handleAddTarget = () => {
|
||||
onChange([...targets, { sheet: 'L', cell: '' }])
|
||||
}
|
||||
|
||||
const handleUpdateTarget = (
|
||||
index: number,
|
||||
field: keyof CellTarget,
|
||||
value: string
|
||||
) => {
|
||||
const updatedTargets = targets.map((target, i) =>
|
||||
i === index ? { ...target, [field]: value } : target
|
||||
)
|
||||
onChange(updatedTargets)
|
||||
}
|
||||
|
||||
const handleRemoveTarget = (index: number) => {
|
||||
onChange(targets.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Целевые ячейки</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddTarget}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{targets.map((target, index) => (
|
||||
<div key={index} className="rounded border bg-gray-50/80 p-2">
|
||||
<div className="mb-1.5 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Лист
|
||||
</label>
|
||||
<Select
|
||||
value={target.sheet}
|
||||
onValueChange={value =>
|
||||
handleUpdateTarget(index, 'sheet', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="L">L (Левый)</SelectItem>
|
||||
<SelectItem value="R">R (Правый)</SelectItem>
|
||||
<SelectItem value="Report">Report</SelectItem>
|
||||
<SelectItem value="Calculations">Calculations</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Ячейка
|
||||
</label>
|
||||
<Input
|
||||
placeholder="A1, B5..."
|
||||
value={target.cell}
|
||||
onChange={e =>
|
||||
handleUpdateTarget(index, 'cell', e.target.value)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTarget(index)}
|
||||
className="h-5 px-1 text-xs text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="mr-1 h-2.5 w-2.5" />
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{targets.length === 0 && (
|
||||
<p className="py-3 text-center text-xs text-gray-500">
|
||||
Добавьте ячейки для связи с элементом
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Live Preview компонент
|
||||
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||||
element,
|
||||
}) => {
|
||||
const elementDefinition = element.type
|
||||
? getElementDefinition(element.type)
|
||||
: null
|
||||
|
||||
if (elementDefinition && elementDefinition.Preview) {
|
||||
return <elementDefinition.Preview config={element as any} />
|
||||
}
|
||||
|
||||
// Fallback для старых элементов
|
||||
const renderPreviewInput = () => {
|
||||
switch (element.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<Input
|
||||
placeholder={element.placeholder || 'Введите текст'}
|
||||
disabled
|
||||
className="w-full"
|
||||
/>
|
||||
)
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<Textarea
|
||||
placeholder={element.placeholder || 'Введите текст'}
|
||||
disabled
|
||||
rows={3}
|
||||
className="w-full resize-none"
|
||||
/>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={element.placeholder || '0'}
|
||||
disabled
|
||||
className="w-full"
|
||||
/>
|
||||
)
|
||||
|
||||
case 'date':
|
||||
return <Input type="date" disabled className="w-full" />
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select disabled>
|
||||
<SelectTrigger className="w-full">
|
||||
<span className="text-gray-500">
|
||||
{element.placeholder || 'Выберите значение'}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'radio':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{element.options?.length ? (
|
||||
element.options.map((option, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<div className="h-4 w-4 rounded-full border border-gray-300" />
|
||||
<span className="text-sm">{option.label}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-4 w-4 rounded-full border border-gray-300" />
|
||||
<span className="text-sm text-gray-500">Вариант 1</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-4 w-4 rounded border border-gray-300" />
|
||||
<span className="text-sm">
|
||||
{element.placeholder || element.label || 'Отметить'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'standards':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-md border border-input bg-background p-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{element.placeholder || 'Выберите эталоны'}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 opacity-50" />
|
||||
<span className="text-xs text-muted-foreground">Выбрать</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Выбрано эталонов: 0
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'calibration-conditions':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2">
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Thermometer className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">20°C</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Droplets className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">65%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Gauge className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">101.3кПа</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">220В</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Radio className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">50Гц</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0">
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'button-group':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{element.options?.length ? (
|
||||
element.options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-all duration-200 hover:border-primary/30 hover:bg-primary/5"
|
||||
>
|
||||
{option.label || `Кнопка ${index + 1}`}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-1.5 text-sm text-muted-foreground">
|
||||
{element.placeholder || 'Добавьте кнопки'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||||
Выберите тип элемента
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
{element.label && (
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||
{element.label}
|
||||
{element.required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{renderPreviewInput()}
|
||||
{element.targetCells && element.targetCells.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{element.targetCells.map((target, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
|
||||
>
|
||||
{target.sheet}!{target.cell}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
||||
const elementDefinition = formData.type
|
||||
? getElementDefinition(formData.type)
|
||||
: null
|
||||
|
||||
const handleAddOption = () => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
options: [...(prev.options || []), { value: '', label: '' }],
|
||||
}))
|
||||
}
|
||||
|
||||
const handleUpdateOption = (
|
||||
index: number,
|
||||
field: 'value' | 'label',
|
||||
value: string
|
||||
) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
options: prev.options?.map((option, i) =>
|
||||
i === index ? { ...option, [field]: value } : option
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const handleRemoveOption = (index: number) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
options: prev.options?.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
const needsOptions =
|
||||
formData.type === 'select' ||
|
||||
formData.type === 'radio' ||
|
||||
formData.type === 'button-group'
|
||||
|
||||
// Если есть определение элемента в реестре, используем его редактор
|
||||
if (elementDefinition && elementDefinition.Editor) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Форма настроек */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Тип элемента</label>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={value => {
|
||||
const newType = value as ElementType
|
||||
console.log('Element type changed to:', newType)
|
||||
|
||||
setFormData(prev => {
|
||||
const updates: Partial<TemplateElement> = { type: newType }
|
||||
|
||||
// Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию
|
||||
if (
|
||||
newType === 'standards' ||
|
||||
newType === 'calibration-conditions'
|
||||
) {
|
||||
console.log(
|
||||
`${newType} type selected, getting element definition`
|
||||
)
|
||||
const elementDefinition = getElementDefinition(newType)
|
||||
console.log('Element definition:', elementDefinition)
|
||||
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
const defaultConfig =
|
||||
elementDefinition.defaultConfig as any
|
||||
console.log('Default config:', defaultConfig)
|
||||
updates.targetCells =
|
||||
elementDefinition.mapToCells?.(defaultConfig) || []
|
||||
console.log('Generated targetCells:', updates.targetCells)
|
||||
}
|
||||
}
|
||||
|
||||
const newFormData = { ...prev, ...updates }
|
||||
console.log('New form data:', newFormData)
|
||||
return newFormData
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getAvailableElementTypes().map(({ type, label, icon }) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Подпись</label>
|
||||
<Input
|
||||
placeholder="Название поля"
|
||||
value={formData.label}
|
||||
onChange={e =>
|
||||
setFormData(prev => ({ ...prev, label: e.target.value }))
|
||||
}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Подсказка</label>
|
||||
<Input
|
||||
placeholder="Введите значение..."
|
||||
value={formData.placeholder}
|
||||
onChange={e =>
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
placeholder: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 py-1">
|
||||
<Checkbox
|
||||
id="required"
|
||||
checked={formData.required}
|
||||
onCheckedChange={checked =>
|
||||
setFormData(prev => ({ ...prev, required: !!checked }))
|
||||
}
|
||||
/>
|
||||
<label htmlFor="required" className="text-sm font-medium">
|
||||
Обязательное поле
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<CellTargetEditor
|
||||
targets={formData.targetCells || []}
|
||||
onChange={targets =>
|
||||
setFormData(prev => ({ ...prev, targetCells: targets }))
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Специфичный редактор элемента */}
|
||||
<elementDefinition.Editor
|
||||
config={formData as any}
|
||||
onChange={newConfig =>
|
||||
setFormData(prev => ({ ...prev, ...newConfig }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Live Preview */}
|
||||
<div className="sticky top-0 flex min-h-[50vh] items-center">
|
||||
<ElementPreview element={formData} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback для старых элементов
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Форма настроек */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Тип элемента</label>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={value => {
|
||||
const newType = value as ElementType
|
||||
console.log('Element type changed to (fallback):', newType)
|
||||
|
||||
setFormData(prev => {
|
||||
const updates: Partial<TemplateElement> = { type: newType }
|
||||
|
||||
// Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию
|
||||
if (
|
||||
newType === 'standards' ||
|
||||
newType === 'calibration-conditions'
|
||||
) {
|
||||
console.log(
|
||||
`${newType} type selected (fallback), getting element definition`
|
||||
)
|
||||
const elementDefinition = getElementDefinition(newType)
|
||||
console.log(
|
||||
'Element definition (fallback):',
|
||||
elementDefinition
|
||||
)
|
||||
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
const defaultConfig = elementDefinition.defaultConfig as any
|
||||
console.log('Default config (fallback):', defaultConfig)
|
||||
updates.targetCells =
|
||||
elementDefinition.mapToCells?.(defaultConfig) || []
|
||||
console.log(
|
||||
'Generated targetCells (fallback):',
|
||||
updates.targetCells
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const newFormData = { ...prev, ...updates }
|
||||
console.log('New form data (fallback):', newFormData)
|
||||
return newFormData
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getAvailableElementTypes().map(({ type, label, icon }) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Подпись</label>
|
||||
<Input
|
||||
placeholder="Название поля"
|
||||
value={formData.label}
|
||||
onChange={e =>
|
||||
setFormData(prev => ({ ...prev, label: e.target.value }))
|
||||
}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CellTargetEditor
|
||||
targets={formData.targetCells || []}
|
||||
onChange={targets =>
|
||||
setFormData(prev => ({ ...prev, targetCells: targets }))
|
||||
}
|
||||
/>
|
||||
|
||||
{needsOptions && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Варианты</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddOption}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-28 space-y-1.5 overflow-y-auto">
|
||||
{formData.options?.map((option, index) => (
|
||||
<div key={index} className="flex gap-1.5">
|
||||
<Input
|
||||
placeholder="Значение"
|
||||
value={option.value}
|
||||
onChange={e =>
|
||||
handleUpdateOption(index, 'value', e.target.value)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Подпись"
|
||||
value={option.label}
|
||||
onChange={e =>
|
||||
handleUpdateOption(index, 'label', e.target.value)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveOption(index)}
|
||||
className="h-7 w-7 text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Live Preview */}
|
||||
<div className="sticky top-0 flex min-h-[50vh] items-center">
|
||||
<ElementPreview element={formData} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CanvasSettingsProps {
|
||||
settings: FormLayoutSettings
|
||||
onChange: (settings: FormLayoutSettings) => void
|
||||
}
|
||||
|
||||
const CanvasSettings: React.FC<CanvasSettingsProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Размер сетки (px)</label>
|
||||
<Select
|
||||
value={settings.gridSize.toString()}
|
||||
onValueChange={value =>
|
||||
onChange({ ...settings, gridSize: parseInt(value) })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10px</SelectItem>
|
||||
<SelectItem value="20">20px</SelectItem>
|
||||
<SelectItem value="25">25px</SelectItem>
|
||||
<SelectItem value="50">50px</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="showGrid"
|
||||
checked={settings.showGrid}
|
||||
onCheckedChange={checked =>
|
||||
onChange({ ...settings, showGrid: !!checked })
|
||||
}
|
||||
/>
|
||||
<label htmlFor="showGrid" className="text-sm font-medium">
|
||||
Показывать сетку
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ElementConstructorProps {
|
||||
template: Template
|
||||
onElementAdd: (element: TemplateElement) => void
|
||||
// Принимаем частичное обновление элемента, чтобы корректно обновлять только изменённые поля
|
||||
onElementUpdate: (
|
||||
elementId: string,
|
||||
updates: Partial<TemplateElement>
|
||||
) => void
|
||||
onElementDelete: (elementId: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
template,
|
||||
onElementAdd,
|
||||
onElementUpdate,
|
||||
onElementDelete,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const elements = template.elements
|
||||
const layoutSettings = template.layoutSettings || {
|
||||
gridSize: 20,
|
||||
showGrid: true,
|
||||
}
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
||||
null
|
||||
)
|
||||
const [formData, setFormData] = useState<Partial<TemplateElement>>({
|
||||
type: 'text',
|
||||
name: '',
|
||||
label: '',
|
||||
targetCells: [],
|
||||
placeholder: '',
|
||||
required: false,
|
||||
options: [],
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
type: 'text',
|
||||
label: '',
|
||||
targetCells: [],
|
||||
placeholder: '',
|
||||
required: false,
|
||||
options: [],
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddElement = () => {
|
||||
console.log('handleAddElement called with formData:', formData)
|
||||
|
||||
if (!formData.label) {
|
||||
console.log('Validation failed: missing label')
|
||||
return
|
||||
}
|
||||
|
||||
// Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны
|
||||
if (
|
||||
formData.type !== 'standards' &&
|
||||
formData.type !== 'calibration-conditions' &&
|
||||
formData.type !== 'button-group' &&
|
||||
!formData.targetCells?.length
|
||||
) {
|
||||
console.log(
|
||||
'Validation failed: missing targetCells for element that requires manual targetCells'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем layout для нового элемента
|
||||
const existingLayouts = elements.map(e => e.layout).filter(Boolean)
|
||||
const defaultLayout = generateDefaultLayout(elements.length)
|
||||
|
||||
const layout = findFreePosition(
|
||||
defaultLayout,
|
||||
existingLayouts as any[],
|
||||
layoutSettings.gridSize
|
||||
)
|
||||
|
||||
// Специальная обработка для элементов с автоматической генерацией targetCells
|
||||
let targetCells = formData.targetCells || []
|
||||
if (
|
||||
formData.type === 'standards' ||
|
||||
formData.type === 'calibration-conditions'
|
||||
) {
|
||||
// Генерируем targetCells автоматически, если их нет
|
||||
if (!targetCells.length) {
|
||||
const elementDefinition = getElementDefinition(formData.type)
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
const defaultConfig = elementDefinition.defaultConfig as any
|
||||
targetCells = elementDefinition.mapToCells?.(defaultConfig) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Для button-group не нужны targetCells, так как это UI элемент
|
||||
if (formData.type === 'button-group') {
|
||||
targetCells = []
|
||||
}
|
||||
|
||||
const newElement: TemplateElement = {
|
||||
id: Date.now().toString(),
|
||||
type: formData.type as ElementType,
|
||||
name: formData.label?.toLowerCase().replace(/\s+/g, '_') || '',
|
||||
label: formData.label,
|
||||
targetCells,
|
||||
placeholder: formData.placeholder,
|
||||
required: formData.required,
|
||||
options: formData.options,
|
||||
order: elements.length,
|
||||
layout,
|
||||
}
|
||||
|
||||
onElementAdd(newElement)
|
||||
resetForm()
|
||||
setIsAddDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleEditElement = (element: TemplateElement) => {
|
||||
setEditingElement(element)
|
||||
setFormData(element)
|
||||
}
|
||||
|
||||
const handleUpdateElement = () => {
|
||||
if (!editingElement || !formData.label) return
|
||||
|
||||
// Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны
|
||||
if (
|
||||
formData.type !== 'standards' &&
|
||||
formData.type !== 'calibration-conditions' &&
|
||||
formData.type !== 'button-group' &&
|
||||
!formData.targetCells?.length
|
||||
)
|
||||
return
|
||||
|
||||
const updatedData = {
|
||||
...formData,
|
||||
name:
|
||||
formData.label?.toLowerCase().replace(/\s+/g, '_') ||
|
||||
formData.name ||
|
||||
'',
|
||||
}
|
||||
|
||||
onElementUpdate(editingElement.id, updatedData)
|
||||
setEditingElement(null)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingElement(null)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const handleAutoArrange = () => {
|
||||
const arrangedElements = autoArrangeElements(
|
||||
elements,
|
||||
layoutSettings.gridSize
|
||||
)
|
||||
|
||||
arrangedElements.forEach((element, index) => {
|
||||
onElementUpdate(element.id, {
|
||||
...element,
|
||||
layout: element.layout,
|
||||
order: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteAndSelect = (elementId: string) => {
|
||||
onElementDelete(elementId)
|
||||
setEditingElement(null)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// const reordered = (reorderedElements: TemplateElement[]) => {
|
||||
// onElementsReorder(reorderedElements);
|
||||
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 flex-col bg-gray-50">
|
||||
<div className="flex items-center justify-between border-b bg-white p-2">
|
||||
<div className="px-2">
|
||||
<h3 className="text-sm font-medium">Холст</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Перетаскивайте элементы для настройки их расположения
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={disabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Настройте параметры нового элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
onClick={() => setIsAddDialogOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddElement}
|
||||
disabled={
|
||||
disabled ||
|
||||
!formData.label ||
|
||||
(formData.type !== 'standards' &&
|
||||
formData.type !== 'calibration-conditions' &&
|
||||
formData.type !== 'button-group' &&
|
||||
!formData.targetCells?.length)
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{editingElement && (
|
||||
<Dialog
|
||||
open={!!editingElement}
|
||||
onOpenChange={() => handleCancelEdit()}
|
||||
>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените параметры элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button onClick={handleCancelEdit} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleUpdateElement} disabled={disabled}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<VisualLayoutEditor
|
||||
elements={elements}
|
||||
layoutSettings={layoutSettings}
|
||||
onElementUpdate={onElementUpdate}
|
||||
onElementDelete={handleDeleteAndSelect}
|
||||
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
|
||||
onElementEdit={handleEditElement}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user