Очистка неиспользуемого и оптимизация
This commit is contained in:
@@ -2,7 +2,14 @@ import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
|||||||
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
||||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, {
|
||||||
|
FC,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
// Импорты из новых модулей
|
// Импорты из новых модулей
|
||||||
import { FormulaBar, Spreadsheet } from './components'
|
import { FormulaBar, Spreadsheet } from './components'
|
||||||
@@ -720,6 +727,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
[templateId]
|
[templateId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
|
||||||
|
const memoizedGetAllModifiedCells = useCallback(
|
||||||
|
multiSheetEngine.getAllModifiedCells,
|
||||||
|
[multiSheetEngine.getAllModifiedCells]
|
||||||
|
)
|
||||||
|
|
||||||
// Используем новый хук автосохранения
|
// Используем новый хук автосохранения
|
||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -731,25 +744,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
loadSavedSheets,
|
loadSavedSheets,
|
||||||
loadSheets, // Добавляем асинхронную функцию загрузки
|
loadSheets, // Добавляем асинхронную функцию загрузки
|
||||||
} = useSheetAutoSave(
|
} = useSheetAutoSave(
|
||||||
multiSheetEngine.getAllModifiedCells,
|
memoizedGetAllModifiedCells,
|
||||||
multiSheetEngine.isCalculating,
|
multiSheetEngine.isCalculating,
|
||||||
revision,
|
revision,
|
||||||
autoSaveOptions
|
autoSaveOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
// Отладка templateId
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log('🆔 Текущий templateId в DualSpreadsheet:', {
|
|
||||||
//templateId,
|
|
||||||
//type: typeof templateId,
|
|
||||||
//isValid: templateId
|
|
||||||
// ? /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
||||||
// templateId
|
|
||||||
// )
|
|
||||||
// : false,
|
|
||||||
//})
|
|
||||||
}, [templateId])
|
|
||||||
|
|
||||||
// Используем хук для перетаскивания разделителя
|
// Используем хук для перетаскивания разделителя
|
||||||
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
|
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
|
||||||
// Используем requestAnimationFrame для более плавного обновления
|
// Используем requestAnimationFrame для более плавного обновления
|
||||||
@@ -932,8 +932,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
style={{ transition: 'none' }}
|
style={{ transition: 'none' }}
|
||||||
>
|
>
|
||||||
{SHEET_TYPES.map((sheetType, index) => (
|
{SHEET_TYPES.map((sheetType, index) => (
|
||||||
|
<React.Fragment key={sheetType}>
|
||||||
<Spreadsheet
|
<Spreadsheet
|
||||||
key={sheetType}
|
|
||||||
sheetType={sheetType}
|
sheetType={sheetType}
|
||||||
columnWidths={columnWidths[sheetType]}
|
columnWidths={columnWidths[sheetType]}
|
||||||
spreadsheetWidths={spreadsheetWidths}
|
spreadsheetWidths={spreadsheetWidths}
|
||||||
@@ -960,7 +960,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
handleColumnResize={handleColumnResize}
|
handleColumnResize={handleColumnResize}
|
||||||
mergedCells={mergedCells}
|
mergedCells={mergedCells}
|
||||||
/>
|
/>
|
||||||
))}
|
{index === 0 && (
|
||||||
<div
|
<div
|
||||||
ref={separatorRef}
|
ref={separatorRef}
|
||||||
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
|
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
|
||||||
@@ -968,6 +968,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
|
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import {
|
|
||||||
ApiSheet,
|
|
||||||
createSheet,
|
|
||||||
getSheetsByTemplate,
|
|
||||||
patchSheet,
|
|
||||||
} from '../../service/sheetApiService'
|
|
||||||
import { SaveAdapter } from '../useAutoSave'
|
|
||||||
|
|
||||||
// Утилита для извлечения UUID из templateId
|
|
||||||
const extractUUID = (id: string): string => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Адаптер для сохранения через API
|
|
||||||
*/
|
|
||||||
export function createApiSaveAdapter(templateId: string): SaveAdapter {
|
|
||||||
let sheetsCache: Record<string, ApiSheet> = {}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
throw new Error('Invalid template ID format')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
async save(data: Record<string, Record<string, any>>) {
|
|
||||||
const sheetNames = Object.keys(data)
|
|
||||||
if (sheetNames.length === 0) return
|
|
||||||
|
|
||||||
const savePromises = sheetNames.map(async sheetName => {
|
|
||||||
const sheetCells = data[sheetName]
|
|
||||||
const existingSheet = sheetsCache[sheetName]
|
|
||||||
|
|
||||||
if (!sheetCells || Object.keys(sheetCells).length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSheet) {
|
|
||||||
// Обновляем существующий лист
|
|
||||||
const mergedCells = {
|
|
||||||
...existingSheet.cells,
|
|
||||||
...sheetCells,
|
|
||||||
}
|
|
||||||
|
|
||||||
const isChanged = Object.keys(sheetCells).some(
|
|
||||||
key => existingSheet.cells[key] !== sheetCells[key]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isChanged) return
|
|
||||||
|
|
||||||
await patchSheet(existingSheet.id, {
|
|
||||||
cells: mergedCells,
|
|
||||||
})
|
|
||||||
|
|
||||||
sheetsCache[sheetName] = {
|
|
||||||
...existingSheet,
|
|
||||||
cells: mergedCells,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Создаем новый лист
|
|
||||||
const newSheet = await createSheet({
|
|
||||||
name: sheetName,
|
|
||||||
cells: sheetCells,
|
|
||||||
template_id: cleanTemplateId,
|
|
||||||
})
|
|
||||||
|
|
||||||
sheetsCache[sheetName] = {
|
|
||||||
id: newSheet.id,
|
|
||||||
name: sheetName,
|
|
||||||
cells: sheetCells,
|
|
||||||
template_id: cleanTemplateId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
deleted_at: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(savePromises)
|
|
||||||
},
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
try {
|
|
||||||
const sheets = await getSheetsByTemplate(cleanTemplateId)
|
|
||||||
const sheetsMap: Record<string, ApiSheet> = {}
|
|
||||||
const result: Record<string, Record<string, any>> = {}
|
|
||||||
|
|
||||||
sheets.forEach(sheet => {
|
|
||||||
sheetsMap[sheet.name] = sheet
|
|
||||||
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
|
|
||||||
result[sheet.name] = sheet.cells
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sheetsCache = sheetsMap
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке листов:', error)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
// Для API очистка может означать удаление всех листов
|
|
||||||
// Пока просто очищаем кеш
|
|
||||||
sheetsCache = {}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { SaveAdapter } from '../useAutoSave'
|
|
||||||
|
|
||||||
interface SavedLocalStorageData {
|
|
||||||
templateId: string
|
|
||||||
formulaData: Record<string, Record<string, any>>
|
|
||||||
timestamp: string
|
|
||||||
version: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ключ для localStorage
|
|
||||||
const getStorageKey = (templateId: string) => {
|
|
||||||
return `formula_data_${templateId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Адаптер для сохранения в localStorage
|
|
||||||
*/
|
|
||||||
export function createLocalStorageSaveAdapter(templateId: string): SaveAdapter {
|
|
||||||
let versionCounter = 1
|
|
||||||
|
|
||||||
// Инициализируем версию из существующих данных
|
|
||||||
try {
|
|
||||||
const storageKey = getStorageKey(templateId)
|
|
||||||
const savedDataStr = localStorage.getItem(storageKey)
|
|
||||||
if (savedDataStr) {
|
|
||||||
const parsedData: SavedLocalStorageData = JSON.parse(savedDataStr)
|
|
||||||
versionCounter = parsedData.version + 1
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Ошибка при загрузке данных из localStorage:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
async save(data: Record<string, Record<string, any>>) {
|
|
||||||
// Имитируем задержку API для реалистичности
|
|
||||||
await new Promise(resolve =>
|
|
||||||
setTimeout(resolve, 200 + Math.random() * 300)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Иногда имитируем ошибку (2% вероятность)
|
|
||||||
if (Math.random() < 0.02) {
|
|
||||||
throw new Error('Ошибка записи в хранилище')
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedData: SavedLocalStorageData = {
|
|
||||||
templateId,
|
|
||||||
formulaData: data,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
version: versionCounter++,
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageKey = getStorageKey(templateId)
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(savedData))
|
|
||||||
},
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
try {
|
|
||||||
const storageKey = getStorageKey(templateId)
|
|
||||||
const savedDataStr = localStorage.getItem(storageKey)
|
|
||||||
|
|
||||||
if (savedDataStr) {
|
|
||||||
const parsedData: SavedLocalStorageData = JSON.parse(savedDataStr)
|
|
||||||
return parsedData.formulaData
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'❌ Ошибка при загрузке сохраненных данных из localStorage:',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
try {
|
|
||||||
const storageKey = getStorageKey(templateId)
|
|
||||||
localStorage.removeItem(storageKey)
|
|
||||||
versionCounter = 1
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Ошибка при очистке сохраненных данных:', error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Адаптер для различных способов сохранения
|
|
||||||
*/
|
|
||||||
export interface SaveAdapter {
|
|
||||||
save: (data: Record<string, Record<string, any>>) => Promise<void>
|
|
||||||
load?: () => Promise<Record<string, Record<string, any>>>
|
|
||||||
clear?: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseAutoSaveOptions {
|
|
||||||
templateId?: string
|
|
||||||
autoSaveDelay?: number
|
|
||||||
enableAutoSave?: boolean
|
|
||||||
adapter: SaveAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Универсальный хук автосохранения
|
|
||||||
* Поддерживает различные адаптеры сохранения (API, localStorage, etc.)
|
|
||||||
*/
|
|
||||||
export const useAutoSave = (
|
|
||||||
getAllModifiedCells: () => Record<string, Record<string, any>>,
|
|
||||||
isCalculating: boolean,
|
|
||||||
revision: number,
|
|
||||||
options: UseAutoSaveOptions
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
templateId,
|
|
||||||
autoSaveDelay = 2000,
|
|
||||||
enableAutoSave = true,
|
|
||||||
adapter,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
// Состояние
|
|
||||||
const [lastSavedRevision, setLastSavedRevision] = useState(0)
|
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
|
||||||
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
// Refs
|
|
||||||
const saveTimeoutRef = useRef<number | null>(null)
|
|
||||||
|
|
||||||
// Основная функция сохранения
|
|
||||||
const performSave = useCallback(
|
|
||||||
async (data: Record<string, Record<string, any>>) => {
|
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!templateId) {
|
|
||||||
console.warn('⚠️ Не указан templateId для сохранения')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(true)
|
|
||||||
setLastSaveError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await adapter.save(data)
|
|
||||||
setLastSavedRevision(revision)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
|
||||||
setLastSaveError(errorMessage)
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[templateId, adapter, revision]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Автосохранение с дебаунсом
|
|
||||||
const scheduleAutoSave = useCallback(() => {
|
|
||||||
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем предыдущий таймер
|
|
||||||
if (saveTimeoutRef.current) {
|
|
||||||
clearTimeout(saveTimeoutRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTimeoutRef.current = setTimeout(() => {
|
|
||||||
const currentData = getAllModifiedCells()
|
|
||||||
performSave(currentData)
|
|
||||||
}, autoSaveDelay)
|
|
||||||
}, [
|
|
||||||
enableAutoSave,
|
|
||||||
isCalculating,
|
|
||||||
isSaving,
|
|
||||||
templateId,
|
|
||||||
autoSaveDelay,
|
|
||||||
getAllModifiedCells,
|
|
||||||
performSave,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Эффект для автосохранения при изменениях
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
enableAutoSave &&
|
|
||||||
revision > lastSavedRevision &&
|
|
||||||
!isCalculating &&
|
|
||||||
!isSaving
|
|
||||||
) {
|
|
||||||
scheduleAutoSave()
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
enableAutoSave,
|
|
||||||
revision,
|
|
||||||
lastSavedRevision,
|
|
||||||
isCalculating,
|
|
||||||
isSaving,
|
|
||||||
scheduleAutoSave,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Ручное сохранение
|
|
||||||
const manualSave = useCallback(() => {
|
|
||||||
// Отменяем автосохранение если оно запланировано
|
|
||||||
if (saveTimeoutRef.current) {
|
|
||||||
clearTimeout(saveTimeoutRef.current)
|
|
||||||
saveTimeoutRef.current = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentData = getAllModifiedCells()
|
|
||||||
|
|
||||||
if (Object.keys(currentData).length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
performSave(currentData)
|
|
||||||
}, [getAllModifiedCells, performSave])
|
|
||||||
|
|
||||||
// Загрузка данных
|
|
||||||
const loadData = useCallback(async () => {
|
|
||||||
if (!adapter.load) return {}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
return await adapter.load()
|
|
||||||
} catch (error) {
|
|
||||||
setLastSaveError(
|
|
||||||
error instanceof Error ? error.message : 'Ошибка загрузки'
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [adapter])
|
|
||||||
|
|
||||||
// Очистка данных
|
|
||||||
const clearData = useCallback(async () => {
|
|
||||||
if (!adapter.clear) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await adapter.clear()
|
|
||||||
setLastSavedRevision(0)
|
|
||||||
} catch (error) {
|
|
||||||
setLastSaveError(
|
|
||||||
error instanceof Error ? error.message : 'Ошибка очистки'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [adapter])
|
|
||||||
|
|
||||||
// Очистка таймаута при размонтировании
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (saveTimeoutRef.current) {
|
|
||||||
clearTimeout(saveTimeoutRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Вычисляемые значения
|
|
||||||
const isDataChanged = revision > lastSavedRevision
|
|
||||||
const hasUnsavedChanges = isDataChanged && !isSaving
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Состояние
|
|
||||||
isSaving,
|
|
||||||
isLoading,
|
|
||||||
isDataChanged,
|
|
||||||
hasUnsavedChanges,
|
|
||||||
lastSaveError,
|
|
||||||
lastSavedRevision,
|
|
||||||
|
|
||||||
// Методы
|
|
||||||
manualSave,
|
|
||||||
loadData,
|
|
||||||
clearData,
|
|
||||||
|
|
||||||
// Утилиты
|
|
||||||
getCurrentData: getAllModifiedCells,
|
|
||||||
clearError: () => setLastSaveError(null),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { createLocalStorageSaveAdapter } from './adapters/localStorageSaveAdapter'
|
|
||||||
import { useAutoSave } from './useAutoSave'
|
|
||||||
|
|
||||||
export interface UseFormulaAutoSaveOptions {
|
|
||||||
templateId?: string
|
|
||||||
autoSaveDelay?: number
|
|
||||||
enableAutoSave?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Хук для автосохранения формул в localStorage
|
|
||||||
* Теперь использует универсальный хук с localStorage адаптером
|
|
||||||
*/
|
|
||||||
export const useFormulaAutoSave = (
|
|
||||||
getAllModifiedCells: () => Record<string, Record<string, any>>,
|
|
||||||
isCalculating: boolean,
|
|
||||||
revision: number,
|
|
||||||
options: UseFormulaAutoSaveOptions = {}
|
|
||||||
) => {
|
|
||||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
|
||||||
|
|
||||||
if (!templateId) {
|
|
||||||
// Возвращаем пустую реализацию если нет templateId
|
|
||||||
return {
|
|
||||||
isSaving: false,
|
|
||||||
isDataChanged: false,
|
|
||||||
hasUnsavedChanges: false,
|
|
||||||
lastSaveError: null,
|
|
||||||
lastSavedRevision: 0,
|
|
||||||
savedData: null,
|
|
||||||
manualSave: () => {},
|
|
||||||
loadSavedFormulas: () => null,
|
|
||||||
clearSavedFormulas: () => {},
|
|
||||||
getCurrentData: getAllModifiedCells,
|
|
||||||
clearError: () => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const adapter = createLocalStorageSaveAdapter(templateId)
|
|
||||||
|
|
||||||
const autoSaveResult = useAutoSave(
|
|
||||||
getAllModifiedCells,
|
|
||||||
isCalculating,
|
|
||||||
revision,
|
|
||||||
{
|
|
||||||
templateId,
|
|
||||||
autoSaveDelay,
|
|
||||||
enableAutoSave,
|
|
||||||
adapter,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...autoSaveResult,
|
|
||||||
savedData: null, // Для совместимости
|
|
||||||
loadSavedFormulas: async () => autoSaveResult.loadData(),
|
|
||||||
clearSavedFormulas: async () => autoSaveResult.clearData(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,10 @@ export const useSheetAutoSave = (
|
|||||||
options: UseSheetAutoSaveOptions = {}
|
options: UseSheetAutoSaveOptions = {}
|
||||||
) => {
|
) => {
|
||||||
console.log('getAllModifiedCells', getAllModifiedCells())
|
console.log('getAllModifiedCells', getAllModifiedCells())
|
||||||
|
const memoizedGetAllModifiedCells = useCallback(getAllModifiedCells, [
|
||||||
|
getAllModifiedCells,
|
||||||
|
])
|
||||||
|
|
||||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
||||||
|
|
||||||
// Состояние
|
// Состояние
|
||||||
@@ -171,7 +175,7 @@ export const useSheetAutoSave = (
|
|||||||
|
|
||||||
if (shouldAutoSave) {
|
if (shouldAutoSave) {
|
||||||
console.log('✅ Выполняем автосохранение для ревизии:', debouncedRevision)
|
console.log('✅ Выполняем автосохранение для ревизии:', debouncedRevision)
|
||||||
performSave(getAllModifiedCells())
|
performSave(memoizedGetAllModifiedCells())
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
enableAutoSave,
|
enableAutoSave,
|
||||||
@@ -180,15 +184,15 @@ export const useSheetAutoSave = (
|
|||||||
isCalculating,
|
isCalculating,
|
||||||
isSaving,
|
isSaving,
|
||||||
templateId,
|
templateId,
|
||||||
getAllModifiedCells,
|
memoizedGetAllModifiedCells,
|
||||||
performSave,
|
performSave,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Ручное сохранение
|
// Ручное сохранение
|
||||||
const manualSave = useCallback(() => {
|
const manualSave = useCallback(() => {
|
||||||
const currentData = getAllModifiedCells()
|
const currentData = memoizedGetAllModifiedCells()
|
||||||
performSave(currentData)
|
performSave(currentData)
|
||||||
}, [getAllModifiedCells, performSave])
|
}, [memoizedGetAllModifiedCells, performSave])
|
||||||
|
|
||||||
// Функция загрузки сохраненных листов
|
// Функция загрузки сохраненных листов
|
||||||
const loadSavedSheets = useCallback((): Record<
|
const loadSavedSheets = useCallback((): Record<
|
||||||
@@ -226,7 +230,7 @@ export const useSheetAutoSave = (
|
|||||||
loadSheets,
|
loadSheets,
|
||||||
|
|
||||||
// Утилиты
|
// Утилиты
|
||||||
getCurrentData: getAllModifiedCells,
|
getCurrentData: memoizedGetAllModifiedCells,
|
||||||
clearError: () => setLastSaveError(null),
|
clearError: () => setLastSaveError(null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { createApiSaveAdapter } from './adapters/apiSaveAdapter'
|
|
||||||
import { useAutoSave } from './useAutoSave'
|
|
||||||
|
|
||||||
export interface UseSheetAutoSaveOptions {
|
|
||||||
templateId?: string
|
|
||||||
autoSaveDelay?: number
|
|
||||||
enableAutoSave?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Хук для автосохранения листов через API
|
|
||||||
* Теперь использует универсальный хук с API адаптером
|
|
||||||
*/
|
|
||||||
export const useSheetAutoSave = (
|
|
||||||
getAllModifiedCells: () => Record<string, Record<string, any>>,
|
|
||||||
isCalculating: boolean,
|
|
||||||
revision: number,
|
|
||||||
options: UseSheetAutoSaveOptions = {}
|
|
||||||
) => {
|
|
||||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
|
||||||
|
|
||||||
if (!templateId) {
|
|
||||||
// Возвращаем пустую реализацию если нет templateId
|
|
||||||
return {
|
|
||||||
isSaving: false,
|
|
||||||
isLoading: false,
|
|
||||||
isDataChanged: false,
|
|
||||||
hasUnsavedChanges: false,
|
|
||||||
lastSaveError: null,
|
|
||||||
lastSavedRevision: 0,
|
|
||||||
sheets: {},
|
|
||||||
manualSave: () => {},
|
|
||||||
loadSavedSheets: () => ({}),
|
|
||||||
loadSheets: async () => ({}),
|
|
||||||
getCurrentData: getAllModifiedCells,
|
|
||||||
clearError: () => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const adapter = createApiSaveAdapter(templateId)
|
|
||||||
|
|
||||||
const autoSaveResult = useAutoSave(
|
|
||||||
getAllModifiedCells,
|
|
||||||
isCalculating,
|
|
||||||
revision,
|
|
||||||
{
|
|
||||||
templateId,
|
|
||||||
autoSaveDelay,
|
|
||||||
enableAutoSave,
|
|
||||||
adapter,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...autoSaveResult,
|
|
||||||
sheets: {}, // Для совместимости
|
|
||||||
loadSavedSheets: async () => autoSaveResult.loadData(),
|
|
||||||
loadSheets: async () => autoSaveResult.loadData(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -441,16 +441,6 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
}
|
}
|
||||||
onSave(data)
|
onSave(data)
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// '[DBG] template elements',
|
|
||||||
// template.elements.map(e => ({
|
|
||||||
// id: e.id,
|
|
||||||
// label: e.label,
|
|
||||||
// target: e.targetCells,
|
|
||||||
// type: e.type,
|
|
||||||
// }))
|
|
||||||
// )
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!engineRef.current) {
|
if (!engineRef.current) {
|
||||||
alert('Движок ещё не инициализирован')
|
alert('Движок ещё не инициализирован')
|
||||||
|
|||||||
Reference in New Issue
Block a user