elements constructor улучшен

This commit is contained in:
2025-07-22 10:33:21 +03:00
parent ac8be1a347
commit 553da77a87
7 changed files with 688 additions and 253 deletions

View File

@@ -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(
: 'Все изменения сохранены'
}
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<Save className="h-3 w-3" />
{lastSaveError
? 'Повторить'
: hasUnsavedChanges

View File

@@ -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<void>
onClearError?: () => void
// Пропсы для настроек макета
onLayoutSettingsChange?: (settings: FormLayoutSettings) => void
}
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
@@ -713,6 +721,12 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
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<ElementConstructorProps> = ({
// };
return (
<div className="flex h-full w-full">
{/* Main Content */}
<div className="flex flex-1 flex-col bg-gray-50">
<div className="flex items-center justify-between border-b bg-white p-2">
<div className="px-2">
<h3 className="text-sm font-medium">Холст</h3>
<p className="text-xs text-gray-500">
Перетаскивайте элементы для настройки их расположения
</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</DialogTrigger>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogDescription>
Настройте параметры нового элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button
onClick={() => setIsAddDialogOpen(false)}
variant="outline"
>
Отмена
</Button>
<Button
onClick={handleAddElement}
disabled={
disabled ||
!formData.label ||
(formData.type !== 'standards' &&
formData.type !== 'calibration-conditions' &&
formData.type !== 'button-group' &&
!formData.targetCells?.length)
}
>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</div>
</DialogContent>
</Dialog>
<div className="flex h-full w-full flex-col bg-background">
<ElementFormulaBar
elementCount={elements.length}
onAddElement={() => 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 && (
<Dialog
open={!!editingElement}
onOpenChange={() => handleCancelEdit()}
>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
<DialogDescription>
Измените параметры элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={handleCancelEdit} variant="outline">
Отмена
</Button>
<Button onClick={handleUpdateElement} disabled={disabled}>
Сохранить изменения
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
<div className="flex-1 overflow-hidden">
<VisualLayoutEditor
elements={elements}
layoutSettings={layoutSettings}
onElementUpdate={onElementUpdate}
onElementDelete={handleDeleteAndSelect}
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
onElementEdit={handleEditElement}
/>
</div>
<div className="flex-1 overflow-hidden">
<VisualLayoutEditor
elements={elements}
layoutSettings={layoutSettings}
onElementUpdate={onElementUpdate}
onElementDelete={handleDeleteAndSelect}
onLayoutSettingsChange={onLayoutSettingsChange || (() => {})}
onElementEdit={handleEditElement}
/>
</div>
{/* Диалог добавления элемента */}
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogDescription>
Настройте параметры нового элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={() => setIsAddDialogOpen(false)} variant="outline">
Отмена
</Button>
<Button
onClick={handleAddElement}
disabled={
disabled ||
!formData.label ||
(formData.type !== 'standards' &&
formData.type !== 'calibration-conditions' &&
formData.type !== 'button-group' &&
!formData.targetCells?.length)
}
>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</div>
</DialogContent>
</Dialog>
{/* Диалог редактирования элемента */}
{editingElement && (
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
<DialogDescription>
Измените параметры элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={handleCancelEdit} variant="outline">
Отмена
</Button>
<Button onClick={handleUpdateElement} disabled={disabled}>
Сохранить изменения
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
)
}

View File

@@ -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<void>
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 (
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="flex min-w-[100px] items-center justify-center rounded-md border bg-muted px-2 py-1">
<span className="text-xs font-medium text-muted-foreground">
{elementCount} элементов
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="outline"
size="sm"
onClick={onAddElement}
disabled={disabled}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
{onAutoArrange && (
<Button
variant="outline"
size="sm"
onClick={onAutoArrange}
disabled={disabled || elementCount === 0}
className="h-7 px-2 text-xs"
>
<Wrench className="mr-1 h-3 w-3" />
Упорядочить
</Button>
)}
{onToggleGrid && (
<Button
variant="outline"
size="sm"
onClick={onToggleGrid}
className="h-7 px-2 text-xs"
>
{showGrid ? (
<EyeOff className="mr-1 h-3 w-3" />
) : (
<Eye className="mr-1 h-3 w-3" />
)}
{showGrid ? 'Скрыть сетку' : 'Показать'}
</Button>
)}
</div>
</div>
<div className="flex items-center gap-2">
{/* Ошибка сохранения */}
{lastSaveError && (
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
<svg
className="h-3 w-3 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<span
className="text-xs text-destructive"
title={lastSaveError}
>
Ошибка сохранения
</span>
{onClearError && (
<button
onClick={onClearError}
className="ml-1 text-destructive/60 hover:text-destructive"
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
)}
{/* Индикатор сохранения */}
{isSaving && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-3.5 w-3.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="ml-1 text-xs">Сохранение...</span>
</div>
)}
{/* Кнопка сохранения */}
{onManualSave && (
<button
onClick={onManualSave}
disabled={isSaving}
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground'
} disabled:opacity-50`}
title={
lastSaveError
? 'Повторить сохранение'
: hasUnsavedChanges
? 'Есть несохраненные изменения'
: 'Все изменения сохранены'
}
>
<Save className="h-3 w-3" />
{lastSaveError
? 'Повторить'
: hasUnsavedChanges
? 'Сохранить'
: 'Сохранено'}
</button>
)}
</div>
</div>
</div>
)
}
)

View File

@@ -0,0 +1,128 @@
import { memo } from 'react'
interface SaveButtonProps {
hasUnsavedChanges: boolean
isSaving: boolean
lastSaveError: string | null
onManualSave: () => Promise<void>
onClearError: () => void
}
export const SaveButton = memo(
({
hasUnsavedChanges,
isSaving,
lastSaveError,
onManualSave,
onClearError,
}: SaveButtonProps) => {
return (
<div className="flex items-center gap-2">
{/* Ошибка сохранения */}
{lastSaveError && (
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
<svg
className="h-3 w-3 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<span className="text-xs text-destructive" title={lastSaveError}>
Ошибка сохранения
</span>
<button
onClick={onClearError}
className="ml-1 text-destructive/60 hover:text-destructive"
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
)}
{/* Индикатор сохранения */}
{isSaving && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-3.5 w-3.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="ml-1 text-xs">Сохранение...</span>
</div>
)}
{/* Кнопка сохранения */}
<button
onClick={onManualSave}
disabled={isSaving}
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground'
} disabled:opacity-50`}
title={
lastSaveError
? 'Повторить сохранение'
: hasUnsavedChanges
? 'Есть несохраненные изменения'
: 'Все изменения сохранены'
}
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{lastSaveError
? 'Повторить'
: hasUnsavedChanges
? 'Сохранить'
: 'Сохранено'}
</button>
</div>
)
}
)

View File

@@ -4,8 +4,6 @@ import {
Droplets,
Edit,
Edit3,
Eye,
EyeOff,
Gauge,
Grid,
Move,
@@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
}, [layoutSettings, onLayoutSettingsChange])
return (
<div className="flex h-full flex-col bg-gray-50">
<div className="flex shrink-0 items-center justify-between border-b bg-white p-2">
<span className="px-2 text-sm text-gray-600">
{elements.length} элементов на холсте
</span>
<Button variant="outline" size="sm" onClick={toggleGrid}>
{layoutSettings.showGrid ? (
<EyeOff className="mr-2 h-4 w-4" />
) : (
<Eye className="mr-2 h-4 w-4" />
)}
{layoutSettings.showGrid ? 'Скрыть сетку' : 'Показать сетку'}
</Button>
</div>
<div className="flex h-full flex-col bg-background">
<div
className="relative flex-1 overflow-auto"
onClick={handleCanvasClick}