@l1yp/file-viewer 0.1.4 → 0.1.6

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.
Files changed (177) hide show
  1. package/README.md +57 -6
  2. package/dist/assets/step.worker-CfDTYOf_.js.map +1 -0
  3. package/dist/components/AnsiText.d.ts +47 -0
  4. package/dist/components/AnsiText.js +41 -0
  5. package/dist/components/AnsiText.js.map +1 -0
  6. package/dist/components/CertViewer.js +1 -1
  7. package/dist/components/CertViewer.js.map +1 -1
  8. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  9. package/dist/components/CodeView.js +1 -1
  10. package/dist/components/CodeView.js.map +1 -1
  11. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +80 -71
  12. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/DocxEmbeddedFiles.js +9 -0
  14. package/dist/components/DocxEmbeddedFiles.js.map +1 -0
  15. package/dist/components/DocxEmbeddedFiles.vue.d.ts +15 -0
  16. package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js +69 -0
  17. package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js.map +1 -0
  18. package/dist/components/DocxNavigationPane.js +1 -1
  19. package/dist/components/DocxNavigationPane.js.map +1 -1
  20. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  21. package/dist/components/DocxViewer.js +1 -1
  22. package/dist/components/DocxViewer.js.map +1 -1
  23. package/dist/components/DocxViewer.vue.d.ts +1 -1
  24. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +182 -50
  25. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  26. package/dist/components/EplViewer.js +1 -1
  27. package/dist/components/EplViewer.js.map +1 -1
  28. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  29. package/dist/components/FileViewerBody.js +1 -1
  30. package/dist/components/FileViewerBody.js.map +1 -1
  31. package/dist/components/FileViewerBody.vue.d.ts +1 -0
  32. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +292 -281
  33. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  34. package/dist/components/FileViewerPanel.js +1 -1
  35. package/dist/components/FileViewerPanel.js.map +1 -1
  36. package/dist/components/FileViewerPanel.vue.d.ts +6 -1
  37. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +53 -51
  38. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  39. package/dist/components/HarViewer.js +1 -1
  40. package/dist/components/HarViewer.js.map +1 -1
  41. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  42. package/dist/components/JksViewer.js +1 -1
  43. package/dist/components/JksViewer.js.map +1 -1
  44. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  45. package/dist/components/LogViewer.js +1 -1
  46. package/dist/components/LogViewer.js.map +1 -1
  47. package/dist/components/LogViewer.vue.d.ts +3 -1
  48. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +284 -280
  49. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  50. package/dist/components/ModelViewModes.js +9 -0
  51. package/dist/components/ModelViewModes.js.map +1 -0
  52. package/dist/components/ModelViewModes.vue.d.ts +11 -0
  53. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js +49 -0
  54. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js.map +1 -0
  55. package/dist/components/ModelViewer.js +1 -1
  56. package/dist/components/ModelViewer.js.map +1 -1
  57. package/dist/components/ModelViewer.vue.d.ts +2 -1
  58. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +183 -96
  59. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/PdfNavigationPane.js +1 -1
  61. package/dist/components/PdfNavigationPane.js.map +1 -1
  62. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  63. package/dist/components/PdfViewer.js +1 -1
  64. package/dist/components/PdfViewer.js.map +1 -1
  65. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  66. package/dist/components/PptxNavigationPane.js +1 -1
  67. package/dist/components/PptxNavigationPane.js.map +1 -1
  68. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  69. package/dist/components/PsdViewer.js +1 -1
  70. package/dist/components/PsdViewer.js.map +1 -1
  71. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  72. package/dist/components/SqliteViewer.js +1 -1
  73. package/dist/components/SqliteViewer.js.map +1 -1
  74. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  75. package/dist/components/ViewerLoadError.js +9 -0
  76. package/dist/components/ViewerLoadError.js.map +1 -0
  77. package/dist/components/ViewerLoadError.vue.d.ts +17 -0
  78. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js +58 -0
  79. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js.map +1 -0
  80. package/dist/components/XlsxChart.js +1 -1
  81. package/dist/components/XlsxChart.js.map +1 -1
  82. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +9 -9
  83. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -1
  84. package/dist/components/XlsxImageLayer.js +9 -0
  85. package/dist/components/XlsxImageLayer.js.map +1 -0
  86. package/dist/components/XlsxImageLayer.vue.d.ts +9 -0
  87. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js +46 -0
  88. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  89. package/dist/components/XlsxViewer.js +1 -1
  90. package/dist/components/XlsxViewer.js.map +1 -1
  91. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +230 -216
  92. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  93. package/dist/components/epl/EplResource.js +1 -1
  94. package/dist/components/epl/EplResource.js.map +1 -1
  95. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -1
  96. package/dist/components/optionalViewer.d.ts +3 -2
  97. package/dist/components/optionalViewer.js +6 -3
  98. package/dist/components/optionalViewer.js.map +1 -1
  99. package/dist/composables/useFileViewer.d.ts +11 -3
  100. package/dist/composables/useFileViewer.js.map +1 -1
  101. package/dist/composables/useTeleportTheme.d.ts +7 -0
  102. package/dist/composables/useTeleportTheme.js +42 -0
  103. package/dist/composables/useTeleportTheme.js.map +1 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +29 -28
  106. package/dist/lib/ansi.d.ts +44 -0
  107. package/dist/lib/ansi.js +224 -0
  108. package/dist/lib/ansi.js.map +1 -0
  109. package/dist/lib/cfb.d.ts +37 -0
  110. package/dist/lib/cfb.js +112 -0
  111. package/dist/lib/cfb.js.map +1 -0
  112. package/dist/lib/docxEmbeddedDom.d.ts +22 -0
  113. package/dist/lib/docxEmbeddedDom.js +128 -0
  114. package/dist/lib/docxEmbeddedDom.js.map +1 -0
  115. package/dist/lib/docxEmbeddings.d.ts +41 -0
  116. package/dist/lib/docxEmbeddings.js +226 -0
  117. package/dist/lib/docxEmbeddings.js.map +1 -0
  118. package/dist/lib/docxTheme.d.ts +6 -0
  119. package/dist/lib/docxTheme.js +64 -0
  120. package/dist/lib/docxTheme.js.map +1 -0
  121. package/dist/lib/epl/code/structures.js +82 -0
  122. package/dist/lib/epl/code/structures.js.map +1 -0
  123. package/dist/lib/epl/code/variable.js +16 -0
  124. package/dist/lib/epl/code/variable.js.map +1 -0
  125. package/dist/lib/epl/eplDocument.js +28 -22
  126. package/dist/lib/epl/eplDocument.js.map +1 -1
  127. package/dist/lib/epl/index.d.ts +2 -1
  128. package/dist/lib/epl/index.js.map +1 -1
  129. package/dist/lib/epl/sections/code.js +37 -0
  130. package/dist/lib/epl/sections/code.js.map +1 -0
  131. package/dist/lib/epl/sections/esystemInfo.js +29 -0
  132. package/dist/lib/epl/sections/esystemInfo.js.map +1 -0
  133. package/dist/lib/epl/sections/losable.js +33 -0
  134. package/dist/lib/epl/sections/losable.js.map +1 -0
  135. package/dist/lib/epl/sections/projectConfig.js +39 -0
  136. package/dist/lib/epl/sections/projectConfig.js.map +1 -0
  137. package/dist/lib/epl/sections/resource.js +84 -0
  138. package/dist/lib/epl/sections/resource.js.map +1 -0
  139. package/dist/lib/fileType.d.ts +3 -1
  140. package/dist/lib/fileType.js +150 -137
  141. package/dist/lib/fileType.js.map +1 -1
  142. package/dist/lib/modelAppearance.d.ts +7 -0
  143. package/dist/lib/modelAppearance.js +41 -0
  144. package/dist/lib/modelAppearance.js.map +1 -0
  145. package/dist/lib/msi.js +62 -154
  146. package/dist/lib/msi.js.map +1 -1
  147. package/dist/lib/olePackage.d.ts +8 -0
  148. package/dist/lib/olePackage.js +37 -0
  149. package/dist/lib/olePackage.js.map +1 -0
  150. package/dist/lib/pptxFirstSlide.d.ts +11 -0
  151. package/dist/lib/pptxFirstSlide.js +34 -0
  152. package/dist/lib/pptxFirstSlide.js.map +1 -0
  153. package/dist/lib/step.d.ts +10 -0
  154. package/dist/lib/step.js +35 -0
  155. package/dist/lib/step.js.map +1 -0
  156. package/dist/lib/step.worker.d.ts +1 -0
  157. package/dist/lib/step.worker.js +18 -0
  158. package/dist/lib/step.worker.js.map +1 -0
  159. package/dist/lib/stepObject.d.ts +4 -0
  160. package/dist/lib/stepObject.js +52 -0
  161. package/dist/lib/stepObject.js.map +1 -0
  162. package/dist/lib/stepTypes.d.ts +60 -0
  163. package/dist/lib/viewerKind.js +10 -5
  164. package/dist/lib/viewerKind.js.map +1 -1
  165. package/dist/lib/xlsx.d.ts +1 -1
  166. package/dist/lib/xlsx.js +106 -89
  167. package/dist/lib/xlsx.js.map +1 -1
  168. package/dist/lib/xlsxCharts.js +71 -119
  169. package/dist/lib/xlsxCharts.js.map +1 -1
  170. package/dist/lib/xlsxImages.d.ts +43 -0
  171. package/dist/lib/xlsxImages.js +114 -0
  172. package/dist/lib/xlsxImages.js.map +1 -0
  173. package/dist/lib/xlsxParts.d.ts +17 -0
  174. package/dist/lib/xlsxParts.js +86 -0
  175. package/dist/lib/xlsxParts.js.map +1 -0
  176. package/dist/style.css +1 -1
  177. package/package.json +8 -3
