подправка интерфейса

This commit is contained in:
2025-07-21 05:40:11 +03:00
parent ebe73614d8
commit b780f0e030
5 changed files with 112 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button'
import { Template } from '@/types/template'
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import React from 'react'
import { useNavigate } from 'react-router-dom'
@@ -19,11 +19,13 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-lg font-semibold">{template.name}</h1>
<p className="mt-0.5 text-xs text-muted-foreground">
Настройте Excel шаблон и значения отчета
</p>
<div className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<h1 className="text-lg font-semibold">Редактор шаблонов</h1>
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground">
{template.name}
</span>
</div>
</div>
<div className="flex gap-2">

View File

@@ -18,6 +18,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
}) => {
const navigate = useNavigate()
// Шаблон считается готовым, когда есть хотя бы один элемент и загружена
// Excel-таблица (что подтверждается наличием mergedCells).
const isConfigured =
template.elements.length > 0 && (template.mergedCells?.length ?? 0) > 0
return (
<Card className={isSelected ? 'border-2 border-blue-500' : ''}>
<CardHeader className="pb-4">
@@ -41,13 +46,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
<Button
size="sm"
className="flex-1"
disabled={!template.excelFile || template.elements.length === 0}
disabled={!isConfigured}
onClick={() => navigate(`/templates/${template.id}/protocols`)}
>
<FileText className="mr-2 h-3 w-3" />
{template.excelFile && template.elements.length > 0
? 'Создать протокол'
: 'Требует настройки'}
{isConfigured ? 'Создать протокол' : 'Требует настройки'}
</Button>
<Button
size="sm"

View File

@@ -1,3 +1,4 @@
import { useTemplateContext } from '@/contexts/TemplateContext'
import {
getFileData,
getLatestFileForTemplate,
@@ -27,6 +28,9 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
const [dataVersion, setDataVersion] = useState(0)
const [serverFileName, setServerFileName] = useState<string | undefined>()
// Получаем функцию обновления шаблона из контекста
const { updateTemplate } = useTemplateContext()
// При монтировании пытаемся загрузить последний файл для шаблона
useEffect(() => {
let isMounted = true
@@ -90,6 +94,13 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
}
setEditedTemplate(updatedTemplate)
// Сохраняем шаблон с новым Excel-файлом, чтобы в списке он больше не
// помечался как «требует настройки»
await updateTemplate({
...updatedTemplate,
updatedAt: new Date(),
})
setDataVersion(v => v + 1)
console.log('✅ Файл загружен с ID:', fileId)

View File

@@ -25,12 +25,28 @@ import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Textarea } from '../ui/textarea'
// Минимальные размеры для разных типов элементов.
// Если понадобится скорректировать размеры для конкретного элемента,
// достаточно изменить эту таблицу.
const ELEMENT_MIN_SIZE: Record<string, { width: number; height: number }> = {
text: { width: 120, height: 40 },
textarea: { width: 160, height: 80 },
number: { width: 120, height: 40 },
date: { width: 140, height: 40 },
select: { width: 140, height: 40 },
radio: { width: 160, height: 80 },
checkbox: { width: 140, height: 40 },
standards: { width: 200, height: 50 },
'calibration-conditions': { width: 250, height: 60 },
'button-group': { width: 180, height: 50 },
}
interface VisualLayoutEditorProps {
elements: TemplateElement[]
layoutSettings: FormLayoutSettings
onElementUpdate: (
elementId: string,
updates: Partial<TemplateElement>,
updates: Partial<TemplateElement>
) => void
onElementDelete: (elementId: string) => void
onLayoutSettingsChange: (settings: FormLayoutSettings) => void
@@ -85,7 +101,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
onUpdate({ ...layout, x: d.x, y: d.y })
}
},
[layout, onUpdate],
[layout, onUpdate]
)
const handleResizeStop = useCallback(
@@ -94,7 +110,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
_dir: any,
ref: HTMLElement,
_delta: any,
pos: { x: number; y: number },
pos: { x: number; y: number }
) => {
onUpdate({
...layout,
@@ -104,7 +120,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
y: pos.y,
})
},
[layout, onUpdate],
[layout, onUpdate]
)
const handleDeleteClick = useCallback(
@@ -112,7 +128,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
e.stopPropagation()
onDelete()
},
[onDelete],
[onDelete]
)
const handleEditClick = useCallback(
@@ -120,7 +136,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
e.stopPropagation()
onEdit?.()
},
[onEdit],
[onEdit]
)
/*
@@ -280,19 +296,17 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
}}
onDragStop={handleDragStop}
onResizeStop={handleResizeStop}
minWidth={120}
minHeight={32}
minWidth={ELEMENT_MIN_SIZE[element.type]?.width ?? 120}
minHeight={ELEMENT_MIN_SIZE[element.type]?.height ?? 32}
bounds="parent"
className={`group ${
isSelected ? 'z-20 ring-2 ring-blue-500 ring-offset-2' : 'z-10'
}`}
className={`group ${isSelected ? 'z-20 ring-2 ring-blue-500' : 'z-10'}`}
onClick={onSelect}
>
<div
className={`h-full cursor-move ${
isSelected
? 'rounded-md border border-blue-200/50 bg-blue-50/20 p-2'
: 'rounded-md border border-transparent bg-transparent p-1 hover:bg-gray-50/10'
? 'rounded-md bg-blue-50/10 p-1'
: 'rounded-md p-1 hover:bg-gray-50/10'
}`}
>
{/* Label and controls (only visible when selected or hovered) */}
@@ -373,7 +387,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
</div>
</Rnd>
)
},
}
)
export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
@@ -386,7 +400,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
}) => {
console.log('VisualLayoutEditor render with elements:', elements)
const [selectedElementId, setSelectedElementId] = useState<string | null>(
null,
null
)
const canvasSize = useMemo(() => {
@@ -401,7 +415,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
const contentHeight = Math.max(
0,
...elements.map((el) => (el.layout?.y || 0) + (el.layout?.height || 0)),
...elements.map(el => (el.layout?.y || 0) + (el.layout?.height || 0))
)
return {
@@ -428,7 +442,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
(elementId: string, layout: ElementLayout) => {
onElementUpdate(elementId, { layout })
},
[onElementUpdate],
[onElementUpdate]
)
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
@@ -471,13 +485,13 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
onClick={handleCanvasClick}
>
{GridPattern}
{elements.map((element) => (
{elements.map(element => (
<DraggableElement
key={element.id}
element={element}
isSelected={selectedElementId === element.id}
onSelect={() => setSelectedElementId(element.id)}
onUpdate={(layout) =>
onUpdate={layout =>
handleElementUpdateCallback(element.id, layout)
}
onDelete={() => onElementDelete(element.id)}

View File

@@ -2,7 +2,7 @@ import { ElementConstructor } from '@/components/TemplateManager/ElementConstruc
import { Button } from '@/components/ui/button'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { Template, TemplateElement } from '@/types/template'
import { ArrowLeft, Wrench } from 'lucide-react'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
@@ -112,87 +112,81 @@ export const ElementsCreation: FC = () => {
}
}
// Показываем загрузку
if (isLoading) {
// Компактные уведомления вместо полноэкранных блокеров
if (isLoading || error || !selectedTemplate) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
<p className="text-gray-600">Загрузка шаблона...</p>
</div>
</div>
)
}
// Показываем ошибку
if (error) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<Wrench className="mx-auto mb-4 h-16 w-16 text-red-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900">
Ошибка загрузки
</h3>
<p className="mb-4 text-gray-600">{error}</p>
<Button onClick={() => navigate('/templates')}>
Назад к шаблонам
</Button>
</div>
</div>
)
}
// Если шаблон не найден
if (!selectedTemplate) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<Wrench className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900">
Шаблон не найден
</h3>
<p className="mb-4 text-gray-600">
Выбранный шаблон не существует или был удален
</p>
<Button onClick={() => navigate('/templates')}>
Назад к шаблонам
</Button>
<div className="p-4">
<div
className={`rounded-md border px-4 py-2 text-sm ${
error
? 'border-destructive/50 bg-destructive/10 text-destructive'
: !selectedTemplate
? 'border-destructive/50 bg-destructive/10 text-destructive'
: 'border-muted bg-muted/20 text-muted-foreground'
}`}
>
{isLoading && 'Загрузка шаблона...'}
{error && `Ошибка загрузки: ${error}`}
{!selectedTemplate && 'Шаблон не найден'}
</div>
</div>
)
}
return (
<div className="h-screen bg-background">
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center px-4">
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={() => navigate('/templates')}
className="mr-4"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center space-x-2">
<Wrench className="h-5 w-5" />
<h1 className="text-lg font-semibold">Редактор элементов</h1>
<Settings className="h-5 w-5" />
<h1 className="text-lg font-semibold">Редактор шаблонов</h1>
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground">
{selectedTemplate.name}
</span>
</div>
{/* Индикатор сохранения */}
{isSaving && (
<div className="ml-auto flex items-center space-x-2 text-sm text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
<span>Сохранение...</span>
</div>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`/templates/${selectedTemplate.id}/elements`)
}
className="flex items-center gap-2"
>
<Wrench className="h-3.5 w-3.5" />
Элементы интерфейса
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`/templates/${selectedTemplate.id}/protocols`)
}
className="flex items-center gap-2"
>
<FileText className="h-3.5 w-3.5" />
Создать протокол
</Button>
</div>
</div>
{/* Индикатор сохранения */}
{isSaving && (
<div className="ml-auto flex items-center space-x-2 text-sm text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
<span>Сохранение...</span>
</div>
)}
<ElementConstructor
template={selectedTemplate}
onElementAdd={handleElementAdd}