объединенные ячейки

This commit is contained in:
2025-07-18 14:34:29 +03:00
parent 8b67db832c
commit 7ba38b3284
2 changed files with 142 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import {
} from 'react' } from 'react'
import { VariableSizeGrid } from 'react-window' import { VariableSizeGrid } from 'react-window'
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine' import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
import { MergedCell } from '../../types/template'
const ROW_COUNT = 90 const ROW_COUNT = 90
const COL_COUNT = 20 const COL_COUNT = 20
@@ -26,9 +27,59 @@ type SheetType = keyof typeof SHEET_NAMES
// --- Helper Functions --- // --- Helper Functions ---
const getColumnLabel = (index: number) => String.fromCharCode(65 + index) const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
// Функция для преобразования адреса ячейки в координаты
const cellAddressToCoords = (address: string): { row: number; col: number } => {
const match = address.match(/^([A-Z]+)(\d+)$/)
if (!match) return { row: 0, col: 0 }
const col = match[1].charCodeAt(0) - 65 // Простое преобразование для одной буквы
const row = parseInt(match[2]) - 1
return { row, col }
}
// Функция для проверки, является ли ячейка частью объединенной группы
const findMergedCellInfo = (
row: number,
col: number,
mergedCells: MergedCell[],
): {
isMerged: boolean
isMainCell: boolean
mergedCell?: MergedCell
spans?: { rowSpan: number; colSpan: number }
} => {
for (const merged of mergedCells) {
const fromCoords = cellAddressToCoords(merged.from)
const toCoords = cellAddressToCoords(merged.to)
// Проверяем, попадает ли текущая ячейка в диапазон объединения
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
return {
isMerged: true,
isMainCell,
mergedCell: merged,
spans: { rowSpan, colSpan },
}
}
}
return { isMerged: false, isMainCell: false }
}
// --- Interfaces --- // --- Interfaces ---
interface DualSpreadsheetProps { interface DualSpreadsheetProps {
templateData?: Record<string, Record<string, any>> templateData?: Record<string, Record<string, any>>
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
} }
// Кэшированные данные ячеек для оптимизации // Кэшированные данные ячеек для оптимизации
@@ -45,6 +96,7 @@ interface CellProps {
rowIndex: number rowIndex: number
colIndex: number colIndex: number
columnWidth: number columnWidth: number
columnWidths: number[] // Добавляем массив всех ширин колонок
isSelected: boolean isSelected: boolean
isEditing: boolean isEditing: boolean
displayValue: string // Отображаемое значение (вычисленное) displayValue: string // Отображаемое значение (вычисленное)
@@ -58,6 +110,14 @@ interface CellProps {
setActiveSheet: (type: SheetType) => void setActiveSheet: (type: SheetType) => void
activeCellRef: React.RefObject<HTMLInputElement> activeCellRef: React.RefObject<HTMLInputElement>
style: CSSProperties // Добавляем style для react-window style: CSSProperties // Добавляем style для react-window
mergedCellInfo?: {
// Информация об объединенных ячейках
isMerged: boolean
isMainCell: boolean
rowSpan?: number
colSpan?: number
mergedValue?: any
}
} }
const Cell = memo( const Cell = memo(
@@ -66,6 +126,7 @@ const Cell = memo(
rowIndex, rowIndex,
colIndex, colIndex,
columnWidth, columnWidth,
columnWidths,
isSelected, isSelected,
isEditing, isEditing,
displayValue, displayValue,
@@ -79,17 +140,63 @@ const Cell = memo(
setActiveSheet, setActiveSheet,
activeCellRef, activeCellRef,
style, // Добавляем style для react-window style, // Добавляем style для react-window
mergedCellInfo,
}: CellProps) => { }: CellProps) => {
// Если ячейка объединена, но не является основной, не рендерим её
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
return null
}
const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${ const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${
isModified ? 'bg-yellow-50' : '' isModified ? 'bg-yellow-50' : ''
}` }`
const cellStyle: CSSProperties = { // Для объединенных ячеек нужно рассчитать правильные размеры
let cellStyle: CSSProperties = {
...style, ...style,
overflow: 'visible', overflow: 'visible',
padding: 0, padding: 0,
} }
// Если это основная ячейка объединенной группы, расширяем её размеры
if (
mergedCellInfo?.isMainCell &&
mergedCellInfo.rowSpan &&
mergedCellInfo.colSpan
) {
const { rowSpan, colSpan } = mergedCellInfo
// Рассчитываем общую ширину для объединенной ячейки
let totalWidth = 0
for (let i = 0; i < colSpan; i++) {
const currentColIndex = colIndex + i
if (currentColIndex < columnWidths.length) {
totalWidth += columnWidths[currentColIndex]
}
}
// Рассчитываем общую высоту для объединенной ячейки
const totalHeight = ROW_HEIGHT * rowSpan
cellStyle = {
...cellStyle,
width: totalWidth,
height: totalHeight,
zIndex: 10, // Поднимаем выше обычных ячеек
backgroundColor: isSelected
? 'hsl(var(--accent))'
: isModified
? '#fefce8'
: 'white',
}
}
// Используем значение из объединенной ячейки, если доступно
const cellDisplayValue =
mergedCellInfo?.isMainCell && mergedCellInfo.mergedValue !== undefined
? String(mergedCellInfo.mergedValue)
: displayValue
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setActiveSheet(sheetType) setActiveSheet(sheetType)
onCellClick(rowIndex, colIndex) onCellClick(rowIndex, colIndex)
@@ -145,7 +252,7 @@ const Cell = memo(
maxWidth: hasRightContent ? `${columnWidth - 16}px` : 'none', maxWidth: hasRightContent ? `${columnWidth - 16}px` : 'none',
}} }}
> >
{displayValue} {cellDisplayValue}
</div> </div>
</div> </div>
)} )}
@@ -271,6 +378,7 @@ const CellRenderer = ({
isEditing, isEditing,
editingValue, editingValue,
cachedData, cachedData,
mergedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -293,6 +401,22 @@ const CellRenderer = ({
? '' // Не показывать вычисленное значение во время редактирования ? '' // Не показывать вычисленное значение во время редактирования
: cellData?.displayValue || '' : cellData?.displayValue || ''
// Получаем информацию об объединенных ячейках только для листа отчета
const mergedCellInfo =
sheetType === 'report' && mergedCells
? findMergedCellInfo(rowIndex, columnIndex, mergedCells)
: { isMerged: false, isMainCell: false }
const mergedCellProps = mergedCellInfo.isMerged
? {
isMerged: true,
isMainCell: mergedCellInfo.isMainCell,
rowSpan: mergedCellInfo.spans?.rowSpan,
colSpan: mergedCellInfo.spans?.colSpan,
mergedValue: mergedCellInfo.mergedCell?.value,
}
: undefined
return ( return (
<Cell <Cell
style={style} style={style}
@@ -300,6 +424,7 @@ const CellRenderer = ({
rowIndex={rowIndex} rowIndex={rowIndex}
colIndex={columnIndex} colIndex={columnIndex}
columnWidth={widths[columnIndex]} columnWidth={widths[columnIndex]}
columnWidths={widths}
isSelected={isCellSelected} isSelected={isCellSelected}
isEditing={isCellEditing} isEditing={isCellEditing}
displayValue={displayValue} displayValue={displayValue}
@@ -312,6 +437,7 @@ const CellRenderer = ({
stopEditing={stopEditing} stopEditing={stopEditing}
setActiveSheet={setActiveSheet} setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef} activeCellRef={activeCellInputRef}
mergedCellInfo={mergedCellProps}
/> />
) )
} }
@@ -349,6 +475,7 @@ interface SpreadsheetProps {
colIndex: number, colIndex: number,
newWidth: number, newWidth: number,
) => void ) => void
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
} }
// --- Spreadsheet Component --- // --- Spreadsheet Component ---
@@ -374,6 +501,7 @@ const Spreadsheet = ({
gridRefs, gridRefs,
onGridScroll, onGridScroll,
handleColumnResize, handleColumnResize,
mergedCells,
}: SpreadsheetProps) => { }: SpreadsheetProps) => {
const widths = columnWidths const widths = columnWidths
const cachedData = getCachedCellData(sheetType) const cachedData = getCachedCellData(sheetType)
@@ -386,6 +514,7 @@ const Spreadsheet = ({
isEditing, isEditing,
editingValue, editingValue,
cachedData, cachedData,
mergedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -530,7 +659,10 @@ const Spreadsheet = ({
) )
} }
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => { const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
templateData = {},
mergedCells = [],
}) => {
const { revision, updateRevision, ...multiSheetEngine } = useMultiSheetEngine( const { revision, updateRevision, ...multiSheetEngine } = useMultiSheetEngine(
{ {
sheets: [ sheets: [
@@ -996,6 +1128,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
gridRefs={gridRefs} gridRefs={gridRefs}
onGridScroll={onGridScroll} onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize} handleColumnResize={handleColumnResize}
mergedCells={mergedCells}
/> />
<div <div
ref={setSeparatorRef} ref={setSeparatorRef}
@@ -1025,6 +1158,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
gridRefs={gridRefs} gridRefs={gridRefs}
onGridScroll={onGridScroll} onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize} handleColumnResize={handleColumnResize}
mergedCells={mergedCells}
/> />
</div> </div>
</div> </div>

View File

@@ -240,7 +240,11 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
{/* Таблицы - теперь занимают всё оставшееся пространство */} {/* Таблицы - теперь занимают всё оставшееся пространство */}
<div className="min-h-0 flex-1"> <div className="min-h-0 flex-1">
<DualSpreadsheet key={dataVersion} templateData={{ L: excelData }} /> <DualSpreadsheet
key={dataVersion}
templateData={{ L: excelData }}
mergedCells={editedTemplate.mergedCells}
/>
</div> </div>
</div> </div>
) )