подстветка ячеек
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user