починка багов
This commit is contained in:
@@ -3,6 +3,13 @@ import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/component/ui/select'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -412,6 +419,9 @@ function CalibrationCondInfo({
|
||||
interface CalibrationConditionsConfig {
|
||||
targetCells: any[]
|
||||
defaultConditions?: CalibrationConditions
|
||||
cellMapping?: {
|
||||
[key: string]: string // параметр -> ячейка из targetCells (по индексу)
|
||||
}
|
||||
}
|
||||
|
||||
export const calibrationConditionsDefinition: ElementDefinition<
|
||||
@@ -424,6 +434,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
version: 1,
|
||||
defaultConfig: {
|
||||
targetCells: [],
|
||||
cellMapping: {},
|
||||
defaultConditions: {
|
||||
temperature: '20',
|
||||
humidity: '65',
|
||||
@@ -433,164 +444,289 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
mapToCells: () => {
|
||||
// Генерируем ячейки для каждого параметра калибровки
|
||||
const cells = [
|
||||
{ sheet: 'L', cell: 'B2', displayName: 'Температура' },
|
||||
{ sheet: 'L', cell: 'B3', displayName: 'Влажность' },
|
||||
{ sheet: 'L', cell: 'B4', displayName: 'Давление' },
|
||||
{ sheet: 'L', cell: 'B5', displayName: 'Напряжение' },
|
||||
{ sheet: 'L', cell: 'B6', displayName: 'Частота' },
|
||||
{ sheet: 'L', cell: 'B7', displayName: 'Дата обновления' },
|
||||
]
|
||||
return cells
|
||||
mapToCells: cfg => {
|
||||
// Используем только ячейки, указанные пользователем в targetCells
|
||||
return cfg.targetCells || []
|
||||
},
|
||||
Editor: ({ config, onChange }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Элемент "Условия калибровки" позволяет вводить и отслеживать параметры
|
||||
окружающей среды при проведении измерений. Поддерживает валидацию
|
||||
значений и отслеживание времени обновления.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Параметры по умолчанию</label>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
|
||||
mapToCellValues: (config, value) => {
|
||||
// Используем targetCells из конфигурации
|
||||
const cells = config.targetCells || []
|
||||
const cellMapping = config.cellMapping || {}
|
||||
|
||||
// Если значение не объект, возвращаем пустые значения
|
||||
if (!value || typeof value !== 'object') {
|
||||
return cells.map(target => ({
|
||||
target,
|
||||
value: '',
|
||||
}))
|
||||
}
|
||||
|
||||
// Создаем обратное сопоставление: индекс ячейки -> параметр
|
||||
const reversedMapping: { [cellIndex: string]: string } = {}
|
||||
Object.entries(cellMapping).forEach(([param, cellIndex]) => {
|
||||
reversedMapping[cellIndex] = param
|
||||
})
|
||||
|
||||
return cells.map((target, idx) => {
|
||||
const paramKey = reversedMapping[idx.toString()]
|
||||
const paramValue = paramKey ? (value as any)[paramKey] : ''
|
||||
|
||||
return {
|
||||
target,
|
||||
value: paramValue || '',
|
||||
}
|
||||
})
|
||||
},
|
||||
Editor: ({ config, onChange }) => {
|
||||
const availableParams = [
|
||||
{ key: 'temperature', label: 'Температура' },
|
||||
{ key: 'humidity', label: 'Влажность' },
|
||||
{ key: 'pressure', label: 'Давление' },
|
||||
{ key: 'voltage', label: 'Напряжение' },
|
||||
{ key: 'frequency', label: 'Частота' },
|
||||
{ key: 'lastUpdated', label: 'Дата обновления' },
|
||||
]
|
||||
|
||||
const targetCells = config.targetCells || []
|
||||
const cellMapping = config.cellMapping || {}
|
||||
|
||||
// Функция для создания автоматического сопоставления по порядку
|
||||
const createAutoMapping = () => {
|
||||
const newMapping: { [key: string]: string } = {}
|
||||
const paramKeys = availableParams.map(p => p.key)
|
||||
|
||||
// Сопоставляем первые N параметров с первыми N ячейками
|
||||
targetCells.forEach((_, index) => {
|
||||
if (index < paramKeys.length) {
|
||||
newMapping[paramKeys[index]] = index.toString()
|
||||
}
|
||||
})
|
||||
|
||||
onChange({
|
||||
...config,
|
||||
cellMapping: newMapping,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCellMapping = (param: string, cellIndex: string) => {
|
||||
const newMapping = { ...cellMapping }
|
||||
if (cellIndex === '' || cellIndex === 'none') {
|
||||
delete newMapping[param]
|
||||
} else {
|
||||
newMapping[param] = cellIndex
|
||||
}
|
||||
onChange({
|
||||
...config,
|
||||
cellMapping: newMapping,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Элемент "Условия калибровки" позволяет вводить и отслеживать
|
||||
параметры окружающей среды при проведении измерений. Поддерживает
|
||||
валидацию значений и отслеживание времени обновления.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Сопоставление параметров с ячейками */}
|
||||
{targetCells.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Температура (°C)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.temperature || '20'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: e.target.value,
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="20"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">
|
||||
Сопоставление с ячейками
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={createAutoMapping}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
Автозаполнение
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-lg border border-gray-200 p-4">
|
||||
{availableParams.map(param => (
|
||||
<div key={param.key} className="flex items-center gap-3">
|
||||
<div className="min-w-[120px]">
|
||||
<span className="text-sm text-gray-700">{param.label}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={cellMapping[param.key] || 'none'}
|
||||
onValueChange={value =>
|
||||
updateCellMapping(param.key, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Не указано" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Не указано</SelectItem>
|
||||
{targetCells.map((cell, index) => (
|
||||
<SelectItem key={index} value={index.toString()}>
|
||||
{cell.sheet}!{cell.cell}{' '}
|
||||
{cell.displayName ? `(${cell.displayName})` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Настройте какой параметр условий калибровки будет записан в какую
|
||||
ячейку. Используйте кнопку "Автозаполнение" для автоматического
|
||||
сопоставления по порядку.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Влажность (%)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.humidity || '65'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: config.defaultConditions?.temperature || '20',
|
||||
humidity: e.target.value,
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="65"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Давление (кПа)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.pressure || '101.3'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: e.target.value,
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="101.3"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Напряжение (В)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.voltage || '220'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: e.target.value,
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="220"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Частота (Гц)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.frequency || '50'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: e.target.value,
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="50"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Параметры по умолчанию</label>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Температура (°C)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.temperature || '20'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature: e.target.value,
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="20"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Влажность (%)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.humidity || '65'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature:
|
||||
config.defaultConditions?.temperature || '20',
|
||||
humidity: e.target.value,
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="65"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Давление (кПа)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.pressure || '101.3'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature:
|
||||
config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: e.target.value,
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="101.3"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Напряжение (В)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.voltage || '220'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature:
|
||||
config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: e.target.value,
|
||||
frequency: config.defaultConditions?.frequency || '50',
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="220"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Частота (Гц)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.frequency || '50'}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
...config.defaultConditions,
|
||||
temperature:
|
||||
config.defaultConditions?.temperature || '20',
|
||||
humidity: config.defaultConditions?.humidity || '65',
|
||||
pressure: config.defaultConditions?.pressure || '101.3',
|
||||
voltage: config.defaultConditions?.voltage || '220',
|
||||
frequency: e.target.value,
|
||||
lastUpdated:
|
||||
config.defaultConditions?.lastUpdated ||
|
||||
new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
Preview: ({ config }) => (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
@@ -613,6 +749,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
</div>
|
||||
),
|
||||
Render: ({ config, value, onChange }) => {
|
||||
// Если значение не установлено, используем defaultConditions из конфигурации
|
||||
const conditions = value ||
|
||||
config.defaultConditions || {
|
||||
temperature: '20',
|
||||
|
||||
@@ -18,6 +18,10 @@ export const checkboxDefinition: ElementDefinition<CheckboxConfig, boolean> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || false }))
|
||||
},
|
||||
Editor: () => (
|
||||
<div className="text-sm text-gray-600">
|
||||
Дополнительные настройки отсутствуют
|
||||
|
||||
@@ -17,6 +17,10 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || '' }))
|
||||
},
|
||||
Editor: ({ config, onChange }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -19,6 +19,10 @@ export const numberDefinition: ElementDefinition<NumberConfig, number> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || 0 }))
|
||||
},
|
||||
Editor: () => (
|
||||
<div className="text-sm text-gray-600">
|
||||
Дополнительные настройки отсутствуют
|
||||
|
||||
@@ -23,6 +23,10 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || '' }))
|
||||
},
|
||||
Editor: ({ config, onChange }) => {
|
||||
const handleAddOption = () => {
|
||||
onChange({
|
||||
|
||||
@@ -30,6 +30,10 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || '' }))
|
||||
},
|
||||
Editor: ({ config, onChange }) => {
|
||||
const handleAddOption = () => {
|
||||
onChange({
|
||||
|
||||
@@ -19,6 +19,10 @@ export const textareaDefinition: ElementDefinition<TextareaConfig, string> = {
|
||||
targetCells: [],
|
||||
},
|
||||
mapToCells: config => config.targetCells || [],
|
||||
mapToCellValues: (config, value) => {
|
||||
const cells = config.targetCells || []
|
||||
return cells.map(target => ({ target, value: value || '' }))
|
||||
},
|
||||
Editor: () => (
|
||||
<div className="text-sm text-gray-600">
|
||||
Дополнительные настройки отсутствуют
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFileData, useLatestFileForTemplate } from '@/hook/useFileQueries'
|
||||
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
||||
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
import { produce } from 'immer'
|
||||
import React, {
|
||||
FC,
|
||||
@@ -78,6 +78,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
row: number
|
||||
col: number
|
||||
} | null>(null)
|
||||
|
||||
// Флаг, что пользователь изменил хотя бы одну ячейку
|
||||
const [userEdited, setUserEdited] = useState(false)
|
||||
|
||||
// Состояние для множественного выделения
|
||||
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
@@ -95,7 +99,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
>({}) // Базовые данные файла
|
||||
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
|
||||
[]
|
||||
) // Подсвеченные ячейки для формул
|
||||
)
|
||||
|
||||
// React Query хуки для оптимизации API запросов
|
||||
const { data: latestFile, isLoading: isLoadingLatestFile } =
|
||||
useLatestFileForTemplate(templateId)
|
||||
const { data: fileData, isLoading: isLoadingFileData } = useFileData(
|
||||
latestFile?.id
|
||||
)
|
||||
|
||||
// Мемоизируем начальное состояние ширин колонок
|
||||
const initialColumnWidths = useMemo(
|
||||
@@ -109,46 +120,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const [columnWidths, setColumnWidths] =
|
||||
useState<Record<SheetType, number[]>>(initialColumnWidths)
|
||||
|
||||
// Мемоизируем начальные refs объекты
|
||||
const initialGridRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialContainerRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialHeaderRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialRowHeaderRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const gridRefs = useRef<Record<SheetType, any>>(initialGridRefs)
|
||||
const containerRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialContainerRefs)
|
||||
const headerRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialHeaderRefs)
|
||||
const rowHeaderRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
|
||||
const gridRefs = useRef<Record<SheetType, any>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
|
||||
const activeCellInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
||||
@@ -171,22 +158,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||
})
|
||||
})
|
||||
// console.log(
|
||||
// '✅ Загружено значений baseFileData для сравнения (приоритет):',
|
||||
// Object.keys(initialTemplateValues.current).length
|
||||
//)
|
||||
} else if (hasTemplateData) {
|
||||
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
|
||||
Object.entries(sheetData).forEach(([cellAddress, value]) => {
|
||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||
})
|
||||
})
|
||||
// console.log(
|
||||
// '✅ Загружено значений templateData для сравнения (fallback):',
|
||||
// Object.keys(initialTemplateValues.current).length
|
||||
// )
|
||||
} else {
|
||||
// console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
|
||||
}
|
||||
}, [baseFileData, templateData])
|
||||
|
||||
@@ -212,7 +189,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
const getCachedCellData = useCallback(
|
||||
(sheetType: SheetType): Record<string, CachedCellData> => {
|
||||
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
|
||||
const cacheKey = `${sheetType}-${revision}-${columnWidths[sheetType].join('-')}`
|
||||
|
||||
if (cellDataCache.current[cacheKey]) {
|
||||
return cellDataCache.current[cacheKey]
|
||||
@@ -262,19 +239,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const cellName = getColumnLabel(col) + (row + 1)
|
||||
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
|
||||
|
||||
// Отладка только для измененных ячеек
|
||||
if (
|
||||
sheetType === 'report' &&
|
||||
baseFileValue &&
|
||||
rawValue !== baseFileValue
|
||||
) {
|
||||
// console.log(`🔧 Измененная ячейка ${cellName}:`, {
|
||||
// baseFileValue,
|
||||
// rawValue,
|
||||
// isModified: true,
|
||||
// })
|
||||
}
|
||||
|
||||
// Нормализуем значения для сравнения
|
||||
const normalizeValue = (value: any): string => {
|
||||
if (value === null || value === undefined) return ''
|
||||
@@ -290,15 +254,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const isModified =
|
||||
sheetType === 'report' && normalizedRaw !== normalizedBaseFile
|
||||
|
||||
// Отладка только для измененных ячеек
|
||||
if (sheetType === 'report' && isModified) {
|
||||
// console.log(`🔍 Измененная ячейка ${cellName}:`, {
|
||||
// baseFileValue,
|
||||
// rawValue,
|
||||
// isModified,
|
||||
// })
|
||||
}
|
||||
|
||||
cachedData[cellKey] = {
|
||||
displayValue,
|
||||
rawValue,
|
||||
@@ -343,11 +298,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
)
|
||||
|
||||
setHighlightedCells(highlights)
|
||||
|
||||
// console.log('🎨 Подсветка ячеек для формулы:', formula, {
|
||||
// references,
|
||||
// highlights,
|
||||
// })
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении подсветки ячеек:', error)
|
||||
setHighlightedCells([])
|
||||
@@ -422,23 +372,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
(save: boolean) => {
|
||||
if (save && selectedCell) {
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
const cellName =
|
||||
getColumnLabel(selectedCell.col) + (selectedCell.row + 1)
|
||||
const oldValue = multiSheetEngine.getCellRawValue(
|
||||
sheetName,
|
||||
selectedCell.row,
|
||||
selectedCell.col
|
||||
)
|
||||
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
|
||||
|
||||
// console.log('💾 Сохранение ячейки:', {
|
||||
//cellName,
|
||||
//oldValue,
|
||||
//newValue: editingValue,
|
||||
//baseFileValue,
|
||||
//isChanged: oldValue !== editingValue,
|
||||
//isModifiedFromBaseFile: baseFileValue !== editingValue,
|
||||
//})
|
||||
|
||||
multiSheetEngine.setCellValueWithoutRecalc(
|
||||
sheetName,
|
||||
@@ -461,23 +394,25 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
(newValue: string) => {
|
||||
if (isEditing) {
|
||||
setEditingValue(newValue)
|
||||
if (!userEdited) setUserEdited(true)
|
||||
}
|
||||
},
|
||||
[isEditing]
|
||||
[isEditing, userEdited]
|
||||
)
|
||||
|
||||
const handleClearCell = useCallback(() => {
|
||||
if (!selectedCell) return
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
|
||||
multiSheetEngine.setCellValueWithoutRecalc(
|
||||
sheetName,
|
||||
selectedCell.row,
|
||||
selectedCell.col,
|
||||
''
|
||||
)
|
||||
setUserEdited(true)
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
// Сбрасываем множественное выделение при очистке одиночной ячейки
|
||||
setMultiSelection([])
|
||||
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
|
||||
|
||||
@@ -515,22 +450,19 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
const cellsToDelete = getAllCellsInSelections(multiSelection)
|
||||
|
||||
cellsToDelete.forEach(({ row, col }: { row: number; col: number }) => {
|
||||
cellsToDelete.forEach(({ row, col }) => {
|
||||
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
||||
})
|
||||
|
||||
setUserEdited(true)
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
|
||||
// Очищаем выделение после удаления
|
||||
setMultiSelection([])
|
||||
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isEditing) {
|
||||
return
|
||||
}
|
||||
if (isEditing) return
|
||||
|
||||
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
|
||||
moveSelection(0, 0)
|
||||
@@ -595,6 +527,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
setEditingSource('cell')
|
||||
setEditingValue(e.key)
|
||||
setIsEditing(true)
|
||||
if (!userEdited) setUserEdited(true)
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -610,13 +543,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
const handleCellClick = useCallback(
|
||||
(row: number, col: number, event?: React.MouseEvent) => {
|
||||
// console.log('🖱️ Обычный клик по ячейке:', {
|
||||
// row,
|
||||
//col,
|
||||
//isEditing,
|
||||
//activeSheet,
|
||||
//})
|
||||
|
||||
if (isEditing) {
|
||||
stopEditing(true)
|
||||
}
|
||||
@@ -710,44 +636,23 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
|
||||
const handleCellReferenceClick = useCallback(
|
||||
(targetSheetType: SheetType, row: number, col: number) => {
|
||||
// console.log('🔗 Клик для вставки ссылки:', {
|
||||
//targetSheetType,
|
||||
//row,
|
||||
//col,
|
||||
//isEditing,
|
||||
//selectedCell,
|
||||
//editingValue,
|
||||
//})
|
||||
|
||||
// Проверяем, что мы в режиме редактирования
|
||||
if (!isEditing || !selectedCell) {
|
||||
// console.log(
|
||||
// '⚠️ Игнорируем клик ссылки: не в режиме редактирования или нет выбранной ячейки'
|
||||
//)
|
||||
if (!isEditing || !selectedCell || !editingValue.startsWith('=')) {
|
||||
stopEditing(true)
|
||||
setSelectedCell({ row, col })
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем ссылку на ячейку
|
||||
let cellReference: string
|
||||
|
||||
// Если это другой лист, делаем квалифицированную ссылку
|
||||
if (targetSheetType !== activeSheet) {
|
||||
cellReference = createQualifiedCellReference(targetSheetType, row, col)
|
||||
// console.log('📋 Создана межлистовая ссылка:', cellReference)
|
||||
} else {
|
||||
// Если это тот же лист, делаем локальную ссылку
|
||||
cellReference = createCellAddress(row, col)
|
||||
// console.log('📋 Создана локальная ссылка:', cellReference)
|
||||
}
|
||||
|
||||
// Добавляем ссылку к текущему значению редактирования
|
||||
const newValue = editingValue + cellReference
|
||||
// console.log('✏️ Новое значение формулы:', {
|
||||
//oldValue: editingValue,
|
||||
//newValue,
|
||||
//})
|
||||
|
||||
setEditingValue(newValue)
|
||||
if (!userEdited) setUserEdited(true)
|
||||
|
||||
if (editingSource === 'formula') {
|
||||
if (formulaBarRef.current) {
|
||||
@@ -795,13 +700,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const handleFormulaBarChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (selectedCell) {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true)
|
||||
}
|
||||
if (!isEditing) setIsEditing(true)
|
||||
setEditingValue(newValue)
|
||||
if (!userEdited) setUserEdited(true)
|
||||
}
|
||||
},
|
||||
[selectedCell, isEditing]
|
||||
[selectedCell, isEditing, userEdited]
|
||||
)
|
||||
|
||||
const handleFormulaBarFocus = useCallback(() => {
|
||||
@@ -860,32 +764,25 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
)
|
||||
|
||||
// Мемоизируем состояние ширин таблиц
|
||||
const initialSpreadsheetWidths = useMemo(
|
||||
() => ({
|
||||
L: 0.5,
|
||||
R: 0.5,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState(
|
||||
initialSpreadsheetWidths
|
||||
)
|
||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
|
||||
L: 0.5,
|
||||
R: 0.5,
|
||||
})
|
||||
|
||||
// Мемоизируем опции автосохранения
|
||||
const autoSaveOptions = useMemo(
|
||||
() => ({
|
||||
templateId,
|
||||
autoSaveDelay: 2000,
|
||||
enableAutoSave,
|
||||
enableAutoSave: enableAutoSave && userEdited,
|
||||
}),
|
||||
[templateId, enableAutoSave]
|
||||
[templateId, enableAutoSave, userEdited]
|
||||
)
|
||||
|
||||
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
|
||||
const memoizedGetAllModifiedCells = useCallback(
|
||||
multiSheetEngine.getAllModifiedCells,
|
||||
[multiSheetEngine.getAllModifiedCells]
|
||||
[revision] // Используем revision вместо функции для более стабильной мемоизации
|
||||
)
|
||||
|
||||
// Используем новый хук автосохранения
|
||||
@@ -896,8 +793,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
lastSaveError,
|
||||
manualSave,
|
||||
clearError,
|
||||
loadSavedSheets,
|
||||
loadSheets, // Добавляем асинхронную функцию загрузки
|
||||
loadSheets,
|
||||
} = useSheetAutoSave(
|
||||
memoizedGetAllModifiedCells,
|
||||
multiSheetEngine.isCalculating,
|
||||
@@ -924,33 +820,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
let finalBaseData: Record<string, Record<string, any>> = {}
|
||||
|
||||
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
|
||||
if (templateId) {
|
||||
try {
|
||||
const latestFile = await getLatestFileForTemplate(templateId)
|
||||
if (latestFile) {
|
||||
// console.log('📁 Найден последний файл для шаблона:', latestFile.name)
|
||||
const fileData = await getFileData(latestFile.id)
|
||||
|
||||
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
||||
// Когда API будет готов, здесь будут реальные данные файла
|
||||
if (Object.keys(fileData.data).length > 0) {
|
||||
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||||
setBaseFileData(finalBaseData)
|
||||
// Сохраняем baseline, чтобы в L попадали только пользовательские изменения
|
||||
setTemplateData(finalBaseData)
|
||||
// console.log('📄 Загружены базовые данные файла:', finalBaseData)
|
||||
} else {
|
||||
// console.log(
|
||||
// '⚠️ getFileData вернул пустые данные, используем templateData'
|
||||
//)
|
||||
}
|
||||
} else {
|
||||
// console.log('⚠️ Файл не найден для templateId:', templateId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке данных файла:', error)
|
||||
}
|
||||
// 1. ИСПОЛЬЗУЕМ ДАННЫЕ ИЗ REACT QUERY КЕША
|
||||
if (fileData && Object.keys(fileData.data).length > 0) {
|
||||
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||||
setBaseFileData(finalBaseData)
|
||||
// Сохраняем baseline, чтобы в листы попадали только пользовательские изменения
|
||||
setTemplateData(finalBaseData)
|
||||
}
|
||||
|
||||
// 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные
|
||||
@@ -961,16 +836,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
|
||||
finalBaseData = templateData
|
||||
setBaseFileData(finalBaseData)
|
||||
// console.log(
|
||||
// '📄 Используем templateData как базовые данные:',
|
||||
// finalBaseData
|
||||
//)
|
||||
setTemplateData(templateData)
|
||||
}
|
||||
|
||||
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
|
||||
try {
|
||||
// console.log('🔍 Загружаем пользовательские изменения из API...')
|
||||
const apiSheets = await loadSheets()
|
||||
|
||||
// Преобразуем листы API в формат данных
|
||||
@@ -995,17 +865,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
}
|
||||
})
|
||||
|
||||
// console.log(
|
||||
// '📂 Загружены пользовательские изменения:',
|
||||
// Object.keys(userChanges)
|
||||
//)
|
||||
|
||||
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
||||
if (Object.keys(userChanges).length > 0) {
|
||||
// console.log(
|
||||
// '🔄 Объединяем базовые данные с пользовательскими изменениями'
|
||||
//)
|
||||
|
||||
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
||||
const mergedData: Record<string, Record<string, any>> = {}
|
||||
|
||||
@@ -1024,24 +885,15 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
})
|
||||
})
|
||||
|
||||
// console.log(
|
||||
// '📊 Загружаем объединенные данные (базовые + пользовательские)'
|
||||
//)
|
||||
multiSheetEngine.loadData(mergedData)
|
||||
} else if (Object.keys(finalBaseData).length > 0) {
|
||||
// console.log(
|
||||
// '📋 Используем только базовые данные (нет пользовательских изменений)'
|
||||
//)
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
} else {
|
||||
// console.log('⚠️ Нет данных для загрузки')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
|
||||
|
||||
// В случае ошибки используем только базовые данные
|
||||
if (Object.keys(finalBaseData).length > 0) {
|
||||
// console.log('📋 Ошибка API, используем только базовые данные')
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
}
|
||||
}
|
||||
@@ -1050,6 +902,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
}, [
|
||||
templateId,
|
||||
templateData,
|
||||
fileData,
|
||||
loadSheets,
|
||||
multiSheetEngine,
|
||||
setTemplateData,
|
||||
@@ -1057,9 +910,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
])
|
||||
|
||||
// Загружаем сохраненные формулы при инициализации
|
||||
// Ждем загрузки данных из React Query перед инициализацией
|
||||
useEffect(() => {
|
||||
initializeData()
|
||||
}, [initializeData])
|
||||
// Инициализируем только когда все данные загружены или их нет вообще
|
||||
const canInitialize =
|
||||
!isLoadingLatestFile && !isLoadingFileData && templateId && !isInitialized
|
||||
|
||||
if (canInitialize) {
|
||||
initializeData()
|
||||
}
|
||||
}, [
|
||||
initializeData,
|
||||
isLoadingLatestFile,
|
||||
isLoadingFileData,
|
||||
templateId,
|
||||
isInitialized,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -8,10 +6,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/component/ui/select'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { Calendar, Settings, Trash2 } from 'lucide-react'
|
||||
import { Info, Settings } from 'lucide-react'
|
||||
|
||||
interface StandardsConfig {
|
||||
registryNumber: string // ГРСИ
|
||||
sheet: string // Лист Excel
|
||||
startCell: string // Первая ячейка (например "A10")
|
||||
maxItems: number // Обычно 7
|
||||
@@ -23,119 +20,13 @@ interface StandardsEditorProps {
|
||||
onChange: (config: StandardsConfig) => void
|
||||
}
|
||||
|
||||
const CellTargetEditor: React.FC<{
|
||||
targets: CellTarget[]
|
||||
onChange: (targets: CellTarget[]) => void
|
||||
}> = ({ targets, onChange }) => {
|
||||
const handleAddTarget = () => {
|
||||
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }])
|
||||
}
|
||||
|
||||
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-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Целевые ячейки</label>
|
||||
<Button variant="outline" size="sm" onClick={handleAddTarget}>
|
||||
<Calendar className="mr-1 h-4 w-4" />
|
||||
Добавить ячейку
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-48 space-y-3 overflow-y-auto">
|
||||
{targets.map((target, index) => (
|
||||
<div key={index} className="rounded-lg border bg-gray-50 p-3">
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Лист
|
||||
</label>
|
||||
<Select
|
||||
value={target.sheet}
|
||||
onValueChange={value =>
|
||||
handleUpdateTarget(index, 'sheet', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<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 className="space-y-1">
|
||||
<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-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Название
|
||||
</label>
|
||||
<Input
|
||||
placeholder="Описание"
|
||||
value={target.displayName || ''}
|
||||
onChange={e =>
|
||||
handleUpdateTarget(index, 'displayName', e.target.value)
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTarget(index)}
|
||||
className="h-6 text-xs text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{targets.length === 0 && (
|
||||
<p className="py-4 text-center text-sm text-gray-500">
|
||||
Добавьте ячейки для связи с элементом
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
}) => {
|
||||
// Обеспечиваем значения по умолчанию
|
||||
const safeConfig = {
|
||||
registryNumber: config.registryNumber || '',
|
||||
sheet: config.sheet || 'Report',
|
||||
sheet: config.sheet || 'L',
|
||||
startCell: config.startCell || 'A1',
|
||||
maxItems: config.maxItems || 7,
|
||||
targetCells: config.targetCells || [],
|
||||
@@ -157,86 +48,36 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Номер ГРСИ</label>
|
||||
<Input
|
||||
placeholder="Введите номер ГРСИ"
|
||||
value={safeConfig.registryNumber}
|
||||
onChange={e => updateConfig({ registryNumber: e.target.value })}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Лист Excel</label>
|
||||
<Select
|
||||
value={safeConfig.sheet}
|
||||
onValueChange={value => updateConfig({ sheet: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<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 className="space-y-2">
|
||||
<label className="text-sm font-medium">Начальная ячейка</label>
|
||||
<Input
|
||||
placeholder="A1, B5..."
|
||||
value={safeConfig.startCell}
|
||||
onChange={e => updateConfig({ startCell: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Целевые ячейки</label>
|
||||
<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>
|
||||
<CellTargetEditor
|
||||
targets={safeConfig.targetCells}
|
||||
onChange={targets => updateConfig({ targetCells: targets })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Calendar className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<Info className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-800">
|
||||
Автоматическое заполнение
|
||||
</div>
|
||||
<div className="font-medium text-blue-800">Настройка ячеек</div>
|
||||
<div className="mt-1 text-xs text-blue-600">
|
||||
При выборе эталонов данные будут записаны в указанные ячейки. Если
|
||||
выбрано меньше эталонов, чем максимальное количество, оставшиеся
|
||||
ячейки останутся пустыми.
|
||||
Ячейки для размещения эталонов настраиваются в разделе "Целевые
|
||||
ячейки" выше. Эталоны будут записываться в указанные ячейки по
|
||||
порядку.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Badge } from '@/component/ui/badge'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Calendar, Settings } from 'lucide-react'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { Settings, Wrench } from 'lucide-react'
|
||||
|
||||
interface StandardsConfig {
|
||||
registryNumber: string
|
||||
sheet: string
|
||||
startCell: string
|
||||
maxItems: number
|
||||
@@ -14,51 +14,122 @@ interface StandardsPreviewProps {
|
||||
config: StandardsConfig
|
||||
}
|
||||
|
||||
// Моковые данные для эталонов (такие же как в definition.tsx)
|
||||
const mockStandards = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Эталон массы 1 кг',
|
||||
shortName: 'ЭМ-1кг',
|
||||
type: 'Масса',
|
||||
registryNumber: 'ГРСИ 12345-01',
|
||||
range: '0.5-2 кг',
|
||||
accuracy: '±0.001 г',
|
||||
certificateNumber: 'СИ-2024-001',
|
||||
validUntil: '2025-12-31',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Эталон длины 1 м',
|
||||
shortName: 'ЭД-1м',
|
||||
type: 'Длина',
|
||||
registryNumber: 'ГРСИ 12345-02',
|
||||
range: '0.5-2 м',
|
||||
accuracy: '±0.001 мм',
|
||||
certificateNumber: 'СИ-2024-002',
|
||||
validUntil: '2025-06-30',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Эталон температуры 20°C',
|
||||
shortName: 'ЭТ-20°C',
|
||||
type: 'Температура',
|
||||
registryNumber: 'ГРСИ 12345-03',
|
||||
range: '15-25°C',
|
||||
accuracy: '±0.01°C',
|
||||
certificateNumber: 'СИ-2024-003',
|
||||
validUntil: '2025-03-15',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Эталон давления 1 Па',
|
||||
shortName: 'ЭД-1Па',
|
||||
type: 'Давление',
|
||||
registryNumber: 'ГРСИ 12345-04',
|
||||
range: '0.5-2 Па',
|
||||
accuracy: '±0.001 Па',
|
||||
certificateNumber: 'СИ-2024-004',
|
||||
validUntil: '2025-09-20',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Эталон времени 1 с',
|
||||
shortName: 'ЭВ-1с',
|
||||
type: 'Время',
|
||||
registryNumber: 'ГРСИ 12345-05',
|
||||
range: '0.5-2 с',
|
||||
accuracy: '±0.000001 с',
|
||||
certificateNumber: 'СИ-2024-005',
|
||||
validUntil: '2025-12-01',
|
||||
},
|
||||
]
|
||||
|
||||
// Функция для получения типа эталона в сокращенном виде
|
||||
const getStandardTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Манометр грузопоршневой':
|
||||
return 'МГП'
|
||||
case 'Калибратор давления':
|
||||
return 'КД'
|
||||
default:
|
||||
return 'МО'
|
||||
}
|
||||
}
|
||||
|
||||
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({
|
||||
config,
|
||||
}) => {
|
||||
// Берем первые config.maxItems эталонов для предпросмотра
|
||||
const previewStandards = mockStandards.slice(0, config.maxItems || 7)
|
||||
|
||||
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">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">Измерительные эталоны</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{/* Блок с измерительными эталонами */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Эталоны</Label>
|
||||
<Button variant="outline" size="sm" disabled className="h-7 px-2">
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
Настроить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-500">
|
||||
ГРСИ: {config.registryNumber || 'Не указан'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Максимум: {config.maxItems} эталонов
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{previewStandards.map((standard, index) => (
|
||||
<div
|
||||
key={standard.id}
|
||||
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
|
||||
>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<span className="text-xs font-medium text-primary">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium text-foreground">
|
||||
{standard.shortName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{standard.accuracy} • {standard.range}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
<Wrench className="mr-1 h-3 w-3" />
|
||||
{getStandardTypeBadge(standard.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" disabled className="text-xs">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Выбрать эталоны
|
||||
</Button>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
0/{config.maxItems} выбрано
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{config.targetCells && config.targetCells.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{config.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"
|
||||
title={target.displayName}
|
||||
>
|
||||
{target.sheet}!{target.cell}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,91 @@
|
||||
import { Badge } from '@/component/ui/badge'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { parseCellAddress } from '@/lib/cell-utils'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { Weight } from 'lucide-react'
|
||||
import { Settings, Weight, Wrench } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { StandardsEditor } from './StandardsEditor'
|
||||
import { StandardsPreview } from './StandardsPreview'
|
||||
import { StandardsSelector } from './StandardsSelector'
|
||||
|
||||
// Моковые данные для эталонов (такие же как в StandardsSelector)
|
||||
const mockStandards = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Эталон массы 1 кг',
|
||||
shortName: 'ЭМ-1кг',
|
||||
type: 'Масса',
|
||||
registryNumber: 'ГРСИ 12345-01',
|
||||
range: '0.5-2 кг',
|
||||
accuracy: '±0.001 г',
|
||||
certificateNumber: 'СИ-2024-001',
|
||||
validUntil: '2025-12-31',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Эталон длины 1 м',
|
||||
shortName: 'ЭД-1м',
|
||||
type: 'Длина',
|
||||
registryNumber: 'ГРСИ 12345-02',
|
||||
range: '0.5-2 м',
|
||||
accuracy: '±0.001 мм',
|
||||
certificateNumber: 'СИ-2024-002',
|
||||
validUntil: '2025-06-30',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Эталон температуры 20°C',
|
||||
shortName: 'ЭТ-20°C',
|
||||
type: 'Температура',
|
||||
registryNumber: 'ГРСИ 12345-03',
|
||||
range: '15-25°C',
|
||||
accuracy: '±0.01°C',
|
||||
certificateNumber: 'СИ-2024-003',
|
||||
validUntil: '2025-03-15',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Эталон давления 1 Па',
|
||||
shortName: 'ЭД-1Па',
|
||||
type: 'Давление',
|
||||
registryNumber: 'ГРСИ 12345-04',
|
||||
range: '0.5-2 Па',
|
||||
accuracy: '±0.001 Па',
|
||||
certificateNumber: 'СИ-2024-004',
|
||||
validUntil: '2025-09-20',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Эталон времени 1 с',
|
||||
shortName: 'ЭВ-1с',
|
||||
type: 'Время',
|
||||
registryNumber: 'ГРСИ 12345-05',
|
||||
range: '0.5-2 с',
|
||||
accuracy: '±0.000001 с',
|
||||
certificateNumber: 'СИ-2024-005',
|
||||
validUntil: '2025-12-01',
|
||||
},
|
||||
]
|
||||
|
||||
// Функция для получения данных эталона по ID
|
||||
const getStandardById = (id: string) => {
|
||||
return mockStandards.find(standard => standard.id === id)
|
||||
}
|
||||
|
||||
// Функция для получения типа эталона в сокращенном виде
|
||||
const getStandardTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Манометр грузопоршневой':
|
||||
return 'МГП'
|
||||
case 'Калибратор давления':
|
||||
return 'КД'
|
||||
default:
|
||||
return 'МО'
|
||||
}
|
||||
}
|
||||
|
||||
interface StandardsConfig {
|
||||
registryNumber: string // ГРСИ
|
||||
sheet: string // Лист Excel
|
||||
startCell: string // Первая ячейка (например "A10")
|
||||
maxItems: number // Обычно 7
|
||||
@@ -20,39 +99,125 @@ export const standardsDefinition: ElementDefinition<StandardsConfig, string[]> =
|
||||
icon: <Weight className="h-4 w-4" />,
|
||||
version: 1,
|
||||
defaultConfig: {
|
||||
registryNumber: '',
|
||||
sheet: 'Report',
|
||||
sheet: 'L',
|
||||
startCell: 'A1',
|
||||
maxItems: 7,
|
||||
targetCells: [],
|
||||
},
|
||||
Editor: StandardsEditor,
|
||||
Preview: StandardsPreview,
|
||||
Render: () => {
|
||||
// Здесь нужно создать обертку для StandardsDialog
|
||||
// Пока возвращаем null, так как StandardsDialog требует дополнительные пропсы
|
||||
return null
|
||||
Render: ({ config, value, onChange }) => {
|
||||
const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
|
||||
const [selectedStandardIds, setSelectedStandardIds] = useState<string[]>(
|
||||
[]
|
||||
)
|
||||
|
||||
// Если value содержит названия эталонов, находим соответствующие ID
|
||||
useEffect(() => {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
const ids = value
|
||||
.map(name => {
|
||||
const standard = mockStandards.find(s => s.name === name)
|
||||
return standard?.id || ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
setSelectedStandardIds(ids)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const selectedStandardsData = selectedStandardIds
|
||||
.map(id => getStandardById(id))
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Блок с измерительными эталонами */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Эталоны</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsStandardsDialogOpen(true)}
|
||||
className="h-7 px-2"
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
Настроить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{selectedStandardsData.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{selectedStandardsData.map(
|
||||
(standard, index) =>
|
||||
standard && (
|
||||
<div
|
||||
key={standard.id}
|
||||
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
|
||||
>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<span className="text-xs font-medium text-primary">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium text-foreground">
|
||||
{standard.shortName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{standard.accuracy} • {standard.range}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 text-xs"
|
||||
>
|
||||
<Wrench className="mr-1 h-3 w-3" />
|
||||
{getStandardTypeBadge(standard.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 p-3 text-center text-sm text-muted-foreground">
|
||||
<Settings className="mx-auto mb-1 h-4 w-4 opacity-50" />
|
||||
Эталоны не выбраны
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StandardsSelector
|
||||
isOpen={isStandardsDialogOpen}
|
||||
onClose={() => setIsStandardsDialogOpen(false)}
|
||||
value={selectedStandardIds}
|
||||
onChange={standardIds => {
|
||||
setSelectedStandardIds(standardIds)
|
||||
// Преобразуем ID эталонов в их названия для сохранения в ячейки
|
||||
const standardNames = standardIds.map(
|
||||
id => getStandardById(id)?.name || ''
|
||||
)
|
||||
onChange?.(standardNames)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
mapToCells: cfg => {
|
||||
const res: CellTarget[] = []
|
||||
const parsed = parseCellAddress(cfg.startCell)
|
||||
// Используем только ячейки, указанные пользователем в targetCells
|
||||
return cfg.targetCells || []
|
||||
},
|
||||
mapToCellValues: (cfg, value) => {
|
||||
// Используем targetCells из конфигурации
|
||||
const cells = cfg.targetCells || []
|
||||
|
||||
if (!parsed) {
|
||||
// Если не удалось распарсить, возвращаем пустой массив
|
||||
return res
|
||||
}
|
||||
|
||||
const { column, row } = parsed
|
||||
|
||||
// Генерируем ячейки вертикально (A1, A2, A3...)
|
||||
for (let i = 0; i < cfg.maxItems; i++) {
|
||||
res.push({
|
||||
sheet: cfg.sheet,
|
||||
cell: `${column}${row + i}`,
|
||||
displayName: `Эталон ${i + 1}`,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
// Сопоставляем значения с ячейками
|
||||
const standardNames = Array.isArray(value) ? value : []
|
||||
return cells.map((target, idx) => ({
|
||||
target,
|
||||
value: standardNames[idx] || '', // Берем значение по индексу или пустую строку
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
getAvailableElementTypes,
|
||||
getElementDefinition,
|
||||
} from '@/entitiy/element/model/interface'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import {
|
||||
autoArrangeElements,
|
||||
findFreePosition,
|
||||
@@ -14,17 +15,7 @@ import {
|
||||
Template,
|
||||
TemplateElement,
|
||||
} from '@/type/template'
|
||||
import {
|
||||
Calendar,
|
||||
Droplets,
|
||||
Edit3,
|
||||
Gauge,
|
||||
Plus,
|
||||
Radio,
|
||||
Thermometer,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Checkbox } from '../ui/checkbox'
|
||||
@@ -43,7 +34,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select'
|
||||
import { Textarea } from '../ui/textarea'
|
||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
@@ -56,6 +46,8 @@ const CellTargetEditor: React.FC<{
|
||||
targets: CellTarget[]
|
||||
onChange: (targets: CellTarget[]) => void
|
||||
}> = ({ targets, onChange }) => {
|
||||
const { warning } = useToast()
|
||||
|
||||
const handleAddTarget = () => {
|
||||
onChange([...targets, { sheet: 'L', cell: '' }])
|
||||
}
|
||||
@@ -75,6 +67,21 @@ const CellTargetEditor: React.FC<{
|
||||
onChange(targets.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleCellInputChange = (index: number, value: string) => {
|
||||
// Проверяем на наличие кириллицы
|
||||
const hasCyrillic = /[а-яё]/i.test(value)
|
||||
|
||||
if (hasCyrillic) {
|
||||
warning('Кириллические символы не разрешены в адресе ячейки', {
|
||||
title: 'Недопустимый символ',
|
||||
duration: 3000,
|
||||
})
|
||||
return // Не обновляем значение
|
||||
}
|
||||
|
||||
handleUpdateTarget(index, 'cell', value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -119,9 +126,7 @@ const CellTargetEditor: React.FC<{
|
||||
<Input
|
||||
placeholder="A1, B5..."
|
||||
value={target.cell}
|
||||
onChange={e =>
|
||||
handleUpdateTarget(index, 'cell', e.target.value)
|
||||
}
|
||||
onChange={e => handleCellInputChange(index, e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -159,163 +164,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback для неизвестных типов элементов
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
@@ -326,7 +175,13 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||||
{element.required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{renderPreviewInput()}
|
||||
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||||
{element.type
|
||||
? `Элемент типа "${element.type}" не найден`
|
||||
: 'Выберите тип элемента'}
|
||||
</div>
|
||||
|
||||
{element.targetCells && element.targetCells.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{element.targetCells.map((target, index) => (
|
||||
@@ -699,7 +554,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
showGrid: true,
|
||||
}
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
||||
null
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ export const ToastContainer: FC<ToastContainerProps> = ({
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed bottom-4 left-4 z-50 flex w-full max-w-sm flex-col-reverse">
|
||||
<div className="pointer-events-none fixed bottom-4 left-4 z-[100] flex w-full max-w-sm flex-col-reverse">
|
||||
{toasts.map(toast => (
|
||||
<div key={toast.id} className="pointer-events-auto">
|
||||
<ToastComponent toast={toast} onRemove={onRemove} />
|
||||
|
||||
Reference in New Issue
Block a user