NestJS backend rewrite; migrate client to FluentUI V9
This commit is contained in:
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Checkbox, Flex, NavLink, Slider, Stack } from '@mantine/core'
|
||||
import { Checkbox, Slider, Text } from '@fluentui/react-components'
|
||||
import BaseLayer from 'ol/layer/Base'
|
||||
import Map from 'ol/Map'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -11,11 +11,11 @@ const MapLayers = ({
|
||||
map
|
||||
}: MapLayersProps) => {
|
||||
return (
|
||||
<Stack gap='0'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{map?.getLayers().getArray() && map?.getLayers().getArray().map((layer, index) => (
|
||||
<LayerSetting key={index} index={index} layer={layer} />
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,21 +40,23 @@ const LayerSetting = ({
|
||||
}, [opacity, layer])
|
||||
|
||||
return (
|
||||
<Flex key={`layer-${index}`} gap='xs' align='center'>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }} key={`layer-${index}`}>
|
||||
<Checkbox
|
||||
checked={visible}
|
||||
onChange={(e) => setVisible(e.currentTarget.checked)}
|
||||
/>
|
||||
<Slider
|
||||
w='100%'
|
||||
width='100%'
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.001}
|
||||
value={opacity}
|
||||
onChange={(value) => setOpacity(value)}
|
||||
onChange={(_, data) => setOpacity(data.value)}
|
||||
/>
|
||||
<NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
|
||||
</Flex>
|
||||
<Text truncate size={300} onClick={() => { console.log(layer.getLayerState()) }}>
|
||||
{layer.get('name')}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { Accordion, ActionIcon, Collapse, ColorSwatch, Flex, MantineStyleProp, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core'
|
||||
import { useMantineColorScheme } from '@mantine/core'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from '../../../http/axiosInstance'
|
||||
import { BASE_URL } from '../../../constants'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { IconChevronDown } from '@tabler/icons-react'
|
||||
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, ColorSwatch, Text } from '@fluentui/react-components'
|
||||
|
||||
const MapLegend = ({
|
||||
selectedDistrict,
|
||||
selectedYear,
|
||||
style
|
||||
}: {
|
||||
selectedDistrict: number | null,
|
||||
selectedYear: number | null,
|
||||
style: MantineStyleProp
|
||||
}) => {
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
|
||||
@ -32,40 +29,42 @@ const MapLegend = ({
|
||||
}
|
||||
)
|
||||
|
||||
const [opened, { toggle }] = useDisclosure(false)
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize maw='300px' w='100%' fz='xs' mt='auto' style={{ ...style, zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
|
||||
<Stack gap='sm' p='sm'>
|
||||
<Flex align='center'>
|
||||
<Text fz='xs'>
|
||||
<div
|
||||
style={{ overflow: 'auto', maxWidth: '300px', width: '100%', marginTop: 'auto', zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}
|
||||
>
|
||||
<Accordion collapsible>
|
||||
<AccordionItem value='existing'>
|
||||
<AccordionHeader>
|
||||
Легенда
|
||||
</Text>
|
||||
</AccordionHeader>
|
||||
|
||||
<ActionIcon ml='auto' variant='subtle' onClick={toggle} >
|
||||
<IconChevronDown style={{ transform: opened ? 'rotate(0deg)' : 'rotate(180deg)' }} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<AccordionPanel>
|
||||
<Accordion multiple collapsible>
|
||||
<AccordionItem value='existing'>
|
||||
<AccordionHeader>
|
||||
Существующие
|
||||
</AccordionHeader>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<Accordion defaultValue={['existing', 'planning']} multiple>
|
||||
<Accordion.Item value='existing' key='existing'>
|
||||
<Accordion.Control>Существующие</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{existingObjectsList && <LegendGroup objectsList={existingObjectsList} border='solid' />}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<AccordionPanel>
|
||||
{existingObjectsList && <LegendGroup objectsList={existingObjectsList} border='solid' />}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<Accordion.Item value='planning' key='planning'>
|
||||
<Accordion.Control>Планируемые</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{planningObjectsList && <LegendGroup objectsList={planningObjectsList} border='dotted' />}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</ScrollAreaAutosize>
|
||||
<AccordionItem value='planning'>
|
||||
<AccordionHeader>
|
||||
Планируемые
|
||||
</AccordionHeader>
|
||||
|
||||
<AccordionPanel>
|
||||
{planningObjectsList && <LegendGroup objectsList={planningObjectsList} border='dotted' />}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -89,15 +88,15 @@ const LegendGroup = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{objectsList.map(object => (
|
||||
<Flex gap='xs' align='center' key={object.id}>
|
||||
<ColorSwatch style={{ border: borderStyle() }} radius={0} size={16} color={`rgb(${object.r},${object.g},${object.b})`} />
|
||||
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }} key={object.id}>
|
||||
<ColorSwatch size='extra-small' style={{ border: borderStyle() }} color={`rgb(${object.r},${object.g},${object.b})`} value={`rgb(${object.r},${object.g},${object.b})`} />
|
||||
-
|
||||
<Text fz='xs'>{object.name}</Text>
|
||||
</Flex>
|
||||
<Text size={200}>{object.name}</Text>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import 'ol/ol.css'
|
||||
import { Container, Stack, Tabs } from '@mantine/core'
|
||||
import OlMap from 'ol/Map'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import TileLayer from 'ol/layer/Tile'
|
||||
@ -12,6 +11,7 @@ import VectorSource from 'ol/source/Vector'
|
||||
import Feature from 'ol/Feature'
|
||||
import { LineString } from 'ol/geom'
|
||||
import { Stroke, Style, Text } from 'ol/style'
|
||||
import { Tab, TabList } from '@fluentui/react-components'
|
||||
|
||||
const center = [14443331.466543002, 8878970.176309839]
|
||||
|
||||
@ -106,19 +106,21 @@ const MapLineTest = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<string | unknown>('map')
|
||||
|
||||
return (
|
||||
<Container fluid w='100%' pos='relative' p={0}>
|
||||
<Tabs h='100%' variant='default' value={'map'} keepMounted={true}>
|
||||
<Stack gap={0} h='100%'>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value={'map'}>Map</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value={'map'} h='100%' pos='relative'>
|
||||
<Container pos='absolute' fluid p={0} w='100%' h='100%' ref={mapElement}></Container>
|
||||
</Tabs.Panel>
|
||||
</Stack>
|
||||
</Tabs>
|
||||
</Container>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<TabList selectedValue='map' onTabSelect={(_, data) => setSelectedTab(data.value)}>
|
||||
<Tab value='map'>
|
||||
Map
|
||||
</Tab>
|
||||
</TabList>
|
||||
{selectedTab === 'map' && <div style={{ width: '100%', height: '100%' }} ref={mapElement}></div>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,106 +1,88 @@
|
||||
import { Button, Flex, FloatingIndicator, Popover, SegmentedControl } from '@mantine/core'
|
||||
import { Mode, setMode, useMapStore } from '../../store/map'
|
||||
import { IconChevronDown, IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
|
||||
import { IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PrintOrientation, setPrintOrientation, usePrintStore } from '../../store/print'
|
||||
import { Button, Menu, MenuItemRadio, MenuList, MenuPopover, MenuProps, MenuTrigger, SplitButton } from '@fluentui/react-components'
|
||||
|
||||
const MapMode = ({
|
||||
map_id
|
||||
}: { map_id: string }) => {
|
||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
|
||||
|
||||
const { mode } = useMapStore().id[map_id]
|
||||
|
||||
const setControlRef = (item: Mode) => (node: HTMLButtonElement) => {
|
||||
controlsRefs[item] = node;
|
||||
setControlsRefs(controlsRefs);
|
||||
}
|
||||
|
||||
const { printOrientation } = usePrintStore()
|
||||
|
||||
useEffect(() => {
|
||||
const [checkedValues, setCheckedValues] = useState<Record<string, string[]>>({ orientation: [printOrientation] })
|
||||
|
||||
const onChange: MenuProps["onCheckedValueChange"] = (
|
||||
_,
|
||||
{ name, checkedItems }
|
||||
) => {
|
||||
setCheckedValues((s) => ({ ...s, [name]: checkedItems }))
|
||||
setPrintOrientation(checkedItems[0] as PrintOrientation)
|
||||
setMode(map_id, 'print' as Mode)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (printOrientation) {
|
||||
setCheckedValues((s) => ({ ...s, ['orientation']: [printOrientation] }))
|
||||
}
|
||||
}, [printOrientation])
|
||||
|
||||
return (
|
||||
<Flex ref={setRootRef} p={4} gap={4}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
padding: '0.25rem'
|
||||
}}>
|
||||
<Button
|
||||
variant={mode === 'view' ? 'filled' : 'subtle'}
|
||||
appearance={mode === 'view' ? 'primary' : 'subtle'}
|
||||
key={'view'}
|
||||
ref={setControlRef('view' as Mode)}
|
||||
onClick={() => {
|
||||
setMode(map_id, 'view' as Mode)
|
||||
}}
|
||||
leftSection={<IconEye size={16} />}
|
||||
mod={{ active: mode === 'view' as Mode }}
|
||||
icon={<IconEye size={16} />}
|
||||
//mod={{ active: mode === 'view' as Mode }}
|
||||
>
|
||||
Просмотр
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={mode === 'edit' ? 'filled' : 'subtle'}
|
||||
appearance={mode === 'edit' ? 'primary' : 'subtle'}
|
||||
key={'edit'}
|
||||
ref={setControlRef('edit' as Mode)}
|
||||
onClick={() => {
|
||||
setMode(map_id, 'edit' as Mode)
|
||||
}}
|
||||
leftSection={<IconEdit size={16} />}
|
||||
mod={{ active: mode === 'edit' as Mode }}
|
||||
icon={<IconEdit size={16} />}
|
||||
//mod={{ active: mode === 'edit' as Mode }}
|
||||
>
|
||||
Редактирование
|
||||
</Button>
|
||||
|
||||
<Popover width='auto' position='bottom-end' >
|
||||
<Popover.Target>
|
||||
<Button.Group>
|
||||
<Button
|
||||
variant={mode === 'print' ? 'filled' : 'subtle'}
|
||||
key={'print'}
|
||||
ref={setControlRef('print' as Mode)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setMode(map_id, 'print' as Mode)
|
||||
}}
|
||||
leftSection={<IconPrinter size={16} />}
|
||||
mod={{ active: mode === 'print' as Mode }}
|
||||
>
|
||||
Печать
|
||||
</Button>
|
||||
<Button variant={mode === 'print' ? 'filled' : 'subtle'} w='auto' p={8} title='Ориентация'>
|
||||
<IconChevronDown size={16} />
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown p={0} style={{ display: 'flex' }}>
|
||||
<SegmentedControl
|
||||
color='blue'
|
||||
value={printOrientation}
|
||||
onChange={(value) => {
|
||||
setPrintOrientation(value as PrintOrientation)
|
||||
setMode(map_id, 'print' as Mode)
|
||||
<Menu checkedValues={checkedValues} onCheckedValueChange={onChange}>
|
||||
<MenuTrigger>
|
||||
<SplitButton
|
||||
appearance={mode === 'print' ? 'primary' : 'subtle'}
|
||||
primaryActionButton={{
|
||||
onClick: () => setMode(map_id, 'print' as Mode)
|
||||
}}
|
||||
data={[
|
||||
{
|
||||
value: 'horizontal',
|
||||
label: (
|
||||
<IconCropLandscape title='Горизонтальная' style={{ display: 'block' }} size={20} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'vertical',
|
||||
label: (
|
||||
<IconCropPortrait title='Вертикальная' style={{ display: 'block' }} size={20} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
icon={<IconPrinter size={16} />}
|
||||
>
|
||||
Печать
|
||||
</SplitButton>
|
||||
</MenuTrigger>
|
||||
|
||||
<FloatingIndicator target={controlsRefs[mode]} parent={rootRef} />
|
||||
</Flex >
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
<MenuItemRadio name='orientation' value='horizontal' icon={<IconCropLandscape style={{ display: 'block' }} />}>
|
||||
Горизонтальная
|
||||
</MenuItemRadio>
|
||||
<MenuItemRadio name='orientation' value='vertical' icon={<IconCropPortrait style={{ display: 'block' }} />}>
|
||||
Вертикальная
|
||||
</MenuItemRadio>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ActionIcon, Button, Checkbox, Flex, Modal, Radio, ScrollAreaAutosize, Select, Stack, Text } from '@mantine/core'
|
||||
import { IconHelp, IconWindowMaximize, IconWindowMinimize } from '@tabler/icons-react'
|
||||
import { IconHelp, IconWindowMaximize, IconWindowMinimize, IconX } from '@tabler/icons-react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { clearPrintArea, PrintScale, setPreviousView, setPrintScale, setPrintScaleLine, useMapStore } from '../../../store/map'
|
||||
import { PrintFormat, PrintOrientation, printResolutions, setPrintOrientation, setPrintResolution, usePrintStore } from '../../../store/print'
|
||||
@ -9,6 +8,7 @@ import { useObjectsStore } from '../../../store/objects'
|
||||
import jsPDF from 'jspdf'
|
||||
import { getCenter } from 'ol/extent'
|
||||
import ScaleLine from 'ol/control/ScaleLine'
|
||||
import { Button, Checkbox, Dropdown, Field, Option, Radio, RadioGroup, Text } from '@fluentui/react-components'
|
||||
|
||||
const MapPrint = ({
|
||||
id,
|
||||
@ -140,94 +140,128 @@ const MapPrint = ({
|
||||
}
|
||||
}, [printScaleLine, printArea])
|
||||
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!!printArea) {
|
||||
setOpened(true)
|
||||
}
|
||||
}, [printArea])
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
clearPrintArea(id)
|
||||
map?.setTarget(mapElement.current as HTMLDivElement)
|
||||
map?.addInteraction(printAreaDraw)
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
return (
|
||||
<Modal.Root
|
||||
scrollAreaComponent={ScrollAreaAutosize}
|
||||
keepMounted size='auto'
|
||||
opened={!!printArea}
|
||||
onClose={() => {
|
||||
clearPrintArea(id)
|
||||
map?.setTarget(mapElement.current as HTMLDivElement)
|
||||
map?.addInteraction(printAreaDraw)
|
||||
}} fullScreen={fullscreen}>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ transition: 'all .3s ease' }}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<div
|
||||
style={{
|
||||
display: opened ? 'flex' : 'none',
|
||||
position: fullscreen ? 'fixed' : 'fixed',
|
||||
zIndex: '9999',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
inset: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'all .3s ease',
|
||||
width: fullscreen ? '100%' : 'auto',
|
||||
height: fullscreen ? '100%' : 'fit-content',
|
||||
background: 'var(--colorNeutralBackground1)',
|
||||
border: '1px solid var(--colorNeutralShadowKey)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', padding: '1rem', alignItems: 'center' }}>
|
||||
<Text>
|
||||
Предпросмотр области печати
|
||||
</Modal.Title>
|
||||
</Text>
|
||||
|
||||
<Flex ml='auto' gap='md'>
|
||||
<ActionIcon title='Помощь' ml='auto' variant='transparent'>
|
||||
<IconHelp color='gray' />
|
||||
</ActionIcon>
|
||||
<ActionIcon title={fullscreen ? 'Свернуть' : 'Развернуть'} variant='transparent' onClick={() => setFullscreen(!fullscreen)}>
|
||||
{fullscreen ? <IconWindowMinimize color='gray' /> : <IconWindowMaximize color='gray' />}
|
||||
</ActionIcon>
|
||||
<Modal.CloseButton title='Закрыть' />
|
||||
</Flex>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Stack align='center'>
|
||||
<Text w='100%'>Область печати можно передвигать.</Text>
|
||||
<div style={{ display: 'flex', marginLeft: 'auto', gap: '1.5rem' }}>
|
||||
<Button appearance='subtle' title='Помощь' style={{ marginLeft: 'auto' }} icon={<IconHelp color='gray' />} />
|
||||
|
||||
<div id='print-portal' style={{
|
||||
width: printOrientation === 'horizontal' ? '594px' : '420px',
|
||||
height: printOrientation === 'horizontal' ? '420px' : '594px'
|
||||
}}>
|
||||
<Button appearance='subtle' title={fullscreen ? 'Свернуть' : 'Развернуть'} style={{ marginLeft: 'auto' }} icon={fullscreen ? <IconWindowMinimize color='gray' /> : <IconWindowMaximize color='gray' />} onClick={() => setFullscreen(!fullscreen)} />
|
||||
|
||||
</div>
|
||||
<Button appearance='subtle' title='Закрыть' icon={<IconX />} onClick={() => setOpened(false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Flex w='100%' wrap='wrap' gap='lg' justify='space-between'>
|
||||
<Radio.Group
|
||||
label='Ориентация'
|
||||
value={printOrientation}
|
||||
onChange={(value) => setPrintOrientation(value as PrintOrientation)}
|
||||
>
|
||||
<Stack>
|
||||
<Radio value='horizontal' label='Горизонтальная' />
|
||||
<Radio value='vertical' label='Вертикальная' />
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', height: 'fit-content', overflow: 'auto' }}>
|
||||
<Text>Область печати можно передвигать.</Text>
|
||||
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label="Разрешение"
|
||||
placeholder="Выберите разрешение"
|
||||
data={printResolutions}
|
||||
<div id='print-portal' style={{
|
||||
width: printOrientation === 'horizontal' ? '594px' : '420px',
|
||||
height: printOrientation === 'horizontal' ? '420px' : '594px',
|
||||
flexShrink: '0'
|
||||
}}>
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%', flexWrap: 'wrap', gap: '1rem', padding: '1rem', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<Field label={'Ориентация'}>
|
||||
<RadioGroup value={printOrientation} onChange={(_, data) => setPrintOrientation(data.value as PrintOrientation)}>
|
||||
<Radio value='horizontal' label='Горизонтальная' />
|
||||
<Radio value='vertical' label='Вертикальная' />
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
|
||||
<Field label="Разрешение">
|
||||
<Dropdown
|
||||
defaultValue={printResolution.toString()}
|
||||
value={printResolution.toString()}
|
||||
onChange={(value) => setPrintResolution(Number(value))}
|
||||
/>
|
||||
selectedOptions={[printResolution.toString()]}
|
||||
onOptionSelect={(_, data) => setPrintResolution(Number(data.optionValue))}
|
||||
>
|
||||
{printResolutions.map((res) => (
|
||||
<Option key={res} text={res} value={res}>
|
||||
{res}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
</Field>
|
||||
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label="Масштаб"
|
||||
placeholder="Выберите масштаб"
|
||||
data={scaleOptions}
|
||||
value={printScale}
|
||||
onChange={(value) => setPrintScale(id, value as PrintScale)}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={printScaleLine}
|
||||
label="Масштабная линия"
|
||||
onChange={(event) => setPrintScaleLine(id, event.currentTarget.checked)}
|
||||
/>
|
||||
</Flex>
|
||||
<Field label="Масштаб">
|
||||
<Dropdown
|
||||
defaultValue={printScale.toString()}
|
||||
value={printScale.toString()}
|
||||
defaultSelectedOptions={[printScale]}
|
||||
selectedOptions={[printScale]}
|
||||
onOptionSelect={(_, data) => setPrintScale(id, data.optionValue as PrintScale)}
|
||||
>
|
||||
{scaleOptions.map((opt) => (
|
||||
<Option key={opt.value} text={opt.label} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
</Field>
|
||||
|
||||
<Flex w='100%' gap='sm' align='center'>
|
||||
<Button ml='auto' onClick={() => {
|
||||
if (previousView) {
|
||||
exportToPDF(printFormat, printResolution, printOrientation)
|
||||
}
|
||||
}}>
|
||||
Печать
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
<Checkbox
|
||||
checked={printScaleLine}
|
||||
label="Масштабная линия"
|
||||
onChange={(event) => setPrintScaleLine(id, event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%', gap: '1rem', padding: '1rem', alignItems: 'center' }}>
|
||||
<Button style={{ marginLeft: 'auto' }} onClick={() => {
|
||||
if (previousView) {
|
||||
exportToPDF(printFormat, printResolution, printOrientation)
|
||||
}
|
||||
}}>
|
||||
Печать
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Divider, Flex, rem, Text } from '@mantine/core'
|
||||
import { CSSProperties } from 'react'
|
||||
import { useMapStore } from '../../../store/map';
|
||||
import { Divider, Text } from '@fluentui/react-components';
|
||||
|
||||
interface IMapStatusbarProps {
|
||||
mapControlsStyle: CSSProperties;
|
||||
@ -14,33 +14,33 @@ const MapStatusbar = ({
|
||||
const { currentCoordinate, currentX, currentY, currentZ, statusText } = useMapStore().id[map_id]
|
||||
|
||||
return (
|
||||
<Flex gap='sm' p={'4px'} w={'100%'} fz={'xs'} style={{ ...mapControlsStyle, borderRadius: 0 }}>
|
||||
<Text fz='xs' w={rem(130)}>
|
||||
<div style={{ ...mapControlsStyle, display: 'flex', gap: '1rem', padding: '0.25rem', width: '100%', borderRadius: 0 }}>
|
||||
<Text size={200}>
|
||||
x: {currentCoordinate?.[0]}
|
||||
</Text>
|
||||
|
||||
<Text fz='xs' w={rem(130)}>
|
||||
<Text size={200}>
|
||||
y: {currentCoordinate?.[1]}
|
||||
</Text>
|
||||
|
||||
<Divider orientation='vertical' />
|
||||
<Divider vertical />
|
||||
|
||||
<Text fz='xs'>
|
||||
<Text size={200}>
|
||||
Z={currentZ}
|
||||
</Text>
|
||||
|
||||
<Text fz='xs'>
|
||||
<Text size={200}>
|
||||
X={currentX}
|
||||
</Text>
|
||||
|
||||
<Text fz='xs'>
|
||||
<Text size={200}>
|
||||
Y={currentY}
|
||||
</Text>
|
||||
|
||||
<Text fz='xs' ml='auto'>
|
||||
<Text size={200} style={{marginLeft: 'auto'}}>
|
||||
{statusText}
|
||||
</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ActionIcon, Flex, useMantineColorScheme } from '@mantine/core'
|
||||
import { useMantineColorScheme } from '@mantine/core'
|
||||
import { IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler, IconTransformPoint } from '@tabler/icons-react'
|
||||
import { getDraw, setCurrentTool, useMapStore } from '../../../store/map';
|
||||
import { saveFeatures } from '../mapUtils';
|
||||
import { Button } from '@fluentui/react-components';
|
||||
|
||||
const MapToolbar = ({
|
||||
map_id
|
||||
@ -10,81 +11,41 @@ const MapToolbar = ({
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<ActionIcon.Group orientation='vertical' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
|
||||
<ActionIcon size='lg' variant='transparent' onClick={() => saveFeatures(map_id)}>
|
||||
<IconExclamationCircle />
|
||||
</ActionIcon>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
|
||||
<Button icon={<IconExclamationCircle />} appearance='transparent' onClick={() => saveFeatures(map_id)} />
|
||||
|
||||
<ActionIcon size='lg' variant='transparent' onClick={() => getDraw(map_id)?.removeLastPoint()}>
|
||||
<IconArrowBackUp />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconArrowBackUp />} appearance='transparent' onClick={() => getDraw(map_id)?.removeLastPoint()} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Edit' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Edit')
|
||||
}}>
|
||||
<IconTransformPoint />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconTransformPoint />} appearance={currentTool === 'Edit' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Edit')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Point')
|
||||
}}>
|
||||
<IconPoint />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconPoint />} appearance={currentTool === 'Point' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Point')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'LineString')
|
||||
}}>
|
||||
<IconLine />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconLine />} appearance={currentTool === 'LineString' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'LineString')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Polygon')
|
||||
}}>
|
||||
<IconPolygon />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconPolygon />} appearance={currentTool === 'Polygon' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Polygon')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Circle')
|
||||
}}>
|
||||
<IconCircle />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconCircle />} appearance={currentTool === 'Circle' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Circle')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Mover' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Mover')
|
||||
}}
|
||||
>
|
||||
<IconArrowsMove />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconArrowsMove />} appearance={currentTool === 'Mover' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Mover')
|
||||
}} />
|
||||
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
variant={currentTool === 'Measure' ? 'filled' : 'transparent'}
|
||||
onClick={() => {
|
||||
setCurrentTool(map_id, 'Measure')
|
||||
}}>
|
||||
<IconRuler />
|
||||
</ActionIcon>
|
||||
</ActionIcon.Group>
|
||||
</Flex>
|
||||
<Button icon={<IconRuler />} appearance={currentTool === 'Measure' ? 'primary' : 'transparent'} onClick={() => {
|
||||
setCurrentTool(map_id, 'Measure')
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { Checkbox, Group, RenderTreeNodePayload, Text } from "@mantine/core";
|
||||
import { IconChevronDown } from "@tabler/icons-react";
|
||||
|
||||
export const MapTreeCheckbox = ({
|
||||
node,
|
||||
expanded,
|
||||
hasChildren,
|
||||
elementProps,
|
||||
tree,
|
||||
}: RenderTreeNodePayload) => {
|
||||
const checked = tree.isNodeChecked(node.value);
|
||||
const indeterminate = tree.isNodeIndeterminate(node.value);
|
||||
|
||||
return (
|
||||
<Group gap="xs" {...elementProps}>
|
||||
<Checkbox.Indicator
|
||||
checked={checked}
|
||||
indeterminate={indeterminate}
|
||||
onClick={() => (!checked ? tree.checkNode(node.value) : tree.uncheckNode(node.value))}
|
||||
/>
|
||||
|
||||
<Group gap={5} onClick={() => tree.toggleExpanded(node.value)}>
|
||||
<Text size="xs">{node.label}</Text>
|
||||
|
||||
{hasChildren && (
|
||||
<IconChevronDown
|
||||
size={14}
|
||||
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import { Flex } from '@mantine/core'
|
||||
import { IObjectData, IObjectType } from '../../interfaces/objects'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from '../../http/axiosInstance'
|
||||
@ -14,11 +13,9 @@ const ObjectData = (object_data: IObjectData) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex gap='sm' direction='column'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{Array.isArray(typeData) && (typeData.find(type => Number(type.id) === Number(object_data.type)) as IObjectType).name}
|
||||
|
||||
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { BASE_URL } from '../../constants'
|
||||
import { IObjectParam, IParam } from '../../interfaces/objects'
|
||||
import TCBParameter from './TCBParameter'
|
||||
import TableValue from './TableValue'
|
||||
import { TableCell, TableCellLayout, TableRow } from '@fluentui/react-components'
|
||||
|
||||
interface ObjectParameterProps {
|
||||
showLabel?: boolean;
|
||||
@ -73,10 +74,19 @@ const ObjectParameter = ({
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
{name}
|
||||
{value as string}
|
||||
</div>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
{name}
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
{value as string}
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Flex, LoadingOverlay } from '@mantine/core';
|
||||
import { IObjectParam } from '../../../interfaces/objects';
|
||||
import ObjectParameter from '../ObjectParameter';
|
||||
import useSWR from 'swr';
|
||||
import { BASE_URL } from '../../../constants';
|
||||
import { fetcher } from '../../../http/axiosInstance';
|
||||
import { useObjectsStore } from '../../../store/objects';
|
||||
import { Spinner, Table, TableBody } from '@fluentui/react-components';
|
||||
|
||||
const ObjectParameters = ({
|
||||
map_id
|
||||
@ -24,40 +24,59 @@ const ObjectParameters = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex gap={'sm'} direction={'column'} pos='relative'>
|
||||
<LoadingOverlay visible={valuesValidating} />
|
||||
{Array.isArray(valuesData) &&
|
||||
Object.entries(
|
||||
valuesData.reduce((acc, param) => {
|
||||
if (!acc[param.id_param]) {
|
||||
acc[param.id_param] = [];
|
||||
}
|
||||
acc[param.id_param].push(param);
|
||||
return acc;
|
||||
}, {} as Record<string, IObjectParam[]>)
|
||||
).map(([id_param, params]) => {
|
||||
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
|
||||
const sortedParams = (params as IObjectParam[]).sort((b, a) => {
|
||||
const dateA = new Date(a.date_s || 0);
|
||||
const dateB = new Date(b.date_s || 0);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
<div style={{ display: 'flex', gap: '1rem', flexDirection: 'column', position: 'relative' }}>
|
||||
{(valuesValidating) && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
return sortedParams.length > 1 ? (
|
||||
sortedParams.map((param: IObjectParam) => {
|
||||
if (param.date_po == null) {
|
||||
return (
|
||||
<ObjectParameter map_id={map_id} key={id_param} param={param} showLabel={false} />
|
||||
<Table size='small'>
|
||||
<TableBody>
|
||||
{Array.isArray(valuesData) &&
|
||||
Object.entries(
|
||||
valuesData.reduce((acc, param) => {
|
||||
if (!acc[param.id_param]) {
|
||||
acc[param.id_param] = [];
|
||||
}
|
||||
acc[param.id_param].push(param);
|
||||
return acc;
|
||||
}, {} as Record<string, IObjectParam[]>)
|
||||
).map(([id_param, params]) => {
|
||||
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
|
||||
const sortedParams = (params as IObjectParam[]).sort((b, a) => {
|
||||
const dateA = new Date(a.date_s || 0);
|
||||
const dateB = new Date(b.date_s || 0);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
|
||||
return sortedParams.length > 1 ? (
|
||||
sortedParams.map((param: IObjectParam) => {
|
||||
if (param.date_po == null) {
|
||||
return (
|
||||
<ObjectParameter map_id={map_id} key={id_param} param={param} showLabel={false} />
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
) : (
|
||||
<ObjectParameter map_id={map_id} key={id_param} param={sortedParams[0]} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
) : (
|
||||
<ObjectParameter map_id={map_id} key={id_param} param={sortedParams[0]} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import useSWR from 'swr'
|
||||
import { BASE_URL } from '../../constants'
|
||||
import { fetcher } from '../../http/axiosInstance'
|
||||
import { Flex } from '@mantine/core'
|
||||
|
||||
const RegionSelect = () => {
|
||||
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.ems), {
|
||||
@ -10,7 +9,7 @@ const RegionSelect = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Flex align='center' justify='center'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{Array.isArray(data) &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width='100%' height='100vh' transform='scale(1, -1)'>
|
||||
{data.map((el, index) => (
|
||||
@ -18,7 +17,7 @@ const RegionSelect = () => {
|
||||
))}
|
||||
</svg>
|
||||
}
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from '../../http/axiosInstance'
|
||||
import { BASE_URL } from '../../constants'
|
||||
import { Text } from '@mantine/core'
|
||||
import TableValue from './TableValue'
|
||||
import { Text } from '@fluentui/react-components';
|
||||
|
||||
interface ITCBParameterProps {
|
||||
value: string;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Checkbox, ComboboxData, Grid, NumberInput, Select, Text, Textarea } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '../../http/axiosInstance';
|
||||
import { BASE_URL } from '../../constants';
|
||||
import { useObjectsStore } from '../../store/objects';
|
||||
import { Checkbox, Input, Select, TableCell, TableCellLayout, TableRow, Text } from '@fluentui/react-components';
|
||||
|
||||
interface TableValueProps {
|
||||
name: string;
|
||||
@ -30,7 +30,7 @@ const TableValue = ({
|
||||
return res.map((el) => ({
|
||||
label: el.name || "",
|
||||
value: JSON.stringify(el.id)
|
||||
})) as ComboboxData
|
||||
}))
|
||||
}
|
||||
}),
|
||||
{
|
||||
@ -40,32 +40,52 @@ const TableValue = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={4} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Text size='xs' style={{ textWrap: 'wrap' }}>{name as string}</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
{type === 'boolean' ?
|
||||
<Checkbox defaultChecked={value as boolean} />
|
||||
:
|
||||
type === 'number' ?
|
||||
<NumberInput
|
||||
size='xs'
|
||||
value={value as number}
|
||||
onChange={() => { }}
|
||||
suffix={unit ? ` ${unit}` : ''}
|
||||
/>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<TableCellLayout truncate>
|
||||
<Text size={200} style={{ textWrap: 'wrap' }}>{name as string}</Text>
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{type === 'boolean' ?
|
||||
// <Select style={{ display: 'flex', width: '100%' }} size='small' defaultChecked={value as boolean}>
|
||||
// {[true, false].map(tcb => (
|
||||
// <option key={JSON.stringify(tcb)} value={JSON.stringify(tcb)}>
|
||||
// {tcb === true ? 'Да' : 'Нет'}
|
||||
// </option>
|
||||
// ))}
|
||||
// </Select>
|
||||
<Checkbox defaultChecked={value as boolean} />
|
||||
:
|
||||
type === 'select' && !isValidating && tcbAll ?
|
||||
<Select size='xs' data={tcbAll} value={JSON.stringify(value)} />
|
||||
type === 'number' ?
|
||||
<Input
|
||||
size='small'
|
||||
style={{ display: 'flex', width: '100%' }}
|
||||
defaultValue={value as string}
|
||||
onChange={() => { }}
|
||||
contentAfter={unit ? ` ${unit}` : ''}
|
||||
//displayValue={unit ? ` ${unit}` : ''}
|
||||
/>
|
||||
:
|
||||
type === 'string' ?
|
||||
<Textarea size='xs' value={value as string} autosize minRows={1} />
|
||||
type === 'select' && !isValidating && tcbAll ?
|
||||
<Select style={{ display: 'flex', width: '100%' }} size='small' defaultValue={JSON.stringify(value)}>
|
||||
{tcbAll.map(tcb => (
|
||||
<option key={tcb.value} value={tcb.value}>
|
||||
{tcb.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
:
|
||||
<Text size='xs'>{value as string}</Text>
|
||||
}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
type === 'string' ?
|
||||
<Input style={{ display: 'flex', width: '100%' }} size='small' value={value as string} />
|
||||
:
|
||||
<Text size={200}>{value as string}</Text>
|
||||
}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ScrollAreaAutosize, Tabs } from '@mantine/core';
|
||||
import { Tab, TabList } from '@fluentui/react-components';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface ITabsPane {
|
||||
title: string;
|
||||
@ -15,30 +16,39 @@ const TabsPane = ({
|
||||
defaultTab,
|
||||
tabs
|
||||
}: TabsPaneProps) => {
|
||||
const [selectedTab, setSelectedTab] = useState<string | unknown>(defaultTab)
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={defaultTab} mah='50%' h={'100%'} style={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'min-content auto'
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: '50%',
|
||||
}}>
|
||||
<ScrollAreaAutosize>
|
||||
<Tabs.List>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxWidth: '100%',
|
||||
overflowX: 'auto',
|
||||
minHeight: 'min-content',
|
||||
borderBottom: '1px solid var(--colorNeutralShadowKey)'
|
||||
}}>
|
||||
<TabList selectedValue={selectedTab} onTabSelect={(_, data) => setSelectedTab(data.value)}>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Tab key={tab.value} value={tab.value}>
|
||||
{tab.title}
|
||||
</Tabs.Tab>
|
||||
<Tab value={tab.value}>{tab.title}</Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</ScrollAreaAutosize>
|
||||
</TabList>
|
||||
</div>
|
||||
|
||||
|
||||
<ScrollAreaAutosize h='100%' offsetScrollbars>
|
||||
{tabs.map(tab => (
|
||||
<Tabs.Panel p='xs' key={tab.value} value={tab.value}>
|
||||
{tab.view}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</ScrollAreaAutosize>
|
||||
</Tabs>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{tabs.find(tab => tab.value === selectedTab)?.view}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user