новые стили редактирования элементов
This commit is contained in:
@@ -44,35 +44,28 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Settings className="h-4 w-4" />
|
||||
Настройки эталонов
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">Максимум эталонов</label>
|
||||
<Select
|
||||
value={safeConfig.maxItems.toString()}
|
||||
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-16">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3">3</SelectItem>
|
||||
<SelectItem value="7">7</SelectItem>
|
||||
<SelectItem value="9">9</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Настройте параметры для выбора измерительных эталонов
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Максимум эталонов</label>
|
||||
<Select
|
||||
value={safeConfig.maxItems.toString()}
|
||||
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3">3 эталона</SelectItem>
|
||||
<SelectItem value="5">5 эталонов</SelectItem>
|
||||
<SelectItem value="7">7 эталонов</SelectItem>
|
||||
<SelectItem value="10">10 эталонов</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
Максимальное количество эталонов для выбора
|
||||
Максимальное количество эталонов для выбора (0-12)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -105,16 +98,6 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||
<p className="text-xs text-gray-500">
|
||||
Эталоны, которые будут выбраны по умолчанию при создании протокола
|
||||
</p>
|
||||
|
||||
<StandardsSelector
|
||||
isOpen={isStandardsDialogOpen}
|
||||
onClose={() => setIsStandardsDialogOpen(false)}
|
||||
value={safeConfig.selectedStandards}
|
||||
onChange={standardIds => {
|
||||
updateConfig({ selectedStandards: standardIds })
|
||||
}}
|
||||
maxItems={safeConfig.maxItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-blue-50 p-3">
|
||||
@@ -130,6 +113,16 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StandardsSelector
|
||||
isOpen={isStandardsDialogOpen}
|
||||
onClose={() => setIsStandardsDialogOpen(false)}
|
||||
value={safeConfig.selectedStandards}
|
||||
onChange={standardIds => {
|
||||
updateConfig({ selectedStandards: standardIds })
|
||||
}}
|
||||
maxItems={safeConfig.maxItems}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,20 @@ import {
|
||||
Template,
|
||||
TemplateElement,
|
||||
} from '@/type/template'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Eye,
|
||||
HelpCircle,
|
||||
Plus,
|
||||
Settings2,
|
||||
Tag,
|
||||
Target,
|
||||
Trash2,
|
||||
Type,
|
||||
} from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
||||
import { Checkbox } from '../ui/checkbox'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,6 +34,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,6 +42,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
@@ -79,72 +92,92 @@ const CellTargetEditor: React.FC<{
|
||||
}
|
||||
|
||||
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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Ячейка
|
||||
</label>
|
||||
<Input
|
||||
placeholder="A1, B5..."
|
||||
value={target.cell}
|
||||
onChange={e => handleCellInputChange(index, 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>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="h-4 w-4 text-blue-600" />
|
||||
<CardTitle className="text-sm">Целевые ячейки</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddTarget}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="max-h-48 space-y-2 overflow-y-auto">
|
||||
{targets.map((target, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 rounded-lg border bg-gray-50/50 p-2"
|
||||
>
|
||||
{/* Кнопки выбора листа */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={target.sheet === 'L' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleUpdateTarget(index, 'sheet', 'L')}
|
||||
className="h-6 w-7 p-0 text-xs"
|
||||
>
|
||||
L
|
||||
</Button>
|
||||
<Button
|
||||
variant={target.sheet === 'R' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleUpdateTarget(index, 'sheet', 'R')}
|
||||
className="h-6 w-7 p-0 text-xs"
|
||||
>
|
||||
R
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Поле ввода ячейки */}
|
||||
<Input
|
||||
placeholder="A1"
|
||||
value={target.cell}
|
||||
onChange={e => handleCellInputChange(index, e.target.value)}
|
||||
className="h-6 w-16 px-2 text-xs"
|
||||
/>
|
||||
|
||||
{/* Кнопка удаления */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTarget(index)}
|
||||
className="ml-auto h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{targets.length === 0 && (
|
||||
<div className="py-4 text-center">
|
||||
<Target className="mx-auto mb-1 h-6 w-6 text-gray-300" />
|
||||
<p className="text-xs text-gray-500">
|
||||
Добавьте ячейки для связи с элементом
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Например: A1, B5, C10
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{targets.length > 0 && (
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
|
||||
<HelpCircle className="h-3 w-3" />
|
||||
Всего ячеек: {targets.length}
|
||||
</div>
|
||||
))}
|
||||
{targets.length === 0 && (
|
||||
<p className="py-3 text-center text-xs text-gray-500">
|
||||
Добавьте ячейки для связи с элементом
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,42 +189,40 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||||
? getElementDefinition(element.type)
|
||||
: null
|
||||
|
||||
if (elementDefinition && elementDefinition.Preview) {
|
||||
return <elementDefinition.Preview config={element as any} />
|
||||
}
|
||||
|
||||
// Fallback для неизвестных типов элементов
|
||||
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>
|
||||
)}
|
||||
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||||
{element.type
|
||||
? `Элемент типа "${element.type}" не найден`
|
||||
: 'Выберите тип элемента'}
|
||||
<Card className="h-full">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="h-4 w-4 text-green-600" />
|
||||
<CardTitle className="text-sm">Предпросмотр элемента</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="min-h-[200px] rounded-lg border border-gray-200 bg-white p-4">
|
||||
{elementDefinition && elementDefinition.Preview ? (
|
||||
<elementDefinition.Preview config={element as any} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<div className="flex items-center justify-center gap-2 rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{element.type
|
||||
? `Элемент типа "${element.type}" не найден`
|
||||
: 'Выберите тип элемента'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,179 +259,113 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
||||
}
|
||||
|
||||
const needsOptions =
|
||||
formData.type === 'select' ||
|
||||
formData.type === 'radio' ||
|
||||
formData.type === 'button_group'
|
||||
formData.type === 'select' || 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
|
||||
const elementDefinition = getElementDefinition(newType)
|
||||
|
||||
setFormData(prev => {
|
||||
const updates: Partial<TemplateElement> = {
|
||||
type: newType,
|
||||
// Сбрасываем targetCells при смене типа
|
||||
targetCells: [],
|
||||
}
|
||||
|
||||
// Применяем конфиг по умолчанию из определения элемента
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
Object.assign(updates, elementDefinition.defaultConfig)
|
||||
}
|
||||
|
||||
return { ...prev, ...updates }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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
|
||||
const elementDefinition = getElementDefinition(newType)
|
||||
<div className="grid h-full grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Настройки элемента */}
|
||||
<div className="max-h-[60vh] space-y-4 overflow-y-auto pr-2">
|
||||
{/* Основные настройки */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="h-4 w-4 text-blue-600" />
|
||||
<CardTitle className="text-sm">Основные настройки</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Type className="h-3 w-3" />
|
||||
Тип элемента
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={value => {
|
||||
const newType = value as ElementType
|
||||
const elementDefinition = getElementDefinition(newType)
|
||||
|
||||
setFormData(prev => {
|
||||
const updates: Partial<TemplateElement> = {
|
||||
type: newType,
|
||||
// Сбрасываем targetCells при смене типа
|
||||
targetCells: [],
|
||||
setFormData(prev => {
|
||||
const updates: Partial<TemplateElement> = {
|
||||
type: newType,
|
||||
targetCells: [],
|
||||
}
|
||||
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
Object.assign(updates, elementDefinition.defaultConfig)
|
||||
}
|
||||
|
||||
return { ...prev, ...updates }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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-1 gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Tag className="h-3 w-3" />
|
||||
Подпись
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Название поля"
|
||||
value={formData.label}
|
||||
onChange={e =>
|
||||
setFormData(prev => ({ ...prev, label: e.target.value }))
|
||||
}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||||
<HelpCircle className="h-3 w-3" />
|
||||
Подсказка
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
// Применяем конфиг по умолчанию из определения элемента
|
||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||
Object.assign(updates, elementDefinition.defaultConfig)
|
||||
}
|
||||
|
||||
return { ...prev, ...updates }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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 =>
|
||||
@@ -408,56 +373,94 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Специфичный редактор элемента */}
|
||||
{elementDefinition && elementDefinition.Editor && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="h-4 w-4 text-purple-600" />
|
||||
<CardTitle className="text-sm">
|
||||
Дополнительные настройки
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<elementDefinition.Editor
|
||||
config={formData as any}
|
||||
onChange={newConfig =>
|
||||
setFormData(prev => ({ ...prev, ...newConfig }))
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Варианты для select/radio/button_group */}
|
||||
{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>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-orange-600" />
|
||||
<CardTitle className="text-sm">Варианты выбора</CardTitle>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddOption}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="max-h-32 space-y-2 overflow-y-auto">
|
||||
{formData.options?.map((option, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Значение"
|
||||
value={option.value}
|
||||
onChange={e =>
|
||||
handleUpdateOption(index, 'value', e.target.value)
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Подпись"
|
||||
value={option.label}
|
||||
onChange={e =>
|
||||
handleUpdateOption(index, 'label', e.target.value)
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveOption(index)}
|
||||
className="h-8 w-8 text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{(!formData.options || formData.options.length === 0) && (
|
||||
<div className="py-4 text-center">
|
||||
<Tag className="mx-auto mb-2 h-6 w-6 text-gray-300" />
|
||||
<p className="text-xs text-gray-500">
|
||||
Добавьте варианты выбора
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Live Preview */}
|
||||
<div className="sticky top-0 flex min-h-[50vh] items-center">
|
||||
{/* Предпросмотр */}
|
||||
<div className="h-fit lg:sticky lg:top-0">
|
||||
<ElementPreview element={formData} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -661,11 +664,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// const reordered = (reorderedElements: TemplateElement[]) => {
|
||||
// onElementsReorder(reorderedElements);
|
||||
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-background">
|
||||
<ElementFormulaBar
|
||||
@@ -704,25 +702,46 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
|
||||
{/* Диалог добавления элемента */}
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
||||
<DialogHeader className="pb-4">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Plus className="h-5 w-5 text-blue-600" />
|
||||
Добавить новый элемент
|
||||
</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}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
{!formData.label && (
|
||||
<div className="flex items-center gap-1 text-amber-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Заполните обязательные поля
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => setIsAddDialogOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddElement}
|
||||
disabled={disabled || !formData.label}
|
||||
className="min-w-[140px]"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -730,21 +749,43 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
{/* Диалог редактирования элемента */}
|
||||
{editingElement && (
|
||||
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
||||
<DialogHeader className="pb-4">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Settings2 className="h-5 w-5 text-green-600" />
|
||||
Редактировать элемент
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените параметры элемента формы.
|
||||
Измените параметры элемента "
|
||||
{editingElement.label || editingElement.name}".
|
||||
</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 className="flex-1 overflow-hidden">
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
{!formData.label && (
|
||||
<div className="flex items-center gap-1 text-amber-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Заполните обязательные поля
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={handleCancelEdit} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateElement}
|
||||
disabled={disabled || !formData.label}
|
||||
className="min-w-[160px]"
|
||||
>
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -150,14 +150,16 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
||||
>
|
||||
{/* Controls (only visible when selected or hovered) */}
|
||||
<div
|
||||
className={`absolute right-1 top-1 flex items-center gap-1 ${
|
||||
className={`absolute right-1 top-1 flex items-center gap-0.5 ${
|
||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
{/* Индикатор ячеек */}
|
||||
{element.targetCells && element.targetCells.length > 0 && (
|
||||
<div className="group/cells relative">
|
||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
||||
<div className="flex !h-5 !w-5 cursor-help items-center justify-center rounded hover:bg-gray-100">
|
||||
<Grid className="!h-3 !w-3 text-gray-500" />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
||||
{element.targetCells.map((cell, i) => (
|
||||
@@ -177,19 +179,19 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 cursor-pointer hover:bg-blue-100"
|
||||
className="!h-5 !w-5 !min-w-0 cursor-pointer !p-0 hover:bg-blue-100"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
<Edit className="h-2.5 w-2.5 text-blue-600" />
|
||||
<Edit className="!h-3 !w-3 text-blue-600" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 cursor-pointer hover:bg-red-100"
|
||||
className="!h-5 !w-5 !min-w-0 cursor-pointer !p-0 hover:bg-red-100"
|
||||
onClick={handleDeleteClick}
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5 text-red-600" />
|
||||
<Trash2 className="!h-3 !w-3 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -286,7 +288,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
||||
onClick={handleCanvasClick}
|
||||
>
|
||||
<div
|
||||
className="relative mx-auto my-8 select-none bg-white shadow-lg"
|
||||
className="relative mx-auto my-8 select-none border border-gray-300 bg-white"
|
||||
style={{ width: canvasSize.width, height: canvasSize.height }}
|
||||
onClick={handleCanvasClick}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user