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} />
</>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 >
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
);
};

View File

@ -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>
)
}

View File

@ -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>
)
}
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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;

View File

@ -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>
)
}

View File

@ -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>
)
}