переименования папок
This commit is contained in:
313
src/hook/useFormulaAutoSave.ts
Normal file
313
src/hook/useFormulaAutoSave.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export interface SavedFormulaData {
|
||||
templateId?: string
|
||||
formulaData: Record<string, Record<string, any>>
|
||||
timestamp: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface UseFormulaAutoSaveOptions {
|
||||
templateId?: string
|
||||
autoSaveDelay?: number // задержка автосохранения в мс
|
||||
enableAutoSave?: boolean
|
||||
}
|
||||
|
||||
// Ключ для localStorage
|
||||
const getStorageKey = (templateId: string) => {
|
||||
console.log('🔑 Получен templateId:', templateId)
|
||||
return `formula_data_${templateId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Хук для автосохранения формул электронных таблиц в localStorage
|
||||
*
|
||||
* Функции:
|
||||
* - Автоматическое сохранение с дебаунсом при изменениях
|
||||
* - Ручное сохранение по требованию
|
||||
* - Отслеживание состояния сохранения
|
||||
* - Сохранение в localStorage (заглушка для API)
|
||||
*/
|
||||
export const useFormulaAutoSave = (
|
||||
getAllModifiedCells: () => Record<string, Record<string, any>>,
|
||||
isCalculating: boolean,
|
||||
revision: number,
|
||||
options: UseFormulaAutoSaveOptions = {}
|
||||
) => {
|
||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
||||
|
||||
// Состояние
|
||||
const [lastSavedRevision, setLastSavedRevision] = useState(0)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
|
||||
const [savedData, setSavedData] = useState<SavedFormulaData | null>(null)
|
||||
|
||||
// Refs
|
||||
const saveTimeoutRef = useRef<number | null>(null)
|
||||
const versionCounter = useRef(1)
|
||||
|
||||
// Загрузка данных из localStorage при инициализации
|
||||
useEffect(() => {
|
||||
if (!templateId) return
|
||||
|
||||
try {
|
||||
const storageKey = getStorageKey(templateId)
|
||||
const savedDataStr = localStorage.getItem(storageKey)
|
||||
|
||||
if (savedDataStr) {
|
||||
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
|
||||
setSavedData(parsedData)
|
||||
versionCounter.current = parsedData.version + 1
|
||||
console.log(
|
||||
'📂 Загружены сохраненные формулы (версия:',
|
||||
parsedData.version,
|
||||
')'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Ошибка при загрузке данных из localStorage:', error)
|
||||
}
|
||||
}, [templateId])
|
||||
|
||||
// Основная функция сохранения в localStorage
|
||||
const performSave = useCallback(
|
||||
async (data: Record<string, Record<string, any>>) => {
|
||||
if (Object.keys(data).length === 0) {
|
||||
console.log('📭 Нет данных для сохранения')
|
||||
return
|
||||
}
|
||||
|
||||
if (!templateId) {
|
||||
console.warn('⚠️ Не указан templateId для сохранения формул')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
setLastSaveError(null)
|
||||
|
||||
try {
|
||||
// Имитируем задержку API для реалистичности
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, 200 + Math.random() * 300)
|
||||
)
|
||||
|
||||
// Иногда имитируем ошибку (2% вероятность)
|
||||
if (Math.random() < 0.02) {
|
||||
throw new Error('Ошибка записи в хранилище')
|
||||
}
|
||||
|
||||
const savedData: SavedFormulaData = {
|
||||
templateId,
|
||||
formulaData: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: versionCounter.current++,
|
||||
}
|
||||
|
||||
// Сохраняем в localStorage
|
||||
const storageKey = getStorageKey(templateId)
|
||||
localStorage.setItem(storageKey, JSON.stringify(savedData))
|
||||
|
||||
setSavedData(savedData)
|
||||
setLastSavedRevision(revision)
|
||||
|
||||
console.log(
|
||||
'💾 Сохранено формул:',
|
||||
Object.values(data).reduce(
|
||||
(sum, sheet) => sum + Object.keys(sheet).length,
|
||||
0
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
setLastSaveError(errorMessage)
|
||||
console.error(
|
||||
'❌ Ошибка при сохранении формул в localStorage:',
|
||||
errorMessage
|
||||
)
|
||||
|
||||
// Не обновляем lastSavedRevision при ошибке, чтобы попытаться снова
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
},
|
||||
[templateId, revision]
|
||||
)
|
||||
|
||||
// Автосохранение с дебаунсом
|
||||
const scheduleAutoSave = useCallback(() => {
|
||||
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
|
||||
enableAutoSave,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
})
|
||||
|
||||
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
|
||||
console.log('❌ Автосохранение отменено по условиям')
|
||||
return
|
||||
}
|
||||
|
||||
// Очищаем предыдущий таймер
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
console.log('🔄 Очищен предыдущий таймер автосохранения')
|
||||
}
|
||||
|
||||
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
console.log('🎯 Выполняем отложенное автосохранение')
|
||||
const currentData = getAllModifiedCells()
|
||||
console.log('📋 Данные для автосохранения:', currentData)
|
||||
performSave(currentData)
|
||||
}, autoSaveDelay)
|
||||
}, [
|
||||
enableAutoSave,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
autoSaveDelay,
|
||||
getAllModifiedCells,
|
||||
performSave,
|
||||
])
|
||||
|
||||
// Эффект для автосохранения при изменениях
|
||||
useEffect(() => {
|
||||
console.log('🔍 Проверка условий автосохранения:', {
|
||||
enableAutoSave,
|
||||
revision,
|
||||
lastSavedRevision,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
shouldSave:
|
||||
enableAutoSave &&
|
||||
revision > lastSavedRevision &&
|
||||
!isCalculating &&
|
||||
!isSaving,
|
||||
})
|
||||
|
||||
if (
|
||||
enableAutoSave &&
|
||||
revision > lastSavedRevision &&
|
||||
!isCalculating &&
|
||||
!isSaving
|
||||
) {
|
||||
console.log('✅ Планируем автосохранение...')
|
||||
scheduleAutoSave()
|
||||
} else {
|
||||
console.log('⏸️ Автосохранение пропущено по условиям')
|
||||
}
|
||||
}, [
|
||||
enableAutoSave,
|
||||
revision,
|
||||
lastSavedRevision,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
scheduleAutoSave,
|
||||
])
|
||||
|
||||
// Ручное сохранение
|
||||
const manualSave = useCallback(() => {
|
||||
console.log('🖱️ Ручное сохранение вызвано')
|
||||
|
||||
// Отменяем автосохранение если оно запланировано
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
saveTimeoutRef.current = null
|
||||
console.log('⏹️ Отменено запланированное автосохранение')
|
||||
}
|
||||
|
||||
const currentData = getAllModifiedCells()
|
||||
console.log('📋 Данные для сохранения:', currentData)
|
||||
|
||||
if (Object.keys(currentData).length === 0) {
|
||||
console.log('📭 Нет измененных данных для сохранения')
|
||||
return
|
||||
}
|
||||
|
||||
performSave(currentData)
|
||||
}, [getAllModifiedCells, performSave])
|
||||
|
||||
// Функция загрузки сохраненных формул из localStorage
|
||||
const loadSavedFormulas = useCallback((): Record<
|
||||
string,
|
||||
Record<string, any>
|
||||
> | null => {
|
||||
if (!templateId) return null
|
||||
|
||||
try {
|
||||
const storageKey = getStorageKey(templateId)
|
||||
const savedDataStr = localStorage.getItem(storageKey)
|
||||
|
||||
if (savedDataStr) {
|
||||
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
|
||||
|
||||
console.log('📂 Формулы загружены из localStorage:', {
|
||||
templateId,
|
||||
version: parsedData.version,
|
||||
timestamp: parsedData.timestamp,
|
||||
})
|
||||
|
||||
return parsedData.formulaData
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'❌ Ошибка при загрузке сохраненных формул из localStorage:',
|
||||
error
|
||||
)
|
||||
return null
|
||||
}
|
||||
}, [templateId])
|
||||
|
||||
// Функция очистки сохраненных данных
|
||||
const clearSavedFormulas = useCallback(() => {
|
||||
if (!templateId) return
|
||||
|
||||
try {
|
||||
const storageKey = getStorageKey(templateId)
|
||||
localStorage.removeItem(storageKey)
|
||||
setSavedData(null)
|
||||
setLastSavedRevision(0)
|
||||
versionCounter.current = 1
|
||||
|
||||
console.log('🗑️ Сохраненные формулы очищены:', { templateId })
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при очистке сохраненных формул:', error)
|
||||
}
|
||||
}, [templateId])
|
||||
|
||||
// Очистка таймаута при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Вычисляемые значения
|
||||
const isDataChanged = revision > lastSavedRevision
|
||||
const hasUnsavedChanges = isDataChanged && !isSaving
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
isSaving,
|
||||
isDataChanged,
|
||||
hasUnsavedChanges,
|
||||
lastSaveError,
|
||||
lastSavedRevision,
|
||||
savedData,
|
||||
|
||||
// Методы
|
||||
manualSave,
|
||||
loadSavedFormulas,
|
||||
clearSavedFormulas,
|
||||
|
||||
// Утилиты
|
||||
getCurrentData: getAllModifiedCells,
|
||||
clearError: () => setLastSaveError(null),
|
||||
}
|
||||
}
|
||||
346
src/hook/useMultiSheetEngine.ts
Normal file
346
src/hook/useMultiSheetEngine.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
CellValue,
|
||||
Workbook,
|
||||
} from '../lib/spreadsheet-engine/spreadsheet-engine'
|
||||
interface SheetConfig {
|
||||
name: string
|
||||
rows: number
|
||||
cols: number
|
||||
}
|
||||
|
||||
interface UseMultiSheetEngineProps {
|
||||
sheets: SheetConfig[]
|
||||
initialData?: Record<string, Record<string, any>>
|
||||
}
|
||||
|
||||
export function useMultiSheetEngine({
|
||||
sheets,
|
||||
initialData = {},
|
||||
}: UseMultiSheetEngineProps) {
|
||||
const workbookRef = useRef<Workbook | null>(null)
|
||||
const [isCalculating, setIsCalculating] = useState(false)
|
||||
const [revision, setRevision] = useState(0) // Новый счетчик для обновлений
|
||||
const debouncedRecalcTimeoutRef = useRef<number | null>(null)
|
||||
const isRecalculatingRef = useRef(false)
|
||||
|
||||
// Храним изначальные данные шаблона для сравнения
|
||||
const templateDataRef = useRef<Record<string, Record<string, any>>>({})
|
||||
|
||||
// Безопасная функция для вычислений
|
||||
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) {
|
||||
workbookRef.current = new Workbook()
|
||||
|
||||
// Создаем листы
|
||||
sheets.forEach(sheetConfig => {
|
||||
workbookRef.current!.addSheet(sheetConfig.name)
|
||||
})
|
||||
|
||||
// Увеличиваем ревизию для начальной отрисовки
|
||||
setRevision(r => r + 1)
|
||||
}
|
||||
}, [sheets])
|
||||
|
||||
// Отдельный useEffect для перезагрузки данных при изменении initialData
|
||||
useEffect(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
if (Object.keys(initialData).length > 0) {
|
||||
// Очищаем только те листы, для которых есть новые данные
|
||||
for (const sheetName of Object.keys(initialData)) {
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (sheet) {
|
||||
sheet.cells.clear()
|
||||
}
|
||||
}
|
||||
|
||||
workbookRef.current.fromJSON(initialData)
|
||||
safeRecalc()
|
||||
} else {
|
||||
// Если данных нет, просто обновляем ревизию
|
||||
setRevision(r => r + 1)
|
||||
}
|
||||
}, [initialData, safeRecalc])
|
||||
|
||||
// Синхронизация всех листов из движка
|
||||
// const syncAllSheetsFromEngine = useCallback(() => {
|
||||
// if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
|
||||
// const newSheetCells: Record<string, CellData[][]> = {}
|
||||
|
||||
// sheets.forEach((sheetConfig) => {
|
||||
// const sheet = workbookRef.current!.getSheet(sheetConfig.name)
|
||||
// if (!sheet) return
|
||||
|
||||
// const cells = Array(sheetConfig.rows)
|
||||
// .fill(null)
|
||||
// .map(() =>
|
||||
// Array(sheetConfig.cols)
|
||||
// .fill(null)
|
||||
// .map(() => ({ value: '', isSelected: false })),
|
||||
// )
|
||||
|
||||
// // Получаем все ячейки из движка
|
||||
// for (let row = 0; row < sheetConfig.rows; row++) {
|
||||
// for (let col = 0; col < sheetConfig.cols; col++) {
|
||||
// const cellName = columnToLabel(col) + (row + 1)
|
||||
// const cell = sheet.getCell(cellName)
|
||||
|
||||
// if (cell.raw !== null && cell.raw !== undefined) {
|
||||
// cells[row][col] = String(cell.raw)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// newSheetCells[sheetConfig.name] = cells
|
||||
// })
|
||||
|
||||
// // setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано
|
||||
// }, [sheets])
|
||||
|
||||
// Отложенный пересчет с дебаунсом
|
||||
const debouncedRecalc = useCallback(() => {
|
||||
if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
|
||||
// Очищаем предыдущий отложенный пересчет
|
||||
if (debouncedRecalcTimeoutRef.current) {
|
||||
clearTimeout(debouncedRecalcTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Уменьшаем задержку для быстрого отклика
|
||||
debouncedRecalcTimeoutRef.current = setTimeout(() => {
|
||||
safeRecalc()
|
||||
}, 100) // Уменьшено с 300ms до 100ms
|
||||
}, [safeRecalc])
|
||||
|
||||
// Функция для немедленного обновления интерфейса без пересчета
|
||||
const updateRevision = useCallback(() => {
|
||||
setRevision(r => r + 1)
|
||||
}, [])
|
||||
|
||||
// Установка значения ячейки без немедленного пересчета
|
||||
const setCellValueWithoutRecalc = useCallback(
|
||||
(sheetName: string, row: number, col: number, value: string) => {
|
||||
if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
|
||||
const cellName = columnToLabel(col) + (row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
// Устанавливаем значение в движке без пересчета
|
||||
workbookRef.current.setCell(qualifiedName, value)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Установка значения ячейки с немедленным пересчетом
|
||||
const setCellValue = useCallback(
|
||||
(sheetName: string, row: number, col: number, value: string) => {
|
||||
if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
|
||||
const cellName = columnToLabel(col) + (row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
// Устанавливаем значение в движке
|
||||
workbookRef.current.setCell(qualifiedName, value)
|
||||
|
||||
// Запускаем безопасный пересчет
|
||||
safeRecalc()
|
||||
},
|
||||
[safeRecalc]
|
||||
)
|
||||
|
||||
// Получение значения ячейки (вычисленное)
|
||||
const getCellValue = useCallback(
|
||||
(sheetName: string, row: number, col: number): CellValue => {
|
||||
if (!workbookRef.current) return ''
|
||||
|
||||
const cellName = columnToLabel(col) + (row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
return workbookRef.current.getValue(qualifiedName)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Получение сырого значения ячейки (формула)
|
||||
const getCellRawValue = useCallback(
|
||||
(sheetName: string, row: number, col: number): string => {
|
||||
if (!workbookRef.current) return ''
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return ''
|
||||
|
||||
const cellName = columnToLabel(col) + (row + 1)
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
return String(cell.raw || '')
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Получение всех измененных ячеек для листа
|
||||
const getModifiedCells = useCallback(
|
||||
(sheetName: string): Record<string, CellValue> => {
|
||||
if (!workbookRef.current) return {}
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return {}
|
||||
|
||||
return sheet.toJSON()
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Установка изначальных данных шаблона
|
||||
const setTemplateData = useCallback(
|
||||
(data: Record<string, Record<string, any>>) => {
|
||||
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
|
||||
console.log('📋 Установлены данные шаблона:', templateDataRef.current)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Получение всех измененных ячеек для всех листов (только пользовательские изменения)
|
||||
const getAllModifiedCells = useCallback((): Record<
|
||||
string,
|
||||
Record<string, CellValue>
|
||||
> => {
|
||||
if (!workbookRef.current) return {}
|
||||
|
||||
const allData = workbookRef.current.toJSON()
|
||||
const modifiedData: Record<string, Record<string, CellValue>> = {}
|
||||
|
||||
// Список всех листов (из движка или baseline)
|
||||
const sheetNamesSet = new Set([
|
||||
...Object.keys(allData),
|
||||
...Object.keys(templateDataRef.current),
|
||||
])
|
||||
|
||||
sheetNamesSet.forEach(sheetName => {
|
||||
const sheetData = allData[sheetName] || {}
|
||||
const templateSheet = templateDataRef.current[sheetName] || {}
|
||||
|
||||
const cellAddresses = new Set([
|
||||
...Object.keys(sheetData),
|
||||
...Object.keys(templateSheet),
|
||||
])
|
||||
|
||||
const modifiedSheet: Record<string, CellValue> = {}
|
||||
|
||||
cellAddresses.forEach(cellAddress => {
|
||||
const currentVal = sheetData[cellAddress] ?? ''
|
||||
const templateVal = templateSheet[cellAddress] ?? ''
|
||||
|
||||
const normalizeValue = (val: any): string => {
|
||||
if (val === null || val === undefined) return ''
|
||||
const str = String(val).trim()
|
||||
if (str === '0' || str === '') return ''
|
||||
return str
|
||||
}
|
||||
|
||||
const normalizedCurrent = normalizeValue(currentVal)
|
||||
const normalizedTemplate = normalizeValue(templateVal)
|
||||
|
||||
if (normalizedCurrent !== normalizedTemplate) {
|
||||
// Если текущего значения нет (ячейка очищена), отправляем ''
|
||||
modifiedSheet[cellAddress] = currentVal
|
||||
console.log(
|
||||
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
|
||||
{
|
||||
template: normalizedTemplate,
|
||||
current: normalizedCurrent,
|
||||
value: currentVal,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(modifiedSheet).length > 0) {
|
||||
modifiedData[sheetName] = modifiedSheet
|
||||
}
|
||||
})
|
||||
|
||||
return modifiedData
|
||||
}, [])
|
||||
|
||||
// Очистка всех данных
|
||||
const clearAllData = useCallback(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
workbookRef.current.clear()
|
||||
setRevision(r => r + 1)
|
||||
}, [])
|
||||
|
||||
// Загрузка данных из JSON
|
||||
const loadData = useCallback(
|
||||
(data: Record<string, Record<string, any>>) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
workbookRef.current.fromJSON(data)
|
||||
safeRecalc()
|
||||
},
|
||||
[safeRecalc]
|
||||
)
|
||||
|
||||
// Пересчет всех формул
|
||||
const recalculate = useCallback(() => {
|
||||
safeRecalc()
|
||||
}, [safeRecalc])
|
||||
|
||||
// Очистка таймаутов при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debouncedRecalcTimeoutRef.current) {
|
||||
clearTimeout(debouncedRecalcTimeoutRef.current)
|
||||
}
|
||||
isRecalculatingRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// sheetCells убран
|
||||
setCellValue,
|
||||
setCellValueWithoutRecalc,
|
||||
debouncedRecalc,
|
||||
updateRevision, // Добавляем новую функцию
|
||||
getCellValue,
|
||||
getCellRawValue,
|
||||
getModifiedCells,
|
||||
getAllModifiedCells,
|
||||
setTemplateData, // Экспортируем функцию установки данных шаблона
|
||||
clearAllData,
|
||||
loadData,
|
||||
recalculate,
|
||||
isCalculating,
|
||||
workbook: workbookRef.current,
|
||||
revision, // Экспортируем ревизию
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для конвертации индекса колонки в букву
|
||||
function columnToLabel(index: number): string {
|
||||
return String.fromCharCode(65 + index)
|
||||
}
|
||||
387
src/hook/useSheetAutoSave.ts
Normal file
387
src/hook/useSheetAutoSave.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
ApiSheet,
|
||||
createSheet,
|
||||
getSheetsByTemplate,
|
||||
patchSheet,
|
||||
} from '../service/sheetApiService'
|
||||
|
||||
export interface UseSheetAutoSaveOptions {
|
||||
templateId?: string
|
||||
autoSaveDelay?: number // задержка автосохранения в мс
|
||||
enableAutoSave?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Хук для автосохранения листов через API
|
||||
*
|
||||
* Функции:
|
||||
* - Автоматическое сохранение с дебаунсом при изменениях
|
||||
* - Ручное сохранение по требованию
|
||||
* - Создание и обновление листов через API
|
||||
* - Загрузка существующих листов
|
||||
*/
|
||||
// Утилита для извлечения UUID из templateId
|
||||
const extractUUID = (id: string): string => {
|
||||
// Если это уже чистый UUID, возвращаем как есть
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
if (uuidRegex.test(id)) {
|
||||
return id
|
||||
}
|
||||
|
||||
// Если есть префикс (например "template-"), извлекаем UUID
|
||||
const match = id.match(
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
|
||||
)
|
||||
if (match) {
|
||||
return match[0]
|
||||
}
|
||||
|
||||
return id // Возвращаем оригинал если не найден UUID
|
||||
}
|
||||
|
||||
export const useSheetAutoSave = (
|
||||
getAllModifiedCells: () => Record<string, Record<string, any>>,
|
||||
isCalculating: boolean,
|
||||
revision: number,
|
||||
options: UseSheetAutoSaveOptions = {}
|
||||
) => {
|
||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
||||
|
||||
// Состояние
|
||||
const [lastSavedRevision, setLastSavedRevision] = useState(0)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
|
||||
const [sheets, setSheets] = useState<Record<string, ApiSheet>>({})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Refs
|
||||
const saveTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
// Загрузка листов для шаблона при инициализации
|
||||
const loadSheets = useCallback(async () => {
|
||||
if (!templateId) return {}
|
||||
|
||||
const cleanTemplateId = extractUUID(templateId)
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
if (!uuidRegex.test(cleanTemplateId)) {
|
||||
console.error('❌ Не удалось извлечь валидный UUID для загрузки:', {
|
||||
original: templateId,
|
||||
extracted: cleanTemplateId,
|
||||
})
|
||||
setLastSaveError('Неверный ID шаблона')
|
||||
return {}
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await getSheetsByTemplate(cleanTemplateId)
|
||||
const sheetsMap = response.sheets.reduce(
|
||||
(acc, sheet) => {
|
||||
const normalizedName = sheet.name === 'Report' ? 'L' : sheet.name
|
||||
acc[normalizedName] = { ...sheet, name: normalizedName }
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ApiSheet>
|
||||
)
|
||||
|
||||
setSheets(sheetsMap)
|
||||
console.log('📂 Загружены листы для шаблона:', Object.keys(sheetsMap))
|
||||
return sheetsMap
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке листов:', error)
|
||||
setLastSaveError('Ошибка загрузки листов')
|
||||
return {}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [templateId])
|
||||
|
||||
// Загрузка листов при изменении templateId
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
loadSheets()
|
||||
}
|
||||
}, [templateId, loadSheets])
|
||||
|
||||
// Основная функция сохранения через API
|
||||
const performSave = useCallback(
|
||||
async (data: Record<string, Record<string, any>>) => {
|
||||
// Сохраняем все листы, но пропускаем пустые
|
||||
const sheetNames = Object.keys(data)
|
||||
if (sheetNames.length === 0) {
|
||||
console.log('📭 Нет данных для сохранения')
|
||||
return
|
||||
}
|
||||
|
||||
if (!templateId) {
|
||||
console.warn('⚠️ Не указан templateId для сохранения листов')
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем и валидируем UUID из templateId
|
||||
const cleanTemplateId = extractUUID(templateId)
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
if (!uuidRegex.test(cleanTemplateId)) {
|
||||
console.error('❌ Не удалось извлечь валидный UUID из templateId:', {
|
||||
original: templateId,
|
||||
extracted: cleanTemplateId,
|
||||
})
|
||||
setLastSaveError('Неверный ID шаблона')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🧹 Очищенный templateId:', {
|
||||
original: templateId,
|
||||
clean: cleanTemplateId,
|
||||
})
|
||||
|
||||
setIsSaving(true)
|
||||
setLastSaveError(null)
|
||||
|
||||
try {
|
||||
const savePromises = sheetNames.map(async sheetName => {
|
||||
const sheetCells = data[sheetName]
|
||||
const existingSheet = sheets[sheetName]
|
||||
|
||||
// Проверяем что есть данные для сохранения
|
||||
if (!sheetCells || Object.keys(sheetCells).length === 0) {
|
||||
console.log(`⏭️ Пропускаем лист "${sheetName}" - нет данных`)
|
||||
return
|
||||
}
|
||||
|
||||
if (existingSheet) {
|
||||
// Объединяем старые и новые данные, чтобы не потерять предыдущие изменения
|
||||
const mergedCells = {
|
||||
...existingSheet.cells,
|
||||
...sheetCells,
|
||||
}
|
||||
|
||||
// Проверяем, изменилось ли что-нибудь
|
||||
const isChanged = Object.keys(sheetCells).some(
|
||||
key => existingSheet.cells[key] !== sheetCells[key]
|
||||
)
|
||||
|
||||
if (!isChanged) {
|
||||
console.log(`⏭️ Лист "${sheetName}" без новых изменений`)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🔄 Обновляем лист "${sheetName}" (patch)`)
|
||||
await patchSheet(existingSheet.id, {
|
||||
cells: mergedCells,
|
||||
})
|
||||
|
||||
// Обновляем локальное состояние
|
||||
setSheets(prev => ({
|
||||
...prev,
|
||||
[sheetName]: {
|
||||
...existingSheet,
|
||||
cells: mergedCells,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
// Создаем новый лист
|
||||
console.log(`✨ Создаем новый лист "${sheetName}"`)
|
||||
console.log('📋 Данные для создания:', {
|
||||
name: sheetName,
|
||||
cells: sheetCells,
|
||||
template_id: cleanTemplateId,
|
||||
cellsCount: Object.keys(sheetCells).length,
|
||||
})
|
||||
const newSheet = await createSheet({
|
||||
name: sheetName,
|
||||
cells: sheetCells,
|
||||
template_id: cleanTemplateId,
|
||||
})
|
||||
|
||||
// Получаем полную информацию о созданном листе
|
||||
const fullSheetData: ApiSheet = {
|
||||
id: newSheet.id,
|
||||
name: sheetName,
|
||||
cells: sheetCells,
|
||||
template_id: cleanTemplateId,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
deleted_at: null,
|
||||
}
|
||||
|
||||
// Обновляем локальное состояние
|
||||
setSheets(prev => ({
|
||||
...prev,
|
||||
[sheetName]: fullSheetData,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(savePromises)
|
||||
setLastSavedRevision(revision)
|
||||
|
||||
const totalCells = Object.values(data).reduce(
|
||||
(sum, sheet) => sum + Object.keys(sheet).length,
|
||||
0
|
||||
)
|
||||
console.log('💾 Сохранено ячеек:', totalCells, 'в листах:', sheetNames)
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
setLastSaveError(errorMessage)
|
||||
console.error('❌ Ошибка при сохранении листов:', errorMessage)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
},
|
||||
[templateId, sheets, revision]
|
||||
)
|
||||
|
||||
// Автосохранение с дебаунсом
|
||||
const scheduleAutoSave = useCallback(() => {
|
||||
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
|
||||
enableAutoSave,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
})
|
||||
|
||||
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
|
||||
console.log('❌ Автосохранение отменено по условиям')
|
||||
return
|
||||
}
|
||||
|
||||
// Очищаем предыдущий таймер
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
console.log('🔄 Очищен предыдущий таймер автосохранения')
|
||||
}
|
||||
|
||||
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
console.log('🎯 Выполняем отложенное автосохранение')
|
||||
const currentData = getAllModifiedCells()
|
||||
console.log('📋 Данные для автосохранения:', currentData)
|
||||
performSave(currentData)
|
||||
}, autoSaveDelay)
|
||||
}, [
|
||||
enableAutoSave,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
autoSaveDelay,
|
||||
getAllModifiedCells,
|
||||
performSave,
|
||||
])
|
||||
|
||||
// Эффект для автосохранения при изменениях
|
||||
useEffect(() => {
|
||||
console.log('🔍 Проверка условий автосохранения:', {
|
||||
enableAutoSave,
|
||||
revision,
|
||||
lastSavedRevision,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
templateId,
|
||||
shouldSave:
|
||||
enableAutoSave &&
|
||||
revision > lastSavedRevision &&
|
||||
!isCalculating &&
|
||||
!isSaving,
|
||||
})
|
||||
|
||||
if (
|
||||
enableAutoSave &&
|
||||
revision > lastSavedRevision &&
|
||||
!isCalculating &&
|
||||
!isSaving
|
||||
) {
|
||||
console.log('✅ Планируем автосохранение...')
|
||||
scheduleAutoSave()
|
||||
} else {
|
||||
console.log('⏸️ Автосохранение пропущено по условиям')
|
||||
}
|
||||
}, [
|
||||
enableAutoSave,
|
||||
revision,
|
||||
lastSavedRevision,
|
||||
isCalculating,
|
||||
isSaving,
|
||||
scheduleAutoSave,
|
||||
])
|
||||
|
||||
// Ручное сохранение
|
||||
const manualSave = useCallback(() => {
|
||||
console.log('🖱️ Ручное сохранение вызвано')
|
||||
|
||||
// Отменяем автосохранение если оно запланировано
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
saveTimeoutRef.current = null
|
||||
console.log('⏹️ Отменено запланированное автосохранение')
|
||||
}
|
||||
|
||||
const currentData = getAllModifiedCells()
|
||||
console.log('📋 Данные для сохранения:', currentData)
|
||||
|
||||
if (Object.keys(currentData).length === 0) {
|
||||
console.log('📭 Нет измененных данных для сохранения')
|
||||
return
|
||||
}
|
||||
|
||||
performSave(currentData)
|
||||
}, [getAllModifiedCells, performSave])
|
||||
|
||||
// Функция загрузки сохраненных листов
|
||||
const loadSavedSheets = useCallback((): Record<
|
||||
string,
|
||||
Record<string, any>
|
||||
> => {
|
||||
const result: Record<string, Record<string, any>> = {}
|
||||
|
||||
Object.entries(sheets).forEach(([sheetName, sheet]) => {
|
||||
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
|
||||
result[sheetName] = sheet.cells
|
||||
}
|
||||
})
|
||||
|
||||
console.log('📂 Загружены сохраненные листы:', Object.keys(result))
|
||||
return result
|
||||
}, [sheets])
|
||||
|
||||
// Очистка таймаута при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Вычисляемые значения
|
||||
const isDataChanged = revision > lastSavedRevision
|
||||
const hasUnsavedChanges = isDataChanged && !isSaving
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
isSaving,
|
||||
isLoading,
|
||||
isDataChanged,
|
||||
hasUnsavedChanges,
|
||||
lastSaveError,
|
||||
lastSavedRevision,
|
||||
sheets,
|
||||
|
||||
// Методы
|
||||
manualSave,
|
||||
loadSavedSheets,
|
||||
loadSheets,
|
||||
|
||||
// Утилиты
|
||||
getCurrentData: getAllModifiedCells,
|
||||
clearError: () => setLastSaveError(null),
|
||||
}
|
||||
}
|
||||
335
src/hook/useSpreadsheetEngine.ts
Normal file
335
src/hook/useSpreadsheetEngine.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
CellUtils,
|
||||
CellValue,
|
||||
Workbook,
|
||||
} from '../lib/spreadsheet-engine/spreadsheet-engine'
|
||||
|
||||
interface CellData {
|
||||
value: string
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
interface UseSpreadsheetEngineProps {
|
||||
rows?: number
|
||||
cols?: number
|
||||
sheetName?: string
|
||||
initialData?: string[][]
|
||||
}
|
||||
|
||||
export function useSpreadsheetEngine({
|
||||
rows = 50,
|
||||
cols = 20,
|
||||
sheetName = 'Sheet1',
|
||||
initialData,
|
||||
}: UseSpreadsheetEngineProps = {}) {
|
||||
const workbookRef = useRef<Workbook | null>(null)
|
||||
const [cells, setCells] = useState<CellData[][]>(() => {
|
||||
return Array(rows)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(cols)
|
||||
.fill(null)
|
||||
.map(() => ({ value: '', isSelected: false }))
|
||||
)
|
||||
})
|
||||
const [isCalculating, setIsCalculating] = useState(false)
|
||||
|
||||
// Инициализация workbook
|
||||
useEffect(() => {
|
||||
if (!workbookRef.current) {
|
||||
workbookRef.current = new Workbook()
|
||||
workbookRef.current.addSheet(sheetName)
|
||||
// addCustomFunctions(workbookRef.current)
|
||||
}
|
||||
|
||||
// Загружаем начальные данные если они есть
|
||||
if (initialData) {
|
||||
loadDataFromArray(initialData)
|
||||
}
|
||||
}, [sheetName, initialData])
|
||||
|
||||
// Конвертация данных из движка в React состояние
|
||||
const syncFromEngine = useCallback(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return
|
||||
|
||||
const newCells = Array(rows)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(cols)
|
||||
.fill(null)
|
||||
.map(() => ({ value: '', isSelected: false }))
|
||||
)
|
||||
|
||||
// Получаем все ячейки из движка
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
// Показываем сырое значение (raw) в UI, а не вычисленное
|
||||
if (cell.raw !== null && cell.raw !== undefined) {
|
||||
newCells[row][col].value = String(cell.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCells(newCells)
|
||||
}, [rows, cols, sheetName])
|
||||
|
||||
// Установка значения ячейки
|
||||
const setCellValue = useCallback(
|
||||
(row: number, col: number, value: string) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
// Конвертируем пустую строку в null
|
||||
const cellValue: CellValue = value === '' ? null : value
|
||||
|
||||
// Если значение начинается с =, то это формула
|
||||
if (typeof cellValue === 'string' && cellValue.startsWith('=')) {
|
||||
workbookRef.current.setCell(qualifiedName, cellValue)
|
||||
} else {
|
||||
// Пытаемся конвертировать в число, если возможно
|
||||
const numValue = Number(cellValue)
|
||||
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
|
||||
workbookRef.current.setCell(qualifiedName, numValue)
|
||||
} else {
|
||||
workbookRef.current.setCell(qualifiedName, cellValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Пересчитываем все формулы
|
||||
workbookRef.current.recalc()
|
||||
|
||||
// Синхронизируем с React состоянием
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error setting cell value:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[sheetName, syncFromEngine]
|
||||
)
|
||||
|
||||
// Получение вычисленного значения ячейки
|
||||
const getCellValue = useCallback(
|
||||
(row: number, col: number): CellValue => {
|
||||
if (!workbookRef.current) return null
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
try {
|
||||
return workbookRef.current.getValue(qualifiedName)
|
||||
} catch (error) {
|
||||
console.error('Error getting cell value:', error)
|
||||
return '#ERROR!'
|
||||
}
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Получение сырого значения ячейки (для отображения в строке формул)
|
||||
const getCellRawValue = useCallback(
|
||||
(row: number, col: number): string => {
|
||||
if (!workbookRef.current) return ''
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
|
||||
if (!sheet) return ''
|
||||
|
||||
const cell = sheet.getCell(cellName)
|
||||
return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : ''
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Загрузка данных из массива
|
||||
const loadDataFromArray = useCallback(
|
||||
(data: string[][]) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
// Очищаем лист
|
||||
sheet.cells.clear()
|
||||
|
||||
// Загружаем данные
|
||||
for (let row = 0; row < data.length && row < rows; row++) {
|
||||
for (let col = 0; col < data[row].length && col < cols; col++) {
|
||||
const value = data[row][col]
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
sheet.setCell(cellName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Пересчитываем все формулы
|
||||
workbookRef.current.recalc()
|
||||
|
||||
// Синхронизируем с React состоянием
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[rows, cols, sheetName, syncFromEngine]
|
||||
)
|
||||
|
||||
// Экспорт данных в массив (для сохранения)
|
||||
const exportToArray = useCallback((): string[][] => {
|
||||
if (!workbookRef.current) return []
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return []
|
||||
|
||||
const result: string[][] = Array(rows)
|
||||
.fill(null)
|
||||
.map(() => Array(cols).fill(''))
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
if (cell.raw !== null && cell.raw !== undefined) {
|
||||
result[row][col] = String(cell.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [rows, cols, sheetName])
|
||||
|
||||
// Экспорт в JSON для бэкенда
|
||||
const exportToJSON = useCallback((): string => {
|
||||
if (!workbookRef.current) return '{}'
|
||||
return JSON.stringify(workbookRef.current.toJSON())
|
||||
}, [])
|
||||
|
||||
// Импорт из JSON с бэкенда
|
||||
const importFromJSON = useCallback(
|
||||
(jsonData: string) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonData)
|
||||
workbookRef.current.fromJSON(data)
|
||||
// addCustomFunctions(workbookRef.current)
|
||||
workbookRef.current.recalc()
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error importing from JSON:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[syncFromEngine]
|
||||
)
|
||||
|
||||
// Добавление пользовательской функции
|
||||
const addFunction = useCallback(
|
||||
(name: string, func: (...args: any[]) => any) => {
|
||||
if (!workbookRef.current) return
|
||||
workbookRef.current.addFunction(name, func)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Пересчёт всех формул
|
||||
const recalculate = useCallback(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
workbookRef.current.recalc()
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error recalculating:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
}, [syncFromEngine])
|
||||
|
||||
// Получение информации о зависимостях ячейки
|
||||
const getCellDependencies = useCallback(
|
||||
(row: number, col: number) => {
|
||||
if (!workbookRef.current) return { precedents: [], dependents: [] }
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
|
||||
if (!sheet) return { precedents: [], dependents: [] }
|
||||
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
return {
|
||||
precedents: Array.from(cell.precedents),
|
||||
dependents: Array.from(cell.dependents),
|
||||
}
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Получение статистики workbook
|
||||
const getWorkbookStats = useCallback(() => {
|
||||
if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 }
|
||||
|
||||
let totalCells = 0
|
||||
let totalFormulas = 0
|
||||
|
||||
for (const sheet of workbookRef.current.sheets.values()) {
|
||||
totalCells += sheet.cells.size
|
||||
|
||||
for (const cell of sheet.cells.values()) {
|
||||
if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) {
|
||||
totalFormulas++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sheets: workbookRef.current.sheets.size,
|
||||
cells: totalCells,
|
||||
formulas: totalFormulas,
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
cells,
|
||||
setCellValue,
|
||||
getCellValue,
|
||||
getCellRawValue,
|
||||
loadDataFromArray,
|
||||
exportToArray,
|
||||
exportToJSON,
|
||||
importFromJSON,
|
||||
addFunction,
|
||||
recalculate,
|
||||
getCellDependencies,
|
||||
getWorkbookStats,
|
||||
isCalculating,
|
||||
workbook: workbookRef.current,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user