В разы улучшена производительность и меьнше багов

This commit is contained in:
2025-07-15 18:11:02 +03:00
parent 40145206b2
commit 75141f5e88
5 changed files with 666 additions and 276 deletions

View File

@@ -1,5 +1,6 @@
import { produce } from 'immer'
import {
CSSProperties,
FC,
memo,
useCallback,
@@ -8,6 +9,7 @@ import {
useRef,
useState,
} from 'react'
import { VariableSizeGrid } from 'react-window'
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
const ROW_COUNT = 90
@@ -29,6 +31,14 @@ interface DualSpreadsheetProps {
templateData?: Record<string, Record<string, any>>
}
// Кэшированные данные ячеек для оптимизации
interface CachedCellData {
displayValue: string
rawValue: string
isModified: boolean
hasRightContent: boolean
}
// --- Child Components ---
interface CellProps {
sheetType: SheetType
@@ -40,12 +50,14 @@ interface CellProps {
displayValue: string // Отображаемое значение (вычисленное)
rawValue: string // "Сырое" значение (формула)
isModified: boolean
hasRightContent: boolean // Есть ли содержимое в ячейках справа
onCellClick: (row: number, col: number) => void
onCellDoubleClick: (row: number, col: number) => void
onCellValueChange: (newValue: string) => void // Изменение значения во время редактирования
stopEditing: (save: boolean) => void
setActiveSheet: (type: SheetType) => void
activeCellRef: React.RefObject<HTMLInputElement>
style: CSSProperties // Добавляем style для react-window
}
const Cell = memo(
@@ -59,24 +71,23 @@ const Cell = memo(
displayValue,
rawValue,
isModified,
hasRightContent,
onCellClick,
onCellDoubleClick,
onCellValueChange,
stopEditing,
setActiveSheet,
activeCellRef,
style, // Добавляем style для react-window
}: CellProps) => {
const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${
isModified ? 'bg-yellow-50' : ''
}`
const style = {
minWidth: `${columnWidth}px`,
width: `${columnWidth}px`,
height: `${ROW_HEIGHT}px`,
overflow: 'hidden',
const cellStyle: CSSProperties = {
...style,
overflow: 'visible',
padding: 0,
position: 'relative' as const,
}
const handleClick = useCallback(() => {
@@ -90,9 +101,9 @@ const Cell = memo(
)
return (
<td
<div
className={className}
style={style}
style={cellStyle}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
@@ -116,17 +127,29 @@ const Cell = memo(
/>
) : (
<div
className="relative h-full w-full truncate px-2 py-1"
style={
isSelected
className="relative flex h-full w-full items-center px-2"
style={{
overflow: hasRightContent ? 'hidden' : 'visible',
whiteSpace: 'nowrap',
...(isSelected
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
: {}
}
: {}),
}}
>
{displayValue}
<div
className="relative"
style={{
overflow: hasRightContent ? 'hidden' : 'visible',
textOverflow: hasRightContent ? 'ellipsis' : 'clip',
whiteSpace: 'nowrap',
maxWidth: hasRightContent ? `${columnWidth - 16}px` : 'none',
}}
>
{displayValue}
</div>
</div>
)}
</td>
</div>
)
},
)
@@ -236,15 +259,82 @@ const FormulaBar = memo(
},
)
// Рендер-проп для ячеек грида
const CellRenderer = ({
columnIndex,
rowIndex,
style,
data,
}: {
columnIndex: number
rowIndex: number
style: CSSProperties
data: any
}) => {
const {
sheetType,
widths,
selectedCell,
activeSheet,
isEditing,
editingValue,
cachedData,
handleCellClick,
handleCellDoubleClick,
handleCellChange,
stopEditing,
setActiveSheet,
activeCellInputRef,
} = data
const isCellSelected =
selectedCell?.row === rowIndex &&
selectedCell?.col === columnIndex &&
activeSheet === sheetType
const isCellEditing = isCellSelected && isEditing
const cellKey = `${rowIndex}-${columnIndex}`
const cellData = cachedData[cellKey]
const rawValue = isCellEditing ? editingValue : cellData?.rawValue || ''
const displayValue = isCellEditing
? '' // Не показывать вычисленное значение во время редактирования
: cellData?.displayValue || ''
return (
<Cell
style={style}
sheetType={sheetType}
rowIndex={rowIndex}
colIndex={columnIndex}
columnWidth={widths[columnIndex]}
isSelected={isCellSelected}
isEditing={isCellEditing}
displayValue={displayValue}
rawValue={rawValue}
isModified={cellData?.isModified || false}
hasRightContent={cellData?.hasRightContent || false}
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
onCellValueChange={handleCellChange}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef}
/>
)
}
// --- Main Component ---
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
const { revision, ...multiSheetEngine } = useMultiSheetEngine({
sheets: [
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
],
initialData: templateData || {},
})
const { revision, updateRevision, ...multiSheetEngine } = useMultiSheetEngine(
{
sheets: [
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
],
initialData: templateData || {},
},
)
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
const [selectedCell, setSelectedCell] = useState<{
@@ -260,6 +350,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
calculations: Array(COL_COUNT).fill(96),
}),
)
const gridRefs = useRef<Record<SheetType, VariableSizeGrid | null>>({
report: null,
calculations: null,
})
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
@@ -270,8 +364,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
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 initialTemplateValues = useRef<Record<string, string>>({})
// Кэш для данных ячеек
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
{},
)
const lastRevision = useRef<number>(-1)
// Загрузка данных шаблона один раз при инициализации
useEffect(() => {
const reportTemplate = templateData?.[SHEET_NAMES.report]
@@ -284,6 +392,95 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// --- Синхронизация UI ---
// Синхронизация прокрутки грида при изменении выделенной ячейки
useEffect(() => {
if (selectedCell && activeSheet) {
const grid = gridRefs.current[activeSheet]
grid?.scrollToItem({
rowIndex: selectedCell.row,
columnIndex: selectedCell.col,
align: 'auto',
})
}
}, [selectedCell, activeSheet])
// Функция для получения кэшированных данных ячеек
const getCachedCellData = useCallback(
(sheetType: SheetType): Record<string, CachedCellData> => {
const cacheKey = `${sheetType}-${revision}`
// Если данные уже кэшированы для этой ревизии, возвращаем их
if (cellDataCache.current[cacheKey]) {
return cellDataCache.current[cacheKey]
}
const sheetName = SHEET_NAMES[sheetType]
const cachedData: Record<string, CachedCellData> = {}
// Предварительно получаем все значения для оптимизации
const allCellValues: Record<string, string> = {}
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}`
const displayValue = String(
multiSheetEngine.getCellValue(sheetName, row, col) || '',
)
allCellValues[cellKey] = displayValue.trim()
}
}
// Вычисляем hasRightContent для всех ячеек сразу
const rightContentMap: Record<string, boolean> = {}
for (let row = 0; row < ROW_COUNT; row++) {
let hasContentOnRight = false
for (let col = COL_COUNT - 1; col >= 0; col--) {
const cellKey = `${row}-${col}`
rightContentMap[cellKey] = hasContentOnRight
if (allCellValues[cellKey]) {
hasContentOnRight = true
}
}
}
// Формируем кэшированные данные
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}`
const rawValue = multiSheetEngine.getCellRawValue(sheetName, row, col)
const displayValue = allCellValues[cellKey]
const cellName = getColumnLabel(col) + (row + 1)
const templateValue = initialTemplateValues.current[cellName] ?? ''
const isModified =
sheetType === 'report' &&
templateValue !== '' &&
rawValue !== templateValue
cachedData[cellKey] = {
displayValue,
rawValue,
isModified,
hasRightContent: rightContentMap[cellKey],
}
}
}
// Очищаем старые кэши для экономии памяти
if (lastRevision.current !== revision) {
Object.keys(cellDataCache.current).forEach((key) => {
if (!key.endsWith(`-${revision}`)) {
delete cellDataCache.current[key]
}
})
lastRevision.current = revision
}
cellDataCache.current[cacheKey] = cachedData
return cachedData
},
[revision, multiSheetEngine, initialTemplateValues],
)
// Синхронизация строки формул
const formulaBarValue = useMemo(() => {
if (isEditing) {
@@ -309,11 +506,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Фокус на инпуте при начале редактирования
useEffect(() => {
if (isEditing) {
if (activeCellInputRef.current) {
activeCellInputRef.current.focus()
activeCellInputRef.current.select()
}
if (isEditing && activeCellInputRef.current) {
const input = activeCellInputRef.current
input.focus()
const length = input.value.length
input.setSelectionRange(length, length)
}
}, [isEditing])
@@ -341,13 +538,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
selectedCell.col,
editingValue,
)
// Немедленно обновляем интерфейс для быстрого отклика
updateRevision()
// Запускаем отложенный пересчет для формул
multiSheetEngine.debouncedRecalc()
}
setIsEditing(false)
setEditingValue('') // Очищаем временное значение
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
},
[activeSheet, selectedCell, editingValue, multiSheetEngine],
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision],
)
const handleCellChange = useCallback(
@@ -359,16 +559,25 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
[isEditing],
)
// Оптимизированная функция очистки ячейки
const handleClearCell = useCallback(() => {
if (!selectedCell) return
const sheetName = SHEET_NAMES[activeSheet]
multiSheetEngine.setCellValue(
// Используем версию без немедленного пересчета для быстрой реакции
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
selectedCell.row,
selectedCell.col,
'',
)
}, [activeSheet, selectedCell, multiSheetEngine])
// Немедленно обновляем интерфейс для быстрого отклика
updateRevision()
// Запускаем отложенный пересчет
multiSheetEngine.debouncedRecalc()
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
const moveSelection = useCallback(
(deltaRow: number, deltaCol: number) => {
@@ -457,6 +666,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
setColumnWidths(
produce((draft) => {
draft[sheetType][colIndex] = Math.max(60, newWidth)
// Принудительно пересчитываем layout грида
const grid = gridRefs.current[sheetType]
if (grid) {
grid.resetAfterColumnIndex(colIndex)
}
}),
)
},
@@ -507,167 +721,179 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
[stopEditing, moveSelection, activeSheet],
)
const onGridScroll = useCallback(
(
sheetType: SheetType,
{ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number },
) => {
if (headerRefs.current[sheetType]) {
headerRefs.current[sheetType]!.scrollLeft = scrollLeft
}
if (rowHeaderRefs.current[sheetType]) {
rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop
}
},
[],
)
// --- Render Functions ---
const renderSpreadsheet = (sheetType: SheetType) => {
const widths = columnWidths[sheetType]
const sheetName = SHEET_NAMES[sheetType]
const renderSpreadsheet = useCallback(
(sheetType: SheetType) => {
const widths = columnWidths[sheetType]
const cachedData = getCachedCellData(sheetType)
return (
<div
ref={(el) => (containerRefs.current[sheetType] = el)}
className="flex h-full flex-col overflow-hidden rounded-lg border bg-card shadow-sm"
onKeyDown={handleKeyDown}
tabIndex={0}
>
<div className="flex-shrink-0 border-b bg-muted/50 px-3 py-1">
<div className="flex items-center space-x-2">
<div
className={`h-2 w-2 rounded-full ${
sheetType === 'report' ? 'bg-primary' : 'bg-emerald-500'
}`}
></div>
<h3 className="text-xs font-medium text-muted-foreground">
{sheetType === 'report' ? 'Отчет' : 'Расчеты'}
</h3>
const itemData = {
sheetType,
widths,
selectedCell,
activeSheet,
isEditing,
editingValue,
cachedData,
handleCellClick,
handleCellDoubleClick,
handleCellChange,
stopEditing,
setActiveSheet,
activeCellInputRef,
}
return (
<div
ref={(el) => (containerRefs.current[sheetType] = el)}
className="flex h-full flex-col overflow-hidden rounded-lg border bg-card shadow-sm"
onKeyDown={handleKeyDown}
tabIndex={0}
>
{/* Header */}
<div className="flex-shrink-0 border-b bg-muted/50 px-3 py-1">
<div className="flex items-center space-x-2">
<div
className={`h-2 w-2 rounded-full ${
sheetType === 'report' ? 'bg-primary' : 'bg-emerald-500'
}`}
></div>
<h3 className="text-xs font-medium text-muted-foreground">
{sheetType === 'report' ? 'Отчет' : 'Расчеты'}
</h3>
</div>
</div>
</div>
<div className="flex-1 overflow-auto">
<table
className="w-full border-collapse"
style={{ tableLayout: 'fixed' }}
>
<thead>
<tr>
<th className="h-7 w-10 border border-border bg-muted text-xs font-medium text-muted-foreground"></th>
{Array.from({ length: COL_COUNT }, (_, i) => {
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const startWidth = widths[i]
const handleMouseMove = (me: MouseEvent) => {
const newWidth = startWidth + (me.clientX - startX)
handleColumnResize(sheetType, i, newWidth)
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return (
<th
key={i}
className="relative h-7 border border-border bg-muted text-center text-xs font-medium text-muted-foreground"
style={{ width: widths[i] }}
{/* Spreadsheet Body */}
<div className="flex flex-1">
{/* Row Headers */}
<div className="flex-shrink-0 bg-muted">
<div className="flex h-7 w-10 items-center justify-center border-b border-r border-border" />
<div
ref={(el) => (rowHeaderRefs.current[sheetType] = el)}
className="overflow-y-hidden"
style={{ height: 600 }} // This should be dynamic based on container size
>
<div>
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
<div
key={rowIndex}
className="flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium text-muted-foreground"
>
{getColumnLabel(i)}
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
onMouseDown={handleMouseDown}
/>
</th>
)
})}
</tr>
</thead>
<tbody>
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex}>
<td className="h-7 w-10 border border-border bg-muted text-center text-xs font-medium text-muted-foreground">
{rowIndex + 1}
</td>
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
const isCellSelected =
selectedCell?.row === rowIndex &&
selectedCell?.col === colIndex &&
activeSheet === sheetType
const isCellEditing = isCellSelected && isEditing
{rowIndex + 1}
</div>
))}
</div>
</div>
</div>
const rawValue = isCellEditing
? editingValue
: multiSheetEngine.getCellRawValue(
sheetName,
rowIndex,
colIndex,
)
const displayValue = isCellEditing
? '' // Не показывать вычисленное значение во время редактирования
: String(
multiSheetEngine.getCellValue(
sheetName,
rowIndex,
colIndex,
) || '',
<div className="flex-1 overflow-hidden">
{/* Column Headers */}
<div
ref={(el) => (headerRefs.current[sheetType] = el)}
className="overflow-x-hidden"
>
<div className="flex">
{Array.from({ length: COL_COUNT }).map((_, i) => {
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const startWidth = widths[i]
const handleMouseMove = (me: MouseEvent) => {
const newWidth = startWidth + (me.clientX - startX)
handleColumnResize(sheetType, i, newWidth)
}
const handleMouseUp = () => {
document.removeEventListener(
'mousemove',
handleMouseMove,
)
document.removeEventListener('mouseup', handleMouseUp)
}
const cellName = getColumnLabel(colIndex) + (rowIndex + 1)
const templateValue =
initialTemplateValues.current[cellName] ?? ''
const isModified =
sheetType === 'report' &&
templateValue !== '' &&
rawValue !== templateValue
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return (
<Cell
key={colIndex}
sheetType={sheetType}
rowIndex={rowIndex}
colIndex={colIndex}
columnWidth={widths[colIndex]}
isSelected={isCellSelected}
isEditing={isCellEditing}
displayValue={displayValue}
rawValue={rawValue}
isModified={isModified}
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
onCellValueChange={handleCellChange}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef}
/>
<div
key={i}
className="relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium text-muted-foreground"
style={{ width: widths[i] }}
>
{getColumnLabel(i)}
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
onMouseDown={handleMouseDown}
/>
</div>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
</div>
</div>
// Используем revision в качестве зависимости, чтобы гарантировать перерисовку
// при изменении данных в движке.
const memoizedReportSheet = useMemo(
() => renderSpreadsheet('report'),
// eslint-disable-next-line react-hooks/exhaustive-deps
{/* Grid */}
<VariableSizeGrid
ref={(el) => (gridRefs.current[sheetType] = el)}
columnCount={COL_COUNT}
rowCount={ROW_COUNT}
columnWidth={(index: number) => widths[index]}
rowHeight={() => ROW_HEIGHT}
height={600} // This should be dynamic based on container size
width={800} // This should be dynamic based on container size
itemData={itemData}
onScroll={(e) => onGridScroll(sheetType, e)}
>
{CellRenderer}
</VariableSizeGrid>
</div>
</div>
</div>
)
},
[
columnWidths.report,
activeSheet,
columnWidths,
getCachedCellData,
handleKeyDown,
handleColumnResize,
selectedCell,
activeSheet,
isEditing,
editingValue,
revision,
handleCellClick,
handleCellDoubleClick,
handleCellChange,
stopEditing,
setActiveSheet,
onGridScroll,
],
)
// Мемоизированные компоненты листов
const memoizedReportSheet = useMemo(
() => renderSpreadsheet('report'),
[renderSpreadsheet, revision], // Добавляем revision для перерендера
)
const memoizedCalculationsSheet = useMemo(
() => renderSpreadsheet('calculations'),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
columnWidths.calculations,
activeSheet,
selectedCell,
isEditing,
editingValue,
revision,
],
[renderSpreadsheet, revision], // Добавляем revision для перерендера
)
return (

View File

@@ -30,6 +30,28 @@ export function useMultiSheetEngine({
const debouncedRecalcTimeoutRef = useRef<number | null>(null)
const isRecalculatingRef = useRef(false)
// Безопасная функция для вычислений
const safeRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return
isRecalculatingRef.current = true
setIsCalculating(true)
try {
workbookRef.current.recalc()
} catch (error) {
console.warn('Ошибка при вычислении:', error)
} finally {
// Уменьшаем задержку для быстрого отклика
setTimeout(() => {
setIsCalculating(false)
isRecalculatingRef.current = false
// Увеличиваем ревизию, чтобы сообщить компонентам о необходимости обновления
setRevision((r) => r + 1)
}, 50) // Уменьшено с 150ms до 50ms
}
}, [])
// Инициализация workbook и листов
useEffect(() => {
if (!workbookRef.current) {
@@ -50,8 +72,7 @@ export function useMultiSheetEngine({
setRevision((r) => r + 1)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Зависимости убраны, чтобы инициализация проходила один раз
}, [initialData, safeRecalc, sheets]) // Восстанавливаем зависимости
// Синхронизация всех листов из движка
const syncAllSheetsFromEngine = useCallback(() => {
@@ -89,28 +110,6 @@ export function useMultiSheetEngine({
// setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано
}, [sheets])
// Безопасная функция для вычислений
const safeRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return
isRecalculatingRef.current = true
setIsCalculating(true)
try {
workbookRef.current.recalc()
} catch (error) {
console.warn('Ошибка при вычислении:', error)
} finally {
// Небольшая задержка для визуального индикатора
setTimeout(() => {
setIsCalculating(false)
isRecalculatingRef.current = false
// Увеличиваем ревизию, чтобы сообщить компонентам о необходимости обновления
setRevision((r) => r + 1)
}, 150)
}
}, [])
// Отложенный пересчет с дебаунсом
const debouncedRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return
@@ -120,12 +119,17 @@ export function useMultiSheetEngine({
clearTimeout(debouncedRecalcTimeoutRef.current)
}
// Запускаем пересчет через 300ms
// Уменьшаем задержку для быстрого отклика
debouncedRecalcTimeoutRef.current = setTimeout(() => {
safeRecalc()
}, 300)
}, 100) // Уменьшено с 300ms до 100ms
}, [safeRecalc])
// Функция для немедленного обновления интерфейса без пересчета
const updateRevision = useCallback(() => {
setRevision((r) => r + 1)
}, [])
// Установка значения ячейки без немедленного пересчета
const setCellValueWithoutRecalc = useCallback(
(sheetName: string, row: number, col: number, value: string) => {
@@ -230,8 +234,6 @@ export function useMultiSheetEngine({
// Пересчет всех формул
const recalculate = useCallback(() => {
if (!workbookRef.current) return
safeRecalc()
}, [safeRecalc])
@@ -250,6 +252,7 @@ export function useMultiSheetEngine({
setCellValue,
setCellValueWithoutRecalc,
debouncedRecalc,
updateRevision, // Добавляем новую функцию
getCellValue,
getCellRawValue,
getModifiedCells,

View File

@@ -1,46 +1,45 @@
ц// Типы элементов интерфейса
export type ElementType = 'text' | 'select' | 'radio' | 'checkbox' | 'number' | 'textarea' | 'date';
// Типы элементов интерфейса
export type ElementType =
| 'text'
| 'select'
| 'radio'
| 'checkbox'
| 'number'
| 'textarea'
| 'date'
export interface ElementOption {
value: string;
label: string;
value: string
label: string
}
export interface TemplateElement {
id: string;
type: ElementType;
name: string;
label: string;
targetCell: string; // например "A1", "B5"
options?: ElementOption[]; // для select и radio
required?: boolean;
defaultValue?: string;
placeholder?: string;
id: string
type: ElementType
name: string
label: string
targetCell: string // например "A1", "B5"
options?: ElementOption[] // для select и radio
required?: boolean
defaultValue?: string
placeholder?: string
}
// Типы шаблонов
export interface Template {
id: string;
name: string;
description?: string;
excelFile?: File;
excelData?: Record<string, any>; // данные из Excel
elements: TemplateElement[];
createdAt: Date;
updatedAt: Date;
id: string
name: string
description?: string
excelFile?: File
excelData?: Record<string, any> // данные из Excel
elements: TemplateElement[]
createdAt: Date
updatedAt: Date
}
// Типы для работы с данными
export interface TemplateData {
templateId: string;
values: Record<string, any>; // elementId -> value
calculatedCells?: Record<string, any>; // cellName -> value
templateId: string
values: Record<string, any> // elementId -> value
calculatedCells?: Record<string, any> // cellName -> value
}
// Типы для экспорта
export interface ExportData {
templateId: string;
templateName: string;
excelFile: File;
cellUpdates: Record<string, any>; // cellName -> value
}