@l1yp/file-viewer 0.1.0 → 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 +2 -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 +9 -0
- package/dist/components/DocxNavigationPane.js.map +1 -0
- package/dist/components/DocxNavigationPane.vue.d.ts +23 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +98 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxViewer.js +3 -1
- package/dist/components/DocxViewer.js.map +1 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +72 -37
- 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 +3 -1
- package/dist/components/FileViewerBody.js.map +1 -0
- package/dist/components/FileViewerBody.vue.d.ts +5 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +250 -247
- 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 +9 -0
- package/dist/components/PptxNavigationPane.js.map +1 -0
- package/dist/components/PptxNavigationPane.vue.d.ts +20 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +120 -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 +71 -39
- 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 +3 -1
- package/dist/components/XlsxViewer.js.map +1 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +301 -304
- 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 +3 -1
- package/dist/components/ZipBrowser.js.map +1 -0
- package/dist/components/ZipBrowser.vue.d.ts +3 -1
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +154 -122
- 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.d.ts +21 -0
- package/dist/composables/useElementNavigation.js +63 -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.d.ts +49 -0
- package/dist/lib/archiveHtml.js +377 -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.d.ts +31 -0
- package/dist/lib/docxNavigation.js +65 -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.d.ts +7 -0
- package/dist/lib/pptxNavigation.js +29 -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":"HarViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/HarViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n type DecodedBody,\n type HarEntry,\n type HarNV,\n decodeContent,\n decodePostData,\n fmtMs,\n fmtSize,\n parseHar,\n parseQuery,\n prettyBody,\n statusClass,\n toCurl,\n} from '@/lib/har'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer HAR (`.har`) renderer. Fetches the archive text and parses the HTTP Archive entirely in-browser\n * (hand-rolled, see lib/har.ts) — no third-party. Lays out a master-detail vertical split tuned for the narrow\n * side pane: a filterable request list on top, and the selected request's detail (general / headers / query /\n * cookies / request + response body) below. Bodies are decoded (standard base64 + the «base64 but undeclared»\n * case some tools emit); images preview from a data URI, binaries show a hex dump.\n *\n * Safety: a HAR body is untrusted upstream content. We never execute it — text bodies render as escaped,\n * optionally syntax-highlighted source; image bodies render through a data-URI `<img>` (an `<img>` never runs\n * scripts, so even an SVG body is inert). No body is fetched as a live resource.\n */\nconst props = defineProps<{\n /** Download URL of the .har 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('')\n\ninterface HarState {\n entries: HarEntry[]\n creator: string\n version: string\n}\nconst doc = ref<HarState | null>(null)\nconst selected = ref<HarEntry | null>(null)\nconst query = ref('')\n\ntype DetailTab =\n | 'general'\n | 'reqHeaders'\n | 'resHeaders'\n | 'query'\n | 'cookies'\n | 'reqBody'\n | 'resBody'\nconst tab = ref<DetailTab>('general')\n\n// A HAR can be large (megabytes of inlined bodies); gate very big ones behind a confirm before parsing.\nconst SIZE_LIMIT = 25 * 1024 * 1024\n\nconst CATEGORY_LABELS: Record<string, string> = {\n doc: '文档',\n xhr: 'XHR',\n js: 'JS',\n css: 'CSS',\n img: '图片',\n font: '字体',\n media: '媒体',\n ws: 'WS',\n other: '其它',\n}\n\n// Category quick-filter: 'all' or a HarCategory; only categories present in the doc are offered.\nconst catFilter = ref<string>('all')\nconst presentCategories = computed(() => {\n const seen = new Set<string>()\n for (const e of doc.value?.entries ?? []) seen.add(e.category)\n return Object.keys(CATEGORY_LABELS).filter((c) => seen.has(c))\n})\n\nconst filtered = computed(() => {\n const list = doc.value?.entries ?? []\n const q = query.value.trim().toLowerCase()\n const cat = catFilter.value\n return list.filter((e) => {\n if (cat !== 'all' && e.category !== cat) return false\n if (!q) return true\n return (\n e.url.toLowerCase().includes(q) ||\n e.method.toLowerCase().includes(q) ||\n String(e.status).includes(q) ||\n e.type.toLowerCase().includes(q)\n )\n })\n})\n\n// Last path segment (or host) as the row's headline; the host shows as the dim sub-line.\nfunction entryName(e: HarEntry): string {\n const seg = e.pathname.split('/').filter(Boolean).pop()\n return seg || e.host || e.pathname || e.url\n}\n\nfunction pick(e: HarEntry): void {\n selected.value = e\n // Reset to a tab that has content: prefer general; it's always present.\n tab.value = 'general'\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n const parsed = parseHar(text)\n doc.value = { entries: parsed.entries, creator: parsed.creator, version: parsed.version }\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 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 })\n\n/* ===== detail derivations ===== */\n\nconst reqHeaders = computed<HarNV[]>(() => selected.value?.raw.request.headers ?? [])\nconst resHeaders = computed<HarNV[]>(() => selected.value?.raw.response.headers ?? [])\nconst queryParams = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n // Prefer the explicit queryString; fall back to parsing the URL's search.\n const qs = e.raw.request.queryString\n if (qs.length) return qs\n const i = e.url.indexOf('?')\n return i >= 0 ? parseQuery(e.url.slice(i)) : []\n})\nconst cookies = computed(() => {\n const e = selected.value\n if (!e) return { req: [], res: [] }\n return { req: e.raw.request.cookies, res: e.raw.response.cookies }\n})\nconst hasCookies = computed(() => cookies.value.req.length + cookies.value.res.length > 0)\n\nconst reqBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodePostData(e.raw.request.postData, 'auto')\n return b.kind === 'empty' ? null : b\n})\nconst resBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodeContent(e.raw.response.content, 'auto')\n return b.kind === 'empty' ? null : b\n})\n\n// General info rows for the selected entry.\nconst generalRows = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n const r = e.raw\n const rows: HarNV[] = [\n { name: '请求 URL', value: e.url },\n { name: '方法', value: e.method },\n { name: '状态', value: e.status ? `${e.status} ${e.statusText}`.trim() : '(无响应)' },\n { name: '协议', value: r.response.httpVersion || r.request.httpVersion || '' },\n { name: '远端 IP', value: e.serverIP },\n { name: '响应类型', value: e.mime || '' },\n { name: '响应大小', value: fmtSize(e.size) },\n { name: '请求体大小', value: e.reqSize ? fmtSize(e.reqSize) : '' },\n { name: '耗时', value: fmtMs(e.time) },\n { name: '开始时间', value: e.startedISO },\n ]\n if (r.response.redirectURL) rows.push({ name: '重定向', value: r.response.redirectURL })\n return rows.filter((row) => row.value)\n})\n\n/** Render a decoded body to HTML for the <pre>: pretty-print JSON, syntax-highlight when we have a language. */\nfunction bodyHtml(body: DecodedBody): string {\n const { text, pretty } = prettyBody(body.text, body.mime)\n const lang = pretty ? 'json' : bodyLangFor(body.mime)\n return lang ? highlightCode(text, lang) : escapeHtml(text)\n}\nfunction bodyLangFor(mime: string): string {\n const m = mime.toLowerCase()\n if (m.includes('javascript') || m.includes('ecmascript')) return 'javascript'\n if (m === 'text/css') return 'css'\n if (m === 'text/html' || m.includes('xhtml')) return 'xml'\n if (m.includes('json')) return 'json'\n if (m.includes('svg') || m.includes('xml')) return 'xml'\n return ''\n}\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/* ===== copy helpers ===== */\n\nconst copiedKey = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyText(text: string, key: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedKey.value = key\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedKey.value = ''), 1400)\n}\nfunction copyCurl(): void {\n if (selected.value) void copyText(toCurl(selected.value), 'curl')\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"har\">\n <p v-if=\"status === 'loading'\" class=\"har__msg\">正在解析 HAR…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"har__msg har__gate\">\n <p>该 HAR 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"har__msg har__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && doc\">\n <!-- toolbar: filter + category chips + count -->\n <div class=\"har__bar\">\n <input\n v-model=\"query\"\n class=\"har__filter\"\n type=\"text\"\n placeholder=\"筛选 URL / 方法 / 状态…\"\n />\n <div class=\"har__cats\">\n <button\n type=\"button\"\n class=\"har__cat\"\n :class=\"{ 'har__cat--on': catFilter === 'all' }\"\n @click=\"catFilter = 'all'\"\n >\n 全部 {{ doc.entries.length }}\n </button>\n <button\n v-for=\"c in presentCategories\"\n :key=\"c\"\n type=\"button\"\n class=\"har__cat\"\n :class=\"[`har__cat--${c}`, { 'har__cat--on': catFilter === c }]\"\n @click=\"catFilter = c\"\n >\n {{ CATEGORY_LABELS[c] }}\n </button>\n </div>\n </div>\n\n <!-- master: request list -->\n <div class=\"har__list\" :class=\"{ 'har__list--split': selected }\">\n <p v-if=\"!filtered.length\" class=\"har__empty\">没有匹配的请求。</p>\n <button\n v-for=\"e in filtered\"\n :key=\"e.index\"\n type=\"button\"\n class=\"har__row\"\n :class=\"{ 'har__row--on': selected?.index === e.index }\"\n :title=\"e.url\"\n @click=\"pick(e)\"\n >\n <span class=\"har__method\" :class=\"`m-${e.method.toLowerCase()}`\">{{ e.method }}</span>\n <span class=\"har__status\" :class=\"statusClass(e.status)\">{{ e.status || '—' }}</span>\n <span class=\"har__name\">\n <span class=\"har__name-main\">{{ entryName(e) }}</span>\n <span class=\"har__name-sub\">{{ e.host }}</span>\n </span>\n <span class=\"har__type\">{{ e.type }}</span>\n <span class=\"har__size\">{{ fmtSize(e.size) }}</span>\n <span class=\"har__time\">{{ fmtMs(e.time) }}</span>\n </button>\n </div>\n\n <!-- detail: tabs + content -->\n <section v-if=\"selected\" class=\"har__detail\">\n <div class=\"har__tabs\">\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'general' }\"\n @click=\"tab = 'general'\"\n >\n 概览\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqHeaders' }\"\n @click=\"tab = 'reqHeaders'\"\n >\n 请求头 <span class=\"har__tab-n\">{{ reqHeaders.length }}</span>\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resHeaders' }\"\n @click=\"tab = 'resHeaders'\"\n >\n 响应头 <span class=\"har__tab-n\">{{ resHeaders.length }}</span>\n </button>\n <button\n v-if=\"queryParams.length\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'query' }\"\n @click=\"tab = 'query'\"\n >\n 查询 <span class=\"har__tab-n\">{{ queryParams.length }}</span>\n </button>\n <button\n v-if=\"hasCookies\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'cookies' }\"\n @click=\"tab = 'cookies'\"\n >\n Cookie\n </button>\n <button\n v-if=\"reqBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqBody' }\"\n @click=\"tab = 'reqBody'\"\n >\n 请求体\n </button>\n <button\n v-if=\"resBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resBody' }\"\n @click=\"tab = 'resBody'\"\n >\n 响应体\n </button>\n </div>\n\n <div class=\"har__pane\">\n <!-- general -->\n <template v-if=\"tab === 'general'\">\n <dl class=\"har__kv\">\n <div v-for=\"row in generalRows\" :key=\"row.name\" class=\"har__kv-row\">\n <dt>{{ row.name }}</dt>\n <dd>{{ row.value }}</dd>\n </div>\n </dl>\n <button type=\"button\" class=\"har__btn har__btn--sm\" @click=\"copyCurl\">\n {{ copiedKey === 'curl' ? '已复制' : '复制为 cURL' }}\n </button>\n </template>\n\n <!-- request headers -->\n <dl v-else-if=\"tab === 'reqHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in reqHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- response headers -->\n <dl v-else-if=\"tab === 'resHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in resHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- query string -->\n <dl v-else-if=\"tab === 'query'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in queryParams\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n\n <!-- cookies -->\n <template v-else-if=\"tab === 'cookies'\">\n <template v-if=\"cookies.req.length\">\n <h4 class=\"har__sub\">请求 Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.req\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n <template v-if=\"cookies.res.length\">\n <h4 class=\"har__sub\">响应 Set-Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.res\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n </template>\n\n <!-- request body -->\n <template v-else-if=\"tab === 'reqBody' && reqBody\">\n <dl v-if=\"reqBody.kind === 'form' && reqBody.form\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in reqBody.form\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n <template v-else>\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\"\n >{{ reqBody.mime || '文本' }} · {{ fmtSize(reqBody.size) }}</span\n >\n <button\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(reqBody.text, 'reqBody')\"\n >\n {{ copiedKey === 'reqBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <pre class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(reqBody)\" /></pre>\n </template>\n </template>\n\n <!-- response body -->\n <template v-else-if=\"tab === 'resBody' && resBody\">\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\">\n {{ resBody.mime || '文本' }} · {{ fmtSize(resBody.size) }}\n <span v-if=\"resBody.wasBase64\" class=\"har__b64\">base64 已解码</span>\n </span>\n <button\n v-if=\"!resBody.isImage\"\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(resBody.text, 'resBody')\"\n >\n {{ copiedKey === 'resBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <div v-if=\"resBody.isImage\" class=\"har__img\">\n <img :src=\"resBody.dataUri\" :alt=\"selected.url\" />\n </div>\n <pre v-else class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(resBody)\" /></pre>\n </template>\n </div>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\n.har {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.har__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.har__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.har__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.har__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.har__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__btn--sm {\n padding: 3px 10px;\n font-size: 11.5px;\n}\n\n/* toolbar */\n.har__bar {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.har__filter {\n width: 100%;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.har__filter:focus {\n outline: none;\n border-color: var(--accent);\n}\n.har__cats {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n.har__cat {\n font-family: inherit;\n font-size: 11px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 2px 9px;\n cursor: pointer;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.har__cat:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__cat--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n\n/* master list */\n.har__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n.har__list--split {\n flex: 0 0 42%;\n border-bottom: 1px solid var(--border);\n}\n.har__empty {\n padding: 18px 14px;\n font-size: 12.5px;\n color: var(--faint);\n}\n.har__row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 12px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.har__row:hover {\n background: var(--surface-2);\n}\n.har__row--on {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.har__method {\n flex-shrink: 0;\n width: 42px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.har__method.m-get {\n color: #2f9e44;\n}\n.har__method.m-post {\n color: #1c7ed6;\n}\n.har__method.m-put,\n.har__method.m-patch {\n color: #f08c00;\n}\n.har__method.m-delete {\n color: #e03131;\n}\n.har__status {\n flex-shrink: 0;\n width: 30px;\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n}\n.har__name {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.har__name-main {\n font-size: 12px;\n color: var(--text-strong);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__name-sub {\n font-size: 10.5px;\n color: var(--faint);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__type {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--muted);\n}\n.har__size,\n.har__time {\n flex-shrink: 0;\n width: 52px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--faint);\n}\n\n/* status colors (shared) */\n.s-2xx {\n color: #2f9e44;\n}\n.s-3xx {\n color: #1c7ed6;\n}\n.s-4xx {\n color: #f08c00;\n}\n.s-5xx {\n color: #e03131;\n}\n.s-pending {\n color: var(--faint);\n}\n\n/* detail */\n.har__detail {\n flex: 1 1 58%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.har__tabs {\n flex-shrink: 0;\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n white-space: nowrap;\n}\n.har__tab {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n padding: 6px 9px;\n cursor: pointer;\n}\n.har__tab:hover {\n color: var(--text);\n}\n.har__tab--on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n.har__tab-n {\n font-size: 10.5px;\n color: var(--faint);\n}\n.har__pane {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 12px 14px;\n}\n\n.har__kv {\n margin: 0 0 12px;\n display: flex;\n flex-direction: column;\n}\n.har__kv-row {\n display: flex;\n gap: 10px;\n padding: 4px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.har__kv-row dt {\n flex-shrink: 0;\n width: 38%;\n max-width: 180px;\n color: var(--muted);\n word-break: break-word;\n}\n.har__kv-row dd {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n color: var(--text);\n word-break: break-word;\n}\n.har__kv--mono dt,\n.har__kv--mono dd {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.har__sub {\n margin: 4px 0 8px;\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n\n.har__bodybar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 8px;\n}\n.har__bodymime {\n font-size: 11px;\n color: var(--faint);\n}\n.har__b64 {\n margin-left: 6px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--surface-2);\n color: var(--muted);\n}\n.har__body {\n margin: 0;\n padding: 10px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n}\n.har__body code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n.har__img {\n display: flex;\n justify-content: center;\n padding: 12px;\n background: var(--surface-2);\n border-radius: 8px;\n}\n.har__img img {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HexViewer.js","names":[],"sources":["../../src/components/HexViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer hex dump: fetch a file's raw bytes and render them as the classic three columns — an offset\n * gutter, 16 bytes per row in hex (split into two 8-byte groups), and an ASCII gutter (printable bytes shown\n * verbatim, everything else as `.`). Like {@link LogViewer} the rows are VIRTUALIZED — only the visible window\n * is in the DOM — so even a multi-MB file scrolls smoothly (a naive dump would mount hundreds of thousands of\n * rows). The bytes never leave the browser; they come from the same download endpoint as any other file.\n *\n * This is the universal fall-back: ANY file can be inspected byte-for-byte here, so it's offered (alongside\n * the other formatters) on the unpreviewable-binary card — never auto-detected. Parsing needs the whole file\n * in memory, so a large file is gated behind a confirm (mirrors BinObjBrowser).\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\nconst ROW_BYTES = 16\nconst LINE_H = 20 // px — MUST match the .hx__row CSS height\nconst OVERSCAN = 12\n// Reading the whole file into memory is unavoidable (the endpoint has no range support); auto-load up to this,\n// gate anything larger behind a confirm. Unknown size auto-loads (the user explicitly opened it as hex).\nconst GATE = 16 * 1024 * 1024\n\ntype Status = 'confirm' | 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst bytes = ref<Uint8Array>(new Uint8Array(0))\n\nconst scrollEl = ref<HTMLElement | null>(null)\nconst scrollTop = ref(0)\nconst viewportH = ref(400)\nlet ro: ResizeObserver | null = null\n\nconst total = computed(() => Math.ceil(bytes.value.length / ROW_BYTES))\n\nconst startIdx = computed(() => Math.max(0, Math.floor(scrollTop.value / LINE_H) - OVERSCAN))\nconst endIdx = computed(() =>\n Math.min(total.value, Math.ceil((scrollTop.value + viewportH.value) / LINE_H) + OVERSCAN),\n)\n\n// Format one row (16 bytes) → offset, the spaced hex string (two 8-byte groups), and the ASCII gutter.\n// Trailing positions on the last row are padded so every row's hex column has the same width (keeps the\n// ASCII column aligned). Built only for visible rows, so it stays cheap regardless of file size.\nfunction rowAt(i: number): { i: number; off: string; hex: string; asc: string } {\n const src = bytes.value\n const start = i * ROW_BYTES\n const cells: string[] = []\n let asc = ''\n for (let j = 0; j < ROW_BYTES; j++) {\n const idx = start + j\n if (idx < src.length) {\n const b = src[idx]\n cells.push(b.toString(16).padStart(2, '0'))\n asc += b >= 0x20 && b <= 0x7e ? String.fromCharCode(b) : '.'\n } else {\n cells.push(' ')\n asc += ' '\n }\n }\n const hex = `${cells.slice(0, 8).join(' ')} ${cells.slice(8, 16).join(' ')}`\n return { i, off: start.toString(16).padStart(8, '0'), hex, asc }\n}\n\nconst visible = computed(() => {\n const rows: { i: number; off: string; hex: string; asc: string }[] = []\n for (let i = startIdx.value; i < endIdx.value; i++) rows.push(rowAt(i))\n return rows\n})\n\nfunction onScroll(): void {\n if (scrollEl.value) scrollTop.value = scrollEl.value.scrollTop\n}\n\n// Jump to a byte offset (accepts decimal or 0x-prefixed / bare hex), scrolling its row into the middle.\nconst gotoInput = ref('')\nfunction gotoOffset(): void {\n const raw = gotoInput.value.trim()\n if (!raw) return\n const n = /^0x/i.test(raw)\n ? parseInt(raw.slice(2), 16)\n : /^[0-9]+$/.test(raw)\n ? parseInt(raw, 10)\n : parseInt(raw, 16)\n if (!Number.isFinite(n) || n < 0) return\n const row = Math.floor(Math.min(n, bytes.value.length) / ROW_BYTES)\n const el = scrollEl.value\n if (el) el.scrollTop = Math.max(0, row * LINE_H - viewportH.value / 2 + LINE_H)\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) {\n status.value = 'ready'\n bytes.value = new Uint8Array(0)\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n bytes.value = new Uint8Array(await res.arrayBuffer())\n scrollTop.value = 0\n if (scrollEl.value) scrollEl.value.scrollTop = 0\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Decide whether to auto-load or gate behind a confirm. Runs on mount (client-only) and whenever a different\n// file is opened into the same panel.\nfunction decideInitial(): void {\n bytes.value = new Uint8Array(0)\n gotoInput.value = ''\n errorMsg.value = ''\n scrollTop.value = 0\n if (props.size != null && props.size > GATE) status.value = 'confirm'\n else void doLoad()\n}\n\n// scrollEl lives inside the ready branch (appears only after load), and is rebuilt on reload — watch the ref\n// and (re)attach the ResizeObserver so viewportH tracks the REAL pane height (see the same pattern in LogViewer).\nwatch(\n scrollEl,\n (el) => {\n ro?.disconnect()\n if (el && typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(() => {\n if (scrollEl.value) viewportH.value = scrollEl.value.clientHeight\n })\n ro.observe(el)\n viewportH.value = el.clientHeight\n }\n },\n { flush: 'post' },\n)\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({ reload: doLoad })\n</script>\n\n<template>\n <div class=\"hx\">\n <!-- large-file gate: don't auto-download; ask first -->\n <div v-if=\"status === 'confirm'\" class=\"hx__msg hx__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将整个读入浏览器以查看十六进制。是否继续?</p>\n <button type=\"button\" class=\"hx__btn\" @click=\"doLoad\">加载并查看</button>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"hx__msg\">正在加载…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"hx__msg hx__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"hx__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <p v-else-if=\"bytes.length === 0\" class=\"hx__msg\">空文件(0 字节)</p>\n\n <template v-else>\n <!-- toolbar: total size + jump-to-offset -->\n <div class=\"hx__bar\">\n <span class=\"hx__total\">{{ formatBytes(bytes.length) }} · {{ bytes.length }} 字节</span>\n <span class=\"hx__spacer\" />\n <input\n v-model=\"gotoInput\"\n class=\"hx__goto\"\n type=\"text\"\n placeholder=\"偏移 (0x…)\"\n aria-label=\"跳转到偏移\"\n @keydown.enter=\"gotoOffset\"\n />\n </div>\n\n <!-- virtual list: a fixed-height sizer drives the scrollbar; only the visible window is mounted -->\n <div ref=\"scrollEl\" class=\"hx__scroll\" @scroll=\"onScroll\">\n <div class=\"hx__sizer\" :style=\"{ height: total * LINE_H + 'px' }\">\n <div class=\"hx__rows\" :style=\"{ paddingTop: startIdx * LINE_H + 'px' }\">\n <div v-for=\"row in visible\" :key=\"row.i\" class=\"hx__row\">\n <span class=\"hx__off\">{{ row.off }}</span>\n <span class=\"hx__hex\">{{ row.hex }}</span>\n <span class=\"hx__asc\">{{ row.asc }}</span>\n </div>\n </div>\n </div>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.hx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.hx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.hx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.hx__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.hx__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.hx__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.hx__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}\n.hx__total {\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.hx__spacer {\n flex: 1 1 auto;\n}\n.hx__goto {\n flex-shrink: 0;\n width: 110px;\n padding: 5px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.hx__goto:focus {\n outline: none;\n border-color: var(--accent);\n}\n\n.hx__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.hx__sizer {\n position: relative;\n min-width: 100%;\n}\n.hx__row {\n display: flex;\n align-items: center;\n gap: 18px;\n height: 20px;\n line-height: 20px;\n padding: 0 16px;\n white-space: pre;\n font-family: var(--font-mono);\n font-size: 12.5px;\n}\n.hx__off {\n flex-shrink: 0;\n color: var(--faint);\n user-select: none;\n}\n.hx__hex {\n flex-shrink: 0;\n color: var(--text);\n}\n.hx__asc {\n flex-shrink: 0;\n color: var(--muted);\n border-left: 1px solid var(--border);\n padding-left: 18px;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HexViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/HexViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer hex dump: fetch a file's raw bytes and render them as the classic three columns — an offset\n * gutter, 16 bytes per row in hex (split into two 8-byte groups), and an ASCII gutter (printable bytes shown\n * verbatim, everything else as `.`). Like {@link LogViewer} the rows are VIRTUALIZED — only the visible window\n * is in the DOM — so even a multi-MB file scrolls smoothly (a naive dump would mount hundreds of thousands of\n * rows). The bytes never leave the browser; they come from the same download endpoint as any other file.\n *\n * This is the universal fall-back: ANY file can be inspected byte-for-byte here, so it's offered (alongside\n * the other formatters) on the unpreviewable-binary card — never auto-detected. Parsing needs the whole file\n * in memory, so a large file is gated behind a confirm (mirrors BinObjBrowser).\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\nconst ROW_BYTES = 16\nconst LINE_H = 20 // px — MUST match the .hx__row CSS height\nconst OVERSCAN = 12\n// Reading the whole file into memory is unavoidable (the endpoint has no range support); auto-load up to this,\n// gate anything larger behind a confirm. Unknown size auto-loads (the user explicitly opened it as hex).\nconst GATE = 16 * 1024 * 1024\n\ntype Status = 'confirm' | 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst bytes = ref<Uint8Array>(new Uint8Array(0))\n\nconst scrollEl = ref<HTMLElement | null>(null)\nconst scrollTop = ref(0)\nconst viewportH = ref(400)\nlet ro: ResizeObserver | null = null\n\nconst total = computed(() => Math.ceil(bytes.value.length / ROW_BYTES))\n\nconst startIdx = computed(() => Math.max(0, Math.floor(scrollTop.value / LINE_H) - OVERSCAN))\nconst endIdx = computed(() =>\n Math.min(total.value, Math.ceil((scrollTop.value + viewportH.value) / LINE_H) + OVERSCAN),\n)\n\n// Format one row (16 bytes) → offset, the spaced hex string (two 8-byte groups), and the ASCII gutter.\n// Trailing positions on the last row are padded so every row's hex column has the same width (keeps the\n// ASCII column aligned). Built only for visible rows, so it stays cheap regardless of file size.\nfunction rowAt(i: number): { i: number; off: string; hex: string; asc: string } {\n const src = bytes.value\n const start = i * ROW_BYTES\n const cells: string[] = []\n let asc = ''\n for (let j = 0; j < ROW_BYTES; j++) {\n const idx = start + j\n if (idx < src.length) {\n const b = src[idx]\n cells.push(b.toString(16).padStart(2, '0'))\n asc += b >= 0x20 && b <= 0x7e ? String.fromCharCode(b) : '.'\n } else {\n cells.push(' ')\n asc += ' '\n }\n }\n const hex = `${cells.slice(0, 8).join(' ')} ${cells.slice(8, 16).join(' ')}`\n return { i, off: start.toString(16).padStart(8, '0'), hex, asc }\n}\n\nconst visible = computed(() => {\n const rows: { i: number; off: string; hex: string; asc: string }[] = []\n for (let i = startIdx.value; i < endIdx.value; i++) rows.push(rowAt(i))\n return rows\n})\n\nfunction onScroll(): void {\n if (scrollEl.value) scrollTop.value = scrollEl.value.scrollTop\n}\n\n// Jump to a byte offset (accepts decimal or 0x-prefixed / bare hex), scrolling its row into the middle.\nconst gotoInput = ref('')\nfunction gotoOffset(): void {\n const raw = gotoInput.value.trim()\n if (!raw) return\n const n = /^0x/i.test(raw)\n ? parseInt(raw.slice(2), 16)\n : /^[0-9]+$/.test(raw)\n ? parseInt(raw, 10)\n : parseInt(raw, 16)\n if (!Number.isFinite(n) || n < 0) return\n const row = Math.floor(Math.min(n, bytes.value.length) / ROW_BYTES)\n const el = scrollEl.value\n if (el) el.scrollTop = Math.max(0, row * LINE_H - viewportH.value / 2 + LINE_H)\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) {\n status.value = 'ready'\n bytes.value = new Uint8Array(0)\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n bytes.value = new Uint8Array(await res.arrayBuffer())\n scrollTop.value = 0\n if (scrollEl.value) scrollEl.value.scrollTop = 0\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Decide whether to auto-load or gate behind a confirm. Runs on mount (client-only) and whenever a different\n// file is opened into the same panel.\nfunction decideInitial(): void {\n bytes.value = new Uint8Array(0)\n gotoInput.value = ''\n errorMsg.value = ''\n scrollTop.value = 0\n if (props.size != null && props.size > GATE) status.value = 'confirm'\n else void doLoad()\n}\n\n// scrollEl lives inside the ready branch (appears only after load), and is rebuilt on reload — watch the ref\n// and (re)attach the ResizeObserver so viewportH tracks the REAL pane height (see the same pattern in LogViewer).\nwatch(\n scrollEl,\n (el) => {\n ro?.disconnect()\n if (el && typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(() => {\n if (scrollEl.value) viewportH.value = scrollEl.value.clientHeight\n })\n ro.observe(el)\n viewportH.value = el.clientHeight\n }\n },\n { flush: 'post' },\n)\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({ reload: doLoad })\n</script>\n\n<template>\n <div class=\"hx\">\n <!-- large-file gate: don't auto-download; ask first -->\n <div v-if=\"status === 'confirm'\" class=\"hx__msg hx__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将整个读入浏览器以查看十六进制。是否继续?</p>\n <button type=\"button\" class=\"hx__btn\" @click=\"doLoad\">加载并查看</button>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"hx__msg\">正在加载…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"hx__msg hx__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"hx__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <p v-else-if=\"bytes.length === 0\" class=\"hx__msg\">空文件(0 字节)</p>\n\n <template v-else>\n <!-- toolbar: total size + jump-to-offset -->\n <div class=\"hx__bar\">\n <span class=\"hx__total\">{{ formatBytes(bytes.length) }} · {{ bytes.length }} 字节</span>\n <span class=\"hx__spacer\" />\n <input\n v-model=\"gotoInput\"\n class=\"hx__goto\"\n type=\"text\"\n placeholder=\"偏移 (0x…)\"\n aria-label=\"跳转到偏移\"\n @keydown.enter=\"gotoOffset\"\n />\n </div>\n\n <!-- virtual list: a fixed-height sizer drives the scrollbar; only the visible window is mounted -->\n <div ref=\"scrollEl\" class=\"hx__scroll\" @scroll=\"onScroll\">\n <div class=\"hx__sizer\" :style=\"{ height: total * LINE_H + 'px' }\">\n <div class=\"hx__rows\" :style=\"{ paddingTop: startIdx * LINE_H + 'px' }\">\n <div v-for=\"row in visible\" :key=\"row.i\" class=\"hx__row\">\n <span class=\"hx__off\">{{ row.off }}</span>\n <span class=\"hx__hex\">{{ row.hex }}</span>\n <span class=\"hx__asc\">{{ row.asc }}</span>\n </div>\n </div>\n </div>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.hx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.hx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.hx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.hx__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.hx__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.hx__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.hx__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}\n.hx__total {\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.hx__spacer {\n flex: 1 1 auto;\n}\n.hx__goto {\n flex-shrink: 0;\n width: 110px;\n padding: 5px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.hx__goto:focus {\n outline: none;\n border-color: var(--accent);\n}\n\n.hx__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.hx__sizer {\n position: relative;\n min-width: 100%;\n}\n.hx__row {\n display: flex;\n align-items: center;\n gap: 18px;\n height: 20px;\n line-height: 20px;\n padding: 0 16px;\n white-space: pre;\n font-family: var(--font-mono);\n font-size: 12.5px;\n}\n.hx__off {\n flex-shrink: 0;\n color: var(--faint);\n user-select: none;\n}\n.hx__hex {\n flex-shrink: 0;\n color: var(--text);\n}\n.hx__asc {\n flex-shrink: 0;\n color: var(--muted);\n border-left: 1px solid var(--border);\n padding-left: 18px;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HprofViewer.js","names":[],"sources":["../../src/components/HprofViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport {\n Hprof,\n HprofParseError,\n decodeStringValue,\n type CollectTarget,\n type HprofClassRow,\n type HprofInstance,\n type HprofObjectView,\n} from '@/lib/hprof'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's HPROF (JVM heap dump) mode: fetch a `.hprof`, stream-parse it in-browser (see lib/hprof.ts),\n * and show a class histogram (instances + shallow size) — then drill into ONE class (or array type) to page\n * through its objects with decoded fields / array content. Mirrors BinObjBrowser: the bytes come from the same\n * download endpoint and never leave the browser.\n *\n * Memory is the whole design constraint (a heap dump can be many GB):\n * - Parsing never loads the file into the JS heap — the Blob is streamed in 8 MB chunks, and only bounded\n * per-class state is kept (Pass A in lib/hprof.ts).\n * - Drilling into a target collects at most a fixed cap of object rows (MAX_COLLECT) via Pass B, then PAGES that\n * cap — so a class with millions of instances still shows instantly. When it overflows the cap, the on-screen\n * filter is local to the loaded rows; a \"search the whole file\" action re-streams with the filter pushed down.\n * - Following a reference (jumping to `#id`) or reading a String's real text both use `resolveObjects`: an\n * on-demand streaming sweep that resolves just the requested ids, bounded to what's on screen. No id→offset\n * index is built, so memory stays ∝ the visible batch, not the object count.\n *\n * Large-file gate (like BinObjBrowser): unless the manifest size is known-small we don't auto-download — a\n * confirm card explains that parsing pulls the whole file, and only fetches on click.\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\nconst LARGE_THRESHOLD = 20 * 1024 * 1024 // auto-load ≤ 20 MB; larger (or unknown) asks first\nconst TABLE_CAP = 2000 // max histogram rows rendered at once (filter to narrow past this)\nconst PAGE_SIZE = 50 // objects per page in the drill-in\nconst STR_CLASS = 'java/lang/String'\n\ntype Status = 'confirm' | 'loading' | 'error' | 'ready'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst progress = ref(0) // 0..1, drives the parse/scan bar\nconst hprof = ref<Hprof | null>(null)\n\nconst view = ref<'overview' | 'class'>('overview')\n\n// ── load + parse ──────────────────────────────────────────────────────────────────────────────────\nfunction decideInitial(): void {\n hprof.value = null\n view.value = 'overview'\n overviewQuery.value = ''\n errorMsg.value = ''\n progress.value = 0\n objStack.value = []\n if (props.size != null && props.size <= LARGE_THRESHOLD) void load()\n else status.value = 'confirm'\n}\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n progress.value = 0\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const blob = await res.blob()\n hprof.value = await Hprof.parse(blob, (pos, size) => {\n progress.value = size ? pos / size : 1\n })\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof HprofParseError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nconst pct = computed(() => Math.round(progress.value * 100))\n\n// ── overview: file meta + class histogram ───────────────────────────────────────────────────────────\nconst summary = computed(() => hprof.value?.summary ?? null)\n\nconst metaRows = computed<{ label: string; value: string }[]>(() => {\n const s = summary.value\n if (!s) return []\n const ts = Number(s.timestamp)\n return [\n { label: '对象总数', value: s.instanceCount.toLocaleString() },\n { label: '类数量', value: s.classCount.toLocaleString() },\n { label: 'Shallow 大小', value: formatBytes(s.heapBytes) },\n { label: 'GC Roots', value: s.gcRoots.toLocaleString() },\n { label: '字符串常量', value: s.stringCount.toLocaleString() },\n { label: 'ID 宽度', value: `${s.idSize} 字节` },\n { label: '转储时间', value: ts > 0 ? new Date(ts).toLocaleString() : '—' },\n { label: '文件大小', value: formatBytes(s.fileSize) },\n ]\n})\n\nconst overviewQuery = ref('')\ntype SortKey = 'bytes' | 'count' | 'name'\nconst sortKey = ref<SortKey>('bytes')\nconst sortAsc = ref(false)\n\nfunction toggleSort(key: SortKey): void {\n if (sortKey.value === key) sortAsc.value = !sortAsc.value\n else {\n sortKey.value = key\n sortAsc.value = key === 'name' // names default A→Z, sizes/counts default high→low\n }\n}\n\nconst sortedRows = computed<HprofClassRow[]>(() => {\n const rows = summary.value?.rows ?? []\n const q = overviewQuery.value.trim().toLowerCase()\n const filtered = q ? rows.filter((r) => r.name.toLowerCase().includes(q)) : rows\n const dir = sortAsc.value ? 1 : -1\n const key = sortKey.value\n // Copy before sort so we never mutate the parser's array.\n return [...filtered].sort((a, b) => {\n if (key === 'name') return dir * a.name.localeCompare(b.name)\n if (key === 'count') return dir * (a.count - b.count) || b.bytes - a.bytes\n return dir * (a.bytes - b.bytes) || b.count - a.count\n })\n})\nconst visibleRows = computed(() => sortedRows.value.slice(0, TABLE_CAP))\nconst tableTruncated = computed(() => sortedRows.value.length > TABLE_CAP)\n\n// ── drill-in (class OR array) ─────────────────────────────────────────────────────────────────────\nconst classSel = ref<HprofClassRow | null>(null)\nconst collecting = ref(false)\nconst collected = ref<HprofInstance[]>([])\nconst collectTotal = ref(0)\nconst collectTruncated = ref(false)\nconst collectQuery = ref('') // the class-view filter box\nconst searchedQuery = ref<string | null>(null) // the query a whole-file scan currently reflects\nconst page = ref(1)\nconst expanded = ref<Set<string>>(new Set())\n\nasync function openTarget(row: HprofClassRow): Promise<void> {\n if (!row.target || !hprof.value) return\n classSel.value = row\n view.value = 'class'\n collectQuery.value = ''\n searchedQuery.value = null\n page.value = 1\n expanded.value = new Set()\n objStack.value = []\n strCache.value = new Map()\n await runCollect(row.target, '')\n}\n\nasync function runCollect(target: CollectTarget, filter: string): Promise<void> {\n if (!hprof.value) return\n collecting.value = true\n progress.value = 0\n try {\n const res = await hprof.value.collect(target, {\n filter: filter || undefined,\n onProgress: (pos, size) => {\n progress.value = size ? pos / size : 1\n },\n })\n collected.value = res.rows\n collectTotal.value = res.total\n collectTruncated.value = res.truncated\n searchedQuery.value = filter ? filter : null\n page.value = 1\n expanded.value = new Set()\n strCache.value = new Map()\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n } finally {\n collecting.value = false\n }\n}\n\nfunction backToOverview(): void {\n view.value = 'overview'\n classSel.value = null\n collected.value = []\n objStack.value = []\n}\n\n// Local filter over the loaded rows (instant). When a whole-file scan is active for the SAME query, its rows\n// are already the matches, so this passes them through.\nconst localFiltered = computed<HprofInstance[]>(() => {\n const q = collectQuery.value.trim().toLowerCase()\n if (!q || q === searchedQuery.value?.toLowerCase()) return collected.value\n return collected.value.filter((o) => rowMatches(o, q))\n})\n\n// Same match as the parser's whole-file filter: id, field `name=value`, or array content (text/hex/elements).\nfunction rowMatches(o: HprofInstance, q: string): boolean {\n if (o.id.includes(q)) return true\n if (o.fields.some((f) => (f.name + '=' + f.value).toLowerCase().includes(q))) return true\n const a = o.array\n if (a) {\n if (a.text && a.text.toLowerCase().includes(q)) return true\n if (a.hex && a.hex.includes(q)) return true\n if (a.elements?.some((e) => e.value.toLowerCase().includes(q))) return true\n }\n return false\n}\n\n// Offer a whole-file scan only when the target overflowed the cap and the current query isn't already scanned.\nconst canWholeFileSearch = computed(\n () =>\n collectTruncated.value &&\n collectQuery.value.trim().length > 0 &&\n collectQuery.value.trim().toLowerCase() !== (searchedQuery.value ?? '').toLowerCase(),\n)\nfunction wholeFileSearch(): void {\n if (classSel.value?.target) void runCollect(classSel.value.target, collectQuery.value.trim())\n}\nfunction clearWholeFileSearch(): void {\n collectQuery.value = ''\n if (classSel.value?.target && searchedQuery.value != null)\n void runCollect(classSel.value.target, '')\n}\n\nconst pageCount = computed(() => Math.max(1, Math.ceil(localFiltered.value.length / PAGE_SIZE)))\nconst pageRows = computed(() => {\n const start = (page.value - 1) * PAGE_SIZE\n return localFiltered.value.slice(start, start + PAGE_SIZE)\n})\n// Keep the page in range as the filtered set shrinks/grows.\nwatch(localFiltered, () => {\n if (page.value > pageCount.value) page.value = pageCount.value\n})\nfunction prevPage(): void {\n if (page.value > 1) page.value--\n}\nfunction nextPage(): void {\n if (page.value < pageCount.value) page.value++\n}\n\nfunction toggleExpand(id: string): void {\n const next = new Set(expanded.value)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n expanded.value = next\n}\n\n// ── String content: dereference each row's `value` byte[]/char[] and decode with its `coder` (lazy, per page) ──\n// A class is \"stringy\" when it's java/lang/String, or looks like it (a `value` object field + a `coder` field).\nconst isStringy = computed(() => {\n const r = collected.value[0]\n if (!r) return false\n const hasValue = r.fields.some(\n (f) => f.name === 'value' && (f.kind === 'ref' || f.kind === 'null'),\n )\n const hasCoder = r.fields.some((f) => f.name === 'coder')\n return hasValue && (classSel.value?.name === STR_CLASS || hasCoder)\n})\nconst strCache = ref<Map<string, string>>(new Map()) // instance id → decoded text\nconst derefBusy = ref(false)\nfunction strText(o: HprofInstance): string | undefined {\n return strCache.value.get(o.id)\n}\n\nasync function derefPage(): Promise<void> {\n if (!hprof.value || !isStringy.value) return\n const todo: { instId: string; refId: string; coder: number | null }[] = []\n for (const o of pageRows.value) {\n if (strCache.value.has(o.id)) continue\n const vf = o.fields.find((f) => f.name === 'value')\n if (!vf || vf.kind !== 'ref' || !vf.refId) {\n // null value ⇒ empty string; no layout ⇒ nothing to show\n strCache.value.set(o.id, vf?.kind === 'null' ? '' : '')\n continue\n }\n const cf = o.fields.find((f) => f.name === 'coder')\n const coder = cf ? parseInt(cf.value, 10) : null\n todo.push({ instId: o.id, refId: vf.refId, coder: Number.isNaN(coder) ? null : coder })\n }\n if (!todo.length) return\n derefBusy.value = true\n try {\n const m = await hprof.value.resolveObjects(new Set(todo.map((t) => t.refId)))\n const next = new Map(strCache.value)\n for (const t of todo) {\n const v = m.get(t.refId)\n next.set(\n t.instId,\n v && v.kind === 'primArray' ? decodeStringValue(v.rawBytes, v.elemType, t.coder) : '',\n )\n }\n strCache.value = next\n } catch {\n // deref is best-effort; leave the raw #id showing on failure\n } finally {\n derefBusy.value = false\n }\n}\nwatch([pageRows, isStringy], () => void derefPage())\n\n// Collapsed one-line preview per row: array content, decoded String, or the first few fields.\nfunction previewLine(o: HprofInstance): string {\n if (o.array) return arrayPreviewLine(o.array)\n if (isStringy.value) {\n const t = strText(o)\n if (t !== undefined) return t === '' ? '\"\"' : `\"${oneLine(clip(t, 160))}\"`\n }\n if (!o.fields.length) return ''\n const head = o.fields.slice(0, 4).map((f) => `${f.name}=${f.value}`)\n return head.join(', ') + (o.fields.length > 4 ? ', …' : '')\n}\nfunction arrayPreviewLine(a: NonNullable<HprofInstance['array']>): string {\n const tag = `(${a.length})`\n if (a.text != null) return `${tag} \"${oneLine(clip(a.text, 160))}\"`\n if (a.elements)\n return (\n `${tag} ` +\n a.elements\n .slice(0, 8)\n .map((e) => e.value)\n .join(', ')\n )\n if (a.hex) return `${tag} ${a.hex}`\n return tag\n}\n\n// ── reference jump: follow a `#id` to the object it points at, with a back stack ─────────────────────\nconst objStack = ref<HprofObjectView[]>([])\nconst objBusy = ref(false)\nconst jumpMsg = ref('')\nconst currentObj = computed<HprofObjectView | null>(() =>\n objStack.value.length ? objStack.value[objStack.value.length - 1] : null,\n)\n\nasync function jumpTo(refId: string | undefined): Promise<void> {\n if (!refId || !hprof.value) return\n jumpMsg.value = ''\n objBusy.value = true\n try {\n const m = await hprof.value.resolveObjects(new Set([refId]))\n const v = m.get(refId)\n if (v) objStack.value = [...objStack.value, v]\n else jumpMsg.value = `#${refId} 未找到(可能已被回收或不在本转储中)`\n } catch (e) {\n jumpMsg.value = e instanceof Error ? e.message : String(e)\n } finally {\n objBusy.value = false\n }\n}\nfunction popObj(): void {\n objStack.value = objStack.value.slice(0, -1)\n jumpMsg.value = ''\n}\nfunction closeObjStack(): void {\n objStack.value = []\n jumpMsg.value = ''\n}\n\n// One-hop annotation of the detail's references (className, or a String's/byte[]'s text) so a jump is informative.\nconst detailAnnots = ref<Map<string, string>>(new Map())\nwatch(currentObj, async (v) => {\n detailAnnots.value = new Map()\n if (!v || !hprof.value) return\n const ids = new Set<string>()\n if (v.kind === 'instance')\n for (const f of v.fields) if (f.kind === 'ref' && f.refId) ids.add(f.refId)\n if (v.kind === 'objArray' && v.array.elements)\n for (const e of v.array.elements) if (e.kind === 'ref' && e.refId) ids.add(e.refId)\n if (!ids.size) return\n const captured = v\n const m = await hprof.value.resolveObjects(ids)\n if (currentObj.value !== captured) return // navigated away while resolving\n const annots = new Map<string, string>()\n for (const [id, ov] of m) annots.set(id, viewSummary(ov))\n detailAnnots.value = annots\n})\nfunction annot(refId: string | undefined): string | undefined {\n return refId ? detailAnnots.value.get(refId) : undefined\n}\nfunction viewSummary(v: HprofObjectView): string {\n if (v.kind === 'class') return 'class ' + v.className\n if (v.kind === 'instance') return v.className\n if (v.kind === 'primArray')\n return v.array.text != null\n ? `${v.className} \"${oneLine(clip(v.array.text, 60))}\"`\n : `${v.className}[${v.length}]`\n return `${v.className.replace(/\\[\\]$/, '')}[${v.length}]` // objArray\n}\n\n// ── small text helpers ───────────────────────────────────────────────────────────────────────────\nfunction clip(s: string, n: number): string {\n return s.length > n ? s.slice(0, n) + '…' : s\n}\nfunction oneLine(s: string): string {\n return s.replace(/\\s+/g, ' ')\n}\n\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => {\n hprof.value = null\n})\n</script>\n\n<template>\n <div class=\"hp\">\n <!-- large-file gate -->\n <div v-if=\"status === 'confirm'\" class=\"hp__confirm\">\n <Icon name=\"code\" :size=\"32\" />\n <p class=\"hp__confirm-title\" :title=\"name\">{{ name }}</p>\n <p class=\"hp__confirm-sub\">\n {{\n size != null ? `文件较大(${formatBytes(size)})` : '文件大小未知'\n }},解析堆转储需将整个文件下载到浏览器(流式解析、内存可控)。\n </p>\n <button type=\"button\" class=\"hp__btn\" @click=\"load\">加载并解析</button>\n </div>\n\n <div v-else-if=\"status === 'loading'\" class=\"hp__progress\">\n <p class=\"hp__progress-label\">正在解析堆转储… {{ pct }}%</p>\n <div class=\"hp__bar\"><div class=\"hp__bar-fill\" :style=\"{ width: pct + '%' }\" /></div>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"hp__msg hp__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"hp__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && summary\">\n <!-- ── object detail (reference jump) — sits above whatever base view is active ── -->\n <template v-if=\"currentObj\">\n <div class=\"hp__crumb\">\n <button type=\"button\" class=\"hp__back\" @click=\"popObj\">\n <Icon name=\"chevronLeft\" :size=\"14\" /> 返回\n </button>\n <span class=\"hp__crumb-name\">\n <span class=\"hp__crumb-cls\">{{ currentObj.className }}</span>\n <span class=\"hp__objid\">#{{ currentObj.id }}</span>\n </span>\n <button\n type=\"button\"\n class=\"hp__ghostbtn hp__crumb-close\"\n title=\"回到列表\"\n @click=\"closeObjStack\"\n >\n 列表\n </button>\n </div>\n\n <div v-if=\"objBusy\" class=\"hp__progress hp__progress--inline\">\n <p class=\"hp__progress-label\">正在解析对象…</p>\n </div>\n <div v-else class=\"hp__list\">\n <!-- instance -->\n <dl v-if=\"currentObj.kind === 'instance'\" class=\"hp__fields hp__fields--detail\">\n <p v-if=\"!currentObj.fields.length\" class=\"hp__nofields\">(无字段或无法解析布局)</p>\n <template v-for=\"(f, fi) in currentObj.fields\" :key=\"fi\">\n <dt class=\"hp__fkey\">{{ f.name }}</dt>\n <dd class=\"hp__fval\" :class=\"{ 'hp__fval--null': f.kind === 'null' }\">\n <template v-if=\"f.kind === 'ref'\">\n <button type=\"button\" class=\"hp__reflink\" @click=\"jumpTo(f.refId)\">\n {{ f.value }}\n </button>\n <span v-if=\"annot(f.refId)\" class=\"hp__annot\">→ {{ annot(f.refId) }}</span>\n </template>\n <span v-else>{{ f.value }}</span>\n </dd>\n </template>\n </dl>\n\n <!-- primitive array -->\n <div v-else-if=\"currentObj.kind === 'primArray'\" class=\"hp__arr\">\n <div class=\"hp__arrmeta\">\n {{ currentObj.className }} · 长度 {{ currentObj.length.toLocaleString() }}\n </div>\n <pre v-if=\"currentObj.array.text != null\" class=\"hp__arrtext\">{{\n currentObj.array.text\n }}</pre>\n <div v-if=\"currentObj.array.hex\" class=\"hp__arrhex\">{{ currentObj.array.hex }}</div>\n <div v-if=\"currentObj.array.elements\" class=\"hp__arrels\">\n <span v-for=\"(e, ei) in currentObj.array.elements\" :key=\"ei\" class=\"hp__arrel\">\n {{ e.value }}\n </span>\n </div>\n <p v-if=\"currentObj.array.truncated\" class=\"hp__nofields\">\n (预览已截断,仅显示前若干项)\n </p>\n </div>\n\n <!-- object array -->\n <div v-else-if=\"currentObj.kind === 'objArray'\" class=\"hp__arr\">\n <div class=\"hp__arrmeta\">\n {{ currentObj.className }} · 长度 {{ currentObj.length.toLocaleString() }}\n </div>\n <div class=\"hp__arrels hp__arrels--obj\">\n <template v-for=\"(e, ei) in currentObj.array.elements ?? []\" :key=\"ei\">\n <button\n v-if=\"e.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--el\"\n @click=\"jumpTo(e.refId)\"\n >\n <span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}\n <span v-if=\"annot(e.refId)\" class=\"hp__annot\">→ {{ annot(e.refId) }}</span>\n </button>\n <span v-else class=\"hp__arrel\"\n ><span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}</span\n >\n </template>\n </div>\n <p v-if=\"currentObj.array.truncated\" class=\"hp__nofields\">\n (预览已截断,仅显示前若干项)\n </p>\n </div>\n\n <!-- class object -->\n <div v-else class=\"hp__arr\">\n <div class=\"hp__arrmeta\">类对象:{{ currentObj.className }}</div>\n <p class=\"hp__nofields\">这是一个 Class 对象。返回列表可在类直方图中查看它的实例。</p>\n </div>\n </div>\n </template>\n\n <!-- ── overview: file meta + class histogram ── -->\n <template v-else-if=\"view === 'overview'\">\n <dl class=\"hp__meta\">\n <template v-for=\"m in metaRows\" :key=\"m.label\">\n <dt class=\"hp__metakey\">{{ m.label }}</dt>\n <dd class=\"hp__metaval\">{{ m.value }}</dd>\n </template>\n </dl>\n\n <div class=\"hp__search\">\n <input v-model=\"overviewQuery\" type=\"search\" class=\"hp__input\" placeholder=\"搜索类名…\" />\n </div>\n\n <div class=\"hp__thead\">\n <button type=\"button\" class=\"hp__th hp__th--name\" @click=\"toggleSort('name')\">\n 类名<span v-if=\"sortKey === 'name'\" class=\"hp__arrow\">{{ sortAsc ? '▲' : '▼' }}</span>\n </button>\n <button type=\"button\" class=\"hp__th hp__th--num\" @click=\"toggleSort('count')\">\n 实例数<span v-if=\"sortKey === 'count'\" class=\"hp__arrow\">{{\n sortAsc ? '▲' : '▼'\n }}</span>\n </button>\n <button type=\"button\" class=\"hp__th hp__th--num\" @click=\"toggleSort('bytes')\">\n Shallow<span v-if=\"sortKey === 'bytes'\" class=\"hp__arrow\">{{\n sortAsc ? '▲' : '▼'\n }}</span>\n </button>\n </div>\n\n <div class=\"hp__list\">\n <p v-if=\"sortedRows.length === 0\" class=\"hp__msg\">无匹配类</p>\n <button\n v-for=\"(row, i) in visibleRows\"\n :key=\"i\"\n type=\"button\"\n class=\"hp__row\"\n :title=\"`查看 ${row.name} 的对象列表`\"\n @click=\"openTarget(row)\"\n >\n <span class=\"hp__name\">{{ row.name }}</span>\n <span class=\"hp__num\">{{ row.count.toLocaleString() }}</span>\n <span class=\"hp__num hp__num--bytes\">{{ formatBytes(row.bytes) }}</span>\n <Icon name=\"chevronRight\" :size=\"14\" class=\"hp__chev\" />\n </button>\n <p v-if=\"tableTruncated\" class=\"hp__more\">\n 仅显示前 {{ TABLE_CAP }} 个类,输入关键词缩小范围(共 {{ sortedRows.length }} 个)\n </p>\n </div>\n </template>\n\n <!-- ── drill-in: paginated object list ── -->\n <template v-else-if=\"classSel\">\n <div class=\"hp__crumb\">\n <button type=\"button\" class=\"hp__back\" @click=\"backToOverview\">\n <Icon name=\"chevronLeft\" :size=\"14\" /> 类列表\n </button>\n <span class=\"hp__crumb-name\" :title=\"classSel.name\">{{ classSel.name }}</span>\n </div>\n\n <div class=\"hp__classbar\">\n <span class=\"hp__classcount\">\n 共 {{ collectTotal.toLocaleString() }} 个实例<template v-if=\"searchedQuery\">\n 匹配「{{ searchedQuery }}」</template\n >\n </span>\n <span\n v-if=\"collectTruncated\"\n class=\"hp__tag\"\n title=\"为控制内存只加载了前若干个;用搜索缩小范围\"\n >\n 已加载前 {{ collected.length.toLocaleString() }} 个\n </span>\n <span v-if=\"derefBusy\" class=\"hp__tag\">解析字符串…</span>\n </div>\n\n <div class=\"hp__search hp__search--row\">\n <input\n v-model=\"collectQuery\"\n type=\"search\"\n class=\"hp__input\"\n placeholder=\"过滤 id / 字段值 / 内容…\"\n @keydown.enter=\"canWholeFileSearch && wholeFileSearch()\"\n />\n <button\n v-if=\"searchedQuery\"\n type=\"button\"\n class=\"hp__ghostbtn\"\n title=\"清除全文搜索\"\n @click=\"clearWholeFileSearch\"\n >\n 清除\n </button>\n </div>\n\n <p v-if=\"canWholeFileSearch\" class=\"hp__hint\">\n 过滤仅作用于已加载的 {{ collected.length.toLocaleString() }} 个。\n <button type=\"button\" class=\"hp__link\" @click=\"wholeFileSearch\">\n 在全部 {{ collectTotal.toLocaleString() }} 个实例中搜索「{{ collectQuery.trim() }}」\n </button>\n </p>\n\n <p v-if=\"jumpMsg\" class=\"hp__jumpmsg\">{{ jumpMsg }}</p>\n\n <div v-if=\"collecting\" class=\"hp__progress hp__progress--inline\">\n <p class=\"hp__progress-label\">正在扫描对象… {{ pct }}%</p>\n <div class=\"hp__bar\"><div class=\"hp__bar-fill\" :style=\"{ width: pct + '%' }\" /></div>\n </div>\n\n <template v-else>\n <div class=\"hp__list\">\n <p v-if=\"localFiltered.length === 0\" class=\"hp__msg\">无匹配对象</p>\n <div v-for=\"o in pageRows\" :key=\"o.id\" class=\"hp__obj\">\n <button type=\"button\" class=\"hp__objhead\" @click=\"toggleExpand(o.id)\">\n <Icon\n :name=\"expanded.has(o.id) ? 'chevronDown' : 'chevronRight'\"\n :size=\"13\"\n class=\"hp__objchev\"\n />\n <span class=\"hp__objid\">#{{ o.id }}</span>\n <span class=\"hp__objsize\">{{ formatBytes(o.size) }}</span>\n <span class=\"hp__objprev\">{{ previewLine(o) }}</span>\n </button>\n\n <div v-if=\"expanded.has(o.id)\" class=\"hp__detail\">\n <!-- array content -->\n <div v-if=\"o.array\" class=\"hp__arr\">\n <pre v-if=\"o.array.text != null\" class=\"hp__arrtext\">{{ o.array.text }}</pre>\n <div v-if=\"o.array.hex\" class=\"hp__arrhex\">{{ o.array.hex }}</div>\n <div\n v-if=\"o.array.elements\"\n class=\"hp__arrels\"\n :class=\"{ 'hp__arrels--obj': o.array.elemClassName }\"\n >\n <template v-for=\"(e, ei) in o.array.elements\" :key=\"ei\">\n <button\n v-if=\"e.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--el\"\n @click=\"jumpTo(e.refId)\"\n >\n <span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}\n </button>\n <span v-else class=\"hp__arrel\"\n ><span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}</span\n >\n </template>\n </div>\n <p v-if=\"o.array.truncated\" class=\"hp__nofields\">\n (预览已截断,完整长度 {{ o.array.length.toLocaleString() }})\n </p>\n </div>\n\n <!-- instance fields -->\n <dl v-else class=\"hp__fields\">\n <p v-if=\"!o.fields.length\" class=\"hp__nofields\">\n (无法解析字段布局,仅显示 id 与大小)\n </p>\n <template v-for=\"(f, fi) in o.fields\" :key=\"fi\">\n <dt class=\"hp__fkey\">{{ f.name }}</dt>\n <dd class=\"hp__fval\" :class=\"{ 'hp__fval--null': f.kind === 'null' }\">\n <template v-if=\"isStringy && f.name === 'value' && strText(o) !== undefined\">\n <span class=\"hp__str\">\"{{ strText(o) }}\"</span>\n <button\n v-if=\"f.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--sm\"\n @click=\"jumpTo(f.refId)\"\n >\n {{ f.value }}\n </button>\n </template>\n <template v-else-if=\"f.kind === 'ref'\">\n <button type=\"button\" class=\"hp__reflink\" @click=\"jumpTo(f.refId)\">\n {{ f.value }}\n </button>\n </template>\n <span v-else>{{ f.value }}</span>\n </dd>\n </template>\n </dl>\n </div>\n </div>\n </div>\n\n <div v-if=\"pageCount > 1\" class=\"hp__pager\">\n <button type=\"button\" class=\"hp__pagebtn\" :disabled=\"page <= 1\" @click=\"prevPage\">\n <Icon name=\"chevronLeft\" :size=\"14\" />\n </button>\n <span class=\"hp__pageinfo\">第 {{ page }} / {{ pageCount }} 页</span>\n <button\n type=\"button\"\n class=\"hp__pagebtn\"\n :disabled=\"page >= pageCount\"\n @click=\"nextPage\"\n >\n <Icon name=\"chevronRight\" :size=\"14\" />\n </button>\n </div>\n </template>\n </template>\n </template>\n </div>\n</template>\n\n<style scoped>\n.hp {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.hp__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.hp__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.hp__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.hp__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Large-file confirm card ------------------------------------------------------------------------- */\n.hp__confirm {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px 28px;\n text-align: center;\n color: var(--muted);\n}\n.hp__confirm-title {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.hp__confirm-sub {\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.hp__btn {\n margin-top: 4px;\n font-family: inherit;\n font-size: 13px;\n color: #fff;\n background: var(--accent);\n border: none;\n border-radius: 8px;\n padding: 8px 18px;\n cursor: pointer;\n}\n.hp__btn:hover {\n filter: brightness(1.06);\n}\n\n/* Progress bar (parse + scan) --------------------------------------------------------------------- */\n.hp__progress {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n.hp__progress--inline {\n flex: 0 0 auto;\n padding: 28px 24px;\n}\n.hp__progress-label {\n font-size: 12.5px;\n color: var(--muted);\n}\n.hp__bar {\n width: min(280px, 70%);\n height: 6px;\n border-radius: 999px;\n background: var(--surface-2);\n overflow: hidden;\n}\n.hp__bar-fill {\n height: 100%;\n background: var(--accent);\n transition: width 0.15s ease-out;\n}\n\n/* File meta grid ---------------------------------------------------------------------------------- */\n.hp__meta {\n flex-shrink: 0;\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);\n gap: 4px 12px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n}\n.hp__metakey {\n color: var(--faint);\n font-size: 11px;\n white-space: nowrap;\n}\n.hp__metaval {\n margin: 0;\n min-width: 0;\n color: var(--text);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n overflow-wrap: anywhere;\n}\n\n/* Search ------------------------------------------------------------------------------------------ */\n.hp__search {\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.hp__search--row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.hp__input {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.hp__input:focus {\n outline: none;\n border-color: var(--accent);\n}\n.hp__ghostbtn {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.hp__ghostbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Histogram table --------------------------------------------------------------------------------- */\n.hp__thead {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 14px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n}\n.hp__th {\n font-family: inherit;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--faint);\n background: transparent;\n border: none;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n.hp__th:hover {\n color: var(--accent);\n}\n.hp__th--name {\n flex: 1 1 auto;\n justify-content: flex-start;\n}\n.hp__th--num {\n flex-shrink: 0;\n width: 76px;\n justify-content: flex-end;\n}\n.hp__arrow {\n font-size: 8px;\n}\n\n.hp__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 2px 0;\n}\n.hp__row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 5px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.hp__row:hover {\n background: var(--surface-2);\n}\n.hp__name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.hp__num {\n flex-shrink: 0;\n width: 76px;\n text-align: right;\n font-size: 12px;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.hp__num--bytes {\n color: var(--faint);\n}\n.hp__chev {\n flex-shrink: 0;\n color: var(--faint);\n}\n.hp__more {\n padding: 10px 16px;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n/* Breadcrumb + class bar -------------------------------------------------------------------------- */\n.hp__crumb {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.hp__back {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n gap: 2px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n padding: 4px 9px 4px 6px;\n cursor: pointer;\n}\n.hp__back:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.hp__crumb-name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text-strong);\n}\n.hp__crumb-cls {\n color: var(--text-strong);\n}\n.hp__crumb-close {\n margin-left: auto;\n}\n.hp__classbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 7px 14px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.hp__classcount {\n font-variant-numeric: tabular-nums;\n}\n.hp__tag {\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--muted);\n font-size: 10.5px;\n}\n.hp__hint {\n flex-shrink: 0;\n padding: 8px 14px;\n font-size: 11.5px;\n line-height: 1.6;\n color: var(--muted);\n background: var(--surface-2);\n}\n.hp__link {\n font-family: inherit;\n font-size: 11.5px;\n color: var(--accent);\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n}\n.hp__jumpmsg {\n flex-shrink: 0;\n padding: 7px 14px;\n font-size: 11.5px;\n color: var(--danger);\n background: var(--surface-2);\n}\n\n/* Object list ------------------------------------------------------------------------------------- */\n.hp__obj {\n border-bottom: 1px solid var(--border);\n}\n.hp__objhead {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.hp__objhead:hover {\n background: var(--surface-2);\n}\n.hp__objchev {\n flex-shrink: 0;\n color: var(--faint);\n}\n.hp__objid {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n}\n.hp__objsize {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.hp__objprev {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--muted);\n}\n.hp__detail {\n background: var(--surface-2);\n}\n.hp__fields {\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 3px 14px;\n padding: 4px 16px 10px 34px;\n background: var(--surface-2);\n}\n.hp__fields--detail {\n padding: 10px 16px;\n}\n.hp__nofields {\n grid-column: 1 / -1;\n margin: 0;\n font-size: 11.5px;\n color: var(--faint);\n}\n.hp__fkey {\n color: var(--faint);\n font-family: var(--font-mono);\n font-size: 11.5px;\n white-space: nowrap;\n}\n.hp__fval {\n margin: 0;\n min-width: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text);\n overflow-wrap: anywhere;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 6px;\n}\n.hp__fval--null {\n color: var(--faint);\n font-style: italic;\n}\n.hp__str {\n color: var(--text-strong);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n/* Reference links --------------------------------------------------------------------------------- */\n.hp__reflink {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.hp__reflink:hover {\n filter: brightness(1.15);\n}\n.hp__reflink--sm {\n font-size: 10.5px;\n opacity: 0.75;\n}\n.hp__reflink--el {\n display: inline-block;\n text-decoration: none;\n padding: 2px 6px;\n margin: 2px;\n border: 1px solid var(--border);\n border-radius: 6px;\n background: var(--surface);\n}\n.hp__reflink--el:hover {\n border-color: var(--accent);\n}\n.hp__annot {\n color: var(--faint);\n font-size: 10.5px;\n font-style: italic;\n}\n\n/* Array content ----------------------------------------------------------------------------------- */\n.hp__arr {\n padding: 8px 16px 12px 34px;\n}\n.hp__fields--detail + .hp__arr,\n.hp__crumb ~ .hp__list .hp__arr {\n padding-left: 16px;\n}\n.hp__arrmeta {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n margin-bottom: 6px;\n}\n.hp__arrtext {\n margin: 0 0 6px;\n max-height: 260px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 6px;\n background: var(--surface);\n border: 1px solid var(--border);\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.hp__arrhex {\n margin-bottom: 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n overflow-wrap: anywhere;\n line-height: 1.6;\n}\n.hp__arrels {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 10px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text);\n}\n.hp__arrels--obj {\n gap: 2px;\n}\n.hp__arrel {\n color: var(--text);\n}\n.hp__eli {\n color: var(--faint);\n}\n\n/* Pager ------------------------------------------------------------------------------------------- */\n.hp__pager {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 8px 14px;\n border-top: 1px solid var(--border);\n}\n.hp__pagebtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 26px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n cursor: pointer;\n}\n.hp__pagebtn:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.hp__pagebtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.hp__pageinfo {\n font-size: 12px;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HprofViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/HprofViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport {\n Hprof,\n HprofParseError,\n decodeStringValue,\n type CollectTarget,\n type HprofClassRow,\n type HprofInstance,\n type HprofObjectView,\n} from '@/lib/hprof'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's HPROF (JVM heap dump) mode: fetch a `.hprof`, stream-parse it in-browser (see lib/hprof.ts),\n * and show a class histogram (instances + shallow size) — then drill into ONE class (or array type) to page\n * through its objects with decoded fields / array content. Mirrors BinObjBrowser: the bytes come from the same\n * download endpoint and never leave the browser.\n *\n * Memory is the whole design constraint (a heap dump can be many GB):\n * - Parsing never loads the file into the JS heap — the Blob is streamed in 8 MB chunks, and only bounded\n * per-class state is kept (Pass A in lib/hprof.ts).\n * - Drilling into a target collects at most a fixed cap of object rows (MAX_COLLECT) via Pass B, then PAGES that\n * cap — so a class with millions of instances still shows instantly. When it overflows the cap, the on-screen\n * filter is local to the loaded rows; a \"search the whole file\" action re-streams with the filter pushed down.\n * - Following a reference (jumping to `#id`) or reading a String's real text both use `resolveObjects`: an\n * on-demand streaming sweep that resolves just the requested ids, bounded to what's on screen. No id→offset\n * index is built, so memory stays ∝ the visible batch, not the object count.\n *\n * Large-file gate (like BinObjBrowser): unless the manifest size is known-small we don't auto-download — a\n * confirm card explains that parsing pulls the whole file, and only fetches on click.\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\nconst LARGE_THRESHOLD = 20 * 1024 * 1024 // auto-load ≤ 20 MB; larger (or unknown) asks first\nconst TABLE_CAP = 2000 // max histogram rows rendered at once (filter to narrow past this)\nconst PAGE_SIZE = 50 // objects per page in the drill-in\nconst STR_CLASS = 'java/lang/String'\n\ntype Status = 'confirm' | 'loading' | 'error' | 'ready'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst progress = ref(0) // 0..1, drives the parse/scan bar\nconst hprof = ref<Hprof | null>(null)\n\nconst view = ref<'overview' | 'class'>('overview')\n\n// ── load + parse ──────────────────────────────────────────────────────────────────────────────────\nfunction decideInitial(): void {\n hprof.value = null\n view.value = 'overview'\n overviewQuery.value = ''\n errorMsg.value = ''\n progress.value = 0\n objStack.value = []\n if (props.size != null && props.size <= LARGE_THRESHOLD) void load()\n else status.value = 'confirm'\n}\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n progress.value = 0\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const blob = await res.blob()\n hprof.value = await Hprof.parse(blob, (pos, size) => {\n progress.value = size ? pos / size : 1\n })\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof HprofParseError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nconst pct = computed(() => Math.round(progress.value * 100))\n\n// ── overview: file meta + class histogram ───────────────────────────────────────────────────────────\nconst summary = computed(() => hprof.value?.summary ?? null)\n\nconst metaRows = computed<{ label: string; value: string }[]>(() => {\n const s = summary.value\n if (!s) return []\n const ts = Number(s.timestamp)\n return [\n { label: '对象总数', value: s.instanceCount.toLocaleString() },\n { label: '类数量', value: s.classCount.toLocaleString() },\n { label: 'Shallow 大小', value: formatBytes(s.heapBytes) },\n { label: 'GC Roots', value: s.gcRoots.toLocaleString() },\n { label: '字符串常量', value: s.stringCount.toLocaleString() },\n { label: 'ID 宽度', value: `${s.idSize} 字节` },\n { label: '转储时间', value: ts > 0 ? new Date(ts).toLocaleString() : '—' },\n { label: '文件大小', value: formatBytes(s.fileSize) },\n ]\n})\n\nconst overviewQuery = ref('')\ntype SortKey = 'bytes' | 'count' | 'name'\nconst sortKey = ref<SortKey>('bytes')\nconst sortAsc = ref(false)\n\nfunction toggleSort(key: SortKey): void {\n if (sortKey.value === key) sortAsc.value = !sortAsc.value\n else {\n sortKey.value = key\n sortAsc.value = key === 'name' // names default A→Z, sizes/counts default high→low\n }\n}\n\nconst sortedRows = computed<HprofClassRow[]>(() => {\n const rows = summary.value?.rows ?? []\n const q = overviewQuery.value.trim().toLowerCase()\n const filtered = q ? rows.filter((r) => r.name.toLowerCase().includes(q)) : rows\n const dir = sortAsc.value ? 1 : -1\n const key = sortKey.value\n // Copy before sort so we never mutate the parser's array.\n return [...filtered].sort((a, b) => {\n if (key === 'name') return dir * a.name.localeCompare(b.name)\n if (key === 'count') return dir * (a.count - b.count) || b.bytes - a.bytes\n return dir * (a.bytes - b.bytes) || b.count - a.count\n })\n})\nconst visibleRows = computed(() => sortedRows.value.slice(0, TABLE_CAP))\nconst tableTruncated = computed(() => sortedRows.value.length > TABLE_CAP)\n\n// ── drill-in (class OR array) ─────────────────────────────────────────────────────────────────────\nconst classSel = ref<HprofClassRow | null>(null)\nconst collecting = ref(false)\nconst collected = ref<HprofInstance[]>([])\nconst collectTotal = ref(0)\nconst collectTruncated = ref(false)\nconst collectQuery = ref('') // the class-view filter box\nconst searchedQuery = ref<string | null>(null) // the query a whole-file scan currently reflects\nconst page = ref(1)\nconst expanded = ref<Set<string>>(new Set())\n\nasync function openTarget(row: HprofClassRow): Promise<void> {\n if (!row.target || !hprof.value) return\n classSel.value = row\n view.value = 'class'\n collectQuery.value = ''\n searchedQuery.value = null\n page.value = 1\n expanded.value = new Set()\n objStack.value = []\n strCache.value = new Map()\n await runCollect(row.target, '')\n}\n\nasync function runCollect(target: CollectTarget, filter: string): Promise<void> {\n if (!hprof.value) return\n collecting.value = true\n progress.value = 0\n try {\n const res = await hprof.value.collect(target, {\n filter: filter || undefined,\n onProgress: (pos, size) => {\n progress.value = size ? pos / size : 1\n },\n })\n collected.value = res.rows\n collectTotal.value = res.total\n collectTruncated.value = res.truncated\n searchedQuery.value = filter ? filter : null\n page.value = 1\n expanded.value = new Set()\n strCache.value = new Map()\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n } finally {\n collecting.value = false\n }\n}\n\nfunction backToOverview(): void {\n view.value = 'overview'\n classSel.value = null\n collected.value = []\n objStack.value = []\n}\n\n// Local filter over the loaded rows (instant). When a whole-file scan is active for the SAME query, its rows\n// are already the matches, so this passes them through.\nconst localFiltered = computed<HprofInstance[]>(() => {\n const q = collectQuery.value.trim().toLowerCase()\n if (!q || q === searchedQuery.value?.toLowerCase()) return collected.value\n return collected.value.filter((o) => rowMatches(o, q))\n})\n\n// Same match as the parser's whole-file filter: id, field `name=value`, or array content (text/hex/elements).\nfunction rowMatches(o: HprofInstance, q: string): boolean {\n if (o.id.includes(q)) return true\n if (o.fields.some((f) => (f.name + '=' + f.value).toLowerCase().includes(q))) return true\n const a = o.array\n if (a) {\n if (a.text && a.text.toLowerCase().includes(q)) return true\n if (a.hex && a.hex.includes(q)) return true\n if (a.elements?.some((e) => e.value.toLowerCase().includes(q))) return true\n }\n return false\n}\n\n// Offer a whole-file scan only when the target overflowed the cap and the current query isn't already scanned.\nconst canWholeFileSearch = computed(\n () =>\n collectTruncated.value &&\n collectQuery.value.trim().length > 0 &&\n collectQuery.value.trim().toLowerCase() !== (searchedQuery.value ?? '').toLowerCase(),\n)\nfunction wholeFileSearch(): void {\n if (classSel.value?.target) void runCollect(classSel.value.target, collectQuery.value.trim())\n}\nfunction clearWholeFileSearch(): void {\n collectQuery.value = ''\n if (classSel.value?.target && searchedQuery.value != null)\n void runCollect(classSel.value.target, '')\n}\n\nconst pageCount = computed(() => Math.max(1, Math.ceil(localFiltered.value.length / PAGE_SIZE)))\nconst pageRows = computed(() => {\n const start = (page.value - 1) * PAGE_SIZE\n return localFiltered.value.slice(start, start + PAGE_SIZE)\n})\n// Keep the page in range as the filtered set shrinks/grows.\nwatch(localFiltered, () => {\n if (page.value > pageCount.value) page.value = pageCount.value\n})\nfunction prevPage(): void {\n if (page.value > 1) page.value--\n}\nfunction nextPage(): void {\n if (page.value < pageCount.value) page.value++\n}\n\nfunction toggleExpand(id: string): void {\n const next = new Set(expanded.value)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n expanded.value = next\n}\n\n// ── String content: dereference each row's `value` byte[]/char[] and decode with its `coder` (lazy, per page) ──\n// A class is \"stringy\" when it's java/lang/String, or looks like it (a `value` object field + a `coder` field).\nconst isStringy = computed(() => {\n const r = collected.value[0]\n if (!r) return false\n const hasValue = r.fields.some(\n (f) => f.name === 'value' && (f.kind === 'ref' || f.kind === 'null'),\n )\n const hasCoder = r.fields.some((f) => f.name === 'coder')\n return hasValue && (classSel.value?.name === STR_CLASS || hasCoder)\n})\nconst strCache = ref<Map<string, string>>(new Map()) // instance id → decoded text\nconst derefBusy = ref(false)\nfunction strText(o: HprofInstance): string | undefined {\n return strCache.value.get(o.id)\n}\n\nasync function derefPage(): Promise<void> {\n if (!hprof.value || !isStringy.value) return\n const todo: { instId: string; refId: string; coder: number | null }[] = []\n for (const o of pageRows.value) {\n if (strCache.value.has(o.id)) continue\n const vf = o.fields.find((f) => f.name === 'value')\n if (!vf || vf.kind !== 'ref' || !vf.refId) {\n // null value ⇒ empty string; no layout ⇒ nothing to show\n strCache.value.set(o.id, vf?.kind === 'null' ? '' : '')\n continue\n }\n const cf = o.fields.find((f) => f.name === 'coder')\n const coder = cf ? parseInt(cf.value, 10) : null\n todo.push({ instId: o.id, refId: vf.refId, coder: Number.isNaN(coder) ? null : coder })\n }\n if (!todo.length) return\n derefBusy.value = true\n try {\n const m = await hprof.value.resolveObjects(new Set(todo.map((t) => t.refId)))\n const next = new Map(strCache.value)\n for (const t of todo) {\n const v = m.get(t.refId)\n next.set(\n t.instId,\n v && v.kind === 'primArray' ? decodeStringValue(v.rawBytes, v.elemType, t.coder) : '',\n )\n }\n strCache.value = next\n } catch {\n // deref is best-effort; leave the raw #id showing on failure\n } finally {\n derefBusy.value = false\n }\n}\nwatch([pageRows, isStringy], () => void derefPage())\n\n// Collapsed one-line preview per row: array content, decoded String, or the first few fields.\nfunction previewLine(o: HprofInstance): string {\n if (o.array) return arrayPreviewLine(o.array)\n if (isStringy.value) {\n const t = strText(o)\n if (t !== undefined) return t === '' ? '\"\"' : `\"${oneLine(clip(t, 160))}\"`\n }\n if (!o.fields.length) return ''\n const head = o.fields.slice(0, 4).map((f) => `${f.name}=${f.value}`)\n return head.join(', ') + (o.fields.length > 4 ? ', …' : '')\n}\nfunction arrayPreviewLine(a: NonNullable<HprofInstance['array']>): string {\n const tag = `(${a.length})`\n if (a.text != null) return `${tag} \"${oneLine(clip(a.text, 160))}\"`\n if (a.elements)\n return (\n `${tag} ` +\n a.elements\n .slice(0, 8)\n .map((e) => e.value)\n .join(', ')\n )\n if (a.hex) return `${tag} ${a.hex}`\n return tag\n}\n\n// ── reference jump: follow a `#id` to the object it points at, with a back stack ─────────────────────\nconst objStack = ref<HprofObjectView[]>([])\nconst objBusy = ref(false)\nconst jumpMsg = ref('')\nconst currentObj = computed<HprofObjectView | null>(() =>\n objStack.value.length ? objStack.value[objStack.value.length - 1] : null,\n)\n\nasync function jumpTo(refId: string | undefined): Promise<void> {\n if (!refId || !hprof.value) return\n jumpMsg.value = ''\n objBusy.value = true\n try {\n const m = await hprof.value.resolveObjects(new Set([refId]))\n const v = m.get(refId)\n if (v) objStack.value = [...objStack.value, v]\n else jumpMsg.value = `#${refId} 未找到(可能已被回收或不在本转储中)`\n } catch (e) {\n jumpMsg.value = e instanceof Error ? e.message : String(e)\n } finally {\n objBusy.value = false\n }\n}\nfunction popObj(): void {\n objStack.value = objStack.value.slice(0, -1)\n jumpMsg.value = ''\n}\nfunction closeObjStack(): void {\n objStack.value = []\n jumpMsg.value = ''\n}\n\n// One-hop annotation of the detail's references (className, or a String's/byte[]'s text) so a jump is informative.\nconst detailAnnots = ref<Map<string, string>>(new Map())\nwatch(currentObj, async (v) => {\n detailAnnots.value = new Map()\n if (!v || !hprof.value) return\n const ids = new Set<string>()\n if (v.kind === 'instance')\n for (const f of v.fields) if (f.kind === 'ref' && f.refId) ids.add(f.refId)\n if (v.kind === 'objArray' && v.array.elements)\n for (const e of v.array.elements) if (e.kind === 'ref' && e.refId) ids.add(e.refId)\n if (!ids.size) return\n const captured = v\n const m = await hprof.value.resolveObjects(ids)\n if (currentObj.value !== captured) return // navigated away while resolving\n const annots = new Map<string, string>()\n for (const [id, ov] of m) annots.set(id, viewSummary(ov))\n detailAnnots.value = annots\n})\nfunction annot(refId: string | undefined): string | undefined {\n return refId ? detailAnnots.value.get(refId) : undefined\n}\nfunction viewSummary(v: HprofObjectView): string {\n if (v.kind === 'class') return 'class ' + v.className\n if (v.kind === 'instance') return v.className\n if (v.kind === 'primArray')\n return v.array.text != null\n ? `${v.className} \"${oneLine(clip(v.array.text, 60))}\"`\n : `${v.className}[${v.length}]`\n return `${v.className.replace(/\\[\\]$/, '')}[${v.length}]` // objArray\n}\n\n// ── small text helpers ───────────────────────────────────────────────────────────────────────────\nfunction clip(s: string, n: number): string {\n return s.length > n ? s.slice(0, n) + '…' : s\n}\nfunction oneLine(s: string): string {\n return s.replace(/\\s+/g, ' ')\n}\n\nonMounted(decideInitial)\nwatch(() => props.url, decideInitial)\nonBeforeUnmount(() => {\n hprof.value = null\n})\n</script>\n\n<template>\n <div class=\"hp\">\n <!-- large-file gate -->\n <div v-if=\"status === 'confirm'\" class=\"hp__confirm\">\n <Icon name=\"code\" :size=\"32\" />\n <p class=\"hp__confirm-title\" :title=\"name\">{{ name }}</p>\n <p class=\"hp__confirm-sub\">\n {{\n size != null ? `文件较大(${formatBytes(size)})` : '文件大小未知'\n }},解析堆转储需将整个文件下载到浏览器(流式解析、内存可控)。\n </p>\n <button type=\"button\" class=\"hp__btn\" @click=\"load\">加载并解析</button>\n </div>\n\n <div v-else-if=\"status === 'loading'\" class=\"hp__progress\">\n <p class=\"hp__progress-label\">正在解析堆转储… {{ pct }}%</p>\n <div class=\"hp__bar\"><div class=\"hp__bar-fill\" :style=\"{ width: pct + '%' }\" /></div>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"hp__msg hp__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"hp__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && summary\">\n <!-- ── object detail (reference jump) — sits above whatever base view is active ── -->\n <template v-if=\"currentObj\">\n <div class=\"hp__crumb\">\n <button type=\"button\" class=\"hp__back\" @click=\"popObj\">\n <Icon name=\"chevronLeft\" :size=\"14\" /> 返回\n </button>\n <span class=\"hp__crumb-name\">\n <span class=\"hp__crumb-cls\">{{ currentObj.className }}</span>\n <span class=\"hp__objid\">#{{ currentObj.id }}</span>\n </span>\n <button\n type=\"button\"\n class=\"hp__ghostbtn hp__crumb-close\"\n title=\"回到列表\"\n @click=\"closeObjStack\"\n >\n 列表\n </button>\n </div>\n\n <div v-if=\"objBusy\" class=\"hp__progress hp__progress--inline\">\n <p class=\"hp__progress-label\">正在解析对象…</p>\n </div>\n <div v-else class=\"hp__list\">\n <!-- instance -->\n <dl v-if=\"currentObj.kind === 'instance'\" class=\"hp__fields hp__fields--detail\">\n <p v-if=\"!currentObj.fields.length\" class=\"hp__nofields\">(无字段或无法解析布局)</p>\n <template v-for=\"(f, fi) in currentObj.fields\" :key=\"fi\">\n <dt class=\"hp__fkey\">{{ f.name }}</dt>\n <dd class=\"hp__fval\" :class=\"{ 'hp__fval--null': f.kind === 'null' }\">\n <template v-if=\"f.kind === 'ref'\">\n <button type=\"button\" class=\"hp__reflink\" @click=\"jumpTo(f.refId)\">\n {{ f.value }}\n </button>\n <span v-if=\"annot(f.refId)\" class=\"hp__annot\">→ {{ annot(f.refId) }}</span>\n </template>\n <span v-else>{{ f.value }}</span>\n </dd>\n </template>\n </dl>\n\n <!-- primitive array -->\n <div v-else-if=\"currentObj.kind === 'primArray'\" class=\"hp__arr\">\n <div class=\"hp__arrmeta\">\n {{ currentObj.className }} · 长度 {{ currentObj.length.toLocaleString() }}\n </div>\n <pre v-if=\"currentObj.array.text != null\" class=\"hp__arrtext\">{{\n currentObj.array.text\n }}</pre>\n <div v-if=\"currentObj.array.hex\" class=\"hp__arrhex\">{{ currentObj.array.hex }}</div>\n <div v-if=\"currentObj.array.elements\" class=\"hp__arrels\">\n <span v-for=\"(e, ei) in currentObj.array.elements\" :key=\"ei\" class=\"hp__arrel\">\n {{ e.value }}\n </span>\n </div>\n <p v-if=\"currentObj.array.truncated\" class=\"hp__nofields\">\n (预览已截断,仅显示前若干项)\n </p>\n </div>\n\n <!-- object array -->\n <div v-else-if=\"currentObj.kind === 'objArray'\" class=\"hp__arr\">\n <div class=\"hp__arrmeta\">\n {{ currentObj.className }} · 长度 {{ currentObj.length.toLocaleString() }}\n </div>\n <div class=\"hp__arrels hp__arrels--obj\">\n <template v-for=\"(e, ei) in currentObj.array.elements ?? []\" :key=\"ei\">\n <button\n v-if=\"e.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--el\"\n @click=\"jumpTo(e.refId)\"\n >\n <span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}\n <span v-if=\"annot(e.refId)\" class=\"hp__annot\">→ {{ annot(e.refId) }}</span>\n </button>\n <span v-else class=\"hp__arrel\"\n ><span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}</span\n >\n </template>\n </div>\n <p v-if=\"currentObj.array.truncated\" class=\"hp__nofields\">\n (预览已截断,仅显示前若干项)\n </p>\n </div>\n\n <!-- class object -->\n <div v-else class=\"hp__arr\">\n <div class=\"hp__arrmeta\">类对象:{{ currentObj.className }}</div>\n <p class=\"hp__nofields\">这是一个 Class 对象。返回列表可在类直方图中查看它的实例。</p>\n </div>\n </div>\n </template>\n\n <!-- ── overview: file meta + class histogram ── -->\n <template v-else-if=\"view === 'overview'\">\n <dl class=\"hp__meta\">\n <template v-for=\"m in metaRows\" :key=\"m.label\">\n <dt class=\"hp__metakey\">{{ m.label }}</dt>\n <dd class=\"hp__metaval\">{{ m.value }}</dd>\n </template>\n </dl>\n\n <div class=\"hp__search\">\n <input v-model=\"overviewQuery\" type=\"search\" class=\"hp__input\" placeholder=\"搜索类名…\" />\n </div>\n\n <div class=\"hp__thead\">\n <button type=\"button\" class=\"hp__th hp__th--name\" @click=\"toggleSort('name')\">\n 类名<span v-if=\"sortKey === 'name'\" class=\"hp__arrow\">{{ sortAsc ? '▲' : '▼' }}</span>\n </button>\n <button type=\"button\" class=\"hp__th hp__th--num\" @click=\"toggleSort('count')\">\n 实例数<span v-if=\"sortKey === 'count'\" class=\"hp__arrow\">{{\n sortAsc ? '▲' : '▼'\n }}</span>\n </button>\n <button type=\"button\" class=\"hp__th hp__th--num\" @click=\"toggleSort('bytes')\">\n Shallow<span v-if=\"sortKey === 'bytes'\" class=\"hp__arrow\">{{\n sortAsc ? '▲' : '▼'\n }}</span>\n </button>\n </div>\n\n <div class=\"hp__list\">\n <p v-if=\"sortedRows.length === 0\" class=\"hp__msg\">无匹配类</p>\n <button\n v-for=\"(row, i) in visibleRows\"\n :key=\"i\"\n type=\"button\"\n class=\"hp__row\"\n :title=\"`查看 ${row.name} 的对象列表`\"\n @click=\"openTarget(row)\"\n >\n <span class=\"hp__name\">{{ row.name }}</span>\n <span class=\"hp__num\">{{ row.count.toLocaleString() }}</span>\n <span class=\"hp__num hp__num--bytes\">{{ formatBytes(row.bytes) }}</span>\n <Icon name=\"chevronRight\" :size=\"14\" class=\"hp__chev\" />\n </button>\n <p v-if=\"tableTruncated\" class=\"hp__more\">\n 仅显示前 {{ TABLE_CAP }} 个类,输入关键词缩小范围(共 {{ sortedRows.length }} 个)\n </p>\n </div>\n </template>\n\n <!-- ── drill-in: paginated object list ── -->\n <template v-else-if=\"classSel\">\n <div class=\"hp__crumb\">\n <button type=\"button\" class=\"hp__back\" @click=\"backToOverview\">\n <Icon name=\"chevronLeft\" :size=\"14\" /> 类列表\n </button>\n <span class=\"hp__crumb-name\" :title=\"classSel.name\">{{ classSel.name }}</span>\n </div>\n\n <div class=\"hp__classbar\">\n <span class=\"hp__classcount\">\n 共 {{ collectTotal.toLocaleString() }} 个实例<template v-if=\"searchedQuery\">\n 匹配「{{ searchedQuery }}」</template\n >\n </span>\n <span\n v-if=\"collectTruncated\"\n class=\"hp__tag\"\n title=\"为控制内存只加载了前若干个;用搜索缩小范围\"\n >\n 已加载前 {{ collected.length.toLocaleString() }} 个\n </span>\n <span v-if=\"derefBusy\" class=\"hp__tag\">解析字符串…</span>\n </div>\n\n <div class=\"hp__search hp__search--row\">\n <input\n v-model=\"collectQuery\"\n type=\"search\"\n class=\"hp__input\"\n placeholder=\"过滤 id / 字段值 / 内容…\"\n @keydown.enter=\"canWholeFileSearch && wholeFileSearch()\"\n />\n <button\n v-if=\"searchedQuery\"\n type=\"button\"\n class=\"hp__ghostbtn\"\n title=\"清除全文搜索\"\n @click=\"clearWholeFileSearch\"\n >\n 清除\n </button>\n </div>\n\n <p v-if=\"canWholeFileSearch\" class=\"hp__hint\">\n 过滤仅作用于已加载的 {{ collected.length.toLocaleString() }} 个。\n <button type=\"button\" class=\"hp__link\" @click=\"wholeFileSearch\">\n 在全部 {{ collectTotal.toLocaleString() }} 个实例中搜索「{{ collectQuery.trim() }}」\n </button>\n </p>\n\n <p v-if=\"jumpMsg\" class=\"hp__jumpmsg\">{{ jumpMsg }}</p>\n\n <div v-if=\"collecting\" class=\"hp__progress hp__progress--inline\">\n <p class=\"hp__progress-label\">正在扫描对象… {{ pct }}%</p>\n <div class=\"hp__bar\"><div class=\"hp__bar-fill\" :style=\"{ width: pct + '%' }\" /></div>\n </div>\n\n <template v-else>\n <div class=\"hp__list\">\n <p v-if=\"localFiltered.length === 0\" class=\"hp__msg\">无匹配对象</p>\n <div v-for=\"o in pageRows\" :key=\"o.id\" class=\"hp__obj\">\n <button type=\"button\" class=\"hp__objhead\" @click=\"toggleExpand(o.id)\">\n <Icon\n :name=\"expanded.has(o.id) ? 'chevronDown' : 'chevronRight'\"\n :size=\"13\"\n class=\"hp__objchev\"\n />\n <span class=\"hp__objid\">#{{ o.id }}</span>\n <span class=\"hp__objsize\">{{ formatBytes(o.size) }}</span>\n <span class=\"hp__objprev\">{{ previewLine(o) }}</span>\n </button>\n\n <div v-if=\"expanded.has(o.id)\" class=\"hp__detail\">\n <!-- array content -->\n <div v-if=\"o.array\" class=\"hp__arr\">\n <pre v-if=\"o.array.text != null\" class=\"hp__arrtext\">{{ o.array.text }}</pre>\n <div v-if=\"o.array.hex\" class=\"hp__arrhex\">{{ o.array.hex }}</div>\n <div\n v-if=\"o.array.elements\"\n class=\"hp__arrels\"\n :class=\"{ 'hp__arrels--obj': o.array.elemClassName }\"\n >\n <template v-for=\"(e, ei) in o.array.elements\" :key=\"ei\">\n <button\n v-if=\"e.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--el\"\n @click=\"jumpTo(e.refId)\"\n >\n <span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}\n </button>\n <span v-else class=\"hp__arrel\"\n ><span class=\"hp__eli\">[{{ e.name }}]</span> {{ e.value }}</span\n >\n </template>\n </div>\n <p v-if=\"o.array.truncated\" class=\"hp__nofields\">\n (预览已截断,完整长度 {{ o.array.length.toLocaleString() }})\n </p>\n </div>\n\n <!-- instance fields -->\n <dl v-else class=\"hp__fields\">\n <p v-if=\"!o.fields.length\" class=\"hp__nofields\">\n (无法解析字段布局,仅显示 id 与大小)\n </p>\n <template v-for=\"(f, fi) in o.fields\" :key=\"fi\">\n <dt class=\"hp__fkey\">{{ f.name }}</dt>\n <dd class=\"hp__fval\" :class=\"{ 'hp__fval--null': f.kind === 'null' }\">\n <template v-if=\"isStringy && f.name === 'value' && strText(o) !== undefined\">\n <span class=\"hp__str\">\"{{ strText(o) }}\"</span>\n <button\n v-if=\"f.kind === 'ref'\"\n type=\"button\"\n class=\"hp__reflink hp__reflink--sm\"\n @click=\"jumpTo(f.refId)\"\n >\n {{ f.value }}\n </button>\n </template>\n <template v-else-if=\"f.kind === 'ref'\">\n <button type=\"button\" class=\"hp__reflink\" @click=\"jumpTo(f.refId)\">\n {{ f.value }}\n </button>\n </template>\n <span v-else>{{ f.value }}</span>\n </dd>\n </template>\n </dl>\n </div>\n </div>\n </div>\n\n <div v-if=\"pageCount > 1\" class=\"hp__pager\">\n <button type=\"button\" class=\"hp__pagebtn\" :disabled=\"page <= 1\" @click=\"prevPage\">\n <Icon name=\"chevronLeft\" :size=\"14\" />\n </button>\n <span class=\"hp__pageinfo\">第 {{ page }} / {{ pageCount }} 页</span>\n <button\n type=\"button\"\n class=\"hp__pagebtn\"\n :disabled=\"page >= pageCount\"\n @click=\"nextPage\"\n >\n <Icon name=\"chevronRight\" :size=\"14\" />\n </button>\n </div>\n </template>\n </template>\n </template>\n </div>\n</template>\n\n<style scoped>\n.hp {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.hp__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.hp__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.hp__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.hp__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Large-file confirm card ------------------------------------------------------------------------- */\n.hp__confirm {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px 28px;\n text-align: center;\n color: var(--muted);\n}\n.hp__confirm-title {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.hp__confirm-sub {\n font-size: 12.5px;\n line-height: 1.6;\n color: var(--muted);\n}\n.hp__btn {\n margin-top: 4px;\n font-family: inherit;\n font-size: 13px;\n color: #fff;\n background: var(--accent);\n border: none;\n border-radius: 8px;\n padding: 8px 18px;\n cursor: pointer;\n}\n.hp__btn:hover {\n filter: brightness(1.06);\n}\n\n/* Progress bar (parse + scan) --------------------------------------------------------------------- */\n.hp__progress {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n.hp__progress--inline {\n flex: 0 0 auto;\n padding: 28px 24px;\n}\n.hp__progress-label {\n font-size: 12.5px;\n color: var(--muted);\n}\n.hp__bar {\n width: min(280px, 70%);\n height: 6px;\n border-radius: 999px;\n background: var(--surface-2);\n overflow: hidden;\n}\n.hp__bar-fill {\n height: 100%;\n background: var(--accent);\n transition: width 0.15s ease-out;\n}\n\n/* File meta grid ---------------------------------------------------------------------------------- */\n.hp__meta {\n flex-shrink: 0;\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);\n gap: 4px 12px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n}\n.hp__metakey {\n color: var(--faint);\n font-size: 11px;\n white-space: nowrap;\n}\n.hp__metaval {\n margin: 0;\n min-width: 0;\n color: var(--text);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n overflow-wrap: anywhere;\n}\n\n/* Search ------------------------------------------------------------------------------------------ */\n.hp__search {\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.hp__search--row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.hp__input {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.hp__input:focus {\n outline: none;\n border-color: var(--accent);\n}\n.hp__ghostbtn {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.hp__ghostbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Histogram table --------------------------------------------------------------------------------- */\n.hp__thead {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 6px 14px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n}\n.hp__th {\n font-family: inherit;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--faint);\n background: transparent;\n border: none;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n.hp__th:hover {\n color: var(--accent);\n}\n.hp__th--name {\n flex: 1 1 auto;\n justify-content: flex-start;\n}\n.hp__th--num {\n flex-shrink: 0;\n width: 76px;\n justify-content: flex-end;\n}\n.hp__arrow {\n font-size: 8px;\n}\n\n.hp__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 2px 0;\n}\n.hp__row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 5px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.hp__row:hover {\n background: var(--surface-2);\n}\n.hp__name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.hp__num {\n flex-shrink: 0;\n width: 76px;\n text-align: right;\n font-size: 12px;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.hp__num--bytes {\n color: var(--faint);\n}\n.hp__chev {\n flex-shrink: 0;\n color: var(--faint);\n}\n.hp__more {\n padding: 10px 16px;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n/* Breadcrumb + class bar -------------------------------------------------------------------------- */\n.hp__crumb {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.hp__back {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n gap: 2px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n padding: 4px 9px 4px 6px;\n cursor: pointer;\n}\n.hp__back:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.hp__crumb-name {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text-strong);\n}\n.hp__crumb-cls {\n color: var(--text-strong);\n}\n.hp__crumb-close {\n margin-left: auto;\n}\n.hp__classbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 7px 14px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.hp__classcount {\n font-variant-numeric: tabular-nums;\n}\n.hp__tag {\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--surface-2);\n color: var(--muted);\n font-size: 10.5px;\n}\n.hp__hint {\n flex-shrink: 0;\n padding: 8px 14px;\n font-size: 11.5px;\n line-height: 1.6;\n color: var(--muted);\n background: var(--surface-2);\n}\n.hp__link {\n font-family: inherit;\n font-size: 11.5px;\n color: var(--accent);\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n}\n.hp__jumpmsg {\n flex-shrink: 0;\n padding: 7px 14px;\n font-size: 11.5px;\n color: var(--danger);\n background: var(--surface-2);\n}\n\n/* Object list ------------------------------------------------------------------------------------- */\n.hp__obj {\n border-bottom: 1px solid var(--border);\n}\n.hp__objhead {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.hp__objhead:hover {\n background: var(--surface-2);\n}\n.hp__objchev {\n flex-shrink: 0;\n color: var(--faint);\n}\n.hp__objid {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n}\n.hp__objsize {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.hp__objprev {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--muted);\n}\n.hp__detail {\n background: var(--surface-2);\n}\n.hp__fields {\n margin: 0;\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 3px 14px;\n padding: 4px 16px 10px 34px;\n background: var(--surface-2);\n}\n.hp__fields--detail {\n padding: 10px 16px;\n}\n.hp__nofields {\n grid-column: 1 / -1;\n margin: 0;\n font-size: 11.5px;\n color: var(--faint);\n}\n.hp__fkey {\n color: var(--faint);\n font-family: var(--font-mono);\n font-size: 11.5px;\n white-space: nowrap;\n}\n.hp__fval {\n margin: 0;\n min-width: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text);\n overflow-wrap: anywhere;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 6px;\n}\n.hp__fval--null {\n color: var(--faint);\n font-style: italic;\n}\n.hp__str {\n color: var(--text-strong);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n/* Reference links --------------------------------------------------------------------------------- */\n.hp__reflink {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--accent);\n background: transparent;\n border: none;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.hp__reflink:hover {\n filter: brightness(1.15);\n}\n.hp__reflink--sm {\n font-size: 10.5px;\n opacity: 0.75;\n}\n.hp__reflink--el {\n display: inline-block;\n text-decoration: none;\n padding: 2px 6px;\n margin: 2px;\n border: 1px solid var(--border);\n border-radius: 6px;\n background: var(--surface);\n}\n.hp__reflink--el:hover {\n border-color: var(--accent);\n}\n.hp__annot {\n color: var(--faint);\n font-size: 10.5px;\n font-style: italic;\n}\n\n/* Array content ----------------------------------------------------------------------------------- */\n.hp__arr {\n padding: 8px 16px 12px 34px;\n}\n.hp__fields--detail + .hp__arr,\n.hp__crumb ~ .hp__list .hp__arr {\n padding-left: 16px;\n}\n.hp__arrmeta {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n margin-bottom: 6px;\n}\n.hp__arrtext {\n margin: 0 0 6px;\n max-height: 260px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 6px;\n background: var(--surface);\n border: 1px solid var(--border);\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.hp__arrhex {\n margin-bottom: 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n overflow-wrap: anywhere;\n line-height: 1.6;\n}\n.hp__arrels {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 10px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text);\n}\n.hp__arrels--obj {\n gap: 2px;\n}\n.hp__arrel {\n color: var(--text);\n}\n.hp__eli {\n color: var(--faint);\n}\n\n/* Pager ------------------------------------------------------------------------------------------- */\n.hp__pager {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 8px 14px;\n border-top: 1px solid var(--border);\n}\n.hp__pagebtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 26px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n cursor: pointer;\n}\n.hp__pagebtn:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.hp__pagebtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.hp__pageinfo {\n font-size: 12px;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageLightbox.js","names":[],"sources":["../../src/components/ImageLightbox.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useLightbox } from '@/composables/useLightbox'\nimport { fileExt } from '@/lib/fileType'\nimport Icon from '@/components/icons/Icon.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\n\n/**\n * Full-screen media lightbox (the \"弹框预览\"). A single instance is mounted in App.vue and driven by the\n * `useLightbox` singleton — any thumbnail / file chip opens it. Teleported to <body> so it overlays everything\n * regardless of the message tree's stacking contexts. Close via the X button, the empty backdrop, or Esc.\n *\n * Images render in the shared {@link ZoomImage} stage (wheel-zoom, drag-pan, dbl-click fit↔2×, toolbar, and\n * long-image fill-width). +/-/0 keys drive it via the exposed methods; a no-drag click on empty area closes.\n * Videos are FETCHED and played from a blob URL: the download endpoint serves `octet-stream` + `nosniff`, so a\n * direct `<video src>` can't determine a codec — but a blob tagged with the right MIME does (no backend change;\n * the same trick ZipBrowser uses).\n */\nconst { current, close } = useLightbox()\n\n// Extension → a concrete video MIME so the blob decodes (the upstream bytes arrive as octet-stream).\nconst VIDEO_MIME: Record<string, string> = {\n mp4: 'video/mp4',\n m4v: 'video/mp4',\n webm: 'video/webm',\n ogv: 'video/ogg',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n avi: 'video/x-msvideo',\n}\n\nconst isVideo = computed(() => current.value?.kind === 'video')\nconst zoomImg = ref<InstanceType<typeof ZoomImage> | null>(null)\n\nconst videoUrl = ref('')\nconst videoLoading = ref(false)\nconst videoError = ref('')\n\nfunction onKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') {\n close()\n } else if (!isVideo.value) {\n if (e.key === '+' || e.key === '=') zoomImg.value?.zoomIn()\n else if (e.key === '-' || e.key === '_') zoomImg.value?.zoomOut()\n else if (e.key === '0') zoomImg.value?.fit()\n }\n}\n\nfunction revokeVideo(): void {\n if (videoUrl.value) {\n URL.revokeObjectURL(videoUrl.value)\n videoUrl.value = ''\n }\n}\n\n// Fetch a video's bytes and wrap them in a typed blob the <video> element can actually decode.\nasync function loadVideo(media: NonNullable<typeof current.value>): Promise<void> {\n videoLoading.value = true\n videoError.value = ''\n try {\n const res = await fetch(media.src)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const raw = await res.blob()\n // Don't trust raw.type — the endpoint always serves octet-stream; derive from the filename extension.\n const type = VIDEO_MIME[fileExt(media.alt || '')] || 'video/mp4'\n const url = URL.createObjectURL(new Blob([raw], { type }))\n // Guard against the lightbox having closed / switched media while the fetch was in flight.\n if (current.value === media) videoUrl.value = url\n else URL.revokeObjectURL(url)\n } catch (e) {\n if (current.value === media) videoError.value = e instanceof Error ? e.message : String(e)\n } finally {\n if (current.value === media) videoLoading.value = false\n }\n}\n\n// Add the Esc listener and lock body scroll only while open; reset video state on every change and (re)load\n// when the new media is a video. Guarded for SSR.\nwatch(current, (media) => {\n if (typeof window === 'undefined') return\n if (media) {\n window.addEventListener('keydown', onKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onKey)\n document.body.style.overflow = ''\n }\n revokeVideo()\n videoError.value = ''\n videoLoading.value = false\n if (media?.kind === 'video') void loadVideo(media)\n})\n\nonBeforeUnmount(() => {\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onKey)\n document.body.style.overflow = ''\n }\n revokeVideo()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <div v-if=\"current\" class=\"lightbox\">\n <button\n type=\"button\"\n class=\"lightbox__close\"\n title=\"关闭 (Esc)\"\n aria-label=\"关闭\"\n @click=\"close\"\n >\n <Icon name=\"close\" :size=\"22\" :stroke-width=\"2.2\" />\n </button>\n\n <template v-if=\"isVideo\">\n <p v-if=\"videoLoading\" class=\"lightbox__msg\">加载视频中…</p>\n <p v-else-if=\"videoError\" class=\"lightbox__msg\">视频加载失败:{{ videoError }}</p>\n <video\n v-else-if=\"videoUrl\"\n class=\"lightbox__video\"\n :src=\"videoUrl\"\n controls\n autoplay\n playsinline\n />\n </template>\n\n <ZoomImage\n v-else\n ref=\"zoomImg\"\n class=\"lightbox__zoom\"\n :src=\"current.src\"\n :alt=\"current.alt || ''\"\n :long=\"current.long\"\n @empty-click=\"close\"\n />\n </div>\n </Teleport>\n</template>\n\n<style scoped>\n.lightbox {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.82);\n animation: lightbox-in 0.12s ease-out;\n}\n@keyframes lightbox-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n/* The zoom/pan stage fills the overlay; its transparent background lets the dark backdrop show through. */\n.lightbox__zoom {\n position: absolute;\n inset: 0;\n}\n.lightbox__video {\n max-width: 94vw;\n max-height: 94vh;\n border-radius: 6px;\n background: #000;\n box-shadow: 0 16px 56px rgba(0, 0, 0, 0.55);\n}\n.lightbox__msg {\n color: #fff;\n font-size: 14px;\n}\n.lightbox__close {\n position: fixed;\n top: 16px;\n right: 16px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.12);\n color: #fff;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.lightbox__close:hover {\n background: rgba(255, 255, 255, 0.24);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageLightbox.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ImageLightbox.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useLightbox } from '@/composables/useLightbox'\nimport { fileExt } from '@/lib/fileType'\nimport Icon from '@/components/icons/Icon.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\n\n/**\n * Full-screen media lightbox (the \"弹框预览\"). A single instance is mounted in App.vue and driven by the\n * `useLightbox` singleton — any thumbnail / file chip opens it. Teleported to <body> so it overlays everything\n * regardless of the message tree's stacking contexts. Close via the X button, the empty backdrop, or Esc.\n *\n * Images render in the shared {@link ZoomImage} stage (wheel-zoom, drag-pan, dbl-click fit↔2×, toolbar, and\n * long-image fill-width). +/-/0 keys drive it via the exposed methods; a no-drag click on empty area closes.\n * Videos are FETCHED and played from a blob URL: the download endpoint serves `octet-stream` + `nosniff`, so a\n * direct `<video src>` can't determine a codec — but a blob tagged with the right MIME does (no backend change;\n * the same trick ZipBrowser uses).\n */\nconst { current, close } = useLightbox()\n\n// Extension → a concrete video MIME so the blob decodes (the upstream bytes arrive as octet-stream).\nconst VIDEO_MIME: Record<string, string> = {\n mp4: 'video/mp4',\n m4v: 'video/mp4',\n webm: 'video/webm',\n ogv: 'video/ogg',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n avi: 'video/x-msvideo',\n}\n\nconst isVideo = computed(() => current.value?.kind === 'video')\nconst zoomImg = ref<InstanceType<typeof ZoomImage> | null>(null)\n\nconst videoUrl = ref('')\nconst videoLoading = ref(false)\nconst videoError = ref('')\n\nfunction onKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') {\n close()\n } else if (!isVideo.value) {\n if (e.key === '+' || e.key === '=') zoomImg.value?.zoomIn()\n else if (e.key === '-' || e.key === '_') zoomImg.value?.zoomOut()\n else if (e.key === '0') zoomImg.value?.fit()\n }\n}\n\nfunction revokeVideo(): void {\n if (videoUrl.value) {\n URL.revokeObjectURL(videoUrl.value)\n videoUrl.value = ''\n }\n}\n\n// Fetch a video's bytes and wrap them in a typed blob the <video> element can actually decode.\nasync function loadVideo(media: NonNullable<typeof current.value>): Promise<void> {\n videoLoading.value = true\n videoError.value = ''\n try {\n const res = await fetch(media.src)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const raw = await res.blob()\n // Don't trust raw.type — the endpoint always serves octet-stream; derive from the filename extension.\n const type = VIDEO_MIME[fileExt(media.alt || '')] || 'video/mp4'\n const url = URL.createObjectURL(new Blob([raw], { type }))\n // Guard against the lightbox having closed / switched media while the fetch was in flight.\n if (current.value === media) videoUrl.value = url\n else URL.revokeObjectURL(url)\n } catch (e) {\n if (current.value === media) videoError.value = e instanceof Error ? e.message : String(e)\n } finally {\n if (current.value === media) videoLoading.value = false\n }\n}\n\n// Add the Esc listener and lock body scroll only while open; reset video state on every change and (re)load\n// when the new media is a video. Guarded for SSR.\nwatch(current, (media) => {\n if (typeof window === 'undefined') return\n if (media) {\n window.addEventListener('keydown', onKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onKey)\n document.body.style.overflow = ''\n }\n revokeVideo()\n videoError.value = ''\n videoLoading.value = false\n if (media?.kind === 'video') void loadVideo(media)\n})\n\nonBeforeUnmount(() => {\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onKey)\n document.body.style.overflow = ''\n }\n revokeVideo()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <div v-if=\"current\" class=\"lightbox\">\n <button\n type=\"button\"\n class=\"lightbox__close\"\n title=\"关闭 (Esc)\"\n aria-label=\"关闭\"\n @click=\"close\"\n >\n <Icon name=\"close\" :size=\"22\" :stroke-width=\"2.2\" />\n </button>\n\n <template v-if=\"isVideo\">\n <p v-if=\"videoLoading\" class=\"lightbox__msg\">加载视频中…</p>\n <p v-else-if=\"videoError\" class=\"lightbox__msg\">视频加载失败:{{ videoError }}</p>\n <video\n v-else-if=\"videoUrl\"\n class=\"lightbox__video\"\n :src=\"videoUrl\"\n controls\n autoplay\n playsinline\n />\n </template>\n\n <ZoomImage\n v-else\n ref=\"zoomImg\"\n class=\"lightbox__zoom\"\n :src=\"current.src\"\n :alt=\"current.alt || ''\"\n :long=\"current.long\"\n @empty-click=\"close\"\n />\n </div>\n </Teleport>\n</template>\n\n<style scoped>\n.lightbox {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.82);\n animation: lightbox-in 0.12s ease-out;\n}\n@keyframes lightbox-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n/* The zoom/pan stage fills the overlay; its transparent background lets the dark backdrop show through. */\n.lightbox__zoom {\n position: absolute;\n inset: 0;\n}\n.lightbox__video {\n max-width: 94vw;\n max-height: 94vh;\n border-radius: 6px;\n background: #000;\n box-shadow: 0 16px 56px rgba(0, 0, 0, 0.55);\n}\n.lightbox__msg {\n color: #fff;\n font-size: 14px;\n}\n.lightbox__close {\n position: fixed;\n top: 16px;\n right: 16px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.12);\n color: #fff;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.lightbox__close:hover {\n background: rgba(255, 255, 255, 0.24);\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAkBA,IAAM,EAAE,YAAS,aAAU,EAAY,GAGjC,IAAqC;GACzC,KAAK;GACL,KAAK;GACL,MAAM;GACN,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;EACP,GAEM,IAAU,QAAe,EAAQ,OAAO,SAAS,OAAO,GACxD,IAAU,EAA2C,IAAI,GAEzD,IAAW,EAAI,EAAE,GACjB,IAAe,EAAI,EAAK,GACxB,IAAa,EAAI,EAAE;EAEzB,SAAS,EAAM,GAAwB;GACrC,AAAI,EAAE,QAAQ,WACZ,EAAM,IACI,EAAQ,UACd,EAAE,QAAQ,OAAO,EAAE,QAAQ,MAAK,EAAQ,OAAO,OAAO,IACjD,EAAE,QAAQ,OAAO,EAAE,QAAQ,MAAK,EAAQ,OAAO,QAAQ,IACvD,EAAE,QAAQ,OAAK,EAAQ,OAAO,IAAI;EAE/C;EAEA,SAAS,IAAoB;GAC3B,AAEE,EAAS,WADT,IAAI,gBAAgB,EAAS,KAAK,GACjB;EAErB;EAGA,eAAe,EAAU,GAAyD;GAEhF,AADA,EAAa,QAAQ,IACrB,EAAW,QAAQ;GACnB,IAAI;IACF,IAAM,IAAM,MAAM,MAAM,EAAM,GAAG;IACjC,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,QAAQ,EAAI,QAAQ;IACjD,IAAM,IAAM,MAAM,EAAI,KAAK,GAErB,IAAO,EAAW,EAAQ,EAAM,OAAO,EAAE,MAAM,aAC/C,IAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,CAAG,GAAG,EAAE,QAAK,CAAC,CAAC;IAEzD,AAAI,EAAQ,UAAU,IAAO,EAAS,QAAQ,IACzC,IAAI,gBAAgB,CAAG;GAC9B,SAAS,GAAG;IACV,AAAI,EAAQ,UAAU,MAAO,EAAW,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GAC3F,UAAU;IACR,AAAI,EAAQ,UAAU,MAAO,EAAa,QAAQ;GACpD;EACF;SAIA,EAAM,IAAU,MAAU;GACpB,OAAO,SAAW,QAClB,KACF,OAAO,iBAAiB,WAAW,CAAK,GACxC,SAAS,KAAK,MAAM,WAAW,aAE/B,OAAO,oBAAoB,WAAW,CAAK,GAC3C,SAAS,KAAK,MAAM,WAAW,KAEjC,EAAY,GACZ,EAAW,QAAQ,IACnB,EAAa,QAAQ,IACjB,GAAO,SAAS,WAAS,EAAe,CAAK;EACnD,CAAC,GAED,QAAsB;GAKpB,AAJI,OAAO,SAAW,QACpB,OAAO,oBAAoB,WAAW,CAAK,GAC3C,SAAS,KAAK,MAAM,WAAW,KAEjC,EAAY;EACd,CAAC,mBAIC,EAmCW,GAAA,EAnCD,IAAG,OAAM,GAAA,CACN,EAAA,CAAA,KAAA,EAAA,GAAX,EAiCM,OAjCN,GAiCM,CAhCJ,EAQS,UAAA;GAPP,MAAK;GACL,OAAM;GACN,OAAM;GACN,cAAW;GACV,SAAK,AAAA,EAAA,QAAA,GAAA,MAAE,EAAA,CAAA,KAAA,EAAA,CAAA,CAAA,CAAA,GAAA,CAAA;MAER,EAAoD,GAAA;GAA9C,MAAK;GAAS,MAAM;GAAK,gBAAc;QAG/B,EAAA,SAAA,EAAA,GAAhB,EAWW,GAAA,EAAA,KAAA,EAAA,GAAA,CAVA,EAAA,SAAA,EAAA,GAAT,EAAuD,KAAvD,GAA6C,QAAM,KACrC,EAAA,SAAA,EAAA,GAAd,EAA2E,KAA3E,GAAgD,YAAO,EAAG,EAAA,KAAU,GAAA,CAAA,KAEvD,EAAA,SAAA,EAAA,GADb,EAOE,SAAA;;GALA,OAAM;GACL,KAAK,EAAA;GACN,UAAA;GACA,UAAA;GACA,aAAA;6CAIJ,EAQE,GAAA;;YANI;GAAJ,KAAI;GACJ,OAAM;GACL,KAAK,EAAA,CAAA,CAAO,CAAC;GACb,KAAK,EAAA,CAAA,CAAO,CAAC,OAAG;GAChB,MAAM,EAAA,CAAA,CAAO,CAAC;GACd,cAAa,EAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InlineText.js","names":[],"sources":["../../src/components/InlineText.ts"],"sourcesContent":["import { defineComponent, h } from 'vue'\nimport { inline } from '@/lib/markdown/inline'\n\n/**\n * Renders inline markdown (code/bold/italic/links/strikethrough) as a fragment,\n * for embedding inside SFC templates (table cells, recipe steps, …). The block\n * renderer in lib/markdown/blocks.ts calls `inline()` directly instead.\n */\nexport default defineComponent({\n name: 'InlineText',\n props: {\n text: { type: String, default: '' },\n kp: { type: String, default: 'i' },\n },\n setup(props) {\n return () => inline(props.text, props.kp) as ReturnType<typeof h>[]\n },\n})\n"],"mappings":";;;AAQA,IAAA,IAAe,EAAgB;CAC7B,MAAM;CACN,OAAO;EACL,MAAM;GAAE,MAAM;GAAQ,SAAS;EAAG;EAClC,IAAI;GAAE,MAAM;GAAQ,SAAS;EAAI;CACnC;CACA,MAAM,GAAO;EACX,aAAa,EAAO,EAAM,MAAM,EAAM,EAAE;CAC1C;AACF,CAAC"}
|