элементы и передвижение
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/component/ui/select'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
@@ -255,8 +256,7 @@ function CalibrationCondInfo({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Компактное отображение */}
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border bg-muted/50 px-3 py-2">
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{fieldConfigs.map(({ key, icon: Icon, unit }) => (
|
||||
<div key={key} className="flex items-center gap-1">
|
||||
@@ -268,19 +268,24 @@ function CalibrationCondInfo({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={openModal}
|
||||
className={`h-6 w-6 p-0 ${
|
||||
isOutdated ? 'text-yellow-600 hover:text-yellow-700' : ''
|
||||
}`}
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</Button>
|
||||
{isOutdated && (
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isOutdated && (
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={openModal}
|
||||
className={cn(
|
||||
'h-6 w-6 p-0',
|
||||
isOutdated && 'text-yellow-600 hover:text-yellow-700'
|
||||
)}
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно */}
|
||||
@@ -729,23 +734,20 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
},
|
||||
Preview: ({ config }) => (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<CalibrationCondInfo
|
||||
conditions={
|
||||
config.defaultConditions || {
|
||||
temperature: '20',
|
||||
humidity: '65',
|
||||
pressure: '101.3',
|
||||
voltage: '220',
|
||||
frequency: '50',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
<CalibrationCondInfo
|
||||
conditions={
|
||||
config.defaultConditions || {
|
||||
temperature: '20',
|
||||
humidity: '65',
|
||||
pressure: '101.3',
|
||||
voltage: '220',
|
||||
frequency: '50',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
isOutdated={false}
|
||||
onUpdate={async () => true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isOutdated={false}
|
||||
onUpdate={async () => true}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
Render: ({ config, value, onChange }) => {
|
||||
|
||||
@@ -773,7 +773,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
const autoSaveOptions = useMemo(
|
||||
() => ({
|
||||
templateId,
|
||||
autoSaveDelay: 2000,
|
||||
autoSaveDelay: 4000,
|
||||
enableAutoSave: enableAutoSave && userEdited,
|
||||
}),
|
||||
[templateId, enableAutoSave, userEdited]
|
||||
@@ -824,7 +824,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
if (fileData && Object.keys(fileData.data).length > 0) {
|
||||
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||||
setBaseFileData(finalBaseData)
|
||||
// Сохраняем baseline, чтобы в листы попадали только пользовательские изменения
|
||||
setTemplateData(finalBaseData)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@ import {
|
||||
getElementDefinition,
|
||||
} from '@/entitiy/element/model/interface'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import {
|
||||
autoArrangeElements,
|
||||
findFreePosition,
|
||||
generateDefaultLayout,
|
||||
} from '@/lib/utils'
|
||||
import { generateDefaultLayout } from '@/lib/utils'
|
||||
import {
|
||||
CellTarget,
|
||||
ElementType,
|
||||
@@ -591,15 +587,8 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
}
|
||||
|
||||
// Генерируем layout для нового элемента
|
||||
const existingLayouts = elements.map(e => e.layout).filter(Boolean)
|
||||
const defaultLayout = generateDefaultLayout(elements.length)
|
||||
|
||||
const layout = findFreePosition(
|
||||
defaultLayout,
|
||||
existingLayouts as any[],
|
||||
layoutSettings.gridSize
|
||||
)
|
||||
|
||||
// Используем targetCells из формы или генерируем через mapToCells
|
||||
let targetCells = formData.targetCells || []
|
||||
if (!targetCells.length) {
|
||||
@@ -609,14 +598,14 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
const newElement: TemplateElement = {
|
||||
id: Date.now().toString(),
|
||||
type: formData.type as ElementType,
|
||||
name: formData.label?.toLowerCase().replace(/\s+/g, '_') || '',
|
||||
name: formData.label || '',
|
||||
label: formData.label,
|
||||
targetCells,
|
||||
placeholder: formData.placeholder,
|
||||
required: formData.required,
|
||||
options: formData.options,
|
||||
order: elements.length,
|
||||
layout,
|
||||
layout: defaultLayout,
|
||||
}
|
||||
|
||||
onElementAdd(newElement)
|
||||
@@ -666,21 +655,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const handleAutoArrange = () => {
|
||||
const arrangedElements = autoArrangeElements(
|
||||
elements,
|
||||
layoutSettings.gridSize
|
||||
)
|
||||
|
||||
arrangedElements.forEach((element, index) => {
|
||||
onElementUpdate(element.id, {
|
||||
...element,
|
||||
layout: element.layout,
|
||||
order: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteAndSelect = (elementId: string) => {
|
||||
onElementDelete(elementId)
|
||||
setEditingElement(null)
|
||||
@@ -697,7 +671,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
<ElementFormulaBar
|
||||
elementCount={elements.length}
|
||||
onAddElement={() => setIsAddDialogOpen(true)}
|
||||
onAutoArrange={handleAutoArrange}
|
||||
disabled={disabled}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
isSaving={isSaving}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||
className="flex items-center gap-1 hover:bg-muted"
|
||||
@@ -39,7 +39,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
||||
<span className="text-sm">Редактор интерфейса</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
||||
className="flex items-center gap-1 hover:bg-muted"
|
||||
|
||||
@@ -21,7 +21,7 @@ const ELEMENT_MIN_SIZE: Record<string, { width: number; height: number }> = {
|
||||
radio: { width: 160, height: 80 },
|
||||
checkbox: { width: 140, height: 40 },
|
||||
standards: { width: 200, height: 50 },
|
||||
calibration_conditions: { width: 250, height: 60 },
|
||||
calibration_conditions: { width: 350, height: 50 },
|
||||
button_group: { width: 180, height: 80 },
|
||||
}
|
||||
|
||||
|
||||
@@ -65,93 +65,6 @@ export function generateDefaultLayout(order: number): ElementLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматическое размещение элементов без пересечений
|
||||
export function autoArrangeElements(
|
||||
elements: TemplateElement[],
|
||||
gridSize: number = 20
|
||||
): TemplateElement[] {
|
||||
const arranged = [...elements]
|
||||
const cols = 3 // Количество колонок
|
||||
const itemWidth = 300
|
||||
const itemHeight = 80
|
||||
const marginX = 20
|
||||
const marginY = 20
|
||||
const startX = 50
|
||||
const startY = 50
|
||||
|
||||
arranged.forEach((element, index) => {
|
||||
const col = index % cols
|
||||
const row = Math.floor(index / cols)
|
||||
|
||||
const x = startX + col * (itemWidth + marginX)
|
||||
const y = startY + row * (itemHeight + marginY)
|
||||
|
||||
// Выравниваем по сетке
|
||||
const alignedX = Math.round(x / gridSize) * gridSize
|
||||
const alignedY = Math.round(y / gridSize) * gridSize
|
||||
|
||||
element.layout = {
|
||||
...element.layout,
|
||||
x: alignedX,
|
||||
y: alignedY,
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
}
|
||||
})
|
||||
|
||||
return arranged
|
||||
}
|
||||
|
||||
// Проверка пересечения двух элементов
|
||||
export function checkElementOverlap(
|
||||
element1: ElementLayout,
|
||||
element2: ElementLayout
|
||||
): boolean {
|
||||
return !(
|
||||
element1.x + element1.width <= element2.x ||
|
||||
element2.x + element2.width <= element1.x ||
|
||||
element1.y + element1.height <= element2.y ||
|
||||
element2.y + element2.height <= element1.y
|
||||
)
|
||||
}
|
||||
|
||||
// Поиск свободного места для элемента (поиск в разумных пределах)
|
||||
export function findFreePosition(
|
||||
newElement: ElementLayout,
|
||||
existingElements: ElementLayout[],
|
||||
gridSize: number = 20
|
||||
): ElementLayout {
|
||||
const step = gridSize
|
||||
const maxWidth = 1600 // Максимальная ширина поиска
|
||||
const maxHeight = 1200 // Максимальная высота поиска
|
||||
const maxAttempts = 500
|
||||
let attempts = 0
|
||||
|
||||
for (let y = 50; y <= maxHeight - newElement.height; y += step) {
|
||||
for (let x = 50; x <= maxWidth - newElement.width; x += step) {
|
||||
attempts++
|
||||
if (attempts > maxAttempts) break
|
||||
|
||||
const testPosition = { ...newElement, x, y }
|
||||
const hasOverlap = existingElements.some(existing =>
|
||||
checkElementOverlap(testPosition, existing)
|
||||
)
|
||||
|
||||
if (!hasOverlap) {
|
||||
return testPosition
|
||||
}
|
||||
}
|
||||
if (attempts > maxAttempts) break
|
||||
}
|
||||
|
||||
// Если не нашли свободное место, размещаем в правом нижнем углу
|
||||
return {
|
||||
...newElement,
|
||||
x: 50,
|
||||
y: existingElements.length * 100 + 50,
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация адреса ячейки - теперь использует централизованную утилиту
|
||||
export function validateCellAddress(cell: string): boolean {
|
||||
return parseCellAddress(cell) !== null
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
@@ -151,9 +151,9 @@ export const ElementsCreation: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="flex-shrink-0 border-b bg-card px-4 py-3">
|
||||
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -174,13 +174,11 @@ export const ElementsCreation: FC = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
navigate(`/templates/${selectedTemplate.id}/elements`)
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(`/templates/${selectedTemplate.id}/edit`)}
|
||||
className="flex items-center gap-2 hover:bg-muted"
|
||||
>
|
||||
<Wrench className="h-3.5 w-3.5" />
|
||||
Элементы интерфейса
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Настройки шаблона
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -188,15 +186,14 @@ export const ElementsCreation: FC = () => {
|
||||
onClick={() =>
|
||||
navigate(`/templates/${selectedTemplate.id}/protocols`)
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-1 hover:bg-muted"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Создать протокол
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="text-sm">Создать протокол</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ElementConstructor
|
||||
template={selectedTemplate}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cellAddressToCoordinates } from '@/lib/cell-utils'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
import { Template, TemplateElement } from '@/type/template'
|
||||
import { ArrowLeft, FileText, Grid, Save } from 'lucide-react'
|
||||
import { ArrowLeft, FileText, Grid, Save, Settings, Wrench } from 'lucide-react'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
@@ -44,25 +44,23 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="relative space-y-3">
|
||||
{/* Показываем индикатор ячеек для всех элементов */}
|
||||
{element.targetCells && element.targetCells.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<div className="group/cells relative">
|
||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
||||
{element.targetCells.map((cell, i) => (
|
||||
<div key={i} className="font-mono">
|
||||
{cell.sheet}!{cell.cell}
|
||||
{cell.displayName && (
|
||||
<span className="ml-1 text-gray-300">
|
||||
({cell.displayName})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="group/cells absolute right-0 top-0">
|
||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
||||
{element.targetCells.map((cell, i) => (
|
||||
<div key={i} className="font-mono">
|
||||
{cell.sheet}!{cell.cell}
|
||||
{cell.displayName && (
|
||||
<span className="ml-1 text-gray-300">
|
||||
({cell.displayName})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -78,6 +76,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
const [showSpreadsheet, setShowSpreadsheet] = useState(false)
|
||||
const engineRef = useRef<any>(null)
|
||||
const toast = useToast()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleFieldChange = (elementId: string, value: any) => {
|
||||
setFormData(prev => ({
|
||||
@@ -266,7 +265,8 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-gray-50">
|
||||
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative flex items-center">
|
||||
{/* Левая часть: кнопка назад и заголовок */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -280,15 +280,40 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{/* Кнопки справа */}
|
||||
<div className="absolute right-0 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/templates/${template.id}/edit`)}
|
||||
className="flex items-center gap-1 hover:bg-muted"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-sm">Редактор шаблона</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||
className="flex items-center gap-1 hover:bg-muted"
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
<span className="text-sm">Редактор интерфейса</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Центрированные кнопки */}
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 gap-2">
|
||||
<Button
|
||||
variant={showSpreadsheet ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setShowSpreadsheet(!showSpreadsheet)}
|
||||
aria-label={
|
||||
showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'
|
||||
}
|
||||
>
|
||||
<Grid className="mr-2 h-4 w-4" />
|
||||
{showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'}
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!canSave}>
|
||||
<Button variant="outline" onClick={handleSave} disabled={!canSave}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Сохранить протокол
|
||||
</Button>
|
||||
|
||||
@@ -23,8 +23,6 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// const isConfigured =
|
||||
// template.elements.length > 0 && template.excelFile !== undefined
|
||||
const isConfigured = true // TODO: remove
|
||||
|
||||
return (
|
||||
@@ -37,12 +35,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
: undefined
|
||||
}
|
||||
className={clsx(
|
||||
'group relative shadow-sm transition-all duration-200',
|
||||
selectionMode && 'hover:cursor-pointer',
|
||||
selectionMode &&
|
||||
isSelected &&
|
||||
'border-2 border-primary ring-4 ring-primary/30 ring-offset-2',
|
||||
'hover:border-2 hover:border-primary hover:shadow-lg'
|
||||
'hover:border-2 hover:border-primary'
|
||||
)}
|
||||
>
|
||||
{selectionMode && (
|
||||
@@ -98,6 +95,7 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-9 flex-1"
|
||||
disabled={selectionMode || !isConfigured}
|
||||
onClick={e => {
|
||||
|
||||
Reference in New Issue
Block a user