переименования папок

This commit is contained in:
2025-07-21 15:02:10 +03:00
parent d0c79bd334
commit 84f0c9c6aa
72 changed files with 245 additions and 249 deletions

View File

@@ -2,10 +2,10 @@ import { FC } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { TemplateProvider } from '../contexts/TemplateContext'
import { ElementsCreation } from '../pages/ElementsCreation'
import { ProtocolCreation } from '../pages/ProtocolCreation'
import { TemplateSetup } from '../pages/TemplateSetup'
import { TemplateProvider } from '../context/TemplateContext'
import { ElementsCreation } from '../page/ElementsCreation'
import { ProtocolCreation } from '../page/ProtocolCreation'
import { TemplateSetup } from '../page/TemplateSetup'
import { Layout } from './Layout'
const App: FC = () => {

View File

@@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/types/template'
import { ElementOption } from '@/type/template'
import { Plus, Square, Trash2 } from 'lucide-react'
interface ButtonGroupConfig {
@@ -39,12 +39,12 @@ export const buttonGroupDefinition: ElementDefinition<
const handleUpdateOption = (
index: number,
field: 'value' | 'label',
value: string,
value: string
) => {
onChange({
...config,
options: config.options.map((option, i) =>
i === index ? { ...option, [field]: value } : option,
i === index ? { ...option, [field]: value } : option
),
})
}
@@ -119,14 +119,14 @@ export const buttonGroupDefinition: ElementDefinition<
<Input
placeholder="Значение"
value={option.value}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
/>
<Input
placeholder="Текст кнопки"
value={option.label}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
/>
@@ -161,8 +161,8 @@ export const buttonGroupDefinition: ElementDefinition<
config.buttonStyle === 'outline'
? 'border-border bg-background text-foreground hover:border-primary/30 hover:bg-primary/5'
: config.buttonStyle === 'ghost'
? 'border-transparent bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground'
: 'border-primary bg-primary text-primary-foreground shadow-sm'
? 'border-transparent bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground'
: 'border-primary bg-primary text-primary-foreground shadow-sm'
}`}
>
{option.label || `Кнопка ${index + 1}`}
@@ -192,10 +192,10 @@ export const buttonGroupDefinition: ElementDefinition<
value === option.value
? 'border-primary bg-primary text-primary-foreground shadow-sm'
: config.buttonStyle === 'outline'
? 'border-border bg-background text-foreground hover:border-primary/30 hover:bg-primary/5'
: config.buttonStyle === 'ghost'
? 'border-transparent bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground'
: 'border-border bg-background text-foreground hover:border-primary/30 hover:bg-primary/5'
? 'border-border bg-background text-foreground hover:border-primary/30 hover:bg-primary/5'
: config.buttonStyle === 'ghost'
? 'border-transparent bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground'
: 'border-border bg-background text-foreground hover:border-primary/30 hover:bg-primary/5'
}`}
>
{option.label}

View File

