AI рефакторинг !
This commit is contained in:
126
src/hook/adapters/apiSaveAdapter.ts
Normal file
126
src/hook/adapters/apiSaveAdapter.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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 = {}
|
||||
},
|
||||
}
|
||||
}
|
||||
86
src/hook/adapters/localStorageSaveAdapter.ts
Normal file
86
src/hook/adapters/localStorageSaveAdapter.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
197
src/hook/useAutoSave.ts
Normal file
197
src/hook/useAutoSave.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
59
src/hook/useFormulaAutoSaveNew.ts
Normal file
59
src/hook/useFormulaAutoSaveNew.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
60
src/hook/useSheetAutoSaveNew.ts
Normal file
60
src/hook/useSheetAutoSaveNew.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user