178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
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))
|
|
}
|
|
|
|
// Миграция элементов из старого формата в новый
|
|
export function migrateTemplateElement(element: any): TemplateElement {
|
|
// Если элемент уже в новом формате
|
|
if (element.targetCells && Array.isArray(element.targetCells)) {
|
|
return element as TemplateElement
|
|
}
|
|
|
|
// Миграция из старого формата
|
|
const targetCells: CellTarget[] = []
|
|
|
|
if (element.targetCell) {
|
|
// Парсим старый формат targetCell (например "A1", "B5")
|
|
const cellMatch = element.targetCell.match(/^([A-Z]+)(\d+)$/)
|
|
if (cellMatch) {
|
|
targetCells.push({
|
|
sheet: 'Report', // По умолчанию используем Report
|
|
cell: element.targetCell,
|
|
displayName: `Ячейка ${element.targetCell}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
...element,
|
|
targetCells,
|
|
order: element.order || 0,
|
|
layout: element.layout || generateDefaultLayout(element.order || 0),
|
|
} as TemplateElement
|
|
}
|
|
|
|
// Получение настроек отображения по умолчанию
|
|
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)
|
|
|
|
return {
|
|
x: col * (elementWidth + padding) + padding,
|
|
y: row * (elementHeight + padding) + padding,
|
|
width: elementWidth,
|
|
height: elementHeight,
|
|
zIndex: 1,
|
|
}
|
|
}
|
|
|
|
// Автоматическое размещение элементов на холсте
|
|
export function autoArrangeElements(
|
|
elements: TemplateElement[],
|
|
gridSize: number = 20
|
|
): TemplateElement[] {
|
|
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 snappedX = Math.round(x / gridSize) * gridSize
|
|
const snappedY = Math.round(y / gridSize) * gridSize
|
|
|
|
return {
|
|
...element,
|
|
layout: {
|
|
x: snappedX,
|
|
y: snappedY,
|
|
width: elementWidth,
|
|
height: elementHeight,
|
|
zIndex: 1,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
// Проверка пересечения элементов
|
|
export function checkElementOverlap(
|
|
element1: ElementLayout,
|
|
element2: ElementLayout,
|
|
threshold: number = 5
|
|
): boolean {
|
|
return !(
|
|
element1.x + element1.width + threshold < element2.x ||
|
|
element2.x + element2.width + threshold < element1.x ||
|
|
element1.y + element1.height + threshold < element2.y ||
|
|
element2.y + element2.height + threshold < 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 /^[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
|
|
|
|
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)}`
|
|
}
|