В разы улучшена производительность и меьнше багов
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user