Дизайн таблиц

This commit is contained in:
2025-07-15 16:41:46 +03:00
parent 90ebcd3772
commit 3b0b4145b1

View File

@@ -12,7 +12,7 @@ import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
const ROW_COUNT = 90 const ROW_COUNT = 90
const COL_COUNT = 20 const COL_COUNT = 20
const ROW_HEIGHT = 32 const ROW_HEIGHT = 28
const SHEET_NAMES = { const SHEET_NAMES = {
report: 'L', report: 'L',
@@ -66,9 +66,9 @@ const Cell = memo(
setActiveSheet, setActiveSheet,
activeCellRef, activeCellRef,
}: CellProps) => { }: CellProps) => {
const className = `border border-gray-200 ${ const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${
isSelected ? 'bg-blue-100' : '' isModified ? 'bg-yellow-50' : ''
} ${isModified ? 'bg-yellow-50' : ''}` }`
const style = { const style = {
minWidth: `${columnWidth}px`, minWidth: `${columnWidth}px`,
@@ -112,12 +112,16 @@ const Cell = memo(
stopEditing(false) stopEditing(false)
} }
}} }}
className="absolute left-0 top-0 z-20 box-border h-full w-full border-2 border-blue-500 p-2" className="absolute left-0 top-0 z-20 box-border h-full w-full border-2 border-primary px-2 py-1"
/> />
) : ( ) : (
<div <div
className="relative h-full w-full truncate p-2" className="relative h-full w-full truncate px-2 py-1"
style={isSelected ? { boxShadow: 'inset 0 0 0 2px #3b82f6' } : {}} style={
isSelected
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
: {}
}
> >
{displayValue} {displayValue}
</div> </div>
@@ -157,43 +161,74 @@ const FormulaBar = memo(
}: FormulaBarProps) => { }: FormulaBarProps) => {
if (activeSheet !== sheetType) { if (activeSheet !== sheetType) {
return ( return (
<div className="flex-shrink-0 border-b border-gray-200 bg-white p-2"> <div className="flex-shrink-0 border-b bg-background px-3 py-1">
<div className="flex h-[34px] items-center" /> <div className="flex h-[28px] items-center" />
</div> </div>
) )
} }
return ( return (
<div className="flex-shrink-0 border-b border-gray-200 bg-white p-2"> <div className="flex-shrink-0 border-b bg-background px-3 py-1">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="min-w-[60px] text-[1rem] text-gray-600"> <div className="flex min-w-[50px] items-center justify-center rounded-md border bg-muted px-2 py-1">
<b> <span className="text-xs font-medium text-muted-foreground">
{selectedCell {selectedCell
? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}`
: ''} : 'A1'}
</b> </span>
</div> </div>
{isCalculating && ( <div className="flex flex-1 items-center space-x-2">
<div className="animate-pulse text-xs text-blue-600"> <div className="flex items-center text-muted-foreground">
Вычисляется... <svg
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
</div> </div>
)}
<div className="flex-1">
<input <input
ref={formulaBarRef} ref={formulaBarRef}
type="text" type="text"
className={`w-full rounded border px-2 py-1 text-sm focus:outline-none ${ className={`flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${
isEditing isEditing ? 'ring-2 ring-ring ring-offset-2' : ''
? 'border-blue-400 focus:ring-2 focus:ring-blue-300' } ${isCalculating ? 'animate-pulse' : ''}`}
: 'border-gray-300 focus:ring-2 focus:ring-blue-500' placeholder="Введите формулу или значение..."
}`}
placeholder="Введите формулу..."
value={formulaBarValue} value={formulaBarValue}
onChange={(e) => onValueChange(e.target.value)} onChange={(e) => onValueChange(e.target.value)}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
{isCalculating && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-3.5 w-3.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -310,7 +345,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
} }
setIsEditing(false) setIsEditing(false)
setEditingValue('') // Очищаем временное значение setEditingValue('') // Очищаем временное значение
containerRefs.current[activeSheet]?.focus() containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, },
[activeSheet, selectedCell, editingValue, multiSheetEngine], [activeSheet, selectedCell, editingValue, multiSheetEngine],
) )
@@ -404,7 +439,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
stopEditing(true) stopEditing(true)
} }
setSelectedCell({ row, col }) setSelectedCell({ row, col })
containerRefs.current[activeSheet]?.focus() containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, },
[isEditing, stopEditing, activeSheet], [isEditing, stopEditing, activeSheet],
) )
@@ -459,10 +494,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
e.preventDefault() e.preventDefault()
stopEditing(true) stopEditing(true)
moveSelection(1, 0) moveSelection(1, 0)
// Предотвращаем прокрутку страницы
setTimeout(() => {
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, 0)
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
e.preventDefault() e.preventDefault()
stopEditing(false) stopEditing(false)
containerRefs.current[activeSheet]?.focus() containerRefs.current[activeSheet]?.focus({ preventScroll: true })
} }
}, },
[stopEditing, moveSelection, activeSheet], [stopEditing, moveSelection, activeSheet],
@@ -476,120 +515,129 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
return ( return (
<div <div
ref={(el) => (containerRefs.current[sheetType] = el)} ref={(el) => (containerRefs.current[sheetType] = el)}
className="overflow-auto rounded-lg border border-gray-300 bg-white shadow-md" className="flex h-full flex-col overflow-hidden rounded-lg border bg-card shadow-sm"
style={{
width: '100%',
height: '100%',
position: 'relative',
}}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
tabIndex={0} tabIndex={0}
> >
<table <div className="flex-shrink-0 border-b bg-muted/50 px-3 py-1">
className="w-full border-collapse" <div className="flex items-center space-x-2">
style={{ tableLayout: 'fixed' }} <div
> className={`h-2 w-2 rounded-full ${
<thead> sheetType === 'report' ? 'bg-primary' : 'bg-emerald-500'
<tr> }`}
<th className="h-8 w-12 border border-gray-300 bg-gray-100 text-xs font-medium text-gray-600"></th> ></div>
{Array.from({ length: COL_COUNT }, (_, i) => { <h3 className="text-xs font-medium text-muted-foreground">
const handleMouseDown = (e: React.MouseEvent) => { {sheetType === 'report' ? 'Отчет' : 'Расчеты'}
e.preventDefault() </h3>
const startX = e.clientX </div>
const startWidth = widths[i] </div>
<div className="flex-1 overflow-auto">
<table
className="w-full border-collapse"
style={{ tableLayout: 'fixed' }}
>
<thead>
<tr>
<th className="h-7 w-10 border border-border bg-muted text-xs font-medium text-muted-foreground"></th>
{Array.from({ length: COL_COUNT }, (_, i) => {
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const startWidth = widths[i]
const handleMouseMove = (me: MouseEvent) => { const handleMouseMove = (me: MouseEvent) => {
const newWidth = startWidth + (me.clientX - startX) const newWidth = startWidth + (me.clientX - startX)
handleColumnResize(sheetType, i, newWidth) handleColumnResize(sheetType, i, newWidth)
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
} }
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return (
<th
key={i}
className="relative h-8 border border-gray-300 bg-gray-100 text-center text-xs font-medium text-gray-600"
style={{ width: widths[i] }}
>
{getColumnLabel(i)}
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-blue-500 hover:bg-opacity-50"
onMouseDown={handleMouseDown}
/>
</th>
)
})}
</tr>
</thead>
<tbody>
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex}>
<td className="h-8 w-12 border border-gray-300 bg-gray-100 text-center text-xs font-medium text-gray-600">
{rowIndex + 1}
</td>
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
const isCellSelected =
selectedCell?.row === rowIndex &&
selectedCell?.col === colIndex &&
activeSheet === sheetType
const isCellEditing = isCellSelected && isEditing
const rawValue = isCellEditing
? editingValue
: multiSheetEngine.getCellRawValue(
sheetName,
rowIndex,
colIndex,
)
const displayValue = isCellEditing
? '' // Не показывать вычисленное значение во время редактирования
: String(
multiSheetEngine.getCellValue(
sheetName,
rowIndex,
colIndex,
) || '',
)
const cellName = getColumnLabel(colIndex) + (rowIndex + 1)
const templateValue =
initialTemplateValues.current[cellName] ?? ''
const isModified =
sheetType === 'report' &&
templateValue !== '' &&
rawValue !== templateValue
return ( return (
<Cell <th
key={colIndex} key={i}
sheetType={sheetType} className="relative h-7 border border-border bg-muted text-center text-xs font-medium text-muted-foreground"
rowIndex={rowIndex} style={{ width: widths[i] }}
colIndex={colIndex} >
columnWidth={widths[colIndex]} {getColumnLabel(i)}
isSelected={isCellSelected} <div
isEditing={isCellEditing} className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
displayValue={displayValue} onMouseDown={handleMouseDown}
rawValue={rawValue} />
isModified={isModified} </th>
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
onCellValueChange={handleCellChange}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef}
/>
) )
})} })}
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex}>
<td className="h-7 w-10 border border-border bg-muted text-center text-xs font-medium text-muted-foreground">
{rowIndex + 1}
</td>
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
const isCellSelected =
selectedCell?.row === rowIndex &&
selectedCell?.col === colIndex &&
activeSheet === sheetType
const isCellEditing = isCellSelected && isEditing
const rawValue = isCellEditing
? editingValue
: multiSheetEngine.getCellRawValue(
sheetName,
rowIndex,
colIndex,
)
const displayValue = isCellEditing
? '' // Не показывать вычисленное значение во время редактирования
: String(
multiSheetEngine.getCellValue(
sheetName,
rowIndex,
colIndex,
) || '',
)
const cellName = getColumnLabel(colIndex) + (rowIndex + 1)
const templateValue =
initialTemplateValues.current[cellName] ?? ''
const isModified =
sheetType === 'report' &&
templateValue !== '' &&
rawValue !== templateValue
return (
<Cell
key={colIndex}
sheetType={sheetType}
rowIndex={rowIndex}
colIndex={colIndex}
columnWidth={widths[colIndex]}
isSelected={isCellSelected}
isEditing={isCellEditing}
displayValue={displayValue}
rawValue={rawValue}
isModified={isModified}
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
onCellValueChange={handleCellChange}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef}
/>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div> </div>
) )
} }
@@ -623,22 +671,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
) )
return ( return (
<div className="flex h-full flex-col space-y-4 bg-gray-100 p-4"> <div className="flex h-screen flex-col bg-background">
{/* Двойная панель */}
<div className="flex flex-grow space-x-4 overflow-hidden">
{/* Левая панель - Отчет */}
<div className="flex w-1/2 flex-col">
<h2 className="mb-2 text-lg font-semibold">Отчет</h2>
{memoizedReportSheet}
</div>
{/* Правая панель - Вычисления */}
<div className="flex w-1/2 flex-col">
<h2 className="mb-2 text-lg font-semibold">Расчеты</h2>
{memoizedCalculationsSheet}
</div>
</div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<FormulaBar <FormulaBar
sheetType={activeSheet} sheetType={activeSheet}
@@ -654,6 +687,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
formulaBarRef={(el) => (formulaBarRefs.current[activeSheet] = el)} formulaBarRef={(el) => (formulaBarRefs.current[activeSheet] = el)}
/> />
</div> </div>
{/* Двойная панель */}
<div className="flex flex-1 gap-1 overflow-hidden p-1">
<div className="min-w-0 flex-1">{memoizedReportSheet}</div>
<div className="min-w-0 flex-1">{memoizedCalculationsSheet}</div>
</div>
</div> </div>
) )
} }