diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
index 3392699..b2e84e1 100644
--- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
+++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
@@ -1,41 +1,67 @@
-import React, { useEffect, useState } from 'react';
-import { useMemo } from 'react';
-import styled from '@emotion/styled';
+import React, { memo, useMemo, useCallback } from 'react';
import { useRealtime } from '../../contexts/RealtimeContext';
-import { toast } from 'react-toastify';
-const ContainerForText = styled.span`
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 3;
- overflow: hidden;
- white-space: pre-wrap;
-`;
+// Константы вне компонента
+const BASE_CELL_STYLES = {
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ boxSizing: 'border-box',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ border: '2px solid transparent',
+ padding: '8px',
+};
-// Стилизованный компонент для плашки блокировки
-const LockBadge = styled.div`
- position: absolute;
- top: 2px;
- right: 2px;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- font-size: 9px;
- padding: 1px 4px;
- border-radius: 3px;
- pointer-events: none;
- z-index: 10;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- letter-spacing: 0.3px;
- backdrop-filter: blur(4px);
- border: 1px solid rgba(255, 255, 255, 0.1);
-`;
+const LOCKED_STYLES = {
+ border: '2px solid #e0e0e0',
+ backgroundColor: '#f5f5f5',
+ cursor: 'not-allowed',
+ opacity: 0.85,
+};
+
+const LOCK_BADGE_STYLES = {
+ position: 'absolute',
+ top: '2px',
+ right: '2px',
+ background: 'rgba(0, 0, 0, 0.7)',
+ color: 'white',
+ fontSize: '9px',
+ padding: '1px 4px',
+ borderRadius: '3px',
+ pointerEvents: 'none',
+ zIndex: 10,
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+ letterSpacing: '0.3px',
+ backdropFilter: 'blur(4px)',
+ border: '1px solid rgba(255, 255, 255, 0.1)',
+};
+
+const CONTAINER_STYLES = {
+ display: '-webkit-box',
+ WebkitBoxOrient: 'vertical',
+ WebkitLineClamp: 3,
+ overflow: 'hidden',
+ whiteSpace: 'pre-wrap',
+ wordBreak: 'break-word',
+};
+
+const INTEGER_COLUMN_IDS = new Set([
+ 'data.header.num_group',
+ 'data.nomenclature_group_id',
+ 'data.header.item_id',
+ 'data.article_id',
+ 'data.code_razdela',
+]);
-// Функция для определения яркости цвета
const getColorBrightness = (hexColor) => {
if (!hexColor) return 255;
-
const color = hexColor.replace('#', '');
-
let r, g, b;
if (color.length === 3) {
r = parseInt(color[0] + color[0], 16);
@@ -48,174 +74,132 @@ const getColorBrightness = (hexColor) => {
} else {
return 255;
}
-
return (0.299 * r + 0.587 * g + 0.114 * b);
};
-// Функция для подсветки текста
-const highlightText = (text, searchQueries) => {
- if (!text) return text;
- const cleanSearchQueries = searchQueries.filter(q => (q !== undefined && q !== ''));
- if (cleanSearchQueries.length == 0) return text;
-
- const escapedQueries = cleanSearchQueries.map(query => query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
- const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
- const parts = text.split(regex);
-
- return parts.map((part, index) =>
- regex.test(part) ? (
-
- {part}
-
- ) : (
- part
- ),
- );
+const formatNumber = (value, asInteger = false) => {
+ if (value === null || value === undefined || value === '') return String(value || '');
+ const num = Number(value);
+ if (isNaN(num)) return String(value);
+ return num.toLocaleString('ru-RU', {
+ minimumFractionDigits: asInteger ? 0 : 1,
+ maximumFractionDigits: asInteger ? 0 : 1,
+ useGrouping: true,
+ });
};
-const highlightWithFormatting = (originalValue, formattedValue, searchQueries) => {
- if (!originalValue && originalValue !== 0) return formattedValue;
+// Оптимизированная функция подсветки
+const createHighlightedContent = (originalValue, displayValue, searchQueries) => {
+ const cleanQueries = searchQueries.filter(Boolean);
+ if (cleanQueries.length === 0) return displayValue;
- const cleanSearchQueries = searchQueries.filter(q => (q !== undefined && q !== ''));
- if (cleanSearchQueries.length == 0) return formattedValue;
-
- const originalString = String(originalValue);
- const escapedQueries = cleanSearchQueries.map(query => query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
+ const searchStr = String(originalValue ?? displayValue);
+ const escapedQueries = cleanQueries.map(q => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
- if (!regex.test(originalString)) {
- return formattedValue;
- }
+ if (!regex.test(searchStr)) return displayValue;
- return (
- {
+ if (!regex.test(part)) return part;
+ return React.createElement('mark', {
+ key: index,
+ style: {
backgroundColor: '#ffeb3b',
color: '#000',
fontWeight: 'bold',
padding: '0 2px',
borderRadius: '2px',
- }}
- >
- {formattedValue}
-
- );
-};
-
-const INTEGER_COLUMN_IDS = new Set([
- 'data.header.num_group',
- 'data.nomenclature_group_id',
- 'data.header.item_id',
- 'data.article_id',
- 'data.code_razdela',
-]);
-
-// Функция для форматирования числа в финансовый формат
-const formatNumber = (value, asInteger = false) => {
- if (value === null || value === undefined || value === '') return String(value || '');
-
- const num = Number(value);
- if (isNaN(num)) return String(value);
-
- if (asInteger) {
- return num.toLocaleString('ru-RU', {
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- useGrouping: true,
- });
- }
-
- return num.toLocaleString('ru-RU', {
- minimumFractionDigits: 1,
- maximumFractionDigits: 1,
- useGrouping: true,
+ },
+ }, part);
});
};
-const Cell = React.memo(({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, color, isEditable }) => {
+const CellComponent = ({
+ cell,
+ row,
+ column,
+ onClick,
+ globalFilter,
+ columnFilter,
+ backgroundColor,
+ color,
+ isEditable,
+ isUpdating,
+ onCellNumberClick,
+}) => {
const { lockedCells } = useRealtime();
const cellKey = `${row.id}_${column.id}`;
-
const isLocked = lockedCells.includes(cellKey);
- const cellValue = cell.getValue();
- let originalValue = cellValue;
- let displayValue = String(cellValue || '');
+ // Мемоизация значения
+ const { rawValue, displayValue, isNumeric } = useMemo(() => {
+ const value = cell.getValue();
+ const isNum = !isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
+ let display = String(value || '');
- const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
- if (isNumeric) {
- const asInteger = INTEGER_COLUMN_IDS.has(column.id);
- displayValue = formatNumber(cellValue, asInteger);
- }
+ if (isNum) {
+ const asInteger = INTEGER_COLUMN_IDS.has(column.id);
+ display = formatNumber(value, asInteger);
+ }
+
+ return { rawValue: value, displayValue: display, isNumeric: isNum };
+ }, [cell, column.id]);
const highlightedContent = useMemo(() => {
- if (!isNumeric) {
- return highlightText(displayValue, [globalFilter, columnFilter]);
- }
- return highlightWithFormatting(originalValue, displayValue, [globalFilter, columnFilter]);
- }, [originalValue, displayValue, globalFilter, columnFilter, isNumeric]);
+ const searchQueries = [globalFilter, columnFilter];
+ return createHighlightedContent(rawValue, displayValue, searchQueries);
+ }, [rawValue, displayValue, globalFilter, columnFilter]);
- // Определяем цвет текста на основе яркости фона
- const getTextColor = () => {
+ const textColor = useMemo(() => {
+ if (isLocked) return '#999999';
const brightness = getColorBrightness(backgroundColor);
-
- // Если яркость меньше 128 (темный фон) - используем белый текст, иначе черный
return brightness < 128 ? '#ffffff' : '#000000';
- };
+ }, [backgroundColor, isLocked]);
- // Базовые стили для заблокированной ячейки
- const lockedStyles = isLocked ? {
- border: '2px solid #e0e0e0',
- backgroundColor: '#f5f5f5',
- cursor: 'not-allowed',
- opacity: 0.85,
- } : {};
+ const cellStyles = useMemo(() => ({
+ ...BASE_CELL_STYLES,
+ backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
+ color: textColor,
+ ...(isLocked ? LOCKED_STYLES : {}),
+ }), [backgroundColor, isLocked, textColor]);
+ const lockBadge = useMemo(() => {
+ if (!isLocked) return null;
+ return
🔒
;
+ }, [isLocked]);
-
- // Определяем финальный цвет текста
- const finalTextColor = isLocked ? '#999999' : getTextColor();
+ const handleClick = useCallback(() => {
+ if (!isLocked && onClick) {
+ onClick();
+ }
+ }, [isLocked, onClick]);
return (
- {highlightedContent}
- {isLocked && (
-
- 🔒
-
- )}
+ {highlightedContent}
+ {lockBadge}
);
+};
+
+const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
+ // Возвращаем true если пропсы равны (не нужно перерендеривать)
+ return (
+ prevProps.cell.getValue() === nextProps.cell.getValue() &&
+ prevProps.row.id === nextProps.row.id &&
+ prevProps.column.id === nextProps.column.id &&
+ prevProps.globalFilter === nextProps.globalFilter &&
+ prevProps.columnFilter === nextProps.columnFilter &&
+ prevProps.backgroundColor === nextProps.backgroundColor &&
+ prevProps.isEditable === nextProps.isEditable &&
+ prevProps.isUpdating === nextProps.isUpdating &&
+ prevProps.onCellNumberClick === nextProps.onCellNumberClick
+ );
});
export default Cell;
\ No newline at end of file
diff --git a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx
index 0f87650..b9b4554 100644
--- a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx
+++ b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx
@@ -1,195 +1,228 @@
-// EditCell.js - обновленная версия
-import { useEffect, useRef, useLayoutEffect, useState } from 'react';
+import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
import { useRealtime } from '../../contexts/RealtimeContext';
-const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
+const EDITOR_STYLES = {
+ position: 'absolute',
+ zIndex: 12,
+ top: 0,
+};
+
+const FORMULA_INDICATOR_STYLES = {
+ position: 'absolute',
+ top: '-20px',
+ left: '4px',
+ fontSize: '10px',
+ color: '#666',
+ background: '#f0f0f0',
+ padding: '2px 6px',
+ borderRadius: '3px',
+ pointerEvents: 'none',
+};
+
+const TEXTAREA_STYLES = {
+ width: '100%',
+ height: 'auto',
+ fieldSizing: 'content',
+ fontSize: 'inherit',
+};
+
+const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
const { endEditing: contextEndEditing } = useRealtime();
-
- const [curValue, setCurValue] = useState(value);
- const [displayValue, setDisplayValue] = useState(value);
- const [isFormulaMode, setIsFormulaMode] = useState(false);
-
- // Ключ для хранения формул в localStorage
const storageKey = `formula_${tableId}_${cellId}`;
- const trFrag = refCell.current.offsetParent.offsetParent;
- const tdFrag = refCell.current.offsetParent;
- const left = tdFrag.offsetLeft;
- const width = refCell.current.offsetParent.offsetWidth;
- const translateY = trFrag.style.transform;
+ // Объединенное состояние
+ const [state, setState] = useState(() => ({
+ currentValue: value,
+ displayValue: value,
+ isFormulaMode: false,
+ }));
- // Загружаем сохраненную формулу при монтировании
+ // Позиция ячейки
+ const [position, setPosition] = useState({ left: 0, width: 0, translateY: '' });
+ const textareaRef = useRef(null);
+
+ const refFinished = useRef(false);
+
+ // Загрузка сохраненной формулы
useEffect(() => {
const savedFormula = loadFromStorage(storageKey);
if (savedFormula && isFormula(savedFormula)) {
- setIsFormulaMode(true);
- setCurValue(savedFormula);
- // Вычисляем и отображаем результат
try {
const formula = extractFormula(savedFormula);
const result = calculateFormula(formula);
- setDisplayValue(formatNumber(result));
- } catch (error) {
- setDisplayValue('#ОШИБКА');
+ setState({
+ currentValue: savedFormula,
+ displayValue: formatNumber(result),
+ isFormulaMode: true,
+ });
+ } catch {
+ setState({
+ currentValue: savedFormula,
+ displayValue: '#ОШИБКА',
+ isFormulaMode: true,
+ });
}
- } else {
- setDisplayValue(value);
}
- }, [storageKey, value]);
+ }, [storageKey]);
- function useAutoResize(value) {
- const ref = useRef(null);
+ // Вычисление позиции
+ useLayoutEffect(() => {
+ if (!refCell?.current) return;
- useLayoutEffect(() => {
- const el = ref.current;
- if (!el) return;
+ const td = refCell.current.offsetParent;
+ const tr = td?.offsetParent;
+ if (td && tr) {
+ setPosition({
+ left: td.offsetLeft,
+ width: td.offsetWidth,
+ translateY: tr.style.transform || '',
+ });
+ }
+ }, [refCell]);
+
+ // Auto-resize
+ useLayoutEffect(() => {
+ const el = textareaRef.current;
+ if (!el) return;
+
+ const resize = () => {
el.style.height = 'auto';
el.style.minHeight = '3rem';
el.style.height = el.scrollHeight + 'px';
el.style.maxHeight = '180px';
- }, [value]);
+ };
- return ref;
- }
+ resize();
+ const observer = new ResizeObserver(resize);
+ observer.observe(el);
- const ref = useAutoResize(value);
+ return () => observer.disconnect();
+ }, [state.currentValue]);
+ // Фокус при монтировании
useEffect(() => {
- if (ref.current && !disabled) {
- ref.current.focus();
- ref.current.setSelectionRange(
- ref.current.value.length,
- ref.current.value.length,
- );
+ const el = textareaRef.current;
+ if (el && !disabled) {
+ el.focus();
+ el.setSelectionRange(el.value.length, el.value.length);
}
- }, [disabled, ref]);
+ }, [disabled]);
- const handleSave = () => {
- // Проверяем, является ли введенное значение формулой
- if (isFormula(curValue)) {
- // Сохраняем формулу в localStorage
- saveToStorage(storageKey, curValue);
- // Вычисляем и сохраняем результат
+ // Сохранение
+ const handleSave = useCallback(() => {
+ const { currentValue } = state;
+
+ if (isFormula(currentValue)) {
+ saveToStorage(storageKey, currentValue);
try {
- const formula = extractFormula(curValue);
+ const formula = extractFormula(currentValue);
const result = calculateFormula(formula);
const formattedResult = formatNumber(result);
- setDisplayValue(formattedResult);
+ setState(prev => ({
+ ...prev,
+ displayValue: formattedResult,
+ isFormulaMode: true,
+ }));
onChange?.(formattedResult);
- setIsFormulaMode(true);
- return;
- } catch (error) {
- setDisplayValue('#ОШИБКА');
- onChange?.(curValue); // Сохраняем как текст, если ошибка
- return;
+ } catch {
+ setState(prev => ({
+ ...prev,
+ displayValue: '#ОШИБКА',
+ isFormulaMode: true,
+ }));
+ onChange?.(currentValue);
}
} else {
- // Если это не формула, удаляем сохраненную формулу
localStorage.removeItem(storageKey);
- setIsFormulaMode(false);
- setDisplayValue(curValue);
- onChange?.(curValue);
+ setState(prev => ({
+ ...prev,
+ displayValue: currentValue,
+ isFormulaMode: false,
+ }));
+ onChange?.(currentValue);
}
- };
+ }, [state, storageKey, onChange]);
- const handleChangeValue = (event) => {
- const input = event.target.value;
- setCurValue(input);
- // При редактировании показываем введенный текст
- setDisplayValue(input);
- };
-
- // Функция для отображения значения в ячейке
- const getDisplayValue = () => {
- if (isFormulaMode && isFormula(curValue)) {
- return displayValue;
- }
- return displayValue;
- };
-
- const finishEditing = () => {
+ // Завершение редактирования
+ const finishEditing = useCallback(() => {
+ if(refFinished.current) return;
+ refFinished.current = true;
if (!disabled) {
handleSave();
}
- const editingCell = table.getState().editingCell || null;
+ const editingCell = table.getState().editingCell;
if (editingCell) {
contextEndEditing?.(editingCell.row, editingCell.column);
}
- };
+ }, [disabled, handleSave, table, contextEndEditing]);
useEffect(() => {
const handleClickOutside = (event) => {
- // Проверяем, был ли клик внутри элемента редактирования
- const editElement = ref.current;
- if (editElement && !editElement.contains(event.target)) {
+ const el = textareaRef.current;
+ if (el && !el.contains(event.target)) {
finishEditing();
}
};
-
document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [finishEditing]);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [ref, finishEditing]);
+ // Обработчики событий
+ const handleChange = useCallback((e) => {
+ const value = e.target.value;
+ setState(prev => ({
+ ...prev,
+ currentValue: value,
+ displayValue: value,
+ }));
+ }, []);
+
+ const handleKeyDown = useCallback((e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ finishEditing();
+ }
+ }, [finishEditing]);
+
+ // Мемоизация стилей
+ const editorStyles = useMemo(() => ({
+ ...EDITOR_STYLES,
+ left: position.left,
+ width: position.width,
+ transform: position.translateY,
+ }), [position]);
+
+ const textareaStyles = useMemo(() => ({
+ ...TEXTAREA_STYLES,
+ backgroundColor: state.isFormulaMode ? '#f8f9fa' : 'white',
+ border: state.isFormulaMode ? '2px solid #4a90d9' : undefined,
+ }), [state.isFormulaMode]);
return (
-
- {isFormulaMode && (
-
- fx
-
+
+ {state.isFormulaMode && (
+ fx
)}
|
);
-};
+});
+
+EditCell.displayName = 'EditCell';
export default EditCell;
\ No newline at end of file
diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx
index c3bdbac..bb47be2 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -28,6 +28,7 @@ import { CircularProgress } from '@mui/material';
import { additionVspRowTable } from './constants/addingRowConfig';
import { SelectVspModal } from './Modals/SelectVspModal';
import { getRowId } from './utils/rowUtils';
+import { debounce } from '@mui/material';
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
@@ -47,11 +48,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
return additionVspRowTable[formType].includes(sheetName);
}, [formType, sheetName]);
- const handleGlobalFilterChange = (value) => {
- startTransition(() => {
- setGlobalFilter(value);
- });
- };
+ const handleGlobalFilterChange = useCallback(
+ debounce((value) => {
+ startTransition(() => {
+ setGlobalFilter(value);
+ });
+ }, 300),
+ []
+ );
const {
isConnected,
@@ -89,17 +93,27 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const [columnsConfig, setColumnsConfig] = useState(null);
+ const configCache = useRef(new Map());
+
useEffect(() => {
+ if (!formType || !sheetName) return;
+ const cacheKey = `${formType}_${sheetName}`;
+
+ if (configCache.current.has(cacheKey)) {
+ setColumnsConfig(configCache.current.get(cacheKey));
+ return;
+ }
+
const loadConfig = async () => {
try {
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
+ configCache.current.set(cacheKey, config.config);
setColumnsConfig(config.config);
} catch (error) {
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
setColumnsConfig({ columns: [], colors: {} });
}
};
-
loadConfig();
}, [formType, sheetName]);
@@ -154,7 +168,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
console.error('Error creating columns:', error);
return [];
}
- }, [columnsConfig, handleUpdateCell, handleClickRowCell, handleCellUpdateError]);
+ }, [columnsConfig]);
const selectedColumnIdRef = useRef(selectedColumnId);
@@ -168,7 +182,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}), [handleUpdateCell]);
const ROW_VIRTUALIZER_OPTIONS = {
- overscan: 30,
+ overscan: 5,
scrollPaddingStart: 0,
scrollPaddingEnd: 0,
estimateSize: () => TABLE_ROW_HEIGHT,
@@ -306,43 +320,34 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const table = useMaterialReactTable(tableConfig);
useEffect(() => {
- if (!dataEditingCells || !dataEditingCells.line_id) {
+ if (!dataEditingCells?.line_id) {
setEditingCell(null);
return;
}
if (editingCell) return;
- const rowId = dataEditingCells.line_id;
- const columnId = dataEditingCells.column;
- try {
- const row = table.getRow(rowId);
- const column = table.getColumn(columnId);
- const cell = row?.getVisibleCells().find(c => c.column.id === columnId);
- if (cell) {
- setEditingCell(cell);
+
+ // Отложить поиск до следующего тика
+ requestAnimationFrame(() => {
+ try {
+ const row = table.getRow(dataEditingCells.line_id);
+ const cell = row?.getVisibleCells().find(
+ c => c.column.id === dataEditingCells.column
+ );
+ if (cell) setEditingCell(cell);
+ } catch (error) {
+ console.error(error);
}
- } catch (error) {
- console.error(error);
- toast.error('Ошибка редактирования ячейки');
- }
- }, [dataEditingCells, editingCell]);
+ });
+ }, [dataEditingCells, editingCell, table]);
+
+ const [isFirstLoad, setIsFirstLoad] = useState(true);
useEffect(() => {
- if (table && data && columns.length !== 0) {
- setIsLoadingData(true)
+ if (isFirstLoad && table && data && columns.length !== 0) {
+ setIsFirstLoad(false);
+ setIsLoadingData(true);
}
- }, [data, columns, table])
-
- useEffect(() => {
- // Принудительно обновляем состояние закрепленных колонок
- // если так не сделать при скролле будут пропадать закрепленные колонки
- if (columnPinning && Object.keys(columnPinning).length > 0) {
- const timer = setTimeout(() => {
- setColumnPinning(prev => ({ ...prev }));
- }, 100);
-
- return () => clearTimeout(timer);
- }
- }, []);
+ }, [data, columns, table, isFirstLoad]);
const handleAddRow = useCallback(() => {
const allRows = table.getRowModel().rows;
diff --git a/web/src/components/RealtimeTable/tableColumns.jsx b/web/src/components/RealtimeTable/tableColumns.jsx
index 1353217..772e03c 100644
--- a/web/src/components/RealtimeTable/tableColumns.jsx
+++ b/web/src/components/RealtimeTable/tableColumns.jsx
@@ -1,11 +1,11 @@
-import React, { useEffect, useRef, useState } from 'react';
+// getTableColumns.js
+import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
-import { columns as mockColumns } from '../../mocks/mockTable';
-import { config } from './constants/FORM_2/AHR';
import { useParams } from 'react-router';
import { useRealtime } from './contexts/RealtimeContext';
-function EditCellPortal({
+// Мемоизированный компонент EditCellPortal
+const EditCellPortal = React.memo(({
cell,
table,
EditCell,
@@ -13,89 +13,78 @@ function EditCellPortal({
onSaveEnd,
onError,
isEditable
-}) {
+}) => {
const { formId, formType, sheetName, direction } = useParams();
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
const ref = useRef(null);
- const [refTbody, setRefTbody] = useState(false);
+ const [refTbody, setRefTbody] = useState(null);
const [isSaving, setIsSaving] = useState(false);
- const isDisabled = false;
const { endEditing: contextEndEditing } = useRealtime();
+ // Оптимизация: вычисляем позицию только при монтировании и изменении cell
useEffect(() => {
if (ref.current) {
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
- setRefTbody(tbodyRef);
+ if (tbodyRef && tbodyRef !== refTbody) {
+ setRefTbody(tbodyRef);
+ }
}
- }, []);
+ }, [refTbody]); // Добавляем refTbody в зависимости
- const handleChange = async (val) => {
+ // Мемоизация обработчика изменения
+ const handleChange = useCallback( (val) => {
table.setEditingCell(null);
try {
if (table.options.meta?.updateCell) {
- const success = await table.options.meta.updateCell(
+ const success = table.options.meta.updateCell(
cell.row,
cell.column,
val
);
- if (success) {
- console.log('Cell updated successfully via WebSocket');
- } else {
+ if (!success) {
console.error('Failed to update cell via WebSocket');
- const errorMsg = 'Не удалось сохранить изменение';
- onError?.(errorMsg);
+ onError?.('Не удалось сохранить изменение');
}
}
} catch (error) {
console.error('Error updating cell:', error);
onError?.(error.message);
}
- };
+ }, [table, cell, onError]);
+
+ // Мемоизация пропсов для EditCell
+ const editCellProps = useMemo(() => ({
+ refCell: ref,
+ cell,
+ value: cell.getValue(),
+ disabled: !isEditable,
+ onChange: handleChange,
+ tableId: tableKey,
+ table,
+ cellId: `${cell.row.id}_${cell.column.id}`,
+ }), [cell, isEditable, handleChange, tableKey, table]);
+
+ // Мемоизация портала
+ const portalContent = useMemo(() => {
+ if (!refTbody || isSaving) return null;
+ return createPortal(
+ ,
+ refTbody
+ );
+ }, [refTbody, isSaving, editCellProps, EditCell]);
return (
<>
-
- {refTbody && !isSaving &&
- createPortal(
- ,
- refTbody,
- )}
- {isSaving && (
- createPortal(
-
- Saving...
-
,
- refTbody
- )
- )}
+
+ {portalContent}
>
);
-}
+});
+EditCellPortal.displayName = 'EditCellPortal';
+
+// Основная функция getTableColumns
export const getTableColumns = ({
Cell,
EditCell,
@@ -104,54 +93,127 @@ export const getTableColumns = ({
onCellNumberClick,
}) => {
const columnColors = { ...columnsConfig.colors };
- const columns = [...columnsConfig.columns];
+ const columns = structuredClone(columnsConfig.columns);
- const processColumns = (columns, EditCell, Cell) => {
+ // Кэши для вычисленных значений
+ const cellPropsCache = new WeakMap();
+ const editPropsCache = new WeakMap();
+
+ // Функция для получения кешированных пропсов Cell
+ const getCachedCellProps = (row, column, table) => {
+ const key = `${row.id}_${column.id}`;
+
+ if (!cellPropsCache.has(row)) {
+ cellPropsCache.set(row, {});
+ }
+
+ const rowCache = cellPropsCache.get(row);
+ if (rowCache[key]) {
+ // Проверяем, не изменились ли фильтры
+ const cached = rowCache[key];
+ const currentGlobalFilter = table.getState().globalFilter;
+ const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
+
+ if (cached.globalFilter === currentGlobalFilter &&
+ cached.columnFilter === currentColumnFilter) {
+ return cached;
+ }
+ }
+
+ const globalFilter = table.getState().globalFilter;
+ const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
+
+ const props = {
+ globalFilter,
+ columnFilter,
+ isEditable: row.original?.row_type === 'INPUT' || false,
+ backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
+ isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
+ _hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}`,
+ };
+
+ rowCache[key] = props;
+ return props;
+ };
+
+ // Функция для получения кешированных пропсов Edit
+ const getCachedEditProps = (row, column, table) => {
+ const key = `${row.id}_${column.id}`;
+
+ if (!editPropsCache.has(row)) {
+ editPropsCache.set(row, {});
+ }
+
+ const rowCache = editPropsCache.get(row);
+ if (rowCache[key]) {
+ return rowCache[key];
+ }
+
+ const props = {
+ isEditable: row.original?.row_type === 'INPUT' || false,
+ };
+
+ rowCache[key] = props;
+ return props;
+ };
+
+ // Оптимизированная функция processColumns
+ const processColumns = (columns) => {
columns.forEach((col) => {
- col.Cell = ({ cell, table, column, row }) => (
- f.id === column.id)?.value}
- isEditable={row.original?.row_type === 'INPUT' || false}
- backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
- color={columnColors[column.id]?.color_type?.['COLOR']}
- isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]}
- />
- );
+ // Оптимизированный Cell
+ col.Cell = ({ cell, table, column, row }) => {
+ const props = getCachedCellProps(row, column, table);
- col.Edit = ({ cell, table, row, column }) => (
-
- );
+ // Используем useMemo для мемоизации пропсов
+ const cellProps = useMemo(() => ({
+ row,
+ column,
+ cell,
+ ...props,
+ }), [row.id, column.id, cell.getValue(), props._hash]);
+
+ return | ;
+ };
+
+ // Оптимизированный Edit
+ col.Edit = ({ cell, table, row, column }) => {
+ const props = getCachedEditProps(row, column, table);
+
+ // Мемоизируем компонент EditCellPortal
+ const editComponent = useMemo(() => (
+
+ ), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
+
+ return editComponent;
+ };
// Рекурсивно обрабатываем вложенные колонки
if (col.columns?.length) {
- processColumns(col.columns, EditCell, Cell);
+ processColumns(col.columns);
}
});
};
- processColumns(columns, EditCell, Cell);
+ processColumns(columns);
+ // Оптимизированная колонка с номерами строк
const rowNumber = {
accessorKey: 'sort_order',
header: '#',
size: 50,
enableEditing: false,
Cell: ({ cell, row }) => {
- return
+ const handleClick = useCallback(() => {
+ onCellNumberClick(row.id);
+ }, [row.id, onCellNumberClick]);
+
+ return ;
},
};
|