переименования папок
This commit is contained in:
@@ -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]
|
||||
}
|
||||
|
||||
133
src/lib/utils.ts
133
src/lib/utils.ts
@@ -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)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user