@@ -1 +1 @@
1
- {"version":3,"file":"DocxNavigationPane.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ElementNavigationItem } from '@/composables/useElementNavigation'\nimport Icon from '@/components/icons/Icon.vue'\n\nconst props = defineProps<{\n outlineItems: readonly ElementNavigationItem[]\n pageItems: readonly ElementNavigationItem[]\n activeOutlineId: string\n activePageId: string\n}>()\n\nconst emit = defineEmits<{ navigate: [item: ElementNavigationItem] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\n\nfunction isActive(item: ElementNavigationItem): boolean {\n return (view.value === 'outline' ? props.activeOutlineId : props.activePageId) === item.id\n}\n</script>\n\n<template>\n <aside class=\"dn\" :class=\"{ 'dn--collapsed': collapsed }\" aria-label=\"文档导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"dn__reopen\"\n aria-label=\"展开文档导航\"\n title=\"展开文档导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"dn__header\">\n <div>\n <strong class=\"dn__title\">导航</strong>\n <span class=\"dn__count\">{{ pageItems.length }} 页</span>\n </div>\n <button\n type=\"button\"\n class=\"dn__collapse\"\n aria-label=\"收起文档导航\"\n title=\"收起文档导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <div class=\"dn__tabs\" role=\"tablist\" aria-label=\"导航方式\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'outline' }\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'pages' }\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n\n <nav class=\"dn__list\" aria-label=\"文档目录\">\n <template v-if=\"view === 'outline'\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__outline-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :style=\"{ paddingLeft: `${10 + item.level * 12}px` }\"\n :aria-current=\"isActive(item) ? 'location' : undefined\"\n :title=\"item.title\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__marker\" />\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n <p v-if=\"!outlineItems.length\" class=\"dn__empty\">\n 文档未设置标题层级,可切换到“页面”导航。\n </p>\n </template>\n\n <template v-else>\n <button\n v-for=\"(item, index) in pageItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__page-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :aria-current=\"isActive(item) ? 'page' : undefined\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__page-sheet\">{{ index + 1 }}</span>\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.dn {\n position: relative;\n flex: 0 0 166px;\n width: 166px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.dn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.dn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.dn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.dn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.dn__collapse,\n.dn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.dn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.dn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n.dn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__tabs {\n margin: 8px 8px 4px;\n padding: 2px;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.dn__tab {\n min-width: 0;\n padding: 5px 4px;\n border: 0;\n border-radius: 6px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.dn__tab--active {\n color: var(--text-strong);\n background: var(--surface);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n.dn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 4px 6px 18px;\n scrollbar-width: thin;\n}\n.dn__outline-item,\n.dn__page-item {\n position: relative;\n width: 100%;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 7px;\n border: 0;\n border-radius: 7px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dn__outline-item {\n min-height: 31px;\n padding-top: 5px;\n padding-right: 7px;\n padding-bottom: 5px;\n}\n.dn__page-item {\n padding: 6px;\n}\n.dn__outline-item:hover,\n.dn__page-item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.dn__marker {\n flex: 0 0 auto;\n width: 3px;\n height: 15px;\n border-radius: 999px;\n background: var(--border-2);\n}\n.dn__item--active .dn__marker {\n background: var(--accent);\n}\n.dn__page-sheet {\n flex: 0 0 auto;\n width: 26px;\n height: 34px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n color: var(--faint);\n background: var(--surface);\n font-size: 9.5px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n.dn__item--active .dn__page-sheet {\n border-color: var(--accent);\n color: var(--accent);\n}\n.dn__label {\n min-width: 0;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 11.5px;\n line-height: 1.35;\n}\n.dn__empty {\n margin: 10px 8px;\n color: var(--faint);\n font-size: 10.5px;\n line-height: 1.55;\n}\n\n@container (max-width: 500px) {\n .dn {\n flex-basis: 136px;\n width: 136px;\n }\n .dn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocxNavigationPane.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ElementNavigationItem } from '@/composables/useElementNavigation'\nimport Icon from '@/components/icons/Icon.vue'\n\nconst props = defineProps<{\n outlineItems: readonly ElementNavigationItem[]\n pageItems: readonly ElementNavigationItem[]\n activeOutlineId: string\n activePageId: string\n}>()\n\nconst emit = defineEmits<{ navigate: [item: ElementNavigationItem] }>()\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\n\nfunction isActive(item: ElementNavigationItem): boolean {\n return (view.value === 'outline' ? props.activeOutlineId : props.activePageId) === item.id\n}\n</script>\n\n<template>\n <aside class=\"dn\" :class=\"{ 'dn--collapsed': collapsed }\" aria-label=\"文档导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"dn__reopen\"\n aria-label=\"展开文档导航\"\n title=\"展开文档导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"dn__header\">\n <div>\n <strong class=\"dn__title\">导航</strong>\n <span class=\"dn__count\">{{ pageItems.length }} 页</span>\n </div>\n <button\n type=\"button\"\n class=\"dn__collapse\"\n aria-label=\"收起文档导航\"\n title=\"收起文档导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <div class=\"dn__tabs\" role=\"tablist\" aria-label=\"导航方式\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'outline' }\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"dn__tab\"\n :class=\"{ 'dn__tab--active': view === 'pages' }\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n\n <nav class=\"dn__list\" aria-label=\"文档目录\">\n <template v-if=\"view === 'outline'\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__outline-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :style=\"{ paddingLeft: `${10 + item.level * 12}px` }\"\n :aria-current=\"isActive(item) ? 'location' : undefined\"\n :title=\"item.title\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__marker\" />\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n <p v-if=\"!outlineItems.length\" class=\"dn__empty\">\n 文档未设置标题层级,可切换到“页面”导航。\n </p>\n </template>\n\n <template v-else>\n <button\n v-for=\"(item, index) in pageItems\"\n :key=\"item.id\"\n type=\"button\"\n class=\"dn__page-item\"\n :class=\"{ 'dn__item--active': isActive(item) }\"\n :aria-current=\"isActive(item) ? 'page' : undefined\"\n @click=\"emit('navigate', item)\"\n >\n <span class=\"dn__page-sheet\">{{ index + 1 }}</span>\n <span class=\"dn__label\">{{ item.title }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.dn {\n position: relative;\n flex: 0 0 166px;\n width: 166px;\n min-width: 0;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n transition: flex-basis 160ms ease;\n}\n.dn--collapsed {\n flex-basis: 0;\n width: 0;\n border-right: 0;\n}\n.dn__header {\n min-height: 45px;\n padding: 9px 8px 8px 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n.dn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n.dn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n.dn__collapse,\n.dn__reopen {\n border: 0;\n color: var(--muted);\n background: transparent;\n cursor: pointer;\n}\n.dn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n place-items: center;\n border-radius: 7px;\n}\n.dn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n.dn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__tabs {\n margin: 8px 8px 4px;\n padding: 2px;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.dn__tab {\n min-width: 0;\n padding: 5px 4px;\n border: 0;\n border-radius: 6px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.dn__tab--active {\n color: var(--text-strong);\n background: var(--surface);\n box-shadow: var(--shadow);\n}\n.dn__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n padding: 4px 6px 18px;\n scrollbar-width: thin;\n}\n.dn__outline-item,\n.dn__page-item {\n position: relative;\n width: 100%;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 7px;\n border: 0;\n border-radius: 7px;\n color: var(--muted);\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dn__outline-item {\n min-height: 31px;\n padding-top: 5px;\n padding-right: 7px;\n padding-bottom: 5px;\n}\n.dn__page-item {\n padding: 6px;\n}\n.dn__outline-item:hover,\n.dn__page-item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n.dn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n.dn__marker {\n flex: 0 0 auto;\n width: 3px;\n height: 15px;\n border-radius: 999px;\n background: var(--border-2);\n}\n.dn__item--active .dn__marker {\n background: var(--accent);\n}\n.dn__page-sheet {\n flex: 0 0 auto;\n width: 26px;\n height: 34px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n color: var(--muted);\n background: var(--surface);\n font-size: 9.5px;\n box-shadow: var(--shadow);\n}\n.dn__item--active .dn__page-sheet {\n border-color: var(--accent);\n color: var(--accent);\n}\n.dn__label {\n min-width: 0;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n font-size: 11.5px;\n line-height: 1.35;\n}\n.dn__empty {\n margin: 10px 8px;\n color: var(--faint);\n font-size: 10.5px;\n line-height: 1.55;\n}\n\n@container (max-width: 500px) {\n .dn {\n flex-basis: 136px;\n width: 136px;\n }\n .dn--collapsed {\n flex-basis: 0;\n width: 0;\n }\n}\n</style>\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./DocxViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/DocxViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-5e8d92a6"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-b4ce7b73"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"DocxViewer.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n if (props.url !== url || docEl.value !== el) return\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready'\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page: a white sheet with a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n margin-bottom: 0;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"DocxViewer.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxEmbeddedFiles from '@/components/DocxEmbeddedFiles.vue'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n extractDocxEmbeddedFile,\n readDocxEmbeddedFiles,\n type DocxEmbeddedFile,\n} from '@/lib/docxEmbeddings'\nimport { mountDocxEmbeddedPreviews, type DocxEmbeddedDomController } from '@/lib/docxEmbeddedDom'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\nimport { formatBytes } from '@/lib/format'\nimport { applyDocxReadingTheme } from '@/lib/docxTheme'\nimport { classifyViewerFile } from '@/lib/viewerKind'\n\n// The shared body lets an extracted package reuse the same viewer as a top-level file. Lazy loading breaks the\n// FileViewerBody → DocxViewer → FileViewerBody module cycle, matching ZipBrowser's nested-entry hand-off.\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated preview (plain pages follow the viewer; authored colors keep their paper). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nlet documentBuffer: ArrayBuffer | null = null\nlet embeddedBlobUrl = ''\nlet embeddedSelectionRun = 0\nlet renderRun = 0\nlet documentScrollTop = 0\nlet inlineEmbedded: DocxEmbeddedDomController | null = null\nconst embeddedFiles = shallowRef<DocxEmbeddedFile[]>([])\nconst selectedEmbedded = shallowRef<DocxEmbeddedFile | null>(null)\nconst embeddedViewerFile = shallowRef<ViewerFile | null>(null)\nconst embeddedStatus = shallowRef<Status>('ready')\nconst embeddedError = shallowRef('')\nconst downloadBusyId = shallowRef('')\nconst downloadError = shallowRef('')\n\nfunction revokeEmbeddedBlob(): void {\n if (!embeddedBlobUrl) return\n URL.revokeObjectURL(embeddedBlobUrl)\n embeddedBlobUrl = ''\n}\n\nfunction clearInlineEmbedded(): void {\n inlineEmbedded?.dispose()\n inlineEmbedded = null\n}\n\nfunction closeEmbedded(restoreDocumentScroll = true): void {\n const shouldRestoreScroll = restoreDocumentScroll && selectedEmbedded.value !== null\n embeddedSelectionRun++\n selectedEmbedded.value = null\n embeddedViewerFile.value = null\n embeddedStatus.value = 'ready'\n embeddedError.value = ''\n revokeEmbeddedBlob()\n if (shouldRestoreScroll) {\n void nextTick(() => {\n if (!selectedEmbedded.value && docEl.value) docEl.value.scrollTop = documentScrollTop\n })\n }\n}\n\nfunction resetEmbedded(): void {\n closeEmbedded(false)\n clearInlineEmbedded()\n documentBuffer = null\n documentScrollTop = 0\n embeddedFiles.value = []\n downloadBusyId.value = ''\n downloadError.value = ''\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n const run = ++renderRun\n status.value = 'loading'\n errorMsg.value = ''\n resetEmbedded()\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n // Embedded packages are a best-effort enhancement: malformed relationship XML must not blank a Word file\n // that docx-preview can otherwise render. Parse concurrently because both consumers only read the buffer.\n const embeddedPromise = readDocxEmbeddedFiles(buf).catch((error) => {\n console.warn('DOCX embedded files unavailable:', error)\n return []\n })\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n const discoveredEmbeddedFiles = await embeddedPromise\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n applyDocxReadingTheme(el)\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n documentBuffer = buf\n embeddedFiles.value = discoveredEmbeddedFiles\n inlineEmbedded = mountDocxEmbeddedPreviews({\n root: el,\n buffer: buf,\n files: discoveredEmbeddedFiles,\n onOpen: (file) => void openEmbedded(file),\n })\n status.value = 'ready'\n } catch (e) {\n if (renderRun !== run) return\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nasync function openEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer) return\n const buffer = documentBuffer\n let pendingUrl = ''\n documentScrollTop = docEl.value?.scrollTop ?? 0\n closeEmbedded(false)\n // closeEmbedded invalidates prior work; this run now becomes the active selection.\n const activeRun = ++embeddedSelectionRun\n selectedEmbedded.value = file\n embeddedStatus.value = 'loading'\n embeddedError.value = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (embeddedSelectionRun !== activeRun || documentBuffer !== buffer) return\n const url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n pendingUrl = url\n const viewerFile = classifyViewerFile({\n name: file.name,\n url,\n size: file.size,\n mime: file.contentType,\n })\n if (embeddedSelectionRun !== activeRun || selectedEmbedded.value !== file) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n embeddedBlobUrl = url\n pendingUrl = ''\n embeddedViewerFile.value = viewerFile\n embeddedStatus.value = 'ready'\n } catch (error) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (embeddedSelectionRun !== activeRun) return\n embeddedError.value = error instanceof Error ? error.message : String(error)\n embeddedStatus.value = 'error'\n }\n}\n\nasync function downloadEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer || downloadBusyId.value) return\n const buffer = documentBuffer\n downloadBusyId.value = file.id\n downloadError.value = ''\n let url = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (documentBuffer !== buffer) return\n url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = file.name\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n } catch (error) {\n downloadError.value = error instanceof Error ? error.message : String(error)\n } finally {\n if (url) setTimeout(() => URL.revokeObjectURL(url), 1000)\n if (downloadBusyId.value === file.id) downloadBusyId.value = ''\n }\n}\n\nwatch(\n [() => props.url, docEl],\n ([url, element]) => {\n if (url && element) void render()\n },\n { immediate: true },\n)\nonBeforeUnmount(() => {\n renderRun++\n resetEmbedded()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready' && !selectedEmbedded\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <div v-if=\"selectedEmbedded\" class=\"dx__embedded-bar\">\n <button type=\"button\" class=\"dx__back\" title=\"返回 Word 文档\" @click=\"closeEmbedded()\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"dx__embedded-title\" :title=\"selectedEmbedded.name\">\n {{ selectedEmbedded.label || selectedEmbedded.name }}\n </span>\n <span class=\"dx__embedded-size\">{{ formatBytes(selectedEmbedded.size) }}</span>\n <button\n type=\"button\"\n class=\"dx__download\"\n title=\"下载内嵌文件\"\n @click=\"downloadEmbedded(selectedEmbedded)\"\n >\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <template v-else-if=\"selectedEmbedded\">\n <p v-if=\"embeddedStatus === 'loading'\" class=\"dx__msg\">正在读取内嵌文件…</p>\n <div v-else-if=\"embeddedStatus === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>读取失败:{{ embeddedError }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"openEmbedded(selectedEmbedded)\">\n 重试\n </button>\n </div>\n <FileViewerBody v-else-if=\"embeddedViewerFile\" :file=\"embeddedViewerFile\" mode=\"render\" />\n </template>\n\n <DocxEmbeddedFiles\n v-else-if=\"status === 'ready' && embeddedFiles.length\"\n :files=\"embeddedFiles\"\n :download-busy-id=\"downloadBusyId\"\n :error=\"downloadError\"\n @open=\"openEmbedded\"\n @download=\"downloadEmbedded\"\n />\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready' && !selectedEmbedded\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dx__embedded-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.dx__back,\n.dx__download {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n padding: 0;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 7px;\n cursor: pointer;\n}\n.dx__back:hover,\n.dx__download:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dx__embedded-title {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 500;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dx__embedded-size {\n flex-shrink: 0;\n color: var(--faint);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* docx-preview intentionally skips OLE `w:object` nodes but leaves their empty <p>. The DOM adapter inserts\n * Word/WPS's inert raster snapshot, or a rendered PPTX first slide when the snapshot is browser-incompatible,\n * into that exact paragraph. The whole preview opens the real embedded file. */\n.dx__doc :deep(.docx-embedded-host) {\n min-height: 0;\n margin: 8px 0 12px;\n text-align: center;\n}\n.dx__doc :deep(.docx-embedded-preview) {\n position: relative;\n display: inline-flex;\n box-sizing: border-box;\n align-items: center;\n justify-content: center;\n gap: 10px;\n width: 100%;\n min-height: 72px;\n padding: 12px;\n overflow: hidden;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);\n cursor: pointer;\n font-family: Arial, sans-serif;\n text-align: left;\n}\n.dx__doc :deep(.docx-embedded-preview:hover),\n.dx__doc :deep(.docx-embedded-preview:focus-visible) {\n border-color: var(--hl-attr);\n box-shadow:\n 0 0 0 2px color-mix(in srgb, var(--hl-attr) 16%, transparent),\n 0 2px 8px rgba(15, 23, 42, 0.16);\n outline: none;\n}\n.dx__doc :deep(.docx-embedded-preview--image) {\n display: inline-block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--pptx) {\n display: block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--loading) {\n cursor: progress;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-slide) {\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-status) {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--muted);\n background: var(--surface-2);\n font-size: 12px;\n font-weight: 600;\n}\n.dx__doc :deep(.docx-embedded-preview__image) {\n display: block;\n width: 100%;\n height: auto;\n object-fit: contain;\n}\n.dx__doc :deep(.docx-embedded-preview__hint) {\n position: absolute;\n right: 8px;\n bottom: 8px;\n padding: 5px 9px;\n color: #fff;\n background: rgba(15, 23, 42, 0.76);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.02em;\n opacity: 0;\n pointer-events: none;\n transform: translateY(3px);\n transition:\n opacity 0.15s,\n transform 0.15s;\n}\n.dx__doc :deep(.docx-embedded-preview:hover .docx-embedded-preview__hint),\n.dx__doc :deep(.docx-embedded-preview:focus-visible .docx-embedded-preview__hint) {\n opacity: 1;\n transform: translateY(0);\n}\n.dx__doc :deep(.docx-embedded-preview__badge) {\n flex-shrink: 0;\n padding: 5px 7px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 14%, var(--surface));\n border-radius: 5px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n}\n.dx__doc :deep(.docx-embedded-preview__fallback-label) {\n min-width: 0;\n overflow: hidden;\n font-size: 12px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page gets a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: var(--shadow);\n margin-bottom: 0;\n}\n/* Complex pages retain their original paper/native controls; simple pages inherit the viewer scheme. */\n.dx__doc :deep(section.docx:not([data-fv-reading-theme='adaptive'])) {\n color-scheme: light;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":""}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the
3
3
  * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a
4
- * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the
4
+ * paginated preview (plain pages follow the viewer; authored colors keep their paper). No CDN / network beyond the
5
5
  * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.
6
6
  *
7
7
  * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a
@@ -1,35 +1,67 @@
1
- import e from "./DocxNavigationPane.js";
2
- import { useElementNavigation as t } from "../composables/useElementNavigation.js";
3
- import { buildDocxNavigation as n, buildDocxPageNavigation as r } from "../lib/docxNavigation.js";
4
- import { createBlock as i, createCommentVNode as a, createElementBlock as o, createElementVNode as s, defineComponent as c, onBeforeUnmount as l, onMounted as u, openBlock as d, shallowRef as f, toDisplayString as p, unref as m, vShow as h, watch as g, withDirectives as _ } from "vue";
5
- import { renderAsync as v } from "docx-preview";
1
+ import { classifyViewerFile as e } from "../lib/viewerKind.js";
2
+ import { formatBytes as t } from "../lib/format.js";
3
+ import n from "./icons/Icon.js";
4
+ import r from "./DocxEmbeddedFiles.js";
5
+ import i from "./DocxNavigationPane.js";
6
+ import { useElementNavigation as a } from "../composables/useElementNavigation.js";
7
+ import { extractDocxEmbeddedFile as o, readDocxEmbeddedFiles as s } from "../lib/docxEmbeddings.js";
8
+ import { mountDocxEmbeddedPreviews as c } from "../lib/docxEmbeddedDom.js";
9
+ import { buildDocxNavigation as ee, buildDocxPageNavigation as l } from "../lib/docxNavigation.js";
10
+ import { applyDocxReadingTheme as te } from "../lib/docxTheme.js";
11
+ import { Fragment as u, createBlock as d, createCommentVNode as f, createElementBlock as p, createElementVNode as m, createVNode as h, defineAsyncComponent as ne, defineComponent as g, nextTick as re, onBeforeUnmount as ie, openBlock as _, shallowRef as v, toDisplayString as y, unref as b, vShow as ae, watch as x, withDirectives as S } from "vue";
12
+ import { renderAsync as C } from "docx-preview";
6
13
  //#region src/components/DocxViewer.vue?vue&type=script&setup=true&lang.ts
7
- var y = { class: "dx" }, b = { class: "dx__stage" }, x = {
14
+ var oe = { class: "dx" }, se = { class: "dx__stage" }, ce = {
8
15
  key: 0,
16
+ class: "dx__embedded-bar"
17
+ }, le = ["title"], ue = { class: "dx__embedded-size" }, de = {
18
+ key: 1,
9
19
  class: "dx__msg"
10
- }, S = {
20
+ }, fe = {
21
+ key: 2,
22
+ class: "dx__msg dx__msg--error"
23
+ }, pe = {
24
+ key: 0,
25
+ class: "dx__msg"
26
+ }, me = {
11
27
  key: 1,
12
28
  class: "dx__msg dx__msg--error"
13
- }, C = /*@__PURE__*/ c({
29
+ }, w = /*@__PURE__*/ g({
14
30
  __name: "DocxViewer",
15
31
  props: {
16
32
  url: {},
17
33
  name: {},
18
34
  size: {}
19
35
  },
20
- setup(c) {
21
- let C = c, w = f("loading"), T = f(""), E = f(null), D = f("outline"), O = f(!1), { outlineItems: k, pageItems: A, activeOutlineId: j, activePageId: M, setItems: N, navigate: P } = t(E);
22
- async function F() {
23
- let e = E.value;
24
- if (!C.url || !e) return;
25
- w.value = "loading", T.value = "", N([], []), e.innerHTML = "";
26
- let t = C.url;
36
+ setup(g) {
37
+ let w = ne(() => import("./FileViewerBody.js")), T = g, E = v("loading"), D = v(""), O = v(null), k = v("outline"), A = v(!1), { outlineItems: he, pageItems: j, activeOutlineId: M, activePageId: N, setItems: P, navigate: F } = a(O), I = null, L = "", R = 0, z = 0, B = 0, V = null, H = v([]), U = v(null), W = v(null), G = v("ready"), K = v(""), q = v(""), J = v("");
38
+ function ge() {
39
+ L &&= (URL.revokeObjectURL(L), "");
40
+ }
41
+ function _e() {
42
+ V?.dispose(), V = null;
43
+ }
44
+ function Y(e = !0) {
45
+ let t = e && U.value !== null;
46
+ R++, U.value = null, W.value = null, G.value = "ready", K.value = "", ge(), t && re(() => {
47
+ !U.value && O.value && (O.value.scrollTop = B);
48
+ });
49
+ }
50
+ function X() {
51
+ Y(!1), _e(), I = null, B = 0, H.value = [], q.value = "", J.value = "";
52
+ }
53
+ async function Z() {
54
+ let e = O.value;
55
+ if (!T.url || !e) return;
56
+ let t = ++z;
57
+ E.value = "loading", D.value = "", X(), P([], []), e.innerHTML = "";
58
+ let n = T.url;
27
59
  try {
28
- let i = await fetch(t);
29
- if (!i.ok) throw Error(`HTTP ${i.status}`);
30
- let a = await i.arrayBuffer();
31
- if (C.url !== t || E.value !== e) return;
32
- let o = await v(a, e, void 0, {
60
+ let r = await fetch(n);
61
+ if (!r.ok) throw Error(`HTTP ${r.status}`);
62
+ let i = await r.arrayBuffer();
63
+ if (z !== t || T.url !== n || O.value !== e) return;
64
+ let a = s(i).catch((e) => (console.warn("DOCX embedded files unavailable:", e), [])), o = await C(i, e, void 0, {
33
65
  className: "docx",
34
66
  inWrapper: !0,
35
67
  ignoreWidth: !0,
@@ -40,35 +72,88 @@ var y = { class: "dx" }, b = { class: "dx__stage" }, x = {
40
72
  renderFootnotes: !0,
41
73
  renderEndnotes: !0,
42
74
  useBase64URL: !0
43
- });
44
- if (C.url !== t || E.value !== e) return;
45
- let s;
75
+ }), u = await a;
76
+ if (z !== t || T.url !== n || O.value !== e) return;
77
+ te(e);
78
+ let d;
46
79
  try {
47
- s = n(o, e);
80
+ d = ee(o, e);
48
81
  } catch (t) {
49
- console.warn("DOCX outline navigation unavailable:", t), s = {
82
+ console.warn("DOCX outline navigation unavailable:", t), d = {
50
83
  outline: [],
51
- pages: r(e)
84
+ pages: l(e)
52
85
  };
53
86
  }
54
- N(s.outline, s.pages), D.value = s.outline.length ? "outline" : "pages", w.value = "ready";
87
+ P(d.outline, d.pages), k.value = d.outline.length ? "outline" : "pages", I = i, H.value = u, V = c({
88
+ root: e,
89
+ buffer: i,
90
+ files: u,
91
+ onOpen: (e) => void Q(e)
92
+ }), E.value = "ready";
93
+ } catch (e) {
94
+ if (z !== t) return;
95
+ D.value = e instanceof Error ? e.message : String(e), E.value = "error";
96
+ }
97
+ }
98
+ async function Q(t) {
99
+ if (!I) return;
100
+ let n = I, r = "";
101
+ B = O.value?.scrollTop ?? 0, Y(!1);
102
+ let i = ++R;
103
+ U.value = t, G.value = "loading", K.value = "";
104
+ try {
105
+ let a = await o(n, t);
106
+ if (R !== i || I !== n) return;
107
+ let s = URL.createObjectURL(new Blob([a], { type: t.contentType }));
108
+ r = s;
109
+ let c = e({
110
+ name: t.name,
111
+ url: s,
112
+ size: t.size,
113
+ mime: t.contentType
114
+ });
115
+ if (R !== i || U.value !== t) {
116
+ URL.revokeObjectURL(s), r = "";
117
+ return;
118
+ }
119
+ L = s, r = "", W.value = c, G.value = "ready";
120
+ } catch (e) {
121
+ if (r && URL.revokeObjectURL(r), R !== i) return;
122
+ K.value = e instanceof Error ? e.message : String(e), G.value = "error";
123
+ }
124
+ }
125
+ async function $(e) {
126
+ if (!I || q.value) return;
127
+ let t = I;
128
+ q.value = e.id, J.value = "";
129
+ let n = "";
130
+ try {
131
+ let r = await o(t, e);
132
+ if (I !== t) return;
133
+ n = URL.createObjectURL(new Blob([r], { type: e.contentType }));
134
+ let i = document.createElement("a");
135
+ i.href = n, i.download = e.name, document.body.appendChild(i), i.click(), i.remove();
55
136
  } catch (e) {
56
- T.value = e instanceof Error ? e.message : String(e), w.value = "error";
137
+ J.value = e instanceof Error ? e.message : String(e);
138
+ } finally {
139
+ n && setTimeout(() => URL.revokeObjectURL(n), 1e3), q.value === e.id && (q.value = "");
57
140
  }
58
141
  }
59
- return u(F), g(() => C.url, F), l(() => {
60
- E.value && (E.value.innerHTML = "");
61
- }), (t, n) => (d(), o("div", y, [w.value === "ready" ? (d(), i(e, {
142
+ return x([() => T.url, O], ([e, t]) => {
143
+ e && t && Z();
144
+ }, { immediate: !0 }), ie(() => {
145
+ z++, X(), O.value && (O.value.innerHTML = "");
146
+ }), (e, a) => (_(), p("div", oe, [E.value === "ready" && !U.value ? (_(), d(i, {
62
147
  key: 0,
63
- collapsed: O.value,
64
- "onUpdate:collapsed": n[0] ||= (e) => O.value = e,
65
- view: D.value,
66
- "onUpdate:view": n[1] ||= (e) => D.value = e,
67
- "outline-items": m(k),
68
- "page-items": m(A),
69
- "active-outline-id": m(j),
70
- "active-page-id": m(M),
71
- onNavigate: m(P)
148
+ collapsed: A.value,
149
+ "onUpdate:collapsed": a[0] ||= (e) => A.value = e,
150
+ view: k.value,
151
+ "onUpdate:view": a[1] ||= (e) => k.value = e,
152
+ "outline-items": b(he),
153
+ "page-items": b(j),
154
+ "active-outline-id": b(M),
155
+ "active-page-id": b(N),
156
+ onNavigate: b(F)
72
157
  }, null, 8, [
73
158
  "collapsed",
74
159
  "view",
@@ -77,18 +162,65 @@ var y = { class: "dx" }, b = { class: "dx__stage" }, x = {
77
162
  "active-outline-id",
78
163
  "active-page-id",
79
164
  "onNavigate"
80
- ])) : a("", !0), s("div", b, [w.value === "loading" ? (d(), o("p", x, "正在渲染 Word 文档…")) : w.value === "error" ? (d(), o("div", S, [s("p", null, "渲染失败:" + p(T.value), 1), s("button", {
81
- type: "button",
82
- class: "dx__retry",
83
- onClick: F
84
- }, "重试")])) : a("", !0), _(s("div", {
85
- ref_key: "docEl",
86
- ref: E,
87
- class: "dx__doc"
88
- }, null, 512), [[h, w.value === "ready"]])])]));
165
+ ])) : f("", !0), m("div", se, [
166
+ U.value ? (_(), p("div", ce, [
167
+ m("button", {
168
+ type: "button",
169
+ class: "dx__back",
170
+ title: "返回 Word 文档",
171
+ onClick: a[2] ||= (e) => Y()
172
+ }, [h(n, {
173
+ name: "arrowLeft",
174
+ size: 16
175
+ })]),
176
+ m("span", {
177
+ class: "dx__embedded-title",
178
+ title: U.value.name
179
+ }, y(U.value.label || U.value.name), 9, le),
180
+ m("span", ue, y(b(t)(U.value.size)), 1),
181
+ m("button", {
182
+ type: "button",
183
+ class: "dx__download",
184
+ title: "下载内嵌文件",
185
+ onClick: a[3] ||= (e) => $(U.value)
186
+ }, [h(n, {
187
+ name: "download",
188
+ size: 15
189
+ })])
190
+ ])) : f("", !0),
191
+ E.value === "loading" ? (_(), p("p", de, "正在渲染 Word 文档…")) : E.value === "error" ? (_(), p("div", fe, [m("p", null, "渲染失败:" + y(D.value), 1), m("button", {
192
+ type: "button",
193
+ class: "dx__retry",
194
+ onClick: Z
195
+ }, "重试")])) : U.value ? (_(), p(u, { key: 3 }, [G.value === "loading" ? (_(), p("p", pe, "正在读取内嵌文件…")) : G.value === "error" ? (_(), p("div", me, [m("p", null, "读取失败:" + y(K.value), 1), m("button", {
196
+ type: "button",
197
+ class: "dx__retry",
198
+ onClick: a[4] ||= (e) => Q(U.value)
199
+ }, " 重试 ")])) : W.value ? (_(), d(b(w), {
200
+ key: 2,
201
+ file: W.value,
202
+ mode: "render"
203
+ }, null, 8, ["file"])) : f("", !0)], 64)) : E.value === "ready" && H.value.length ? (_(), d(r, {
204
+ key: 4,
205
+ files: H.value,
206
+ "download-busy-id": q.value,
207
+ error: J.value,
208
+ onOpen: Q,
209
+ onDownload: $
210
+ }, null, 8, [
211
+ "files",
212
+ "download-busy-id",
213
+ "error"
214
+ ])) : f("", !0),
215
+ S(m("div", {
216
+ ref_key: "docEl",
217
+ ref: O,
218
+ class: "dx__doc"
219
+ }, null, 512), [[ae, E.value === "ready" && !U.value]])
220
+ ])]));
89
221
  }
90
222
  });
91
223
  //#endregion
92
- export { C as default };
224
+ export { w as default };
93
225
 
94
226
  //# sourceMappingURL=DocxViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n if (props.url !== url || docEl.value !== el) return\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nonMounted(render)\nwatch(() => props.url, render)\nonBeforeUnmount(() => {\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready'\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page: a white sheet with a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n margin-bottom: 0;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAqBA,IAAM,IAAQ,GAQR,IAAS,EAAmB,SAAS,GACrC,IAAW,EAAW,EAAE,GAExB,IAAQ,EAA+B,IAAI,GAC3C,IAAU,EAAgC,SAAS,GACnD,IAAe,EAAW,EAAK,GAC/B,EAAE,iBAAc,cAAW,oBAAiB,iBAAc,aAAU,gBACxE,EAAqB,CAAK;EAE5B,eAAe,IAAwB;GACrC,IAAM,IAAK,EAAM;GACjB,IAAI,CAAC,EAAM,OAAO,CAAC,GAAI;GAIvB,AAHA,EAAO,QAAQ,WACf,EAAS,QAAQ,IACjB,EAAS,CAAC,GAAG,CAAC,CAAC,GACf,EAAG,YAAY;GACf,IAAM,IAAM,EAAM;GAClB,IAAI;IACF,IAAM,IAAM,MAAM,MAAM,CAAG;IAC3B,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,QAAQ,EAAI,QAAQ;IACjD,IAAM,IAAM,MAAM,EAAI,YAAY;IAElC,IAAI,EAAM,QAAQ,KAAO,EAAM,UAAU,GAAI;IAC7C,IAAM,IAAY,MAAM,EAAY,GAAK,GAAI,KAAA,GAAW;KACtD,WAAW;KACX,WAAW;KAGX,aAAa;KACb,cAAc;KACd,YAAY;KACZ,eAAe;KACf,eAAe;KACf,iBAAiB;KACjB,gBAAgB;KAChB,cAAc;IAChB,CAAC;IACD,IAAI,EAAM,QAAQ,KAAO,EAAM,UAAU,GAAI;IAC7C,IAAI;IACJ,IAAI;KACF,IAAa,EAAoB,GAAU,CAAE;IAC/C,SAAS,GAAO;KAGd,AADA,QAAQ,KAAK,wCAAwC,CAAK,GAC1D,IAAa;MAAE,SAAS,CAAC;MAAG,OAAO,EAAwB,CAAE;KAAE;IACjE;IAGA,AAFA,EAAS,EAAW,SAAS,EAAW,KAAK,GAC7C,EAAQ,QAAQ,EAAW,QAAQ,SAAS,YAAY,SACxD,EAAO,QAAQ;GACjB,SAAS,GAAG;IAEV,AADA,EAAS,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC1D,EAAO,QAAQ;GACjB;EACF;SAEA,EAAU,CAAM,GAChB,QAAY,EAAM,KAAK,CAAM,GAC7B,QAAsB;GACpB,AAAI,EAAM,UAAO,EAAM,MAAM,YAAY;EAC3C,CAAC,mBAIC,EAoBM,OApBN,GAoBM,CAlBI,EAAA,UAAM,WAAA,EAAA,GADd,EASE,GAAA;;GAPQ,WAAW,EAAA;2CAAY,QAAA;GACvB,MAAM,EAAA;sCAAO,QAAA;GACpB,iBAAe,EAAA,CAAA;GACf,cAAY,EAAA,CAAA;GACZ,qBAAmB,EAAA,CAAA;GACnB,kBAAgB,EAAA,CAAA;GAChB,YAAU,EAAA,CAAA;;;;;;;;;mBAEb,EAQM,OARN,GAQM,CAPK,EAAA,UAAM,aAAA,EAAA,GAAf,EAAgE,KAAhE,GAA+C,eAAa,KAC5C,EAAA,UAAM,WAAA,EAAA,GAAtB,EAGM,OAHN,GAGM,CAFJ,EAA0B,KAAA,MAAvB,UAAK,EAAG,EAAA,KAAQ,GAAA,CAAA,GACnB,EAAmE,UAAA;GAA3D,MAAK;GAAS,OAAM;GAAa,SAAO;KAAQ,IAAE,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAAA,EAG5D,EAA+D,OAAA;YAA1B;GAAJ,KAAI;GAAQ,OAAM;sBAAtC,EAAA,UAAM,OAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"DocxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxEmbeddedFiles from '@/components/DocxEmbeddedFiles.vue'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n extractDocxEmbeddedFile,\n readDocxEmbeddedFiles,\n type DocxEmbeddedFile,\n} from '@/lib/docxEmbeddings'\nimport { mountDocxEmbeddedPreviews, type DocxEmbeddedDomController } from '@/lib/docxEmbeddedDom'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\nimport { formatBytes } from '@/lib/format'\nimport { applyDocxReadingTheme } from '@/lib/docxTheme'\nimport { classifyViewerFile } from '@/lib/viewerKind'\n\n// The shared body lets an extracted package reuse the same viewer as a top-level file. Lazy loading breaks the\n// FileViewerBody → DocxViewer → FileViewerBody module cycle, matching ZipBrowser's nested-entry hand-off.\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated preview (plain pages follow the viewer; authored colors keep their paper). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nlet documentBuffer: ArrayBuffer | null = null\nlet embeddedBlobUrl = ''\nlet embeddedSelectionRun = 0\nlet renderRun = 0\nlet documentScrollTop = 0\nlet inlineEmbedded: DocxEmbeddedDomController | null = null\nconst embeddedFiles = shallowRef<DocxEmbeddedFile[]>([])\nconst selectedEmbedded = shallowRef<DocxEmbeddedFile | null>(null)\nconst embeddedViewerFile = shallowRef<ViewerFile | null>(null)\nconst embeddedStatus = shallowRef<Status>('ready')\nconst embeddedError = shallowRef('')\nconst downloadBusyId = shallowRef('')\nconst downloadError = shallowRef('')\n\nfunction revokeEmbeddedBlob(): void {\n if (!embeddedBlobUrl) return\n URL.revokeObjectURL(embeddedBlobUrl)\n embeddedBlobUrl = ''\n}\n\nfunction clearInlineEmbedded(): void {\n inlineEmbedded?.dispose()\n inlineEmbedded = null\n}\n\nfunction closeEmbedded(restoreDocumentScroll = true): void {\n const shouldRestoreScroll = restoreDocumentScroll && selectedEmbedded.value !== null\n embeddedSelectionRun++\n selectedEmbedded.value = null\n embeddedViewerFile.value = null\n embeddedStatus.value = 'ready'\n embeddedError.value = ''\n revokeEmbeddedBlob()\n if (shouldRestoreScroll) {\n void nextTick(() => {\n if (!selectedEmbedded.value && docEl.value) docEl.value.scrollTop = documentScrollTop\n })\n }\n}\n\nfunction resetEmbedded(): void {\n closeEmbedded(false)\n clearInlineEmbedded()\n documentBuffer = null\n documentScrollTop = 0\n embeddedFiles.value = []\n downloadBusyId.value = ''\n downloadError.value = ''\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n const run = ++renderRun\n status.value = 'loading'\n errorMsg.value = ''\n resetEmbedded()\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n // Embedded packages are a best-effort enhancement: malformed relationship XML must not blank a Word file\n // that docx-preview can otherwise render. Parse concurrently because both consumers only read the buffer.\n const embeddedPromise = readDocxEmbeddedFiles(buf).catch((error) => {\n console.warn('DOCX embedded files unavailable:', error)\n return []\n })\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n const discoveredEmbeddedFiles = await embeddedPromise\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n applyDocxReadingTheme(el)\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n documentBuffer = buf\n embeddedFiles.value = discoveredEmbeddedFiles\n inlineEmbedded = mountDocxEmbeddedPreviews({\n root: el,\n buffer: buf,\n files: discoveredEmbeddedFiles,\n onOpen: (file) => void openEmbedded(file),\n })\n status.value = 'ready'\n } catch (e) {\n if (renderRun !== run) return\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nasync function openEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer) return\n const buffer = documentBuffer\n let pendingUrl = ''\n documentScrollTop = docEl.value?.scrollTop ?? 0\n closeEmbedded(false)\n // closeEmbedded invalidates prior work; this run now becomes the active selection.\n const activeRun = ++embeddedSelectionRun\n selectedEmbedded.value = file\n embeddedStatus.value = 'loading'\n embeddedError.value = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (embeddedSelectionRun !== activeRun || documentBuffer !== buffer) return\n const url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n pendingUrl = url\n const viewerFile = classifyViewerFile({\n name: file.name,\n url,\n size: file.size,\n mime: file.contentType,\n })\n if (embeddedSelectionRun !== activeRun || selectedEmbedded.value !== file) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n embeddedBlobUrl = url\n pendingUrl = ''\n embeddedViewerFile.value = viewerFile\n embeddedStatus.value = 'ready'\n } catch (error) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (embeddedSelectionRun !== activeRun) return\n embeddedError.value = error instanceof Error ? error.message : String(error)\n embeddedStatus.value = 'error'\n }\n}\n\nasync function downloadEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer || downloadBusyId.value) return\n const buffer = documentBuffer\n downloadBusyId.value = file.id\n downloadError.value = ''\n let url = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (documentBuffer !== buffer) return\n url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = file.name\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n } catch (error) {\n downloadError.value = error instanceof Error ? error.message : String(error)\n } finally {\n if (url) setTimeout(() => URL.revokeObjectURL(url), 1000)\n if (downloadBusyId.value === file.id) downloadBusyId.value = ''\n }\n}\n\nwatch(\n [() => props.url, docEl],\n ([url, element]) => {\n if (url && element) void render()\n },\n { immediate: true },\n)\nonBeforeUnmount(() => {\n renderRun++\n resetEmbedded()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready' && !selectedEmbedded\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <div v-if=\"selectedEmbedded\" class=\"dx__embedded-bar\">\n <button type=\"button\" class=\"dx__back\" title=\"返回 Word 文档\" @click=\"closeEmbedded()\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"dx__embedded-title\" :title=\"selectedEmbedded.name\">\n {{ selectedEmbedded.label || selectedEmbedded.name }}\n </span>\n <span class=\"dx__embedded-size\">{{ formatBytes(selectedEmbedded.size) }}</span>\n <button\n type=\"button\"\n class=\"dx__download\"\n title=\"下载内嵌文件\"\n @click=\"downloadEmbedded(selectedEmbedded)\"\n >\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <template v-else-if=\"selectedEmbedded\">\n <p v-if=\"embeddedStatus === 'loading'\" class=\"dx__msg\">正在读取内嵌文件…</p>\n <div v-else-if=\"embeddedStatus === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>读取失败:{{ embeddedError }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"openEmbedded(selectedEmbedded)\">\n 重试\n </button>\n </div>\n <FileViewerBody v-else-if=\"embeddedViewerFile\" :file=\"embeddedViewerFile\" mode=\"render\" />\n </template>\n\n <DocxEmbeddedFiles\n v-else-if=\"status === 'ready' && embeddedFiles.length\"\n :files=\"embeddedFiles\"\n :download-busy-id=\"downloadBusyId\"\n :error=\"downloadError\"\n @open=\"openEmbedded\"\n @download=\"downloadEmbedded\"\n />\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready' && !selectedEmbedded\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dx__embedded-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.dx__back,\n.dx__download {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n padding: 0;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 7px;\n cursor: pointer;\n}\n.dx__back:hover,\n.dx__download:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dx__embedded-title {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 500;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dx__embedded-size {\n flex-shrink: 0;\n color: var(--faint);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* docx-preview intentionally skips OLE `w:object` nodes but leaves their empty <p>. The DOM adapter inserts\n * Word/WPS's inert raster snapshot, or a rendered PPTX first slide when the snapshot is browser-incompatible,\n * into that exact paragraph. The whole preview opens the real embedded file. */\n.dx__doc :deep(.docx-embedded-host) {\n min-height: 0;\n margin: 8px 0 12px;\n text-align: center;\n}\n.dx__doc :deep(.docx-embedded-preview) {\n position: relative;\n display: inline-flex;\n box-sizing: border-box;\n align-items: center;\n justify-content: center;\n gap: 10px;\n width: 100%;\n min-height: 72px;\n padding: 12px;\n overflow: hidden;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);\n cursor: pointer;\n font-family: Arial, sans-serif;\n text-align: left;\n}\n.dx__doc :deep(.docx-embedded-preview:hover),\n.dx__doc :deep(.docx-embedded-preview:focus-visible) {\n border-color: var(--hl-attr);\n box-shadow:\n 0 0 0 2px color-mix(in srgb, var(--hl-attr) 16%, transparent),\n 0 2px 8px rgba(15, 23, 42, 0.16);\n outline: none;\n}\n.dx__doc :deep(.docx-embedded-preview--image) {\n display: inline-block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--pptx) {\n display: block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--loading) {\n cursor: progress;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-slide) {\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-status) {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--muted);\n background: var(--surface-2);\n font-size: 12px;\n font-weight: 600;\n}\n.dx__doc :deep(.docx-embedded-preview__image) {\n display: block;\n width: 100%;\n height: auto;\n object-fit: contain;\n}\n.dx__doc :deep(.docx-embedded-preview__hint) {\n position: absolute;\n right: 8px;\n bottom: 8px;\n padding: 5px 9px;\n color: #fff;\n background: rgba(15, 23, 42, 0.76);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.02em;\n opacity: 0;\n pointer-events: none;\n transform: translateY(3px);\n transition:\n opacity 0.15s,\n transform 0.15s;\n}\n.dx__doc :deep(.docx-embedded-preview:hover .docx-embedded-preview__hint),\n.dx__doc :deep(.docx-embedded-preview:focus-visible .docx-embedded-preview__hint) {\n opacity: 1;\n transform: translateY(0);\n}\n.dx__doc :deep(.docx-embedded-preview__badge) {\n flex-shrink: 0;\n padding: 5px 7px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 14%, var(--surface));\n border-radius: 5px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n}\n.dx__doc :deep(.docx-embedded-preview__fallback-label) {\n min-width: 0;\n overflow: hidden;\n font-size: 12px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page gets a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: var(--shadow);\n margin-bottom: 0;\n}\n/* Complex pages retain their original paper/native controls; simple pages inherit the viewer scheme. */\n.dx__doc :deep(section.docx:not([data-fv-reading-theme='adaptive'])) {\n color-scheme: light;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./EplViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/EplViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-6b3500dc"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-562b3ee6"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"EplViewer.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\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\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* Fixed light \"IDE\" palette so the viewer reads like 易语言 regardless of the app theme. */\n --epl-bg: #ffffff;\n --epl-fg: #1f1f1f;\n --epl-border: #c4ccc4;\n --epl-th-bg: #dce8dc;\n --epl-th-fg: #2c3a2c;\n --epl-ident: #1b50c8;\n --epl-keyword: #0a3fb0;\n --epl-string: #c01818;\n --epl-comment: #1f8a3b;\n --epl-check: #1a7a1a;\n --epl-guide: #9aa6b2;\n --epl-gutter: #97a0a8;\n --epl-gutter-bg: #f4f6f4;\n --epl-row-hover: #eef4ff;\n --epl-sel: #cfe0ff;\n --epl-side-bg: #f6f7f6;\n --epl-tabbar-bg: #dfe3df;\n --epl-tab-bg: #e9ece9;\n --epl-tab-active-bg: #ffffff;\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__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.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__badge {\n font-size: 11px;\n color: #0a5;\n background: #e3f3e8;\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: #5a6a5a;\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: #999;\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"EplViewer.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\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\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* IDE roles follow the containing viewer, including explicit themes and host overrides. */\n --epl-bg: var(--surface);\n --epl-fg: var(--text);\n --epl-border: var(--border-2);\n --epl-th-bg: color-mix(in srgb, var(--hl-name) 12%, var(--surface));\n --epl-th-fg: var(--text-strong);\n --epl-ident: var(--hl-attr);\n --epl-keyword: var(--hl-kw);\n --epl-string: var(--hl-str);\n --epl-comment: var(--hl-name);\n --epl-check: var(--hl-name);\n --epl-guide: var(--border-2);\n --epl-gutter: var(--muted);\n --epl-gutter-bg: var(--code-bg);\n --epl-row-hover: var(--accent-soft);\n --epl-sel: color-mix(in srgb, var(--hl-attr) 18%, var(--surface));\n --epl-side-bg: var(--surface-2);\n --epl-tabbar-bg: var(--surface-2);\n --epl-tab-bg: var(--code-head);\n --epl-tab-active-bg: var(--surface);\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__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.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__badge {\n font-size: 11px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 12%, transparent);\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: var(--muted);\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--muted);\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":""}