Единый интерфейс элементов
This commit is contained in:
@@ -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('Ошибка при создании протокола')
|
||||
|
||||
Reference in New Issue
Block a user