@l1yp/file-viewer 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ArchiveBrowser.js +1 -1
- package/dist/components/ArchiveBrowser.js.map +1 -1
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +105 -94
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerBody.js +1 -1
- package/dist/components/FileViewerBody.js.map +1 -1
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ImageLightbox.js +1 -1
- package/dist/components/ImageLightbox.js.map +1 -1
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +101 -55
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/JsonlViewer.js +1 -1
- package/dist/components/JsonlViewer.js.map +1 -1
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/LightboxGalleryStrip.js +9 -0
- package/dist/components/LightboxGalleryStrip.js.map +1 -0
- package/dist/components/LightboxGalleryStrip.vue.d.ts +12 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfNavigationPane.js +9 -0
- package/dist/components/PdfNavigationPane.js.map +1 -0
- package/dist/components/PdfNavigationPane.vue.d.ts +24 -0
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js +149 -0
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfViewer.js +1 -1
- package/dist/components/PdfViewer.js.map +1 -1
- package/dist/components/PdfViewer.vue.d.ts +0 -11
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +131 -69
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/SevenZipBrowser.js +1 -1
- package/dist/components/SevenZipBrowser.js.map +1 -1
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +157 -146
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZipBrowser.js +1 -1
- package/dist/components/ZipBrowser.js.map +1 -1
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +70 -61
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZoomImage.js +1 -1
- package/dist/components/ZoomImage.js.map +1 -1
- package/dist/components/ZoomImage.vue.d.ts +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +104 -95
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useArchiveImageGallery.d.ts +15 -0
- package/dist/composables/useArchiveImageGallery.js +62 -0
- package/dist/composables/useArchiveImageGallery.js.map +1 -0
- package/dist/composables/useLightbox.d.ts +29 -21
- package/dist/composables/useLightbox.js +72 -12
- package/dist/composables/useLightbox.js.map +1 -1
- package/dist/lib/archiveImages.d.ts +14 -0
- package/dist/lib/archiveImages.js +15 -0
- package/dist/lib/archiveImages.js.map +1 -0
- package/dist/lib/pdfNavigation.d.ts +11 -0
- package/dist/lib/pdfNavigation.js +32 -0
- package/dist/lib/pdfNavigation.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -4,33 +4,34 @@ import { formatBytes as a } from "../lib/format.js";
|
|
|
4
4
|
import o from "./icons/Icon.js";
|
|
5
5
|
import { decodeTextSniff as te } from "../lib/encoding.js";
|
|
6
6
|
import { ZipEncryptedError as s, ZipPasswordError as c, extractZipEntry as l, readZip as u, zipSupported as ne } from "../lib/zip.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { useArchiveImageGallery as re } from "../composables/useArchiveImageGallery.js";
|
|
8
|
+
import { buildArchiveHtmlPreview as ie } from "../lib/archiveHtml.js";
|
|
9
|
+
import { Fragment as d, computed as f, createBlock as p, createCommentVNode as m, createElementBlock as h, createElementVNode as g, createVNode as _, defineAsyncComponent as v, defineComponent as y, normalizeClass as b, normalizeStyle as ae, onBeforeUnmount as oe, onMounted as se, openBlock as x, reactive as ce, ref as S, renderList as le, toDisplayString as C, unref as w, vModelText as ue, watch as de, withDirectives as fe, withKeys as pe } from "vue";
|
|
9
10
|
//#region src/components/ZipBrowser.vue?vue&type=script&setup=true&lang.ts
|
|
10
|
-
var
|
|
11
|
+
var me = { class: "zip" }, he = {
|
|
11
12
|
key: 0,
|
|
12
13
|
class: "zip__msg"
|
|
13
|
-
},
|
|
14
|
+
}, ge = {
|
|
14
15
|
key: 1,
|
|
15
16
|
class: "zip__msg zip__msg--error"
|
|
16
|
-
},
|
|
17
|
+
}, _e = { class: "zip__bar" }, ve = ["title"], ye = { class: "zip__bsize" }, be = {
|
|
17
18
|
key: 0,
|
|
18
19
|
class: "zip__view",
|
|
19
20
|
role: "group",
|
|
20
21
|
"aria-label": "渲染或源码"
|
|
21
|
-
},
|
|
22
|
+
}, xe = ["title"], Se = {
|
|
22
23
|
key: 0,
|
|
23
24
|
class: "zip__msg"
|
|
24
|
-
},
|
|
25
|
+
}, Ce = {
|
|
25
26
|
key: 1,
|
|
26
27
|
class: "zip__msg zip__msg--error"
|
|
27
|
-
},
|
|
28
|
+
}, we = { class: "zip__summary" }, Te = ["placeholder"], Ee = {
|
|
28
29
|
key: 0,
|
|
29
30
|
class: "zip__pwerr"
|
|
30
|
-
},
|
|
31
|
+
}, De = { class: "zip__scroll zip__tree" }, Oe = {
|
|
31
32
|
key: 0,
|
|
32
33
|
class: "zip__msg"
|
|
33
|
-
},
|
|
34
|
+
}, ke = ["onClick"], Ae = { class: "zip__twisty" }, je = { class: "zip__rowicon" }, Me = { class: "zip__rowname" }, Ne = {
|
|
34
35
|
key: 1,
|
|
35
36
|
class: "zip__rowsize"
|
|
36
37
|
}, T = /*@__PURE__*/ y({
|
|
@@ -40,14 +41,14 @@ var pe = { class: "zip" }, me = {
|
|
|
40
41
|
name: {}
|
|
41
42
|
},
|
|
42
43
|
setup(y) {
|
|
43
|
-
let T = v(() => import("./DicomViewer.js")),
|
|
44
|
+
let T = v(() => import("./DicomViewer.js")), Pe = v(() => import("./FileViewerBody.js")), E = y, D = S("loading"), O = S(""), k = null, A = S([]), j = S(""), M = S(""), N = S(!1), Fe = f(() => A.value.some((e) => e.encrypted)), P = S({
|
|
44
45
|
name: "",
|
|
45
46
|
path: "",
|
|
46
47
|
isDir: !0,
|
|
47
48
|
size: 0,
|
|
48
49
|
children: []
|
|
49
|
-
}), F =
|
|
50
|
-
function
|
|
50
|
+
}), F = ce(/* @__PURE__ */ new Set()), Ie = f(() => A.value.reduce((e, t) => e + +!t.isDir, 0)), Le = f(() => A.value.reduce((e, t) => e + (t.isDir ? 0 : t.size), 0));
|
|
51
|
+
function Re(e) {
|
|
51
52
|
let t = {
|
|
52
53
|
name: "",
|
|
53
54
|
path: "",
|
|
@@ -101,7 +102,7 @@ var pe = { class: "zip" }, me = {
|
|
|
101
102
|
return t(P.value.children, 0), e;
|
|
102
103
|
});
|
|
103
104
|
async function R() {
|
|
104
|
-
if (J++, D.value = "loading", O.value = "", z.value = null, U.value = null, W.value = "", j.value = "", M.value = "", N.value = !1, X(), Y.value = [], !ne()) {
|
|
105
|
+
if (J++, Ge.clear(), D.value = "loading", O.value = "", z.value = null, U.value = null, W.value = "", j.value = "", M.value = "", N.value = !1, X(), Y.value = [], !ne()) {
|
|
105
106
|
O.value = "当前浏览器不支持在前端解压 ZIP(缺少 DecompressionStream)", D.value = "error";
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
@@ -110,12 +111,12 @@ var pe = { class: "zip" }, me = {
|
|
|
110
111
|
if (!e.ok) throw Error(`HTTP ${e.status}`);
|
|
111
112
|
k = await e.arrayBuffer();
|
|
112
113
|
let t = u(k);
|
|
113
|
-
A.value = t, P.value =
|
|
114
|
+
A.value = t, P.value = Re(t), F.clear(), D.value = "ready";
|
|
114
115
|
} catch (e) {
|
|
115
116
|
O.value = e instanceof Error ? e.message : String(e), D.value = "error";
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
|
-
let z = S(null), B = S("ready"), V = S(""), H = S(""), U = S(null), W = S(""), G = S(!1), K = S("render"), q = S(""), J = 0, Y = S([]),
|
|
119
|
+
let z = S(null), B = S("ready"), V = S(""), H = S(""), U = S(null), W = S(""), G = S(!1), K = S("render"), q = S(""), J = 0, Y = S([]), ze = S(0), Be = S(""), Ve = f(() => Y.value.reduce((e, t) => e + (t.size ?? 0), 0)), He = f(() => r(U.value, H.value));
|
|
119
120
|
function X() {
|
|
120
121
|
q.value &&= (URL.revokeObjectURL(q.value), "");
|
|
121
122
|
}
|
|
@@ -123,25 +124,33 @@ var pe = { class: "zip" }, me = {
|
|
|
123
124
|
let t = e.lastIndexOf("/");
|
|
124
125
|
return t < 0 ? "" : e.slice(0, t);
|
|
125
126
|
}
|
|
126
|
-
function
|
|
127
|
+
function Ue(e) {
|
|
127
128
|
let n = Z(e.path);
|
|
128
129
|
return A.value.filter((e) => !e.isDir && t(e.name) && Z(e.path) === n).sort((e, t) => e.path.localeCompare(t.path, void 0, {
|
|
129
130
|
numeric: !0,
|
|
130
131
|
sensitivity: "base"
|
|
131
132
|
}));
|
|
132
133
|
}
|
|
133
|
-
function
|
|
134
|
+
function We(t, r) {
|
|
134
135
|
if (n(r)) return !0;
|
|
135
136
|
if (e(r)) return !1;
|
|
136
137
|
let i = Math.min(t.length, 4096);
|
|
137
138
|
for (let e = 0; e < i; e++) if (t[e] === 0) return !1;
|
|
138
139
|
return !0;
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
let Ge = re({
|
|
142
|
+
entries: () => A.value,
|
|
143
|
+
archiveName: () => E.name,
|
|
144
|
+
extract: async (e) => {
|
|
145
|
+
if (!k) throw Error("ZIP 尚未加载");
|
|
146
|
+
return l(k, e, j.value);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
async function Ke(e) {
|
|
141
150
|
let n = ++J, r = "";
|
|
142
151
|
if (z.value = e, B.value = "loading", V.value = "", H.value = "", U.value = null, W.value = "", G.value = !1, K.value = "render", X(), Y.value = [], k) try {
|
|
143
152
|
if (t(e.name)) {
|
|
144
|
-
let t =
|
|
153
|
+
let t = Ue(e);
|
|
145
154
|
if (t.length > 1) {
|
|
146
155
|
Y.value = t.map((e) => ({
|
|
147
156
|
name: e.name,
|
|
@@ -150,7 +159,7 @@ var pe = { class: "zip" }, me = {
|
|
|
150
159
|
let t = await l(k, e, j.value), n = new ArrayBuffer(t.byteLength);
|
|
151
160
|
return new Uint8Array(n).set(t), n;
|
|
152
161
|
}
|
|
153
|
-
})),
|
|
162
|
+
})), ze.value = Math.max(0, t.findIndex((t) => t.path === e.path)), Be.value = Z(e.path).split("/").pop() || e.name, G.value = !0, B.value = "ready";
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
156
165
|
}
|
|
@@ -162,12 +171,12 @@ var pe = { class: "zip" }, me = {
|
|
|
162
171
|
name: e.name,
|
|
163
172
|
url: s,
|
|
164
173
|
size: e.size,
|
|
165
|
-
textHint:
|
|
174
|
+
textHint: We(o, e.name)
|
|
166
175
|
}), u = "";
|
|
167
176
|
if (c.html) {
|
|
168
177
|
let t = te(o);
|
|
169
178
|
c.content = t;
|
|
170
|
-
let i = await
|
|
179
|
+
let i = await ie({
|
|
171
180
|
html: t,
|
|
172
181
|
documentPath: e.path,
|
|
173
182
|
entries: A.value,
|
|
@@ -183,12 +192,12 @@ var pe = { class: "zip" }, me = {
|
|
|
183
192
|
URL.revokeObjectURL(s), r = "";
|
|
184
193
|
return;
|
|
185
194
|
}
|
|
186
|
-
q.value = s, r = "", W.value = u, U.value = c, B.value = "ready";
|
|
195
|
+
q.value = s, r = "", W.value = u, U.value = c, B.value = "ready", c.image && Ge.open(e, o);
|
|
187
196
|
} catch (e) {
|
|
188
197
|
r && URL.revokeObjectURL(r), e instanceof s ? V.value = "该文件已加密:请返回列表并在顶部输入密码" : e instanceof c ? (V.value = "密码错误", N.value = !1) : V.value = e instanceof Error ? e.message : String(e), B.value = "error";
|
|
189
198
|
}
|
|
190
199
|
}
|
|
191
|
-
async function
|
|
200
|
+
async function qe() {
|
|
192
201
|
M.value = "";
|
|
193
202
|
let e = A.value.find((e) => !e.isDir && e.encrypted && e.size > 0);
|
|
194
203
|
if (!e || !k) {
|
|
@@ -196,42 +205,42 @@ var pe = { class: "zip" }, me = {
|
|
|
196
205
|
return;
|
|
197
206
|
}
|
|
198
207
|
try {
|
|
199
|
-
await l(k, e, j.value), N.value = !0, z.value &&
|
|
208
|
+
await l(k, e, j.value), N.value = !0, z.value && Ke(z.value);
|
|
200
209
|
} catch (e) {
|
|
201
210
|
N.value = !1, M.value = e instanceof c ? "密码错误" : e instanceof Error ? e.message : String(e);
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
|
-
function
|
|
205
|
-
e.isDir ? F.has(e.path) ? F.delete(e.path) : F.add(e.path) : e.entry &&
|
|
213
|
+
function Je(e) {
|
|
214
|
+
e.isDir ? F.has(e.path) ? F.delete(e.path) : F.add(e.path) : e.entry && Ke(e.entry);
|
|
206
215
|
}
|
|
207
|
-
function
|
|
216
|
+
function Ye() {
|
|
208
217
|
J++, z.value = null, H.value = "", U.value = null, W.value = "", G.value = !1, X(), Y.value = [];
|
|
209
218
|
}
|
|
210
|
-
async function
|
|
219
|
+
async function Xe(e) {
|
|
211
220
|
if (k) try {
|
|
212
221
|
let t = URL.createObjectURL(new Blob([await l(k, e, j.value)])), n = document.createElement("a");
|
|
213
222
|
n.href = t, n.download = e.name, document.body.appendChild(n), n.click(), n.remove(), setTimeout(() => URL.revokeObjectURL(t), 1e3);
|
|
214
223
|
} catch {}
|
|
215
224
|
}
|
|
216
225
|
let Q = S(!1), $;
|
|
217
|
-
async function
|
|
226
|
+
async function Ze() {
|
|
218
227
|
try {
|
|
219
228
|
await navigator.clipboard?.writeText(H.value);
|
|
220
229
|
} catch {}
|
|
221
230
|
Q.value = !0, clearTimeout($), $ = setTimeout(() => Q.value = !1, 1400);
|
|
222
231
|
}
|
|
223
|
-
return
|
|
232
|
+
return se(R), de(() => E.url, R), oe(() => {
|
|
224
233
|
J++, X(), clearTimeout($);
|
|
225
|
-
}), (e, t) => (x(), h("div",
|
|
234
|
+
}), (e, t) => (x(), h("div", me, [D.value === "loading" ? (x(), h("p", he, "正在解压…")) : D.value === "error" ? (x(), h("div", ge, [g("p", null, C(O.value), 1), g("button", {
|
|
226
235
|
type: "button",
|
|
227
236
|
class: "zip__retry",
|
|
228
237
|
onClick: R
|
|
229
|
-
}, "重试")])) : z.value ? (x(), h(d, { key: 2 }, [g("div",
|
|
238
|
+
}, "重试")])) : z.value ? (x(), h(d, { key: 2 }, [g("div", _e, [
|
|
230
239
|
g("button", {
|
|
231
240
|
type: "button",
|
|
232
241
|
class: "zip__back",
|
|
233
242
|
title: "返回列表",
|
|
234
|
-
onClick:
|
|
243
|
+
onClick: Ye
|
|
235
244
|
}, [_(o, {
|
|
236
245
|
name: "arrowLeft",
|
|
237
246
|
size: 16
|
|
@@ -239,9 +248,9 @@ var pe = { class: "zip" }, me = {
|
|
|
239
248
|
g("span", {
|
|
240
249
|
class: "zip__path",
|
|
241
250
|
title: z.value.path
|
|
242
|
-
}, C(z.value.path), 9,
|
|
243
|
-
g("span",
|
|
244
|
-
|
|
251
|
+
}, C(z.value.path), 9, ve),
|
|
252
|
+
g("span", ye, C(w(a)(z.value.size)), 1),
|
|
253
|
+
He.value ? (x(), h("div", be, [g("button", {
|
|
245
254
|
type: "button",
|
|
246
255
|
class: b(["zip__vbtn", { "zip__vbtn--on": K.value === "render" }]),
|
|
247
256
|
title: "渲染",
|
|
@@ -263,32 +272,32 @@ var pe = { class: "zip" }, me = {
|
|
|
263
272
|
type: "button",
|
|
264
273
|
class: "zip__act",
|
|
265
274
|
title: Q.value ? "已复制" : "复制",
|
|
266
|
-
onClick:
|
|
275
|
+
onClick: Ze
|
|
267
276
|
}, [_(o, {
|
|
268
277
|
name: "copy",
|
|
269
278
|
size: 14
|
|
270
|
-
})], 8,
|
|
279
|
+
})], 8, xe)) : m("", !0),
|
|
271
280
|
g("button", {
|
|
272
281
|
type: "button",
|
|
273
282
|
class: "zip__act",
|
|
274
283
|
title: "下载",
|
|
275
|
-
onClick: t[2] ||= (e) =>
|
|
284
|
+
onClick: t[2] ||= (e) => Xe(z.value)
|
|
276
285
|
}, [_(o, {
|
|
277
286
|
name: "download",
|
|
278
287
|
size: 15
|
|
279
288
|
})])
|
|
280
|
-
]), B.value === "loading" ? (x(), h("p",
|
|
289
|
+
]), B.value === "loading" ? (x(), h("p", Se, "读取中…")) : B.value === "error" ? (x(), h("div", Ce, [g("p", null, C(V.value), 1)])) : G.value ? (x(), p(w(T), {
|
|
281
290
|
key: 2,
|
|
282
291
|
slices: Y.value,
|
|
283
|
-
start:
|
|
284
|
-
name:
|
|
285
|
-
size:
|
|
292
|
+
start: ze.value,
|
|
293
|
+
name: Be.value,
|
|
294
|
+
size: Ve.value
|
|
286
295
|
}, null, 8, [
|
|
287
296
|
"slices",
|
|
288
297
|
"start",
|
|
289
298
|
"name",
|
|
290
299
|
"size"
|
|
291
|
-
])) : U.value ? (x(), p(w(
|
|
300
|
+
])) : U.value ? (x(), p(w(Pe), {
|
|
292
301
|
key: 3,
|
|
293
302
|
file: U.value,
|
|
294
303
|
mode: K.value,
|
|
@@ -299,8 +308,8 @@ var pe = { class: "zip" }, me = {
|
|
|
299
308
|
"mode",
|
|
300
309
|
"html-preview"
|
|
301
310
|
])) : m("", !0)], 64)) : (x(), h(d, { key: 3 }, [
|
|
302
|
-
g("div",
|
|
303
|
-
|
|
311
|
+
g("div", we, C(Ie.value) + " 个文件 · " + C(w(a)(Le.value)), 1),
|
|
312
|
+
Fe.value ? (x(), h("div", {
|
|
304
313
|
key: 0,
|
|
305
314
|
class: b(["zip__pw", { "zip__pw--ok": N.value }])
|
|
306
315
|
}, [
|
|
@@ -308,38 +317,38 @@ var pe = { class: "zip" }, me = {
|
|
|
308
317
|
name: "key",
|
|
309
318
|
size: 14
|
|
310
319
|
}),
|
|
311
|
-
|
|
320
|
+
fe(g("input", {
|
|
312
321
|
"onUpdate:modelValue": t[4] ||= (e) => j.value = e,
|
|
313
322
|
type: "password",
|
|
314
323
|
class: "zip__pwinput",
|
|
315
324
|
placeholder: N.value ? "已解锁(如需可重新输入)" : "此压缩包已加密,输入密码解锁",
|
|
316
|
-
onKeyup:
|
|
317
|
-
}, null, 40,
|
|
325
|
+
onKeyup: pe(qe, ["enter"])
|
|
326
|
+
}, null, 40, Te), [[ue, j.value]]),
|
|
318
327
|
g("button", {
|
|
319
328
|
type: "button",
|
|
320
329
|
class: "zip__pwbtn",
|
|
321
|
-
onClick:
|
|
330
|
+
onClick: qe
|
|
322
331
|
}, C(N.value ? "已解锁" : "解锁"), 1),
|
|
323
|
-
M.value ? (x(), h("span",
|
|
332
|
+
M.value ? (x(), h("span", Ee, C(M.value), 1)) : m("", !0)
|
|
324
333
|
], 2)) : m("", !0),
|
|
325
|
-
g("div",
|
|
334
|
+
g("div", De, [L.value.length === 0 ? (x(), h("p", Oe, "空压缩包")) : m("", !0), (x(!0), h(d, null, le(L.value, (e) => (x(), h("button", {
|
|
326
335
|
key: e.node.path,
|
|
327
336
|
type: "button",
|
|
328
337
|
class: b(["zip__row", { "zip__row--dir": e.node.isDir }]),
|
|
329
|
-
style:
|
|
330
|
-
onClick: (t) =>
|
|
338
|
+
style: ae({ paddingLeft: `${12 + e.depth * 14}px` }),
|
|
339
|
+
onClick: (t) => Je(e.node)
|
|
331
340
|
}, [
|
|
332
|
-
g("span",
|
|
341
|
+
g("span", Ae, [e.node.isDir ? (x(), p(o, {
|
|
333
342
|
key: 0,
|
|
334
343
|
name: "chevronRight",
|
|
335
344
|
size: 13,
|
|
336
345
|
class: b({ "zip__twisty--open": F.has(e.node.path) })
|
|
337
346
|
}, null, 8, ["class"])) : m("", !0)]),
|
|
338
|
-
g("span",
|
|
347
|
+
g("span", je, [_(o, {
|
|
339
348
|
name: e.node.isDir ? "folder" : "fileOutline",
|
|
340
349
|
size: 15
|
|
341
350
|
}, null, 8, ["name"])]),
|
|
342
|
-
g("span",
|
|
351
|
+
g("span", Me, C(e.node.name), 1),
|
|
343
352
|
e.node.entry?.encrypted ? (x(), p(o, {
|
|
344
353
|
key: 0,
|
|
345
354
|
name: "key",
|
|
@@ -347,8 +356,8 @@ var pe = { class: "zip" }, me = {
|
|
|
347
356
|
class: "zip__rowlock",
|
|
348
357
|
title: "已加密"
|
|
349
358
|
})) : m("", !0),
|
|
350
|
-
e.node.isDir ? m("", !0) : (x(), h("span",
|
|
351
|
-
], 14,
|
|
359
|
+
e.node.isDir ? m("", !0) : (x(), h("span", Ne, C(w(a)(e.node.size)), 1))
|
|
360
|
+
], 14, ke))), 128))])
|
|
352
361
|
], 64))]));
|
|
353
362
|
}
|
|
354
363
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZipBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractZipEntry,\n readZip,\n zipSupported,\n ZipEncryptedError,\n ZipPasswordError,\n type ZipEntry,\n} from '@/lib/zip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { buildArchiveHtmlPreview } from '@/lib/archiveHtml'\nimport { decodeTextSniff } from '@/lib/encoding'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — FileViewerBody has no slice model, so the\n// DICOM stack stays a direct hand-off here (a lone .dcm goes through the body like any other entry).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy-imported\n// to break the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's ZIP mode: fetch an archive's bytes, decompress them entirely in-browser (see lib/zip.ts),\n * and browse the contents. A master→detail within the narrow pane — a collapsible file tree, and selecting an\n * entry extracts just that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file\n * inside the zip previews with every viewer a top-level file gets — code/image/pdf/cert/audio/nested-zip/…).\n * HTML is the one multi-entry case: its relative JS/CSS/image/font dependencies are resolved from the same ZIP\n * and rewritten before entering the existing sandboxed iframe. Other entry bytes are not retained; selecting\n * decodes on demand, and the download button re-extracts.\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nconst entries = ref<ZipEntry[]>([])\n\n// Encrypted archives: a password bar appears once any entry is encrypted. The password is held only in memory\n// and passed to each extraction (ZipCrypto / WinZip AES decrypt happens in lib/zip.ts).\nconst password = ref('')\nconst pwError = ref('')\nconst unlocked = ref(false)\nconst anyEncrypted = computed(() => entries.value.some((e) => e.encrypted))\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n selectionRun++\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n entryFile.value = null\n entryHtmlPreview.value = ''\n password.value = ''\n pwError.value = ''\n unlocked.value = false\n revokeBlob()\n stackSlices.value = []\n if (!zipSupported()) {\n errorMsg.value = '当前浏览器不支持在前端解压 ZIP(缺少 DecompressionStream)'\n status.value = 'error'\n return\n }\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n const list = readZip(buffer)\n entries.value = list\n root.value = buildTree(list)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready') // covers extraction; the body owns loading once entryFile is set\nconst entryErr = ref('')\nconst entryText = ref('') // the body emits loaded text up so the copy button can use it\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryHtmlPreview = ref('') // dependency-rewritten HTML; source/copy still use the original text\nconst entryStack = ref(false) // DICOM series (handled directly, not via the body)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('') // entry blob URL fed to the body (revoked on change/unmount)\nlet selectionRun = 0 // stale guard for async HTML dependency extraction (including selecting the same row twice)\n\n// DICOM series stack: when the clicked .dcm has sibling .dcm files in the same folder, they are browsed as\n// one scrollable series. Slices extract lazily; the DICOM viewer caches/prefetches them.\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ZipEntry): ZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs. Feeds the\n// body's source fall-back so an extensionless text file still opens as source instead of a download card.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nasync function selectEntry(e: ZipEntry): Promise<void> {\n const run = ++selectionRun\n let pendingUrl = ''\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer) return\n try {\n // DICOM series: a folder holding several .dcm → browse them as one scrollable series (slices on demand).\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = await extractZipEntry(buffer as ArrayBuffer, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const archiveBuffer = buffer\n const bytes = await extractZipEntry(archiveBuffer, e, password.value)\n if (selectionRun !== run || selected.value !== e) return\n const url = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n pendingUrl = url\n const file = classifyViewerFile({\n name: e.name,\n url,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n let htmlPreview = ''\n if (file.html) {\n const source = decodeTextSniff(bytes)\n file.content = source\n const preview = await buildArchiveHtmlPreview({\n html: source,\n documentPath: e.path,\n entries: entries.value,\n loadEntry: (dependency) => extractZipEntry(archiveBuffer, dependency, password.value),\n })\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n htmlPreview = preview.html\n }\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n blobUrl.value = url\n pendingUrl = ''\n entryHtmlPreview.value = htmlPreview\n entryFile.value = file\n entryStatus.value = 'ready'\n } catch (err) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (err instanceof ZipEncryptedError)\n entryErr.value = '该文件已加密:请返回列表并在顶部输入密码'\n else if (err instanceof ZipPasswordError) {\n entryErr.value = '密码错误'\n unlocked.value = false\n } else entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\n// Validate the entered password against the first encrypted entry, then unlock (and re-open any selected file).\nasync function applyPassword(): Promise<void> {\n pwError.value = ''\n const enc = entries.value.find((e) => !e.isDir && e.encrypted && e.size > 0)\n if (!enc || !buffer) {\n unlocked.value = true\n return\n }\n try {\n await extractZipEntry(buffer, enc, password.value)\n unlocked.value = true\n if (selected.value) void selectEntry(selected.value)\n } catch (e) {\n unlocked.value = false\n pwError.value =\n e instanceof ZipPasswordError ? '密码错误' : e instanceof Error ? e.message : String(e)\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selectionRun++\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand so we don't retain decompressed bytes for the whole session.\nasync function downloadEntry(e: ZipEntry): Promise<void> {\n if (!buffer) return\n try {\n const url = URL.createObjectURL(new Blob([await extractZipEntry(buffer, e, password.value)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n selectionRun++\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"zip\">\n <p v-if=\"status === 'loading'\" class=\"zip__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"zip__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"zip__bar\">\n <button type=\"button\" class=\"zip__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"zip__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"zip__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"zip__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"zip__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"zip__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"zip__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n :html-preview=\"entryHtmlPreview || undefined\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"zip__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when the archive has encrypted entries (ZipCrypto / WinZip AES) -->\n <div v-if=\"anyEncrypted\" class=\"zip__pw\" :class=\"{ 'zip__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"zip__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包已加密,输入密码解锁'\"\n @keyup.enter=\"applyPassword\"\n />\n <button type=\"button\" class=\"zip__pwbtn\" @click=\"applyPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"zip__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"zip__scroll zip__tree\">\n <p v-if=\"rows.length === 0\" class=\"zip__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"zip__row\"\n :class=\"{ 'zip__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"zip__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'zip__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"zip__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"zip__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"zip__rowlock\"\n title=\"已加密\"\n />\n <span v-if=\"!row.node.isDir\" class=\"zip__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.zip {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.zip__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.zip__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.zip__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.zip__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.zip__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n/* Password bar (encrypted archives). */\n.zip__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.zip__pw--ok {\n color: var(--accent);\n}\n.zip__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.zip__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.zip__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.zip__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.zip__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.zip__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.zip__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.zip__tree {\n padding: 6px 0;\n}\n.zip__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.zip__row:hover {\n background: var(--surface-2);\n}\n.zip__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.zip__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.zip__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.zip__row--dir .zip__rowicon {\n color: var(--accent);\n}\n.zip__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.zip__row--dir .zip__rowname {\n font-weight: 500;\n}\n.zip__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.zip__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.zip__back,\n.zip__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.zip__back:hover,\n.zip__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.zip__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.zip__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n/* Render/source toggle (when the selected entry supports it) — compact segmented control. */\n.zip__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.zip__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.zip__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ZipBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ZipBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport {\n extractZipEntry,\n readZip,\n zipSupported,\n ZipEncryptedError,\n ZipPasswordError,\n type ZipEntry,\n} from '@/lib/zip'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useArchiveImageGallery } from '@/composables/useArchiveImageGallery'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { buildArchiveHtmlPreview } from '@/lib/archiveHtml'\nimport { decodeTextSniff } from '@/lib/encoding'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — FileViewerBody has no slice model, so the\n// DICOM stack stays a direct hand-off here (a lone .dcm goes through the body like any other entry).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy-imported\n// to break the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's ZIP mode: fetch an archive's bytes, decompress them entirely in-browser (see lib/zip.ts),\n * and browse the contents. A master→detail within the narrow pane — a collapsible file tree, and selecting an\n * entry extracts just that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file\n * inside the zip previews with every viewer a top-level file gets — code/image/pdf/cert/audio/nested-zip/…).\n * HTML is the one multi-entry case: its relative JS/CSS/image/font dependencies are resolved from the same ZIP\n * and rewritten before entering the existing sandboxed iframe. Other entry bytes are not retained; selecting\n * decodes on demand, and the download button re-extracts.\n */\nconst props = defineProps<{ url: string; name: string }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet buffer: ArrayBuffer | null = null\nconst entries = ref<ZipEntry[]>([])\n\n// Encrypted archives: a password bar appears once any entry is encrypted. The password is held only in memory\n// and passed to each extraction (ZipCrypto / WinZip AES decrypt happens in lib/zip.ts).\nconst password = ref('')\nconst pwError = ref('')\nconst unlocked = ref(false)\nconst anyEncrypted = computed(() => entries.value.some((e) => e.encrypted))\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ZipEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ZipEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n selectionRun++\n imageGallery.clear()\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n entryFile.value = null\n entryHtmlPreview.value = ''\n password.value = ''\n pwError.value = ''\n unlocked.value = false\n revokeBlob()\n stackSlices.value = []\n if (!zipSupported()) {\n errorMsg.value = '当前浏览器不支持在前端解压 ZIP(缺少 DecompressionStream)'\n status.value = 'error'\n return\n }\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n buffer = await res.arrayBuffer()\n const list = readZip(buffer)\n entries.value = list\n root.value = buildTree(list)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ZipEntry | null>(null)\nconst entryStatus = ref<Status>('ready') // covers extraction; the body owns loading once entryFile is set\nconst entryErr = ref('')\nconst entryText = ref('') // the body emits loaded text up so the copy button can use it\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryHtmlPreview = ref('') // dependency-rewritten HTML; source/copy still use the original text\nconst entryStack = ref(false) // DICOM series (handled directly, not via the body)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('') // entry blob URL fed to the body (revoked on change/unmount)\nlet selectionRun = 0 // stale guard for async HTML dependency extraction (including selecting the same row twice)\n\n// DICOM series stack: when the clicked .dcm has sibling .dcm files in the same folder, they are browsed as\n// one scrollable series. Slices extract lazily; the DICOM viewer caches/prefetches them.\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ZipEntry): ZipEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs. Feeds the\n// body's source fall-back so an extensionless text file still opens as source instead of a download card.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nconst imageGallery = useArchiveImageGallery<ZipEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: async (entry) => {\n if (!buffer) throw new Error('ZIP 尚未加载')\n return extractZipEntry(buffer, entry, password.value)\n },\n})\n\nasync function selectEntry(e: ZipEntry): Promise<void> {\n const run = ++selectionRun\n let pendingUrl = ''\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!buffer) return\n try {\n // DICOM series: a folder holding several .dcm → browse them as one scrollable series (slices on demand).\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => {\n const u = await extractZipEntry(buffer as ArrayBuffer, se, password.value)\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n },\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const archiveBuffer = buffer\n const bytes = await extractZipEntry(archiveBuffer, e, password.value)\n if (selectionRun !== run || selected.value !== e) return\n const url = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n pendingUrl = url\n const file = classifyViewerFile({\n name: e.name,\n url,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n let htmlPreview = ''\n if (file.html) {\n const source = decodeTextSniff(bytes)\n file.content = source\n const preview = await buildArchiveHtmlPreview({\n html: source,\n documentPath: e.path,\n entries: entries.value,\n loadEntry: (dependency) => extractZipEntry(archiveBuffer, dependency, password.value),\n })\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n htmlPreview = preview.html\n }\n if (selectionRun !== run || selected.value !== e) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n blobUrl.value = url\n pendingUrl = ''\n entryHtmlPreview.value = htmlPreview\n entryFile.value = file\n entryStatus.value = 'ready'\n if (file.image) void imageGallery.open(e, bytes)\n } catch (err) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (err instanceof ZipEncryptedError)\n entryErr.value = '该文件已加密:请返回列表并在顶部输入密码'\n else if (err instanceof ZipPasswordError) {\n entryErr.value = '密码错误'\n unlocked.value = false\n } else entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\n// Validate the entered password against the first encrypted entry, then unlock (and re-open any selected file).\nasync function applyPassword(): Promise<void> {\n pwError.value = ''\n const enc = entries.value.find((e) => !e.isDir && e.encrypted && e.size > 0)\n if (!enc || !buffer) {\n unlocked.value = true\n return\n }\n try {\n await extractZipEntry(buffer, enc, password.value)\n unlocked.value = true\n if (selected.value) void selectEntry(selected.value)\n } catch (e) {\n unlocked.value = false\n pwError.value =\n e instanceof ZipPasswordError ? '密码错误' : e instanceof Error ? e.message : String(e)\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selectionRun++\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryHtmlPreview.value = ''\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\n// Re-extract on demand so we don't retain decompressed bytes for the whole session.\nasync function downloadEntry(e: ZipEntry): Promise<void> {\n if (!buffer) return\n try {\n const url = URL.createObjectURL(new Blob([await extractZipEntry(buffer, e, password.value)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n selectionRun++\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"zip\">\n <p v-if=\"status === 'loading'\" class=\"zip__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"zip__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"zip__bar\">\n <button type=\"button\" class=\"zip__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"zip__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"zip__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"zip__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"zip__vbtn\"\n :class=\"{ 'zip__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"zip__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"zip__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"zip__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"zip__msg zip__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series (wheel/cine翻层) -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n :html-preview=\"entryHtmlPreview || undefined\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"zip__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n\n <!-- password bar: shown when the archive has encrypted entries (ZipCrypto / WinZip AES) -->\n <div v-if=\"anyEncrypted\" class=\"zip__pw\" :class=\"{ 'zip__pw--ok': unlocked }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"zip__pwinput\"\n :placeholder=\"unlocked ? '已解锁(如需可重新输入)' : '此压缩包已加密,输入密码解锁'\"\n @keyup.enter=\"applyPassword\"\n />\n <button type=\"button\" class=\"zip__pwbtn\" @click=\"applyPassword\">\n {{ unlocked ? '已解锁' : '解锁' }}\n </button>\n <span v-if=\"pwError\" class=\"zip__pwerr\">{{ pwError }}</span>\n </div>\n\n <div class=\"zip__scroll zip__tree\">\n <p v-if=\"rows.length === 0\" class=\"zip__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"zip__row\"\n :class=\"{ 'zip__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"zip__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'zip__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"zip__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"zip__rowname\">{{ row.node.name }}</span>\n <Icon\n v-if=\"row.node.entry?.encrypted\"\n name=\"key\"\n :size=\"12\"\n class=\"zip__rowlock\"\n title=\"已加密\"\n />\n <span v-if=\"!row.node.isDir\" class=\"zip__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.zip {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.zip__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.zip__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.zip__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.zip__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.zip__summary {\n flex-shrink: 0;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n/* Password bar (encrypted archives). */\n.zip__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n color: var(--muted);\n}\n.zip__pw--ok {\n color: var(--accent);\n}\n.zip__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.zip__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.zip__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 12px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.zip__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.zip__pwerr {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--danger);\n}\n.zip__rowlock {\n flex-shrink: 0;\n color: var(--faint);\n}\n\n.zip__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.zip__tree {\n padding: 6px 0;\n}\n.zip__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.zip__row:hover {\n background: var(--surface-2);\n}\n.zip__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.zip__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.zip__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.zip__row--dir .zip__rowicon {\n color: var(--accent);\n}\n.zip__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.zip__row--dir .zip__rowname {\n font-weight: 500;\n}\n.zip__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.zip__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.zip__back,\n.zip__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.zip__back:hover,\n.zip__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.zip__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.zip__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n/* Render/source toggle (when the selected entry supports it) — compact segmented control. */\n.zip__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.zip__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.zip__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\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 "./ZoomImage.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/ZoomImage.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-dfea1bb7"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoomImage.js","names":[],"sources":["../../src/components/ZoomImage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\n/**\n * A pan + zoom image stage, shared by the full-screen {@link ImageLightbox} and the in-archive image preview\n * ({@link ZipBrowser}/{@link SevenZipBrowser}/{@link ArchiveBrowser}). Fills its parent (which must give it a\n * size). Wheel zooms toward the cursor, drag pans, double-click toggles fit↔2×, and a bottom toolbar offers\n * −/百分比/+. Initial fit is \"contain\" for a normal image, or fill-width-top-aligned for a `long` one (a tall\n * screenshot) so it reads top-down and pans vertically. A no-drag click on empty area emits `emptyClick` (the\n * lightbox closes on it; the archive preview ignores it).\n *\n * Exposes `zoomIn`/`zoomOut`/`fit` so a host can wire keyboard shortcuts to them.\n */\nconst props = withDefaults(defineProps<{ src: string; alt?: string; long?: boolean }>(), {\n alt: '',\n long: false,\n})\nconst emit = defineEmits<{ emptyClick: [] }>()\n\nconst stageEl = ref<HTMLElement | null>(null)\nconst imgEl = ref<HTMLImageElement | null>(null)\nconst natW = ref(0)\nconst natH = ref(0)\nconst scale = ref(1) // CSS px per image px (1 = 100%)\nconst tx = ref(0) // translate of the image's top-left within the stage\nconst ty = ref(0)\nconst fitScale = ref(1) // scale that \"contains\" the image in the stage\nconst ready = ref(false) // image measured → show it (avoids a pre-fit flash)\n\nconst percent = ref(100)\nfunction syncPercent(): void {\n percent.value = Math.round(scale.value * 100)\n}\n\nfunction stageSize(): { w: number; h: number } {\n const el = stageEl.value\n if (el && el.clientWidth) return { w: el.clientWidth, h: el.clientHeight }\n return { w: 1024, h: 768 }\n}\nfunction minScale(): number {\n return Math.max(0.05, Math.min(fitScale.value, 1) * 0.5)\n}\nfunction maxScale(): number {\n return Math.max(fitScale.value, 1) * 8\n}\n\n// Keep the image within the stage: center an axis whose content is smaller than the stage, otherwise clamp so\n// its edges can't be dragged inside the viewport. `topAlign` pins a too-tall image to its top (initial long view).\nfunction clampView(topAlign = false): void {\n const { w, h } = stageSize()\n const cw = natW.value * scale.value\n const ch = natH.value * scale.value\n tx.value = cw <= w ? (w - cw) / 2 : Math.min(0, Math.max(w - cw, tx.value))\n if (ch <= h) ty.value = (h - ch) / 2\n else if (topAlign) ty.value = 0\n else ty.value = Math.min(0, Math.max(h - ch, ty.value))\n syncPercent()\n}\n\n// (Re)fit the measured image: contain it, or — for a long image — fill the width and pin to the top.\nfunction fitView(): void {\n if (!natW.value || !natH.value) return\n const { w, h } = stageSize()\n fitScale.value = Math.min(w / natW.value, h / natH.value)\n scale.value = props.long ? (w * 0.98) / natW.value : Math.min(fitScale.value, 1)\n clampView(props.long)\n}\n\nfunction clampScale(s: number): number {\n return Math.min(maxScale(), Math.max(minScale(), s))\n}\n\n// Zoom keeping the image point under (cx, cy) — stage-relative coords — fixed in place.\nfunction zoomAt(cx: number, cy: number, factor: number): void {\n const next = clampScale(scale.value * factor)\n if (next === scale.value) return\n const ix = (cx - tx.value) / scale.value\n const iy = (cy - ty.value) / scale.value\n scale.value = next\n tx.value = cx - ix * next\n ty.value = cy - iy * next\n clampView(false)\n}\nfunction zoomCenter(factor: number): void {\n const { w, h } = stageSize()\n zoomAt(w / 2, h / 2, factor)\n}\n\nfunction onWheel(e: WheelEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n zoomAt(cx, cy, e.deltaY < 0 ? 1.15 : 1 / 1.15)\n}\nfunction onDblClick(e: MouseEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n // Near the fit scale → zoom in toward the cursor; otherwise snap back to fit.\n if (scale.value <= fitScale.value * 1.05)\n zoomAt(cx, cy, Math.max(2, 1 / Math.max(fitScale.value, 0.001)))\n else fitView()\n}\n\n// Pointer drag → pan (covers mouse + touch). A no-move pointerup on empty area emits `emptyClick`.\nlet dragging = false\nlet moved = false\nlet lastX = 0\nlet lastY = 0\nlet downTarget: EventTarget | null = null\n\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0) return\n dragging = true\n moved = false\n lastX = e.clientX\n lastY = e.clientY\n downTarget = e.target\n stageEl.value?.setPointerCapture(e.pointerId)\n}\nfunction onPointerMove(e: PointerEvent): void {\n if (!dragging) return\n const dx = e.clientX - lastX\n const dy = e.clientY - lastY\n if (!moved && Math.abs(dx) + Math.abs(dy) > 3) moved = true\n lastX = e.clientX\n lastY = e.clientY\n tx.value += dx\n ty.value += dy\n clampView(false)\n}\nfunction onPointerUp(e: PointerEvent): void {\n if (!dragging) return\n dragging = false\n try {\n stageEl.value?.releasePointerCapture(e.pointerId)\n } catch {\n /* capture already released — ignore */\n }\n if (!moved && downTarget !== imgEl.value) emit('emptyClick')\n}\n\nfunction onImgLoad(): void {\n const img = imgEl.value\n if (!img) return\n natW.value = img.naturalWidth || 1\n natH.value = img.naturalHeight || 1\n fitView()\n ready.value = true\n}\n\n// Re-fit when the container is resized (the side panel's drag splitter, or the window).\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n if (ready.value) fitView()\n}\n\n// Reset for a new source; the transform is recomputed on the <img>'s load.\nwatch(\n () => props.src,\n () => {\n ready.value = false\n natW.value = 0\n natH.value = 0\n scale.value = 1\n tx.value = 0\n ty.value = 0\n fitScale.value = 1\n syncPercent()\n },\n)\n\nonMounted(() => {\n if (typeof ResizeObserver !== 'undefined' && stageEl.value) {\n ro = new ResizeObserver(onResize)\n ro.observe(stageEl.value)\n }\n})\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({\n zoomIn: () => zoomCenter(1.2),\n zoomOut: () => zoomCenter(1 / 1.2),\n fit: () => fitView(),\n})\n</script>\n\n<template>\n <div\n ref=\"stageEl\"\n class=\"zi\"\n :class=\"{ 'zi--grab': scale > fitScale }\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @dblclick=\"onDblClick\"\n >\n <img\n ref=\"imgEl\"\n class=\"zi__img\"\n :class=\"{ 'zi__img--ready': ready }\"\n :src=\"src\"\n :alt=\"alt\"\n draggable=\"false\"\n :style=\"{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\"\n @load=\"onImgLoad\"\n @dragstart.prevent\n />\n <div class=\"zi__toolbar\" @pointerdown.stop @dblclick.stop @wheel.stop>\n <button type=\"button\" class=\"zi__tool\" title=\"缩小 (-)\" @click=\"zoomCenter(1 / 1.2)\">\n −\n </button>\n <button type=\"button\" class=\"zi__pct\" title=\"适应窗口 (0)\" @click=\"fitView()\">\n {{ percent }}%\n </button>\n <button type=\"button\" class=\"zi__tool\" title=\"放大 (+)\" @click=\"zoomCenter(1.2)\">+</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.zi {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: zoom-out;\n touch-action: none;\n}\n.zi--grab {\n cursor: grab;\n}\n.zi--grab:active {\n cursor: grabbing;\n}\n.zi__img {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n will-change: transform;\n user-select: none;\n -webkit-user-drag: none;\n opacity: 0;\n}\n.zi__img--ready {\n opacity: 1;\n}\n.zi__toolbar {\n position: absolute;\n bottom: 14px;\n left: 50%;\n transform: translateX(-50%);\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 4px;\n border-radius: 12px;\n background: rgba(28, 28, 30, 0.82);\n backdrop-filter: blur(8px);\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);\n}\n.zi__tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 32px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-size: 19px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__tool:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n.zi__pct {\n min-width: 56px;\n height: 32px;\n padding: 0 8px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-family: inherit;\n font-size: 12.5px;\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__pct:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"ZoomImage.js","names":[],"sources":["../../src/components/ZoomImage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\n\n/**\n * A pan + zoom image stage, shared by the full-screen {@link ImageLightbox} and the in-archive image preview\n * ({@link ZipBrowser}/{@link SevenZipBrowser}/{@link ArchiveBrowser}). Fills its parent (which must give it a\n * size). Wheel zooms toward the cursor, drag pans, double-click toggles fit↔2×, and a bottom toolbar offers\n * −/百分比/+. Initial fit is \"contain\" for a normal image, or fill-width-top-aligned for a `long` one (a tall\n * screenshot) so it reads top-down and pans vertically. A no-drag click on empty area emits `emptyClick` (the\n * lightbox closes on it; the archive preview ignores it).\n *\n * Exposes `zoomIn`/`zoomOut`/`fit` so a host can wire keyboard shortcuts to them.\n */\nconst props = withDefaults(\n defineProps<{ src: string; alt?: string; long?: boolean; toolbarBottom?: number }>(),\n {\n alt: '',\n long: false,\n toolbarBottom: 14,\n },\n)\nconst emit = defineEmits<{ emptyClick: [] }>()\n\nconst stageEl = useTemplateRef<HTMLElement>('stageEl')\nconst imgEl = useTemplateRef<HTMLImageElement>('imgEl')\nconst natW = shallowRef(0)\nconst natH = shallowRef(0)\nconst scale = shallowRef(1) // CSS px per image px (1 = 100%)\nconst tx = shallowRef(0) // pan offset from the stage's centered image position\nconst ty = shallowRef(0)\nconst fitScale = shallowRef(1) // scale that \"contains\" the image in the stage\nconst ready = shallowRef(false) // image measured → show it (avoids a pre-fit flash)\n\nconst percent = shallowRef(100)\nconst FIT_PADDING = 24\nfunction syncPercent(): void {\n percent.value = Math.round(scale.value * 100)\n}\n\nfunction stageSize(): { w: number; h: number } | null {\n const el = stageEl.value\n if (!el || !el.clientWidth || !el.clientHeight) return null\n return { w: el.clientWidth, h: el.clientHeight }\n}\nfunction minScale(): number {\n return Math.max(0.05, Math.min(fitScale.value, 1) * 0.5)\n}\nfunction maxScale(): number {\n return Math.max(fitScale.value, 1) * 8\n}\n\n// Keep the image within the stage: reset an axis whose content is smaller than the stage to the centered base,\n// otherwise clamp its center-relative pan offset. `topAlign` pins a too-tall image to its top (initial long view).\nfunction clampView(topAlign = false): void {\n const size = stageSize()\n if (!size) return\n const { w, h } = size\n const cw = natW.value * scale.value\n const ch = natH.value * scale.value\n const maxX = Math.max(0, (cw - w) / 2)\n const maxY = Math.max(0, (ch - h) / 2)\n tx.value = cw <= w ? 0 : Math.min(maxX, Math.max(-maxX, tx.value))\n if (ch <= h) ty.value = 0\n else if (topAlign) ty.value = maxY\n else ty.value = Math.min(maxY, Math.max(-maxY, ty.value))\n syncPercent()\n}\n\n// (Re)fit the measured image: contain it, or — for a long image — fill the width and pin to the top.\nfunction fitView(): boolean {\n if (!natW.value || !natH.value) return false\n const size = stageSize()\n if (!size) return false\n const { w, h } = size\n const padding = props.long ? 0 : FIT_PADDING * 2\n const availableWidth = Math.max(1, w - padding)\n const availableHeight = Math.max(1, h - padding)\n fitScale.value = Math.min(availableWidth / natW.value, availableHeight / natH.value)\n scale.value = props.long ? (w * 0.98) / natW.value : Math.min(fitScale.value, 1)\n tx.value = 0\n ty.value = 0\n clampView(props.long)\n return true\n}\n\nfunction clampScale(s: number): number {\n return Math.min(maxScale(), Math.max(minScale(), s))\n}\n\n// Zoom keeping the image point under (cx, cy) — stage-relative coords — fixed in place.\nfunction zoomAt(cx: number, cy: number, factor: number): void {\n const size = stageSize()\n if (!size) return\n const next = clampScale(scale.value * factor)\n if (next === scale.value) return\n const ix = (cx - size.w / 2 - tx.value) / scale.value\n const iy = (cy - size.h / 2 - ty.value) / scale.value\n scale.value = next\n tx.value = cx - size.w / 2 - ix * next\n ty.value = cy - size.h / 2 - iy * next\n clampView(false)\n}\nfunction zoomCenter(factor: number): void {\n const size = stageSize()\n if (!size) return\n const { w, h } = size\n zoomAt(w / 2, h / 2, factor)\n}\n\nfunction onWheel(e: WheelEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n zoomAt(cx, cy, e.deltaY < 0 ? 1.15 : 1 / 1.15)\n}\nfunction onDblClick(e: MouseEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n // Near the fit scale → zoom in toward the cursor; otherwise snap back to fit.\n if (scale.value <= fitScale.value * 1.05)\n zoomAt(cx, cy, Math.max(2, 1 / Math.max(fitScale.value, 0.001)))\n else fitView()\n}\n\n// Pointer drag → pan (covers mouse + touch). A no-move pointerup on empty area emits `emptyClick`.\nlet dragging = false\nlet moved = false\nlet lastX = 0\nlet lastY = 0\nlet downTarget: EventTarget | null = null\n\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0) return\n dragging = true\n moved = false\n lastX = e.clientX\n lastY = e.clientY\n downTarget = e.target\n stageEl.value?.setPointerCapture(e.pointerId)\n}\nfunction onPointerMove(e: PointerEvent): void {\n if (!dragging) return\n const dx = e.clientX - lastX\n const dy = e.clientY - lastY\n if (!moved && Math.abs(dx) + Math.abs(dy) > 3) moved = true\n lastX = e.clientX\n lastY = e.clientY\n tx.value += dx\n ty.value += dy\n clampView(false)\n}\nfunction onPointerUp(e: PointerEvent): void {\n if (!dragging) return\n dragging = false\n try {\n stageEl.value?.releasePointerCapture(e.pointerId)\n } catch {\n /* capture already released — ignore */\n }\n if (!moved && downTarget !== imgEl.value) emit('emptyClick')\n}\n\nfunction onImgLoad(): void {\n const img = imgEl.value\n if (!img) return\n natW.value = img.naturalWidth || 1\n natH.value = img.naturalHeight || 1\n ready.value = fitView()\n}\n\n// Re-fit when the container is resized (the side panel's drag splitter, or the window).\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n if (natW.value && natH.value) ready.value = fitView()\n}\n\n// Reset for a new source; the transform is recomputed on the <img>'s load.\nwatch(\n () => props.src,\n () => {\n ready.value = false\n natW.value = 0\n natH.value = 0\n scale.value = 1\n tx.value = 0\n ty.value = 0\n fitScale.value = 1\n syncPercent()\n },\n)\n\nonMounted(() => {\n if (typeof ResizeObserver !== 'undefined' && stageEl.value) {\n ro = new ResizeObserver(onResize)\n ro.observe(stageEl.value)\n }\n})\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({\n zoomIn: () => zoomCenter(1.2),\n zoomOut: () => zoomCenter(1 / 1.2),\n fit: () => {\n fitView()\n },\n})\n</script>\n\n<template>\n <div\n ref=\"stageEl\"\n class=\"zi\"\n :class=\"{ 'zi--grab': scale > fitScale }\"\n :style=\"{ '--zi-toolbar-bottom': `${toolbarBottom}px` }\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @dblclick=\"onDblClick\"\n >\n <div class=\"zi__center\">\n <img\n ref=\"imgEl\"\n class=\"zi__img\"\n :class=\"{ 'zi__img--ready': ready }\"\n :src=\"src\"\n :alt=\"alt\"\n draggable=\"false\"\n :style=\"{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\"\n @load=\"onImgLoad\"\n @dragstart.prevent\n />\n </div>\n <div class=\"zi__toolbar\" @pointerdown.stop @dblclick.stop @wheel.stop>\n <button type=\"button\" class=\"zi__tool\" title=\"缩小 (-)\" @click=\"zoomCenter(1 / 1.2)\">\n −\n </button>\n <button type=\"button\" class=\"zi__pct\" title=\"适应窗口 (0)\" @click=\"fitView()\">\n {{ percent }}%\n </button>\n <button type=\"button\" class=\"zi__tool\" title=\"放大 (+)\" @click=\"zoomCenter(1.2)\">+</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.zi {\n position: relative;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n cursor: zoom-out;\n touch-action: none;\n}\n.zi--grab {\n cursor: grab;\n}\n.zi--grab:active {\n cursor: grabbing;\n}\n.zi__center {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n}\n.zi__img {\n flex: none;\n max-width: none;\n max-height: none;\n transform-origin: center;\n will-change: transform;\n user-select: none;\n -webkit-user-drag: none;\n opacity: 0;\n}\n.zi__img--ready {\n opacity: 1;\n}\n.zi__toolbar {\n position: absolute;\n bottom: var(--zi-toolbar-bottom, 14px);\n left: 50%;\n transform: translateX(-50%);\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 4px;\n border-radius: 12px;\n background: rgba(28, 28, 30, 0.82);\n backdrop-filter: blur(8px);\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);\n}\n.zi__tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 32px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-size: 19px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__tool:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n.zi__pct {\n min-width: 56px;\n height: 32px;\n padding: 0 8px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-family: inherit;\n font-size: 12.5px;\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__pct:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n</style>\n"],"mappings":""}
|
|
@@ -12,6 +12,7 @@ type __VLS_Props = {
|
|
|
12
12
|
src: string;
|
|
13
13
|
alt?: string;
|
|
14
14
|
long?: boolean;
|
|
15
|
+
toolbarBottom?: number;
|
|
15
16
|
};
|
|
16
17
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
17
18
|
zoomIn: () => void;
|
|
@@ -24,6 +25,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
24
25
|
}>, {
|
|
25
26
|
alt: string;
|
|
26
27
|
long: boolean;
|
|
28
|
+
toolbarBottom: number;
|
|
27
29
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
30
|
declare const _default: typeof __VLS_export;
|
|
29
31
|
export default _default;
|