From 553da77a8798f42b3c9753b2516894a0a26d9180 Mon Sep 17 00:00:00 2001 From: tlartem Date: Tue, 22 Jul 2025 10:33:21 +0300 Subject: [PATCH] =?UTF-8?q?elements=20constructor=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DualSpreadsheet/components/FormulaBar.tsx | 15 +- .../TemplateManager/ElementConstructor.tsx | 190 ++++++------ .../TemplateManager/ElementFormulaBar.tsx | 190 ++++++++++++ src/component/TemplateManager/SaveButton.tsx | 128 ++++++++ .../TemplateManager/VisualLayoutEditor.tsx | 18 +- src/hook/useDelayedSave.ts | 111 +++++++ src/page/ElementsCreation/Page.tsx | 289 +++++++++--------- 7 files changed, 688 insertions(+), 253 deletions(-) create mode 100644 src/component/TemplateManager/ElementFormulaBar.tsx create mode 100644 src/component/TemplateManager/SaveButton.tsx create mode 100644 src/hook/useDelayedSave.ts diff --git a/src/component/DualSpreadsheet/components/FormulaBar.tsx b/src/component/DualSpreadsheet/components/FormulaBar.tsx index c6e9f1c..4118977 100644 --- a/src/component/DualSpreadsheet/components/FormulaBar.tsx +++ b/src/component/DualSpreadsheet/components/FormulaBar.tsx @@ -1,3 +1,4 @@ +import { Save } from 'lucide-react' import { memo } from 'react' import { FormulaBarProps } from '../types' import { getColumnLabel } from '../utils' @@ -163,19 +164,7 @@ export const FormulaBar = memo( : 'Все изменения сохранены' } > - - - + {lastSaveError ? 'Повторить' : hasUnsavedChanges diff --git a/src/component/TemplateManager/ElementConstructor.tsx b/src/component/TemplateManager/ElementConstructor.tsx index f76f91b..c715198 100644 --- a/src/component/TemplateManager/ElementConstructor.tsx +++ b/src/component/TemplateManager/ElementConstructor.tsx @@ -34,7 +34,6 @@ import { DialogDescription, DialogHeader, DialogTitle, - DialogTrigger, } from '../ui/dialog' import { Input } from '../ui/input' import { @@ -45,6 +44,7 @@ import { SelectValue, } from '../ui/select' import { Textarea } from '../ui/textarea' +import { ElementFormulaBar } from './ElementFormulaBar' import { VisualLayoutEditor } from './VisualLayoutEditor' interface ElementFormProps { @@ -705,6 +705,14 @@ interface ElementConstructorProps { ) => void onElementDelete: (elementId: string) => void disabled?: boolean + // Пропсы для сохранения + hasUnsavedChanges?: boolean + isSaving?: boolean + lastSaveError?: string | null + onManualSave?: () => Promise + onClearError?: () => void + // Пропсы для настроек макета + onLayoutSettingsChange?: (settings: FormLayoutSettings) => void } export const ElementConstructor: React.FC = ({ @@ -713,6 +721,12 @@ export const ElementConstructor: React.FC = ({ onElementUpdate, onElementDelete, disabled = false, + hasUnsavedChanges = false, + isSaving = false, + lastSaveError = null, + onManualSave, + onClearError, + onLayoutSettingsChange, }) => { const elements = template.elements const layoutSettings = template.layoutSettings || { @@ -877,94 +891,96 @@ export const ElementConstructor: React.FC = ({ // }; return ( -
- {/* Main Content */} -
-
-
-

Холст

-

- Перетаскивайте элементы для настройки их расположения -

-
-
- - - - - - - Добавить новый элемент - - Настройте параметры нового элемента формы. - - - -
- - -
-
-
+
+ setIsAddDialogOpen(true)} + onAutoArrange={handleAutoArrange} + disabled={disabled} + hasUnsavedChanges={hasUnsavedChanges} + isSaving={isSaving} + lastSaveError={lastSaveError} + onManualSave={onManualSave} + onClearError={onClearError} + showGrid={layoutSettings.showGrid} + onToggleGrid={ + onLayoutSettingsChange + ? () => { + const newSettings = { + ...layoutSettings, + showGrid: !layoutSettings.showGrid, + } + onLayoutSettingsChange(newSettings) + } + : undefined + } + /> - {editingElement && ( - handleCancelEdit()} - > - - - Редактировать элемент - - Измените параметры элемента формы. - - - -
- - -
-
-
- )} -
-
-
- {}} // Настройки макета только для чтения - onElementEdit={handleEditElement} - /> -
+
+ {})} + onElementEdit={handleEditElement} + />
+ + {/* Диалог добавления элемента */} + + + + Добавить новый элемент + + Настройте параметры нового элемента формы. + + + +
+ + +
+
+
+ + {/* Диалог редактирования элемента */} + {editingElement && ( + handleCancelEdit()}> + + + Редактировать элемент + + Измените параметры элемента формы. + + + +
+ + +
+
+
+ )}
) } diff --git a/src/component/TemplateManager/ElementFormulaBar.tsx b/src/component/TemplateManager/ElementFormulaBar.tsx new file mode 100644 index 0000000..d5c2b4d --- /dev/null +++ b/src/component/TemplateManager/ElementFormulaBar.tsx @@ -0,0 +1,190 @@ +import { Eye, EyeOff, Plus, Save, Wrench } from 'lucide-react' +import { memo } from 'react' +import { Button } from '../ui/button' + +interface ElementFormulaBarProps { + elementCount: number + onAddElement: () => void + onAutoArrange?: () => void + disabled?: boolean + // Пропсы для кнопки сохранения + hasUnsavedChanges?: boolean + isSaving?: boolean + lastSaveError?: string | null + onManualSave?: () => Promise + onClearError?: () => void + // Пропсы для управления сеткой + showGrid?: boolean + onToggleGrid?: () => void +} + +export const ElementFormulaBar = memo( + ({ + elementCount, + onAddElement, + onAutoArrange, + disabled = false, + hasUnsavedChanges = false, + isSaving = false, + lastSaveError = null, + onManualSave, + onClearError, + showGrid = true, + onToggleGrid, + }: ElementFormulaBarProps) => { + return ( +
+
+
+
+ + {elementCount} элементов + +
+
+ + {onAutoArrange && ( + + )} + {onToggleGrid && ( + + )} +
+
+ +
+ {/* Ошибка сохранения */} + {lastSaveError && ( +
+ + + + + Ошибка сохранения + + {onClearError && ( + + )} +
+ )} + + {/* Индикатор сохранения */} + {isSaving && ( +
+ + + + + Сохранение... +
+ )} + + {/* Кнопка сохранения */} + {onManualSave && ( + + )} +
+
+
+ ) + } +) diff --git a/src/component/TemplateManager/SaveButton.tsx b/src/component/TemplateManager/SaveButton.tsx new file mode 100644 index 0000000..50b6d25 --- /dev/null +++ b/src/component/TemplateManager/SaveButton.tsx @@ -0,0 +1,128 @@ +import { memo } from 'react' + +interface SaveButtonProps { + hasUnsavedChanges: boolean + isSaving: boolean + lastSaveError: string | null + onManualSave: () => Promise + onClearError: () => void +} + +export const SaveButton = memo( + ({ + hasUnsavedChanges, + isSaving, + lastSaveError, + onManualSave, + onClearError, + }: SaveButtonProps) => { + return ( +
+ {/* Ошибка сохранения */} + {lastSaveError && ( +
+ + + + + Ошибка сохранения + + +
+ )} + + {/* Индикатор сохранения */} + {isSaving && ( +
+ + + + + Сохранение... +
+ )} + + {/* Кнопка сохранения */} + +
+ ) + } +) diff --git a/src/component/TemplateManager/VisualLayoutEditor.tsx b/src/component/TemplateManager/VisualLayoutEditor.tsx index bb82103..34c1719 100644 --- a/src/component/TemplateManager/VisualLayoutEditor.tsx +++ b/src/component/TemplateManager/VisualLayoutEditor.tsx @@ -4,8 +4,6 @@ import { Droplets, Edit, Edit3, - Eye, - EyeOff, Gauge, Grid, Move, @@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC = ({ }, [layoutSettings, onLayoutSettingsChange]) return ( -
-
- - {elements.length} элементов на холсте - - -
- +
{ + onSave: (data: T) => Promise + onSuccess?: (data: T) => void // Коллбек после успешного сохранения + delay?: number // Задержка в миллисекундах (по умолчанию 2000мс) +} + +interface UseDelayedSaveReturn { + hasUnsavedChanges: boolean + isSaving: boolean + lastSaveError: string | null + updateData: (data: T) => void + manualSave: () => Promise + clearError: () => void +} + +export function useDelayedSave({ + onSave, + onSuccess, + delay = 2000, +}: UseDelayedSaveOptions): UseDelayedSaveReturn { + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [lastSaveError, setLastSaveError] = useState(null) + + const timeoutRef = useRef(null) + const pendingDataRef = useRef(null) + + // Очистка таймера при размонтировании компонента + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + const clearError = useCallback(() => { + setLastSaveError(null) + }, []) + + const performSave = useCallback( + async (data: T) => { + if (isSaving) return + + setIsSaving(true) + setLastSaveError(null) + + try { + await onSave(data) + setHasUnsavedChanges(false) + pendingDataRef.current = null + // Вызываем коллбек успешного сохранения + onSuccess?.(data) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Неизвестная ошибка' + setLastSaveError(errorMessage) + console.error('Ошибка при сохранении:', error) + } finally { + setIsSaving(false) + } + }, + [onSave, onSuccess, isSaving] + ) + + const updateData = useCallback( + (data: T) => { + // Отменяем предыдущий таймер + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + // Сохраняем данные для отложенного сохранения + pendingDataRef.current = data + setHasUnsavedChanges(true) + setLastSaveError(null) + + // Устанавливаем новый таймер + timeoutRef.current = setTimeout(() => { + if (pendingDataRef.current) { + performSave(pendingDataRef.current) + } + }, delay) + }, + [delay, performSave] + ) + + const manualSave = useCallback(async () => { + // Отменяем автоматическое сохранение + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + + // Сохраняем немедленно, если есть несохранённые данные + if (pendingDataRef.current) { + await performSave(pendingDataRef.current) + } + }, [performSave]) + + return { + hasUnsavedChanges, + isSaving, + lastSaveError, + updateData, + manualSave, + clearError, + } +} diff --git a/src/page/ElementsCreation/Page.tsx b/src/page/ElementsCreation/Page.tsx index a9f438c..e49c30b 100644 --- a/src/page/ElementsCreation/Page.tsx +++ b/src/page/ElementsCreation/Page.tsx @@ -1,9 +1,10 @@ import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor' import { Button } from '@/component/ui/button' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext' +import { useDelayedSave } from '@/hook/useDelayedSave' import { Template, TemplateElement } from '@/type/template' import { ArrowLeft, FileText, Wrench } from 'lucide-react' -import { FC, useEffect, useState } from 'react' +import { FC, useCallback, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' export const ElementsCreation: FC = () => { @@ -13,48 +14,56 @@ export const ElementsCreation: FC = () => { const [selectedTemplate, setSelectedTemplate] = useState