@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
@@ -4,28 +4,26 @@ import { VIEWER_FORMATTERS as r, findFormatter as i } from "../lib/viewerFormats
4
4
  import a from "./icons/Icon.js";
5
5
  import { useFileViewer as o } from "../composables/useFileViewer.js";
6
6
  import s from "./FileViewerBody.js";
7
- import { Fragment as c, computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineComponent as ee, normalizeClass as h, onBeforeUnmount as g, openBlock as _, ref as v, renderList as te, toDisplayString as y, unref as b, watch as x } from "vue";
7
+ import { Fragment as c, computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineComponent as h, normalizeClass as g, onBeforeUnmount as _, openBlock as v, ref as y, renderList as ee, toDisplayString as b, unref as x, watch as S } from "vue";
8
8
  //#region src/components/FileViewerPanel.vue?vue&type=script&setup=true&lang.ts
9
- var ne = {
10
- key: 0,
11
- class: "fv"
12
- }, S = { class: "fv__head" }, re = {
9
+ var te = ["data-theme"], C = { class: "fv__head" }, ne = {
13
10
  key: 0,
14
11
  class: "fv__view",
15
12
  role: "group",
16
13
  "aria-label": "渲染或源码"
17
- }, C = ["aria-pressed"], w = ["aria-pressed"], T = ["title"], E = { class: "fv__fname" }, D = { class: "fv__ext" }, O = { class: "fv__actions" }, k = ["title", "aria-expanded"], A = {
14
+ }, re = ["aria-pressed"], w = ["aria-pressed"], T = ["title"], E = { class: "fv__fname" }, D = { class: "fv__ext" }, O = { class: "fv__actions" }, k = ["title", "aria-expanded"], A = {
18
15
  key: 0,
19
16
  class: "fv__menu-pop",
20
17
  role: "menu"
21
- }, j = ["aria-checked"], ie = ["aria-checked", "onClick"], ae = { class: "fv__menu-label" }, oe = ["href", "download"], M = /*@__PURE__*/ ee({
18
+ }, j = ["aria-checked"], ie = ["aria-checked", "onClick"], ae = { class: "fv__menu-label" }, oe = ["href", "download"], M = /*@__PURE__*/ h({
22
19
  __name: "FileViewerPanel",
23
- setup(ee) {
24
- let { current: M, width: se, setWidth: ce, close: N } = o(), P = v("render"), F = v(""), I = v(!1), L, R = v(null), z = v(null), B = l(() => z.value ?? M.value), V = l(() => M.value?.content != null), H = l(() => n(B.value)), U = l(() => t(B.value, F.value)), W = r, G = v(!1), K = v(null), q = v(""), J = l(() => i(q.value)), le = l(() => !!M.value?.url), Y = l(() => J.value ? `已强制以「${J.value.label}」打开 —— 点击切换查看方式` : "打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)");
25
- function ue(e) {
20
+ props: { theme: { default: "inherit" } },
21
+ setup(h) {
22
+ let { current: M, width: se, setWidth: ce, close: N } = o(), P = y("render"), F = y(""), I = y(!1), L, R = y(null), z = y(null), B = l(() => z.value ?? M.value), V = l(() => M.value?.content != null), H = l(() => n(B.value)), U = l(() => !H.value || B.value?.log === !0), W = l(() => t(B.value, F.value)), le = r, G = y(!1), K = y(null), q = y(""), J = l(() => i(q.value)), Y = l(() => !!M.value?.url), ue = l(() => J.value ? `已强制以「${J.value.label}」打开 —— 点击切换查看方式` : "打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)");
23
+ function de(e) {
26
24
  R.value?.pickFormat(e), G.value = !1;
27
25
  }
28
- function de() {
26
+ function fe() {
29
27
  R.value?.resetFormat(), G.value = !1;
30
28
  }
31
29
  function X(e) {
@@ -34,24 +32,24 @@ var ne = {
34
32
  function Z(e) {
35
33
  e.key === "Escape" && (G.value = !1);
36
34
  }
37
- x(G, (e) => {
35
+ S(G, (e) => {
38
36
  typeof document > "u" || (e ? (document.addEventListener("click", X), document.addEventListener("keydown", Z)) : (document.removeEventListener("click", X), document.removeEventListener("keydown", Z)));
39
37
  });
40
- let Q = l(() => e(M.value?.name ?? "").toUpperCase()), fe = l(() => {
38
+ let Q = l(() => e(M.value?.name ?? "").toUpperCase()), pe = l(() => {
41
39
  let t = M.value?.name ?? "", n = e(t);
42
40
  return n ? t.slice(0, t.length - n.length - 1) : t;
43
41
  });
44
- x(M, (e) => {
42
+ S(M, (e) => {
45
43
  e && (P.value = "render", F.value = "", z.value = null, q.value = "", G.value = !1);
46
44
  });
47
- async function pe() {
45
+ async function me() {
48
46
  try {
49
- await navigator.clipboard?.writeText(F.value);
47
+ await navigator.clipboard?.writeText(R.value?.copyText() ?? F.value);
50
48
  } catch {}
51
49
  I.value = !0, clearTimeout(L), L = setTimeout(() => I.value = !1, 1400);
52
50
  }
53
- let $ = v(!1);
54
- function me(e) {
51
+ let $ = y(!1);
52
+ function he(e) {
55
53
  e.preventDefault();
56
54
  let t = e.currentTarget, n = e.clientX, r = se.value, i = document.body, a = i.style.cursor, o = i.style.userSelect;
57
55
  t.setPointerCapture(e.pointerId), $.value = !0, i.style.cursor = "col-resize", i.style.userSelect = "none";
@@ -60,27 +58,31 @@ var ne = {
60
58
  };
61
59
  t.addEventListener("pointermove", s), t.addEventListener("pointerup", l), document.addEventListener("selectstart", c);
62
60
  }
63
- return g(() => {
61
+ return _(() => {
64
62
  clearTimeout(L), typeof document < "u" && (document.removeEventListener("click", X), document.removeEventListener("keydown", Z));
65
- }), (e, t) => b(M) ? (_(), f("aside", ne, [
63
+ }), (e, t) => x(M) ? (v(), f("aside", {
64
+ key: 0,
65
+ class: "fv-root fv",
66
+ "data-theme": h.theme === "inherit" ? void 0 : h.theme
67
+ }, [
66
68
  p("div", {
67
- class: h(["fv__resizer", { "fv__resizer--active": $.value }]),
69
+ class: g(["fv__resizer", { "fv__resizer--active": $.value }]),
68
70
  title: "拖动调整宽度",
69
- onPointerdown: me
71
+ onPointerdown: he
70
72
  }, [...t[8] ||= [p("span", { class: "fv__grip" }, null, -1)]], 34),
71
- p("header", S, [
72
- U.value ? (_(), f("div", re, [p("button", {
73
+ p("header", C, [
74
+ W.value ? (v(), f("div", ne, [p("button", {
73
75
  type: "button",
74
- class: h(["fv__view-btn", { "fv__view-btn--on": P.value === "render" }]),
76
+ class: g(["fv__view-btn", { "fv__view-btn--on": P.value === "render" }]),
75
77
  "aria-pressed": P.value === "render",
76
78
  title: "渲染",
77
79
  onClick: t[0] ||= (e) => P.value = "render"
78
80
  }, [m(a, {
79
81
  name: "eye",
80
82
  size: 15
81
- })], 10, C), p("button", {
83
+ })], 10, re), p("button", {
82
84
  type: "button",
83
- class: h(["fv__view-btn", { "fv__view-btn--on": P.value === "source" }]),
85
+ class: g(["fv__view-btn", { "fv__view-btn--on": P.value === "source" }]),
84
86
  "aria-pressed": P.value === "source",
85
87
  title: "源码",
86
88
  onClick: t[1] ||= (e) => P.value = "source"
@@ -90,38 +92,38 @@ var ne = {
90
92
  })], 10, w)])) : d("", !0),
91
93
  p("span", {
92
94
  class: "fv__title",
93
- title: b(M).name
94
- }, [p("span", E, y(fe.value), 1), Q.value ? (_(), f(c, { key: 0 }, [t[9] ||= p("span", { class: "fv__dot" }, "·", -1), p("span", D, y(Q.value), 1)], 64)) : d("", !0)], 8, T),
95
+ title: x(M).name
96
+ }, [p("span", E, b(pe.value), 1), Q.value ? (v(), f(c, { key: 0 }, [t[9] ||= p("span", { class: "fv__dot" }, "·", -1), p("span", D, b(Q.value), 1)], 64)) : d("", !0)], 8, T),
95
97
  p("div", O, [
96
- le.value ? (_(), f("div", {
98
+ Y.value ? (v(), f("div", {
97
99
  key: 0,
98
100
  ref_key: "menuRef",
99
101
  ref: K,
100
102
  class: "fv__menu"
101
103
  }, [p("button", {
102
104
  type: "button",
103
- class: h(["fv__btn fv__btn--text", { "fv__btn--on": q.value }]),
104
- title: Y.value,
105
+ class: g(["fv__btn fv__btn--text", { "fv__btn--on": q.value }]),
106
+ title: ue.value,
105
107
  "aria-haspopup": !0,
106
108
  "aria-expanded": G.value,
107
109
  onClick: t[2] ||= (e) => G.value = !G.value
108
110
  }, [m(a, {
109
111
  name: "apps",
110
112
  size: 14
111
- }), p("span", null, y(J.value ? J.value.label : "打开方式"), 1)], 10, k), G.value ? (_(), f("div", A, [
113
+ }), p("span", null, b(J.value ? J.value.label : "打开方式"), 1)], 10, k), G.value ? (v(), f("div", A, [
112
114
  p("button", {
113
115
  type: "button",
114
- class: h(["fv__menu-item", { "fv__menu-item--on": !q.value }]),
116
+ class: g(["fv__menu-item", { "fv__menu-item--on": !q.value }]),
115
117
  role: "menuitemradio",
116
118
  "aria-checked": !q.value,
117
- onClick: de
119
+ onClick: fe
118
120
  }, [
119
121
  m(a, {
120
122
  name: "eye",
121
123
  size: 14
122
124
  }),
123
125
  t[10] ||= p("span", { class: "fv__menu-label" }, "自动检测", -1),
124
- q.value ? d("", !0) : (_(), u(a, {
126
+ q.value ? d("", !0) : (v(), u(a, {
125
127
  key: 0,
126
128
  name: "check",
127
129
  size: 15,
@@ -129,20 +131,20 @@ var ne = {
129
131
  }))
130
132
  ], 10, j),
131
133
  t[11] ||= p("div", { class: "fv__menu-sep" }, null, -1),
132
- (_(!0), f(c, null, te(b(W), (e) => (_(), f("button", {
134
+ (v(!0), f(c, null, ee(x(le), (e) => (v(), f("button", {
133
135
  key: e.key,
134
136
  type: "button",
135
- class: h(["fv__menu-item", { "fv__menu-item--on": q.value === e.key }]),
137
+ class: g(["fv__menu-item", { "fv__menu-item--on": q.value === e.key }]),
136
138
  role: "menuitemradio",
137
139
  "aria-checked": q.value === e.key,
138
- onClick: (t) => ue(e.key)
140
+ onClick: (t) => de(e.key)
139
141
  }, [
140
142
  m(a, {
141
143
  name: e.icon,
142
144
  size: 14
143
145
  }, null, 8, ["name"]),
144
- p("span", ae, y(e.label), 1),
145
- q.value === e.key ? (_(), u(a, {
146
+ p("span", ae, b(e.label), 1),
147
+ q.value === e.key ? (v(), u(a, {
146
148
  key: 0,
147
149
  name: "check",
148
150
  size: 15,
@@ -150,26 +152,26 @@ var ne = {
150
152
  })) : d("", !0)
151
153
  ], 10, ie))), 128))
152
154
  ])) : d("", !0)], 512)) : d("", !0),
153
- H.value ? d("", !0) : (_(), f("button", {
155
+ U.value ? (v(), f("button", {
154
156
  key: 1,
155
157
  type: "button",
156
158
  class: "fv__btn fv__btn--text",
157
- onClick: pe
159
+ onClick: me
158
160
  }, [m(a, {
159
161
  name: "copy",
160
162
  size: 14
161
- }), p("span", null, y(I.value ? "Copied" : "Copy"), 1)])),
162
- V.value ? d("", !0) : (_(), f("a", {
163
+ }), p("span", null, b(I.value ? "Copied" : "Copy"), 1)])) : d("", !0),
164
+ V.value ? d("", !0) : (v(), f("a", {
163
165
  key: 2,
164
166
  class: "fv__btn",
165
- href: b(M).url,
166
- download: b(M).name,
167
+ href: x(M).url,
168
+ download: x(M).name,
167
169
  title: "下载"
168
170
  }, [m(a, {
169
171
  name: "download",
170
172
  size: 15
171
173
  })], 8, oe)),
172
- !V.value && !H.value ? (_(), f("button", {
174
+ !V.value && !H.value ? (v(), f("button", {
173
175
  key: 3,
174
176
  type: "button",
175
177
  class: "fv__btn",
@@ -183,7 +185,7 @@ var ne = {
183
185
  type: "button",
184
186
  class: "fv__btn",
185
187
  title: "关闭",
186
- onClick: t[4] ||= (...e) => b(N) && b(N)(...e)
188
+ onClick: t[4] ||= (...e) => x(N) && x(N)(...e)
187
189
  }, [m(a, {
188
190
  name: "close",
189
191
  size: 16
@@ -193,14 +195,14 @@ var ne = {
193
195
  m(s, {
194
196
  ref_key: "bodyRef",
195
197
  ref: R,
196
- file: b(M),
198
+ file: x(M),
197
199
  mode: P.value,
198
200
  "host-chooser": "",
199
201
  onLoaded: t[5] ||= (e) => F.value = e,
200
202
  onFormatchange: t[6] ||= (e) => z.value = e,
201
203
  onFormatpick: t[7] ||= (e) => q.value = e
202
204
  }, null, 8, ["file", "mode"])
203
- ])) : d("", !0);
205
+ ], 8, te)) : d("", !0);
204
206
  }
205
207
  });
206
208
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"FileViewerPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useFileViewer, type ViewerFile } from '@/composables/useFileViewer'\nimport { canToggleView, skipsTextLoad } from '@/lib/viewerKind'\nimport { VIEWER_FORMATTERS, findFormatter } from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport FileViewerBody from '@/components/FileViewerBody.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Right-docked viewer for files (DetailView reflows the conversation left to make room — a split pane, not an\n * overlay). The header owns the title, the render/source toggle, and the copy/download/refresh/close actions;\n * the CONTENT is delegated to {@link FileViewerBody}, the shared type dispatcher (also used by the archive\n * browsers, so a file inside a zip previews with the same viewers). The left edge is a drag splitter.\n */\nconst { current, width, setWidth, close } = useFileViewer()\n\ntype ViewMode = 'render' | 'source'\nconst mode = ref<ViewMode>('render')\nconst text = ref('') // body emits the loaded text up so the copy button can use it\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nconst bodyRef = ref<{\n reload: () => void\n pickFormat: (key: string) => void\n resetFormat: () => void\n} | null>(null)\n\n// The EFFECTIVE file the body is actually showing — it differs from `current` when the user picks a formatter\n// for an otherwise-unpreviewable file (\"open with\", see FileViewerBody). The body announces it via\n// `formatchange`, so the header's render/source toggle + copy/refresh track the chosen format, not the raw file.\nconst shown = ref<ViewerFile | null>(null)\nconst effective = computed(() => shown.value ?? current.value)\n\n// Inline content (e.g. a loaded skill): rendered directly — no download/refresh, but copy still applies.\nconst isInline = computed(() => current.value?.content != null)\n// A file whose content comes from its own sub-viewer / URL (not the panel-loaded text): no copy/refresh.\nconst selfRendered = computed(() => skipsTextLoad(effective.value))\n// Markdown / html / svg / subtitle / geo / csv / mind-map can flip between rendered and source.\nconst canToggle = computed(() => canToggleView(effective.value, text.value))\n\n// ── \"open with\" menu ──────────────────────────────────────────────────────────────────────────\n// The universal entry point: any file can be force-opened as a different formatter (hex / text / image /\n// audio / …) when auto-detection guessed wrong. The body owns the actual override; this header menu just\n// drives it through the exposed pickFormat / resetFormat. `activeFormat` mirrors the body's current choice\n// (it announces it via `formatpick`), so the menu checks the active row and the button labels it.\nconst formatters = VIEWER_FORMATTERS\nconst menuOpen = ref(false)\nconst menuRef = ref<HTMLElement | null>(null)\nconst activeFormat = ref('')\nconst activeFormatter = computed(() => findFormatter(activeFormat.value))\n// Formatters reinterpret fetched bytes, so they need a URL — hide the menu for inline-only content (no url).\nconst showChooser = computed(() => !!current.value?.url)\nconst chooserTitle = computed(() =>\n activeFormatter.value\n ? `已强制以「${activeFormatter.value.label}」打开 —— 点击切换查看方式`\n : '打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)',\n)\n\nfunction chooseFormat(key: string): void {\n bodyRef.value?.pickFormat(key)\n menuOpen.value = false\n}\nfunction chooseAuto(): void {\n bodyRef.value?.resetFormat()\n menuOpen.value = false\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside menuRef, so a click on either is \"inside\" and never self-closes.\nfunction onDocPointer(e: MouseEvent): void {\n if (menuRef.value && !menuRef.value.contains(e.target as Node)) menuOpen.value = false\n}\nfunction onMenuKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') menuOpen.value = false\n}\nwatch(menuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onDocPointer)\n document.addEventListener('keydown', onMenuKey)\n } else {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n\n// Header: \"name · EXT\" with the extension split off and emphasized.\nconst ext = computed(() => fileExt(current.value?.name ?? '').toUpperCase())\nconst baseName = computed(() => {\n const n = current.value?.name ?? ''\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\n\n// Re-load whenever a different file is opened (the panel stays mounted across files);\n// each newly opened file starts in its rendered (default) view. The body owns the actual fetch.\nwatch(current, (f) => {\n if (!f) return\n mode.value = 'render'\n text.value = ''\n shown.value = null // body re-announces via `formatchange`; clear so a stale override never lingers\n activeFormat.value = '' // a new file starts auto-detected (the body also re-announces via `formatpick`)\n menuOpen.value = false\n})\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nconst resizing = ref(false)\n\n// Drag the left edge to resize. The pane is docked right, so moving the cursor left widens it.\n// `resizing` drives a scoped class that colors only the grip — no global body class, so the color\n// cannot leak onto the page. The page-wide resize cursor + selection lock are set inline on <body>.\nfunction onResizeStart(e: PointerEvent): void {\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n const startX = e.clientX\n const startW = width.value\n const body = document.body\n const prevCursor = body.style.cursor\n const prevSelect = body.style.userSelect\n el.setPointerCapture(e.pointerId)\n resizing.value = true\n body.style.cursor = 'col-resize'\n body.style.userSelect = 'none'\n const move = (ev: PointerEvent) => setWidth(startW + (startX - ev.clientX))\n const noSelect = (ev: Event) => ev.preventDefault()\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n document.removeEventListener('selectstart', noSelect)\n body.style.cursor = prevCursor\n body.style.userSelect = prevSelect\n resizing.value = false\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n document.addEventListener('selectstart', noSelect)\n}\n\nonBeforeUnmount(() => {\n clearTimeout(copyTimer)\n if (typeof document !== 'undefined') {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n</script>\n\n<template>\n <aside v-if=\"current\" class=\"fv\">\n <div\n class=\"fv__resizer\"\n :class=\"{ 'fv__resizer--active': resizing }\"\n title=\"拖动调整宽度\"\n @pointerdown=\"onResizeStart\"\n >\n <span class=\"fv__grip\" />\n </div>\n\n <header class=\"fv__head\">\n <!-- markdown / html / svg / …: flip between the rendered view and its source. -->\n <div v-if=\"canToggle\" class=\"fv__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'render' }\"\n :aria-pressed=\"mode === 'render'\"\n title=\"渲染\"\n @click=\"mode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'source' }\"\n :aria-pressed=\"mode === 'source'\"\n title=\"源码\"\n @click=\"mode = 'source'\"\n >\n <Icon name=\"code\" :size=\"15\" />\n </button>\n </div>\n <span class=\"fv__title\" :title=\"current.name\">\n <span class=\"fv__fname\">{{ baseName }}</span>\n <template v-if=\"ext\"\n ><span class=\"fv__dot\">·</span><span class=\"fv__ext\">{{ ext }}</span></template\n >\n </span>\n <div class=\"fv__actions\">\n <!-- \"open with\" menu: universal viewer chooser — any file can be force-opened as hex/text/image/audio/…\n when auto-detection guessed wrong. Drives the body's override via the exposed pickFormat/resetFormat. -->\n <div v-if=\"showChooser\" ref=\"menuRef\" class=\"fv__menu\">\n <button\n type=\"button\"\n class=\"fv__btn fv__btn--text\"\n :class=\"{ 'fv__btn--on': activeFormat }\"\n :title=\"chooserTitle\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"menuOpen\"\n @click=\"menuOpen = !menuOpen\"\n >\n <Icon name=\"apps\" :size=\"14\" /><span>{{\n activeFormatter ? activeFormatter.label : '打开方式'\n }}</span>\n </button>\n <div v-if=\"menuOpen\" class=\"fv__menu-pop\" role=\"menu\">\n <button\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': !activeFormat }\"\n role=\"menuitemradio\"\n :aria-checked=\"!activeFormat\"\n @click=\"chooseAuto\"\n >\n <Icon name=\"eye\" :size=\"14\" /><span class=\"fv__menu-label\">自动检测</span>\n <Icon v-if=\"!activeFormat\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n <div class=\"fv__menu-sep\" />\n <button\n v-for=\"f in formatters\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': activeFormat === f.key }\"\n role=\"menuitemradio\"\n :aria-checked=\"activeFormat === f.key\"\n @click=\"chooseFormat(f.key)\"\n >\n <Icon :name=\"f.icon\" :size=\"14\" /><span class=\"fv__menu-label\">{{ f.label }}</span>\n <Icon v-if=\"activeFormat === f.key\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n </div>\n </div>\n <button v-if=\"!selfRendered\" type=\"button\" class=\"fv__btn fv__btn--text\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"14\" /><span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n <a\n v-if=\"!isInline\"\n class=\"fv__btn\"\n :href=\"current.url\"\n :download=\"current.name\"\n title=\"下载\"\n ><Icon name=\"download\" :size=\"15\"\n /></a>\n <button\n v-if=\"!isInline && !selfRendered\"\n type=\"button\"\n class=\"fv__btn\"\n title=\"刷新\"\n @click=\"bodyRef?.reload()\"\n >\n <Icon name=\"retry\" :size=\"15\" />\n </button>\n <button type=\"button\" class=\"fv__btn\" title=\"关闭\" @click=\"close\">\n <Icon name=\"close\" :size=\"16\" />\n </button>\n </div>\n </header>\n\n <FileViewerBody\n ref=\"bodyRef\"\n :file=\"current\"\n :mode=\"mode\"\n host-chooser\n @loaded=\"text = $event\"\n @formatchange=\"shown = $event\"\n @formatpick=\"activeFormat = $event\"\n />\n </aside>\n</template>\n\n<style scoped>\n/* In-flow flex sibling of the conversation (DetailView is the flex row), so each pane owns its own\n * scrollbar — the conversation's sits at its right edge, not hidden behind a floating overlay. */\n.fv {\n --fv-code-fs: 12.5px;\n --fv-code-lh: 21px;\n position: relative;\n flex: 0 0 var(--fv-width);\n min-width: 0;\n height: 100%;\n z-index: 55;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n border-left: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n/* Left-edge drag handle: a wide invisible hit area with a full-height rail + a small centered grip. */\n.fv__resizer {\n position: absolute;\n top: 0;\n bottom: 0;\n left: -4px;\n width: 11px;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: col-resize;\n touch-action: none;\n}\n.fv__resizer::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 2px;\n transform: translateX(-50%);\n background: transparent;\n transition: background 0.15s;\n}\n.fv__grip {\n position: relative;\n width: 5px;\n height: 34px;\n border-radius: 3px;\n background: var(--border-2);\n transition:\n background 0.15s,\n height 0.15s;\n}\n.fv__resizer:hover::before,\n.fv__resizer--active::before {\n background: #6ea8fe;\n}\n.fv__resizer:hover .fv__grip,\n.fv__resizer--active .fv__grip {\n background: #6ea8fe;\n}\n.fv__resizer--active .fv__grip {\n height: 48px;\n}\n\n.fv__head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-shrink: 0;\n padding: 10px 12px 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n/* Render/source segmented toggle, top-left. */\n.fv__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.fv__view-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__view-btn:hover {\n color: var(--muted);\n}\n.fv__view-btn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n.fv__title {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n.fv__fname {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 13px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.fv__dot {\n flex-shrink: 0;\n color: var(--faint);\n}\n.fv__ext {\n flex-shrink: 0;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.fv__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n.fv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n height: 30px;\n min-width: 30px;\n padding: 0 7px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12px;\n text-decoration: none;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__btn:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__btn--text {\n padding: 0 9px;\n}\n/* \"open with\" toggle, accented while an override is active (so the forced format reads at a glance). */\n.fv__btn--on {\n color: var(--accent);\n background: var(--surface-2);\n}\n.fv__btn--on:hover {\n color: var(--accent);\n}\n\n/* \"Open with\" dropdown: anchored under its toggle button, right-aligned to the actions edge. */\n.fv__menu {\n position: relative;\n display: inline-flex;\n}\n.fv__menu-pop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 60;\n min-width: 184px;\n padding: 5px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.fv__menu-item {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 7px 9px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.fv__menu-item:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__menu-item--on {\n color: var(--accent);\n}\n.fv__menu-label {\n flex: 1 1 auto;\n min-width: 0;\n}\n.fv__menu-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.fv__menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--border);\n}\n\n@media (max-width: 860px) {\n /* Too narrow to split — overlay the viewer full-screen instead. */\n .fv {\n position: fixed;\n inset: 0;\n flex-basis: auto;\n width: 100%;\n z-index: 70;\n }\n .fv__resizer {\n display: none;\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"FileViewerPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useFileViewer, type ViewerFile } from '@/composables/useFileViewer'\nimport { canToggleView, skipsTextLoad } from '@/lib/viewerKind'\nimport { VIEWER_FORMATTERS, findFormatter } from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport FileViewerBody from '@/components/FileViewerBody.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\nwithDefaults(defineProps<{ theme?: 'light' | 'dark' | 'system' | 'inherit' }>(), {\n theme: 'inherit',\n})\n\n/**\n * Right-docked viewer for files (DetailView reflows the conversation left to make room — a split pane, not an\n * overlay). The header owns the title, the render/source toggle, and the copy/download/refresh/close actions;\n * the CONTENT is delegated to {@link FileViewerBody}, the shared type dispatcher (also used by the archive\n * browsers, so a file inside a zip previews with the same viewers). The left edge is a drag splitter.\n */\nconst { current, width, setWidth, close } = useFileViewer()\n\ntype ViewMode = 'render' | 'source'\nconst mode = ref<ViewMode>('render')\nconst text = ref('') // body emits the loaded text up so the copy button can use it\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nconst bodyRef = ref<{\n reload: () => void\n pickFormat: (key: string) => void\n resetFormat: () => void\n copyText: () => string\n} | null>(null)\n\n// The EFFECTIVE file the body is actually showing — it differs from `current` when the user picks a formatter\n// for an otherwise-unpreviewable file (\"open with\", see FileViewerBody). The body announces it via\n// `formatchange`, so the header's render/source toggle + copy/refresh track the chosen format, not the raw file.\nconst shown = ref<ViewerFile | null>(null)\nconst effective = computed(() => shown.value ?? current.value)\n\n// Inline content (e.g. a loaded skill): rendered directly — no download/refresh, but copy still applies.\nconst isInline = computed(() => current.value?.content != null)\n// A file whose content comes from its own sub-viewer / URL (not the panel-loaded text): no copy/refresh.\nconst selfRendered = computed(() => skipsTextLoad(effective.value))\n// LogViewer self-fetches for virtualization but exposes its control-free lines for on-demand copy.\nconst canCopy = computed(() => !selfRendered.value || effective.value?.log === true)\n// Markdown / html / svg / subtitle / geo / csv / mind-map can flip between rendered and source.\nconst canToggle = computed(() => canToggleView(effective.value, text.value))\n\n// ── \"open with\" menu ──────────────────────────────────────────────────────────────────────────\n// The universal entry point: any file can be force-opened as a different formatter (hex / text / image /\n// audio / …) when auto-detection guessed wrong. The body owns the actual override; this header menu just\n// drives it through the exposed pickFormat / resetFormat. `activeFormat` mirrors the body's current choice\n// (it announces it via `formatpick`), so the menu checks the active row and the button labels it.\nconst formatters = VIEWER_FORMATTERS\nconst menuOpen = ref(false)\nconst menuRef = ref<HTMLElement | null>(null)\nconst activeFormat = ref('')\nconst activeFormatter = computed(() => findFormatter(activeFormat.value))\n// Formatters reinterpret fetched bytes, so they need a URL — hide the menu for inline-only content (no url).\nconst showChooser = computed(() => !!current.value?.url)\nconst chooserTitle = computed(() =>\n activeFormatter.value\n ? `已强制以「${activeFormatter.value.label}」打开 —— 点击切换查看方式`\n : '打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)',\n)\n\nfunction chooseFormat(key: string): void {\n bodyRef.value?.pickFormat(key)\n menuOpen.value = false\n}\nfunction chooseAuto(): void {\n bodyRef.value?.resetFormat()\n menuOpen.value = false\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside menuRef, so a click on either is \"inside\" and never self-closes.\nfunction onDocPointer(e: MouseEvent): void {\n if (menuRef.value && !menuRef.value.contains(e.target as Node)) menuOpen.value = false\n}\nfunction onMenuKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') menuOpen.value = false\n}\nwatch(menuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onDocPointer)\n document.addEventListener('keydown', onMenuKey)\n } else {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n\n// Header: \"name · EXT\" with the extension split off and emphasized.\nconst ext = computed(() => fileExt(current.value?.name ?? '').toUpperCase())\nconst baseName = computed(() => {\n const n = current.value?.name ?? ''\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\n\n// Re-load whenever a different file is opened (the panel stays mounted across files);\n// each newly opened file starts in its rendered (default) view. The body owns the actual fetch.\nwatch(current, (f) => {\n if (!f) return\n mode.value = 'render'\n text.value = ''\n shown.value = null // body re-announces via `formatchange`; clear so a stale override never lingers\n activeFormat.value = '' // a new file starts auto-detected (the body also re-announces via `formatpick`)\n menuOpen.value = false\n})\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(bodyRef.value?.copyText() ?? text.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nconst resizing = ref(false)\n\n// Drag the left edge to resize. The pane is docked right, so moving the cursor left widens it.\n// `resizing` drives a scoped class that colors only the grip — no global body class, so the color\n// cannot leak onto the page. The page-wide resize cursor + selection lock are set inline on <body>.\nfunction onResizeStart(e: PointerEvent): void {\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n const startX = e.clientX\n const startW = width.value\n const body = document.body\n const prevCursor = body.style.cursor\n const prevSelect = body.style.userSelect\n el.setPointerCapture(e.pointerId)\n resizing.value = true\n body.style.cursor = 'col-resize'\n body.style.userSelect = 'none'\n const move = (ev: PointerEvent) => setWidth(startW + (startX - ev.clientX))\n const noSelect = (ev: Event) => ev.preventDefault()\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n document.removeEventListener('selectstart', noSelect)\n body.style.cursor = prevCursor\n body.style.userSelect = prevSelect\n resizing.value = false\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n document.addEventListener('selectstart', noSelect)\n}\n\nonBeforeUnmount(() => {\n clearTimeout(copyTimer)\n if (typeof document !== 'undefined') {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n</script>\n\n<template>\n <aside v-if=\"current\" class=\"fv-root fv\" :data-theme=\"theme === 'inherit' ? undefined : theme\">\n <div\n class=\"fv__resizer\"\n :class=\"{ 'fv__resizer--active': resizing }\"\n title=\"拖动调整宽度\"\n @pointerdown=\"onResizeStart\"\n >\n <span class=\"fv__grip\" />\n </div>\n\n <header class=\"fv__head\">\n <!-- markdown / html / svg / …: flip between the rendered view and its source. -->\n <div v-if=\"canToggle\" class=\"fv__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'render' }\"\n :aria-pressed=\"mode === 'render'\"\n title=\"渲染\"\n @click=\"mode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'source' }\"\n :aria-pressed=\"mode === 'source'\"\n title=\"源码\"\n @click=\"mode = 'source'\"\n >\n <Icon name=\"code\" :size=\"15\" />\n </button>\n </div>\n <span class=\"fv__title\" :title=\"current.name\">\n <span class=\"fv__fname\">{{ baseName }}</span>\n <template v-if=\"ext\"\n ><span class=\"fv__dot\">·</span><span class=\"fv__ext\">{{ ext }}</span></template\n >\n </span>\n <div class=\"fv__actions\">\n <!-- \"open with\" menu: universal viewer chooser — any file can be force-opened as hex/text/image/audio/…\n when auto-detection guessed wrong. Drives the body's override via the exposed pickFormat/resetFormat. -->\n <div v-if=\"showChooser\" ref=\"menuRef\" class=\"fv__menu\">\n <button\n type=\"button\"\n class=\"fv__btn fv__btn--text\"\n :class=\"{ 'fv__btn--on': activeFormat }\"\n :title=\"chooserTitle\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"menuOpen\"\n @click=\"menuOpen = !menuOpen\"\n >\n <Icon name=\"apps\" :size=\"14\" /><span>{{\n activeFormatter ? activeFormatter.label : '打开方式'\n }}</span>\n </button>\n <div v-if=\"menuOpen\" class=\"fv__menu-pop\" role=\"menu\">\n <button\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': !activeFormat }\"\n role=\"menuitemradio\"\n :aria-checked=\"!activeFormat\"\n @click=\"chooseAuto\"\n >\n <Icon name=\"eye\" :size=\"14\" /><span class=\"fv__menu-label\">自动检测</span>\n <Icon v-if=\"!activeFormat\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n <div class=\"fv__menu-sep\" />\n <button\n v-for=\"f in formatters\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': activeFormat === f.key }\"\n role=\"menuitemradio\"\n :aria-checked=\"activeFormat === f.key\"\n @click=\"chooseFormat(f.key)\"\n >\n <Icon :name=\"f.icon\" :size=\"14\" /><span class=\"fv__menu-label\">{{ f.label }}</span>\n <Icon v-if=\"activeFormat === f.key\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n </div>\n </div>\n <button v-if=\"canCopy\" type=\"button\" class=\"fv__btn fv__btn--text\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"14\" /><span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n <a\n v-if=\"!isInline\"\n class=\"fv__btn\"\n :href=\"current.url\"\n :download=\"current.name\"\n title=\"下载\"\n ><Icon name=\"download\" :size=\"15\"\n /></a>\n <button\n v-if=\"!isInline && !selfRendered\"\n type=\"button\"\n class=\"fv__btn\"\n title=\"刷新\"\n @click=\"bodyRef?.reload()\"\n >\n <Icon name=\"retry\" :size=\"15\" />\n </button>\n <button type=\"button\" class=\"fv__btn\" title=\"关闭\" @click=\"close\">\n <Icon name=\"close\" :size=\"16\" />\n </button>\n </div>\n </header>\n\n <FileViewerBody\n ref=\"bodyRef\"\n :file=\"current\"\n :mode=\"mode\"\n host-chooser\n @loaded=\"text = $event\"\n @formatchange=\"shown = $event\"\n @formatpick=\"activeFormat = $event\"\n />\n </aside>\n</template>\n\n<style scoped>\n/* In-flow flex sibling of the conversation (DetailView is the flex row), so each pane owns its own\n * scrollbar — the conversation's sits at its right edge, not hidden behind a floating overlay. */\n.fv {\n --fv-code-fs: 12.5px;\n --fv-code-lh: 21px;\n position: relative;\n flex: 0 0 var(--fv-width);\n min-width: 0;\n height: 100%;\n z-index: 55;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n border-left: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n/* Left-edge drag handle: a wide invisible hit area with a full-height rail + a small centered grip. */\n.fv__resizer {\n position: absolute;\n top: 0;\n bottom: 0;\n left: -4px;\n width: 11px;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: col-resize;\n touch-action: none;\n}\n.fv__resizer::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 2px;\n transform: translateX(-50%);\n background: transparent;\n transition: background 0.15s;\n}\n.fv__grip {\n position: relative;\n width: 5px;\n height: 34px;\n border-radius: 3px;\n background: var(--border-2);\n transition:\n background 0.15s,\n height 0.15s;\n}\n.fv__resizer:hover::before,\n.fv__resizer--active::before {\n background: var(--hl-attr);\n}\n.fv__resizer:hover .fv__grip,\n.fv__resizer--active .fv__grip {\n background: var(--hl-attr);\n}\n.fv__resizer--active .fv__grip {\n height: 48px;\n}\n\n.fv__head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-shrink: 0;\n padding: 10px 12px 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n/* Render/source segmented toggle, top-left. */\n.fv__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.fv__view-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__view-btn:hover {\n color: var(--muted);\n}\n.fv__view-btn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n.fv__title {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n.fv__fname {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 13px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.fv__dot {\n flex-shrink: 0;\n color: var(--faint);\n}\n.fv__ext {\n flex-shrink: 0;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.fv__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n.fv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n height: 30px;\n min-width: 30px;\n padding: 0 7px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12px;\n text-decoration: none;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__btn:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__btn--text {\n padding: 0 9px;\n}\n/* \"open with\" toggle, accented while an override is active (so the forced format reads at a glance). */\n.fv__btn--on {\n color: var(--accent);\n background: var(--surface-2);\n}\n.fv__btn--on:hover {\n color: var(--accent);\n}\n\n/* \"Open with\" dropdown: anchored under its toggle button, right-aligned to the actions edge. */\n.fv__menu {\n position: relative;\n display: inline-flex;\n}\n.fv__menu-pop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 60;\n min-width: 184px;\n padding: 5px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.fv__menu-item {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 7px 9px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.fv__menu-item:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__menu-item--on {\n color: var(--accent);\n}\n.fv__menu-label {\n flex: 1 1 auto;\n min-width: 0;\n}\n.fv__menu-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.fv__menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--border);\n}\n\n@media (max-width: 860px) {\n /* Too narrow to split — overlay the viewer full-screen instead. */\n .fv {\n position: fixed;\n inset: 0;\n flex-basis: auto;\n width: 100%;\n z-index: 70;\n }\n .fv__resizer {\n display: none;\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 "./HarViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/HarViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-4b79eb8b"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-c882020f"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"HarViewer.js","names":[],"sources":["../../src/components/HarViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n type DecodedBody,\n type HarEntry,\n type HarNV,\n decodeContent,\n decodePostData,\n fmtMs,\n fmtSize,\n parseHar,\n parseQuery,\n prettyBody,\n statusClass,\n toCurl,\n} from '@/lib/har'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer HAR (`.har`) renderer. Fetches the archive text and parses the HTTP Archive entirely in-browser\n * (hand-rolled, see lib/har.ts) — no third-party. Lays out a master-detail vertical split tuned for the narrow\n * side pane: a filterable request list on top, and the selected request's detail (general / headers / query /\n * cookies / request + response body) below. Bodies are decoded (standard base64 + the «base64 but undeclared»\n * case some tools emit); images preview from a data URI, binaries show a hex dump.\n *\n * Safety: a HAR body is untrusted upstream content. We never execute it — text bodies render as escaped,\n * optionally syntax-highlighted source; image bodies render through a data-URI `<img>` (an `<img>` never runs\n * scripts, so even an SVG body is inert). No body is fetched as a live resource.\n */\nconst props = defineProps<{\n /** Download URL of the .har bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\ninterface HarState {\n entries: HarEntry[]\n creator: string\n version: string\n}\nconst doc = ref<HarState | null>(null)\nconst selected = ref<HarEntry | null>(null)\nconst query = ref('')\n\ntype DetailTab =\n | 'general'\n | 'reqHeaders'\n | 'resHeaders'\n | 'query'\n | 'cookies'\n | 'reqBody'\n | 'resBody'\nconst tab = ref<DetailTab>('general')\n\n// A HAR can be large (megabytes of inlined bodies); gate very big ones behind a confirm before parsing.\nconst SIZE_LIMIT = 25 * 1024 * 1024\n\nconst CATEGORY_LABELS: Record<string, string> = {\n doc: '文档',\n xhr: 'XHR',\n js: 'JS',\n css: 'CSS',\n img: '图片',\n font: '字体',\n media: '媒体',\n ws: 'WS',\n other: '其它',\n}\n\n// Category quick-filter: 'all' or a HarCategory; only categories present in the doc are offered.\nconst catFilter = ref<string>('all')\nconst presentCategories = computed(() => {\n const seen = new Set<string>()\n for (const e of doc.value?.entries ?? []) seen.add(e.category)\n return Object.keys(CATEGORY_LABELS).filter((c) => seen.has(c))\n})\n\nconst filtered = computed(() => {\n const list = doc.value?.entries ?? []\n const q = query.value.trim().toLowerCase()\n const cat = catFilter.value\n return list.filter((e) => {\n if (cat !== 'all' && e.category !== cat) return false\n if (!q) return true\n return (\n e.url.toLowerCase().includes(q) ||\n e.method.toLowerCase().includes(q) ||\n String(e.status).includes(q) ||\n e.type.toLowerCase().includes(q)\n )\n })\n})\n\n// Last path segment (or host) as the row's headline; the host shows as the dim sub-line.\nfunction entryName(e: HarEntry): string {\n const seg = e.pathname.split('/').filter(Boolean).pop()\n return seg || e.host || e.pathname || e.url\n}\n\nfunction pick(e: HarEntry): void {\n selected.value = e\n // Reset to a tab that has content: prefer general; it's always present.\n tab.value = 'general'\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n const parsed = parseHar(text)\n doc.value = { entries: parsed.entries, creator: parsed.creator, version: parsed.version }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\n/* ===== detail derivations ===== */\n\nconst reqHeaders = computed<HarNV[]>(() => selected.value?.raw.request.headers ?? [])\nconst resHeaders = computed<HarNV[]>(() => selected.value?.raw.response.headers ?? [])\nconst queryParams = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n // Prefer the explicit queryString; fall back to parsing the URL's search.\n const qs = e.raw.request.queryString\n if (qs.length) return qs\n const i = e.url.indexOf('?')\n return i >= 0 ? parseQuery(e.url.slice(i)) : []\n})\nconst cookies = computed(() => {\n const e = selected.value\n if (!e) return { req: [], res: [] }\n return { req: e.raw.request.cookies, res: e.raw.response.cookies }\n})\nconst hasCookies = computed(() => cookies.value.req.length + cookies.value.res.length > 0)\n\nconst reqBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodePostData(e.raw.request.postData, 'auto')\n return b.kind === 'empty' ? null : b\n})\nconst resBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodeContent(e.raw.response.content, 'auto')\n return b.kind === 'empty' ? null : b\n})\n\n// General info rows for the selected entry.\nconst generalRows = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n const r = e.raw\n const rows: HarNV[] = [\n { name: '请求 URL', value: e.url },\n { name: '方法', value: e.method },\n { name: '状态', value: e.status ? `${e.status} ${e.statusText}`.trim() : '(无响应)' },\n { name: '协议', value: r.response.httpVersion || r.request.httpVersion || '' },\n { name: '远端 IP', value: e.serverIP },\n { name: '响应类型', value: e.mime || '' },\n { name: '响应大小', value: fmtSize(e.size) },\n { name: '请求体大小', value: e.reqSize ? fmtSize(e.reqSize) : '' },\n { name: '耗时', value: fmtMs(e.time) },\n { name: '开始时间', value: e.startedISO },\n ]\n if (r.response.redirectURL) rows.push({ name: '重定向', value: r.response.redirectURL })\n return rows.filter((row) => row.value)\n})\n\n/** Render a decoded body to HTML for the <pre>: pretty-print JSON, syntax-highlight when we have a language. */\nfunction bodyHtml(body: DecodedBody): string {\n const { text, pretty } = prettyBody(body.text, body.mime)\n const lang = pretty ? 'json' : bodyLangFor(body.mime)\n return lang ? highlightCode(text, lang) : escapeHtml(text)\n}\nfunction bodyLangFor(mime: string): string {\n const m = mime.toLowerCase()\n if (m.includes('javascript') || m.includes('ecmascript')) return 'javascript'\n if (m === 'text/css') return 'css'\n if (m === 'text/html' || m.includes('xhtml')) return 'xml'\n if (m.includes('json')) return 'json'\n if (m.includes('svg') || m.includes('xml')) return 'xml'\n return ''\n}\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/* ===== copy helpers ===== */\n\nconst copiedKey = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyText(text: string, key: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedKey.value = key\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedKey.value = ''), 1400)\n}\nfunction copyCurl(): void {\n if (selected.value) void copyText(toCurl(selected.value), 'curl')\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"har\">\n <p v-if=\"status === 'loading'\" class=\"har__msg\">正在解析 HAR…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"har__msg har__gate\">\n <p>该 HAR 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"har__msg har__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && doc\">\n <!-- toolbar: filter + category chips + count -->\n <div class=\"har__bar\">\n <input\n v-model=\"query\"\n class=\"har__filter\"\n type=\"text\"\n placeholder=\"筛选 URL / 方法 / 状态…\"\n />\n <div class=\"har__cats\">\n <button\n type=\"button\"\n class=\"har__cat\"\n :class=\"{ 'har__cat--on': catFilter === 'all' }\"\n @click=\"catFilter = 'all'\"\n >\n 全部 {{ doc.entries.length }}\n </button>\n <button\n v-for=\"c in presentCategories\"\n :key=\"c\"\n type=\"button\"\n class=\"har__cat\"\n :class=\"[`har__cat--${c}`, { 'har__cat--on': catFilter === c }]\"\n @click=\"catFilter = c\"\n >\n {{ CATEGORY_LABELS[c] }}\n </button>\n </div>\n </div>\n\n <!-- master: request list -->\n <div class=\"har__list\" :class=\"{ 'har__list--split': selected }\">\n <p v-if=\"!filtered.length\" class=\"har__empty\">没有匹配的请求。</p>\n <button\n v-for=\"e in filtered\"\n :key=\"e.index\"\n type=\"button\"\n class=\"har__row\"\n :class=\"{ 'har__row--on': selected?.index === e.index }\"\n :title=\"e.url\"\n @click=\"pick(e)\"\n >\n <span class=\"har__method\" :class=\"`m-${e.method.toLowerCase()}`\">{{ e.method }}</span>\n <span class=\"har__status\" :class=\"statusClass(e.status)\">{{ e.status || '—' }}</span>\n <span class=\"har__name\">\n <span class=\"har__name-main\">{{ entryName(e) }}</span>\n <span class=\"har__name-sub\">{{ e.host }}</span>\n </span>\n <span class=\"har__type\">{{ e.type }}</span>\n <span class=\"har__size\">{{ fmtSize(e.size) }}</span>\n <span class=\"har__time\">{{ fmtMs(e.time) }}</span>\n </button>\n </div>\n\n <!-- detail: tabs + content -->\n <section v-if=\"selected\" class=\"har__detail\">\n <div class=\"har__tabs\">\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'general' }\"\n @click=\"tab = 'general'\"\n >\n 概览\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqHeaders' }\"\n @click=\"tab = 'reqHeaders'\"\n >\n 请求头 <span class=\"har__tab-n\">{{ reqHeaders.length }}</span>\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resHeaders' }\"\n @click=\"tab = 'resHeaders'\"\n >\n 响应头 <span class=\"har__tab-n\">{{ resHeaders.length }}</span>\n </button>\n <button\n v-if=\"queryParams.length\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'query' }\"\n @click=\"tab = 'query'\"\n >\n 查询 <span class=\"har__tab-n\">{{ queryParams.length }}</span>\n </button>\n <button\n v-if=\"hasCookies\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'cookies' }\"\n @click=\"tab = 'cookies'\"\n >\n Cookie\n </button>\n <button\n v-if=\"reqBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqBody' }\"\n @click=\"tab = 'reqBody'\"\n >\n 请求体\n </button>\n <button\n v-if=\"resBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resBody' }\"\n @click=\"tab = 'resBody'\"\n >\n 响应体\n </button>\n </div>\n\n <div class=\"har__pane\">\n <!-- general -->\n <template v-if=\"tab === 'general'\">\n <dl class=\"har__kv\">\n <div v-for=\"row in generalRows\" :key=\"row.name\" class=\"har__kv-row\">\n <dt>{{ row.name }}</dt>\n <dd>{{ row.value }}</dd>\n </div>\n </dl>\n <button type=\"button\" class=\"har__btn har__btn--sm\" @click=\"copyCurl\">\n {{ copiedKey === 'curl' ? '已复制' : '复制为 cURL' }}\n </button>\n </template>\n\n <!-- request headers -->\n <dl v-else-if=\"tab === 'reqHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in reqHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- response headers -->\n <dl v-else-if=\"tab === 'resHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in resHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- query string -->\n <dl v-else-if=\"tab === 'query'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in queryParams\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n\n <!-- cookies -->\n <template v-else-if=\"tab === 'cookies'\">\n <template v-if=\"cookies.req.length\">\n <h4 class=\"har__sub\">请求 Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.req\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n <template v-if=\"cookies.res.length\">\n <h4 class=\"har__sub\">响应 Set-Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.res\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n </template>\n\n <!-- request body -->\n <template v-else-if=\"tab === 'reqBody' && reqBody\">\n <dl v-if=\"reqBody.kind === 'form' && reqBody.form\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in reqBody.form\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n <template v-else>\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\"\n >{{ reqBody.mime || '文本' }} · {{ fmtSize(reqBody.size) }}</span\n >\n <button\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(reqBody.text, 'reqBody')\"\n >\n {{ copiedKey === 'reqBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <pre class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(reqBody)\" /></pre>\n </template>\n </template>\n\n <!-- response body -->\n <template v-else-if=\"tab === 'resBody' && resBody\">\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\">\n {{ resBody.mime || '文本' }} · {{ fmtSize(resBody.size) }}\n <span v-if=\"resBody.wasBase64\" class=\"har__b64\">base64 已解码</span>\n </span>\n <button\n v-if=\"!resBody.isImage\"\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(resBody.text, 'resBody')\"\n >\n {{ copiedKey === 'resBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <div v-if=\"resBody.isImage\" class=\"har__img\">\n <img :src=\"resBody.dataUri\" :alt=\"selected.url\" />\n </div>\n <pre v-else class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(resBody)\" /></pre>\n </template>\n </div>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\n.har {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.har__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.har__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.har__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.har__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.har__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__btn--sm {\n padding: 3px 10px;\n font-size: 11.5px;\n}\n\n/* toolbar */\n.har__bar {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.har__filter {\n width: 100%;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.har__filter:focus {\n outline: none;\n border-color: var(--accent);\n}\n.har__cats {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n.har__cat {\n font-family: inherit;\n font-size: 11px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 2px 9px;\n cursor: pointer;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.har__cat:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__cat--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n\n/* master list */\n.har__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n.har__list--split {\n flex: 0 0 42%;\n border-bottom: 1px solid var(--border);\n}\n.har__empty {\n padding: 18px 14px;\n font-size: 12.5px;\n color: var(--faint);\n}\n.har__row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 12px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.har__row:hover {\n background: var(--surface-2);\n}\n.har__row--on {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.har__method {\n flex-shrink: 0;\n width: 42px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.har__method.m-get {\n color: #2f9e44;\n}\n.har__method.m-post {\n color: #1c7ed6;\n}\n.har__method.m-put,\n.har__method.m-patch {\n color: #f08c00;\n}\n.har__method.m-delete {\n color: #e03131;\n}\n.har__status {\n flex-shrink: 0;\n width: 30px;\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n}\n.har__name {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.har__name-main {\n font-size: 12px;\n color: var(--text-strong);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__name-sub {\n font-size: 10.5px;\n color: var(--faint);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__type {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--muted);\n}\n.har__size,\n.har__time {\n flex-shrink: 0;\n width: 52px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--faint);\n}\n\n/* status colors (shared) */\n.s-2xx {\n color: #2f9e44;\n}\n.s-3xx {\n color: #1c7ed6;\n}\n.s-4xx {\n color: #f08c00;\n}\n.s-5xx {\n color: #e03131;\n}\n.s-pending {\n color: var(--faint);\n}\n\n/* detail */\n.har__detail {\n flex: 1 1 58%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.har__tabs {\n flex-shrink: 0;\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n white-space: nowrap;\n}\n.har__tab {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n padding: 6px 9px;\n cursor: pointer;\n}\n.har__tab:hover {\n color: var(--text);\n}\n.har__tab--on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n.har__tab-n {\n font-size: 10.5px;\n color: var(--faint);\n}\n.har__pane {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 12px 14px;\n}\n\n.har__kv {\n margin: 0 0 12px;\n display: flex;\n flex-direction: column;\n}\n.har__kv-row {\n display: flex;\n gap: 10px;\n padding: 4px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.har__kv-row dt {\n flex-shrink: 0;\n width: 38%;\n max-width: 180px;\n color: var(--muted);\n word-break: break-word;\n}\n.har__kv-row dd {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n color: var(--text);\n word-break: break-word;\n}\n.har__kv--mono dt,\n.har__kv--mono dd {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.har__sub {\n margin: 4px 0 8px;\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n\n.har__bodybar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 8px;\n}\n.har__bodymime {\n font-size: 11px;\n color: var(--faint);\n}\n.har__b64 {\n margin-left: 6px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--surface-2);\n color: var(--muted);\n}\n.har__body {\n margin: 0;\n padding: 10px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n}\n.har__body code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n.har__img {\n display: flex;\n justify-content: center;\n padding: 12px;\n background: var(--surface-2);\n border-radius: 8px;\n}\n.har__img img {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"HarViewer.js","names":[],"sources":["../../src/components/HarViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n type DecodedBody,\n type HarEntry,\n type HarNV,\n decodeContent,\n decodePostData,\n fmtMs,\n fmtSize,\n parseHar,\n parseQuery,\n prettyBody,\n statusClass,\n toCurl,\n} from '@/lib/har'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer HAR (`.har`) renderer. Fetches the archive text and parses the HTTP Archive entirely in-browser\n * (hand-rolled, see lib/har.ts) — no third-party. Lays out a master-detail vertical split tuned for the narrow\n * side pane: a filterable request list on top, and the selected request's detail (general / headers / query /\n * cookies / request + response body) below. Bodies are decoded (standard base64 + the «base64 but undeclared»\n * case some tools emit); images preview from a data URI, binaries show a hex dump.\n *\n * Safety: a HAR body is untrusted upstream content. We never execute it — text bodies render as escaped,\n * optionally syntax-highlighted source; image bodies render through a data-URI `<img>` (an `<img>` never runs\n * scripts, so even an SVG body is inert). No body is fetched as a live resource.\n */\nconst props = defineProps<{\n /** Download URL of the .har bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\ninterface HarState {\n entries: HarEntry[]\n creator: string\n version: string\n}\nconst doc = ref<HarState | null>(null)\nconst selected = ref<HarEntry | null>(null)\nconst query = ref('')\n\ntype DetailTab =\n | 'general'\n | 'reqHeaders'\n | 'resHeaders'\n | 'query'\n | 'cookies'\n | 'reqBody'\n | 'resBody'\nconst tab = ref<DetailTab>('general')\n\n// A HAR can be large (megabytes of inlined bodies); gate very big ones behind a confirm before parsing.\nconst SIZE_LIMIT = 25 * 1024 * 1024\n\nconst CATEGORY_LABELS: Record<string, string> = {\n doc: '文档',\n xhr: 'XHR',\n js: 'JS',\n css: 'CSS',\n img: '图片',\n font: '字体',\n media: '媒体',\n ws: 'WS',\n other: '其它',\n}\n\n// Category quick-filter: 'all' or a HarCategory; only categories present in the doc are offered.\nconst catFilter = ref<string>('all')\nconst presentCategories = computed(() => {\n const seen = new Set<string>()\n for (const e of doc.value?.entries ?? []) seen.add(e.category)\n return Object.keys(CATEGORY_LABELS).filter((c) => seen.has(c))\n})\n\nconst filtered = computed(() => {\n const list = doc.value?.entries ?? []\n const q = query.value.trim().toLowerCase()\n const cat = catFilter.value\n return list.filter((e) => {\n if (cat !== 'all' && e.category !== cat) return false\n if (!q) return true\n return (\n e.url.toLowerCase().includes(q) ||\n e.method.toLowerCase().includes(q) ||\n String(e.status).includes(q) ||\n e.type.toLowerCase().includes(q)\n )\n })\n})\n\n// Last path segment (or host) as the row's headline; the host shows as the dim sub-line.\nfunction entryName(e: HarEntry): string {\n const seg = e.pathname.split('/').filter(Boolean).pop()\n return seg || e.host || e.pathname || e.url\n}\n\nfunction pick(e: HarEntry): void {\n selected.value = e\n // Reset to a tab that has content: prefer general; it's always present.\n tab.value = 'general'\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n const parsed = parseHar(text)\n doc.value = { entries: parsed.entries, creator: parsed.creator, version: parsed.version }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\n/* ===== detail derivations ===== */\n\nconst reqHeaders = computed<HarNV[]>(() => selected.value?.raw.request.headers ?? [])\nconst resHeaders = computed<HarNV[]>(() => selected.value?.raw.response.headers ?? [])\nconst queryParams = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n // Prefer the explicit queryString; fall back to parsing the URL's search.\n const qs = e.raw.request.queryString\n if (qs.length) return qs\n const i = e.url.indexOf('?')\n return i >= 0 ? parseQuery(e.url.slice(i)) : []\n})\nconst cookies = computed(() => {\n const e = selected.value\n if (!e) return { req: [], res: [] }\n return { req: e.raw.request.cookies, res: e.raw.response.cookies }\n})\nconst hasCookies = computed(() => cookies.value.req.length + cookies.value.res.length > 0)\n\nconst reqBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodePostData(e.raw.request.postData, 'auto')\n return b.kind === 'empty' ? null : b\n})\nconst resBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodeContent(e.raw.response.content, 'auto')\n return b.kind === 'empty' ? null : b\n})\n\n// General info rows for the selected entry.\nconst generalRows = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n const r = e.raw\n const rows: HarNV[] = [\n { name: '请求 URL', value: e.url },\n { name: '方法', value: e.method },\n { name: '状态', value: e.status ? `${e.status} ${e.statusText}`.trim() : '(无响应)' },\n { name: '协议', value: r.response.httpVersion || r.request.httpVersion || '' },\n { name: '远端 IP', value: e.serverIP },\n { name: '响应类型', value: e.mime || '' },\n { name: '响应大小', value: fmtSize(e.size) },\n { name: '请求体大小', value: e.reqSize ? fmtSize(e.reqSize) : '' },\n { name: '耗时', value: fmtMs(e.time) },\n { name: '开始时间', value: e.startedISO },\n ]\n if (r.response.redirectURL) rows.push({ name: '重定向', value: r.response.redirectURL })\n return rows.filter((row) => row.value)\n})\n\n/** Render a decoded body to HTML for the <pre>: pretty-print JSON, syntax-highlight when we have a language. */\nfunction bodyHtml(body: DecodedBody): string {\n const { text, pretty } = prettyBody(body.text, body.mime)\n const lang = pretty ? 'json' : bodyLangFor(body.mime)\n return lang ? highlightCode(text, lang) : escapeHtml(text)\n}\nfunction bodyLangFor(mime: string): string {\n const m = mime.toLowerCase()\n if (m.includes('javascript') || m.includes('ecmascript')) return 'javascript'\n if (m === 'text/css') return 'css'\n if (m === 'text/html' || m.includes('xhtml')) return 'xml'\n if (m.includes('json')) return 'json'\n if (m.includes('svg') || m.includes('xml')) return 'xml'\n return ''\n}\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/* ===== copy helpers ===== */\n\nconst copiedKey = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyText(text: string, key: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedKey.value = key\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedKey.value = ''), 1400)\n}\nfunction copyCurl(): void {\n if (selected.value) void copyText(toCurl(selected.value), 'curl')\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"har\">\n <p v-if=\"status === 'loading'\" class=\"har__msg\">正在解析 HAR…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"har__msg har__gate\">\n <p>该 HAR 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"har__msg har__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && doc\">\n <!-- toolbar: filter + category chips + count -->\n <div class=\"har__bar\">\n <input\n v-model=\"query\"\n class=\"har__filter\"\n type=\"text\"\n placeholder=\"筛选 URL / 方法 / 状态…\"\n />\n <div class=\"har__cats\">\n <button\n type=\"button\"\n class=\"har__cat\"\n :class=\"{ 'har__cat--on': catFilter === 'all' }\"\n @click=\"catFilter = 'all'\"\n >\n 全部 {{ doc.entries.length }}\n </button>\n <button\n v-for=\"c in presentCategories\"\n :key=\"c\"\n type=\"button\"\n class=\"har__cat\"\n :class=\"[`har__cat--${c}`, { 'har__cat--on': catFilter === c }]\"\n @click=\"catFilter = c\"\n >\n {{ CATEGORY_LABELS[c] }}\n </button>\n </div>\n </div>\n\n <!-- master: request list -->\n <div class=\"har__list\" :class=\"{ 'har__list--split': selected }\">\n <p v-if=\"!filtered.length\" class=\"har__empty\">没有匹配的请求。</p>\n <button\n v-for=\"e in filtered\"\n :key=\"e.index\"\n type=\"button\"\n class=\"har__row\"\n :class=\"{ 'har__row--on': selected?.index === e.index }\"\n :title=\"e.url\"\n @click=\"pick(e)\"\n >\n <span class=\"har__method\" :class=\"`m-${e.method.toLowerCase()}`\">{{ e.method }}</span>\n <span class=\"har__status\" :class=\"statusClass(e.status)\">{{ e.status || '—' }}</span>\n <span class=\"har__name\">\n <span class=\"har__name-main\">{{ entryName(e) }}</span>\n <span class=\"har__name-sub\">{{ e.host }}</span>\n </span>\n <span class=\"har__type\">{{ e.type }}</span>\n <span class=\"har__size\">{{ fmtSize(e.size) }}</span>\n <span class=\"har__time\">{{ fmtMs(e.time) }}</span>\n </button>\n </div>\n\n <!-- detail: tabs + content -->\n <section v-if=\"selected\" class=\"har__detail\">\n <div class=\"har__tabs\">\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'general' }\"\n @click=\"tab = 'general'\"\n >\n 概览\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqHeaders' }\"\n @click=\"tab = 'reqHeaders'\"\n >\n 请求头 <span class=\"har__tab-n\">{{ reqHeaders.length }}</span>\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resHeaders' }\"\n @click=\"tab = 'resHeaders'\"\n >\n 响应头 <span class=\"har__tab-n\">{{ resHeaders.length }}</span>\n </button>\n <button\n v-if=\"queryParams.length\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'query' }\"\n @click=\"tab = 'query'\"\n >\n 查询 <span class=\"har__tab-n\">{{ queryParams.length }}</span>\n </button>\n <button\n v-if=\"hasCookies\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'cookies' }\"\n @click=\"tab = 'cookies'\"\n >\n Cookie\n </button>\n <button\n v-if=\"reqBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqBody' }\"\n @click=\"tab = 'reqBody'\"\n >\n 请求体\n </button>\n <button\n v-if=\"resBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resBody' }\"\n @click=\"tab = 'resBody'\"\n >\n 响应体\n </button>\n </div>\n\n <div class=\"har__pane\">\n <!-- general -->\n <template v-if=\"tab === 'general'\">\n <dl class=\"har__kv\">\n <div v-for=\"row in generalRows\" :key=\"row.name\" class=\"har__kv-row\">\n <dt>{{ row.name }}</dt>\n <dd>{{ row.value }}</dd>\n </div>\n </dl>\n <button type=\"button\" class=\"har__btn har__btn--sm\" @click=\"copyCurl\">\n {{ copiedKey === 'curl' ? '已复制' : '复制为 cURL' }}\n </button>\n </template>\n\n <!-- request headers -->\n <dl v-else-if=\"tab === 'reqHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in reqHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- response headers -->\n <dl v-else-if=\"tab === 'resHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in resHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- query string -->\n <dl v-else-if=\"tab === 'query'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in queryParams\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n\n <!-- cookies -->\n <template v-else-if=\"tab === 'cookies'\">\n <template v-if=\"cookies.req.length\">\n <h4 class=\"har__sub\">请求 Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.req\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n <template v-if=\"cookies.res.length\">\n <h4 class=\"har__sub\">响应 Set-Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.res\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n </template>\n\n <!-- request body -->\n <template v-else-if=\"tab === 'reqBody' && reqBody\">\n <dl v-if=\"reqBody.kind === 'form' && reqBody.form\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in reqBody.form\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n <template v-else>\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\"\n >{{ reqBody.mime || '文本' }} · {{ fmtSize(reqBody.size) }}</span\n >\n <button\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(reqBody.text, 'reqBody')\"\n >\n {{ copiedKey === 'reqBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <pre class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(reqBody)\" /></pre>\n </template>\n </template>\n\n <!-- response body -->\n <template v-else-if=\"tab === 'resBody' && resBody\">\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\">\n {{ resBody.mime || '文本' }} · {{ fmtSize(resBody.size) }}\n <span v-if=\"resBody.wasBase64\" class=\"har__b64\">base64 已解码</span>\n </span>\n <button\n v-if=\"!resBody.isImage\"\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(resBody.text, 'resBody')\"\n >\n {{ copiedKey === 'resBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <div v-if=\"resBody.isImage\" class=\"har__img\">\n <img :src=\"resBody.dataUri\" :alt=\"selected.url\" />\n </div>\n <pre v-else class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(resBody)\" /></pre>\n </template>\n </div>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\n.har {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.har__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.har__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.har__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.har__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.har__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__btn--sm {\n padding: 3px 10px;\n font-size: 11.5px;\n}\n\n/* toolbar */\n.har__bar {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.har__filter {\n width: 100%;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.har__filter:focus {\n outline: none;\n border-color: var(--accent);\n}\n.har__cats {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n.har__cat {\n font-family: inherit;\n font-size: 11px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 2px 9px;\n cursor: pointer;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.har__cat:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__cat--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n\n/* master list */\n.har__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n.har__list--split {\n flex: 0 0 42%;\n border-bottom: 1px solid var(--border);\n}\n.har__empty {\n padding: 18px 14px;\n font-size: 12.5px;\n color: var(--faint);\n}\n.har__row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 12px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.har__row:hover {\n background: var(--surface-2);\n}\n.har__row--on {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.har__method {\n flex-shrink: 0;\n width: 42px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.har__method.m-get {\n color: var(--hl-name);\n}\n.har__method.m-post {\n color: var(--hl-attr);\n}\n.har__method.m-put,\n.har__method.m-patch {\n color: var(--ansi-yellow);\n}\n.har__method.m-delete {\n color: var(--danger);\n}\n.har__status {\n flex-shrink: 0;\n width: 30px;\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n}\n.har__name {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.har__name-main {\n font-size: 12px;\n color: var(--text-strong);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__name-sub {\n font-size: 10.5px;\n color: var(--faint);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__type {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--muted);\n}\n.har__size,\n.har__time {\n flex-shrink: 0;\n width: 52px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--faint);\n}\n\n/* status colors (shared) */\n.s-2xx {\n color: var(--hl-name);\n}\n.s-3xx {\n color: var(--hl-attr);\n}\n.s-4xx {\n color: var(--ansi-yellow);\n}\n.s-5xx {\n color: var(--danger);\n}\n.s-pending {\n color: var(--faint);\n}\n\n/* detail */\n.har__detail {\n flex: 1 1 58%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.har__tabs {\n flex-shrink: 0;\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n white-space: nowrap;\n}\n.har__tab {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n padding: 6px 9px;\n cursor: pointer;\n}\n.har__tab:hover {\n color: var(--text);\n}\n.har__tab--on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n.har__tab-n {\n font-size: 10.5px;\n color: var(--faint);\n}\n.har__pane {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 12px 14px;\n}\n\n.har__kv {\n margin: 0 0 12px;\n display: flex;\n flex-direction: column;\n}\n.har__kv-row {\n display: flex;\n gap: 10px;\n padding: 4px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.har__kv-row dt {\n flex-shrink: 0;\n width: 38%;\n max-width: 180px;\n color: var(--muted);\n word-break: break-word;\n}\n.har__kv-row dd {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n color: var(--text);\n word-break: break-word;\n}\n.har__kv--mono dt,\n.har__kv--mono dd {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.har__sub {\n margin: 4px 0 8px;\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n\n.har__bodybar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 8px;\n}\n.har__bodymime {\n font-size: 11px;\n color: var(--faint);\n}\n.har__b64 {\n margin-left: 6px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--surface-2);\n color: var(--muted);\n}\n.har__body {\n margin: 0;\n padding: 10px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n}\n.har__body code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n.har__img {\n display: flex;\n justify-content: center;\n padding: 12px;\n background: var(--surface-2);\n border-radius: 8px;\n}\n.har__img img {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n</style>\n"],"mappings":""}