@@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/types/template'
import { ElementOption } from '@/type/template'
import { CheckSquare, Plus, Trash2 } from 'lucide-react'
interface RadioConfig {
@@ -32,12 +32,12 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
const handleUpdateOption = (
index: number,
field: 'value' | 'label',
value: string,
value: string
) => {
onChange({
...config,
options: config.options.map((option, i) =>
i === index ? { ...option, [field]: value } : option,
i === index ? { ...option, [field]: value } : option
),
})
}
@@ -65,14 +65,14 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
<Input
placeholder="Значение"
value={option.value}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
/>
@@ -121,7 +121,7 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
name="radio-group"
value={option.value}
checked={value === option.value}
onChange={(e) => onChange?.(e.target.value)}
onChange={e => onChange?.(e.target.value)}
required={config.required}
className="h-4 w-4"
/>

View File

@@ -8,7 +8,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/types/template'
import { ElementOption } from '@/type/template'
import { ChevronDown, Plus, Trash2 } from 'lucide-react'
interface SelectConfig {
@@ -39,12 +39,12 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
const handleUpdateOption = (
index: number,
field: 'value' | 'label',
value: string,
value: string
) => {
onChange({
...config,
options: config.options.map((option, i) =>
i === index ? { ...option, [field]: value } : option,
i === index ? { ...option, [field]: value } : option
),
})
}
@@ -72,14 +72,14 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
<Input
placeholder="Значение"
value={option.value}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={(e) =>
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
/>

View File

@@ -1,6 +1,6 @@
import { useMultiSheetEngine } from '@/hooks/useMultiSheetEngine'
import { useSheetAutoSave } from '@/hooks/useSheetAutoSave'
import { getFileData, getLatestFileForTemplate } from '@/services/fileApiSevice'
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiSevice'
import { produce } from 'immer'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'

View File

@@ -1,4 +1,4 @@
import { MergedCell } from '@/types/template'
import { MergedCell } from '@/type/template'
import { CSSProperties } from 'react'
import { VariableSizeGrid } from 'react-window'

View File

@@ -1,4 +1,4 @@
import { MergedCell } from '@/types/template'
import { MergedCell } from '@/type/template'
import { MergedCellInfo } from './types'
// --- Helper Functions ---

View File

@@ -1,59 +1,73 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { CellTarget } from "@/types/template";
import { Calendar, Settings, Trash2 } from "lucide-react";
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { CellTarget } from '@/type/template'
import { Calendar, Settings, Trash2 } from 'lucide-react'
interface StandardsConfig {
registryNumber: string; // ГРСИ
sheet: string; // Лист Excel
startCell: string; // Первая ячейка (например "A10")
maxItems: number; // Обычно 7
targetCells: CellTarget[];
registryNumber: string // ГРСИ
sheet: string // Лист Excel
startCell: string // Первая ячейка (например "A10")
maxItems: number // Обычно 7
targetCells: CellTarget[]
}
interface StandardsEditorProps {
config: StandardsConfig;
onChange: (config: StandardsConfig) => void;
config: StandardsConfig
onChange: (config: StandardsConfig) => void
}
const CellTargetEditor: React.FC<{
targets: CellTarget[];
onChange: (targets: CellTarget[]) => void;
targets: CellTarget[]
onChange: (targets: CellTarget[]) => void
}> = ({ targets, onChange }) => {
const handleAddTarget = () => {
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }]);
};
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }])
}
const handleUpdateTarget = (index: number, field: keyof CellTarget, value: string) => {
const handleUpdateTarget = (
index: number,
field: keyof CellTarget,
value: string
) => {
const updatedTargets = targets.map((target, i) =>
i === index ? { ...target, [field]: value } : target
);
onChange(updatedTargets);
};
)
onChange(updatedTargets)
}
const handleRemoveTarget = (index: number) => {
onChange(targets.filter((_, i) => i !== index));
};
onChange(targets.filter((_, i) => i !== index))
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Целевые ячейки</label>
<Button variant="outline" size="sm" onClick={handleAddTarget}>
<Calendar className="h-4 w-4 mr-1" />
<Calendar className="mr-1 h-4 w-4" />
Добавить ячейку
</Button>
</div>
<div className="space-y-3 max-h-48 overflow-y-auto">
<div className="max-h-48 space-y-3 overflow-y-auto">
{targets.map((target, index) => (
<div key={index} className="p-3 border rounded-lg bg-gray-50">
<div className="grid grid-cols-3 gap-2 mb-2">
<div key={index} className="rounded-lg border bg-gray-50 p-3">
<div className="mb-2 grid grid-cols-3 gap-2">
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Лист</label>
<label className="text-xs font-medium text-gray-600">
Лист
</label>
<Select
value={target.sheet}
onValueChange={(value) => handleUpdateTarget(index, 'sheet', value)}
onValueChange={value =>
handleUpdateTarget(index, 'sheet', value)
}
>
<SelectTrigger className="h-8">
<SelectValue />
@@ -67,20 +81,28 @@ const CellTargetEditor: React.FC<{
</Select>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Ячейка</label>
<label className="text-xs font-medium text-gray-600">
Ячейка
</label>
<Input
placeholder="A1, B5..."
value={target.cell}
onChange={(e) => handleUpdateTarget(index, 'cell', e.target.value)}
onChange={e =>
handleUpdateTarget(index, 'cell', e.target.value)
}
className="h-8"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Название</label>
<label className="text-xs font-medium text-gray-600">
Название
</label>
<Input
placeholder="Описание"
value={target.displayName || ''}
onChange={(e) => handleUpdateTarget(index, 'displayName', e.target.value)}
onChange={e =>
handleUpdateTarget(index, 'displayName', e.target.value)
}
className="h-8"
/>
</div>
@@ -91,22 +113,25 @@ const CellTargetEditor: React.FC<{
onClick={() => handleRemoveTarget(index)}
className="h-6 text-xs text-red-600 hover:text-red-700"
>
<Trash2 className="h-3 w-3 mr-1" />
<Trash2 className="mr-1 h-3 w-3" />
Удалить
</Button>
</div>
))}
{targets.length === 0 && (
<p className="text-sm text-gray-500 text-center py-4">
<p className="py-4 text-center text-sm text-gray-500">
Добавьте ячейки для связи с элементом
</p>
)}
</div>
</div>
);
};
)
}
export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChange }) => {
export const StandardsEditor: React.FC<StandardsEditorProps> = ({
config,
onChange,
}) => {
// Обеспечиваем значения по умолчанию
const safeConfig = {
registryNumber: config.registryNumber || '',
@@ -114,16 +139,16 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
startCell: config.startCell || 'A1',
maxItems: config.maxItems || 7,
targetCells: config.targetCells || [],
};
}
const updateConfig = (updates: Partial<StandardsConfig>) => {
onChange({ ...safeConfig, ...updates });
};
onChange({ ...safeConfig, ...updates })
}
return (
<div className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium flex items-center gap-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Settings className="h-4 w-4" />
Настройки эталонов
</label>
@@ -138,7 +163,7 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
<Input
placeholder="Введите номер ГРСИ"
value={safeConfig.registryNumber}
onChange={(e) => updateConfig({ registryNumber: e.target.value })}
onChange={e => updateConfig({ registryNumber: e.target.value })}
/>
</div>
@@ -146,7 +171,7 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={(value) => updateConfig({ maxItems: parseInt(value) })}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
@@ -166,7 +191,7 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
<label className="text-sm font-medium">Лист Excel</label>
<Select
value={safeConfig.sheet}
onValueChange={(value) => updateConfig({ sheet: value })}
onValueChange={value => updateConfig({ sheet: value })}
>
<SelectTrigger>
<SelectValue />
@@ -185,7 +210,7 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
<Input
placeholder="A1, B5..."
value={safeConfig.startCell}
onChange={(e) => updateConfig({ startCell: e.target.value })}
onChange={e => updateConfig({ startCell: e.target.value })}
/>
</div>
</div>
@@ -197,23 +222,25 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({ config, onChan
</p>
<CellTargetEditor
targets={safeConfig.targetCells}
onChange={(targets) => updateConfig({ targetCells: targets })}
onChange={targets => updateConfig({ targetCells: targets })}
/>
</div>
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="flex items-start gap-2">
<Calendar className="h-4 w-4 text-blue-600 mt-0.5" />
<Calendar className="mt-0.5 h-4 w-4 text-blue-600" />
<div className="text-sm">
<div className="font-medium text-blue-800">Автоматическое заполнение</div>
<div className="text-blue-600 text-xs mt-1">
При выборе эталонов данные будут записаны в указанные ячейки.
Если выбрано меньше эталонов, чем максимальное количество,
оставшиеся ячейки останутся пустыми.
<div className="font-medium text-blue-800">
Автоматическое заполнение
</div>
<div className="mt-1 text-xs text-blue-600">
При выборе эталонов данные будут записаны в указанные ячейки. Если
выбрано меньше эталонов, чем максимальное количество, оставшиеся
ячейки останутся пустыми.
</div>
</div>
</div>
</div>
</div>
);
};
)
}

