234 lines
9.0 KiB
TypeScript
234 lines
9.0 KiB
TypeScript
import { Save, Sigma } from 'lucide-react'
|
||
import { memo } from 'react'
|
||
import { ExcelUploadPanel } from '../../TemplateManager/ExcelUploadPanel'
|
||
import { FormulaBarProps } from '../types'
|
||
import { getColumnLabel } from '../utils'
|
||
|
||
export const FormulaBar = memo(
|
||
({
|
||
selectedCell,
|
||
formulaBarValue,
|
||
isCalculating,
|
||
isEditing,
|
||
onValueChange,
|
||
onFocus,
|
||
onBlur,
|
||
onKeyDown,
|
||
formulaBarRef,
|
||
isSaving = false,
|
||
isLoading = false,
|
||
hasUnsavedChanges = false,
|
||
lastSaveError = null,
|
||
onManualSave,
|
||
onClearError,
|
||
excelFileName,
|
||
isExcelLoading = false,
|
||
onExcelFileChange,
|
||
}: Omit<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
|
||
return (
|
||
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1.5">
|
||
<div className="flex items-center space-x-3">
|
||
{/* Адрес ячейки */}
|
||
<div className="flex min-w-[45px] items-center justify-center rounded border bg-muted px-2 py-1">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
{selectedCell
|
||
? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}`
|
||
: 'A1'}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Строка формул */}
|
||
<div className="flex flex-1 items-center space-x-2">
|
||
<div className="flex items-center text-muted-foreground">
|
||
<Sigma className="h-5 w-5" />
|
||
</div>
|
||
<input
|
||
ref={formulaBarRef}
|
||
type="text"
|
||
className={`text-m flex-1 rounded border border-input bg-background px-2 py-0.5 outline-none ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${isCalculating ? 'animate-pulse' : ''}`}
|
||
placeholder="Введите формулу или значение..."
|
||
value={formulaBarValue}
|
||
onChange={e => onValueChange(e.target.value)}
|
||
onFocus={onFocus}
|
||
onBlur={e => {
|
||
const relatedTarget = e.relatedTarget as HTMLElement
|
||
if (
|
||
relatedTarget &&
|
||
relatedTarget.closest('input[type="text"]')
|
||
) {
|
||
return
|
||
}
|
||
onBlur(e)
|
||
}}
|
||
onKeyDown={onKeyDown}
|
||
onMouseDown={e => {
|
||
// Если поле не в режиме редактирования, нужно запустить редактирование
|
||
// но сохранить позицию курсора
|
||
if (!isEditing) {
|
||
// Вычисляем позицию клика вручную
|
||
const input = e.currentTarget
|
||
const rect = input.getBoundingClientRect()
|
||
const clickX =
|
||
e.clientX -
|
||
rect.left -
|
||
parseInt(getComputedStyle(input).paddingLeft)
|
||
|
||
// Создаем временный range для определения позиции
|
||
const tempSpan = document.createElement('span')
|
||
tempSpan.textContent = input.value
|
||
tempSpan.style.cssText = getComputedStyle(input).cssText
|
||
tempSpan.style.position = 'absolute'
|
||
tempSpan.style.visibility = 'hidden'
|
||
tempSpan.style.whiteSpace = 'pre'
|
||
document.body.appendChild(tempSpan)
|
||
|
||
let position = 0
|
||
let bestDistance = Infinity
|
||
|
||
for (let i = 0; i <= input.value.length; i++) {
|
||
tempSpan.textContent = input.value.substring(0, i)
|
||
const distance = Math.abs(tempSpan.offsetWidth - clickX)
|
||
if (distance < bestDistance) {
|
||
bestDistance = distance
|
||
position = i
|
||
}
|
||
}
|
||
|
||
document.body.removeChild(tempSpan)
|
||
|
||
// Сохраняем вычисленную позицию в data-атрибуте
|
||
input.dataset.clickPosition = position.toString()
|
||
|
||
// Запускаем редактирование
|
||
onFocus()
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Правая группа: индикаторы и действия */}
|
||
<div className="flex items-center gap-2">
|
||
{/* Индикатор процесса */}
|
||
{(isCalculating || isSaving || isLoading) && (
|
||
<div className="flex items-center text-muted-foreground">
|
||
<svg
|
||
className="h-2.5 w-2.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>
|
||
<span className="ml-1 text-xs">
|
||
{isSaving
|
||
? 'Сохранение...'
|
||
: isLoading
|
||
? 'Загрузка...'
|
||
: 'Расчет...'}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Ошибка сохранения */}
|
||
{lastSaveError && (
|
||
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
|
||
<svg
|
||
className="h-3 w-3 text-destructive"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||
/>
|
||
</svg>
|
||
<span
|
||
className="text-xs text-destructive"
|
||
title={lastSaveError}
|
||
>
|
||
Ошибка
|
||
</span>
|
||
{onClearError && (
|
||
<button
|
||
onClick={onClearError}
|
||
className="ml-0.5 text-destructive/60 hover:text-destructive"
|
||
>
|
||
<svg
|
||
className="h-2 w-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ExcelUploadPanel */}
|
||
{onExcelFileChange && (
|
||
<ExcelUploadPanel
|
||
fileName={excelFileName}
|
||
isLoading={isExcelLoading}
|
||
onUploadClick={() => {}}
|
||
onFileChange={onExcelFileChange}
|
||
/>
|
||
)}
|
||
|
||
{/* Кнопка сохранения */}
|
||
{onManualSave && (
|
||
<button
|
||
onClick={onManualSave}
|
||
disabled={isSaving}
|
||
className={`flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors ${
|
||
hasUnsavedChanges
|
||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||
: lastSaveError
|
||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||
: 'bg-muted text-muted-foreground'
|
||
} disabled:opacity-50`}
|
||
title={
|
||
lastSaveError
|
||
? 'Повторить сохранение'
|
||
: hasUnsavedChanges
|
||
? 'Есть несохраненные изменения'
|
||
: 'Все изменения сохранены'
|
||
}
|
||
>
|
||
<Save className="h-3 w-3" />
|
||
<span className="hidden sm:inline">
|
||
{lastSaveError
|
||
? 'Повторить'
|
||
: hasUnsavedChanges
|
||
? 'Сохранить'
|
||
: 'Сохранено'}
|
||
</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
)
|