From b0de7632593c0797430a869679d54aa8c56bbd2d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 18:09:52 +0400 Subject: [PATCH] =?UTF-8?q?perf(dict-vsp):=20=D0=B2=D0=B8=D1=80=D1=82?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA,=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Модалки в DictVspModals (forwardRef), таблица в memo со стабильными колбэками, enableRowVirtualization, size/minSize/maxSize колонок выверены под grid-раскладку. Co-Authored-By: Claude Opus 5.5 --- web/src/pages/DictVspPage/DictVspPage.jsx | 30 ++-- .../DictVspPage/components/DictVspModals.jsx | 65 ++++++++ .../DictVspPage/components/TableDictVsp.jsx | 148 ++++++------------ 3 files changed, 137 insertions(+), 106 deletions(-) create mode 100644 web/src/pages/DictVspPage/components/DictVspModals.jsx diff --git a/web/src/pages/DictVspPage/DictVspPage.jsx b/web/src/pages/DictVspPage/DictVspPage.jsx index 50d07b8..d9b1992 100644 --- a/web/src/pages/DictVspPage/DictVspPage.jsx +++ b/web/src/pages/DictVspPage/DictVspPage.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { NameTask, TaskInfoContainer } from '../../components/common/SwitchFormTask/SwitchFormTask.style'; import { Box, CircularProgress, Stack } from '@mui/material'; @@ -11,6 +11,7 @@ import { HeaderPortal } from '../../components/common/HeaderPortal'; import { ROLES_NAME_ID } from '../../constants/constants'; import { PageContainer } from '../../components/common/PageContainer'; import DictVspFilters from './components/DictVspFilters'; +import DictVspModals from './components/DictVspModals'; import TableDictVsp from './components/TableDictVsp'; const DictVspInfo = () => { @@ -30,6 +31,7 @@ function DictVspPage() { const [ssps, setSsps] = useState([]); const [selectedSsp, setSelectedSsp] = useState(null); const { user } = useAuth(); + const modalsRef = useRef(null); const loadData = useCallback(async () => { if (!user) return; @@ -68,7 +70,7 @@ function DictVspPage() { loadData(); }, [loadData]); - const handleAdd = async (formData) => { + const handleAdd = useCallback(async (formData) => { try { const response = await DictVspApi.create(formData); if (response.success) { @@ -82,9 +84,9 @@ function DictVspPage() { toast.error(error.response?.data?.message || 'Ошибка создания ВСП'); throw error; } - }; + }, []); - const handleEdit = async (formData) => { + const handleEdit = useCallback(async (formData) => { try { const response = await DictVspApi.update(formData.id, formData); if (response.success) { @@ -98,9 +100,9 @@ function DictVspPage() { toast.error(error.response?.data?.message || 'Ошибка обновления ВСП'); throw error; } - }; + }, []); - const handleDelete = async (vspId) => { + const handleDelete = useCallback(async (vspId) => { try { const response = await DictVspApi.delete(vspId); if (response.success) { @@ -114,7 +116,11 @@ function DictVspPage() { toast.error(error.response?.data?.message || 'Ошибка удаления ВСП'); throw error; } - }; + }, []); + + const handleAddClick = useCallback(() => modalsRef.current?.openAdd(), []); + const handleEditClick = useCallback((item) => modalsRef.current?.openEdit(item), []); + const handleExportClick = useCallback(() => modalsRef.current?.openExport(), []); if (isLoading) { return ( @@ -139,12 +145,18 @@ function DictVspPage() { )} + diff --git a/web/src/pages/DictVspPage/components/DictVspModals.jsx b/web/src/pages/DictVspPage/components/DictVspModals.jsx new file mode 100644 index 0000000..22b7b01 --- /dev/null +++ b/web/src/pages/DictVspPage/components/DictVspModals.jsx @@ -0,0 +1,65 @@ +import { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; +import ModalEditAddVsp from './Modals/ModalEditAddVsp'; +import ModalExport from './Modals/ModalExport'; + +// Состояние модалок живёт здесь, а не в таблице: открытие/закрытие не перерисовывает MRT. +const DictVspModals = forwardRef(({ onAdd, onEdit, onDelete, ssps }, ref) => { + const [modalOpen, setModalOpen] = useState(false); + const [modalExportOpen, setModalExportOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [saveLoading, setSaveLoading] = useState(false); + + useImperativeHandle( + ref, + () => ({ + openAdd: () => { + setEditingItem(null); + setModalOpen(true); + }, + openEdit: (item) => { + setEditingItem(item); + setModalOpen(true); + }, + openExport: () => setModalExportOpen(true), + }), + [], + ); + + const handleSave = async (formData) => { + setSaveLoading(true); + try { + if (editingItem) { + await onEdit?.({ ...formData, id: editingItem.id }); + } else { + await onAdd?.(formData); + } + setModalOpen(false); + } catch (error) { + console.error('Error saving:', error); + } finally { + setSaveLoading(false); + } + }; + + const closeEdit = useCallback(() => setModalOpen(false), []); + const closeExport = useCallback(() => setModalExportOpen(false), []); + + return ( + <> + + + + ); +}); + +DictVspModals.displayName = 'DictVspModals'; + +export default DictVspModals; diff --git a/web/src/pages/DictVspPage/components/TableDictVsp.jsx b/web/src/pages/DictVspPage/components/TableDictVsp.jsx index 9796933..db92a98 100644 --- a/web/src/pages/DictVspPage/components/TableDictVsp.jsx +++ b/web/src/pages/DictVspPage/components/TableDictVsp.jsx @@ -1,55 +1,19 @@ import { Box, IconButton, Stack, Typography } from '@mui/material'; import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; -import { useMemo, useRef, useState } from 'react'; +import { memo, useMemo, useRef } from 'react'; import { useAuth } from '../../../app/context/AuthProvider'; import { PrimaryButton, PrimaryOutlinedButton } from '../../../components/common/Buttons/Buttons'; import { EditPenSvg } from '../../../components/common/icons/icons'; import { ROLES_NAME_ID } from '../../../constants/constants'; -import ModalEditAddVsp from './Modals/ModalEditAddVsp'; -import ModalExport from './Modals/ModalExport'; +import { filterVspBySsp } from '../utils/filterVspBySsp'; -const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, selectedSsp }) => { +const ROW_ESTIMATED_HEIGHT = 54; + +const TableDictVsp = ({ data, onEditClick, onAddClick, onExportClick, loading = false, selectedSsp }) => { const { user } = useAuth(); - const [modalOpen, setModalOpen] = useState(false); - const [modalExportOpen, setModalExportOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); - const [saveLoading, setSaveLoading] = useState(false); const tableContainerRef = useRef(null); - const filteredData = useMemo(() => { - if (!selectedSsp) { - return data; - } - return data.filter((item) => item.ssp_id === selectedSsp.id); - }, [data, selectedSsp]); - - const handleEdit = (item) => { - setEditingItem(item); - setModalOpen(true); - }; - - const handleAdd = () => { - setEditingItem(null); - setModalOpen(true); - }; - - const handleExport = () => [setModalExportOpen(true)]; - - const handleSave = async (formData) => { - setSaveLoading(true); - try { - if (editingItem) { - await onEdit?.({ ...formData, id: editingItem.id }); - } else { - await onAdd?.(formData); - } - setModalOpen(false); - } catch (error) { - console.error('Error saving:', error); - } finally { - setSaveLoading(false); - } - }; + const filteredData = useMemo(() => filterVspBySsp(data, selectedSsp), [data, selectedSsp]); const columns = useMemo( () => [ @@ -70,9 +34,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'system_code', header: 'Код РФ', - size: 180, - minSize: 50, - maxSize: 100, + size: 110, + minSize: 90, + maxSize: 140, Cell: ({ cell }) => ( {cell.getValue() || '-'} @@ -84,9 +48,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'registration_number', header: 'Регистрационный номер ВСП', - size: 180, - minSize: 150, - maxSize: 200, + size: 250, + minSize: 200, + maxSize: 300, Cell: ({ cell }) => ( {cell.getValue() || '-'} @@ -98,9 +62,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'regional_branch', header: 'Региональный филиал/ССП', - size: 120, - minSize: 100, - maxSize: 150, + size: 230, + minSize: 180, + maxSize: 300, Cell: ({ cell }) => {cell.getValue() || '-'}, enableColumnActions: false, enableSorting: false, @@ -118,9 +82,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'address', header: 'Адрес объекта', - size: 250, + size: 300, minSize: 200, - maxSize: 350, + maxSize: 500, Cell: ({ cell }) => {cell.getValue() || '-'}, enableColumnActions: false, enableSorting: false, @@ -128,9 +92,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'approved_format', header: 'Утверждённый формат', - size: 150, - minSize: 120, - maxSize: 180, + size: 190, + minSize: 160, + maxSize: 240, Cell: ({ cell }) => {cell.getValue() || '-'}, enableColumnActions: false, enableSorting: false, @@ -138,9 +102,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'open_date', header: 'Дата открытия ВСП', - size: 130, - minSize: 110, - maxSize: 150, + size: 180, + minSize: 150, + maxSize: 220, Cell: ({ cell }) => { const date = cell.getValue(); return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'}; @@ -151,9 +115,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'close_date', header: 'Дата закрытия ВСП', - size: 130, - minSize: 110, - maxSize: 150, + size: 180, + minSize: 150, + maxSize: 220, Cell: ({ cell }) => { const date = cell.getValue(); return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'}; @@ -164,9 +128,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'placement_type', header: 'Форма расположения ВСП', - size: 170, - minSize: 140, - maxSize: 200, + size: 220, + minSize: 180, + maxSize: 260, Cell: ({ cell }) => {cell.getValue() || '-'}, enableColumnActions: false, enableSorting: false, @@ -174,9 +138,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'staff_count', header: 'Штатная численность ВСП', - size: 170, - minSize: 140, - maxSize: 200, + size: 230, + minSize: 190, + maxSize: 270, Cell: ({ cell }) => { const value = cell.getValue(); return {value !== undefined && value !== null ? value : '-'}; @@ -197,9 +161,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'rent_contract_num', header: 'Номер договора', - size: 130, - minSize: 110, - maxSize: 160, + size: 150, + minSize: 130, + maxSize: 200, Cell: ({ cell }) => {cell.getValue() || '-'}, enableColumnActions: false, enableSorting: false, @@ -207,9 +171,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'rent_end_date', header: 'Дата окончания договора', - size: 150, - minSize: 130, - maxSize: 170, + size: 230, + minSize: 190, + maxSize: 270, Cell: ({ cell }) => { const date = cell.getValue(); return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'}; @@ -220,9 +184,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se { accessorKey: 'notes', header: 'Примечания', - size: 200, - minSize: 150, - maxSize: 300, + size: 300, + minSize: 200, + maxSize: 500, Cell: ({ cell }) => ( {cell.getValue() || '-'} @@ -245,6 +209,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se enableGlobalFilter: false, enableSorting: false, enablePagination: false, + enableRowVirtualization: true, + // Высоту строк MRT измеряет сам (по умолчанию): «Примечания» многострочные (pre-wrap). + rowVirtualizerOptions: { overscan: 5, estimateSize: () => ROW_ESTIMATED_HEIGHT }, positionActionsColumn: 'last', // Включаем изменение размера колонок для лучшего UX при горизонтальном скролле @@ -279,8 +246,8 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se Справочник объектов - Экспортировать - {user.role_id == ROLES_NAME_ID.admin && + Добавить объект} + Экспортировать + {user.role_id == ROLES_NAME_ID.admin && + Добавить объект} ), @@ -325,7 +292,7 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se }}> handleEdit(row.original)} + onClick={() => onEditClick(row.original)} sx={{ padding: '4px', '&:hover': { @@ -479,23 +446,10 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se }); return ( - <> - - - - - setModalOpen(false)} - onSave={handleSave} - onDelete={onDelete} - initialData={editingItem} - loading={saveLoading} - ssps={ssps} - /> - setModalExportOpen(false)} ssps={ssps} /> - + + + ); }; -export default TableDictVsp; +export default memo(TableDictVsp);