View File

@@ -1,6 +1,6 @@
import { ElementDefinition } from '@/lib/element-registry'
import { parseCellAddress } from '@/lib/utils'
import { CellTarget } from '@/types/template'
import { CellTarget } from '@/type/template'
import { Weight } from 'lucide-react'
import { StandardsEditor } from './StandardsEditor'
import { StandardsPreview } from './StandardsPreview'
@@ -32,7 +32,7 @@ export const standardsDefinition: ElementDefinition<StandardsConfig, string[]> =
// Пока возвращаем null, так как StandardsDialog требует дополнительные пропсы
return null
},
mapToCells: (cfg) => {
mapToCells: cfg => {
const res: CellTarget[] = []
const parsed = parseCellAddress(cfg.startCell)

View File

@@ -13,7 +13,7 @@ import {
FormLayoutSettings,
Template,
TemplateElement,
} from '@/types/template'
} from '@/type/template'
import {
Calendar,
Droplets,

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button'
import { Template } from '@/types/template'
import { Template } from '@/type/template'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import React from 'react'
import { useNavigate } from 'react-router-dom'

View File

@@ -1,5 +1,5 @@
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet'
import { ExcelParseResult } from '@/services/fileApiSevice'
import { ExcelParseResult } from '@/service/fileApiSevice'
import React from 'react'
interface SpreadsheetViewerProps {

View File

@@ -1,7 +1,7 @@
import { FileText, Settings, Wrench } from 'lucide-react'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { Template } from '../../types/template'
import { Template } from '../../type/template'
import { Button } from '../ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'

View File

@@ -1,10 +1,10 @@
import { useTemplateContext } from '@/contexts/TemplateContext'
import { useTemplateContext } from '@/context/TemplateContext'
import {
getFileData,
getLatestFileForTemplate,
parseExcelFile,
} from '@/services/fileApiSevice'
import { Template } from '@/types/template'
} from '@/service/fileApiSevice'
import { Template } from '@/type/template'
import React, { useEffect, useState } from 'react'
import { ExcelUploadPanel } from './ExcelUploadPanel'
import { FormattingToolbar } from './FormattingToolbar'

View File

@@ -1,8 +1,8 @@
import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTemplateContext } from '../../contexts/TemplateContext'
import { Template } from '../../types/template'
import { useTemplateContext } from '../../context/TemplateContext'
import { Template } from '../../type/template'
import { Button } from '../ui/button'
import {
Dialog,

View File

@@ -20,7 +20,7 @@ import {
ElementLayout,
FormLayoutSettings,
TemplateElement,
} from '../../types/template'
} from '../../type/template'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Textarea } from '../ui/textarea'

