Files
protoc-frontend/src/hook/useSpreadsheetEngine.ts

336 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}