NestJS backend rewrite; migrate client to FluentUI V9

This commit is contained in:
2025-09-18 15:48:08 +09:00
parent 32ff36a12c
commit 34529cea68
62 changed files with 5642 additions and 3679 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { CSSProperties, useEffect, useRef, useState } from 'react'
import 'ol/ol.css'
import { Modify } from 'ol/interaction'
import { ImageStatic, Vector as VectorSource } from 'ol/source'
@ -14,8 +14,8 @@ import { addInteractions, handleImageDrop, loadFeatures, processFigure, processL
import useSWR, { SWRConfiguration } from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { ActionIcon, Autocomplete, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay, Stack, Container, Transition, } from '@mantine/core'
import { IconBoxMultiple, IconBoxPadding, IconChevronDown, IconChevronLeft, IconPlus, IconSearch, IconUpload, } from '@tabler/icons-react'
import { useMantineColorScheme } from '@mantine/core'
import { IconBoxMultiple, IconBoxPadding, IconChevronLeft, IconPlus, IconUpload, } from '@tabler/icons-react'
import { ICitySettings, IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar'
@ -34,6 +34,8 @@ import GisService from '../../services/GisService'
import MapMode from './MapMode'
import { satMapsProviders, schemas } from '../../constants/map'
import MapPrint from './MapPrint/MapPrint'
import { Field, Menu, MenuButton, MenuList, MenuPopover, MenuTrigger, Combobox, Option, Button, Divider, Spinner, Portal } from '@fluentui/react-components'
import { IRegion } from '../../interfaces/fuel'
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
@ -177,7 +179,7 @@ const MapComponent = ({
})
}
const mapControlsStyle: MantineStyleProp = {
const mapControlsStyle: CSSProperties = {
borderRadius: '4px',
zIndex: '1',
backgroundColor: colorScheme === 'light' ? '#F0F0F0CC' : '#000000CC',
@ -381,7 +383,7 @@ const MapComponent = ({
useEffect(() => {
if (!selectedRegion) {
setSelectedRegion(id, null)
setSelectedRegion(id, undefined)
setSelectedYear(id, null)
}
}, [selectedRegion, selectedDistrict, id])
@ -468,73 +470,154 @@ const MapComponent = ({
<MapPrint id={id} mapElement={mapElement} />
{active &&
<Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}>
<Autocomplete
form='search_object'
<Portal mountNode={document.querySelector('#header-portal')}>
<div style={{ display: 'flex', gap: '1rem' }}>
<Combobox
placeholder="Поиск"
flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchObject(value)}
onOptionSubmit={(value) => setCurrentObjectId(id, value)}
rightSection={
searchObject !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchObject('')
}}
aria-label="Clear value"
/>
)
}
leftSection={<IconSearch size={16} />}
value={searchObject}
/>
<MantineSelect
placeholder="Регион"
flex={'1'}
data={regionsData ? regionsData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
onChange={(value) => setSelectedRegion(id, Number(value))}
clearable
onClear={() => setSelectedRegion(id, null)}
searchable
value={selectedRegion ? selectedRegion.toString() : null}
/>
<MantineSelect
placeholder="Населённый пункт"
flex={'1'}
data={districtsData ? districtsData.map((item: { name: string, id: number, district_name: string }) => ({ label: [item.name, item.district_name].join(' - '), value: item.id.toString() })) : []}
onChange={(value) => setSelectedDistrict(id, Number(value))}
clearable
onClear={() => { setSelectedDistrict(id, null) }}
searchable
value={selectedDistrict ? selectedDistrict.toString() : null}
/>
<MantineSelect placeholder='Схема' w='92px'
data={schemas.map(el => ({ label: el, value: el }))}
onChange={(e) => {
if (e) {
setSelectedYear(id, Number(e))
} else {
setSelectedYear(id, null)
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setCurrentObjectId(id, data.optionValue);
setSearchObject(
searchData?.find((item: any) => item.id_object.toString() === data.optionValue)?.value ?? ""
);
}
}}
onClear={() => setSelectedYear(id, null)}
value={selectedYear ? selectedYear?.toString() : null}
onChange={(e) => {
setSearchObject(e.currentTarget.value); // free typing like Mantine's onChange
}}
clearable
/>
style={{ minWidth: 'auto' }}
>
{searchData
? searchData.map((item: { value: string; id_object: string }) => (
<Option key={item.id_object} value={item.id_object.toString()}>
{item.value}
</Option>
))
: null}
</Combobox>
<Button variant={alignMode ? 'filled' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)}>
<IconBoxPadding style={{ width: rem(20), height: rem(20) }} />
</Button>
<Combobox
placeholder="Регион"
clearable
// 👇 show label instead of id
value={
selectedRegion
? regionsData?.find((item: IRegion) => item.id === selectedRegion)?.name ?? ""
: ""
}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedRegion(id, Number(data.optionValue));
} else {
setSelectedRegion(id, undefined);
}
}}
style={{ minWidth: 'auto' }}
>
{regionsData
? regionsData.map((item: { name: string; id: number }) => (
<Option key={item.id} value={item.id.toString()}>
{item.name}
</Option>
))
: null}
</Combobox>
<Menu position="bottom-end" transitionProps={{ transition: 'pop-top-right' }}>
<Combobox
placeholder="Населённый пункт"
clearable
value={
selectedDistrict
? districtsData?.find((item: { id: number }) => item.id === selectedDistrict)?.name +
" - " +
districtsData?.find((item: { id: number }) => item.id === selectedDistrict)?.district_name
: ""
}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedDistrict(id, Number(data.optionValue));
} else {
setSelectedDistrict(id, null);
}
}}
style={{ minWidth: 'auto' }}
>
{districtsData
? districtsData.map(
(item: { name: string; id: number; district_name: string }) => (
<Option text={`${item.name} - ${item.district_name}`} key={item.id} value={item.id.toString()}>
{item.name} - {item.district_name}
</Option>
)
)
: null}
</Combobox>
<Combobox
placeholder="Схема"
clearable
style={{ width: "92px", minWidth: 'auto' }}
value={selectedYear ? selectedYear.toString() : ""}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedYear(id, Number(data.optionValue));
} else {
setSelectedYear(id, null);
}
}}
>
{schemas.map((el) => (
<Option key={el} value={el}>
{el}
</Option>
))}
</Combobox>
<Button icon={<IconBoxPadding />} appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} />
<Menu persistOnItemClick positioning={{ autoSize: true }}>
<MenuTrigger disableButtonEnhancement>
<MenuButton appearance='subtle' icon={<IconBoxMultiple />}>Слои</MenuButton>
</MenuTrigger>
<MenuPopover>
<MenuList>
<Field>Настройка видимости слоёв</Field>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Combobox
defaultValue={satMapsProviders.find(provider => provider.value === satMapsProvider)?.label ?? ""}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSatMapsProvider(id, data.optionValue as SatelliteMapsProvider);
}
}}
>
{satMapsProviders.map((provider) => (
<Option text={provider.label} key={provider.value} value={provider.value}>
{provider.label}
</Option>
))}
</Combobox>
</div>
<div style={{
display: 'flex',
}}>
<Button icon={<IconUpload />} appearance='transparent' onClick={() => submitOverlay(file, polygonExtent, rectCoords)} />
<Button icon={<IconPlus />} appearance='transparent' title='Добавить подложку' />
</div>
<MapLayers map={map} />
</MenuList>
</MenuPopover>
</Menu>
{/* <Menu position="bottom-end" transitionProps={{ transition: 'pop-top-right' }}>
<Menu.Target>
<Button variant='transparent'>
<Group gap={7} wrap='nowrap' style={{ flexShrink: 0 }} title='Слои'>
@ -562,68 +645,99 @@ const MapComponent = ({
<MapLayers map={map} />
</Flex>
</Menu.Dropdown>
</Menu>
</Flex>
</Portal>
</Menu> */}
</div>
</Portal >
}
<Container pos='absolute' w='100%' h='100%' p='0' fluid>
<Flex direction='column' w='100%' h='100%'>
<Flex w='100%' h='94%' p='xs' style={{ flexGrow: 1 }}>
<Stack w='100%' maw='380px'>
<Flex w='100%' h='100%' gap='xs'>
<div style={{ position: 'absolute', width: '100%', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
<div style={{ display: 'flex', width: '100%', height: '94%', padding: '0.5rem', flexGrow: 1 }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '380px' }}>
<div style={{ display: 'flex', width: '100%', height: '100%', gap: '0.5rem' }}>
{selectedRegion && selectedDistrict && selectedYear &&
<Flex direction='column' h={'100%'} w={leftPaneHidden ? '0px' : '100%'} style={{ ...mapControlsStyle, transition: 'width .3s ease' }}>
<div
style={{
...mapControlsStyle,
transition: 'width .3s ease',
display: 'flex',
flexDirection: 'column',
height: '100%',
width: leftPaneHidden ? '0px' : '100%',
overflow: 'hidden'
}}
>
<TabsPane defaultTab='objects' tabs={objectsPane} />
<Divider />
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>
</div>
}
{!!selectedRegion && !!selectedDistrict && !!selectedYear &&
<Button p='0' variant='subtle' w='32' style={{ zIndex: '1' }} onClick={() => setLeftPaneHidden(!leftPaneHidden)}>
<IconChevronLeft size={16} style={{ transform: `${leftPaneHidden ? 'rotate(180deg)' : ''}` }} />
</Button>
<Button
icon={<IconChevronLeft size={16}
style={{
transform: `${leftPaneHidden ? 'rotate(180deg)' : ''}`,
}} />}
style={{
zIndex: '1',
display: 'flex',
height: 'min-content'
}}
appearance='subtle'
onClick={() => setLeftPaneHidden(!leftPaneHidden)}
/>
}
</Flex>
</Stack>
</div>
</div>
<Stack w='100%' align='center'>
<Stack style={mapControlsStyle} w='fit-content'>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }} >
<div style={{ ...mapControlsStyle, display: 'flex', flexDirection: 'column', width: 'fit-content' }}>
<MapMode map_id={id} />
</Stack>
</Stack>
</div>
</div>
<Stack w='100%' maw='340px' align='flex-end' justify='space-between'>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '340px', alignItems: 'flex-end', justifyContent: 'space-between' }}>
{selectedRegion && selectedDistrict && selectedYear && mode === 'edit' &&
<MapToolbar map_id={id} />
}
<Transition
mounted={!!selectedRegion && !!selectedDistrict && !!selectedYear}
transition="slide-left"
duration={200}
timingFunction="ease"
>
{(styles) => <MapLegend style={styles} selectedDistrict={selectedDistrict} selectedYear={selectedYear} />}
</Transition>
</Stack>
</Flex>
{!!selectedRegion && !!selectedDistrict && !!selectedYear &&
<MapLegend selectedDistrict={selectedDistrict} selectedYear={selectedYear} />
}
</div>
</div>
<Flex w='100%'>
<div style={{ display: 'flex', width: '100%' }}>
<MapStatusbar
map_id={id}
mapControlsStyle={mapControlsStyle}
/>
</Flex>
</Flex>
</Container>
</div>
</div>
</div>
<Container pos='absolute' fluid p={0} w='100%' h='100%' mah='100%' ref={mapElement} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}>
<div style={{ position: 'absolute', width: '100%', height: '100%', maxHeight: '100%' }} ref={mapElement} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}>
<div ref={tooltipRef}></div>
</Container>
</div>
{(linesValidating || figuresValidating) && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "rgba(255, 255, 255, 0.6)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 9999,
}}
>
<Spinner size="large" label="Загрузка..." />
</div>
)}
<LoadingOverlay visible={linesValidating || figuresValidating} />
</>
)
}