View File

@@ -15,8 +15,8 @@ import {
patchTemplate,
templateToCreateInput,
templateToPatchInput,
} from '../services/templateApiService'
import { Template, TemplateElement } from '../types/template'
} from '../service/templateApiService'
import { Template, TemplateElement } from '../type/template'
interface TemplateContextType {
templates: Template[]

View File

@@ -4,7 +4,7 @@ import {
createSheet,
getSheetsByTemplate,
patchSheet,
} from '../services/sheetApiService'
} from '../service/sheetApiService'
export interface UseSheetAutoSaveOptions {
templateId?: string

View File

@@ -1,42 +1,48 @@
import { ReactNode } from "react";
import { CellTarget, ElementType } from "../types/template";
import { ReactNode } from 'react'
import { CellTarget, ElementType } from '../type/template'
export interface ElementDefinition<Config = any, Value = any> {
/* служебные */
type: ElementType; // "text" | "select" | ...
label: string; // Человекочитаемое имя
icon: ReactNode;
type: ElementType // "text" | "select" | ...
label: string // Человекочитаемое имя
icon: ReactNode
/* React-компоненты */
Editor: React.FC<{ config: Config; onChange(c: Config): void }>;
Preview: React.FC<{ config: Config }>;
Render: React.FC<{ config: Config; value: Value; onChange?(v: Value): void }>;
Editor: React.FC<{ config: Config; onChange(c: Config): void }>
Preview: React.FC<{ config: Config }>
Render: React.FC<{ config: Config; value: Value; onChange?(v: Value): void }>
/* бизнес-логика */
defaultConfig: Config;
mapToCells?(config: Config): CellTarget[]; // куда пишем данные
defaultConfig: Config
mapToCells?(config: Config): CellTarget[] // куда пишем данные
}
export const elementRegistry: Record<ElementType, ElementDefinition> = {} as any;
export const elementRegistry: Record<ElementType, ElementDefinition> = {} as any
// Функция для регистрации элемента
export function registerElement<T extends ElementType>(
type: T,
definition: ElementDefinition
) {
elementRegistry[type] = definition;
elementRegistry[type] = definition
}
// Получить все доступные типы элементов
export function getAvailableElementTypes(): Array<{ type: ElementType; label: string; icon: ReactNode }> {
export function getAvailableElementTypes(): Array<{
type: ElementType
label: string
icon: ReactNode
}> {
return Object.values(elementRegistry).map(({ type, label, icon }) => ({
type,
label,
icon,
}));
}))
}
// Получить определение элемента по типу
export function getElementDefinition(type: ElementType): ElementDefinition | undefined {
return elementRegistry[type];
}
export function getElementDefinition(
type: ElementType
): ElementDefinition | undefined {
return elementRegistry[type]
}

