сохранение при смене страниц
This commit is contained in:
@@ -11,15 +11,10 @@ export const Cell = memo(
|
||||
isSelected,
|
||||
isInMultiSelection,
|
||||
multiSelectionActive,
|
||||
selectionTop = false,
|
||||
selectionBottom = false,
|
||||
selectionLeft = false,
|
||||
selectionRight = false,
|
||||
isEditing,
|
||||
displayValue,
|
||||
rawValue,
|
||||
isModified,
|
||||
nextContentCol,
|
||||
availablePx,
|
||||
onCellClick,
|
||||
onCellDoubleClick,
|
||||
@@ -32,157 +27,98 @@ export const Cell = memo(
|
||||
stopEditing,
|
||||
setActiveSheet,
|
||||
activeCellRef,
|
||||
style, // Добавляем style для react-window
|
||||
style,
|
||||
mergedCellInfo,
|
||||
highlightColor, // Цвет подсветки для формул
|
||||
isCopied, // Является ли ячейка скопированной
|
||||
isCut, // Является ли ячейка вырезанной
|
||||
highlightColor,
|
||||
isCopied,
|
||||
isCut,
|
||||
}: CellProps) => {
|
||||
// Если ячейка объединена, но не является основной, не рендерим её
|
||||
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Определяем классы CSS для подсветки и обычного состояния
|
||||
let className = 'border border-border'
|
||||
|
||||
// Базовый style для ячейки
|
||||
let cellStyle: CSSProperties = {
|
||||
...style,
|
||||
overflow: 'visible',
|
||||
padding: 0,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
// Приоритет стилей: подсветка формул > множественное выделение > изменения > обычное состояние
|
||||
if (highlightColor) {
|
||||
className += ` ${highlightColor}`
|
||||
} else if (isModified && !mergedCellInfo?.isMerged) {
|
||||
className += ' bg-yellow-200'
|
||||
}
|
||||
|
||||
// Настраиваем box-shadow, фон и z-index для выделенной области
|
||||
if (isInMultiSelection) {
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
backgroundColor: 'rgba(59,130,246,0.08)', // голубоватый оттенок
|
||||
}
|
||||
}
|
||||
|
||||
// Стили для скопированных/вырезанных ячеек
|
||||
if (isCopied || isCut) {
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
border: '2px dashed hsl(var(--primary))',
|
||||
backgroundColor: isCut
|
||||
? 'rgba(239, 68, 68, 0.1)'
|
||||
: 'rgba(34, 197, 94, 0.1)', // красноватый для вырезанных, зеленоватый для скопированных
|
||||
}
|
||||
}
|
||||
|
||||
// Отладка стилей для измененных ячеек
|
||||
if (isModified) {
|
||||
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
|
||||
// className,
|
||||
// isModified,
|
||||
// isSelected,
|
||||
// displayValue,
|
||||
// highlightColor,
|
||||
//})
|
||||
}
|
||||
|
||||
// Для объединенных ячеек нужно рассчитать правильные размеры
|
||||
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 getCellStyles = (): { className: string; style: CSSProperties } => {
|
||||
let className = 'border border-border'
|
||||
let cellStyle: CSSProperties = {
|
||||
...style,
|
||||
overflow: 'visible',
|
||||
padding: 0,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
// Рассчитываем общую высоту для объединенной ячейки
|
||||
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 */
|
||||
className += ` ${highlightColor}`
|
||||
} else if (isModified && !mergedCellInfo?.isMerged) {
|
||||
className += ' bg-yellow-200'
|
||||
}
|
||||
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
width: totalWidth,
|
||||
height: totalHeight,
|
||||
zIndex: 10, // Поднимаем выше обычных ячеек
|
||||
backgroundColor,
|
||||
// Множественное выделение
|
||||
if (isInMultiSelection) {
|
||||
cellStyle.backgroundColor = 'rgba(59,130,246,0.08)'
|
||||
}
|
||||
|
||||
// Стили для скопированных/вырезанных ячеек
|
||||
if (isCopied || isCut) {
|
||||
cellStyle.border = '2px dashed hsl(var(--primary))'
|
||||
cellStyle.backgroundColor = isCut
|
||||
? 'rgba(239, 68, 68, 0.1)'
|
||||
: 'rgba(34, 197, 94, 0.1)'
|
||||
}
|
||||
|
||||
// Объединенные ячейки
|
||||
if (
|
||||
mergedCellInfo?.isMainCell &&
|
||||
mergedCellInfo.rowSpan &&
|
||||
mergedCellInfo.colSpan
|
||||
) {
|
||||
const { rowSpan, colSpan } = mergedCellInfo
|
||||
|
||||
// Рассчитываем размеры объединенной ячейки
|
||||
const totalWidth = columnWidths
|
||||
.slice(colIndex, colIndex + colSpan)
|
||||
.reduce((sum, width) => sum + width, 0)
|
||||
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'
|
||||
}
|
||||
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
width: totalWidth,
|
||||
height: totalHeight,
|
||||
zIndex: 10,
|
||||
backgroundColor,
|
||||
}
|
||||
}
|
||||
|
||||
return { className, style: cellStyle }
|
||||
}
|
||||
|
||||
// Используем обычное значение displayValue для всех ячеек, включая объединенные
|
||||
const cellDisplayValue = displayValue
|
||||
const { className, style: cellStyle } = getCellStyles()
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// console.log('🖱️ Cell клик:', {
|
||||
// rowIndex,
|
||||
// colIndex,
|
||||
// sheetType,
|
||||
// isInEditMode,
|
||||
// isSelected,
|
||||
//hasReferenceHandler: !!onCellReferenceClick,
|
||||
//})
|
||||
|
||||
// Если мы в режиме редактирования, вставка была обработана на mousedown.
|
||||
// Останавливаем всплытие, чтобы родительский Spreadsheet не сменил активный лист.
|
||||
if (isInEditMode) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// Обычная логика клика (выделение ячейки)
|
||||
// console.log('📍 Обычный клик - выбираем ячейку:', {
|
||||
// rowIndex,
|
||||
// colIndex,
|
||||
// sheetType,
|
||||
//})
|
||||
|
||||
// Подсказка для пользователя
|
||||
if (!isInEditMode) {
|
||||
// console.log(
|
||||
// '💡 Чтобы вставить ссылку на ячейку: 1) Выберите ячейку 2) Нажмите F2 или Enter для редактирования 3) Кликните на нужную ячейку'
|
||||
//)
|
||||
}
|
||||
|
||||
onCellClick(rowIndex, colIndex, e)
|
||||
},
|
||||
[
|
||||
setActiveSheet,
|
||||
sheetType,
|
||||
onCellClick,
|
||||
onCellReferenceClick,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
isInEditMode,
|
||||
isSelected,
|
||||
]
|
||||
[isInEditMode, onCellClick, rowIndex, colIndex]
|
||||
)
|
||||
|
||||
const handleDoubleClick = useCallback(
|
||||
@@ -193,24 +129,18 @@ export const Cell = memo(
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isInEditMode) {
|
||||
// блокируем стандартное выделение текста браузером
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Используем mousedown, чтобы сработать раньше blur FormulaBar
|
||||
// Вставка ссылки в режиме редактирования
|
||||
if (isInEditMode && onCellReferenceClick && !isSelected) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
// console.log('🖱️ mousedown – вставка ссылки до blur:', {
|
||||
// rowIndex,
|
||||
// colIndex,
|
||||
// sheetType,
|
||||
//})
|
||||
onCellReferenceClick(sheetType, rowIndex, colIndex)
|
||||
return
|
||||
}
|
||||
|
||||
// Обработчик для начала drag selection
|
||||
// Drag selection
|
||||
if (onCellMouseDown) {
|
||||
setActiveSheet(sheetType)
|
||||
onCellMouseDown(rowIndex, colIndex, e)
|
||||
@@ -229,17 +159,57 @@ export const Cell = memo(
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (onCellMouseEnter) {
|
||||
onCellMouseEnter(rowIndex, colIndex)
|
||||
}
|
||||
onCellMouseEnter?.(rowIndex, colIndex)
|
||||
}, [onCellMouseEnter, rowIndex, colIndex])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (onCellMouseUp) {
|
||||
onCellMouseUp()
|
||||
}
|
||||
onCellMouseUp?.()
|
||||
}, [onCellMouseUp])
|
||||
|
||||
const handleInputBlur = useCallback(
|
||||
(e: React.FocusEvent) => {
|
||||
const relatedTarget = e.relatedTarget as HTMLElement
|
||||
if (relatedTarget?.closest('.formula-bar')) {
|
||||
return
|
||||
}
|
||||
stopEditing(true)
|
||||
},
|
||||
[stopEditing]
|
||||
)
|
||||
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
stopEditing(true)
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
stopEditing(false)
|
||||
}
|
||||
},
|
||||
[stopEditing]
|
||||
)
|
||||
|
||||
// Стили для отображения содержимого
|
||||
const contentStyles: CSSProperties = {
|
||||
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
...(isSelected && !multiSelectionActive
|
||||
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
|
||||
: highlightColor
|
||||
? { boxShadow: 'inset 0 0 0 2px currentColor' }
|
||||
: {}),
|
||||
}
|
||||
|
||||
const textStyles: CSSProperties = {
|
||||
width: availablePx > columnWidth ? availablePx - 8 : columnWidth - 16,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
@@ -254,57 +224,23 @@ export const Cell = memo(
|
||||
<input
|
||||
ref={activeCellRef as React.RefObject<HTMLInputElement>}
|
||||
type="text"
|
||||
value={rawValue} // Во время редактирования показываем текущее редактируемое значение
|
||||
value={rawValue}
|
||||
onChange={e => onCellValueChange(e.target.value)}
|
||||
style={{ width: columnWidth - 4 }}
|
||||
onBlur={e => {
|
||||
// Проверяем, не переходит ли фокус в formula bar
|
||||
const relatedTarget = e.relatedTarget as HTMLElement
|
||||
if (relatedTarget && relatedTarget.closest('.formula-bar')) {
|
||||
return // Не завершаем редактирование
|
||||
}
|
||||
stopEditing(true)
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
stopEditing(true)
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
stopEditing(false)
|
||||
}
|
||||
}}
|
||||
onBlur={handleInputBlur}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
className="absolute left-0 top-0 z-20 box-border h-full border-2 border-primary px-2 py-1"
|
||||
/>
|
||||
) : (
|
||||
// Контейнер, который допускает перепрыгивание текста, если впереди есть свободное место
|
||||
<div
|
||||
className="relative flex h-full w-full items-center px-2"
|
||||
style={{
|
||||
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
...(isSelected && !multiSelectionActive
|
||||
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
|
||||
: highlightColor
|
||||
? { boxShadow: 'inset 0 0 0 2px currentColor' }
|
||||
: {}),
|
||||
}}
|
||||
style={contentStyles}
|
||||
>
|
||||
<div
|
||||
className="absolute left-1 top-0 flex h-full items-center"
|
||||
style={{
|
||||
width:
|
||||
availablePx > columnWidth
|
||||
? availablePx - 8
|
||||
: columnWidth - 16,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
pointerEvents: 'none', // чтобы клики шли в "родную" ячейку
|
||||
zIndex: 1,
|
||||
}}
|
||||
style={textStyles}
|
||||
>
|
||||
{cellDisplayValue}
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CellRendererProps } from '../types'
|
||||
import { findMergedCellInfo, isInAnySelection } from '../utils'
|
||||
import { Cell } from './Cell'
|
||||
|
||||
// Рендер-проп для ячеек грида
|
||||
export const CellRenderer = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
@@ -34,55 +33,25 @@ export const CellRenderer = ({
|
||||
activeCellInputRef,
|
||||
} = data
|
||||
|
||||
const isActiveSheet = activeSheet === sheetType
|
||||
const isCellSelected =
|
||||
selectedCell?.row === rowIndex &&
|
||||
selectedCell?.col === columnIndex &&
|
||||
activeSheet === sheetType
|
||||
isActiveSheet
|
||||
|
||||
// Проверяем, является ли ячейка частью множественного выделения
|
||||
const isInMultiSelection =
|
||||
sheetType === activeSheet &&
|
||||
isInAnySelection(rowIndex, columnIndex, multiSelection)
|
||||
|
||||
const multiSelectionActive =
|
||||
sheetType === activeSheet && multiSelection.length > 0
|
||||
|
||||
// Вычисляем границы выделения, если ячейка внутри выделения
|
||||
let selectionTop = false,
|
||||
selectionBottom = false,
|
||||
selectionLeft = false,
|
||||
selectionRight = false
|
||||
|
||||
if (isInMultiSelection && multiSelection.length > 0) {
|
||||
const bounds = {
|
||||
top: Math.min(...multiSelection.map(s => Math.min(s.startRow, s.endRow))),
|
||||
bottom: Math.max(
|
||||
...multiSelection.map(s => Math.max(s.startRow, s.endRow))
|
||||
),
|
||||
left: Math.min(
|
||||
...multiSelection.map(s => Math.min(s.startCol, s.endCol))
|
||||
),
|
||||
right: Math.max(
|
||||
...multiSelection.map(s => Math.max(s.startCol, s.endCol))
|
||||
),
|
||||
}
|
||||
selectionTop = rowIndex === bounds.top
|
||||
selectionBottom = rowIndex === bounds.bottom
|
||||
selectionLeft = columnIndex === bounds.left
|
||||
selectionRight = columnIndex === bounds.right
|
||||
}
|
||||
isActiveSheet && isInAnySelection(rowIndex, columnIndex, multiSelection)
|
||||
|
||||
const multiSelectionActive = isActiveSheet && multiSelection.length > 0
|
||||
const isCellEditing = isCellSelected && isEditing
|
||||
|
||||
const cellKey = `${rowIndex}-${columnIndex}`
|
||||
const cellData = cachedData[cellKey]
|
||||
|
||||
const rawValue = isCellEditing ? editingValue : cellData?.rawValue || ''
|
||||
const displayValue = isCellEditing
|
||||
? '' // Не показывать вычисленное значение во время редактирования
|
||||
: cellData?.displayValue || ''
|
||||
const displayValue = isCellEditing ? '' : cellData?.displayValue || ''
|
||||
|
||||
// Проверяем, подсвечена ли эта ячейка
|
||||
// Подсветка ячеек для формул
|
||||
const currentSheetName = sheetType === 'report' ? 'L' : 'R'
|
||||
const highlightedCell = highlightedCells.find(
|
||||
highlight =>
|
||||
@@ -90,23 +59,8 @@ export const CellRenderer = ({
|
||||
highlight.row === rowIndex &&
|
||||
highlight.col === columnIndex
|
||||
)
|
||||
const highlightColor = highlightedCell?.color
|
||||
|
||||
// Отладка для измененных ячеек
|
||||
if (cellData?.isModified) {
|
||||
// console.log(
|
||||
// `🔍 CellRenderer: Измененная ячейка ${rowIndex},${columnIndex}:`,
|
||||
// {
|
||||
// cellKey,
|
||||
// isModified: cellData.isModified,
|
||||
// displayValue,
|
||||
// rawValue,
|
||||
// highlightColor,
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
||||
// Получаем информацию об объединенных ячейках только для листа отчета
|
||||
// Объединенные ячейки только для листа отчета
|
||||
const mergedCellInfo =
|
||||
sheetType === 'report' && mergedCells
|
||||
? findMergedCellInfo(rowIndex, columnIndex, mergedCells)
|
||||
@@ -118,14 +72,12 @@ export const CellRenderer = ({
|
||||
isMainCell: mergedCellInfo.isMainCell,
|
||||
rowSpan: mergedCellInfo.spans?.rowSpan,
|
||||
colSpan: mergedCellInfo.spans?.colSpan,
|
||||
// Убираем mergedValue - будем использовать актуальные данные из движка
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Проверяем, является ли ячейка скопированной
|
||||
// Скопированные/вырезанные ячейки
|
||||
const isCopied = Boolean(
|
||||
copiedCells &&
|
||||
copiedCells.sourceSheet === sheetType &&
|
||||
copiedCells?.sourceSheet === sheetType &&
|
||||
copiedCells.data.some(
|
||||
cell => cell.row === rowIndex && cell.col === columnIndex
|
||||
)
|
||||
@@ -143,15 +95,10 @@ export const CellRenderer = ({
|
||||
isSelected={isCellSelected}
|
||||
isInMultiSelection={isInMultiSelection}
|
||||
multiSelectionActive={multiSelectionActive}
|
||||
selectionTop={selectionTop}
|
||||
selectionBottom={selectionBottom}
|
||||
selectionLeft={selectionLeft}
|
||||
selectionRight={selectionRight}
|
||||
isEditing={isCellEditing}
|
||||
displayValue={displayValue}
|
||||
rawValue={rawValue}
|
||||
isModified={cellData?.isModified || false}
|
||||
nextContentCol={cellData?.nextContentCol || 0}
|
||||
availablePx={cellData?.availablePx || widths[columnIndex]}
|
||||
onCellClick={handleCellClick}
|
||||
onCellDoubleClick={handleCellDoubleClick}
|
||||
@@ -165,7 +112,7 @@ export const CellRenderer = ({
|
||||
setActiveSheet={setActiveSheet}
|
||||
activeCellRef={activeCellInputRef}
|
||||
mergedCellInfo={mergedCellProps}
|
||||
highlightColor={highlightColor}
|
||||
highlightColor={highlightedCell?.color}
|
||||
isCopied={isCopied}
|
||||
isCut={isCut}
|
||||
/>
|
||||
|
||||
@@ -185,6 +185,16 @@ export const FormulaBar = memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ExcelUploadPanel */}
|
||||
{onExcelFileChange && (
|
||||
<ExcelUploadPanel
|
||||
fileName={excelFileName}
|
||||
isLoading={isExcelLoading}
|
||||
onUploadClick={() => {}}
|
||||
onFileChange={onExcelFileChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Кнопка сохранения */}
|
||||
{onManualSave && (
|
||||
<button
|
||||
@@ -215,16 +225,6 @@ export const FormulaBar = memo(
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* ExcelUploadPanel */}
|
||||
{onExcelFileChange && (
|
||||
<ExcelUploadPanel
|
||||
fileName={excelFileName}
|
||||
isLoading={isExcelLoading}
|
||||
onUploadClick={() => {}}
|
||||
onFileChange={onExcelFileChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,8 @@ import { COL_COUNT, ROW_COUNT, SHEET_NAMES, SpreadsheetProps } from '../types'
|
||||
import { getColumnLabel } from '../utils'
|
||||
import { CellRenderer } from './CellRenderer'
|
||||
|
||||
// Создаем типизированный компонент для совместимости с React 18
|
||||
const Grid = VariableSizeGrid as unknown as React.ComponentType<any>
|
||||
|
||||
// --- Spreadsheet Component ---
|
||||
export const Spreadsheet = ({
|
||||
sheetType,
|
||||
columnWidths,
|
||||
@@ -40,12 +38,12 @@ export const Spreadsheet = ({
|
||||
mergedCells,
|
||||
copiedCells,
|
||||
}: SpreadsheetProps) => {
|
||||
const widths = columnWidths
|
||||
const cachedData = getCachedCellData(sheetType)
|
||||
const isActiveSheet = activeSheet === sheetType
|
||||
|
||||
const itemData = {
|
||||
sheetType,
|
||||
widths,
|
||||
widths: columnWidths,
|
||||
selectedCell,
|
||||
multiSelection,
|
||||
activeSheet,
|
||||
@@ -55,8 +53,6 @@ export const Spreadsheet = ({
|
||||
mergedCells,
|
||||
highlightedCells,
|
||||
copiedCells,
|
||||
multiSelectionActive:
|
||||
activeSheet === sheetType && multiSelection.length > 0,
|
||||
handleCellClick,
|
||||
handleCellDoubleClick,
|
||||
handleCellChange,
|
||||
@@ -70,19 +66,8 @@ export const Spreadsheet = ({
|
||||
activeCellInputRef,
|
||||
}
|
||||
|
||||
// Отладочное логирование для проверки состояния
|
||||
// console.log(`📊 Spreadsheet ${sheetType} состояние:`, {
|
||||
// isEditing,
|
||||
// isInEditMode,
|
||||
// selectedCell,
|
||||
// activeSheet,
|
||||
// isActiveSheet: activeSheet === sheetType,
|
||||
//})
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [gridSize, setGridSize] = useState({ width: 0, height: 0 })
|
||||
|
||||
// Состояние текущего скролла грида
|
||||
const [scrollPos, setScrollPos] = useState({ left: 0, top: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,17 +85,98 @@ export const Spreadsheet = ({
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
const selectedRows = new Set<number>()
|
||||
const selectedCols = new Set<number>()
|
||||
if (sheetType === activeSheet && multiSelection.length > 0) {
|
||||
multiSelection.forEach(sel => {
|
||||
const top = Math.min(sel.startRow, sel.endRow)
|
||||
const bottom = Math.max(sel.startRow, sel.endRow)
|
||||
const left = Math.min(sel.startCol, sel.endCol)
|
||||
const right = Math.max(sel.startCol, sel.endCol)
|
||||
for (let r = top; r <= bottom; r++) selectedRows.add(r)
|
||||
for (let c = left; c <= right; c++) selectedCols.add(c)
|
||||
})
|
||||
// Вычисляем выделенные строки и колонки для активного листа
|
||||
const getSelectedRowsAndCols = () => {
|
||||
const selectedRows = new Set<number>()
|
||||
const selectedCols = new Set<number>()
|
||||
|
||||
if (isActiveSheet && multiSelection.length > 0) {
|
||||
multiSelection.forEach(sel => {
|
||||
const top = Math.min(sel.startRow, sel.endRow)
|
||||
const bottom = Math.max(sel.startRow, sel.endRow)
|
||||
const left = Math.min(sel.startCol, sel.endCol)
|
||||
const right = Math.max(sel.startCol, sel.endCol)
|
||||
for (let r = top; r <= bottom; r++) selectedRows.add(r)
|
||||
for (let c = left; c <= right; c++) selectedCols.add(c)
|
||||
})
|
||||
}
|
||||
|
||||
return { selectedRows, selectedCols }
|
||||
}
|
||||
|
||||
const { selectedRows, selectedCols } = getSelectedRowsAndCols()
|
||||
|
||||
const handleColumnResizeStart =
|
||||
(colIndex: number) => (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const startX = e.clientX
|
||||
const startWidth = columnWidths[colIndex]
|
||||
|
||||
const handleMouseMove = (me: MouseEvent) => {
|
||||
const newWidth = startWidth + (me.clientX - startX)
|
||||
handleColumnResize(sheetType, colIndex, newWidth)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
// Вычисляем границы множественного выделения
|
||||
const getSelectionBounds = () => {
|
||||
if (!isActiveSheet || multiSelection.length === 0) return null
|
||||
|
||||
return {
|
||||
topRow: Math.min(
|
||||
...multiSelection.map(sel => Math.min(sel.startRow, sel.endRow))
|
||||
),
|
||||
bottomRow: Math.max(
|
||||
...multiSelection.map(sel => Math.max(sel.startRow, sel.endRow))
|
||||
),
|
||||
leftCol: Math.min(
|
||||
...multiSelection.map(sel => Math.min(sel.startCol, sel.endCol))
|
||||
),
|
||||
rightCol: Math.max(
|
||||
...multiSelection.map(sel => Math.max(sel.startCol, sel.endCol))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const selectionBounds = getSelectionBounds()
|
||||
|
||||
const renderSelectionOverlay = () => {
|
||||
if (!selectionBounds) return null
|
||||
|
||||
const topPx = selectionBounds.topRow * 28 - scrollPos.top
|
||||
const heightPx =
|
||||
(selectionBounds.bottomRow - selectionBounds.topRow + 1) * 28
|
||||
const leftPx =
|
||||
columnWidths
|
||||
.slice(0, selectionBounds.leftCol)
|
||||
.reduce((a, b) => a + b, 0) - scrollPos.left
|
||||
const widthPx = columnWidths
|
||||
.slice(selectionBounds.leftCol, selectionBounds.rightCol + 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: topPx,
|
||||
left: leftPx,
|
||||
width: widthPx,
|
||||
height: heightPx,
|
||||
border: '2px solid hsl(var(--primary))',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
zIndex: 5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -131,12 +197,10 @@ export const Spreadsheet = ({
|
||||
style={{ userSelect: isEditing ? 'text' : 'none' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* Spreadsheet Body */}
|
||||
<div className="flex flex-1">
|
||||
{/* Row Headers */}
|
||||
<div className="flex-shrink-0 bg-muted">
|
||||
<div className="relative flex h-7 w-10 items-center justify-center border-b border-r border-border">
|
||||
{/* Компактное обозначение листа */}
|
||||
<div className="absolute inset-0 flex items-end justify-end p-0.5">
|
||||
<span
|
||||
className={`flex h-3 w-3 items-center justify-center rounded-sm text-[9px] font-medium text-white ${
|
||||
@@ -159,13 +223,16 @@ export const Spreadsheet = ({
|
||||
<div>
|
||||
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => {
|
||||
const isActiveRow =
|
||||
(selectedCell?.row === rowIndex &&
|
||||
activeSheet === sheetType) ||
|
||||
(selectedCell?.row === rowIndex && isActiveSheet) ||
|
||||
selectedRows.has(rowIndex)
|
||||
return (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className={`flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium ${isActiveRow ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
|
||||
className={`flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium ${
|
||||
isActiveRow
|
||||
? 'bg-primary/15 font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{rowIndex + 1}
|
||||
</div>
|
||||
@@ -184,38 +251,25 @@ export const Spreadsheet = ({
|
||||
className="column-headers overflow-x-hidden"
|
||||
>
|
||||
<div className="flex">
|
||||
{Array.from({ length: COL_COUNT }).map((_, i) => {
|
||||
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
|
||||
const isActiveColumn =
|
||||
(selectedCell?.col === i && activeSheet === sheetType) ||
|
||||
selectedCols.has(i)
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const startX = e.clientX
|
||||
const startWidth = widths[i]
|
||||
(selectedCell?.col === colIndex && isActiveSheet) ||
|
||||
selectedCols.has(colIndex)
|
||||
|
||||
const handleMouseMove = (me: MouseEvent) => {
|
||||
const newWidth = startWidth + (me.clientX - startX)
|
||||
handleColumnResize(sheetType, i, newWidth)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium ${isActiveColumn ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
|
||||
style={{ width: widths[i] }}
|
||||
key={colIndex}
|
||||
className={`relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium ${
|
||||
isActiveColumn
|
||||
? 'bg-primary/15 font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
style={{ width: columnWidths[colIndex] }}
|
||||
>
|
||||
{getColumnLabel(i)}
|
||||
{getColumnLabel(colIndex)}
|
||||
<div
|
||||
className="resizer absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleColumnResizeStart(colIndex)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -225,60 +279,7 @@ export const Spreadsheet = ({
|
||||
|
||||
{/* Grid */}
|
||||
<div className="relative min-h-0 flex-1">
|
||||
{/* selection overlay */}
|
||||
{sheetType === activeSheet &&
|
||||
multiSelection.length > 0 &&
|
||||
(() => {
|
||||
const bounds = {
|
||||
topRow: Math.min(
|
||||
...multiSelection.map(sel =>
|
||||
Math.min(sel.startRow, sel.endRow)
|
||||
)
|
||||
),
|
||||
bottomRow: Math.max(
|
||||
...multiSelection.map(sel =>
|
||||
Math.max(sel.startRow, sel.endRow)
|
||||
)
|
||||
),
|
||||
leftCol: Math.min(
|
||||
...multiSelection.map(sel =>
|
||||
Math.min(sel.startCol, sel.endCol)
|
||||
)
|
||||
),
|
||||
rightCol: Math.max(
|
||||
...multiSelection.map(sel =>
|
||||
Math.max(sel.startCol, sel.endCol)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
const topPx =
|
||||
bounds.topRow * 28 /* ROW_HEIGHT */ - scrollPos.top
|
||||
const heightPx = (bounds.bottomRow - bounds.topRow + 1) * 28
|
||||
const leftPx =
|
||||
widths.slice(0, bounds.leftCol).reduce((a, b) => a + b, 0) -
|
||||
scrollPos.left
|
||||
const widthPx = widths
|
||||
.slice(bounds.leftCol, bounds.rightCol + 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: topPx,
|
||||
left: leftPx,
|
||||
width: widthPx,
|
||||
height: heightPx,
|
||||
border: '2px solid hsl(var(--primary))',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'none',
|
||||
zIndex: 5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
{renderSelectionOverlay()}
|
||||
<Grid
|
||||
ref={(el: any) => {
|
||||
if (gridRefs.current) {
|
||||
@@ -287,7 +288,7 @@ export const Spreadsheet = ({
|
||||
}}
|
||||
columnCount={COL_COUNT}
|
||||
rowCount={ROW_COUNT}
|
||||
columnWidth={(index: number) => widths[index]}
|
||||
columnWidth={(index: number) => columnWidths[index]}
|
||||
rowHeight={() => 28}
|
||||
height={gridSize.height}
|
||||
width={gridSize.width}
|
||||
|
||||
Reference in New Issue
Block a user