ручной рефакторинг
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
import { useFormulaAutoSave } from '@/hooks/useFormulaAutoSave'
|
||||||
|
import { useMultiSheetEngine } from '@/hooks/useMultiSheetEngine'
|
||||||
|
import { MergedCell } from '@/types/template'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
@@ -10,9 +13,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { VariableSizeGrid } from 'react-window'
|
import { VariableSizeGrid } from 'react-window'
|
||||||
import { useFormulaAutoSave } from '../../hooks/useFormulaAutoSave'
|
|
||||||
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
|
||||||
@@ -25,6 +25,9 @@ const SHEET_NAMES = {
|
|||||||
|
|
||||||
type SheetType = keyof typeof SHEET_NAMES
|
type SheetType = keyof typeof SHEET_NAMES
|
||||||
|
|
||||||
|
// Добавляем массив типов листов для итерации
|
||||||
|
const SHEET_TYPES: SheetType[] = ['report', 'calculations']
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
|
const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ const cellAddressToCoords = (address: string): { row: number; col: number } => {
|
|||||||
const findMergedCellInfo = (
|
const findMergedCellInfo = (
|
||||||
row: number,
|
row: number,
|
||||||
col: number,
|
col: number,
|
||||||
mergedCells: MergedCell[],
|
mergedCells: MergedCell[]
|
||||||
): {
|
): {
|
||||||
isMerged: boolean
|
isMerged: boolean
|
||||||
isMainCell: boolean
|
isMainCell: boolean
|
||||||
@@ -77,6 +80,57 @@ const findMergedCellInfo = (
|
|||||||
return { isMerged: false, isMainCell: false }
|
return { isMerged: false, isMainCell: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Хук для перетаскивания разделителя ---
|
||||||
|
const useDraggableDivider = (onDrag: (delta: number) => void) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
document.body.style.cursor = 'col-resize'
|
||||||
|
|
||||||
|
const container = el.parentElement
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const bounds = container.getBoundingClientRect()
|
||||||
|
|
||||||
|
const handleMouseMove = (me: MouseEvent) => {
|
||||||
|
// Получаем актуальные границы контейнера
|
||||||
|
const currentBounds = container.getBoundingClientRect()
|
||||||
|
const newLeftWidth = me.clientX - currentBounds.left
|
||||||
|
const totalWidth = currentBounds.width
|
||||||
|
|
||||||
|
// Рассчитываем процент напрямую от позиции мыши
|
||||||
|
let newLeftPercentage = newLeftWidth / totalWidth
|
||||||
|
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage))
|
||||||
|
|
||||||
|
// Вызываем callback с новыми процентами
|
||||||
|
onDrag(newLeftPercentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.body.style.cursor = 'auto'
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('mousedown', handleMouseDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
}
|
||||||
|
}, [onDrag])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
interface DualSpreadsheetProps {
|
interface DualSpreadsheetProps {
|
||||||
templateData?: Record<string, Record<string, any>>
|
templateData?: Record<string, Record<string, any>>
|
||||||
@@ -197,8 +251,8 @@ const Cell = memo(
|
|||||||
backgroundColor: isSelected
|
backgroundColor: isSelected
|
||||||
? 'hsl(var(--accent))'
|
? 'hsl(var(--accent))'
|
||||||
: isModified
|
: isModified
|
||||||
? '#fef3c7'
|
? '#fef3c7'
|
||||||
: 'white',
|
: 'white',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +266,7 @@ const Cell = memo(
|
|||||||
|
|
||||||
const handleDoubleClick = useCallback(
|
const handleDoubleClick = useCallback(
|
||||||
() => onCellDoubleClick(rowIndex, colIndex),
|
() => onCellDoubleClick(rowIndex, colIndex),
|
||||||
[onCellDoubleClick, rowIndex, colIndex],
|
[onCellDoubleClick, rowIndex, colIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -227,8 +281,8 @@ const Cell = memo(
|
|||||||
ref={activeCellRef}
|
ref={activeCellRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={rawValue} // Во время редактирования показываем текущее редактируемое значение
|
value={rawValue} // Во время редактирования показываем текущее редактируемое значение
|
||||||
onChange={(e) => onCellValueChange(e.target.value)}
|
onChange={e => onCellValueChange(e.target.value)}
|
||||||
onBlur={(e) => {
|
onBlur={e => {
|
||||||
// Проверяем, не переходит ли фокус в formula bar
|
// Проверяем, не переходит ли фокус в formula bar
|
||||||
const relatedTarget = e.relatedTarget as HTMLElement
|
const relatedTarget = e.relatedTarget as HTMLElement
|
||||||
if (relatedTarget && relatedTarget.closest('.formula-bar')) {
|
if (relatedTarget && relatedTarget.closest('.formula-bar')) {
|
||||||
@@ -236,7 +290,7 @@ const Cell = memo(
|
|||||||
}
|
}
|
||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
@@ -273,7 +327,7 @@ const Cell = memo(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
interface FormulaBarProps {
|
interface FormulaBarProps {
|
||||||
@@ -347,9 +401,9 @@ const FormulaBar = memo(
|
|||||||
} ${isCalculating ? 'animate-pulse' : ''}`}
|
} ${isCalculating ? 'animate-pulse' : ''}`}
|
||||||
placeholder="Введите формулу или значение..."
|
placeholder="Введите формулу или значение..."
|
||||||
value={formulaBarValue}
|
value={formulaBarValue}
|
||||||
onChange={(e) => onValueChange(e.target.value)}
|
onChange={e => onValueChange(e.target.value)}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={(e) => {
|
onBlur={e => {
|
||||||
// Проверяем, не переходит ли фокус в ячейку редактирования
|
// Проверяем, не переходит ли фокус в ячейку редактирования
|
||||||
const relatedTarget = e.relatedTarget as HTMLElement
|
const relatedTarget = e.relatedTarget as HTMLElement
|
||||||
if (
|
if (
|
||||||
@@ -441,15 +495,15 @@ const FormulaBar = memo(
|
|||||||
hasUnsavedChanges
|
hasUnsavedChanges
|
||||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||||
: lastSaveError
|
: lastSaveError
|
||||||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||||
: 'bg-muted text-muted-foreground'
|
: 'bg-muted text-muted-foreground'
|
||||||
} disabled:opacity-50`}
|
} disabled:opacity-50`}
|
||||||
title={
|
title={
|
||||||
lastSaveError
|
lastSaveError
|
||||||
? 'Повторить сохранение'
|
? 'Повторить сохранение'
|
||||||
: hasUnsavedChanges
|
: hasUnsavedChanges
|
||||||
? 'Есть несохраненные изменения'
|
? 'Есть несохраненные изменения'
|
||||||
: 'Все изменения сохранены'
|
: 'Все изменения сохранены'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -468,15 +522,15 @@ const FormulaBar = memo(
|
|||||||
{lastSaveError
|
{lastSaveError
|
||||||
? 'Повторить'
|
? 'Повторить'
|
||||||
: hasUnsavedChanges
|
: hasUnsavedChanges
|
||||||
? 'Сохранить'
|
? 'Сохранить'
|
||||||
: 'Сохранено'}
|
: 'Сохранено'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Рендер-проп для ячеек грида
|
// Рендер-проп для ячеек грида
|
||||||
@@ -531,7 +585,7 @@ const CellRenderer = ({
|
|||||||
isModified: cellData.isModified,
|
isModified: cellData.isModified,
|
||||||
displayValue,
|
displayValue,
|
||||||
rawValue,
|
rawValue,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,15 +653,15 @@ interface SpreadsheetProps {
|
|||||||
rowHeaderRefs: React.MutableRefObject<
|
rowHeaderRefs: React.MutableRefObject<
|
||||||
Record<SheetType, HTMLDivElement | null>
|
Record<SheetType, HTMLDivElement | null>
|
||||||
>
|
>
|
||||||
gridRefs: React.MutableRefObject<Record<SheetType, VariableSizeGrid | null>>
|
gridRefs: React.MutableRefObject<Record<SheetType, any>>
|
||||||
onGridScroll: (
|
onGridScroll: (
|
||||||
sheetType: SheetType,
|
sheetType: SheetType,
|
||||||
params: { scrollLeft: number; scrollTop: number },
|
params: { scrollLeft: number; scrollTop: number }
|
||||||
) => void
|
) => void
|
||||||
handleColumnResize: (
|
handleColumnResize: (
|
||||||
sheetType: SheetType,
|
sheetType: SheetType,
|
||||||
colIndex: number,
|
colIndex: number,
|
||||||
newWidth: number,
|
newWidth: number
|
||||||
) => void
|
) => void
|
||||||
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
||||||
}
|
}
|
||||||
@@ -681,11 +735,12 @@ const Spreadsheet = ({
|
|||||||
className="flex min-w-0 flex-col overflow-hidden"
|
className="flex min-w-0 flex-col overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`,
|
flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`,
|
||||||
|
transition: 'none',
|
||||||
}}
|
}}
|
||||||
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}
|
||||||
@@ -709,7 +764,7 @@ 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 }}
|
||||||
>
|
>
|
||||||
@@ -729,7 +784,7 @@ 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">
|
||||||
@@ -772,7 +827,7 @@ const Spreadsheet = ({
|
|||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
<VariableSizeGrid
|
<VariableSizeGrid
|
||||||
ref={(el) => (gridRefs.current[sheetType] = el)}
|
ref={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]}
|
||||||
@@ -780,7 +835,7 @@ const Spreadsheet = ({
|
|||||||
height={gridSize.height}
|
height={gridSize.height}
|
||||||
width={gridSize.width}
|
width={gridSize.width}
|
||||||
itemData={itemData}
|
itemData={itemData}
|
||||||
onScroll={(e) => onGridScroll(sheetType, e)}
|
onScroll={e => onGridScroll(sheetType, e)}
|
||||||
>
|
>
|
||||||
{CellRenderer}
|
{CellRenderer}
|
||||||
</VariableSizeGrid>
|
</VariableSizeGrid>
|
||||||
@@ -804,7 +859,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
|
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
|
||||||
],
|
],
|
||||||
initialData: templateData || {},
|
initialData: templateData || {},
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
||||||
@@ -820,9 +875,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
() => ({
|
() => ({
|
||||||
report: Array(COL_COUNT).fill(96),
|
report: Array(COL_COUNT).fill(96),
|
||||||
calculations: Array(COL_COUNT).fill(96),
|
calculations: Array(COL_COUNT).fill(96),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
const gridRefs = useRef<Record<SheetType, VariableSizeGrid | null>>({
|
const gridRefs = useRef<Record<SheetType, any>>({
|
||||||
report: null,
|
report: null,
|
||||||
calculations: null,
|
calculations: null,
|
||||||
})
|
})
|
||||||
@@ -844,7 +899,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const initialTemplateValues = useRef<Record<string, string>>({})
|
const initialTemplateValues = useRef<Record<string, string>>({})
|
||||||
|
|
||||||
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
|
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
|
||||||
{},
|
{}
|
||||||
)
|
)
|
||||||
const lastRevision = useRef<number>(-1)
|
const lastRevision = useRef<number>(-1)
|
||||||
|
|
||||||
@@ -866,7 +921,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'✅ Загружено значений шаблона:',
|
'✅ Загружено значений шаблона:',
|
||||||
Object.keys(initialTemplateValues.current).length,
|
Object.keys(initialTemplateValues.current).length
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ DualSpreadsheet: Данные шаблона отчета не найдены')
|
console.log('⚠️ DualSpreadsheet: Данные шаблона отчета не найдены')
|
||||||
@@ -900,7 +955,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
for (let col = 0; col < COL_COUNT; col++) {
|
for (let col = 0; col < COL_COUNT; col++) {
|
||||||
const cellKey = `${row}-${col}`
|
const cellKey = `${row}-${col}`
|
||||||
const displayValue = String(
|
const displayValue = String(
|
||||||
multiSheetEngine.getCellValue(sheetName, row, col) || '',
|
multiSheetEngine.getCellValue(sheetName, row, col) || ''
|
||||||
)
|
)
|
||||||
allCellValues[cellKey] = displayValue.trim()
|
allCellValues[cellKey] = displayValue.trim()
|
||||||
}
|
}
|
||||||
@@ -976,7 +1031,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastRevision.current !== revision) {
|
if (lastRevision.current !== revision) {
|
||||||
Object.keys(cellDataCache.current).forEach((key) => {
|
Object.keys(cellDataCache.current).forEach(key => {
|
||||||
if (!key.endsWith(`-${revision}`)) {
|
if (!key.endsWith(`-${revision}`)) {
|
||||||
delete cellDataCache.current[key]
|
delete cellDataCache.current[key]
|
||||||
}
|
}
|
||||||
@@ -987,7 +1042,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
cellDataCache.current[cacheKey] = cachedData
|
cellDataCache.current[cacheKey] = cachedData
|
||||||
return cachedData
|
return cachedData
|
||||||
},
|
},
|
||||||
[revision, multiSheetEngine, initialTemplateValues],
|
[revision, multiSheetEngine, initialTemplateValues]
|
||||||
)
|
)
|
||||||
|
|
||||||
const formulaBarValue = useMemo(() => {
|
const formulaBarValue = useMemo(() => {
|
||||||
@@ -999,7 +1054,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
return multiSheetEngine.getCellRawValue(
|
return multiSheetEngine.getCellRawValue(
|
||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
@@ -1034,7 +1089,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const rawValue = multiSheetEngine.getCellRawValue(
|
const rawValue = multiSheetEngine.getCellRawValue(
|
||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col
|
||||||
)
|
)
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
setEditingValue(rawValue)
|
setEditingValue(rawValue)
|
||||||
@@ -1049,7 +1104,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const oldValue = multiSheetEngine.getCellRawValue(
|
const oldValue = multiSheetEngine.getCellRawValue(
|
||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col
|
||||||
)
|
)
|
||||||
const templateValue = initialTemplateValues.current[cellName] ?? ''
|
const templateValue = initialTemplateValues.current[cellName] ?? ''
|
||||||
|
|
||||||
@@ -1066,7 +1121,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col,
|
||||||
editingValue,
|
editingValue
|
||||||
)
|
)
|
||||||
updateRevision()
|
updateRevision()
|
||||||
multiSheetEngine.debouncedRecalc()
|
multiSheetEngine.debouncedRecalc()
|
||||||
@@ -1075,7 +1130,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setEditingValue('')
|
setEditingValue('')
|
||||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||||
},
|
},
|
||||||
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision],
|
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCellChange = useCallback(
|
const handleCellChange = useCallback(
|
||||||
@@ -1084,7 +1139,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setEditingValue(newValue)
|
setEditingValue(newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isEditing],
|
[isEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClearCell = useCallback(() => {
|
const handleClearCell = useCallback(() => {
|
||||||
@@ -1094,7 +1149,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col,
|
||||||
'',
|
''
|
||||||
)
|
)
|
||||||
updateRevision()
|
updateRevision()
|
||||||
multiSheetEngine.debouncedRecalc()
|
multiSheetEngine.debouncedRecalc()
|
||||||
@@ -1113,16 +1168,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
const newRow = Math.max(
|
const newRow = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow),
|
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
|
||||||
)
|
)
|
||||||
const newCol = Math.max(
|
const newCol = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(COL_COUNT - 1, selectedCell.col + deltaCol),
|
Math.min(COL_COUNT - 1, selectedCell.col + deltaCol)
|
||||||
)
|
)
|
||||||
|
|
||||||
setSelectedCell({ row: newRow, col: newCol })
|
setSelectedCell({ row: newRow, col: newCol })
|
||||||
},
|
},
|
||||||
[selectedCell, isEditing, stopEditing],
|
[selectedCell, isEditing, stopEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
@@ -1158,7 +1213,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell],
|
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCellClick = useCallback(
|
const handleCellClick = useCallback(
|
||||||
@@ -1169,7 +1224,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setSelectedCell({ row, col })
|
setSelectedCell({ row, col })
|
||||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||||
},
|
},
|
||||||
[isEditing, stopEditing, activeSheet],
|
[isEditing, stopEditing, activeSheet]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCellDoubleClick = useCallback(
|
const handleCellDoubleClick = useCallback(
|
||||||
@@ -1177,22 +1232,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setSelectedCell({ row, col })
|
setSelectedCell({ row, col })
|
||||||
startEditing()
|
startEditing()
|
||||||
},
|
},
|
||||||
[startEditing],
|
[startEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleColumnResize = useCallback(
|
const handleColumnResize = useCallback(
|
||||||
(sheetType: SheetType, colIndex: number, newWidth: number) => {
|
(sheetType: SheetType, colIndex: number, newWidth: number) => {
|
||||||
setColumnWidths(
|
setColumnWidths(
|
||||||
produce((draft) => {
|
produce(draft => {
|
||||||
draft[sheetType][colIndex] = Math.max(60, newWidth)
|
draft[sheetType][colIndex] = Math.max(60, newWidth)
|
||||||
const grid = gridRefs.current[sheetType]
|
const grid = gridRefs.current[sheetType]
|
||||||
if (grid) {
|
if (grid) {
|
||||||
grid.resetAfterColumnIndex(colIndex)
|
grid.resetAfterColumnIndex(colIndex)
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[],
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFormulaBarChange = useCallback(
|
const handleFormulaBarChange = useCallback(
|
||||||
@@ -1204,7 +1259,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setEditingValue(newValue)
|
setEditingValue(newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedCell, isEditing],
|
[selectedCell, isEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFormulaBarFocus = useCallback(() => {
|
const handleFormulaBarFocus = useCallback(() => {
|
||||||
@@ -1225,7 +1280,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isEditing, stopEditing],
|
[isEditing, stopEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFormulaBarKeyDown = useCallback(
|
const handleFormulaBarKeyDown = useCallback(
|
||||||
@@ -1243,13 +1298,13 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[stopEditing, moveSelection, activeSheet],
|
[stopEditing, moveSelection, activeSheet]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onGridScroll = useCallback(
|
const onGridScroll = useCallback(
|
||||||
(
|
(
|
||||||
sheetType: SheetType,
|
sheetType: SheetType,
|
||||||
{ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number },
|
{ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number }
|
||||||
) => {
|
) => {
|
||||||
if (headerRefs.current[sheetType]) {
|
if (headerRefs.current[sheetType]) {
|
||||||
headerRefs.current[sheetType]!.scrollLeft = scrollLeft
|
headerRefs.current[sheetType]!.scrollLeft = scrollLeft
|
||||||
@@ -1258,10 +1313,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop
|
rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [separatorRef, setSeparatorRef] = useState<HTMLDivElement | null>(null)
|
|
||||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
|
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
|
||||||
L: 0.5,
|
L: 0.5,
|
||||||
R: 0.5,
|
R: 0.5,
|
||||||
@@ -1283,9 +1337,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
templateId,
|
templateId,
|
||||||
autoSaveDelay: 2000,
|
autoSaveDelay: 2000,
|
||||||
enableAutoSave: true,
|
enableAutoSave: true,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Используем хук для перетаскивания разделителя
|
||||||
|
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
|
||||||
|
// Используем requestAnimationFrame для более плавного обновления
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setSpreadsheetWidths({
|
||||||
|
L: newLeftPercentage,
|
||||||
|
R: 1 - newLeftPercentage,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const separatorRef = useDraggableDivider(handleDividerDrag)
|
||||||
|
|
||||||
// Загружаем сохраненные формулы при инициализации
|
// Загружаем сохраненные формулы при инициализации
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!templateId || isInitialized) return
|
if (!templateId || isInitialized) return
|
||||||
@@ -1293,7 +1360,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
// Проверяем, есть ли начальные данные
|
// Проверяем, есть ли начальные данные
|
||||||
const hasInitialData =
|
const hasInitialData =
|
||||||
Object.keys(templateData).length > 0 &&
|
Object.keys(templateData).length > 0 &&
|
||||||
Object.values(templateData).some((sheet) => Object.keys(sheet).length > 0)
|
Object.values(templateData).some(sheet => Object.keys(sheet).length > 0)
|
||||||
|
|
||||||
// Всегда загружаем сохраненные данные, если они есть
|
// Всегда загружаем сохраненные данные, если они есть
|
||||||
const savedFormulas = loadSavedFormulas()
|
const savedFormulas = loadSavedFormulas()
|
||||||
@@ -1348,47 +1415,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
// --- Spreadsheet Component ---
|
// --- Spreadsheet Component ---
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!separatorRef) return
|
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
document.body.style.cursor = 'col-resize'
|
|
||||||
|
|
||||||
const handleMouseMove = (me: MouseEvent) => {
|
|
||||||
const container = separatorRef.parentElement
|
|
||||||
if (!container) return
|
|
||||||
const bounds = container.getBoundingClientRect()
|
|
||||||
const newLeftWidth = me.clientX - bounds.left
|
|
||||||
const totalWidth = bounds.width
|
|
||||||
|
|
||||||
let newLeftPercentage = newLeftWidth / totalWidth
|
|
||||||
// Ограничиваем минимальную ширину (например 20%)
|
|
||||||
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage))
|
|
||||||
|
|
||||||
setSpreadsheetWidths({
|
|
||||||
L: newLeftPercentage,
|
|
||||||
R: 1 - newLeftPercentage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
document.body.style.cursor = 'auto'
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
separatorRef.addEventListener('mousedown', handleMouseDown)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
separatorRef.removeEventListener('mousedown', handleMouseDown)
|
|
||||||
}
|
|
||||||
}, [separatorRef])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-background">
|
<div className="flex h-full flex-col bg-background">
|
||||||
<FormulaBar
|
<FormulaBar
|
||||||
@@ -1400,7 +1426,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
onFocus={handleFormulaBarFocus}
|
onFocus={handleFormulaBarFocus}
|
||||||
onBlur={handleFormulaBarBlur}
|
onBlur={handleFormulaBarBlur}
|
||||||
onKeyDown={handleFormulaBarKeyDown}
|
onKeyDown={handleFormulaBarKeyDown}
|
||||||
formulaBarRef={(el) => {
|
formulaBarRef={el => {
|
||||||
formulaBarRef.current = el
|
formulaBarRef.current = el
|
||||||
}}
|
}}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
@@ -1409,61 +1435,44 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
onManualSave={manualSave}
|
onManualSave={manualSave}
|
||||||
onClearError={clearError}
|
onClearError={clearError}
|
||||||
/>
|
/>
|
||||||
<div className="flex min-w-0 flex-1 gap-1 overflow-hidden">
|
<div
|
||||||
<Spreadsheet
|
className="flex min-w-0 flex-1 gap-1 overflow-hidden"
|
||||||
sheetType="report"
|
style={{ transition: 'none' }}
|
||||||
columnWidths={columnWidths.report}
|
>
|
||||||
spreadsheetWidths={spreadsheetWidths}
|
{SHEET_TYPES.map((sheetType, index) => (
|
||||||
getCachedCellData={getCachedCellData}
|
<Spreadsheet
|
||||||
selectedCell={selectedCell}
|
key={sheetType}
|
||||||
activeSheet={activeSheet}
|
sheetType={sheetType}
|
||||||
isEditing={isEditing}
|
columnWidths={columnWidths[sheetType]}
|
||||||
editingValue={editingValue}
|
spreadsheetWidths={spreadsheetWidths}
|
||||||
handleCellClick={handleCellClick}
|
getCachedCellData={getCachedCellData}
|
||||||
handleCellDoubleClick={handleCellDoubleClick}
|
selectedCell={selectedCell}
|
||||||
handleCellChange={handleCellChange}
|
activeSheet={activeSheet}
|
||||||
stopEditing={stopEditing}
|
isEditing={isEditing}
|
||||||
setActiveSheet={setActiveSheet}
|
editingValue={editingValue}
|
||||||
activeCellInputRef={activeCellInputRef}
|
handleCellClick={handleCellClick}
|
||||||
containerRefs={containerRefs}
|
handleCellDoubleClick={handleCellDoubleClick}
|
||||||
handleKeyDown={handleKeyDown}
|
handleCellChange={handleCellChange}
|
||||||
headerRefs={headerRefs}
|
stopEditing={stopEditing}
|
||||||
rowHeaderRefs={rowHeaderRefs}
|
setActiveSheet={setActiveSheet}
|
||||||
gridRefs={gridRefs}
|
activeCellInputRef={activeCellInputRef}
|
||||||
onGridScroll={onGridScroll}
|
containerRefs={containerRefs}
|
||||||
handleColumnResize={handleColumnResize}
|
handleKeyDown={handleKeyDown}
|
||||||
mergedCells={mergedCells}
|
headerRefs={headerRefs}
|
||||||
/>
|
rowHeaderRefs={rowHeaderRefs}
|
||||||
|
gridRefs={gridRefs}
|
||||||
|
onGridScroll={onGridScroll}
|
||||||
|
handleColumnResize={handleColumnResize}
|
||||||
|
mergedCells={mergedCells}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<div
|
<div
|
||||||
ref={setSeparatorRef}
|
ref={separatorRef}
|
||||||
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
|
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
>
|
>
|
||||||
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
|
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
|
||||||
</div>
|
</div>
|
||||||
<Spreadsheet
|
|
||||||
sheetType="calculations"
|
|
||||||
columnWidths={columnWidths.calculations}
|
|
||||||
spreadsheetWidths={spreadsheetWidths}
|
|
||||||
getCachedCellData={getCachedCellData}
|
|
||||||
selectedCell={selectedCell}
|
|
||||||
activeSheet={activeSheet}
|
|
||||||
isEditing={isEditing}
|
|
||||||
editingValue={editingValue}
|
|
||||||
handleCellClick={handleCellClick}
|
|
||||||
handleCellDoubleClick={handleCellDoubleClick}
|
|
||||||
handleCellChange={handleCellChange}
|
|
||||||
stopEditing={stopEditing}
|
|
||||||
setActiveSheet={setActiveSheet}
|
|
||||||
activeCellInputRef={activeCellInputRef}
|
|
||||||
containerRefs={containerRefs}
|
|
||||||
handleKeyDown={handleKeyDown}
|
|
||||||
headerRefs={headerRefs}
|
|
||||||
rowHeaderRefs={rowHeaderRefs}
|
|
||||||
gridRefs={gridRefs}
|
|
||||||
onGridScroll={onGridScroll}
|
|
||||||
handleColumnResize={handleColumnResize}
|
|
||||||
mergedCells={mergedCells}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
64
src/components/TemplateManager/ExcelUploadPanel.tsx
Normal file
64
src/components/TemplateManager/ExcelUploadPanel.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { FileText, Upload } from 'lucide-react'
|
||||||
|
import React, { useRef } from 'react'
|
||||||
|
|
||||||
|
interface ExcelUploadPanelProps {
|
||||||
|
fileName?: string
|
||||||
|
isLoading: boolean
|
||||||
|
onUploadClick: () => void
|
||||||
|
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcelUploadPanel: React.FC<ExcelUploadPanelProps> = ({
|
||||||
|
fileName,
|
||||||
|
isLoading,
|
||||||
|
onUploadClick,
|
||||||
|
onFileChange,
|
||||||
|
}) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
onChange={onFileChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 rounded-md border px-2 py-1 ${
|
||||||
|
fileName
|
||||||
|
? 'border-emerald-200 bg-emerald-50'
|
||||||
|
: 'border-gray-200 bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText
|
||||||
|
className={`h-3 w-3 ${fileName ? 'text-emerald-600' : 'text-gray-400'}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`max-w-32 truncate text-xs font-medium ${
|
||||||
|
fileName ? 'text-emerald-700' : 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{fileName || 'Файл не выбран'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="h-7 rounded-md px-2"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
|
) : (
|
||||||
|
<Upload className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
61
src/components/TemplateManager/FormattingToolbar.tsx
Normal file
61
src/components/TemplateManager/FormattingToolbar.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import {
|
||||||
|
AlignCenter,
|
||||||
|
AlignLeft,
|
||||||
|
AlignRight,
|
||||||
|
Bold,
|
||||||
|
Grid,
|
||||||
|
Italic,
|
||||||
|
Palette,
|
||||||
|
Type,
|
||||||
|
Underline,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const FormattingToolbar: React.FC = () => (
|
||||||
|
<div className="flex items-center gap-0.5 rounded-lg border bg-background p-0.5">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{[Bold, Italic, Underline].map((Icon, i) => (
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 rounded p-0"
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="mx-0.5 h-4" />
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{[AlignLeft, AlignCenter, AlignRight].map((Icon, i) => (
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 rounded p-0"
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="mx-0.5 h-4" />
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{[Palette, Type, Grid].map((Icon, i) => (
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 rounded p-0"
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
52
src/components/TemplateManager/HeaderBar.tsx
Normal file
52
src/components/TemplateManager/HeaderBar.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Template } from '@/types/template'
|
||||||
|
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface HeaderBarProps {
|
||||||
|
template: Template
|
||||||
|
onBack: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold">{template.name}</h1>
|
||||||
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
Настройте Excel шаблон и значения отчета
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Wrench className="h-3.5 w-3.5" />
|
||||||
|
Элементы интерфейса
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
Создать протокол
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
src/components/TemplateManager/SpreadsheetViewer.tsx
Normal file
26
src/components/TemplateManager/SpreadsheetViewer.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet'
|
||||||
|
import { ExcelParseResult } from '@/services/excelSevice'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface SpreadsheetViewerProps {
|
||||||
|
data: Record<string, any>
|
||||||
|
mergedCells: ExcelParseResult['mergedCells']
|
||||||
|
dataVersion: number
|
||||||
|
templateId: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpreadsheetViewer: React.FC<SpreadsheetViewerProps> = ({
|
||||||
|
data,
|
||||||
|
mergedCells,
|
||||||
|
dataVersion,
|
||||||
|
templateId,
|
||||||
|
}) => (
|
||||||
|
<div className="min-h-0 flex-1">
|
||||||
|
<DualSpreadsheet
|
||||||
|
key={dataVersion}
|
||||||
|
templateData={{ L: data }}
|
||||||
|
mergedCells={mergedCells}
|
||||||
|
templateId={`template-${templateId}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
@@ -1,25 +1,10 @@
|
|||||||
import {
|
import { parseExcelFile } from '@/services/excelSevice'
|
||||||
AlignCenter,
|
import { Template } from '@/types/template'
|
||||||
AlignLeft,
|
import React, { useState } from 'react'
|
||||||
AlignRight,
|
import { ExcelUploadPanel } from './ExcelUploadPanel'
|
||||||
ArrowLeft,
|
import { FormattingToolbar } from './FormattingToolbar'
|
||||||
Bold,
|
import { HeaderBar } from './HeaderBar'
|
||||||
FileText,
|
import { SpreadsheetViewer } from './SpreadsheetViewer'
|
||||||
Grid,
|
|
||||||
Italic,
|
|
||||||
Palette,
|
|
||||||
Type,
|
|
||||||
Underline,
|
|
||||||
Upload,
|
|
||||||
Wrench,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import React, { useRef, useState } from 'react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import * as XLSX from 'xlsx'
|
|
||||||
import { Template } from '../../types/template'
|
|
||||||
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet'
|
|
||||||
import { Button } from '../ui/button'
|
|
||||||
import { Separator } from '../ui/separator'
|
|
||||||
|
|
||||||
interface TemplateEditorProps {
|
interface TemplateEditorProps {
|
||||||
template: Template
|
template: Template
|
||||||
@@ -30,284 +15,56 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
template,
|
template,
|
||||||
onBack,
|
onBack,
|
||||||
}) => {
|
}) => {
|
||||||
const [editedTemplate, setEditedTemplate] = useState<Template>(template)
|
const [editedTemplate, setEditedTemplate] = useState(template)
|
||||||
const [excelData, setExcelData] = useState<Record<string, any>>(
|
const [excelData, setExcelData] = useState<Record<string, any>>(
|
||||||
template.excelData || {},
|
template.excelData || {}
|
||||||
)
|
)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [dataVersion, setDataVersion] = useState(0) // Для принудительного
|
const [dataVersion, setDataVersion] = useState(0)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0]
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
console.log('No file selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const reader = new FileReader()
|
try {
|
||||||
|
const { data, mergedCells } = await parseExcelFile(file)
|
||||||
reader.onload = (e) => {
|
setExcelData(data)
|
||||||
try {
|
setEditedTemplate(prev => ({
|
||||||
const data = new Uint8Array(e.target?.result as ArrayBuffer)
|
...prev,
|
||||||
const workbook = XLSX.read(data, { type: 'array' })
|
excelFile: file,
|
||||||
|
excelData: data,
|
||||||
// Читаем первый лист
|
mergedCells,
|
||||||
const firstSheetName = workbook.SheetNames[0]
|
updatedAt: new Date(),
|
||||||
const worksheet = workbook.Sheets[firstSheetName]
|
}))
|
||||||
|
setDataVersion(v => v + 1)
|
||||||
// Получаем диапазон ячеек
|
} catch (err) {
|
||||||
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1')
|
console.error(err)
|
||||||
|
alert('Ошибка при чтении Excel файла')
|
||||||
// Преобразуем в формат для нашей системы
|
} finally {
|
||||||
const formattedData: Record<string, any> = {}
|
|
||||||
const mergedCells: Array<{ from: string; to: string; value: any }> = []
|
|
||||||
|
|
||||||
// Обрабатываем объединенные ячейки
|
|
||||||
const merges = worksheet['!merges'] || []
|
|
||||||
|
|
||||||
// Сначала читаем обычные ячейки
|
|
||||||
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
||||||
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
||||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
|
||||||
const cell = worksheet[cellAddress]
|
|
||||||
|
|
||||||
if (
|
|
||||||
cell &&
|
|
||||||
cell.v !== undefined &&
|
|
||||||
cell.v !== null &&
|
|
||||||
cell.v !== ''
|
|
||||||
) {
|
|
||||||
formattedData[cellAddress] = cell.v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обрабатываем объединенные ячейки
|
|
||||||
merges.forEach((merge) => {
|
|
||||||
const startCell = XLSX.utils.encode_cell({
|
|
||||||
r: merge.s.r,
|
|
||||||
c: merge.s.c,
|
|
||||||
})
|
|
||||||
const endCell = XLSX.utils.encode_cell({ r: merge.e.r, c: merge.e.c })
|
|
||||||
const startCellData = worksheet[startCell]
|
|
||||||
|
|
||||||
if (startCellData && startCellData.v !== undefined) {
|
|
||||||
// Записываем значение в первую ячейку объединенного диапазона
|
|
||||||
formattedData[startCell] = startCellData.v
|
|
||||||
|
|
||||||
// Сохраняем информацию об объединении для UI
|
|
||||||
mergedCells.push({
|
|
||||||
from: startCell,
|
|
||||||
to: endCell,
|
|
||||||
value: startCellData.v,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Заполняем остальные ячейки в объединенном диапазоне пустыми значениями
|
|
||||||
// но помечаем их как часть объединения
|
|
||||||
for (let R = merge.s.r; R <= merge.e.r; ++R) {
|
|
||||||
for (let C = merge.s.c; C <= merge.e.c; ++C) {
|
|
||||||
const cellAddr = XLSX.utils.encode_cell({ r: R, c: C })
|
|
||||||
if (cellAddr !== startCell) {
|
|
||||||
formattedData[cellAddr] = '' // Пустое значение для объединенных ячеек
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Обновляем состояние
|
|
||||||
const newData = { ...formattedData }
|
|
||||||
setExcelData(newData)
|
|
||||||
setEditedTemplate((prev) => ({
|
|
||||||
...prev,
|
|
||||||
excelFile: file,
|
|
||||||
excelData: newData,
|
|
||||||
mergedCells: mergedCells, // Сохраняем информацию об объединенных ячейках
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Принудительно обновляем версию данных для перерендера
|
|
||||||
setDataVersion((prev) => prev + 1)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при чтении Excel файла:', error)
|
|
||||||
alert('Ошибка при чтении Excel файла')
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.onerror = () => {
|
|
||||||
alert('Ошибка при загрузке файла')
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateElements = () => {
|
|
||||||
navigate(`/templates/${editedTemplate.id}/elements`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-background">
|
<div className="flex h-screen flex-col bg-background">
|
||||||
{/* Заголовок */}
|
<HeaderBar template={editedTemplate} onBack={onBack} />
|
||||||
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-semibold">{editedTemplate.name}</h1>
|
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
||||||
Настройте Excel шаблон и значения отчета
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCreateElements}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Wrench className="h-3.5 w-3.5" />
|
|
||||||
Элементы интерфейса
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/templates/${editedTemplate.id}/protocols`)
|
|
||||||
}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FileText className="h-3.5 w-3.5" />
|
|
||||||
Создать протокол
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Компактная панель загрузки Excel и инструментов */}
|
|
||||||
<div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2">
|
<div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Левая часть - панель инструментов стилизации */}
|
<FormattingToolbar />
|
||||||
<div className="flex items-center gap-0.5 rounded-lg border bg-background p-0.5">
|
<ExcelUploadPanel
|
||||||
{/* Группа текстового форматирования */}
|
fileName={editedTemplate.excelFile?.name}
|
||||||
<div className="flex items-center">
|
isLoading={isLoading}
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
onUploadClick={() => {}}
|
||||||
<Bold className="h-3 w-3" />
|
onFileChange={handleFileChange}
|
||||||
</Button>
|
/>
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<Italic className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<Underline className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-0.5 h-4" />
|
|
||||||
|
|
||||||
{/* Группа выравнивания */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<AlignLeft className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<AlignCenter className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<AlignRight className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-0.5 h-4" />
|
|
||||||
|
|
||||||
{/* Группа цвета и стилей */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<Palette className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<Type className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
|
|
||||||
<Grid className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Правая часть - загрузка Excel */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".xlsx,.xls"
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Статус файла - всегда отображается */}
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-1.5 rounded-md border px-2 py-1 ${
|
|
||||||
editedTemplate.excelFile
|
|
||||||
? 'border-emerald-200 bg-emerald-50'
|
|
||||||
: 'border-gray-200 bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FileText
|
|
||||||
className={`h-3 w-3 ${
|
|
||||||
editedTemplate.excelFile
|
|
||||||
? 'text-emerald-600'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`max-w-32 truncate text-xs font-medium ${
|
|
||||||
editedTemplate.excelFile
|
|
||||||
? 'text-emerald-700'
|
|
||||||
: 'text-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{editedTemplate.excelFile
|
|
||||||
? editedTemplate.excelFile.name
|
|
||||||
: 'Файл не выбран'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Кнопка загрузки - всегда отображается */}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
className="h-7 rounded-md px-2"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
||||||
) : (
|
|
||||||
<Upload className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SpreadsheetViewer
|
||||||
{/* Таблицы - теперь занимают всё оставшееся пространство */}
|
data={excelData}
|
||||||
<div className="min-h-0 flex-1">
|
mergedCells={editedTemplate.mergedCells || []}
|
||||||
<DualSpreadsheet
|
dataVersion={dataVersion}
|
||||||
key={dataVersion}
|
templateId={editedTemplate.id}
|
||||||
templateData={{ L: excelData }}
|
/>
|
||||||
mergedCells={editedTemplate.mergedCells}
|
|
||||||
templateId={`template-${editedTemplate.id}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/services/excelSevice.ts
Normal file
58
src/services/excelSevice.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
|
export interface ExcelParseResult {
|
||||||
|
data: Record<string, any>
|
||||||
|
mergedCells: Array<{ from: string; to: string; value: any }>
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch(/api/upload)
|
||||||
|
export async function parseExcelFile(file: File): Promise<ExcelParseResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = e => {
|
||||||
|
try {
|
||||||
|
const data = new Uint8Array(e.target?.result as ArrayBuffer)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const firstSheetName = workbook.SheetNames[0]
|
||||||
|
const worksheet = workbook.Sheets[firstSheetName]
|
||||||
|
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1')
|
||||||
|
const formattedData: Record<string, any> = {}
|
||||||
|
const mergedCells: ExcelParseResult['mergedCells'] = []
|
||||||
|
|
||||||
|
// Основные ячейки
|
||||||
|
for (let R = range.s.r; R <= range.e.r; ++R) {
|
||||||
|
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||||||
|
const addr = XLSX.utils.encode_cell({ r: R, c: C })
|
||||||
|
const cell = worksheet[addr]
|
||||||
|
if (cell && cell.v != null && cell.v !== '') {
|
||||||
|
formattedData[addr] = cell.v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработать объединения
|
||||||
|
;(worksheet['!merges'] || []).forEach(m => {
|
||||||
|
const from = XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })
|
||||||
|
const to = XLSX.utils.encode_cell({ r: m.e.r, c: m.e.c })
|
||||||
|
const val = worksheet[from]?.v
|
||||||
|
if (val != null) {
|
||||||
|
mergedCells.push({ from, to, value: val })
|
||||||
|
// остальные ячейки затираем пустым, чтобы UI знал об объединении
|
||||||
|
for (let R = m.s.r; R <= m.e.r; ++R) {
|
||||||
|
for (let C = m.s.c; C <= m.e.c; ++C) {
|
||||||
|
const addr = XLSX.utils.encode_cell({ r: R, c: C })
|
||||||
|
if (addr !== from) formattedData[addr] = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve({ data: formattedData, mergedCells })
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = () => reject(new Error('Ошибка чтения файла'))
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: 'var(--radius)',
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user