View File

@@ -1,6 +1,11 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { CellTarget, ElementLayout, FormLayoutSettings, TemplateElement } from "../types/template";
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import {
CellTarget,
ElementLayout,
FormLayoutSettings,
TemplateElement,
} from '../type/template'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -10,21 +15,21 @@ export function cn(...inputs: ClassValue[]) {
export function migrateTemplateElement(element: any): TemplateElement {
// Если элемент уже в новом формате
if (element.targetCells && Array.isArray(element.targetCells)) {
return element as TemplateElement;
return element as TemplateElement
}
// Миграция из старого формата
const targetCells: CellTarget[] = [];
const targetCells: CellTarget[] = []
if (element.targetCell) {
// Парсим старый формат targetCell (например "A1", "B5")
const cellMatch = element.targetCell.match(/^([A-Z]+)(\d+)$/);
const cellMatch = element.targetCell.match(/^([A-Z]+)(\d+)$/)
if (cellMatch) {
targetCells.push({
sheet: 'Report', // По умолчанию используем Report
cell: element.targetCell,
displayName: `Ячейка ${element.targetCell}`,
});
})
}
}
@@ -33,7 +38,7 @@ export function migrateTemplateElement(element: any): TemplateElement {
targetCells,
order: element.order || 0,
layout: element.layout || generateDefaultLayout(element.order || 0),
} as TemplateElement;
} as TemplateElement
}
// Получение настроек отображения по умолчанию
@@ -41,49 +46,49 @@ export function getDefaultLayoutSettings(): FormLayoutSettings {
return {
gridSize: 20,
showGrid: true,
};
}
}
// Генерация позиции по умолчанию для нового элемента
export function generateDefaultLayout(order: number = 0): ElementLayout {
const cols = 4; // Количество колонок для автоматического размещения
const elementWidth = 300;
const elementHeight = 80;
const padding = 50;
const col = order % cols;
const row = Math.floor(order / cols);
const cols = 4 // Количество колонок для автоматического размещения
const elementWidth = 300
const elementHeight = 80
const padding = 50
const col = order % cols
const row = Math.floor(order / cols)
return {
x: col * (elementWidth + padding) + padding,
y: row * (elementHeight + padding) + padding,
width: elementWidth,
height: elementHeight,
zIndex: 1,
};
}
}
// Автоматическое размещение элементов на холсте
export function autoArrangeElements(
elements: TemplateElement[],
elements: TemplateElement[],
gridSize: number = 20
): TemplateElement[] {
const elementWidth = 300;
const elementHeight = 80;
const padding = 50;
const cols = 4; // Фиксированное количество колонок
const elementWidth = 300
const elementHeight = 80
const padding = 50
const cols = 4 // Фиксированное количество колонок
return elements.map((element, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * (elementWidth + padding) + padding;
const y = row * (elementHeight + padding) + padding;
const col = index % cols
const row = Math.floor(index / cols)
const x = col * (elementWidth + padding) + padding
const y = row * (elementHeight + padding) + padding
// Привязка к сетке
const snappedX = Math.round(x / gridSize) * gridSize;
const snappedY = Math.round(y / gridSize) * gridSize;
const snappedX = Math.round(x / gridSize) * gridSize
const snappedY = Math.round(y / gridSize) * gridSize
return {
...element,
layout: {
@@ -93,8 +98,8 @@ export function autoArrangeElements(
height: elementHeight,
zIndex: 1,
},
};
});
}
})
}
// Проверка пересечения элементов
@@ -108,7 +113,7 @@ export function checkElementOverlap(
element2.x + element2.width + threshold < element1.x ||
element1.y + element1.height + threshold < element2.y ||
element2.y + element2.height + threshold < element1.y
);
)
}
// Поиск свободного места для элемента (поиск в разумных пределах)
@@ -117,54 +122,56 @@ export function findFreePosition(
existingElements: ElementLayout[],
gridSize: number = 20
): ElementLayout {
const step = gridSize;
const maxWidth = 1600; // Максимальная ширина поиска
const maxHeight = 1200; // Максимальная высота поиска
const maxAttempts = 500;
let attempts = 0;
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 =>
attempts++
if (attempts > maxAttempts) break
const testPosition = { ...newElement, x, y }
const hasOverlap = existingElements.some(existing =>
checkElementOverlap(testPosition, existing)
);
)
if (!hasOverlap) {
return testPosition;
return testPosition
}
}
if (attempts > maxAttempts) break;
if (attempts > maxAttempts) break
}
// Если не нашли свободное место, размещаем в правом нижнем углу
return {
...newElement,
x: 50,
y: existingElements.length * 100 + 50
};
y: existingElements.length * 100 + 50,
}
}
// Валидация адреса ячейки
export function validateCellAddress(cell: string): boolean {
return /^[A-Z]+\d+$/.test(cell);
return /^[A-Z]+\d+$/.test(cell)
}
// Парсинг адреса ячейки в компоненты
export function parseCellAddress(cell: string): { column: string; row: number } | null {
const match = cell.match(/^([A-Z]+)(\d+)$/);
if (!match) return null;
export function parseCellAddress(
cell: string
): { column: string; row: number } | null {
const match = cell.match(/^([A-Z]+)(\d+)$/)
if (!match) return null
return {
column: match[1],
row: parseInt(match[2], 10),
};
}
}
// Генерация уникального ID для элемента
export function generateElementId(): string {
return `element_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
return `element_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}

