Единый интерфейс элементов

This commit is contained in:
2025-07-22 19:44:49 +03:00
parent 553da77a87
commit 3fc352f8e9
36 changed files with 2108 additions and 675 deletions

View File

@@ -14,9 +14,9 @@ import {
SelectValue,
} from '@/component/ui/select'
import { Textarea } from '@/component/ui/textarea'
import { getElementDefinition } from '@/entitiy/element/model/interface'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { cellAddressToCoordinates } from '@/lib/cell-utils'
import { getElementDefinition } from '@/lib/element-registry'
import { useToast } from '@/lib/hooks/useToast'
import { getLatestFileForTemplate } from '@/service/fileApiService'
import { Template, TemplateElement } from '@/type/template'
@@ -118,6 +118,17 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
const renderInput = () => {
switch (element.type) {
case 'text':
// Используем определение элемента из реестра для рендеринга
const textDefinition = getElementDefinition('text')
if (textDefinition && textDefinition.Render) {
return (
<textDefinition.Render
config={element as any}
value={value}
onChange={onChange}
/>
)
}
return (
<Input
placeholder={element.placeholder}
@@ -291,9 +302,9 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
</div>
)
case 'calibration-conditions':
case 'calibration_conditions':
// Используем определение элемента из реестра для рендеринга
const elementDefinition = getElementDefinition('calibration-conditions')
const elementDefinition = getElementDefinition('calibration_conditions')
if (elementDefinition && elementDefinition.Render) {
return (
<elementDefinition.Render
@@ -305,9 +316,9 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
}
return null
case 'button-group':
case 'button_group':
// Используем определение элемента из реестра для рендеринга
const buttonGroupDefinition = getElementDefinition('button-group')
const buttonGroupDefinition = getElementDefinition('button_group')
if (buttonGroupDefinition && buttonGroupDefinition.Render) {
return (
<buttonGroupDefinition.Render
@@ -324,37 +335,74 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
}
}
// Проверяем, использует ли элемент определение из реестра для рендеринга
const elementDefinition = getElementDefinition(element.type)
const usesElementRender =
elementDefinition &&
elementDefinition.Render &&
(element.type === 'text' ||
element.type === 'calibration_conditions' ||
element.type === 'button_group')
return (
<div className="space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-1">
<label className="text-sm font-medium text-gray-900">
{element.label}
</label>
{element.required && (
<span className="-mt-1.5 inline-block h-1 w-1 rounded-full bg-red-500" />
{/* Показываем label и индикатор ячеек только если элемент не использует свой Render компонент */}
{!usesElementRender && (
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-1">
<label className="text-sm font-medium text-gray-900">
{element.label}
</label>
{element.required && (
<span className="-mt-1.5 inline-block h-1 w-1 rounded-full bg-red-500" />
)}
</div>
{/* Индикатор ячеек */}
{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="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) => (
<div key={i} className="font-mono">
{cell.sheet}!{cell.cell}
{cell.displayName && (
<span className="ml-1 text-gray-300">
({cell.displayName})
</span>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* Индикатор ячеек */}
{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="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) => (
<div key={i} className="font-mono">
{cell.sheet}!{cell.cell}
{cell.displayName && (
<span className="ml-1 text-gray-300">
({cell.displayName})
</span>
)}
</div>
))}
)}
{/* Для элементов с собственными Render компонентами показываем только индикатор ячеек */}
{usesElementRender &&
element.targetCells &&
element.targetCells.length > 0 && (
<div className="flex justify-end">
<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="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) => (
<div key={i} className="font-mono">
{cell.sheet}!{cell.cell}
{cell.displayName && (
<span className="ml-1 text-gray-300">
({cell.displayName})
</span>
)}
</div>
))}
</div>
</div>
</div>
)}
</div>
{renderInput()}
</div>
)
@@ -388,7 +436,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
// Специальная обработка для условий калибровки
if (
element.type === 'calibration-conditions' &&
element.type === 'calibration_conditions' &&
typeof value === 'object' &&
value !== null
) {
@@ -403,6 +451,15 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
cellValue = (value as any)[propsOrder[idx]] ?? ''
}
// Специальная обработка для группы кнопок
if (element.type === 'button_group' && typeof value === 'string') {
// Найдем соответствующий option для получения label
const buttonOption = element.options?.find(
opt => opt.value === value
)
cellValue = buttonOption ? buttonOption.label : value
}
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
@@ -497,7 +554,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
// 2) Объект условия калибровки -> пишем свойства по порядку
if (
el.type === 'calibration-conditions' &&
el.type === 'calibration_conditions' &&
typeof value === 'object' &&
value !== null
) {
@@ -524,7 +581,25 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
return
}
// 3) Примитив или прочие объекты -> одно значение во все ячейки
// 3) Группа кнопок -> записываем label выбранной кнопки
if (el.type === 'button_group' && typeof value === 'string') {
const buttonOption = el.options?.find(opt => opt.value === value)
const cellValue = buttonOption ? buttonOption.label : value
cells.forEach(tc => {
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
engineRef.current.setCellValueWithoutRecalc(
sheetName,
row,
col,
cellValue
)
})
return
}
// 4) Примитив или прочие объекты -> одно значение во все ячейки
cells.forEach(tc => {
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
@@ -579,6 +654,33 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
// Получаем файл как blob и скачиваем его
const blob = await resp.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
// Получаем имя файла из заголовка Content-Disposition или создаем по умолчанию
const contentDisposition = resp.headers.get('content-disposition')
let filename = `protocol_${template.name}_${new Date().toISOString().split('T')[0]}.xlsx`
if (contentDisposition) {
const matches = contentDisposition.match(
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
)
if (matches && matches[1]) {
filename = matches[1].replace(/['"]/g, '')
}
}
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
toast.success('Протокол создан и загружен!')
} catch (err) {
console.error('Ошибка при создании протокола', err)
toast.error('Ошибка при создании протокола')