diff --git a/src/component/DualSpreadsheet/DualSpreadsheet.tsx b/src/component/DualSpreadsheet/DualSpreadsheet.tsx index b9f445e..cde3440 100644 --- a/src/component/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/component/DualSpreadsheet/DualSpreadsheet.tsx @@ -11,12 +11,17 @@ import { CachedCellData, COL_COUNT, DualSpreadsheetProps, + HighlightedCell, ROW_COUNT, SHEET_NAMES, SHEET_TYPES, SheetType, } from './types' -import { getColumnLabel } from './utils' +import { + convertReferencesToHighlights, + extractCellReferences, + getColumnLabel, +} from './utils' const DualSpreadsheet: FC = ({ templateData = {}, @@ -66,6 +71,9 @@ const DualSpreadsheet: FC = ({ const [baseFileData, setBaseFileData] = useState< Record> >({}) // Базовые данные файла + const [highlightedCells, setHighlightedCells] = useState( + [] + ) // Подсвеченные ячейки для формул // Мемоизируем начальное состояние ширин колонок const initialColumnWidths = useMemo( @@ -120,7 +128,7 @@ const DualSpreadsheet: FC = ({ const rowHeaderRefs = useRef>(initialRowHeaderRefs) - const activeCellInputRef = useRef(null) + const activeCellInputRef = useRef(null) const formulaBarRef = useRef(null) const initialTemplateValues = useRef>({}) @@ -294,6 +302,38 @@ const DualSpreadsheet: FC = ({ [revision, multiSheetEngine, initialTemplateValues, columnWidths] ) + // Функция для обновления подсветки ячеек при вводе формулы + const updateCellHighlights = useCallback( + (formula: string) => { + if (!formula.startsWith('=')) { + setHighlightedCells([]) + return + } + + try { + // Извлекаем ссылки на ячейки из формулы + const references = extractCellReferences(formula) + + // Конвертируем ссылки в подсвеченные ячейки + const highlights = convertReferencesToHighlights( + references, + activeSheet + ) + + setHighlightedCells(highlights) + + console.log('🎨 Подсветка ячеек для формулы:', formula, { + references, + highlights, + }) + } catch (error) { + console.error('Ошибка при обновлении подсветки ячеек:', error) + setHighlightedCells([]) + } + }, + [activeSheet] + ) + const formulaBarValue = useMemo(() => { if (isEditing) { return editingValue @@ -332,6 +372,16 @@ const DualSpreadsheet: FC = ({ } }, [editingValue, isEditing]) + // Обновление подсветки ячеек при вводе формулы + useEffect(() => { + if (isEditing) { + updateCellHighlights(editingValue) + } else { + // Очищаем подсветку когда не редактируем + setHighlightedCells([]) + } + }, [isEditing, editingValue, updateCellHighlights]) + const startEditing = useCallback(() => { if (!selectedCell) return const sheetName = SHEET_NAMES[activeSheet] @@ -642,10 +692,7 @@ const DualSpreadsheet: FC = ({ try { const latestFile = await getLatestFileForTemplate(templateId) if (latestFile) { - console.log( - '📁 Найден последний файл для шаблона:', - latestFile.filename - ) + console.log('📁 Найден последний файл для шаблона:', latestFile.name) const fileData = await getFileData(latestFile.id) // TODO: Пока getFileData возвращает пустые данные, используем templateData @@ -813,6 +860,7 @@ const DualSpreadsheet: FC = ({ activeSheet={activeSheet} isEditing={isEditing} editingValue={editingValue} + highlightedCells={highlightedCells} handleCellClick={handleCellClick} handleCellDoubleClick={handleCellDoubleClick} handleCellChange={handleCellChange} diff --git a/src/component/DualSpreadsheet/components/Cell.tsx b/src/component/DualSpreadsheet/components/Cell.tsx index 86775f6..853f878 100644 --- a/src/component/DualSpreadsheet/components/Cell.tsx +++ b/src/component/DualSpreadsheet/components/Cell.tsx @@ -23,13 +23,22 @@ export const Cell = memo( activeCellRef, style, // Добавляем style для react-window mergedCellInfo, + highlightColor, // Цвет подсветки для формул }: CellProps) => { // Если ячейка объединена, но не является основной, не рендерим её if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) { return null } - const className = `border border-border ${isModified && !mergedCellInfo?.isMerged ? 'bg-yellow-200' : ''}` + // Определяем классы CSS для подсветки и обычного состояния + let className = 'border border-border' + + // Приоритет стилей: подсветка формул > изменения > обычное состояние + if (highlightColor) { + className += ` ${highlightColor}` + } else if (isModified && !mergedCellInfo?.isMerged) { + className += ' bg-yellow-200' + } // Отладка стилей для измененных ячеек if (isModified) { @@ -38,6 +47,7 @@ export const Cell = memo( isModified, isSelected, displayValue, + highlightColor, }) } @@ -68,16 +78,27 @@ export const Cell = memo( // Рассчитываем общую высоту для объединенной ячейки const totalHeight = ROW_HEIGHT * rowSpan + // Определяем цвет фона для объединенной ячейки + let backgroundColor = 'white' + if (highlightColor) { + // Если есть подсветка формулы, используем её + const bgMatch = highlightColor.match(/bg-(\w+)-(\d+)/) + if (bgMatch) { + backgroundColor = `var(--${bgMatch[1]}-${bgMatch[2]})` + } + } else if (isSelected) { + backgroundColor = 'hsl(var(--accent))' + } else if (isModified) { + backgroundColor = + '#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */ + } + cellStyle = { ...cellStyle, width: totalWidth, height: totalHeight, zIndex: 10, // Поднимаем выше обычных ячеек - backgroundColor: isSelected - ? 'hsl(var(--accent))' - : isModified - ? '#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */ - : 'white', + backgroundColor, } } diff --git a/src/component/DualSpreadsheet/components/CellRenderer.tsx b/src/component/DualSpreadsheet/components/CellRenderer.tsx index 86c7510..b9cbf23 100644 --- a/src/component/DualSpreadsheet/components/CellRenderer.tsx +++ b/src/component/DualSpreadsheet/components/CellRenderer.tsx @@ -18,6 +18,7 @@ export const CellRenderer = ({ editingValue, cachedData, mergedCells, + highlightedCells, handleCellClick, handleCellDoubleClick, handleCellChange, @@ -40,6 +41,16 @@ export const CellRenderer = ({ ? '' // Не показывать вычисленное значение во время редактирования : cellData?.displayValue || '' + // Проверяем, подсвечена ли эта ячейка + const currentSheetName = sheetType === 'report' ? 'L' : 'R' + const highlightedCell = highlightedCells.find( + highlight => + highlight.sheet === currentSheetName && + highlight.row === rowIndex && + highlight.col === columnIndex + ) + const highlightColor = highlightedCell?.color + // Отладка для измененных ячеек if (cellData?.isModified) { console.log( @@ -49,6 +60,7 @@ export const CellRenderer = ({ isModified: cellData.isModified, displayValue, rawValue, + highlightColor, } ) } @@ -91,6 +103,7 @@ export const CellRenderer = ({ setActiveSheet={setActiveSheet} activeCellRef={activeCellInputRef} mergedCellInfo={mergedCellProps} + highlightColor={highlightColor} /> ) } diff --git a/src/component/DualSpreadsheet/components/Spreadsheet.tsx b/src/component/DualSpreadsheet/components/Spreadsheet.tsx index d7b478d..bbc9157 100644 --- a/src/component/DualSpreadsheet/components/Spreadsheet.tsx +++ b/src/component/DualSpreadsheet/components/Spreadsheet.tsx @@ -14,6 +14,7 @@ export const Spreadsheet = ({ activeSheet, isEditing, editingValue, + highlightedCells, handleCellClick, handleCellDoubleClick, handleCellChange, @@ -41,6 +42,7 @@ export const Spreadsheet = ({ editingValue, cachedData, mergedCells, + highlightedCells, handleCellClick, handleCellDoubleClick, handleCellChange, @@ -78,7 +80,9 @@ export const Spreadsheet = ({ onClick={() => setActiveSheet(sheetType)} >
(containerRefs.current[sheetType] = el)} + ref={el => { + containerRefs.current[sheetType] = el + }} className="flex h-full flex-col overflow-hidden border bg-card shadow-sm" onKeyDown={handleKeyDown} tabIndex={0} @@ -102,7 +106,9 @@ export const Spreadsheet = ({
(rowHeaderRefs.current[sheetType] = el)} + ref={el => { + rowHeaderRefs.current[sheetType] = el + }} className="overflow-y-hidden" style={{ height: gridSize.height }} > @@ -126,7 +132,9 @@ export const Spreadsheet = ({
{/* Column Headers */}
(headerRefs.current[sheetType] = el)} + ref={el => { + headerRefs.current[sheetType] = el + }} className="column-headers overflow-x-hidden" >
@@ -171,9 +179,9 @@ export const Spreadsheet = ({ {/* Grid */}
- (gridRefs.current[sheetType] = el) - } + ref={(el: VariableSizeGrid | null) => { + gridRefs.current[sheetType] = el + }} columnCount={COL_COUNT} rowCount={ROW_COUNT} columnWidth={(index: number) => widths[index]} diff --git a/src/component/DualSpreadsheet/types.ts b/src/component/DualSpreadsheet/types.ts index efa8390..979f811 100644 --- a/src/component/DualSpreadsheet/types.ts +++ b/src/component/DualSpreadsheet/types.ts @@ -16,6 +16,14 @@ export type SheetType = keyof typeof SHEET_NAMES // Добавляем массив типов листов для итерации export const SHEET_TYPES: SheetType[] = ['report', 'calculations'] +// Интерфейс для подсвеченной ячейки +export interface HighlightedCell { + sheet: string // нормализованное имя листа ('L' или 'R') + row: number + col: number + color: string // класс CSS для цвета подсветки +} + // Кэшированные данные ячеек для оптимизации export interface CachedCellData { displayValue: string @@ -59,6 +67,7 @@ export interface CellProps { rowSpan?: number colSpan?: number } + highlightColor?: string // Цвет подсветки для формул } export interface FormulaBarProps { @@ -91,6 +100,7 @@ export interface CellRendererData { editingValue: string cachedData: Record mergedCells?: MergedCell[] + highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки handleCellClick: (row: number, col: number) => void handleCellDoubleClick: (row: number, col: number) => void handleCellChange: (newValue: string) => void @@ -115,6 +125,7 @@ export interface SpreadsheetProps { activeSheet: SheetType isEditing: boolean editingValue: string + highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки handleCellClick: (row: number, col: number) => void handleCellDoubleClick: (row: number, col: number) => void handleCellChange: (newValue: string) => void diff --git a/src/component/DualSpreadsheet/utils.ts b/src/component/DualSpreadsheet/utils.ts index 8f92fd9..25da080 100644 --- a/src/component/DualSpreadsheet/utils.ts +++ b/src/component/DualSpreadsheet/utils.ts @@ -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 +} diff --git a/src/component/TemplateManager/HeaderBar.tsx b/src/component/TemplateManager/HeaderBar.tsx index 600405b..5f4c546 100644 --- a/src/component/TemplateManager/HeaderBar.tsx +++ b/src/component/TemplateManager/HeaderBar.tsx @@ -36,7 +36,7 @@ export const HeaderBar: React.FC = ({ template, onBack }) => { className="flex items-center gap-1 hover:bg-muted" > - Элементы интерфейса + Редактор интерфейса
- -

Редактор шаблонов

+ +

Редактор интерфейса

{selectedTemplate.name}