336 lines
9.5 KiB
TypeScript
336 lines
9.5 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
||
import {
|
||
CellUtils,
|
||
CellValue,
|
||
Workbook,
|
||
} from '../feature/excelEngine/excelEngine'
|
||
|
||
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,
|
||
}
|
||
}
|