Report test; Map printing test

This commit is contained in:
cracklesparkle
2025-01-31 15:53:58 +09:00
parent 0788a401ca
commit c08f839b70
12 changed files with 323 additions and 57 deletions

BIN
client/public/template.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
client/public/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -14,13 +14,13 @@ 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 } from '@mantine/core'
import { ActionIcon, Autocomplete, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay, Stack, Container, Modal, Transition } from '@mantine/core'
import { IconBoxMultiple, IconBoxPadding, IconChevronDown, IconPlus, IconSearch, IconUpload } from '@tabler/icons-react'
import { ICitySettings, IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel } from '../../store/map'
import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel, clearPrintArea } from '../../store/map'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
@ -73,6 +73,7 @@ const MapComponent = ({
nodeLayerSource, drawingLayerSource,
satLayer, staticMapLayer, figuresLayer, linesLayer,
regionsLayer, districtBoundLayer, baseLayer,
printArea, printSource, printAreaDraw
} = useMapStore().id[id]
// Tab settings
@ -383,6 +384,17 @@ const MapComponent = ({
// }
// }, [searchParams, figuresData, linesData])
useEffect(() => {
if (selectedDistrict === null) {
setSelectedYear(id, null)
}
if (selectedRegion === null) {
setSelectedYear(id, null)
setSelectedDistrict(id, null)
}
}, [selectedDistrict, selectedRegion, id])
useEffect(() => {
const districtBoundSource = districtBoundLayer.getSource()
@ -505,8 +517,42 @@ const MapComponent = ({
}
}, [colorScheme])
useEffect(() => {
if (map) {
if (mode === 'print') {
map.addInteraction(printAreaDraw)
} else {
map.removeInteraction(printAreaDraw)
}
}
}, [mode, map, printAreaDraw])
useEffect(() => {
if (printArea) {
map?.setTarget('print-portal')
// map?.setView(new View({
// extent: printArea
// }))
printSource.clear()
map?.getView().setCenter(getCenter(printArea))
map?.getView().fit(printArea, {
size: [640, 320]
})
}
}, [printArea, map])
return (
<>
<Modal keepMounted size='auto' opened={!!printArea} onClose={() => {
clearPrintArea(id)
map?.setTarget(mapElement.current as HTMLDivElement)
}} title="Предпросмотр области">
<div id='print-portal' style={{ width: '640px', height: '320px' }}>
</div>
</Modal>
{active &&
<Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}>
@ -636,22 +682,40 @@ const MapComponent = ({
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>
}
<Transition
mounted={!!selectedRegion && !!selectedDistrict && !!selectedYear}
transition="slide-right"
duration={200}
timingFunction="ease"
>
{(styles) => <Flex direction='column' h={'100%'} w={'100%'} style={{ ...mapControlsStyle, ...styles }}>
<TabsPane defaultTab='objects' tabs={objectsPane} />
<Divider />
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>}
</Transition>
</Stack>
<Stack w='100%' align='center'>
<Flex style={mapControlsStyle} w='fit-content'>
<Stack style={mapControlsStyle} w='fit-content'>
<MapMode map_id={id} />
</Flex>
</Stack>
</Stack>
<Stack w='100%' maw='340px' align='flex-end' justify='space-between'>
{selectedRegion && selectedDistrict && selectedYear && mode === 'edit' &&
<MapToolbar map_id={id} />
}
{selectedRegion && selectedDistrict && selectedYear &&
<MapLegend selectedDistrict={selectedDistrict} selectedYear={selectedYear} />
}
<Transition
mounted={!!selectedRegion && !!selectedDistrict && !!selectedYear}
transition="slide-left"
duration={200}
timingFunction="ease"
>
{(styles) => <MapLegend style={styles} selectedDistrict={selectedDistrict} selectedYear={selectedYear} />}
</Transition>
</Stack>
</Flex>

View File

@ -1,14 +1,16 @@
import { Accordion, ColorSwatch, Flex, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core'
import { Accordion, ColorSwatch, Flex, MantineStyleProp, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core'
import useSWR from 'swr';
import { fetcher } from '../../../http/axiosInstance';
import { BASE_URL } from '../../../constants';
const MapLegend = ({
selectedDistrict,
selectedYear
selectedYear,
style
}: {
selectedDistrict: number | null,
selectedYear: number | null
selectedYear: number | null,
style: MantineStyleProp
}) => {
const { colorScheme } = useMantineColorScheme();
@ -29,7 +31,7 @@ const MapLegend = ({
)
return (
<ScrollAreaAutosize offsetScrollbars maw='300px' w='100%' fz='xs' mt='auto' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ScrollAreaAutosize offsetScrollbars 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'>
<Text fz='xs'>
Легенда

View File

@ -1,6 +1,6 @@
import { Center, SegmentedControl } from '@mantine/core'
import { getMode, Mode, setMode } from '../../store/map'
import { IconEdit, IconEye } from '@tabler/icons-react'
import { IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
const MapMode = ({
map_id
@ -25,6 +25,15 @@ const MapMode = ({
</Center>
),
},
{
value: 'print',
label: (
<Center style={{ gap: 10 }}>
<IconPrinter size={16} />
<span>Печать</span>
</Center>
),
},
]} />
)
}

View File

@ -17,6 +17,7 @@ import { fromCircle, fromExtent } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map";
import Collection from "ol/Collection";
import { GeometryFunction, SketchCoordType } from "ol/interaction/Draw";
const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords;
@ -341,9 +342,47 @@ export const updateImageSource = (
}
};
export const fixedAspectRatioBox: GeometryFunction = (
coordinates: SketchCoordType,
geometry: SimpleGeometry | undefined,
): SimpleGeometry => {
// Ensure coordinates is an array of at least two points
if (!Array.isArray(coordinates) || coordinates.length < 2) {
return geometry ?? new Polygon([]);
}
const [start, end] = coordinates as Coordinate[]; // Ensure it's a Coordinate array
const minX = start[0];
const minY = start[1];
const maxX = end[0];
let maxY = end[1];
const width = maxX - minX;
const height = width / 2; // Enforce 2:1 aspect ratio
maxY = minY + height;
// Define the rectangle's coordinates
const boxCoords: Coordinate[][] = [[
[minX, minY],
[maxX, minY],
[maxX, maxY],
[minX, maxY],
[minX, minY], // Close the polygon
]];
if (geometry) {
geometry.setCoordinates(boxCoords);
return geometry;
}
return new Polygon(boxCoords);
};
export const addInteractions = (
map_id: string
) => {
console.log("Adding interactions")
const currentTool = getCurrentTool(map_id)
const clearPrevious = getMeasureClearPrevious(map_id)
const measureType = getMeasureType(map_id)

View File

@ -152,10 +152,10 @@ const pages = [
component: <PrintReport />,
drawer: true,
dashboard: true,
enabled: true,
enabled: false,
},
{
label: "DB Manager",
label: "Тест БД",
path: "/db-manager",
icon: <IconComponents />,
component: <DBManager />,

View File

@ -9,7 +9,7 @@ import { pages } from '../constants/app';
function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(false)
const navigate = useNavigate()
const getPageTitle = () => {

View File

@ -17,12 +17,12 @@ function MapTest() {
region: 11,
district: 145,
},
{
id: uuidv4(),
year: 2023,
region: 11,
district: 146,
},
// {
// id: uuidv4(),
// year: 2023,
// region: 11,
// district: 146,
// },
]
useEffect(() => {

View File

@ -1,4 +1,6 @@
import { Button, Flex } from "@mantine/core";
import { useState } from "react";
import createReport from 'docx-templates'
const xslTemplate = `<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
@ -904,9 +906,109 @@ const xslTemplate = `<?xml version="1.0" encoding="utf-8"?>
</xsl:stylesheet>`
const PrintReport = () => {
const handleGenerateExcel = () => {
// Define the example XML data
const xmlData = `
const [loading, setLoading] = useState(false);
const generateDocx = async () => {
setLoading(true);
try {
// Fetch the DOCX template from the public folder
const response = await fetch("/template.docx");
const response_table = await fetch("/template_table.docx");
const templateArrayBuffer = await response.arrayBuffer();
const templateArrayBuffer_table = await response_table.arrayBuffer();
// Convert ArrayBuffer to Uint8Array (Fix TypeScript error)
const templateUint8Array = new Uint8Array(templateArrayBuffer);
const templateUint8Array_table = new Uint8Array(templateArrayBuffer_table);
// Fetch the image (Example: Load from public folder)
const imageResponse = await fetch("/test.png"); // Change this to your image path
const imageBlob = await imageResponse.blob();
const imageArrayBuffer = await imageBlob.arrayBuffer();
const imageUint8Array = new Uint8Array(imageArrayBuffer);
// Generate the DOCX file with the replacement
const report = await createReport({
template: templateUint8Array, // Ensure it's Uint8Array
data: {
test: "Hello World",
myImage: {
width: 6, // Width in cm
height: 6, // Height in cm
data: imageUint8Array, // Image binary data
extension: ".png", // Specify the image format
},
},
});
const report_table = await createReport({
template: templateUint8Array_table, // Ensure it's Uint8Array
data: {
test: "Hello World",
rows: [
{
first: 'A',
second: 'B',
third: 'C',
fourth: 'D',
},
{
first: 'E',
second: 'F',
third: 'G',
fourth: 'H',
},
{
first: 'I',
second: 'J',
third: 'K',
fourth: 'L',
}
]
},
});
// Convert Uint8Array to a Blob
const blob = new Blob([report], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
const blob_table = new Blob([report_table], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
// Create a download link and trigger the download
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "report.docx";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Create a download link and trigger the download
const url_table = URL.createObjectURL(blob_table);
const a_table = document.createElement("a");
a_table.href = url_table;
a_table.download = "report_table.docx";
document.body.appendChild(a_table);
a_table.click();
document.body.removeChild(a_table);
// Revoke the object URL after download
URL.revokeObjectURL(url);
URL.revokeObjectURL(url_table);
} catch (error) {
console.error("Error generating DOCX:", error);
} finally {
setLoading(false);
}
}
const handleGenerateExcel = () => {
// Define the example XML data
const xmlData = `
<root>
<Kvp>
<style_id>1</style_id>
@ -928,40 +1030,41 @@ const PrintReport = () => {
</root>
`;
// Parse the XSL template and XML data
const parser = new DOMParser();
const xslDoc = parser.parseFromString(xslTemplate, "application/xml");
const xmlDoc = parser.parseFromString(xmlData, "application/xml");
// Parse the XSL template and XML data
const parser = new DOMParser();
const xslDoc = parser.parseFromString(xslTemplate, "application/xml");
const xmlDoc = parser.parseFromString(xmlData, "application/xml");
// Apply the transformation
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xslDoc);
const resultDocument = xsltProcessor.transformToDocument(xmlDoc);
// Apply the transformation
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xslDoc);
const resultDocument = xsltProcessor.transformToDocument(xmlDoc);
// Serialize the result to a string
const serializer = new XMLSerializer();
const resultXml = serializer.serializeToString(resultDocument);
// Serialize the result to a string
const serializer = new XMLSerializer();
const resultXml = serializer.serializeToString(resultDocument);
// Add missing Excel-specific headers if needed
const correctedXml = `<?xml version="1.0" encoding="utf-8"?>\n` + resultXml
// Add missing Excel-specific headers if needed
const correctedXml = `<?xml version="1.0" encoding="utf-8"?>\n` + resultXml
// Convert to Blob and trigger download
const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "template.xls";
link.click();
// Convert to Blob and trigger download
const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "template.xls";
link.click();
// Clean up
URL.revokeObjectURL(url);
}
// Clean up
URL.revokeObjectURL(url);
}
return (
<Flex p='sm'>
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
</Flex>
)
return (
<Flex p='sm' gap='sm'>
<Button onClick={generateDocx} disabled={loading}>{loading ? "Генерация отчета..." : "Сохранить в docx"}</Button>
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
</Flex>
)
}
export default PrintReport

View File

@ -22,11 +22,11 @@ import { click, pointerMove } from 'ol/events/condition';
import { measureStyleFunction, modifyStyle } from '../components/map/Measure/MeasureStyles';
import MapBrowserEvent from 'ol/MapBrowserEvent';
import { transform } from 'ol/proj';
import { applyTransformations, calculateTransformations, zoomToFeature } from '../components/map/mapUtils';
import { applyTransformations, calculateTransformations, fixedAspectRatioBox, zoomToFeature } from '../components/map/mapUtils';
import { setCurrentObjectId, setSelectedRegion } from './objects';
import View from 'ol/View';
export type Mode = 'edit' | 'view'
export type Mode = 'edit' | 'view' | 'print'
interface MapState {
id: Record<string, {
@ -77,6 +77,11 @@ interface MapState {
overlayLayer: VectorLayer;
regionSelect: Select;
lineSelect: Select;
printArea: Extent | null;
printLayer: VectorLayer;
printSource: VectorSource;
printAreaDraw: Draw;
printPreviewSize: number[];
}>;
}
@ -206,6 +211,24 @@ export const initializeMapState = (
const alignModeLayer = new VectorLayer({ source: new VectorSource(), properties: { id: uuidv4(), type: 'align', name: 'Подгонка' } })
const printSource = new VectorSource()
const printLayer = new VectorLayer({
source: printSource
})
const printAreaDraw = new Draw({
source: printSource,
type: 'Circle',
geometryFunction: fixedAspectRatioBox
})
printAreaDraw.on('drawend', (e) => {
const extent = e.feature.getGeometry()?.getExtent()
if (extent) {
setPrintArea(id, extent)
}
})
const map = new Map({
controls: [],
layers: [
@ -222,7 +245,8 @@ export const initializeMapState = (
overlayLayer,
nodeLayer,
measureLayer,
alignModeLayer
alignModeLayer,
printLayer
]
})
@ -364,13 +388,38 @@ export const initializeMapState = (
nodeLayer: nodeLayer,
overlayLayer: overlayLayer,
regionSelect: regionSelect,
lineSelect: lineSelect
lineSelect: lineSelect,
printArea: null,
printLayer: printLayer,
printSource: printSource,
printAreaDraw: printAreaDraw,
printPreviewSize: [640, 320]
}
}
}
})
}
export const clearPrintArea = (id: string) => useMapStore.setState((state) => {
state.id[id].printSource.clear()
return {
id: {
...state.id,
[id]: { ...state.id[id], printArea: null }
}
}
})
export const setPrintArea = (id: string, extent: Extent | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printArea: extent }
}
}
})
export const getFiguresLayer = (id: string) => useMapStore.getState().id[id].figuresLayer
export const getLinesLayer = (id: string) => useMapStore.getState().id[id].linesLayer
export const getMeasureModify = (id: string) => useMapStore.getState().id[id].measureModify