подстветка ячеек
This commit is contained in:
@@ -11,12 +11,17 @@ import {
|
|||||||
CachedCellData,
|
CachedCellData,
|
||||||
COL_COUNT,
|
COL_COUNT,
|
||||||
DualSpreadsheetProps,
|
DualSpreadsheetProps,
|
||||||
|
HighlightedCell,
|
||||||
ROW_COUNT,
|
ROW_COUNT,
|
||||||
SHEET_NAMES,
|
SHEET_NAMES,
|
||||||
SHEET_TYPES,
|
SHEET_TYPES,
|
||||||
SheetType,
|
SheetType,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { getColumnLabel } from './utils'
|
import {
|
||||||
|
convertReferencesToHighlights,
|
||||||
|
extractCellReferences,
|
||||||
|
getColumnLabel,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||||
templateData = {},
|
templateData = {},
|
||||||
@@ -66,6 +71,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const [baseFileData, setBaseFileData] = useState<
|
const [baseFileData, setBaseFileData] = useState<
|
||||||
Record<string, Record<string, any>>
|
Record<string, Record<string, any>>
|
||||||
>({}) // Базовые данные файла
|
>({}) // Базовые данные файла
|
||||||
|
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
|
||||||
|
[]
|
||||||
|
) // Подсвеченные ячейки для формул
|
||||||
|
|
||||||
// Мемоизируем начальное состояние ширин колонок
|
// Мемоизируем начальное состояние ширин колонок
|
||||||
const initialColumnWidths = useMemo(
|
const initialColumnWidths = useMemo(
|
||||||
@@ -120,7 +128,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const rowHeaderRefs =
|
const rowHeaderRefs =
|
||||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
|
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
|
||||||
|
|
||||||
const activeCellInputRef = useRef<HTMLInputElement>(null)
|
const activeCellInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
||||||
const initialTemplateValues = useRef<Record<string, string>>({})
|
const initialTemplateValues = useRef<Record<string, string>>({})
|
||||||
|
|
||||||
@@ -294,6 +302,38 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
[revision, multiSheetEngine, initialTemplateValues, columnWidths]
|
[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(() => {
|
const formulaBarValue = useMemo(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return editingValue
|
return editingValue
|
||||||
@@ -332,6 +372,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
}
|
}
|
||||||
}, [editingValue, isEditing])
|
}, [editingValue, isEditing])
|
||||||
|
|
||||||
|
// Обновление подсветки ячеек при вводе формулы
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
updateCellHighlights(editingValue)
|
||||||
|
} else {
|
||||||
|
// Очищаем подсветку когда не редактируем
|
||||||
|
setHighlightedCells([])
|
||||||
|
}
|
||||||
|
}, [isEditing, editingValue, updateCellHighlights])
|
||||||
|
|
||||||
const startEditing = useCallback(() => {
|
const startEditing = useCallback(() => {
|
||||||
if (!selectedCell) return
|
if (!selectedCell) return
|
||||||
const sheetName = SHEET_NAMES[activeSheet]
|
const sheetName = SHEET_NAMES[activeSheet]
|
||||||
@@ -642,10 +692,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
try {
|
try {
|
||||||
const latestFile = await getLatestFileForTemplate(templateId)
|
const latestFile = await getLatestFileForTemplate(templateId)
|
||||||
if (latestFile) {
|
if (latestFile) {
|
||||||
console.log(
|
console.log('📁 Найден последний файл для шаблона:', latestFile.name)
|
||||||
'📁 Найден последний файл для шаблона:',
|
|
||||||
latestFile.filename
|
|
||||||
)
|
|
||||||
const fileData = await getFileData(latestFile.id)
|
const fileData = await getFileData(latestFile.id)
|
||||||
|
|
||||||
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
||||||
@@ -813,6 +860,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
activeSheet={activeSheet}
|
activeSheet={activeSheet}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
editingValue={editingValue}
|
editingValue={editingValue}
|
||||||
|
highlightedCells={highlightedCells}
|
||||||
handleCellClick={handleCellClick}
|
handleCellClick={handleCellClick}
|
||||||
handleCellDoubleClick={handleCellDoubleClick}
|
handleCellDoubleClick={handleCellDoubleClick}
|
||||||
handleCellChange={handleCellChange}
|
handleCellChange={handleCellChange}
|
||||||
|
|||||||
@@ -23,13 +23,22 @@ export const Cell = memo(
|
|||||||
activeCellRef,
|
activeCellRef,
|
||||||
style, // Добавляем style для react-window
|
style, // Добавляем style для react-window
|
||||||
mergedCellInfo,
|
mergedCellInfo,
|
||||||
|
highlightColor, // Цвет подсветки для формул
|
||||||
}: CellProps) => {
|
}: CellProps) => {
|
||||||
// Если ячейка объединена, но не является основной, не рендерим её
|
// Если ячейка объединена, но не является основной, не рендерим её
|
||||||
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
||||||
return null
|
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) {
|
if (isModified) {
|
||||||
@@ -38,6 +47,7 @@ export const Cell = memo(
|
|||||||
isModified,
|
isModified,
|
||||||
isSelected,
|
isSelected,
|
||||||
displayValue,
|
displayValue,
|
||||||
|
highlightColor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,16 +78,27 @@ export const Cell = memo(
|
|||||||
// Рассчитываем общую высоту для объединенной ячейки
|
// Рассчитываем общую высоту для объединенной ячейки
|
||||||
const totalHeight = ROW_HEIGHT * rowSpan
|
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 = {
|
||||||
...cellStyle,
|
...cellStyle,
|
||||||
width: totalWidth,
|
width: totalWidth,
|
||||||
height: totalHeight,
|
height: totalHeight,
|
||||||
zIndex: 10, // Поднимаем выше обычных ячеек
|
zIndex: 10, // Поднимаем выше обычных ячеек
|
||||||
backgroundColor: isSelected
|
backgroundColor,
|
||||||
? 'hsl(var(--accent))'
|
|
||||||
: isModified
|
|
||||||
? '#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */
|
|
||||||
: 'white',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const CellRenderer = ({
|
|||||||
editingValue,
|
editingValue,
|
||||||
cachedData,
|
cachedData,
|
||||||
mergedCells,
|
mergedCells,
|
||||||
|
highlightedCells,
|
||||||
handleCellClick,
|
handleCellClick,
|
||||||
handleCellDoubleClick,
|
handleCellDoubleClick,
|
||||||
handleCellChange,
|
handleCellChange,
|
||||||
@@ -40,6 +41,16 @@ export const CellRenderer = ({
|
|||||||
? '' // Не показывать вычисленное значение во время редактирования
|
? '' // Не показывать вычисленное значение во время редактирования
|
||||||
: cellData?.displayValue || ''
|
: 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) {
|
if (cellData?.isModified) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -49,6 +60,7 @@ export const CellRenderer = ({
|
|||||||
isModified: cellData.isModified,
|
isModified: cellData.isModified,
|
||||||
displayValue,
|
displayValue,
|
||||||
rawValue,
|
rawValue,
|
||||||
|
highlightColor,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -91,6 +103,7 @@ export const CellRenderer = ({
|
|||||||
setActiveSheet={setActiveSheet}
|
setActiveSheet={setActiveSheet}
|
||||||
activeCellRef={activeCellInputRef}
|
activeCellRef={activeCellInputRef}
|
||||||
mergedCellInfo={mergedCellProps}
|
mergedCellInfo={mergedCellProps}
|
||||||
|
highlightColor={highlightColor}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const Spreadsheet = ({
|
|||||||
activeSheet,
|
activeSheet,
|
||||||
isEditing,
|
isEditing,
|
||||||
editingValue,
|
editingValue,
|
||||||
|
highlightedCells,
|
||||||
handleCellClick,
|
handleCellClick,
|
||||||
handleCellDoubleClick,
|
handleCellDoubleClick,
|
||||||
handleCellChange,
|
handleCellChange,
|
||||||
@@ -41,6 +42,7 @@ export const Spreadsheet = ({
|
|||||||
editingValue,
|
editingValue,
|
||||||
cachedData,
|
cachedData,
|
||||||
mergedCells,
|
mergedCells,
|
||||||
|
highlightedCells,
|
||||||
handleCellClick,
|
handleCellClick,
|
||||||
handleCellDoubleClick,
|
handleCellDoubleClick,
|
||||||
handleCellChange,
|
handleCellChange,
|
||||||
@@ -78,7 +80,9 @@ export const Spreadsheet = ({
|
|||||||
onClick={() => setActiveSheet(sheetType)}
|
onClick={() => setActiveSheet(sheetType)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={el => (containerRefs.current[sheetType] = el)}
|
ref={el => {
|
||||||
|
containerRefs.current[sheetType] = el
|
||||||
|
}}
|
||||||
className="flex h-full flex-col overflow-hidden border bg-card shadow-sm"
|
className="flex h-full flex-col overflow-hidden border bg-card shadow-sm"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -102,7 +106,9 @@ export const Spreadsheet = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={el => (rowHeaderRefs.current[sheetType] = el)}
|
ref={el => {
|
||||||
|
rowHeaderRefs.current[sheetType] = el
|
||||||
|
}}
|
||||||
className="overflow-y-hidden"
|
className="overflow-y-hidden"
|
||||||
style={{ height: gridSize.height }}
|
style={{ height: gridSize.height }}
|
||||||
>
|
>
|
||||||
@@ -126,7 +132,9 @@ export const Spreadsheet = ({
|
|||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{/* Column Headers */}
|
{/* Column Headers */}
|
||||||
<div
|
<div
|
||||||
ref={el => (headerRefs.current[sheetType] = el)}
|
ref={el => {
|
||||||
|
headerRefs.current[sheetType] = el
|
||||||
|
}}
|
||||||
className="column-headers overflow-x-hidden"
|
className="column-headers overflow-x-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -171,9 +179,9 @@ export const Spreadsheet = ({
|
|||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
<VariableSizeGrid
|
<VariableSizeGrid
|
||||||
ref={(el: VariableSizeGrid | null) =>
|
ref={(el: VariableSizeGrid | null) => {
|
||||||
(gridRefs.current[sheetType] = el)
|
gridRefs.current[sheetType] = el
|
||||||
}
|
}}
|
||||||
columnCount={COL_COUNT}
|
columnCount={COL_COUNT}
|
||||||
rowCount={ROW_COUNT}
|
rowCount={ROW_COUNT}
|
||||||
columnWidth={(index: number) => widths[index]}
|
columnWidth={(index: number) => widths[index]}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export type SheetType = keyof typeof SHEET_NAMES
|
|||||||
// Добавляем массив типов листов для итерации
|
// Добавляем массив типов листов для итерации
|
||||||
export const SHEET_TYPES: SheetType[] = ['report', 'calculations']
|
export const SHEET_TYPES: SheetType[] = ['report', 'calculations']
|
||||||
|
|
||||||
|
// Интерфейс для подсвеченной ячейки
|
||||||
|
export interface HighlightedCell {
|
||||||
|
sheet: string // нормализованное имя листа ('L' или 'R')
|
||||||
|
row: number
|
||||||
|
col: number
|
||||||
|
color: string // класс CSS для цвета подсветки
|
||||||
|
}
|
||||||
|
|
||||||
// Кэшированные данные ячеек для оптимизации
|
// Кэшированные данные ячеек для оптимизации
|
||||||
export interface CachedCellData {
|
export interface CachedCellData {
|
||||||
displayValue: string
|
displayValue: string
|
||||||
@@ -59,6 +67,7 @@ export interface CellProps {
|
|||||||
rowSpan?: number
|
rowSpan?: number
|
||||||
colSpan?: number
|
colSpan?: number
|
||||||
}
|
}
|
||||||
|
highlightColor?: string // Цвет подсветки для формул
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormulaBarProps {
|
export interface FormulaBarProps {
|
||||||
@@ -91,6 +100,7 @@ export interface CellRendererData {
|
|||||||
editingValue: string
|
editingValue: string
|
||||||
cachedData: Record<string, CachedCellData>
|
cachedData: Record<string, CachedCellData>
|
||||||
mergedCells?: MergedCell[]
|
mergedCells?: MergedCell[]
|
||||||
|
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
|
||||||
handleCellClick: (row: number, col: number) => void
|
handleCellClick: (row: number, col: number) => void
|
||||||
handleCellDoubleClick: (row: number, col: number) => void
|
handleCellDoubleClick: (row: number, col: number) => void
|
||||||
handleCellChange: (newValue: string) => void
|
handleCellChange: (newValue: string) => void
|
||||||
@@ -115,6 +125,7 @@ export interface SpreadsheetProps {
|
|||||||
activeSheet: SheetType
|
activeSheet: SheetType
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
editingValue: string
|
editingValue: string
|
||||||
|
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
|
||||||
handleCellClick: (row: number, col: number) => void
|
handleCellClick: (row: number, col: number) => void
|
||||||
handleCellDoubleClick: (row: number, col: number) => void
|
handleCellDoubleClick: (row: number, col: number) => void
|
||||||
handleCellChange: (newValue: string) => void
|
handleCellChange: (newValue: string) => void
|
||||||
|
|||||||
@@ -1,42 +1,289 @@
|
|||||||
import { cellAddressToCoordinates, getColumnLabel } from '@/lib/cell-utils'
|
|
||||||
import { MergedCell } from '@/type/template'
|
import { MergedCell } from '@/type/template'
|
||||||
import { MergedCellInfo } from './types'
|
import { MergedCellInfo } from './types'
|
||||||
|
|
||||||
// --- Helper Functions ---
|
export function getColumnLabel(index: number): string {
|
||||||
export { getColumnLabel }
|
let label = ''
|
||||||
|
let col = index
|
||||||
|
while (col >= 0) {
|
||||||
|
label = String.fromCharCode(65 + (col % 26)) + label
|
||||||
|
col = Math.floor(col / 26) - 1
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
// Вспомогательная функция для совместимости с существующим кодом
|
export function findMergedCellInfo(
|
||||||
export const cellAddressToCoords = cellAddressToCoordinates
|
|
||||||
|
|
||||||
// Функция для проверки, является ли ячейка частью объединенной группы
|
|
||||||
export const findMergedCellInfo = (
|
|
||||||
row: number,
|
row: number,
|
||||||
col: number,
|
col: number,
|
||||||
mergedCells: MergedCell[]
|
mergedCells: MergedCell[]
|
||||||
): MergedCellInfo => {
|
): MergedCellInfo {
|
||||||
for (const merged of mergedCells) {
|
for (const mergedCell of mergedCells) {
|
||||||
const fromCoords = cellAddressToCoordinates(merged.from)
|
const { startRow, startCol, endRow, endCol } = mergedCell
|
||||||
const toCoords = cellAddressToCoordinates(merged.to)
|
|
||||||
|
|
||||||
// Проверяем, попадает ли текущая ячейка в диапазон объединения
|
// Проверяем, попадает ли ячейка в диапазон объединенных ячеек
|
||||||
if (
|
if (row >= startRow && row <= endRow && col >= startCol && col <= endCol) {
|
||||||
row >= fromCoords.row &&
|
// Это главная ячейка (левая верхняя)
|
||||||
row <= toCoords.row &&
|
const isMainCell = row === startRow && col === startCol
|
||||||
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
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMerged: true,
|
isMerged: true,
|
||||||
isMainCell,
|
isMainCell,
|
||||||
mergedCell: merged,
|
mergedCell,
|
||||||
spans: { rowSpan, colSpan },
|
spans: {
|
||||||
|
rowSpan: endRow - startRow + 1,
|
||||||
|
colSpan: endCol - startCol + 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isMerged: false, isMainCell: false }
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
|||||||
className="flex items-center gap-1 hover:bg-muted"
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Wrench className="h-4 w-4" />
|
<Wrench className="h-4 w-4" />
|
||||||
<span className="text-sm">Элементы интерфейса</span>
|
<span className="text-sm">Редактор интерфейса</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ElementConstructor } from '@/component/TemplateManager/ElementConstruct
|
|||||||
import { Button } from '@/component/ui/button'
|
import { Button } from '@/component/ui/button'
|
||||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||||
import { Template, TemplateElement } from '@/type/template'
|
import { Template, TemplateElement } from '@/type/template'
|
||||||
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ export const ElementsCreation: FC = () => {
|
|||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Settings className="h-5 w-5" />
|
<Wrench className="h-5 w-5" />
|
||||||
<h1 className="text-lg font-semibold">Редактор шаблонов</h1>
|
<h1 className="text-lg font-semibold">Редактор интерфейса</h1>
|
||||||
<span className="text-sm text-muted-foreground">—</span>
|
<span className="text-sm text-muted-foreground">—</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{selectedTemplate.name}
|
{selectedTemplate.name}
|
||||||
|
|||||||
Reference in New Issue
Block a user