подстветка ячеек

This commit is contained in:
2025-07-22 05:35:04 +03:00
parent 555505e09e
commit faad68cdc0
8 changed files with 394 additions and 46 deletions

View File

@@ -1,42 +1,289 @@
import { cellAddressToCoordinates, getColumnLabel } from '@/lib/cell-utils'
import { MergedCell } from '@/type/template'
import { MergedCellInfo } from './types'
// --- Helper Functions ---
export { getColumnLabel }
export function getColumnLabel(index: number): string {
let label = ''
let col = index
while (col >= 0) {
label = String.fromCharCode(65 + (col % 26)) + label
col = Math.floor(col / 26) - 1
}
return label
}
// Вспомогательная функция для совместимости с существующим кодом
export const cellAddressToCoords = cellAddressToCoordinates
// Функция для проверки, является ли ячейка частью объединенной группы
export const findMergedCellInfo = (
export function findMergedCellInfo(
row: number,
col: number,
mergedCells: MergedCell[]
): MergedCellInfo => {
for (const merged of mergedCells) {
const fromCoords = cellAddressToCoordinates(merged.from)
const toCoords = cellAddressToCoordinates(merged.to)
): MergedCellInfo {
for (const mergedCell of mergedCells) {
const { startRow, startCol, endRow, endCol } = mergedCell
// Проверяем, попадает ли текущая ячейка в диапазон объединения
if (
row >= fromCoords.row &&
row <= toCoords.row &&
col >= fromCoords.col &&
col <= toCoords.col
) {
const isMainCell = row === fromCoords.row && col === fromCoords.col
const rowSpan = toCoords.row - fromCoords.row + 1
const colSpan = toCoords.col - fromCoords.col + 1
// Проверяем, попадает ли ячейка в диапазон объединенных ячеек
if (row >= startRow && row <= endRow && col >= startCol && col <= endCol) {
// Это главная ячейка (левая верхняя)
const isMainCell = row === startRow && col === startCol
return {
isMerged: true,
isMainCell,
mergedCell: merged,
spans: { rowSpan, colSpan },
mergedCell,
spans: {
rowSpan: endRow - startRow + 1,
colSpan: endCol - startCol + 1,
},
}
}
}
return { isMerged: false, isMainCell: false }
}
// Регулярные выражения для разных типов ссылок на ячейки
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g // SheetName!A1
const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g // A1, B2, etc.
const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // A1:B2, D5:D7, etc.
const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // SheetName!A1:B2
// Интерфейс для ссылки на ячейку
export interface CellReference {
sheet: string | null // null для локальных ссылок
address: string // например "A1" или "B2:C3"
isRange: boolean
startRow: number
startCol: number
endRow: number
endCol: number
}
// Парсит адрес ячейки типа "A1" в координаты
export function parseCellAddress(
address: string
): { row: number; col: number } | null {
const match = address.match(/^([A-Z]+)(\d+)$/)
if (!match) return null
const column = match[1]
const row = parseInt(match[2], 10)
// Конвертируем колонку в индекс
let colIndex = 0
for (let i = 0; i < column.length; i++) {
colIndex = colIndex * 26 + (column.charCodeAt(i) - 65 + 1)
}
colIndex -= 1 // Конвертируем в 0-based индекс
return { row: row - 1, col: colIndex } // Конвертируем в 0-based индексы
}
// Парсит диапазон ячеек типа "A1:B2"
export function parseCellRange(range: string): {
startRow: number
startCol: number
endRow: number
endCol: number
} | null {
const parts = range.split(':')
if (parts.length !== 2) return null
const start = parseCellAddress(parts[0])
const end = parseCellAddress(parts[1])
if (!start || !end) return null
return {
startRow: Math.min(start.row, end.row),
startCol: Math.min(start.col, end.col),
endRow: Math.max(start.row, end.row),
endCol: Math.max(start.col, end.col),
}
}
// Извлекает все ссылки на ячейки из формулы
export function extractCellReferences(formula: string): CellReference[] {
if (!formula.startsWith('=')) return []
const expr = formula.substring(1) // Убираем знак =
const references: CellReference[] = []
// Функция для проверки, не является ли ссылка частью имени функции
const isFunction = (ref: string): boolean => {
const commonFunctions = [
'SUM',
'AVERAGE',
'COUNT',
'MAX',
'MIN',
'IF',
'AND',
'OR',
'NOT',
'СУММ',
'СРЕДНЕЕ',
'СЧЁТ',
'МАКС',
'МИН',
'ЕСЛИ',
]
return commonFunctions.includes(ref.toUpperCase())
}
// 1. Обрабатываем квалифицированные диапазоны (SheetName!A1:B2)
const qualifiedRangeMatches = expr.match(QUALIFIED_RANGE_RE)
if (qualifiedRangeMatches) {
for (const match of qualifiedRangeMatches) {
const [sheetPart, rangePart] = match.split('!')
const rangeCoords = parseCellRange(rangePart)
if (rangeCoords) {
references.push({
sheet: sheetPart,
address: rangePart,
isRange: true,
...rangeCoords,
})
}
}
}
// 2. Обрабатываем локальные диапазоны (A1:B2)
let cleanExpr = expr
// Удаляем уже обработанные квалифицированные диапазоны
cleanExpr = cleanExpr.replace(QUALIFIED_RANGE_RE, '')
const rangeMatches = cleanExpr.match(RANGE_RE)
if (rangeMatches) {
for (const match of rangeMatches) {
const rangeCoords = parseCellRange(match)
if (rangeCoords) {
references.push({
sheet: null,
address: match,
isRange: true,
...rangeCoords,
})
}
}
}
// 3. Обрабатываем квалифицированные одиночные ссылки (SheetName!A1)
const qualifiedMatches = expr.match(QUALIFIED_REF_RE)
if (qualifiedMatches) {
for (const match of qualifiedMatches) {
const [sheetPart, cellPart] = match.split('!')
const cellCoords = parseCellAddress(cellPart)
if (cellCoords) {
references.push({
sheet: sheetPart,
address: cellPart,
isRange: false,
startRow: cellCoords.row,
startCol: cellCoords.col,
endRow: cellCoords.row,
endCol: cellCoords.col,
})
}
}
}
// 4. Обрабатываем локальные одиночные ссылки (A1, B2)
// Удаляем уже обработанные квалифицированные ссылки и диапазоны
cleanExpr = expr
.replace(QUALIFIED_RANGE_RE, '')
.replace(QUALIFIED_REF_RE, '')
.replace(RANGE_RE, '')
const localMatches = cleanExpr.match(LOCAL_REF_RE)
if (localMatches) {
for (const match of localMatches) {
// Пропускаем функции
if (isFunction(match)) continue
const cellCoords = parseCellAddress(match)
if (cellCoords) {
references.push({
sheet: null,
address: match,
isRange: false,
startRow: cellCoords.row,
startCol: cellCoords.col,
endRow: cellCoords.row,
endCol: cellCoords.col,
})
}
}
}
return references
}
// Интерфейс для подсвеченной ячейки
export interface HighlightedCell {
sheet: string // нормализованное имя листа ('L' или 'R')
row: number
col: number
color: string // класс CSS для цвета подсветки
}
// Конвертирует ссылки на ячейки в подсвеченные ячейки
export function convertReferencesToHighlights(
references: CellReference[],
currentSheetType: 'report' | 'calculations'
): HighlightedCell[] {
const highlights: HighlightedCell[] = []
const colors = [
'bg-blue-200 border-blue-400',
'bg-green-200 border-green-400',
'bg-purple-200 border-purple-400',
'bg-red-200 border-red-400',
'bg-yellow-200 border-yellow-400',
'bg-indigo-200 border-indigo-400',
'bg-pink-200 border-pink-400',
'bg-orange-200 border-orange-400',
]
let colorIndex = 0
for (const ref of references) {
let sheetName: string
// Определяем имя листа
if (ref.sheet) {
// Квалифицированная ссылка
if (ref.sheet === 'L' || ref.sheet.toLowerCase() === 'report') {
sheetName = 'L'
} else if (
ref.sheet === 'R' ||
ref.sheet.toLowerCase() === 'calculations'
) {
sheetName = 'R'
} else {
// Неизвестный лист, пропускаем
continue
}
} else {
// Локальная ссылка - используем текущий лист
sheetName = currentSheetType === 'report' ? 'L' : 'R'
}
const color = colors[colorIndex % colors.length]
colorIndex++
// Добавляем все ячейки в диапазоне
for (let row = ref.startRow; row <= ref.endRow; row++) {
for (let col = ref.startCol; col <= ref.endCol; col++) {
highlights.push({
sheet: sheetName,
row,
col,
color,
})
}
}
}
return highlights
}