@l1yp/file-viewer 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/FileViewer.js +2 -0
- package/dist/FileViewer.js.map +1 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AppxViewer.js +2 -0
- package/dist/components/AppxViewer.js.map +1 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ArchiveBrowser.js +2 -0
- package/dist/components/ArchiveBrowser.js.map +1 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AudioViewer.js +2 -0
- package/dist/components/AudioViewer.js.map +1 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/BinObjBrowser.js +2 -0
- package/dist/components/BinObjBrowser.js.map +1 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CertViewer.js +2 -0
- package/dist/components/CertViewer.js.map +1 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ClassViewer.js +2 -0
- package/dist/components/ClassViewer.js.map +1 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeBlock.js +2 -0
- package/dist/components/CodeBlock.js.map +1 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeView.js +2 -0
- package/dist/components/CodeView.js.map +1 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CsvViewer.js +2 -0
- package/dist/components/CsvViewer.js.map +1 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DicomViewer.js +2 -0
- package/dist/components/DicomViewer.js.map +1 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxNavigationPane.js +2 -0
- package/dist/components/DocxNavigationPane.js.map +1 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxViewer.js +2 -0
- package/dist/components/DocxViewer.js.map +1 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DrawioViewer.js +2 -0
- package/dist/components/DrawioViewer.js.map +1 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DxfViewer.js +2 -0
- package/dist/components/DxfViewer.js.map +1 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EbookViewer.js +2 -0
- package/dist/components/EbookViewer.js.map +1 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EmlViewer.js +2 -0
- package/dist/components/EmlViewer.js.map +1 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EplViewer.js +2 -0
- package/dist/components/EplViewer.js.map +1 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerBody.js +2 -0
- package/dist/components/FileViewerBody.js.map +1 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerPanel.js +2 -0
- package/dist/components/FileViewerPanel.js.map +1 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FontViewer.js +2 -0
- package/dist/components/FontViewer.js.map +1 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FrontmatterCard.js +2 -0
- package/dist/components/FrontmatterCard.js.map +1 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/GeoViewer.js +2 -0
- package/dist/components/GeoViewer.js.map +1 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HarViewer.js +2 -0
- package/dist/components/HarViewer.js.map +1 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HexViewer.js +2 -0
- package/dist/components/HexViewer.js.map +1 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HprofViewer.js +2 -0
- package/dist/components/HprofViewer.js.map +1 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ImageLightbox.js +2 -0
- package/dist/components/ImageLightbox.js.map +1 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/InlineText.js +2 -0
- package/dist/components/InlineText.js.map +1 -0
- package/dist/components/JksViewer.js +2 -0
- package/dist/components/JksViewer.js.map +1 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/JsonlViewer.js +2 -0
- package/dist/components/JsonlViewer.js.map +1 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LogViewer.js +2 -0
- package/dist/components/LogViewer.js.map +1 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LrcViewer.js +2 -0
- package/dist/components/LrcViewer.js.map +1 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownTable.js +2 -0
- package/dist/components/MarkdownTable.js.map +1 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownText.js +2 -0
- package/dist/components/MarkdownText.js.map +1 -0
- package/dist/components/MhtViewer.js +2 -0
- package/dist/components/MhtViewer.js.map +1 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MindMapView.js +2 -0
- package/dist/components/MindMapView.js.map +1 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MissingDependency.js +2 -0
- package/dist/components/MissingDependency.js.map +1 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ModelViewer.js +2 -0
- package/dist/components/ModelViewer.js.map +1 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MsiViewer.js +2 -0
- package/dist/components/MsiViewer.js.map +1 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/NotebookViewer.js +2 -0
- package/dist/components/NotebookViewer.js.map +1 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfViewer.js +2 -0
- package/dist/components/PdfViewer.js.map +1 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxNavigationPane.js +2 -0
- package/dist/components/PptxNavigationPane.js.map +1 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxViewer.js +3 -1
- package/dist/components/PptxViewer.js.map +1 -0
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +55 -49
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PsdViewer.js +2 -0
- package/dist/components/PsdViewer.js.map +1 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/QmcAudioViewer.js +2 -0
- package/dist/components/QmcAudioViewer.js.map +1 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RasterViewer.js +2 -0
- package/dist/components/RasterViewer.js.map +1 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RtfViewer.js +2 -0
- package/dist/components/RtfViewer.js.map +1 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SevenZipBrowser.js +2 -0
- package/dist/components/SevenZipBrowser.js.map +1 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SqliteViewer.js +2 -0
- package/dist/components/SqliteViewer.js.map +1 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SubtitleViewer.js +2 -0
- package/dist/components/SubtitleViewer.js.map +1 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxChart.js +2 -0
- package/dist/components/XlsxChart.js.map +1 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxViewer.js +2 -0
- package/dist/components/XlsxViewer.js.map +1 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XmindViewer.js +2 -0
- package/dist/components/XmindViewer.js.map +1 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZipBrowser.js +2 -0
- package/dist/components/ZipBrowser.js.map +1 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZoomImage.js +2 -0
- package/dist/components/ZoomImage.js.map +1 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.js +2 -0
- package/dist/components/epl/EplCodeBlock.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplDoc.js +2 -0
- package/dist/components/epl/EplDoc.js.map +1 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplMethod.js +2 -0
- package/dist/components/epl/EplMethod.js.map +1 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplResource.js +2 -0
- package/dist/components/epl/EplResource.js.map +1 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplTree.js +2 -0
- package/dist/components/epl/EplTree.js.map +1 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplVarTable.js +2 -0
- package/dist/components/epl/EplVarTable.js.map +1 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/numbering.js +2 -0
- package/dist/components/epl/numbering.js.map +1 -0
- package/dist/components/icons/Icon.js +2 -0
- package/dist/components/icons/Icon.js.map +1 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/icons/iconPaths.js +2 -0
- package/dist/components/icons/iconPaths.js.map +1 -0
- package/dist/components/optionalViewer.js +2 -0
- package/dist/components/optionalViewer.js.map +1 -0
- package/dist/components/ui/ClampBlock.js +2 -0
- package/dist/components/ui/ClampBlock.js.map +1 -0
- package/dist/composables/useElementNavigation.js +2 -0
- package/dist/composables/useElementNavigation.js.map +1 -0
- package/dist/composables/useFileViewer.js +2 -0
- package/dist/composables/useFileViewer.js.map +1 -0
- package/dist/composables/useLightbox.js +2 -0
- package/dist/composables/useLightbox.js.map +1 -0
- package/dist/composables/useLogRules.js +2 -0
- package/dist/composables/useLogRules.js.map +1 -0
- package/dist/lib/aes.js +2 -0
- package/dist/lib/aes.js.map +1 -0
- package/dist/lib/appx.js +2 -0
- package/dist/lib/appx.js.map +1 -0
- package/dist/lib/archiveHtml.js +2 -0
- package/dist/lib/archiveHtml.js.map +1 -0
- package/dist/lib/asn1.js +2 -0
- package/dist/lib/asn1.js.map +1 -0
- package/dist/lib/binobj.js +2 -0
- package/dist/lib/binobj.js.map +1 -0
- package/dist/lib/brotli.js +2 -0
- package/dist/lib/brotli.js.map +1 -0
- package/dist/lib/bzip2.js +2 -0
- package/dist/lib/bzip2.js.map +1 -0
- package/dist/lib/classfile.js +2 -0
- package/dist/lib/classfile.js.map +1 -0
- package/dist/lib/codePreview.js +2 -0
- package/dist/lib/codePreview.js.map +1 -0
- package/dist/lib/codefold.js +2 -0
- package/dist/lib/codefold.js.map +1 -0
- package/dist/lib/codeformat.js +2 -0
- package/dist/lib/codeformat.js.map +1 -0
- package/dist/lib/csv.js +2 -0
- package/dist/lib/csv.js.map +1 -0
- package/dist/lib/des.js +2 -0
- package/dist/lib/des.js.map +1 -0
- package/dist/lib/diagram.js +2 -0
- package/dist/lib/diagram.js.map +1 -0
- package/dist/lib/dicom.js +2 -0
- package/dist/lib/dicom.js.map +1 -0
- package/dist/lib/dicomCodecs.js +2 -0
- package/dist/lib/dicomCodecs.js.map +1 -0
- package/dist/lib/dicomRender.js +2 -0
- package/dist/lib/dicomRender.js.map +1 -0
- package/dist/lib/docxNavigation.js +2 -0
- package/dist/lib/docxNavigation.js.map +1 -0
- package/dist/lib/dxf.js +2 -0
- package/dist/lib/dxf.js.map +1 -0
- package/dist/lib/eml.js +2 -0
- package/dist/lib/eml.js.map +1 -0
- package/dist/lib/encoding.js +2 -0
- package/dist/lib/encoding.js.map +1 -0
- package/dist/lib/epl/code/codeDataParser.js +2 -0
- package/dist/lib/epl/code/codeDataParser.js.map +1 -0
- package/dist/lib/epl/code/codeLines.js +2 -0
- package/dist/lib/epl/code/codeLines.js.map +1 -0
- package/dist/lib/epl/code/textCode.js +2 -0
- package/dist/lib/epl/code/textCode.js.map +1 -0
- package/dist/lib/epl/eplDocument.js +2 -0
- package/dist/lib/epl/eplDocument.js.map +1 -0
- package/dist/lib/epl/imageList.js +2 -0
- package/dist/lib/epl/imageList.js.map +1 -0
- package/dist/lib/epl/index.js +2 -0
- package/dist/lib/epl/index.js.map +1 -0
- package/dist/lib/epl/io/binaryReader.js +2 -0
- package/dist/lib/epl/io/binaryReader.js.map +1 -0
- package/dist/lib/epl/io/encoding.js +2 -0
- package/dist/lib/epl/io/encoding.js.map +1 -0
- package/dist/lib/epl/model/eplSystemId.js +2 -0
- package/dist/lib/epl/model/eplSystemId.js.map +1 -0
- package/dist/lib/epl/names/eplLibData.js +2 -0
- package/dist/lib/epl/names/eplLibData.js.map +1 -0
- package/dist/lib/epl/names/idToNameMap.js +2 -0
- package/dist/lib/epl/names/idToNameMap.js.map +1 -0
- package/dist/lib/epl/names/libNameProvider.js +2 -0
- package/dist/lib/epl/names/libNameProvider.js.map +1 -0
- package/dist/lib/epl/program.js +2 -0
- package/dist/lib/epl/program.js.map +1 -0
- package/dist/lib/epl/projectFileReader.js +2 -0
- package/dist/lib/epl/projectFileReader.js.map +1 -0
- package/dist/lib/fb2.js +2 -0
- package/dist/lib/fb2.js.map +1 -0
- package/dist/lib/fileType.js +2 -0
- package/dist/lib/fileType.js.map +1 -0
- package/dist/lib/fonts.js +2 -0
- package/dist/lib/fonts.js.map +1 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/frontmatter.js +2 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/geo.js +2 -0
- package/dist/lib/geo.js.map +1 -0
- package/dist/lib/har.js +2 -0
- package/dist/lib/har.js.map +1 -0
- package/dist/lib/highlight.js +2 -0
- package/dist/lib/highlight.js.map +1 -0
- package/dist/lib/highlightLangs.js +2 -0
- package/dist/lib/highlightLangs.js.map +1 -0
- package/dist/lib/hprof.js +2 -0
- package/dist/lib/hprof.js.map +1 -0
- package/dist/lib/ipynb.js +2 -0
- package/dist/lib/ipynb.js.map +1 -0
- package/dist/lib/jks.js +2 -0
- package/dist/lib/jks.js.map +1 -0
- package/dist/lib/jsonl.js +2 -0
- package/dist/lib/jsonl.js.map +1 -0
- package/dist/lib/lrc.js +2 -0
- package/dist/lib/lrc.js.map +1 -0
- package/dist/lib/lzma.js +2 -0
- package/dist/lib/lzma.js.map +1 -0
- package/dist/lib/markdown/blocks.js +2 -0
- package/dist/lib/markdown/blocks.js.map +1 -0
- package/dist/lib/markdown/inline.js +2 -0
- package/dist/lib/markdown/inline.js.map +1 -0
- package/dist/lib/markdown/math.js +2 -0
- package/dist/lib/markdown/math.js.map +1 -0
- package/dist/lib/markdown/segmentize.js +2 -0
- package/dist/lib/markdown/segmentize.js.map +1 -0
- package/dist/lib/markdown/styles.js +2 -0
- package/dist/lib/markdown/styles.js.map +1 -0
- package/dist/lib/mht.js +2 -0
- package/dist/lib/mht.js.map +1 -0
- package/dist/lib/midi.js +2 -0
- package/dist/lib/midi.js.map +1 -0
- package/dist/lib/mime.js +2 -0
- package/dist/lib/mime.js.map +1 -0
- package/dist/lib/mindmap.js +2 -0
- package/dist/lib/mindmap.js.map +1 -0
- package/dist/lib/mobi.js +2 -0
- package/dist/lib/mobi.js.map +1 -0
- package/dist/lib/msi.js +2 -0
- package/dist/lib/msi.js.map +1 -0
- package/dist/lib/optionalDep.js +2 -0
- package/dist/lib/optionalDep.js.map +1 -0
- package/dist/lib/pkcs12.js +2 -0
- package/dist/lib/pkcs12.js.map +1 -0
- package/dist/lib/pptxNavigation.js +2 -0
- package/dist/lib/pptxNavigation.js.map +1 -0
- package/dist/lib/qmc.js +2 -0
- package/dist/lib/qmc.js.map +1 -0
- package/dist/lib/rar.js +2 -0
- package/dist/lib/rar.js.map +1 -0
- package/dist/lib/rawPreview.js +2 -0
- package/dist/lib/rawPreview.js.map +1 -0
- package/dist/lib/rc2.js +2 -0
- package/dist/lib/rc2.js.map +1 -0
- package/dist/lib/resolveInput.js +2 -0
- package/dist/lib/resolveInput.js.map +1 -0
- package/dist/lib/rtf.js +2 -0
- package/dist/lib/rtf.js.map +1 -0
- package/dist/lib/sevenZip.js +2 -0
- package/dist/lib/sevenZip.js.map +1 -0
- package/dist/lib/sha1.js +2 -0
- package/dist/lib/sha1.js.map +1 -0
- package/dist/lib/sha256.js +2 -0
- package/dist/lib/sha256.js.map +1 -0
- package/dist/lib/speechAudio.js +2 -0
- package/dist/lib/speechAudio.js.map +1 -0
- package/dist/lib/sqlite.js +2 -0
- package/dist/lib/sqlite.js.map +1 -0
- package/dist/lib/subtitle.js +2 -0
- package/dist/lib/subtitle.js.map +1 -0
- package/dist/lib/tarball.js +2 -0
- package/dist/lib/tarball.js.map +1 -0
- package/dist/lib/viewerFormats.js +2 -0
- package/dist/lib/viewerFormats.js.map +1 -0
- package/dist/lib/viewerKind.js +2 -0
- package/dist/lib/viewerKind.js.map +1 -0
- package/dist/lib/x509.js +2 -0
- package/dist/lib/x509.js.map +1 -0
- package/dist/lib/xlsx.js +2 -0
- package/dist/lib/xlsx.js.map +1 -0
- package/dist/lib/xlsxCharts.js +2 -0
- package/dist/lib/xlsxCharts.js.map +1 -0
- package/dist/lib/xmind.js +2 -0
- package/dist/lib/xmind.js.map +1 -0
- package/dist/lib/zip.js +2 -0
- package/dist/lib/zip.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DicomViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DicomViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { DicomError, readDicom, type DicomSlice, type ParsedDicom } from '@/lib/dicom'\nimport {\n decodeDicomFrame,\n transferSyntaxName,\n UnsupportedTransferSyntaxError,\n} from '@/lib/dicomCodecs'\nimport {\n autoWindow,\n buildFramePixels,\n renderToImageData,\n type FramePixels,\n} from '@/lib/dicomRender'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * The side viewer's DICOM (.dcm) mode: fetch a medical image's bytes, parse + decode it entirely in-browser\n * (see lib/dicom.ts → lib/dicomCodecs.ts → lib/dicomRender.ts), and draw it to a canvas with the controls a\n * basic medical viewer needs:\n * - window/level (brightness/contrast): left-drag on the image — horizontal = width, vertical = center.\n * - frames: a slider for multi-frame series (each frame decoded on demand).\n * - invert / zoom (wheel or buttons) / fit / reset.\n * Grayscale (CT/MR/X-ray — MONOCHROME1/2) is the primary path; RGB and palette-colour images render too.\n * The bytes come from the same download endpoint as any other file and never leave the browser.\n *\n * Two frame sources, unified behind {@link framePixelsAt}:\n * • single file (`url`) — frames come from that one file (multi-frame DICOM, e.g. enhanced CT / US cine);\n * • series stack (`slices`) — a list of independent single-frame DICOM files browsed as one scrollable\n * series (the classic \"one folder = one series, one file per slice\" hospital export). The wheel/cine\n * then steps through *files*. Decoded frames are cached + background-prefetched either way.\n */\nconst props = defineProps<{\n url?: string\n name: string\n /** Single-file: that file's size. Stack: the whole series' total bytes (just for the info strip). */\n size?: number\n /** Series-stack mode: independent DICOM files browsed as one series. Takes precedence over `url`. */\n slices?: DicomSlice[]\n /** Initial slice index in stack mode (the file the user actually clicked). */\n start?: number\n}>()\n\n// Stack mode is active when given more than one slice; a lone slice falls back to plain single-file display.\nconst stackMode = computed(() => (props.slices?.length ?? 0) > 1)\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\nconst parsed = ref<ParsedDicom | null>(null)\nconst framePixels = ref<FramePixels | null>(null)\nconst frameIndex = ref(0)\nconst decoding = ref(false)\n\n// Cine playback (multi-frame): decoded frames are cached so scrubbing/auto-play is instant after the first\n// pass, and a background prefetch warms the cache so even the very first loop is smooth.\nconst frameCache = new Map<number, FramePixels>()\nconst playing = ref(false)\nconst cineFps = ref(5)\nlet playTimer = 0\nlet prefetchToken = 0\n\nconst windowCenter = ref(0)\nconst windowWidth = ref(1)\nconst invert = ref(false)\nconst zoom = ref(1)\n// Initial window (from the file or auto) so \"reset\" can restore it after the user drags.\nlet initialCenter = 0\nlet initialWidth = 1\n\nconst canvasRef = ref<HTMLCanvasElement | null>(null)\nconst stageRef = ref<HTMLDivElement | null>(null)\nlet fitScale = 1\n\nconst meta = computed(() => parsed.value?.meta ?? null)\n// Full metadata table (patient/study/series/…), shown on demand via the toolbar toggle. Off by default —\n// it can include patient demographics (PHI), so the user opts in per file.\nconst showMeta = ref(false)\nconst tagGroups = computed(() => parsed.value?.tags ?? [])\nconst isGray = computed(() => framePixels.value?.mode === 'gray')\nconst totalFrames = computed(() =>\n stackMode.value ? props.slices!.length : (meta.value?.numberOfFrames ?? 1),\n)\nconst tsName = computed(() => (meta.value ? transferSyntaxName(meta.value.transferSyntaxUid) : ''))\n\nasync function loadFile(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n parsed.value = null\n framePixels.value = null\n zoom.value = 1\n invert.value = false\n showMeta.value = false\n stopPlay()\n frameCache.clear()\n prefetchToken++ // abort any prefetch still running for the previous file\n // Stack mode opens on the slice the user clicked; single-file always starts at frame 0.\n const startAt =\n stackMode.value && props.start != null\n ? Math.min(Math.max(0, props.start), props.slices!.length - 1)\n : 0\n frameIndex.value = startAt\n try {\n if (!stackMode.value) {\n const res = await fetch(props.url ?? '')\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n parsed.value = readDicom(await res.arrayBuffer())\n }\n // decodeFrame owns the ready/error transition (and waits for the canvas to mount before painting). In\n // stack mode it parses slice `startAt` itself and establishes the representative meta from it.\n if (await decodeFrame(startAt, true)) void prefetchFrames()\n } catch (e) {\n fail(e)\n }\n}\n\nfunction fail(e: unknown): void {\n errorMsg.value =\n e instanceof DicomError || e instanceof UnsupportedTransferSyntaxError || e instanceof Error\n ? e.message\n : String(e)\n status.value = 'error'\n}\n\n// Decode + display one frame. `resetWindow` re-initialises the window from the file/auto (first load and\n// reset); frame navigation keeps the user's current window.\n// Produce the pixels for index `i` from whichever frame source is active:\n// • stack mode → slice `i` is its own DICOM file: parse it and decode its single frame. The first slice\n// to be decoded also establishes the representative meta/tags shown in the info strip + info panel.\n// • single file → frame `i` of the one already-parsed file.\nasync function framePixelsAt(i: number): Promise<FramePixels> {\n if (stackMode.value) {\n const pd = readDicom(await props.slices![i].load())\n parsed.value ??= pd\n return buildFramePixels(await decodeDicomFrame(pd.meta, pd.frameBytes(0)), pd.meta)\n }\n const p = parsed.value!\n return buildFramePixels(await decodeDicomFrame(p.meta, p.frameBytes(i)), p.meta)\n}\n\nasync function decodeFrame(i: number, resetWindow: boolean): Promise<boolean> {\n if (!stackMode.value && !parsed.value) return false\n let fp = frameCache.get(i)\n if (!fp) {\n decoding.value = true\n try {\n fp = await framePixelsAt(i)\n frameCache.set(i, fp)\n } catch (e) {\n fail(e)\n return false\n } finally {\n decoding.value = false\n }\n }\n // A newer frame was requested while this one was decoding (fast scroll / cine) — drop the stale result.\n if (i !== frameIndex.value) return false\n framePixels.value = fp\n const m = parsed.value?.meta\n if (resetWindow && fp.mode === 'gray' && m) {\n const fileHasWindow = m.windowCenter != null && m.windowWidth != null && m.windowWidth > 0\n const w = fileHasWindow\n ? { center: m.windowCenter as number, width: m.windowWidth as number }\n : autoWindow(fp)\n windowCenter.value = w.center\n windowWidth.value = w.width\n initialCenter = w.center\n initialWidth = w.width\n }\n // First successful frame: flip to 'ready' so the <canvas> mounts, then wait a tick before the first paint\n // (otherwise draw() runs against an unmounted canvas and no-ops, leaving it black). Later frames just draw.\n const firstPaint = status.value !== 'ready'\n status.value = 'ready'\n if (firstPaint) {\n await nextTick()\n computeFit()\n }\n draw()\n return true\n}\n\n// Background-decode the remaining frames into the cache so cine playback and scrubbing are smooth from the\n// first loop. Yields between frames to keep the UI responsive; aborts if the file changes (token bump).\nasync function prefetchFrames(): Promise<void> {\n const n = totalFrames.value\n if (n <= 1 || (!stackMode.value && !parsed.value)) return\n const token = ++prefetchToken\n for (let i = 0; i < n; i++) {\n if (token !== prefetchToken) return // file changed — stop\n if (!frameCache.has(i)) {\n try {\n const fp = await framePixelsAt(i)\n if (token !== prefetchToken) return\n frameCache.set(i, fp)\n } catch {\n // Ignore — navigating to this frame on demand will surface the error if there is one.\n }\n }\n await new Promise((r) => setTimeout(r, 0)) // yield to the event loop between frames\n }\n}\n\n// --- drawing -------------------------------------------------------------------------------------------\nlet rafId = 0\nfunction scheduleDraw(): void {\n if (rafId) return\n rafId = requestAnimationFrame(() => {\n rafId = 0\n draw()\n })\n}\n\nfunction draw(): void {\n const fp = framePixels.value\n const cv = canvasRef.value\n if (!fp || !cv) return\n if (cv.width !== fp.width) cv.width = fp.width\n if (cv.height !== fp.height) cv.height = fp.height\n const ctx = cv.getContext('2d')\n if (!ctx) return\n const img = renderToImageData(fp, {\n windowCenter: windowCenter.value,\n windowWidth: windowWidth.value,\n invert: invert.value,\n })\n ctx.putImageData(img, 0, 0)\n applyDisplaySize()\n}\n\nfunction computeFit(): void {\n const st = stageRef.value\n const fp = framePixels.value\n if (!st || !fp) return\n const cw = Math.max(1, st.clientWidth - 24)\n const ch = Math.max(1, st.clientHeight - 24)\n const s = Math.min(cw / fp.width, ch / fp.height)\n fitScale = Number.isFinite(s) && s > 0 ? s : 1\n}\n\nfunction applyDisplaySize(): void {\n const cv = canvasRef.value\n const fp = framePixels.value\n if (!cv || !fp) return\n const scale = fitScale * zoom.value\n cv.style.width = `${Math.max(1, Math.round(fp.width * scale))}px`\n cv.style.height = `${Math.max(1, Math.round(fp.height * scale))}px`\n}\n\n// --- interaction ---------------------------------------------------------------------------------------\n// Left-drag = window/level (radiology convention: horizontal → width/contrast, vertical → center/brightness).\n// Disabled for RGB/palette images (no VOI window applies).\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0 || !isGray.value) return\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n el.setPointerCapture(e.pointerId)\n const startX = e.clientX\n const startY = e.clientY\n const startWW = windowWidth.value\n const startWC = windowCenter.value\n const fp = framePixels.value\n const range = fp ? fp.maxValue - fp.minValue || 255 : 255\n const k = Math.max(1, range / 512)\n const move = (ev: PointerEvent) => {\n windowWidth.value = Math.max(1, startWW + (ev.clientX - startX) * k)\n windowCenter.value = startWC + (ev.clientY - startY) * k\n scheduleDraw()\n }\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n}\n\nfunction onWheel(e: WheelEvent): void {\n e.preventDefault()\n // Multi-frame DICOM: the wheel scrubs through slices like a hospital viewer (hold Ctrl/Shift to zoom).\n // Single-frame: nothing to scrub, so the wheel zooms as before.\n if (totalFrames.value > 1 && !e.ctrlKey && !e.shiftKey) {\n stopPlay() // a manual scrub takes over from auto-play\n stepFrame(e.deltaY > 0 ? 1 : -1)\n return\n }\n const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12\n zoom.value = Math.min(20, Math.max(0.1, zoom.value * factor))\n applyDisplaySize()\n}\n\n// --- cine (multi-frame playback) -----------------------------------------------------------------------\n// Wheel/buttons clamp at the ends; auto-play wraps so it loops like a cine clip.\nfunction stepFrame(delta: number): void {\n const n = totalFrames.value\n if (n <= 1) return\n frameIndex.value = Math.min(n - 1, Math.max(0, frameIndex.value + delta))\n}\n\nfunction togglePlay(): void {\n if (playing.value) stopPlay()\n else startPlay()\n}\nfunction startPlay(): void {\n if (totalFrames.value <= 1 || playing.value) return\n playing.value = true\n const tick = () => {\n if (!playing.value) return\n const n = totalFrames.value\n frameIndex.value = (frameIndex.value + 1) % n // wrap → loop\n playTimer = window.setTimeout(tick, Math.max(20, Math.round(1000 / cineFps.value)))\n }\n tick()\n}\nfunction stopPlay(): void {\n playing.value = false\n if (playTimer) {\n clearTimeout(playTimer)\n playTimer = 0\n }\n}\n\nfunction zoomBy(factor: number): void {\n zoom.value = Math.min(20, Math.max(0.1, zoom.value * factor))\n applyDisplaySize()\n}\n\nfunction fitToStage(): void {\n zoom.value = 1\n computeFit()\n applyDisplaySize()\n}\n\nfunction resetView(): void {\n zoom.value = 1\n invert.value = false\n if (framePixels.value?.mode === 'gray') {\n windowCenter.value = initialCenter\n windowWidth.value = initialWidth\n }\n computeFit()\n draw()\n}\n\nwatch(frameIndex, (i) => {\n if (status.value === 'ready') void decodeFrame(i, false)\n})\nwatch(invert, scheduleDraw)\n\n// Re-fit when the panel is resized (the splitter or window changes the stage size).\nlet ro: ResizeObserver | null = null\nonMounted(() => {\n void loadFile()\n if (typeof ResizeObserver !== 'undefined' && stageRef.value) {\n ro = new ResizeObserver(() => {\n computeFit()\n applyDisplaySize()\n })\n ro.observe(stageRef.value)\n }\n})\n// Reload when the source changes — a new single-file URL, or a new series stack (identity changes when the\n// browser rebuilds the slice list for a different folder / clicked file).\nwatch([() => props.url, () => props.slices], loadFile)\nonBeforeUnmount(() => {\n stopPlay()\n prefetchToken++ // stop any in-flight prefetch\n if (rafId) cancelAnimationFrame(rafId)\n ro?.disconnect()\n})\n\nconst wcLabel = computed(() => Math.round(windowCenter.value))\nconst wwLabel = computed(() => Math.round(windowWidth.value))\nconst zoomLabel = computed(() => `${Math.round(fitScale * zoom.value * 100)}%`)\n</script>\n\n<template>\n <div class=\"dv\">\n <p v-if=\"status === 'loading'\" class=\"dv__msg\">加载并解码 DICOM…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"dv__msg dv__msg--error\">\n <p>{{ errorMsg }}</p>\n <a class=\"dv__dl\" :href=\"url\" :download=\"name\">下载原文件</a>\n <button type=\"button\" class=\"dv__retry\" @click=\"loadFile\">重试</button>\n </div>\n\n <template v-else>\n <!-- toolbar -->\n <div class=\"dv__bar\">\n <div class=\"dv__group\">\n <button type=\"button\" class=\"dv__btn\" title=\"缩小\" @click=\"zoomBy(1 / 1.2)\">−</button>\n <span class=\"dv__zoom\">{{ zoomLabel }}</span>\n <button type=\"button\" class=\"dv__btn\" title=\"放大\" @click=\"zoomBy(1.2)\">+</button>\n <button type=\"button\" class=\"dv__btn\" title=\"适应窗口\" @click=\"fitToStage\">适应</button>\n </div>\n <div class=\"dv__group\">\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': invert }\"\n :disabled=\"!isGray\"\n title=\"反相显示\"\n @click=\"invert = !invert\"\n >\n 反相\n </button>\n <button type=\"button\" class=\"dv__btn\" title=\"重置窗位/缩放\" @click=\"resetView\">\n 重置\n </button>\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': showMeta }\"\n :disabled=\"!tagGroups.length\"\n title=\"DICOM 元数据\"\n @click=\"showMeta = !showMeta\"\n >\n 信息\n </button>\n </div>\n </div>\n\n <!-- image canvas -->\n <div ref=\"stageRef\" class=\"dv__stage\" @wheel=\"onWheel\">\n <canvas\n ref=\"canvasRef\"\n class=\"dv__canvas\"\n :class=\"{ 'dv__canvas--wl': isGray }\"\n @pointerdown=\"onPointerDown\"\n />\n <span v-if=\"decoding\" class=\"dv__decoding\">解码中…</span>\n\n <!-- DICOM metadata overlay (toggled by the toolbar's 信息 button). Covers the image while open;\n scrolls on its own — @wheel.stop keeps the wheel from zooming the image underneath. -->\n <div v-if=\"showMeta\" class=\"dv__meta\" @wheel.stop>\n <div class=\"dv__meta-head\">\n <span>DICOM 元数据</span>\n <button type=\"button\" class=\"dv__meta-close\" title=\"关闭\" @click=\"showMeta = false\">\n ✕\n </button>\n </div>\n <div v-for=\"g in tagGroups\" :key=\"g.title\" class=\"dv__meta-group\">\n <h4 class=\"dv__meta-title\">{{ g.title }}</h4>\n <div v-for=\"r in g.rows\" :key=\"r.label\" class=\"dv__meta-row\">\n <span class=\"dv__meta-label\">{{ r.label }}</span>\n <span class=\"dv__meta-val\">{{ r.value }}</span>\n </div>\n </div>\n <p v-if=\"!tagGroups.length\" class=\"dv__meta-empty\">无可显示的元数据</p>\n </div>\n </div>\n\n <!-- multi-frame: cine playback + scrub slider (wheel over the image also scrubs) -->\n <div v-if=\"totalFrames > 1\" class=\"dv__frames\">\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': playing }\"\n :title=\"playing ? '暂停' : '播放(循环)'\"\n @click=\"togglePlay\"\n >\n {{ playing ? '⏸' : '▶' }}\n </button>\n <select v-model.number=\"cineFps\" class=\"dv__fps\" title=\"播放速度(帧/秒)\">\n <option :value=\"5\">5 fps</option>\n <option :value=\"10\">10 fps</option>\n <option :value=\"15\">15 fps</option>\n <option :value=\"24\">24 fps</option>\n </select>\n <button\n type=\"button\"\n class=\"dv__btn\"\n :disabled=\"frameIndex <= 0\"\n @click=\"(stopPlay(), (frameIndex = Math.max(0, frameIndex - 1)))\"\n >\n ‹\n </button>\n <input\n v-model.number=\"frameIndex\"\n type=\"range\"\n class=\"dv__range\"\n min=\"0\"\n :max=\"totalFrames - 1\"\n step=\"1\"\n @pointerdown=\"stopPlay\"\n />\n <button\n type=\"button\"\n class=\"dv__btn\"\n :disabled=\"frameIndex >= totalFrames - 1\"\n @click=\"(stopPlay(), (frameIndex = Math.min(totalFrames - 1, frameIndex + 1)))\"\n >\n ›\n </button>\n <span class=\"dv__frameno\">{{ frameIndex + 1 }} / {{ totalFrames }}</span>\n <span class=\"dv__hint\">滚轮切换层面</span>\n </div>\n\n <!-- metadata strip -->\n <div v-if=\"meta\" class=\"dv__info\">\n <span v-if=\"meta.modality\" class=\"dv__tag\">{{ meta.modality }}</span>\n <span v-if=\"stackMode\" class=\"dv__series\">序列 · {{ totalFrames }} 层</span>\n <span>{{ meta.columns }}×{{ meta.rows }}</span>\n <span>{{ meta.bitsStored }}/{{ meta.bitsAllocated }}-bit</span>\n <span>{{ meta.photometric }}</span>\n <span v-if=\"isGray\" class=\"dv__wl\">窗位 C {{ wcLabel }} · W {{ wwLabel }}</span>\n <span class=\"dv__ts\">{{ tsName }}</span>\n <span v-if=\"props.size\">{{ formatBytes(props.size) }}</span>\n <span v-if=\"meta.seriesDescription\" class=\"dv__desc\" :title=\"meta.seriesDescription\">{{\n meta.seriesDescription\n }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.dv {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.dv__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dv__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dv__dl,\n.dv__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n text-decoration: none;\n}\n.dv__dl:hover,\n.dv__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Toolbar --------------------------------------------------------------------------------------------- */\n.dv__bar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.dv__group {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n.dv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 28px;\n padding: 0 9px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.dv__btn:hover:not(:disabled) {\n border-color: var(--accent);\n}\n.dv__btn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.dv__btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.dv__zoom {\n min-width: 46px;\n text-align: center;\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Image stage --------------------------------------------------------------------------------------- */\n.dv__stage {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 12px;\n /* a neutral dark mat so grayscale medical images read with proper contrast */\n background: repeating-conic-gradient(#1a1a1a 0% 25%, #202020 0% 50%) 50% / 22px 22px;\n}\n.dv__canvas {\n display: block;\n margin: auto;\n image-rendering: auto;\n background: #000;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);\n}\n.dv__canvas--wl {\n cursor: crosshair;\n touch-action: none;\n}\n.dv__decoding {\n position: absolute;\n top: 10px;\n right: 12px;\n padding: 3px 9px;\n border-radius: 999px;\n background: rgba(0, 0, 0, 0.6);\n color: #fff;\n font-size: 11px;\n}\n\n/* Metadata overlay --------------------------------------------------------------------------------- */\n.dv__meta {\n position: absolute;\n inset: 0;\n z-index: 3;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 10px 14px 18px;\n background: color-mix(in srgb, var(--surface) 94%, transparent);\n backdrop-filter: blur(2px);\n}\n.dv__meta-head {\n position: sticky;\n top: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin: -10px -14px 8px;\n padding: 9px 14px;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.dv__meta-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n font-size: 13px;\n cursor: pointer;\n}\n.dv__meta-close:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dv__meta-group {\n margin-top: 12px;\n}\n.dv__meta-title {\n margin: 0 0 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--accent);\n}\n.dv__meta-row {\n display: flex;\n gap: 10px;\n padding: 3px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.dv__meta-label {\n flex: 0 0 38%;\n color: var(--faint);\n}\n.dv__meta-val {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--text);\n word-break: break-word;\n font-variant-numeric: tabular-nums;\n}\n.dv__meta-empty {\n margin-top: 16px;\n font-size: 12.5px;\n color: var(--faint);\n}\n\n/* Frame slider -------------------------------------------------------------------------------------- */\n.dv__frames {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n}\n.dv__range {\n flex: 1 1 auto;\n min-width: 0;\n accent-color: var(--accent);\n}\n.dv__fps {\n flex-shrink: 0;\n height: 28px;\n padding: 0 4px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n.dv__frameno {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.dv__hint {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n opacity: 0.8;\n}\n\n/* Metadata strip ------------------------------------------------------------------------------------ */\n.dv__info {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\n padding: 9px 14px;\n border-top: 1px solid var(--border);\n font-size: 11.5px;\n color: var(--faint);\n}\n.dv__tag {\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--accent);\n color: #fff;\n font-weight: 600;\n}\n.dv__series {\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--accent);\n color: var(--accent);\n font-weight: 600;\n}\n.dv__wl {\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.dv__ts {\n color: var(--muted);\n}\n.dv__desc {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocxNavigationPane.js","names":[],"sources":["../../src/components/DocxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ElementNavigationItem } from '@/composables/useElementNavigation'\nimport Icon from '@/components/icons/Icon.vue'\n\nconst props = defineProps<{\n outlineItems: readonly ElementNavigationItem[]\n pageItems: readonly ElementNavigationItem[]\n activeOutlineId: string\n activePageId: string\n}>()\n\nconst emit = defineEmits<{ navigate: [item: ElementNavigationItem] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\n\nfunction isActive(item: ElementNavigationItem): boolean {\n return (view.value === 'outline' ? props.activeOutlineId : props.activePageId) === item.id\n}\n</script>\n\n<template>\n <aside class=\"dn\" :class=\"{ 'dn--collapsed': collapsed }\" aria-label=\"文档导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"dn__reopen\"\n aria-label=\"展开文档导航\"\n title=\"展开文档导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"dn__header\">\n <div>\n <strong class=\"dn__title\">导航</strong>\n <span class=\"dn__count\">{{ pageItems.length }} 页</span>\n </div>\n <button\n type=\"button\"\n class=\"dn__collapse\"\n aria-label=\"收起文档导航\"\n title=\"收起文档导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <div class=\"dn__tabs\" role=\"tablist\" aria-label=\"导航方式\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'outline' }\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'pages' }\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n\n <nav class=\"dn__list\" aria-label=\"文档目录\">\n <template v-if=\"view === 'outline'\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__outline-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :style=\"{ paddingLeft: `${10 + item.level * 12}px` }\"\n :aria-current=\"isActive(item) ? 'location' : undefined\"\n :title=\"item.title\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__marker\" />\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n <p v-if=\"!outlineItems.length\" class=\"dn__empty\">\n 文档未设置标题层级,可切换到“页面”导航。\n </p>\n </template>\n\n <template v-else>\n <button\n v-for=\"(item, index) in pageItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__page-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :aria-current=\"isActive(item) ? 'page' : undefined\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__page-sheet\">{{ index + 1 }}</span>\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.dn {\n position: relative;\n flex: 0 0 166px;\n width: 166px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.dn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.dn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.dn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.dn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.dn__collapse,\n.dn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.dn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.dn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n.dn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__tabs {\n margin: 8px 8px 4px;\n padding: 2px;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.dn__tab {\n min-width: 0;\n padding: 5px 4px;\n border: 0;\n border-radius: 6px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.dn__tab--active {\n color: var(--text-strong);\n background: var(--surface);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n.dn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 4px 6px 18px;\n scrollbar-width: thin;\n}\n.dn__outline-item,\n.dn__page-item {\n position: relative;\n width: 100%;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 7px;\n border: 0;\n border-radius: 7px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dn__outline-item {\n min-height: 31px;\n padding-top: 5px;\n padding-right: 7px;\n padding-bottom: 5px;\n}\n.dn__page-item {\n padding: 6px;\n}\n.dn__outline-item:hover,\n.dn__page-item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.dn__marker {\n flex: 0 0 auto;\n width: 3px;\n height: 15px;\n border-radius: 999px;\n background: var(--border-2);\n}\n.dn__item--active .dn__marker {\n background: var(--accent);\n}\n.dn__page-sheet {\n flex: 0 0 auto;\n width: 26px;\n height: 34px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n color: var(--faint);\n background: var(--surface);\n font-size: 9.5px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n.dn__item--active .dn__page-sheet {\n border-color: var(--accent);\n color: var(--accent);\n}\n.dn__label {\n min-width: 0;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 11.5px;\n line-height: 1.35;\n}\n.dn__empty {\n margin: 10px 8px;\n color: var(--faint);\n font-size: 10.5px;\n line-height: 1.55;\n}\n\n@container (max-width: 500px) {\n .dn {\n flex-basis: 136px;\n width: 136px;\n }\n .dn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocxNavigationPane.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ElementNavigationItem } from '@/composables/useElementNavigation'\nimport Icon from '@/components/icons/Icon.vue'\n\nconst props = defineProps<{\n outlineItems: readonly ElementNavigationItem[]\n pageItems: readonly ElementNavigationItem[]\n activeOutlineId: string\n activePageId: string\n}>()\n\nconst emit = defineEmits<{ navigate: [item: ElementNavigationItem] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\n\nfunction isActive(item: ElementNavigationItem): boolean {\n return (view.value === 'outline' ? props.activeOutlineId : props.activePageId) === item.id\n}\n</script>\n\n<template>\n <aside class=\"dn\" :class=\"{ 'dn--collapsed': collapsed }\" aria-label=\"文档导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"dn__reopen\"\n aria-label=\"展开文档导航\"\n title=\"展开文档导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"dn__header\">\n <div>\n <strong class=\"dn__title\">导航</strong>\n <span class=\"dn__count\">{{ pageItems.length }} 页</span>\n </div>\n <button\n type=\"button\"\n class=\"dn__collapse\"\n aria-label=\"收起文档导航\"\n title=\"收起文档导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <div class=\"dn__tabs\" role=\"tablist\" aria-label=\"导航方式\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'outline' }\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'pages' }\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n\n <nav class=\"dn__list\" aria-label=\"文档目录\">\n <template v-if=\"view === 'outline'\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__outline-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :style=\"{ paddingLeft: `${10 + item.level * 12}px` }\"\n :aria-current=\"isActive(item) ? 'location' : undefined\"\n :title=\"item.title\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__marker\" />\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n <p v-if=\"!outlineItems.length\" class=\"dn__empty\">\n 文档未设置标题层级,可切换到“页面”导航。\n </p>\n </template>\n\n <template v-else>\n <button\n v-for=\"(item, index) in pageItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__page-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :aria-current=\"isActive(item) ? 'page' : undefined\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__page-sheet\">{{ index + 1 }}</span>\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.dn {\n position: relative;\n flex: 0 0 166px;\n width: 166px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.dn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.dn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.dn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.dn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.dn__collapse,\n.dn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.dn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.dn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n.dn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__tabs {\n margin: 8px 8px 4px;\n padding: 2px;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.dn__tab {\n min-width: 0;\n padding: 5px 4px;\n border: 0;\n border-radius: 6px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.dn__tab--active {\n color: var(--text-strong);\n background: var(--surface);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n.dn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 4px 6px 18px;\n scrollbar-width: thin;\n}\n.dn__outline-item,\n.dn__page-item {\n position: relative;\n width: 100%;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 7px;\n border: 0;\n border-radius: 7px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dn__outline-item {\n min-height: 31px;\n padding-top: 5px;\n padding-right: 7px;\n padding-bottom: 5px;\n}\n.dn__page-item {\n padding: 6px;\n}\n.dn__outline-item:hover,\n.dn__page-item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.dn__marker {\n flex: 0 0 auto;\n width: 3px;\n height: 15px;\n border-radius: 999px;\n background: var(--border-2);\n}\n.dn__item--active .dn__marker {\n background: var(--accent);\n}\n.dn__page-sheet {\n flex: 0 0 auto;\n width: 26px;\n height: 34px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n color: var(--faint);\n background: var(--surface);\n font-size: 9.5px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n.dn__item--active .dn__page-sheet {\n border-color: var(--accent);\n color: var(--accent);\n}\n.dn__label {\n min-width: 0;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 11.5px;\n line-height: 1.35;\n}\n.dn__empty {\n margin: 10px 8px;\n color: var(--faint);\n font-size: 10.5px;\n line-height: 1.55;\n}\n\n@container (max-width: 500px) {\n .dn {\n flex-basis: 136px;\n width: 136px;\n }\n .dn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocxViewer.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n if (props.url !== url || docEl.value !== el) return\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready'\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page: a white sheet with a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n margin-bottom: 0;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n if (props.url !== url || docEl.value !== el) return\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready'\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page: a white sheet with a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n margin-bottom: 0;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAqBA,IAAM,IAAQ,GAQR,IAAS,EAAmB,SAAS,GACrC,IAAW,EAAW,EAAE,GAExB,IAAQ,EAA+B,IAAI,GAC3C,IAAU,EAAgC,SAAS,GACnD,IAAe,EAAW,EAAK,GAC/B,EAAE,iBAAc,cAAW,oBAAiB,iBAAc,aAAU,gBACxE,EAAqB,CAAK;EAE5B,eAAe,IAAwB;GACrC,IAAM,IAAK,EAAM;GACjB,IAAI,CAAC,EAAM,OAAO,CAAC,GAAI;GAIvB,AAHA,EAAO,QAAQ,WACf,EAAS,QAAQ,IACjB,EAAS,CAAC,GAAG,CAAC,CAAC,GACf,EAAG,YAAY;GACf,IAAM,IAAM,EAAM;GAClB,IAAI;IACF,IAAM,IAAM,MAAM,MAAM,CAAG;IAC3B,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,QAAQ,EAAI,QAAQ;IACjD,IAAM,IAAM,MAAM,EAAI,YAAY;IAElC,IAAI,EAAM,QAAQ,KAAO,EAAM,UAAU,GAAI;IAC7C,IAAM,IAAY,MAAM,EAAY,GAAK,GAAI,KAAA,GAAW;KACtD,WAAW;KACX,WAAW;KAGX,aAAa;KACb,cAAc;KACd,YAAY;KACZ,eAAe;KACf,eAAe;KACf,iBAAiB;KACjB,gBAAgB;KAChB,cAAc;IAChB,CAAC;IACD,IAAI,EAAM,QAAQ,KAAO,EAAM,UAAU,GAAI;IAC7C,IAAI;IACJ,IAAI;KACF,IAAa,EAAoB,GAAU,CAAE;IAC/C,SAAS,GAAO;KAGd,AADA,QAAQ,KAAK,wCAAwC,CAAK,GAC1D,IAAa;MAAE,SAAS,CAAC;MAAG,OAAO,EAAwB,CAAE;KAAE;IACjE;IAGA,AAFA,EAAS,EAAW,SAAS,EAAW,KAAK,GAC7C,EAAQ,QAAQ,EAAW,QAAQ,SAAS,YAAY,SACxD,EAAO,QAAQ;GACjB,SAAS,GAAG;IAEV,AADA,EAAS,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC1D,EAAO,QAAQ;GACjB;EACF;SAEA,EAAU,CAAM,GAChB,QAAY,EAAM,KAAK,CAAM,GAC7B,QAAsB;GACpB,AAAI,EAAM,UAAO,EAAM,MAAM,YAAY;EAC3C,CAAC,mBAIC,EAoBM,OApBN,GAoBM,CAlBI,EAAA,UAAM,WAAA,EAAA,GADd,EASE,GAAA;;GAPQ,WAAW,EAAA;2CAAY,QAAA;GACvB,MAAM,EAAA;sCAAO,QAAA;GACpB,iBAAe,EAAA,CAAA;GACf,cAAY,EAAA,CAAA;GACZ,qBAAmB,EAAA,CAAA;GACnB,kBAAgB,EAAA,CAAA;GAChB,YAAU,EAAA,CAAA;;;;;;;;;mBAEb,EAQM,OARN,GAQM,CAPK,EAAA,UAAM,aAAA,EAAA,GAAf,EAAgE,KAAhE,GAA+C,eAAa,KAC5C,EAAA,UAAM,WAAA,EAAA,GAAtB,EAGM,OAHN,GAGM,CAFJ,EAA0B,KAAA,MAAvB,UAAK,EAAG,EAAA,KAAQ,GAAA,CAAA,GACnB,EAAmE,UAAA;GAA3D,MAAK;GAAS,OAAM;GAAa,SAAO;KAAQ,IAAE,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAAA,EAG5D,EAA+D,OAAA;YAA1B;GAAJ,KAAI;GAAQ,OAAM;sBAAtC,EAAA,UAAM,OAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DrawioViewer.js","names":[],"sources":["../../src/components/DrawioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { renderDiagram, type DiagramKind } from '@/lib/diagram'\n\n/**\n * Renders a .drawio (mxGraph) or .excalidraw scene as an inline SVG (see lib/diagram.ts — hand-rolled,\n * zero-dependency, best-effort). Consumes the text the side viewer already fetched (no second download),\n * like the subtitle/geo/csv renderers. Rendering is async (drawio diagrams are usually DEFLATE-compressed).\n * The produced SVG is OUR markup built from the parsed geometry, so v-html is safe; on a parse failure the\n * user can fall back to the source toggle (raw XML/JSON) or download the file.\n */\nconst props = defineProps<{ text: string; name: string; kind: DiagramKind }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst svg = ref('')\nconst errorMsg = ref('')\n\nasync function render(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n svg.value = ''\n const t = props.text\n try {\n const out = await renderDiagram(t, props.kind)\n if (props.text !== t) return // a newer text superseded us mid-render\n svg.value = out\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nwatch(() => [props.text, props.kind], render, { immediate: true })\n</script>\n\n<template>\n <div class=\"dgm\">\n <p v-if=\"status === 'loading'\" class=\"dgm__msg\">正在渲染图表…</p>\n <div v-else-if=\"status === 'error'\" class=\"dgm__msg dgm__msg--error\">\n <p>无法渲染该图表:{{ errorMsg }}</p>\n <p class=\"dgm__hint\">\n 可切换到「源码」查看原始\n {{ kind === 'excalidraw' ? 'JSON' : 'XML' }},或下载后用对应工具打开。\n </p>\n </div>\n <!-- svg is our own generated markup (no upstream HTML), safe to inline -->\n <div v-else class=\"dgm__scroll\" v-html=\"svg\" />\n </div>\n</template>\n\n<style scoped>\n.dgm {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-2);\n}\n.dgm__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n/* Contain the WHOLE diagram within the pane on BOTH axes so tall or wide diagrams never overflow the\n container (a tall flowchart used to fit-width and run off the bottom). The SVG keeps its aspect ratio via\n its viewBox and shrinks to fit; overflow:auto stays only as a safety net. */\n.dgm__scroll :deep(svg) {\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n}\n.dgm__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dgm__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.dgm__hint {\n color: var(--faint);\n font-size: 12px;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DrawioViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DrawioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { renderDiagram, type DiagramKind } from '@/lib/diagram'\n\n/**\n * Renders a .drawio (mxGraph) or .excalidraw scene as an inline SVG (see lib/diagram.ts — hand-rolled,\n * zero-dependency, best-effort). Consumes the text the side viewer already fetched (no second download),\n * like the subtitle/geo/csv renderers. Rendering is async (drawio diagrams are usually DEFLATE-compressed).\n * The produced SVG is OUR markup built from the parsed geometry, so v-html is safe; on a parse failure the\n * user can fall back to the source toggle (raw XML/JSON) or download the file.\n */\nconst props = defineProps<{ text: string; name: string; kind: DiagramKind }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst svg = ref('')\nconst errorMsg = ref('')\n\nasync function render(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n svg.value = ''\n const t = props.text\n try {\n const out = await renderDiagram(t, props.kind)\n if (props.text !== t) return // a newer text superseded us mid-render\n svg.value = out\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nwatch(() => [props.text, props.kind], render, { immediate: true })\n</script>\n\n<template>\n <div class=\"dgm\">\n <p v-if=\"status === 'loading'\" class=\"dgm__msg\">正在渲染图表…</p>\n <div v-else-if=\"status === 'error'\" class=\"dgm__msg dgm__msg--error\">\n <p>无法渲染该图表:{{ errorMsg }}</p>\n <p class=\"dgm__hint\">\n 可切换到「源码」查看原始\n {{ kind === 'excalidraw' ? 'JSON' : 'XML' }},或下载后用对应工具打开。\n </p>\n </div>\n <!-- svg is our own generated markup (no upstream HTML), safe to inline -->\n <div v-else class=\"dgm__scroll\" v-html=\"svg\" />\n </div>\n</template>\n\n<style scoped>\n.dgm {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-2);\n}\n.dgm__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n/* Contain the WHOLE diagram within the pane on BOTH axes so tall or wide diagrams never overflow the\n container (a tall flowchart used to fit-width and run off the bottom). The SVG keeps its aspect ratio via\n its viewBox and shrinks to fit; overflow:auto stays only as a safety net. */\n.dgm__scroll :deep(svg) {\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n}\n.dgm__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dgm__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.dgm__hint {\n color: var(--faint);\n font-size: 12px;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;EAWA,IAAM,IAAQ,GAGR,IAAS,EAAY,SAAS,GAC9B,IAAM,EAAI,EAAE,GACZ,IAAW,EAAI,EAAE;EAEvB,eAAe,IAAwB;GAGrC,AAFA,EAAO,QAAQ,WACf,EAAS,QAAQ,IACjB,EAAI,QAAQ;GACZ,IAAM,IAAI,EAAM;GAChB,IAAI;IACF,IAAM,IAAM,MAAM,EAAc,GAAG,EAAM,IAAI;IAC7C,IAAI,EAAM,SAAS,GAAG;IAEtB,AADA,EAAI,QAAQ,GACZ,EAAO,QAAQ;GACjB,SAAS,GAAG;IAEV,AADA,EAAS,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC1D,EAAO,QAAQ;GACjB;EACF;SAEA,QAAY,CAAC,EAAM,MAAM,EAAM,IAAI,GAAG,GAAQ,EAAE,WAAW,GAAK,CAAC,mBAI/D,EAWM,OAXN,GAWM,CAVK,EAAA,UAAM,aAAA,EAAA,GAAf,EAA2D,KAA3D,GAAgD,SAAO,KACvC,EAAA,UAAM,WAAA,EAAA,GAAtB,EAMM,OANN,GAMM,CALJ,EAA6B,KAAA,MAA1B,aAAQ,EAAG,EAAA,KAAQ,GAAA,CAAA,GACtB,EAGI,KAHJ,GAAqB,mBAEnB,EAAG,EAAA,SAAI,eAAA,SAAA,KAAA,IAAqC,kBAC9C,CAAA,CAAA,CAAA,MAAA,EAAA,GAGF,EAA+C,OAAA;;GAAnC,OAAM;GAAc,WAAQ,EAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DxfViewer.js","names":[],"sources":["../../src/components/DxfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, reactive, ref, watch } from 'vue'\nimport { parseDxf, type DxfPrim, type ParsedDxf } from '@/lib/dxf'\n\n/**\n * Side-viewer for AutoCAD DXF drawings (.dxf). Fetches the text, parses it in-browser (see lib/dxf.ts —\n * hand-rolled, zero-dependency) into drawable primitives, and draws them as an SVG. DXF Y is up, so the whole\n * drawing is rendered inside a y-flip group (text is flipped back locally to stay upright). Drag to pan, wheel\n * to zoom; toggle layers in the list. Binary DXF isn't supported.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst parsed = ref<ParsedDxf | null>(null)\nconst hiddenLayers = reactive(new Set<string>())\n\nasync function load(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n parsed.value = null\n hiddenLayers.clear()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const head = new TextDecoder('latin1').decode(new Uint8Array(buf.slice(0, 22)))\n if (head.startsWith('AutoCAD Binary DXF')) {\n throw new Error('暂不支持二进制 DXF(请用 ASCII DXF)')\n }\n const text = new TextDecoder('utf-8').decode(buf)\n const r = parseDxf(text)\n if (!r.prims.length || !r.bounds) throw new Error('未解析到可绘制的图元')\n parsed.value = r\n status.value = 'ready'\n fit()\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Primitives grouped by layer (so a layer toggles as one <g>).\nconst byLayer = computed(() => {\n const groups = new Map<string, DxfPrim[]>()\n for (const p of parsed.value?.prims ?? []) {\n const g = groups.get(p.layer)\n if (g) g.push(p)\n else groups.set(p.layer, [p])\n }\n return groups\n})\n\n// ── viewBox (pan/zoom), in the FLIPPED coordinate space (content group is scaled y:-1) ──────────\nconst vb = ref({ x: 0, y: 0, w: 1, h: 1 })\n\nfunction fit(): void {\n const b = parsed.value?.bounds\n if (!b) return\n let w = b.maxX - b.minX\n let h = b.maxY - b.minY\n if (!(w > 0)) w = 1\n if (!(h > 0)) h = 1\n const pad = Math.max(w, h) * 0.05\n // flipped: visible y ranges over [-maxY, -minY]\n vb.value = { x: b.minX - pad, y: -b.maxY - pad, w: w + 2 * pad, h: h + 2 * pad }\n}\n\nconst viewBox = computed(() => `${vb.value.x} ${vb.value.y} ${vb.value.w} ${vb.value.h}`)\n\nfunction onWheel(e: WheelEvent): void {\n const f = e.deltaY > 0 ? 1.15 : 1 / 1.15\n const cx = vb.value.x + vb.value.w / 2\n const cy = vb.value.y + vb.value.h / 2\n const nw = vb.value.w * f\n const nh = vb.value.h * f\n vb.value = { x: cx - nw / 2, y: cy - nh / 2, w: nw, h: nh }\n}\n\nlet panning = false\nlet lastX = 0\nlet lastY = 0\nlet svgEl: SVGSVGElement | null = null\nfunction onDown(e: PointerEvent): void {\n panning = true\n lastX = e.clientX\n lastY = e.clientY\n svgEl = e.currentTarget as SVGSVGElement\n svgEl.setPointerCapture(e.pointerId)\n}\nfunction onMove(e: PointerEvent): void {\n if (!panning || !svgEl) return\n const rect = svgEl.getBoundingClientRect()\n const dx = ((e.clientX - lastX) * vb.value.w) / rect.width\n const dy = ((e.clientY - lastY) * vb.value.h) / rect.height\n vb.value = { ...vb.value, x: vb.value.x - dx, y: vb.value.y - dy }\n lastX = e.clientX\n lastY = e.clientY\n}\nfunction onUp(e: PointerEvent): void {\n panning = false\n try {\n svgEl?.releasePointerCapture(e.pointerId)\n } catch {\n /* already released */\n }\n}\n\nfunction toggleLayer(name: string): void {\n if (hiddenLayers.has(name)) hiddenLayers.delete(name)\n else hiddenLayers.add(name)\n}\n\n// ── primitive → SVG geometry helpers ──────────────────────────────────────────────────────────\nconst rnd = (n: number): number => Math.round(n * 1000) / 1000\nfunction polyPoints(pts: [number, number][]): string {\n return pts.map((p) => `${rnd(p[0])},${rnd(p[1])}`).join(' ')\n}\n// Tessellate an arc (angles in degrees, CCW) into a path, avoiding SVG arc-flag/flip pitfalls.\nfunction arcPath(cx: number, cy: number, r: number, a0: number, a1: number): string {\n let span = a1 - a0\n while (span <= 0) span += 360\n while (span > 360) span -= 360\n const steps = Math.max(2, Math.min(180, Math.ceil((span / 360) * 96)))\n let d = ''\n for (let i = 0; i <= steps; i++) {\n const a = ((a0 + (span * i) / steps) * Math.PI) / 180\n d += `${i ? 'L' : 'M'} ${rnd(cx + r * Math.cos(a))} ${rnd(cy + r * Math.sin(a))} `\n }\n return d\n}\n// Point size in user units, scaled to the current zoom so a POINT marker stays ~constant on screen.\nconst ptR = computed(() => Math.max(vb.value.w, vb.value.h) * 0.004)\n\nconst summary = computed(() => {\n const p = parsed.value\n if (!p) return ''\n const total = Object.values(p.counts).reduce((a, b) => a + b, 0)\n return `${total} 实体 · ${p.layers.length} 图层`\n})\n\nwatch(() => props.url, load, { immediate: true })\n</script>\n\n<template>\n <div class=\"dxf\">\n <p v-if=\"status === 'loading'\" class=\"dxf__msg\">正在解析 DXF…</p>\n <div v-else-if=\"status === 'error'\" class=\"dxf__msg dxf__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dxf__btn\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"parsed\">\n <div class=\"dxf__bar\">\n <span class=\"dxf__badge\">DXF</span>\n <span class=\"dxf__dim\">{{ summary }}</span>\n <span class=\"dxf__spacer\" />\n <button type=\"button\" class=\"dxf__reset\" @click=\"fit\">重置视图</button>\n </div>\n\n <div class=\"dxf__stage\">\n <svg\n class=\"dxf__svg\"\n :viewBox=\"viewBox\"\n preserveAspectRatio=\"xMidYMid meet\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onDown\"\n @pointermove=\"onMove\"\n @pointerup=\"onUp\"\n @pointercancel=\"onUp\"\n >\n <!-- DXF Y is up → flip the whole drawing; text is re-flipped locally to stay upright. -->\n <g transform=\"matrix(1 0 0 -1 0 0)\">\n <g v-for=\"[layer, prims] in byLayer\" v-show=\"!hiddenLayers.has(layer)\" :key=\"layer\">\n <template v-for=\"(p, i) in prims\" :key=\"i\">\n <line\n v-if=\"p.kind === 'line'\"\n :x1=\"rnd(p.x1)\"\n :y1=\"rnd(p.y1)\"\n :x2=\"rnd(p.x2)\"\n :y2=\"rnd(p.y2)\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polygon\n v-else-if=\"p.kind === 'polyline' && p.closed\"\n :points=\"polyPoints(p.pts)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polyline\n v-else-if=\"p.kind === 'polyline'\"\n :points=\"polyPoints(p.pts)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <circle\n v-else-if=\"p.kind === 'circle'\"\n :cx=\"rnd(p.cx)\"\n :cy=\"rnd(p.cy)\"\n :r=\"rnd(p.r)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <path\n v-else-if=\"p.kind === 'arc'\"\n :d=\"arcPath(p.cx, p.cy, p.r, p.a0, p.a1)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polygon\n v-else-if=\"p.kind === 'polygon'\"\n :points=\"polyPoints(p.pts)\"\n :fill=\"p.color\"\n fill-opacity=\"0.5\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <circle\n v-else-if=\"p.kind === 'point'\"\n :cx=\"rnd(p.x)\"\n :cy=\"rnd(p.y)\"\n :r=\"ptR\"\n :fill=\"p.color\"\n />\n <g\n v-else-if=\"p.kind === 'text'\"\n :transform=\"`translate(${rnd(p.x)},${rnd(p.y)}) scale(1,-1) rotate(${rnd(-p.rot)})`\"\n >\n <text\n class=\"dxf__text\"\n :font-size=\"rnd(p.h)\"\n :fill=\"p.color\"\n :text-anchor=\"p.align === 'c' ? 'middle' : p.align === 'r' ? 'end' : 'start'\"\n >\n <tspan\n v-for=\"(ln, li) in p.text.split('\\n')\"\n :key=\"li\"\n x=\"0\"\n :dy=\"li === 0 ? 0 : p.h * 1.25\"\n >\n {{ ln }}\n </tspan>\n </text>\n </g>\n </template>\n </g>\n </g>\n </svg>\n </div>\n\n <ul v-if=\"parsed.layers.length\" class=\"dxf__layers\">\n <li v-for=\"l in parsed.layers\" :key=\"l.name\">\n <button\n type=\"button\"\n class=\"dxf__layer\"\n :class=\"{ 'dxf__layer--off': hiddenLayers.has(l.name) }\"\n @click=\"toggleLayer(l.name)\"\n >\n <span class=\"dxf__swatch\" :style=\"{ background: l.color }\" />\n <span class=\"dxf__lname\">{{ l.name }}</span>\n <span class=\"dxf__eye\">{{ hiddenLayers.has(l.name) ? '隐藏' : '显示' }}</span>\n </button>\n </li>\n </ul>\n </template>\n </div>\n</template>\n\n<style scoped>\n.dxf {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.dxf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dxf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dxf__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dxf__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dxf__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--muted);\n}\n.dxf__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.dxf__dim {\n color: var(--faint);\n}\n.dxf__spacer {\n flex: 1 1 auto;\n}\n.dxf__reset {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.dxf__reset:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dxf__stage {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n background: var(--surface-2);\n}\n.dxf__svg {\n width: 100%;\n height: 100%;\n touch-action: none;\n cursor: grab;\n}\n.dxf__svg:active {\n cursor: grabbing;\n}\n.dxf__text {\n font-family: var(--font-sans, sans-serif);\n dominant-baseline: alphabetic;\n}\n\n.dxf__layers {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n flex-shrink: 0;\n max-height: 30%;\n overflow: auto;\n overscroll-behavior: contain;\n border-top: 1px solid var(--border);\n}\n.dxf__layer {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 6px 16px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n color: var(--text);\n cursor: pointer;\n}\n.dxf__layer:hover {\n background: var(--surface-2);\n}\n.dxf__layer--off {\n opacity: 0.45;\n}\n.dxf__swatch {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n border-radius: 3px;\n border: 1px solid var(--border-2);\n}\n.dxf__lname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n}\n.dxf__eye {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DxfViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DxfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, reactive, ref, watch } from 'vue'\nimport { parseDxf, type DxfPrim, type ParsedDxf } from '@/lib/dxf'\n\n/**\n * Side-viewer for AutoCAD DXF drawings (.dxf). Fetches the text, parses it in-browser (see lib/dxf.ts —\n * hand-rolled, zero-dependency) into drawable primitives, and draws them as an SVG. DXF Y is up, so the whole\n * drawing is rendered inside a y-flip group (text is flipped back locally to stay upright). Drag to pan, wheel\n * to zoom; toggle layers in the list. Binary DXF isn't supported.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst parsed = ref<ParsedDxf | null>(null)\nconst hiddenLayers = reactive(new Set<string>())\n\nasync function load(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n parsed.value = null\n hiddenLayers.clear()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const head = new TextDecoder('latin1').decode(new Uint8Array(buf.slice(0, 22)))\n if (head.startsWith('AutoCAD Binary DXF')) {\n throw new Error('暂不支持二进制 DXF(请用 ASCII DXF)')\n }\n const text = new TextDecoder('utf-8').decode(buf)\n const r = parseDxf(text)\n if (!r.prims.length || !r.bounds) throw new Error('未解析到可绘制的图元')\n parsed.value = r\n status.value = 'ready'\n fit()\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Primitives grouped by layer (so a layer toggles as one <g>).\nconst byLayer = computed(() => {\n const groups = new Map<string, DxfPrim[]>()\n for (const p of parsed.value?.prims ?? []) {\n const g = groups.get(p.layer)\n if (g) g.push(p)\n else groups.set(p.layer, [p])\n }\n return groups\n})\n\n// ── viewBox (pan/zoom), in the FLIPPED coordinate space (content group is scaled y:-1) ──────────\nconst vb = ref({ x: 0, y: 0, w: 1, h: 1 })\n\nfunction fit(): void {\n const b = parsed.value?.bounds\n if (!b) return\n let w = b.maxX - b.minX\n let h = b.maxY - b.minY\n if (!(w > 0)) w = 1\n if (!(h > 0)) h = 1\n const pad = Math.max(w, h) * 0.05\n // flipped: visible y ranges over [-maxY, -minY]\n vb.value = { x: b.minX - pad, y: -b.maxY - pad, w: w + 2 * pad, h: h + 2 * pad }\n}\n\nconst viewBox = computed(() => `${vb.value.x} ${vb.value.y} ${vb.value.w} ${vb.value.h}`)\n\nfunction onWheel(e: WheelEvent): void {\n const f = e.deltaY > 0 ? 1.15 : 1 / 1.15\n const cx = vb.value.x + vb.value.w / 2\n const cy = vb.value.y + vb.value.h / 2\n const nw = vb.value.w * f\n const nh = vb.value.h * f\n vb.value = { x: cx - nw / 2, y: cy - nh / 2, w: nw, h: nh }\n}\n\nlet panning = false\nlet lastX = 0\nlet lastY = 0\nlet svgEl: SVGSVGElement | null = null\nfunction onDown(e: PointerEvent): void {\n panning = true\n lastX = e.clientX\n lastY = e.clientY\n svgEl = e.currentTarget as SVGSVGElement\n svgEl.setPointerCapture(e.pointerId)\n}\nfunction onMove(e: PointerEvent): void {\n if (!panning || !svgEl) return\n const rect = svgEl.getBoundingClientRect()\n const dx = ((e.clientX - lastX) * vb.value.w) / rect.width\n const dy = ((e.clientY - lastY) * vb.value.h) / rect.height\n vb.value = { ...vb.value, x: vb.value.x - dx, y: vb.value.y - dy }\n lastX = e.clientX\n lastY = e.clientY\n}\nfunction onUp(e: PointerEvent): void {\n panning = false\n try {\n svgEl?.releasePointerCapture(e.pointerId)\n } catch {\n /* already released */\n }\n}\n\nfunction toggleLayer(name: string): void {\n if (hiddenLayers.has(name)) hiddenLayers.delete(name)\n else hiddenLayers.add(name)\n}\n\n// ── primitive → SVG geometry helpers ──────────────────────────────────────────────────────────\nconst rnd = (n: number): number => Math.round(n * 1000) / 1000\nfunction polyPoints(pts: [number, number][]): string {\n return pts.map((p) => `${rnd(p[0])},${rnd(p[1])}`).join(' ')\n}\n// Tessellate an arc (angles in degrees, CCW) into a path, avoiding SVG arc-flag/flip pitfalls.\nfunction arcPath(cx: number, cy: number, r: number, a0: number, a1: number): string {\n let span = a1 - a0\n while (span <= 0) span += 360\n while (span > 360) span -= 360\n const steps = Math.max(2, Math.min(180, Math.ceil((span / 360) * 96)))\n let d = ''\n for (let i = 0; i <= steps; i++) {\n const a = ((a0 + (span * i) / steps) * Math.PI) / 180\n d += `${i ? 'L' : 'M'} ${rnd(cx + r * Math.cos(a))} ${rnd(cy + r * Math.sin(a))} `\n }\n return d\n}\n// Point size in user units, scaled to the current zoom so a POINT marker stays ~constant on screen.\nconst ptR = computed(() => Math.max(vb.value.w, vb.value.h) * 0.004)\n\nconst summary = computed(() => {\n const p = parsed.value\n if (!p) return ''\n const total = Object.values(p.counts).reduce((a, b) => a + b, 0)\n return `${total} 实体 · ${p.layers.length} 图层`\n})\n\nwatch(() => props.url, load, { immediate: true })\n</script>\n\n<template>\n <div class=\"dxf\">\n <p v-if=\"status === 'loading'\" class=\"dxf__msg\">正在解析 DXF…</p>\n <div v-else-if=\"status === 'error'\" class=\"dxf__msg dxf__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dxf__btn\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"parsed\">\n <div class=\"dxf__bar\">\n <span class=\"dxf__badge\">DXF</span>\n <span class=\"dxf__dim\">{{ summary }}</span>\n <span class=\"dxf__spacer\" />\n <button type=\"button\" class=\"dxf__reset\" @click=\"fit\">重置视图</button>\n </div>\n\n <div class=\"dxf__stage\">\n <svg\n class=\"dxf__svg\"\n :viewBox=\"viewBox\"\n preserveAspectRatio=\"xMidYMid meet\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onDown\"\n @pointermove=\"onMove\"\n @pointerup=\"onUp\"\n @pointercancel=\"onUp\"\n >\n <!-- DXF Y is up → flip the whole drawing; text is re-flipped locally to stay upright. -->\n <g transform=\"matrix(1 0 0 -1 0 0)\">\n <g v-for=\"[layer, prims] in byLayer\" v-show=\"!hiddenLayers.has(layer)\" :key=\"layer\">\n <template v-for=\"(p, i) in prims\" :key=\"i\">\n <line\n v-if=\"p.kind === 'line'\"\n :x1=\"rnd(p.x1)\"\n :y1=\"rnd(p.y1)\"\n :x2=\"rnd(p.x2)\"\n :y2=\"rnd(p.y2)\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polygon\n v-else-if=\"p.kind === 'polyline' && p.closed\"\n :points=\"polyPoints(p.pts)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polyline\n v-else-if=\"p.kind === 'polyline'\"\n :points=\"polyPoints(p.pts)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <circle\n v-else-if=\"p.kind === 'circle'\"\n :cx=\"rnd(p.cx)\"\n :cy=\"rnd(p.cy)\"\n :r=\"rnd(p.r)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <path\n v-else-if=\"p.kind === 'arc'\"\n :d=\"arcPath(p.cx, p.cy, p.r, p.a0, p.a1)\"\n fill=\"none\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <polygon\n v-else-if=\"p.kind === 'polygon'\"\n :points=\"polyPoints(p.pts)\"\n :fill=\"p.color\"\n fill-opacity=\"0.5\"\n :stroke=\"p.color\"\n vector-effect=\"non-scaling-stroke\"\n />\n <circle\n v-else-if=\"p.kind === 'point'\"\n :cx=\"rnd(p.x)\"\n :cy=\"rnd(p.y)\"\n :r=\"ptR\"\n :fill=\"p.color\"\n />\n <g\n v-else-if=\"p.kind === 'text'\"\n :transform=\"`translate(${rnd(p.x)},${rnd(p.y)}) scale(1,-1) rotate(${rnd(-p.rot)})`\"\n >\n <text\n class=\"dxf__text\"\n :font-size=\"rnd(p.h)\"\n :fill=\"p.color\"\n :text-anchor=\"p.align === 'c' ? 'middle' : p.align === 'r' ? 'end' : 'start'\"\n >\n <tspan\n v-for=\"(ln, li) in p.text.split('\\n')\"\n :key=\"li\"\n x=\"0\"\n :dy=\"li === 0 ? 0 : p.h * 1.25\"\n >\n {{ ln }}\n </tspan>\n </text>\n </g>\n </template>\n </g>\n </g>\n </svg>\n </div>\n\n <ul v-if=\"parsed.layers.length\" class=\"dxf__layers\">\n <li v-for=\"l in parsed.layers\" :key=\"l.name\">\n <button\n type=\"button\"\n class=\"dxf__layer\"\n :class=\"{ 'dxf__layer--off': hiddenLayers.has(l.name) }\"\n @click=\"toggleLayer(l.name)\"\n >\n <span class=\"dxf__swatch\" :style=\"{ background: l.color }\" />\n <span class=\"dxf__lname\">{{ l.name }}</span>\n <span class=\"dxf__eye\">{{ hiddenLayers.has(l.name) ? '隐藏' : '显示' }}</span>\n </button>\n </li>\n </ul>\n </template>\n </div>\n</template>\n\n<style scoped>\n.dxf {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n.dxf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dxf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dxf__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dxf__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dxf__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--muted);\n}\n.dxf__badge {\n padding: 2px 9px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--accent);\n font-weight: 600;\n}\n.dxf__dim {\n color: var(--faint);\n}\n.dxf__spacer {\n flex: 1 1 auto;\n}\n.dxf__reset {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.dxf__reset:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dxf__stage {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n background: var(--surface-2);\n}\n.dxf__svg {\n width: 100%;\n height: 100%;\n touch-action: none;\n cursor: grab;\n}\n.dxf__svg:active {\n cursor: grabbing;\n}\n.dxf__text {\n font-family: var(--font-sans, sans-serif);\n dominant-baseline: alphabetic;\n}\n\n.dxf__layers {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n flex-shrink: 0;\n max-height: 30%;\n overflow: auto;\n overscroll-behavior: contain;\n border-top: 1px solid var(--border);\n}\n.dxf__layer {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 6px 16px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n color: var(--text);\n cursor: pointer;\n}\n.dxf__layer:hover {\n background: var(--surface-2);\n}\n.dxf__layer--off {\n opacity: 0.45;\n}\n.dxf__swatch {\n width: 12px;\n height: 12px;\n flex-shrink: 0;\n border-radius: 3px;\n border: 1px solid var(--border-2);\n}\n.dxf__lname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n}\n.dxf__eye {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EbookViewer.js","names":[],"sources":["../../src/components/EbookViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { Book, Rendition } from 'epubjs'\nimport { parseFb2 } from '@/lib/fb2'\nimport { parseMobi } from '@/lib/mobi'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport Icon from '@/components/icons/Icon.vue'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer e-book reader. By kind:\n * - epub: rendered with epub.js (dynamically imported so its weight only ships for EPUBs) — paginated, with\n * prev/next and a chapter (TOC) jump. Scripts in the book are DISABLED (`allowScriptedContent: false`).\n * - fb2: parsed in-browser (see lib/fb2.ts) into a sandboxed HTML document.\n * - mobi/azw3: BEST EFFORT — extracted in-browser (see lib/mobi.ts: PalmDOC text records) into a sandboxed\n * HTML document; HUFF/CDIC or DRM'd books surface a clear \"download to read\" message.\n * - djvu: not previewable in-browser (would need a heavy WASM decoder) — shows a download hint.\n *\n * All book HTML (fb2/mobi) renders in a fully sandboxed, script-disabled iframe (CSP injected in the doc),\n * same hardening as the other HTML previews. The epub host stays mounted across loading so epub.js can render\n * into it; status messages sit on top as an overlay.\n */\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n kind: 'epub' | 'fb2' | 'mobi' | 'djvu'\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when the EPUB engine (epubjs, an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst host = ref<HTMLElement | null>(null)\nconst srcdoc = ref('') // fb2 / mobi sandboxed-iframe content\nconst toc = ref<{ label: string; href: string }[]>([])\nconst tocHref = ref('')\n\nconst SIZE_LIMIT = 96 * 1024 * 1024\n\nlet book: Book | null = null\nlet rendition: Rendition | null = null\n\nconst CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst READ_STYLE =\n `body{margin:0;padding:24px 28px 80px;max-width:46rem;margin-inline:auto;` +\n `font:16px/1.7 Georgia,'Source Serif 4',serif;color:#1a1a1a;background:#fff}` +\n `img{max-width:100%;height:auto}p{margin:0 0 .9em}`\n\nfunction wrapHtmlDoc(fragmentOrDoc: string): string {\n const m = fragmentOrDoc.match(/<body[^>]*>([\\s\\S]*)<\\/body>/i)\n const body = m ? m[1] : fragmentOrDoc\n return (\n `<!doctype html><html><head><meta charset=\"utf-8\">${CSP}` +\n `<style>${READ_STYLE}</style></head><body>${body}</body></html>`\n )\n}\n\n// Decode an XML text file honoring its declared `encoding=` (FB2 is often UTF-8, sometimes windows-1251).\nfunction decodeXml(buf: ArrayBuffer): string {\n const head = new TextDecoder('latin1').decode(\n new Uint8Array(buf, 0, Math.min(200, buf.byteLength)),\n )\n const m = head.match(/encoding=[\"']([\\w-]+)[\"']/i)\n const enc = (m ? m[1] : 'utf-8').toLowerCase()\n try {\n return new TextDecoder(enc).decode(buf)\n } catch {\n return new TextDecoder('utf-8').decode(buf)\n }\n}\n\nfunction destroyEpub(): void {\n try {\n rendition?.destroy()\n book?.destroy()\n } catch {\n /* already torn down — ignore */\n }\n rendition = null\n book = null\n toc.value = []\n tocHref.value = ''\n}\n\nasync function initEpub(buf: ArrayBuffer): Promise<void> {\n destroyEpub()\n if (!host.value) throw new Error('容器未就绪')\n const ePub = (await import('epubjs')).default\n book = ePub(buf)\n rendition = book.renderTo(host.value, {\n width: '100%',\n height: '100%',\n flow: 'paginated',\n spread: 'none',\n allowScriptedContent: false,\n })\n await rendition.display()\n try {\n const nav = await book.loaded.navigation\n type Nav = { label: string; href: string; subitems?: Nav[] }\n const flat: { label: string; href: string }[] = []\n const walk = (items: Nav[], depth: number): void => {\n for (const it of items) {\n flat.push({ label: `${' '.repeat(depth)}${(it.label || '').trim()}`, href: it.href })\n if (it.subitems?.length) walk(it.subitems, depth + 1)\n }\n }\n walk((nav.toc as unknown as Nav[]) ?? [], 0)\n toc.value = flat\n } catch {\n /* no navigation — leave the TOC empty */\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (props.kind === 'djvu') {\n status.value = 'ready' // nothing to fetch; the template shows the unsupported notice\n return\n }\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n srcdoc.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n if (props.kind === 'epub') {\n await initEpub(buf)\n } else if (props.kind === 'fb2') {\n srcdoc.value = parseFb2(decodeXml(buf)).html\n } else {\n srcdoc.value = wrapHtmlDoc(parseMobi(buf).html)\n }\n status.value = 'ready'\n } catch (e) {\n if (props.kind === 'epub' && isMissingModule(e)) missingDeps.value = ['epubjs']\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction prev(): void {\n void rendition?.prev()\n}\nfunction next(): void {\n void rendition?.next()\n}\nfunction jumpToc(): void {\n if (tocHref.value) void rendition?.display(tocHref.value)\n}\n\nonMounted(() => {\n watch(() => [props.url, props.kind], load, { immediate: true })\n})\nonBeforeUnmount(destroyEpub)\n</script>\n\n<template>\n <div class=\"ebk\">\n <!-- epub host stays mounted across loading so epub.js can render into it; nav appears when ready -->\n <template v-if=\"props.kind === 'epub'\">\n <div ref=\"host\" class=\"ebk__host\" />\n <div v-if=\"status === 'ready'\" class=\"ebk__nav\">\n <button type=\"button\" class=\"ebk__navbtn\" title=\"上一页\" @click=\"prev\">\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n <select v-if=\"toc.length\" v-model=\"tocHref\" class=\"ebk__toc\" @change=\"jumpToc\">\n <option value=\"\">目录…</option>\n <option v-for=\"(it, i) in toc\" :key=\"i\" :value=\"it.href\">{{ it.label }}</option>\n </select>\n <button type=\"button\" class=\"ebk__navbtn\" title=\"下一页\" @click=\"next\">\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n </div>\n </template>\n\n <!-- fb2 / mobi: the book HTML in a sandboxed, script-disabled iframe -->\n <iframe\n v-else-if=\"(props.kind === 'fb2' || props.kind === 'mobi') && status === 'ready'\"\n class=\"ebk__frame\"\n :srcdoc=\"srcdoc\"\n title=\"电子书预览\"\n sandbox=\"\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- djvu: not previewable in-browser -->\n <div v-else-if=\"props.kind === 'djvu' && status === 'ready'\" class=\"ebk__msg ebk__unsupported\">\n <p>DjVu 格式暂不支持浏览器内预览。</p>\n <p class=\"ebk__hint\">请下载后用本地 DjVu 阅读器打开。</p>\n </div>\n\n <!-- status overlays (sit on top of the epub host; the sole content for the other kinds while loading) -->\n <div v-if=\"status === 'loading'\" class=\"ebk__overlay\">\n <p class=\"ebk__msg\">正在加载电子书…</p>\n </div>\n <div v-else-if=\"status === 'gate'\" class=\"ebk__overlay\">\n <div class=\"ebk__msg ebk__gate\">\n <p>该电子书较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"ebk__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n </div>\n <div v-else-if=\"missingDeps\" class=\"ebk__overlay\">\n <MissingDependency\n :pkg=\"missingDeps\"\n label=\"EPUB\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"ebk__overlay\">\n <div class=\"ebk__msg ebk__msg--error\">\n <p>无法预览:{{ errorMsg }}</p>\n <button type=\"button\" class=\"ebk__btn\" @click=\"doLoad\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ebk {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: #fff;\n}\n\n.ebk__overlay {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n background: var(--surface);\n}\n.ebk__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ebk__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ebk__gate,\n.ebk__unsupported {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.ebk__hint {\n color: var(--faint);\n font-size: 12px;\n}\n.ebk__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.ebk__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.ebk__unsupported {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.ebk__frame {\n flex: 1 1 auto;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* epub: a white reading stage with epub.js mounted into the host + a bottom nav bar. */\n.ebk__host {\n flex: 1 1 auto;\n min-height: 0;\n}\n.ebk__nav {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n background: var(--surface);\n}\n/* Page-flip buttons match the file-viewer header's icon buttons (.fv__btn): 30px box, 15px outline Icon. */\n.ebk__navbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 30px;\n padding: 0;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.ebk__navbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.ebk__toc {\n flex: 1 1 auto;\n min-width: 0;\n max-width: 100%;\n height: 30px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EbookViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/EbookViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { Book, Rendition } from 'epubjs'\nimport { parseFb2 } from '@/lib/fb2'\nimport { parseMobi } from '@/lib/mobi'\nimport { formatBytes } from '@/lib/format'\nimport { isMissingModule } from '@/lib/optionalDep'\nimport Icon from '@/components/icons/Icon.vue'\nimport MissingDependency from '@/components/MissingDependency.vue'\n\n/**\n * Side-viewer e-book reader. By kind:\n * - epub: rendered with epub.js (dynamically imported so its weight only ships for EPUBs) — paginated, with\n * prev/next and a chapter (TOC) jump. Scripts in the book are DISABLED (`allowScriptedContent: false`).\n * - fb2: parsed in-browser (see lib/fb2.ts) into a sandboxed HTML document.\n * - mobi/azw3: BEST EFFORT — extracted in-browser (see lib/mobi.ts: PalmDOC text records) into a sandboxed\n * HTML document; HUFF/CDIC or DRM'd books surface a clear \"download to read\" message.\n * - djvu: not previewable in-browser (would need a heavy WASM decoder) — shows a download hint.\n *\n * All book HTML (fb2/mobi) renders in a fully sandboxed, script-disabled iframe (CSP injected in the doc),\n * same hardening as the other HTML previews. The epub host stays mounted across loading so epub.js can render\n * into it; status messages sit on top as an overlay.\n */\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n kind: 'epub' | 'fb2' | 'mobi' | 'djvu'\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n// Set when the EPUB engine (epubjs, an optional peer) isn't installed → show the install card, not a raw error.\nconst missingDeps = ref<string[] | null>(null)\nconst host = ref<HTMLElement | null>(null)\nconst srcdoc = ref('') // fb2 / mobi sandboxed-iframe content\nconst toc = ref<{ label: string; href: string }[]>([])\nconst tocHref = ref('')\n\nconst SIZE_LIMIT = 96 * 1024 * 1024\n\nlet book: Book | null = null\nlet rendition: Rendition | null = null\n\nconst CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst READ_STYLE =\n `body{margin:0;padding:24px 28px 80px;max-width:46rem;margin-inline:auto;` +\n `font:16px/1.7 Georgia,'Source Serif 4',serif;color:#1a1a1a;background:#fff}` +\n `img{max-width:100%;height:auto}p{margin:0 0 .9em}`\n\nfunction wrapHtmlDoc(fragmentOrDoc: string): string {\n const m = fragmentOrDoc.match(/<body[^>]*>([\\s\\S]*)<\\/body>/i)\n const body = m ? m[1] : fragmentOrDoc\n return (\n `<!doctype html><html><head><meta charset=\"utf-8\">${CSP}` +\n `<style>${READ_STYLE}</style></head><body>${body}</body></html>`\n )\n}\n\n// Decode an XML text file honoring its declared `encoding=` (FB2 is often UTF-8, sometimes windows-1251).\nfunction decodeXml(buf: ArrayBuffer): string {\n const head = new TextDecoder('latin1').decode(\n new Uint8Array(buf, 0, Math.min(200, buf.byteLength)),\n )\n const m = head.match(/encoding=[\"']([\\w-]+)[\"']/i)\n const enc = (m ? m[1] : 'utf-8').toLowerCase()\n try {\n return new TextDecoder(enc).decode(buf)\n } catch {\n return new TextDecoder('utf-8').decode(buf)\n }\n}\n\nfunction destroyEpub(): void {\n try {\n rendition?.destroy()\n book?.destroy()\n } catch {\n /* already torn down — ignore */\n }\n rendition = null\n book = null\n toc.value = []\n tocHref.value = ''\n}\n\nasync function initEpub(buf: ArrayBuffer): Promise<void> {\n destroyEpub()\n if (!host.value) throw new Error('容器未就绪')\n const ePub = (await import('epubjs')).default\n book = ePub(buf)\n rendition = book.renderTo(host.value, {\n width: '100%',\n height: '100%',\n flow: 'paginated',\n spread: 'none',\n allowScriptedContent: false,\n })\n await rendition.display()\n try {\n const nav = await book.loaded.navigation\n type Nav = { label: string; href: string; subitems?: Nav[] }\n const flat: { label: string; href: string }[] = []\n const walk = (items: Nav[], depth: number): void => {\n for (const it of items) {\n flat.push({ label: `${' '.repeat(depth)}${(it.label || '').trim()}`, href: it.href })\n if (it.subitems?.length) walk(it.subitems, depth + 1)\n }\n }\n walk((nav.toc as unknown as Nav[]) ?? [], 0)\n toc.value = flat\n } catch {\n /* no navigation — leave the TOC empty */\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (props.kind === 'djvu') {\n status.value = 'ready' // nothing to fetch; the template shows the unsupported notice\n return\n }\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n missingDeps.value = null\n srcdoc.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n if (props.kind === 'epub') {\n await initEpub(buf)\n } else if (props.kind === 'fb2') {\n srcdoc.value = parseFb2(decodeXml(buf)).html\n } else {\n srcdoc.value = wrapHtmlDoc(parseMobi(buf).html)\n }\n status.value = 'ready'\n } catch (e) {\n if (props.kind === 'epub' && isMissingModule(e)) missingDeps.value = ['epubjs']\n else errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction prev(): void {\n void rendition?.prev()\n}\nfunction next(): void {\n void rendition?.next()\n}\nfunction jumpToc(): void {\n if (tocHref.value) void rendition?.display(tocHref.value)\n}\n\nonMounted(() => {\n watch(() => [props.url, props.kind], load, { immediate: true })\n})\nonBeforeUnmount(destroyEpub)\n</script>\n\n<template>\n <div class=\"ebk\">\n <!-- epub host stays mounted across loading so epub.js can render into it; nav appears when ready -->\n <template v-if=\"props.kind === 'epub'\">\n <div ref=\"host\" class=\"ebk__host\" />\n <div v-if=\"status === 'ready'\" class=\"ebk__nav\">\n <button type=\"button\" class=\"ebk__navbtn\" title=\"上一页\" @click=\"prev\">\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n <select v-if=\"toc.length\" v-model=\"tocHref\" class=\"ebk__toc\" @change=\"jumpToc\">\n <option value=\"\">目录…</option>\n <option v-for=\"(it, i) in toc\" :key=\"i\" :value=\"it.href\">{{ it.label }}</option>\n </select>\n <button type=\"button\" class=\"ebk__navbtn\" title=\"下一页\" @click=\"next\">\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n </div>\n </template>\n\n <!-- fb2 / mobi: the book HTML in a sandboxed, script-disabled iframe -->\n <iframe\n v-else-if=\"(props.kind === 'fb2' || props.kind === 'mobi') && status === 'ready'\"\n class=\"ebk__frame\"\n :srcdoc=\"srcdoc\"\n title=\"电子书预览\"\n sandbox=\"\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- djvu: not previewable in-browser -->\n <div v-else-if=\"props.kind === 'djvu' && status === 'ready'\" class=\"ebk__msg ebk__unsupported\">\n <p>DjVu 格式暂不支持浏览器内预览。</p>\n <p class=\"ebk__hint\">请下载后用本地 DjVu 阅读器打开。</p>\n </div>\n\n <!-- status overlays (sit on top of the epub host; the sole content for the other kinds while loading) -->\n <div v-if=\"status === 'loading'\" class=\"ebk__overlay\">\n <p class=\"ebk__msg\">正在加载电子书…</p>\n </div>\n <div v-else-if=\"status === 'gate'\" class=\"ebk__overlay\">\n <div class=\"ebk__msg ebk__gate\">\n <p>该电子书较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"ebk__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n </div>\n <div v-else-if=\"missingDeps\" class=\"ebk__overlay\">\n <MissingDependency\n :pkg=\"missingDeps\"\n label=\"EPUB\"\n :download-url=\"props.url\"\n :download-name=\"props.name\"\n />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"ebk__overlay\">\n <div class=\"ebk__msg ebk__msg--error\">\n <p>无法预览:{{ errorMsg }}</p>\n <button type=\"button\" class=\"ebk__btn\" @click=\"doLoad\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ebk {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: #fff;\n}\n\n.ebk__overlay {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n background: var(--surface);\n}\n.ebk__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ebk__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ebk__gate,\n.ebk__unsupported {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.ebk__hint {\n color: var(--faint);\n font-size: 12px;\n}\n.ebk__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.ebk__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.ebk__unsupported {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.ebk__frame {\n flex: 1 1 auto;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* epub: a white reading stage with epub.js mounted into the host + a bottom nav bar. */\n.ebk__host {\n flex: 1 1 auto;\n min-height: 0;\n}\n.ebk__nav {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n background: var(--surface);\n}\n/* Page-flip buttons match the file-viewer header's icon buttons (.fv__btn): 30px box, 15px outline Icon. */\n.ebk__navbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 30px;\n padding: 0;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.ebk__navbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.ebk__toc {\n flex: 1 1 auto;\n min-width: 0;\n max-width: 100%;\n height: 30px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;8CAwCM,IAAa,KAAK,OAAO,MAKzB,IACJ,uIAEI,IACJ;;;;;;;;;EA1BF,IAAM,IAAQ,GAQR,IAAS,EAAY,SAAS,GAC9B,IAAW,EAAI,EAAE,GAEjB,IAAc,EAAqB,IAAI,GACvC,IAAO,EAAwB,IAAI,GACnC,IAAS,EAAI,EAAE,GACf,IAAM,EAAuC,CAAC,CAAC,GAC/C,IAAU,EAAI,EAAE,GAIlB,IAAoB,MACpB,IAA8B;EAUlC,SAAS,EAAY,GAA+B;GAClD,IAAM,IAAI,EAAc,MAAM,+BAA+B;GAE7D,OACE,oDAAoD,EAAA,SAC1C,EAAW,uBAHV,IAAI,EAAE,KAAK,EAG2B;EAErD;EAGA,SAAS,EAAU,GAA0B;GAI3C,IAAM,IAHO,IAAI,YAAY,QAAQ,CAAC,CAAC,OACrC,IAAI,WAAW,GAAK,GAAG,KAAK,IAAI,KAAK,EAAI,UAAU,CAAC,CAE5C,CAAA,CAAK,MAAM,4BAA4B,GAC3C,KAAO,IAAI,EAAE,KAAK,QAAA,CAAS,YAAY;GAC7C,IAAI;IACF,OAAO,IAAI,YAAY,CAAG,CAAC,CAAC,OAAO,CAAG;GACxC,QAAQ;IACN,OAAO,IAAI,YAAY,OAAO,CAAC,CAAC,OAAO,CAAG;GAC5C;EACF;EAEA,SAAS,IAAoB;GAC3B,IAAI;IAEF,AADA,GAAW,QAAQ,GACnB,GAAM,QAAQ;GAChB,QAAQ,CAER;GAIA,AAHA,IAAY,MACZ,IAAO,MACP,EAAI,QAAQ,CAAC,GACb,EAAQ,QAAQ;EAClB;EAEA,eAAe,EAAS,GAAiC;GAEvD,IADA,EAAY,GACR,CAAC,EAAK,OAAO,MAAU,MAAM,OAAO;GACxC,IAAM,KAAQ,MAAM,OAAO,UAAA,CAAW;GAStC,AARA,IAAO,EAAK,CAAG,GACf,IAAY,EAAK,SAAS,EAAK,OAAO;IACpC,OAAO;IACP,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,sBAAsB;GACxB,CAAC,GACD,MAAM,EAAU,QAAQ;GACxB,IAAI;IACF,IAAM,IAAM,MAAM,EAAK,OAAO,YAExB,IAA0C,CAAC,GAC3C,KAAQ,GAAc,MAAwB;KAClD,KAAK,IAAM,KAAM,GAEf,AADA,EAAK,KAAK;MAAE,OAAO,GAAG,IAAI,OAAO,CAAK,KAAK,EAAG,SAAS,GAAA,CAAI,KAAK;MAAK,MAAM,EAAG;KAAK,CAAC,GAChF,EAAG,UAAU,UAAQ,EAAK,EAAG,UAAU,IAAQ,CAAC;IAExD;IAEA,AADA,EAAM,EAAI,OAA4B,CAAC,GAAG,CAAC,GAC3C,EAAI,QAAQ;GACd,QAAQ,CAER;EACF;EAEA,eAAe,IAAwB;GACrC,IAAI,EAAM,SAAS,QAAQ;IACzB,EAAO,QAAQ;IACf;GACF;GACK,MAAM,KAIX;IAHA,EAAO,QAAQ,WACf,EAAS,QAAQ,IACjB,EAAY,QAAQ,MACpB,EAAO,QAAQ;IACf,IAAI;KACF,IAAM,IAAM,MAAM,MAAM,EAAM,GAAG;KACjC,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,QAAQ,EAAI,QAAQ;KACjD,IAAM,IAAM,MAAM,EAAI,YAAY;KAQlC,AAPI,EAAM,SAAS,SACjB,MAAM,EAAS,CAAG,IACT,EAAM,SAAS,QACxB,EAAO,QAAQ,EAAS,EAAU,CAAG,CAAC,CAAC,CAAC,OAExC,EAAO,QAAQ,EAAY,EAAU,CAAG,CAAC,CAAC,IAAI,GAEhD,EAAO,QAAQ;IACjB,SAAS,GAAG;KAGV,AAFI,EAAM,SAAS,UAAU,EAAgB,CAAC,IAAG,EAAY,QAAQ,CAAC,QAAQ,IACzE,EAAS,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC/D,EAAO,QAAQ;IACjB;GAjBe;EAkBjB;EAEA,SAAS,IAAa;GACpB,IAAI,EAAM,QAAQ,EAAM,OAAO,GAAY;IACzC,EAAO,QAAQ;IACf;GACF;GACA,EAAY;EACd;EAEA,SAAS,IAAa;GACpB,GAAgB,KAAK;EACvB;EACA,SAAS,IAAa;GACpB,GAAgB,KAAK;EACvB;EACA,SAAS,KAAgB;GACvB,AAAI,EAAQ,SAAO,GAAgB,QAAQ,EAAQ,KAAK;EAC1D;SAEA,QAAgB;GACd,QAAY,CAAC,EAAM,KAAK,EAAM,IAAI,GAAG,GAAM,EAAE,WAAW,GAAK,CAAC;EAChE,CAAC,GACD,EAAgB,CAAW,mBAIzB,EA0DM,OA1DN,GA0DM,CAxDY,EAAM,SAAI,UAAA,EAAA,GAA1B,EAcW,GAAA,EAAA,KAAA,EAAA,GAAA,CAbT,EAAoC,OAAA;YAA3B;GAAJ,KAAI;GAAO,OAAM;iBACX,EAAA,UAAM,WAAA,EAAA,GAAjB,EAWM,OAXN,GAWM;GAVJ,EAES,UAAA;IAFD,MAAK;IAAS,OAAM;IAAc,OAAM;IAAO,SAAO;OAC5D,EAAsC,GAAA;IAAhC,MAAK;IAAe,MAAM;;GAEpB,EAAA,MAAI,SAAA,GAAA,EAAA,GAAlB,EAGS,UAAA;;6CAHiC,QAAA;IAAE,OAAM;IAAY,UAAQ;gBACpE,EAA6B,UAAA,EAArB,OAAM,GAAE,GAAC,OAAG,EAAA,IAAA,EAAA,EAAA,GACpB,EAAgF,GAAA,MAAA,EAAtD,EAAA,QAAV,GAAI,YAApB,EAAgF,UAAA;IAAhD,KAAK;IAAI,OAAO,EAAG;QAAS,EAAG,KAAK,GAAA,GAAA,CAAA,yBAFnC,EAAA,KAAO,CAAA,CAAA,IAAA,EAAA,IAAA,EAAA;GAI1C,EAES,UAAA;IAFD,MAAK;IAAS,OAAM;IAAc,OAAM;IAAO,SAAO;OAC5D,EAAuC,GAAA;IAAjC,MAAK;IAAgB,MAAM;;4BAOzB,EAAM,SAAI,SAAc,EAAM,SAAI,WAAgB,EAAA,UAAM,WAAA,EAAA,GADtE,EAOE,UAAA;;GALA,OAAM;GACL,QAAQ,EAAA;GACT,OAAM;GACN,SAAQ;GACR,gBAAe;oBAID,EAAM,SAAI,UAAe,EAAA,UAAM,WAAA,EAAA,GAA/C,EAGM,OAHN,GAGM,CAAA,GAAA,AAAA,EAAA,OAAA,CAFJ,EAAyB,KAAA,MAAtB,sBAAkB,EAAA,GACrB,EAA4C,KAAA,EAAzC,OAAM,YAAW,GAAC,uBAAmB,EAAA,CAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAI/B,EAAA,UAAM,aAAA,EAAA,GAAjB,EAEM,OAFN,GAEM,CAAA,GAAA,AAAA,EAAA,OAAA,CADJ,EAAgC,KAAA,EAA7B,OAAM,WAAU,GAAC,YAAQ,EAAA,CAAA,CAAA,CAAA,KAEd,EAAA,UAAM,UAAA,EAAA,GAAtB,EAKM,OALN,GAKM,CAJJ,EAGM,OAHN,GAGM,CAFJ,EAA2D,KAAA,MAAxD,YAAO,EAAG,EAAA,CAAA,CAAW,CAAC,EAAM,IAAI,CAAA,IAAI,oBAAgB,CAAA,GACvD,EAAqE,UAAA;GAA7D,MAAK;GAAS,OAAM;GAAY,SAAO;KAAQ,OAAK,CAAA,CAAA,CAAA,CAAA,KAGhD,EAAA,SAAA,EAAA,GAAhB,EAOM,OAPN,GAOM,CANJ,EAKE,GAAA;GAJC,KAAK,EAAA;GACN,OAAM;GACL,gBAAc,EAAM;GACpB,iBAAe,EAAM;;;;;UAGV,EAAA,UAAM,WAAA,EAAA,GAAtB,EAKM,OALN,GAKM,CAJJ,EAGM,OAHN,GAGM,CAFJ,EAA0B,KAAA,MAAvB,UAAK,EAAG,EAAA,KAAQ,GAAA,CAAA,GACnB,EAAkE,UAAA;GAA1D,MAAK;GAAS,OAAM;GAAY,SAAO;KAAQ,IAAE,CAAA,CAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmlViewer.js","names":[],"sources":["../../src/components/EmlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseEml, type ParsedEmail } from '@/lib/eml'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer email (`.eml`) renderer. Fetches the message bytes from the download URL and parses the MIME\n * tree entirely in-browser (see lib/eml.ts) — header card, the best body (HTML preferred, plain fallback),\n * and the attachments. Mirrors the other byte-fetching viewers (DICOM/DOCX/PDF): a real download URL in,\n * a self-owned fetch + render.\n *\n * Safety: an HTML body is hostile markup, so it renders the SAME way the panel's HTML preview does — in a\n * fully sandboxed iframe (opaque origin, no `allow-scripts`) whose markup carries an injected\n * `script-src 'none'` CSP, so NO upstream script runs. Inline `cid:` images are resolved to data URLs\n * (which load fine in the opaque-origin frame, unlike parent-origin blob URLs). A plain-text body is HTML-\n * escaped before display, so it can never inject markup.\n */\nconst props = defineProps<{\n /** Download URL of the .eml bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst mail = ref<ParsedEmail | null>(null)\nconst bodyMode = ref<'html' | 'text'>('html')\n\n// Emails are usually small, but attachments can balloon the file; gate very large ones behind a confirm\n// (all decoding is in-memory). Mirrors the binary browser's large-file guard.\nconst SIZE_LIMIT = 12 * 1024 * 1024\n\ninterface AttView {\n filename: string\n contentType: string\n size: number\n url: string\n}\nconst attachments = ref<AttView[]>([])\nconst htmlUrl = ref('')\nlet objectUrls: string[] = []\n\nfunction revokeAll(): void {\n for (const u of objectUrls) URL.revokeObjectURL(u)\n objectUrls = []\n htmlUrl.value = ''\n attachments.value = []\n}\n\n// Same hardening as FileViewerPanel's HTML preview: kill all script/plugin/base/form vectors. Multiple CSPs\n// intersect, so a document's own CSP can't loosen this.\nconst NO_SCRIPT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\n\nfunction injectNoScriptCsp(html: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + NO_SCRIPT_CSP + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${NO_SCRIPT_CSP}</head><body>${html}</body></html>`\n}\n\n/** Base64-encode bytes (chunked) for an inline image's data URL. */\nfunction bytesToBase64(bytes: Uint8Array): string {\n let bin = ''\n const CHUNK = 0x8000\n for (let i = 0; i < bytes.length; i += CHUNK) {\n bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK))\n }\n return btoa(bin)\n}\n\n/** Rewrite `cid:` references in the HTML body to data URLs from the inline-image map. */\nfunction resolveCids(html: string, parsed: ParsedEmail): string {\n if (!parsed.inlineById.size) return html\n return html.replace(/cid:([^\"')\\s>]+)/gi, (whole, id: string) => {\n const att = parsed.inlineById.get(id) ?? parsed.inlineById.get(decodeURIComponent(id))\n if (!att) return whole\n return `data:${att.contentType || 'application/octet-stream'};base64,${bytesToBase64(att.bytes)}`\n })\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n// Plain-text body → escaped HTML with bare URLs linkified (opens in a new tab; escaped first, so safe).\nconst textHtml = computed(() => {\n const t = mail.value?.textBody\n if (!t) return ''\n return escapeHtml(t).replace(\n /(https?:\\/\\/[^\\s<]+)/g,\n (u) => `<a href=\"${u}\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">${u}</a>`,\n )\n})\n\nconst hasHtml = computed(() => !!mail.value?.htmlBody && mail.value.htmlBody.trim().length > 0)\nconst hasText = computed(() => !!mail.value?.textBody && mail.value.textBody.trim().length > 0)\nconst hasBody = computed(() => hasHtml.value || hasText.value)\nconst showHtml = computed(() => bodyMode.value === 'html' && hasHtml.value)\n\n// Human-friendly Date header (falls back to the raw value when it doesn't parse).\nconst dateDisplay = computed(() => {\n const d = mail.value?.date\n if (!d) return ''\n const t = Date.parse(d)\n if (Number.isNaN(t)) return d\n try {\n return new Date(t).toLocaleString('zh-CN', { dateStyle: 'medium', timeStyle: 'short' })\n } catch {\n return d\n }\n})\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n revokeAll()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const parsed = parseEml(new Uint8Array(buf))\n\n // Visible attachments exclude inline images (those render inside the HTML body).\n for (const att of parsed.attachments) {\n if (att.isInline) continue\n const blob = new Blob([att.bytes], {\n type: att.contentType || 'application/octet-stream',\n })\n const url = URL.createObjectURL(blob)\n objectUrls.push(url)\n attachments.value.push({\n filename: att.filename,\n contentType: att.contentType,\n size: att.size,\n url,\n })\n }\n if (parsed.htmlBody !== null) {\n const safe = injectNoScriptCsp(resolveCids(parsed.htmlBody, parsed))\n const url = URL.createObjectURL(new Blob([safe], { type: 'text/html' }))\n objectUrls.push(url)\n htmlUrl.value = url\n }\n\n mail.value = parsed\n bodyMode.value = parsed.htmlBody && parsed.htmlBody.trim() ? 'html' : 'text'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n // Gate oversized files behind a confirm; otherwise fetch + parse straight away.\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(revokeAll)\n</script>\n\n<template>\n <div class=\"eml\">\n <p v-if=\"status === 'loading'\" class=\"eml__msg\">正在解析邮件…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"eml__msg eml__gate\">\n <p>该邮件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"eml__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"eml__msg eml__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"eml__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && mail\">\n <!-- header card: subject + addressing + date -->\n <div class=\"eml__head\">\n <div class=\"eml__subject\">{{ mail.subject || '(无主题)' }}</div>\n <dl class=\"eml__fields\">\n <div v-if=\"mail.from\" class=\"eml__field\">\n <dt>发件人</dt>\n <dd>{{ mail.from }}</dd>\n </div>\n <div v-if=\"mail.to\" class=\"eml__field\">\n <dt>收件人</dt>\n <dd>{{ mail.to }}</dd>\n </div>\n <div v-if=\"mail.cc\" class=\"eml__field\">\n <dt>抄送</dt>\n <dd>{{ mail.cc }}</dd>\n </div>\n <div v-if=\"dateDisplay\" class=\"eml__field\">\n <dt>日期</dt>\n <dd>{{ dateDisplay }}</dd>\n </div>\n </dl>\n\n <!-- attachments strip -->\n <div v-if=\"attachments.length\" class=\"eml__atts\">\n <span class=\"eml__atts-label\">\n <Icon name=\"fileOutline\" :size=\"13\" />附件 {{ attachments.length }}\n </span>\n <a\n v-for=\"(att, i) in attachments\"\n :key=\"i\"\n class=\"eml__att\"\n :href=\"att.url\"\n :download=\"att.filename\"\n :title=\"`下载 ${att.filename}`\"\n >\n <span class=\"eml__att-name\">{{ att.filename }}</span>\n <span v-if=\"att.size\" class=\"eml__att-size\">{{ formatBytes(att.size) }}</span>\n <Icon name=\"download\" :size=\"13\" />\n </a>\n </div>\n\n <!-- body mode toggle (only when both alternatives exist) -->\n <div v-if=\"hasHtml && hasText\" class=\"eml__toggle\" role=\"group\" aria-label=\"正文格式\">\n <button\n type=\"button\"\n class=\"eml__tab\"\n :class=\"{ 'eml__tab--on': bodyMode === 'html' }\"\n @click=\"bodyMode = 'html'\"\n >\n HTML\n </button>\n <button\n type=\"button\"\n class=\"eml__tab\"\n :class=\"{ 'eml__tab--on': bodyMode === 'text' }\"\n @click=\"bodyMode = 'text'\"\n >\n 纯文本\n </button>\n </div>\n </div>\n\n <!-- body: HTML in a sandboxed iframe, or escaped plain text; falls through to an empty notice -->\n <div v-if=\"showHtml\" class=\"eml__html\">\n <iframe\n class=\"eml__frame\"\n :src=\"htmlUrl\"\n title=\"邮件正文\"\n sandbox=\"\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n <!-- eslint-disable-next-line vue/no-v-html — textHtml is escaped, then only URLs are linkified -->\n <div v-else-if=\"hasText\" class=\"eml__text\" v-html=\"textHtml\" />\n <p v-else class=\"eml__msg\">(此邮件没有可显示的正文)</p>\n </template>\n </div>\n</template>\n\n<style scoped>\n.eml {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.eml__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.eml__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.eml__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.eml__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.eml__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Header card: subject + fields + attachments, fixed above the scrolling body. */\n.eml__head {\n flex-shrink: 0;\n padding: 14px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.eml__subject {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--text-strong);\n word-break: break-word;\n}\n.eml__fields {\n margin: 10px 0 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n.eml__field {\n display: flex;\n gap: 8px;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.eml__field dt {\n flex-shrink: 0;\n width: 48px;\n color: var(--faint);\n}\n.eml__field dd {\n margin: 0;\n min-width: 0;\n color: var(--text);\n word-break: break-word;\n}\n\n.eml__atts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n padding-top: 12px;\n border-top: 1px dashed var(--border);\n}\n.eml__atts-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n color: var(--faint);\n}\n.eml__att {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 5px 9px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 8px;\n color: var(--text);\n text-decoration: none;\n font-size: 12px;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.eml__att:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.eml__att-name {\n min-width: 0;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eml__att-size {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.eml__toggle {\n display: inline-flex;\n gap: 2px;\n margin-top: 12px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.eml__tab {\n padding: 4px 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n}\n.eml__tab--on {\n color: var(--accent);\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n\n/* HTML body: the iframe fills the remaining height on a white canvas (like the HTML file preview). */\n.eml__html {\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.eml__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* Plain-text body: monospace, wrapped, scrolls within the pane. */\n.eml__text {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n margin: 0;\n padding: 16px 18px 60px;\n font-family: var(--font-mono);\n font-size: 13px;\n line-height: 1.6;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--code-bg);\n}\n.eml__text :deep(a) {\n color: var(--accent);\n text-decoration: underline;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmlViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/EmlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseEml, type ParsedEmail } from '@/lib/eml'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer email (`.eml`) renderer. Fetches the message bytes from the download URL and parses the MIME\n * tree entirely in-browser (see lib/eml.ts) — header card, the best body (HTML preferred, plain fallback),\n * and the attachments. Mirrors the other byte-fetching viewers (DICOM/DOCX/PDF): a real download URL in,\n * a self-owned fetch + render.\n *\n * Safety: an HTML body is hostile markup, so it renders the SAME way the panel's HTML preview does — in a\n * fully sandboxed iframe (opaque origin, no `allow-scripts`) whose markup carries an injected\n * `script-src 'none'` CSP, so NO upstream script runs. Inline `cid:` images are resolved to data URLs\n * (which load fine in the opaque-origin frame, unlike parent-origin blob URLs). A plain-text body is HTML-\n * escaped before display, so it can never inject markup.\n */\nconst props = defineProps<{\n /** Download URL of the .eml bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst mail = ref<ParsedEmail | null>(null)\nconst bodyMode = ref<'html' | 'text'>('html')\n\n// Emails are usually small, but attachments can balloon the file; gate very large ones behind a confirm\n// (all decoding is in-memory). Mirrors the binary browser's large-file guard.\nconst SIZE_LIMIT = 12 * 1024 * 1024\n\ninterface AttView {\n filename: string\n contentType: string\n size: number\n url: string\n}\nconst attachments = ref<AttView[]>([])\nconst htmlUrl = ref('')\nlet objectUrls: string[] = []\n\nfunction revokeAll(): void {\n for (const u of objectUrls) URL.revokeObjectURL(u)\n objectUrls = []\n htmlUrl.value = ''\n attachments.value = []\n}\n\n// Same hardening as FileViewerPanel's HTML preview: kill all script/plugin/base/form vectors. Multiple CSPs\n// intersect, so a document's own CSP can't loosen this.\nconst NO_SCRIPT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\n\nfunction injectNoScriptCsp(html: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + NO_SCRIPT_CSP + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${NO_SCRIPT_CSP}</head><body>${html}</body></html>`\n}\n\n/** Base64-encode bytes (chunked) for an inline image's data URL. */\nfunction bytesToBase64(bytes: Uint8Array): string {\n let bin = ''\n const CHUNK = 0x8000\n for (let i = 0; i < bytes.length; i += CHUNK) {\n bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK))\n }\n return btoa(bin)\n}\n\n/** Rewrite `cid:` references in the HTML body to data URLs from the inline-image map. */\nfunction resolveCids(html: string, parsed: ParsedEmail): string {\n if (!parsed.inlineById.size) return html\n return html.replace(/cid:([^\"')\\s>]+)/gi, (whole, id: string) => {\n const att = parsed.inlineById.get(id) ?? parsed.inlineById.get(decodeURIComponent(id))\n if (!att) return whole\n return `data:${att.contentType || 'application/octet-stream'};base64,${bytesToBase64(att.bytes)}`\n })\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n// Plain-text body → escaped HTML with bare URLs linkified (opens in a new tab; escaped first, so safe).\nconst textHtml = computed(() => {\n const t = mail.value?.textBody\n if (!t) return ''\n return escapeHtml(t).replace(\n /(https?:\\/\\/[^\\s<]+)/g,\n (u) => `<a href=\"${u}\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">${u}</a>`,\n )\n})\n\nconst hasHtml = computed(() => !!mail.value?.htmlBody && mail.value.htmlBody.trim().length > 0)\nconst hasText = computed(() => !!mail.value?.textBody && mail.value.textBody.trim().length > 0)\nconst hasBody = computed(() => hasHtml.value || hasText.value)\nconst showHtml = computed(() => bodyMode.value === 'html' && hasHtml.value)\n\n// Human-friendly Date header (falls back to the raw value when it doesn't parse).\nconst dateDisplay = computed(() => {\n const d = mail.value?.date\n if (!d) return ''\n const t = Date.parse(d)\n if (Number.isNaN(t)) return d\n try {\n return new Date(t).toLocaleString('zh-CN', { dateStyle: 'medium', timeStyle: 'short' })\n } catch {\n return d\n }\n})\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n revokeAll()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n const parsed = parseEml(new Uint8Array(buf))\n\n // Visible attachments exclude inline images (those render inside the HTML body).\n for (const att of parsed.attachments) {\n if (att.isInline) continue\n const blob = new Blob([att.bytes], {\n type: att.contentType || 'application/octet-stream',\n })\n const url = URL.createObjectURL(blob)\n objectUrls.push(url)\n attachments.value.push({\n filename: att.filename,\n contentType: att.contentType,\n size: att.size,\n url,\n })\n }\n if (parsed.htmlBody !== null) {\n const safe = injectNoScriptCsp(resolveCids(parsed.htmlBody, parsed))\n const url = URL.createObjectURL(new Blob([safe], { type: 'text/html' }))\n objectUrls.push(url)\n htmlUrl.value = url\n }\n\n mail.value = parsed\n bodyMode.value = parsed.htmlBody && parsed.htmlBody.trim() ? 'html' : 'text'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n // Gate oversized files behind a confirm; otherwise fetch + parse straight away.\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(revokeAll)\n</script>\n\n<template>\n <div class=\"eml\">\n <p v-if=\"status === 'loading'\" class=\"eml__msg\">正在解析邮件…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"eml__msg eml__gate\">\n <p>该邮件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"eml__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"eml__msg eml__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"eml__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && mail\">\n <!-- header card: subject + addressing + date -->\n <div class=\"eml__head\">\n <div class=\"eml__subject\">{{ mail.subject || '(无主题)' }}</div>\n <dl class=\"eml__fields\">\n <div v-if=\"mail.from\" class=\"eml__field\">\n <dt>发件人</dt>\n <dd>{{ mail.from }}</dd>\n </div>\n <div v-if=\"mail.to\" class=\"eml__field\">\n <dt>收件人</dt>\n <dd>{{ mail.to }}</dd>\n </div>\n <div v-if=\"mail.cc\" class=\"eml__field\">\n <dt>抄送</dt>\n <dd>{{ mail.cc }}</dd>\n </div>\n <div v-if=\"dateDisplay\" class=\"eml__field\">\n <dt>日期</dt>\n <dd>{{ dateDisplay }}</dd>\n </div>\n </dl>\n\n <!-- attachments strip -->\n <div v-if=\"attachments.length\" class=\"eml__atts\">\n <span class=\"eml__atts-label\">\n <Icon name=\"fileOutline\" :size=\"13\" />附件 {{ attachments.length }}\n </span>\n <a\n v-for=\"(att, i) in attachments\"\n :key=\"i\"\n class=\"eml__att\"\n :href=\"att.url\"\n :download=\"att.filename\"\n :title=\"`下载 ${att.filename}`\"\n >\n <span class=\"eml__att-name\">{{ att.filename }}</span>\n <span v-if=\"att.size\" class=\"eml__att-size\">{{ formatBytes(att.size) }}</span>\n <Icon name=\"download\" :size=\"13\" />\n </a>\n </div>\n\n <!-- body mode toggle (only when both alternatives exist) -->\n <div v-if=\"hasHtml && hasText\" class=\"eml__toggle\" role=\"group\" aria-label=\"正文格式\">\n <button\n type=\"button\"\n class=\"eml__tab\"\n :class=\"{ 'eml__tab--on': bodyMode === 'html' }\"\n @click=\"bodyMode = 'html'\"\n >\n HTML\n </button>\n <button\n type=\"button\"\n class=\"eml__tab\"\n :class=\"{ 'eml__tab--on': bodyMode === 'text' }\"\n @click=\"bodyMode = 'text'\"\n >\n 纯文本\n </button>\n </div>\n </div>\n\n <!-- body: HTML in a sandboxed iframe, or escaped plain text; falls through to an empty notice -->\n <div v-if=\"showHtml\" class=\"eml__html\">\n <iframe\n class=\"eml__frame\"\n :src=\"htmlUrl\"\n title=\"邮件正文\"\n sandbox=\"\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n <!-- eslint-disable-next-line vue/no-v-html — textHtml is escaped, then only URLs are linkified -->\n <div v-else-if=\"hasText\" class=\"eml__text\" v-html=\"textHtml\" />\n <p v-else class=\"eml__msg\">(此邮件没有可显示的正文)</p>\n </template>\n </div>\n</template>\n\n<style scoped>\n.eml {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.eml__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.eml__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.eml__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.eml__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.eml__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Header card: subject + fields + attachments, fixed above the scrolling body. */\n.eml__head {\n flex-shrink: 0;\n padding: 14px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.eml__subject {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--text-strong);\n word-break: break-word;\n}\n.eml__fields {\n margin: 10px 0 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n.eml__field {\n display: flex;\n gap: 8px;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.eml__field dt {\n flex-shrink: 0;\n width: 48px;\n color: var(--faint);\n}\n.eml__field dd {\n margin: 0;\n min-width: 0;\n color: var(--text);\n word-break: break-word;\n}\n\n.eml__atts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n padding-top: 12px;\n border-top: 1px dashed var(--border);\n}\n.eml__atts-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n color: var(--faint);\n}\n.eml__att {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 5px 9px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 8px;\n color: var(--text);\n text-decoration: none;\n font-size: 12px;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.eml__att:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.eml__att-name {\n min-width: 0;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eml__att-size {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.eml__toggle {\n display: inline-flex;\n gap: 2px;\n margin-top: 12px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.eml__tab {\n padding: 4px 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n}\n.eml__tab--on {\n color: var(--accent);\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n\n/* HTML body: the iframe fills the remaining height on a white canvas (like the HTML file preview). */\n.eml__html {\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.eml__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* Plain-text body: monospace, wrapped, scrolls within the pane. */\n.eml__text {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n margin: 0;\n padding: 16px 18px 60px;\n font-family: var(--font-mono);\n font-size: 13px;\n line-height: 1.6;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n background: var(--code-bg);\n}\n.eml__text :deep(a) {\n color: var(--accent);\n text-decoration: underline;\n}\n</style>\n"],"mappings":"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"}
|