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