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

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

@@ -43,6 +43,7 @@
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@types/node": "^24.0.15",
"@types/react": "^18.0.37",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.0.11",

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,18 +46,18 @@ 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 cols = 4 // Количество колонок для автоматического размещения
const elementWidth = 300
const elementHeight = 80
const padding = 50
const col = order % cols;
const row = Math.floor(order / cols);
const col = order % cols
const row = Math.floor(order / cols)
return {
x: col * (elementWidth + padding) + padding,
@@ -60,7 +65,7 @@ export function generateDefaultLayout(order: number = 0): ElementLayout {
width: elementWidth,
height: elementHeight,
zIndex: 1,
};
}
}
// Автоматическое размещение элементов на холсте
@@ -68,21 +73,21 @@ export function autoArrangeElements(
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 col = index % cols
const row = Math.floor(index / cols)
const x = col * (elementWidth + padding) + padding;
const y = row * (elementHeight + padding) + padding;
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,
@@ -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;
attempts++
if (attempts > maxAttempts) break
const testPosition = { ...newElement, x, y };
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'
// Утилитарные функции для преобразования данных

View File

@@ -8,12 +8,13 @@
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"outDir": "./dist/",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/*": ["./src/*"]
}
},
"ts-node": {

View File

@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve('src'),
'@': path.resolve(__dirname, 'src'),
'@widgets': path.resolve('src/widgets'),
'@features': path.resolve('src/features'),
'@entities': path.resolve('src/entities'),

View File

@@ -840,6 +840,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/node@^24.0.15":
version "24.0.15"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.15.tgz#f34fbc973e7d64217106e0c59ed8761e6b51381e"
integrity sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==
dependencies:
undici-types "~7.8.0"
"@types/prop-types@*":
version "15.7.15"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
@@ -1156,11 +1163,6 @@ acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
adler-32@~1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -1418,14 +1420,6 @@ caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001726:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85"
integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==
cfb@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
dependencies:
adler-32 "~1.3.0"
crc-32 "~1.2.0"
chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -1477,11 +1471,6 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
codepage@~1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -1514,11 +1503,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
crc-32@~1.2.0, crc-32@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
cross-spawn@^7.0.2, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -2113,11 +2097,6 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
frac@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
fraction.js@^4.3.7:
version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@@ -3563,13 +3542,6 @@ source-map-js@^1.2.1:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
ssf@~0.11.2:
version "0.11.2"
resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c"
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
dependencies:
frac "~1.1.2"
stable-hash-x@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9"
@@ -3896,6 +3868,11 @@ unbox-primitive@^1.1.0:
has-symbols "^1.1.0"
which-boxed-primitive "^1.1.1"
undici-types@~7.8.0:
version "7.8.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
unrs-resolver@^1.7.11:
version "1.11.1"
resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9"
@@ -4059,21 +4036,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
wmf@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da"
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
word@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961"
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@@ -4097,19 +4064,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
xlsx@^0.18.5:
version "0.18.5"
resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0"
integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
dependencies:
adler-32 "~1.3.0"
cfb "~1.2.1"
codepage "~1.15.0"
crc-32 "~1.2.1"
ssf "~0.11.2"
wmf "~1.0.1"
word "~0.3.0"
yaml@^2.3.4:
version "2.8.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6"