View File

@@ -1,7 +1,7 @@
import { ElementConstructor } from '@/components/TemplateManager/ElementConstructor'
import { Button } from '@/components/ui/button'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { Template, TemplateElement } from '@/types/template'
import { useTemplateContext } from '@/context/TemplateContext'
import { Template, TemplateElement } from '@/type/template'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'

View File

@@ -14,10 +14,10 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { useTemplateContext } from '@/context/TemplateContext'
import { getElementDefinition } from '@/lib/element-registry'
import { getLatestFileForTemplate } from '@/services/fileApiSevice'
import { Template, TemplateElement } from '@/types/template'
import { getLatestFileForTemplate } from '@/service/fileApiSevice'
import { Template, TemplateElement } from '@/type/template'
import {
ArrowLeft,
Download,

View File

@@ -1,5 +1,5 @@
import { TemplateManager } from '@/components/TemplateManager/TemplateManager'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { useTemplateContext } from '@/context/TemplateContext'
import { FC, useEffect } from 'react'
import { useParams } from 'react-router-dom'

View File

@@ -38,7 +38,7 @@ export interface ApiTemplate {
// Импортируем типы для преобразования
import { getDefaultLayoutSettings, migrateTemplateElement } from '../lib/utils'
import { Template, TemplateElement } from '../types/template'
import { Template, TemplateElement } from '../type/template'
// Утилитарные функции для преобразования данных