@l1yp/file-viewer 0.1.2 → 0.1.3

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.
@@ -1,116 +1,178 @@
1
- import { createCommentVNode as e, createElementBlock as t, createElementVNode as n, defineComponent as r, onBeforeUnmount as i, onMounted as a, openBlock as o, ref as s, toDisplayString as c, watch as l } from "vue";
2
- import * as u from "pdfjs-dist";
3
- import d from "pdfjs-dist/build/pdf.worker.min.mjs?url";
1
+ import e from "./PdfNavigationPane.js";
2
+ import { loadPdfNavigation as t } from "../lib/pdfNavigation.js";
3
+ import { createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, defineComponent as o, onBeforeUnmount as s, onMounted as c, openBlock as l, shallowRef as u, toDisplayString as d, useTemplateRef as f, watch as p } from "vue";
4
+ import * as m from "pdfjs-dist";
5
+ import h from "pdfjs-dist/build/pdf.worker.min.mjs?url";
4
6
  //#region src/components/PdfViewer.vue?vue&type=script&setup=true&lang.ts
5
- var f = { class: "pf" }, p = {
7
+ var g = { class: "pf" }, _ = { class: "pf__stage" }, v = {
6
8
  key: 0,
7
9
  class: "pf__overlay"
8
- }, m = {
10
+ }, y = {
9
11
  key: 1,
10
12
  class: "pf__overlay"
11
- }, h = { class: "pf__msg pf__msg--error" }, g = 2, _ = /*@__PURE__*/ r({
13
+ }, b = { class: "pf__msg pf__msg--error" }, x = 2, S = 24, C = 104, w = /*@__PURE__*/ o({
12
14
  __name: "PdfViewer",
13
15
  props: {
14
16
  url: {},
15
17
  name: {},
16
18
  size: {}
17
19
  },
18
- setup(r) {
19
- u.GlobalWorkerOptions.workerSrc = d;
20
- let _ = r, v = s("loading"), y = s(""), b = s(0), x = s(null), S = null, C = [], w = 0;
21
- function T() {
22
- for (let e of C) try {
20
+ setup(o) {
21
+ m.GlobalWorkerOptions.workerSrc = h;
22
+ let w = o, T = u("loading"), E = u(""), D = u(0), O = u(1), k = u([]), A = u([]), j = u([]), M = u(!1), N = u("outline"), P = f("docEl"), F = null, I = [], L = 0, R, z = "", B = null, V = 0;
23
+ function H() {
24
+ for (let e of I) try {
23
25
  e.cancel();
24
26
  } catch {}
25
- C = [];
27
+ I = [];
26
28
  }
27
- function E() {
28
- if (T(), S) {
29
+ function U() {
30
+ if (H(), F) {
29
31
  try {
30
- S.destroy();
32
+ F.destroy();
31
33
  } catch {}
32
- S = null;
34
+ F = null;
33
35
  }
34
36
  }
35
- async function D() {
36
- let e = x.value, t = S;
37
+ function W(e) {
38
+ if (!e.width || !e.height) return null;
39
+ try {
40
+ let t = document.createElement("canvas");
41
+ t.width = C, t.height = Math.max(1, Math.round(C * (e.height / e.width)));
42
+ let n = t.getContext("2d");
43
+ return n ? (n.fillStyle = "#fff", n.fillRect(0, 0, t.width, t.height), n.drawImage(e, 0, 0, t.width, t.height), t.toDataURL("image/jpeg", .78)) : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function G(e, t = "smooth") {
49
+ let n = P.value, r = n?.querySelector(`[data-page-number="${e}"]`);
50
+ if (!n || !r) return;
51
+ O.value = e;
52
+ let i = n.getBoundingClientRect(), a = r.getBoundingClientRect(), o = Number.parseFloat(getComputedStyle(n).paddingTop);
53
+ n.scrollTo({
54
+ top: n.scrollTop + a.top - i.top - o,
55
+ behavior: t
56
+ });
57
+ }
58
+ function K() {
59
+ V ||= window.requestAnimationFrame(() => {
60
+ V = 0;
61
+ let e = P.value;
62
+ if (!e) return;
63
+ let t = e.getBoundingClientRect().top + Math.min(e.clientHeight * .36, 220), n = O.value, r = Infinity;
64
+ for (let i of e.querySelectorAll("[data-page-number]")) {
65
+ let e = i.getBoundingClientRect(), a = Math.min(Math.max(t, e.top), e.bottom), o = Math.abs(t - a);
66
+ o >= r || (r = o, n = Number(i.dataset.pageNumber) || n);
67
+ }
68
+ O.value = n;
69
+ });
70
+ }
71
+ async function q() {
72
+ let e = P.value, t = F;
37
73
  if (!e || !t) return;
38
- let n = ++w;
39
- T(), e.innerHTML = "";
40
- let r = getComputedStyle(e), i = parseFloat(r.paddingLeft) + parseFloat(r.paddingRight), a = Math.max(120, e.clientWidth - i), o = Math.min(typeof window < "u" && window.devicePixelRatio || 1, g);
41
- for (let r = 1; r <= t.numPages; r++) {
74
+ let n = ++L, r = O.value, i = e.scrollTop > 0;
75
+ H(), e.innerHTML = "";
76
+ let a = getComputedStyle(e), o = Number.parseFloat(a.paddingLeft) + Number.parseFloat(a.paddingRight), s = Number.parseFloat(a.paddingTop) + Number.parseFloat(a.paddingBottom), c = Math.max(120, e.clientWidth - o - S), l = Math.max(160, e.clientHeight - s - S), u = Math.min(window.devicePixelRatio || 1, x), d = Array(t.numPages).fill(null);
77
+ for (let r = 1; r <= t.numPages; r += 1) {
42
78
  let i;
43
79
  try {
44
80
  i = await t.getPage(r);
45
81
  } catch {
46
- if (n !== w) return;
82
+ if (n !== L) return;
47
83
  continue;
48
84
  }
49
- if (n !== w) return;
50
- let s = i.getViewport({ scale: 1 }), c = a / s.width, l = i.getViewport({ scale: c * o }), u = document.createElement("canvas");
51
- if (u.className = "pf__page", u.width = Math.ceil(l.width), u.height = Math.ceil(l.height), u.style.width = `${Math.round(a)}px`, u.style.height = `${Math.round(a * (s.height / s.width))}px`, !u.getContext("2d")) continue;
52
- e.appendChild(u);
53
- let d = i.render({
54
- canvas: u,
55
- viewport: l
85
+ if (n !== L) return;
86
+ let a = i.getViewport({ scale: 1 }), o = Math.min(c / a.width, l / a.height), s = Math.round(a.width * o), f = Math.round(a.height * o), p = i.getViewport({ scale: o * u }), m = document.createElement("div");
87
+ m.className = "pf__page-wrap", m.dataset.pageNumber = `${r}`;
88
+ let h = document.createElement("canvas");
89
+ if (h.className = "pf__page", h.width = Math.ceil(p.width), h.height = Math.ceil(p.height), h.style.width = `${s}px`, h.style.height = `${f}px`, h.setAttribute("aria-label", `PDF 第 ${r} 页`), !h.getContext("2d")) continue;
90
+ m.appendChild(h), e.appendChild(m);
91
+ let g = i.render({
92
+ canvas: h,
93
+ viewport: p
56
94
  });
57
- C.push(d);
95
+ I.push(g);
96
+ let _ = !0;
58
97
  try {
59
- await d.promise;
98
+ await g.promise;
60
99
  } catch {
61
- if (n !== w) return;
100
+ if (n !== L) return;
101
+ _ = !1;
62
102
  }
63
- if (n !== w) return;
103
+ if (n !== L) return;
104
+ _ && (d[r - 1] = W(h));
64
105
  }
106
+ j.value = d, i && G(r, "auto"), K();
65
107
  }
66
- async function O() {
67
- let e = x.value;
68
- if (!_.url || !e) return;
69
- v.value = "loading", y.value = "", E(), e.innerHTML = "";
70
- let t = _.url;
108
+ async function J() {
109
+ let e = P.value;
110
+ if (!w.url || !e) return;
111
+ T.value = "loading", E.value = "", k.value = [], A.value = [], j.value = [], U(), e.innerHTML = "";
112
+ let n = w.url;
71
113
  try {
72
- let n = await fetch(t);
73
- if (!n.ok) throw Error(`HTTP ${n.status}`);
74
- let r = await n.arrayBuffer();
75
- if (_.url !== t || x.value !== e) return;
76
- let i = await u.getDocument({ data: new Uint8Array(r) }).promise;
77
- if (_.url !== t || x.value !== e) {
78
- try {
79
- i.destroy();
80
- } catch {}
114
+ let r = await fetch(n);
115
+ if (!r.ok) throw Error(`HTTP ${r.status}`);
116
+ let i = new Uint8Array(await r.arrayBuffer());
117
+ if (w.url !== n || P.value !== e) return;
118
+ let a = await m.getDocument({ data: i }).promise;
119
+ if (w.url !== n || P.value !== e) {
120
+ a.destroy();
81
121
  return;
82
122
  }
83
- if (S = i, b.value = i.numPages, await D(), _.url !== t || x.value !== e) return;
84
- v.value = "ready";
123
+ F = a, D.value = a.numPages, O.value = 1;
124
+ let o = await t(a);
125
+ if (w.url !== n || P.value !== e || (k.value = o.outline, A.value = o.pageLabels, N.value = o.outline.length ? "outline" : "pages", await q(), w.url !== n || P.value !== e)) return;
126
+ T.value = "ready";
85
127
  } catch (e) {
86
- y.value = e instanceof Error ? e.message : String(e), v.value = "error";
128
+ E.value = e instanceof Error ? e.message : String(e), T.value = "error";
87
129
  }
88
130
  }
89
- let k, A = 0, j = null;
90
- function M() {
91
- let e = x.value;
92
- if (!e || !S) return;
93
- let t = e.clientWidth;
94
- Math.abs(t - A) < 8 || (A = t, clearTimeout(k), k = setTimeout(() => {
95
- S && D();
131
+ function Y() {
132
+ let e = P.value;
133
+ if (!e || !F) return;
134
+ let t = `${e.clientWidth}x${e.clientHeight}`;
135
+ !e.clientWidth || !e.clientHeight || t === z || (z = t, clearTimeout(R), R = setTimeout(() => {
136
+ F && q();
96
137
  }, 180));
97
138
  }
98
- return a(() => {
99
- O(), typeof ResizeObserver < "u" && x.value && (A = x.value.clientWidth, j = new ResizeObserver(M), j.observe(x.value));
100
- }), l(() => _.url, O), i(() => {
101
- clearTimeout(k), j?.disconnect(), w++, E(), x.value && (x.value.innerHTML = "");
102
- }), (r, i) => (o(), t("div", f, [n("div", {
139
+ return c(() => {
140
+ J(), typeof ResizeObserver < "u" && P.value && (z = `${P.value.clientWidth}x${P.value.clientHeight}`, B = new ResizeObserver(Y), B.observe(P.value));
141
+ }), p(() => w.url, J), s(() => {
142
+ clearTimeout(R), B?.disconnect(), V && window.cancelAnimationFrame(V), L += 1, U(), P.value && (P.value.innerHTML = "");
143
+ }), (t, o) => (l(), i("div", g, [T.value === "ready" ? (l(), n(e, {
144
+ key: 0,
145
+ collapsed: M.value,
146
+ "onUpdate:collapsed": o[0] ||= (e) => M.value = e,
147
+ view: N.value,
148
+ "onUpdate:view": o[1] ||= (e) => N.value = e,
149
+ "outline-items": k.value,
150
+ "page-count": D.value,
151
+ "page-labels": A.value,
152
+ "page-thumbnails": j.value,
153
+ "active-page": O.value,
154
+ onNavigate: G
155
+ }, null, 8, [
156
+ "collapsed",
157
+ "view",
158
+ "outline-items",
159
+ "page-count",
160
+ "page-labels",
161
+ "page-thumbnails",
162
+ "active-page"
163
+ ])) : r("", !0), a("div", _, [a("div", {
103
164
  ref_key: "docEl",
104
- ref: x,
105
- class: "pf__doc"
106
- }, null, 512), v.value === "loading" ? (o(), t("div", p, [...i[0] ||= [n("p", { class: "pf__msg" }, "正在渲染 PDF…", -1)]])) : v.value === "error" ? (o(), t("div", m, [n("div", h, [n("p", null, "渲染失败:" + c(y.value), 1), n("button", {
165
+ ref: P,
166
+ class: "pf__doc",
167
+ onScrollPassive: K
168
+ }, null, 544), T.value === "loading" ? (l(), i("div", v, [...o[2] ||= [a("p", { class: "pf__msg" }, "正在渲染 PDF…", -1)]])) : T.value === "error" ? (l(), i("div", y, [a("div", b, [a("p", null, "渲染失败:" + d(E.value), 1), a("button", {
107
169
  type: "button",
108
170
  class: "pf__retry",
109
- onClick: O
110
- }, "重试")])])) : e("", !0)]));
171
+ onClick: J
172
+ }, "重试")])])) : r("", !0)])]));
111
173
  }
112
174
  });
113
175
  //#endregion
114
- export { _ as default };
176
+ export { w as default };
115
177
 
116
178
  //# sourceMappingURL=PdfViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PdfViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`); we point pdfjs at\n// it. The `.mjs` is served as text/javascript by Ktor (see AGENTS.md) so the module worker loads in prod.\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\n/**\n * Side-viewer PDF (`.pdf`) renderer. Fetches the document bytes from the download URL and rasterizes each\n * page to a <canvas> entirely in-browser via pdfjs-dist (https://github.com/mozilla/pdf.js) — a vertically\n * scrollable list of pages (white sheets on a neutral \"desk\", like the docx/pptx viewers). No CDN / network\n * beyond the file fetch: pdfjs bundles its own worker, fetched lazily as a same-origin asset (see above).\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/pptx/zip): a real download URL for a held attachment,\n * or a blob URL minted by the archive browsers from already-extracted bytes. Pages re-rasterize when the pane\n * is resized (debounced) so text stays crisp at the new width.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pdf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst pageCount = ref(0)\n// The scroll container the page canvases render into. Always mounted (the loading/error states overlay it).\nconst docEl = ref<HTMLElement | null>(null)\n\n// Live document proxy + in-flight render tasks — torn down on re-render / unmount (the worker and page\n// rasterizers don't GC on their own).\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\n// Bumped on every (re)render so a stale async pass (older file, or a superseded resize) can bail out.\nlet renderToken = 0\n// Cap the backing-store resolution: device pixels × this, clamped, keeps big/high-DPI pages from minting\n// multi-hundred-MB canvases while staying crisp.\nconst MAX_SCALE = 2\n\nfunction cancelRenders(): void {\n for (const t of renderTasks) {\n try {\n t.cancel()\n } catch {\n /* already settled — ignore */\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (pdfDoc) {\n try {\n pdfDoc.destroy()\n } catch {\n /* already torn down — ignore */\n }\n pdfDoc = null\n }\n}\n\n// Rasterize every page of the live document into the container at its current width. Called on load and on\n// resize; guarded by `renderToken` so an outdated pass (file switched, or a newer resize started) stops early.\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n cancelRenders()\n el.innerHTML = ''\n // Available CSS width for a page (minus the container's horizontal padding). Floor at a sane minimum so a\n // momentarily-zero clientWidth (not yet laid out) doesn't render a 0-px canvas.\n const style = getComputedStyle(el)\n const padX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)\n const cssWidth = Math.max(120, el.clientWidth - padX)\n const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, MAX_SCALE)\n for (let n = 1; n <= doc.numPages; n++) {\n let page\n try {\n page = await doc.getPage(n)\n } catch {\n if (token !== renderToken) return\n continue // skip an unreadable page rather than failing the whole doc\n }\n if (token !== renderToken) return\n const base = page.getViewport({ scale: 1 })\n const cssScale = cssWidth / base.width\n const viewport = page.getViewport({ scale: cssScale * dpr })\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n // Display at the CSS width; the extra backing-store pixels (× dpr) make text sharp on HiDPI screens.\n canvas.style.width = `${Math.round(cssWidth)}px`\n canvas.style.height = `${Math.round(cssWidth * (base.height / base.width))}px`\n if (!canvas.getContext('2d')) continue // no 2D context available — skip rather than crash the render\n el.appendChild(canvas)\n // Pass `canvas` (the preferred param in pdfjs v5); it derives the 2D context itself.\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n try {\n await task.promise\n } catch {\n // RenderingCancelledException on supersede/unmount is expected — only a live token cares about failures.\n if (token !== renderToken) return\n }\n if (token !== renderToken) return\n }\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer load may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const doc = await pdfjsLib.getDocument({ data: new Uint8Array(buf) }).promise\n if (props.url !== url || docEl.value !== el) {\n try {\n doc.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n pdfDoc = doc\n pageCount.value = doc.numPages\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\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// Re-rasterize on width changes (debounced) so pages stay crisp as the pane is dragged wider/narrower. Only\n// the width matters; height changes don't affect page scale.\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastWidth = 0\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const w = el.clientWidth\n if (Math.abs(w - lastWidth) < 8) return // ignore sub-pixel / negligible jitter\n lastWidth = w\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastWidth = docEl.value.clientWidth\n ro = new ResizeObserver(onResize)\n ro.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n ro?.disconnect()\n renderToken++ // invalidate any in-flight render pass\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <!-- Always mounted so the renderer has a sized, scrollable container; states overlay it. -->\n <div ref=\"docEl\" class=\"pf__doc\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n/* Scroll region holding the rendered pages — white sheets on a neutral \"desk\", like the docx/pptx viewers. */\n.pf__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 18px 16px 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n}\n/* Each page canvas: a white sheet with a soft shadow. Width is set inline (the rasterized CSS width); the\n * canvas paints its own white, so no background needed here. */\n.pf__doc :deep(.pf__page) {\n display: block;\n max-width: 100%;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n border-radius: 2px;\n background: #fff;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pf__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.pf__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"PdfViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`).\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\nimport PdfNavigationPane from './PdfNavigationPane.vue'\nimport { loadPdfNavigation, type PdfNavigationItem } from '../lib/pdfNavigation'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\nconst pageCount = shallowRef(0)\nconst activePage = shallowRef(1)\nconst outlineItems = shallowRef<PdfNavigationItem[]>([])\nconst pageLabels = shallowRef<string[]>([])\nconst pageThumbnails = shallowRef<Array<string | null>>([])\nconst navigationCollapsed = shallowRef(false)\nconst navigationView = shallowRef<'outline' | 'pages'>('outline')\nconst docEl = useTemplateRef<HTMLElement>('docEl')\n\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\nlet renderToken = 0\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastSize = ''\nlet resizeObserver: ResizeObserver | null = null\nlet scrollFrame = 0\nconst MAX_SCALE = 2\nconst PAGE_GUTTER = 24\nconst THUMBNAIL_WIDTH = 104\n\nfunction cancelRenders(): void {\n for (const task of renderTasks) {\n try {\n task.cancel()\n } catch {\n // The task may already have settled.\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (!pdfDoc) return\n try {\n pdfDoc.destroy()\n } catch {\n // The worker may already have been torn down.\n }\n pdfDoc = null\n}\n\nfunction createPageThumbnail(canvas: HTMLCanvasElement): string | null {\n if (!canvas.width || !canvas.height) return null\n try {\n const thumbnail = document.createElement('canvas')\n thumbnail.width = THUMBNAIL_WIDTH\n thumbnail.height = Math.max(1, Math.round(THUMBNAIL_WIDTH * (canvas.height / canvas.width)))\n const context = thumbnail.getContext('2d')\n if (!context) return null\n context.fillStyle = '#fff'\n context.fillRect(0, 0, thumbnail.width, thumbnail.height)\n context.drawImage(canvas, 0, 0, thumbnail.width, thumbnail.height)\n return thumbnail.toDataURL('image/jpeg', 0.78)\n } catch {\n return null\n }\n}\n\nfunction scrollToPage(pageNumber: number, behavior: ScrollBehavior = 'smooth'): void {\n const el = docEl.value\n const target = el?.querySelector<HTMLElement>(`[data-page-number=\"${pageNumber}\"]`)\n if (!el || !target) return\n activePage.value = pageNumber\n const rootRect = el.getBoundingClientRect()\n const targetRect = target.getBoundingClientRect()\n const paddingTop = Number.parseFloat(getComputedStyle(el).paddingTop)\n el.scrollTo({\n top: el.scrollTop + targetRect.top - rootRect.top - paddingTop,\n behavior,\n })\n}\n\nfunction syncActivePage(): void {\n if (scrollFrame) return\n scrollFrame = window.requestAnimationFrame(() => {\n scrollFrame = 0\n const el = docEl.value\n if (!el) return\n const rootRect = el.getBoundingClientRect()\n const targetY = rootRect.top + Math.min(el.clientHeight * 0.36, 220)\n let nearestPage = activePage.value\n let nearestDistance = Number.POSITIVE_INFINITY\n for (const page of el.querySelectorAll<HTMLElement>('[data-page-number]')) {\n const rect = page.getBoundingClientRect()\n const pageTarget = Math.min(Math.max(targetY, rect.top), rect.bottom)\n const distance = Math.abs(targetY - pageTarget)\n if (distance >= nearestDistance) continue\n nearestDistance = distance\n nearestPage = Number(page.dataset.pageNumber) || nearestPage\n }\n activePage.value = nearestPage\n })\n}\n\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n const preservedPage = activePage.value\n const preserveScroll = el.scrollTop > 0\n cancelRenders()\n el.innerHTML = ''\n\n const style = getComputedStyle(el)\n const padX = Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight)\n const padY = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom)\n const availableWidth = Math.max(120, el.clientWidth - padX - PAGE_GUTTER)\n const availableHeight = Math.max(160, el.clientHeight - padY - PAGE_GUTTER)\n const outputScale = Math.min(window.devicePixelRatio || 1, MAX_SCALE)\n const thumbnails = Array<string | null>(doc.numPages).fill(null)\n\n for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber += 1) {\n let page\n try {\n page = await doc.getPage(pageNumber)\n } catch {\n if (token !== renderToken) return\n continue\n }\n if (token !== renderToken) return\n\n const base = page.getViewport({ scale: 1 })\n const cssScale = Math.min(availableWidth / base.width, availableHeight / base.height)\n const cssWidth = Math.round(base.width * cssScale)\n const cssHeight = Math.round(base.height * cssScale)\n const viewport = page.getViewport({ scale: cssScale * outputScale })\n\n const pageWrap = document.createElement('div')\n pageWrap.className = 'pf__page-wrap'\n pageWrap.dataset.pageNumber = `${pageNumber}`\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n canvas.style.width = `${cssWidth}px`\n canvas.style.height = `${cssHeight}px`\n canvas.setAttribute('aria-label', `PDF 第 ${pageNumber} 页`)\n if (!canvas.getContext('2d')) continue\n pageWrap.appendChild(canvas)\n el.appendChild(pageWrap)\n\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n let rendered = true\n try {\n await task.promise\n } catch {\n if (token !== renderToken) return\n rendered = false\n }\n if (token !== renderToken) return\n if (rendered) thumbnails[pageNumber - 1] = createPageThumbnail(canvas)\n }\n\n pageThumbnails.value = thumbnails\n if (preserveScroll) scrollToPage(preservedPage, 'auto')\n syncActivePage()\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n outlineItems.value = []\n pageLabels.value = []\n pageThumbnails.value = []\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n\n try {\n const response = await fetch(url)\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n const bytes = new Uint8Array(await response.arrayBuffer())\n if (props.url !== url || docEl.value !== el) return\n\n const document = await pdfjsLib.getDocument({ data: bytes }).promise\n if (props.url !== url || docEl.value !== el) {\n document.destroy()\n return\n }\n pdfDoc = document\n pageCount.value = document.numPages\n activePage.value = 1\n const navigation = await loadPdfNavigation(document)\n if (props.url !== url || docEl.value !== el) return\n outlineItems.value = navigation.outline\n pageLabels.value = navigation.pageLabels\n navigationView.value = navigation.outline.length ? 'outline' : 'pages'\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\n status.value = 'ready'\n } catch (cause) {\n errorMsg.value = cause instanceof Error ? cause.message : String(cause)\n status.value = 'error'\n }\n}\n\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const size = `${el.clientWidth}x${el.clientHeight}`\n if (!el.clientWidth || !el.clientHeight || size === lastSize) return\n lastSize = size\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n void load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastSize = `${docEl.value.clientWidth}x${docEl.value.clientHeight}`\n resizeObserver = new ResizeObserver(onResize)\n resizeObserver.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n resizeObserver?.disconnect()\n if (scrollFrame) window.cancelAnimationFrame(scrollFrame)\n renderToken += 1\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <PdfNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navigationCollapsed\"\n v-model:view=\"navigationView\"\n :outline-items=\"outlineItems\"\n :page-count=\"pageCount\"\n :page-labels=\"pageLabels\"\n :page-thumbnails=\"pageThumbnails\"\n :active-page=\"activePage\"\n @navigate=\"scrollToPage\"\n />\n <div class=\"pf__stage\">\n <div ref=\"docEl\" class=\"pf__doc\" @scroll.passive=\"syncActivePage\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex: 1 1 auto;\n overflow: hidden;\n container-type: inline-size;\n background: var(--surface);\n}\n\n.pf__stage {\n position: relative;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex: 1 1 0;\n overflow: hidden;\n}\n\n.pf__doc {\n width: 100%;\n min-width: 0;\n min-height: 0;\n flex: 1 1 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 24px 20px 42px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n box-sizing: border-box;\n background: var(--surface-2);\n scrollbar-color: #555961 transparent;\n scrollbar-width: thin;\n}\n\n.pf__doc :deep(.pf__page-wrap) {\n max-width: 100%;\n display: flex;\n flex: none;\n justify-content: center;\n}\n\n.pf__doc :deep(.pf__page) {\n max-width: 100%;\n display: block;\n background: #fff;\n border-radius: 2px;\n box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);\n}\n\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n\n.pf__retry {\n padding: 5px 12px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n}\n\n.pf__retry:hover {\n color: var(--accent);\n border-color: var(--accent);\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-96f5f467"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-dc19394f"]]);
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(defineProps<{ src: string; alt?: string; long?: boolean }>(), {\n alt: '',\n long: false,\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) // translate of the image's top-left within the stage\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 } {\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 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 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 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__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,6 +1,6 @@
1
- import { createElementBlock as e, createElementVNode as t, defineComponent as n, normalizeClass as r, normalizeStyle as i, onBeforeUnmount as a, onMounted as o, openBlock as s, ref as c, toDisplayString as l, watch as u, withModifiers as d } from "vue";
1
+ import { createElementBlock as e, createElementVNode as t, defineComponent as n, normalizeClass as r, normalizeStyle as i, onBeforeUnmount as a, onMounted as o, openBlock as s, shallowRef as c, toDisplayString as l, useTemplateRef as u, watch as d, withModifiers as f } from "vue";
2
2
  //#region src/components/ZoomImage.vue?vue&type=script&setup=true&lang.ts
3
- var f = ["src", "alt"], p = /*@__PURE__*/ n({
3
+ var p = ["src", "alt"], m = 24, h = /*@__PURE__*/ n({
4
4
  __name: "ZoomImage",
5
5
  props: {
6
6
  src: {},
@@ -11,13 +11,13 @@ var f = ["src", "alt"], p = /*@__PURE__*/ n({
11
11
  }
12
12
  },
13
13
  emits: ["emptyClick"],
14
- setup(n, { expose: p, emit: m }) {
15
- let h = n, g = m, _ = c(null), v = c(null), y = c(0), b = c(0), x = c(1), S = c(0), C = c(0), w = c(1), T = c(!1), E = c(100);
16
- function D() {
17
- E.value = Math.round(x.value * 100);
14
+ setup(n, { expose: h, emit: g }) {
15
+ let _ = n, v = g, y = u("stageEl"), b = u("imgEl"), x = c(0), S = c(0), C = c(1), w = c(0), T = c(0), E = c(1), D = c(!1), O = c(100);
16
+ function k() {
17
+ O.value = Math.round(C.value * 100);
18
18
  }
19
- function O() {
20
- let e = _.value;
19
+ function A() {
20
+ let e = y.value;
21
21
  return e && e.clientWidth ? {
22
22
  w: e.clientWidth,
23
23
  h: e.clientHeight
@@ -26,124 +26,124 @@ var f = ["src", "alt"], p = /*@__PURE__*/ n({
26
26
  h: 768
27
27
  };
28
28
  }
29
- function k() {
30
- return Math.max(.05, Math.min(w.value, 1) * .5);
31
- }
32
- function A() {
33
- return Math.max(w.value, 1) * 8;
34
- }
35
- function j(e = !1) {
36
- let { w: t, h: n } = O(), r = y.value * x.value, i = b.value * x.value;
37
- S.value = r <= t ? (t - r) / 2 : Math.min(0, Math.max(t - r, S.value)), i <= n ? C.value = (n - i) / 2 : e ? C.value = 0 : C.value = Math.min(0, Math.max(n - i, C.value)), D();
29
+ function j() {
30
+ return Math.max(.05, Math.min(E.value, 1) * .5);
38
31
  }
39
32
  function M() {
40
- if (!y.value || !b.value) return;
41
- let { w: e, h: t } = O();
42
- w.value = Math.min(e / y.value, t / b.value), x.value = h.long ? e * .98 / y.value : Math.min(w.value, 1), j(h.long);
33
+ return Math.max(E.value, 1) * 8;
43
34
  }
44
- function N(e) {
45
- return Math.min(A(), Math.max(k(), e));
35
+ function N(e = !1) {
36
+ let { w: t, h: n } = A(), r = x.value * C.value, i = S.value * C.value;
37
+ w.value = r <= t ? (t - r) / 2 : Math.min(0, Math.max(t - r, w.value)), i <= n ? T.value = (n - i) / 2 : e ? T.value = 0 : T.value = Math.min(0, Math.max(n - i, T.value)), k();
46
38
  }
47
- function P(e, t, n) {
48
- let r = N(x.value * n);
49
- if (r === x.value) return;
50
- let i = (e - S.value) / x.value, a = (t - C.value) / x.value;
51
- x.value = r, S.value = e - i * r, C.value = t - a * r, j(!1);
39
+ function P() {
40
+ if (!x.value || !S.value) return;
41
+ let { w: e, h: t } = A(), n = _.long ? 0 : m * 2, r = Math.max(1, e - n), i = Math.max(1, t - n);
42
+ E.value = Math.min(r / x.value, i / S.value), C.value = _.long ? e * .98 / x.value : Math.min(E.value, 1), N(_.long);
52
43
  }
53
44
  function F(e) {
54
- let { w: t, h: n } = O();
55
- P(t / 2, n / 2, e);
45
+ return Math.min(M(), Math.max(j(), e));
56
46
  }
57
- function I(e) {
58
- let t = _.value?.getBoundingClientRect();
59
- P(t ? e.clientX - t.left : e.clientX, t ? e.clientY - t.top : e.clientY, e.deltaY < 0 ? 1.15 : 1 / 1.15);
47
+ function I(e, t, n) {
48
+ let r = F(C.value * n);
49
+ if (r === C.value) return;
50
+ let i = (e - w.value) / C.value, a = (t - T.value) / C.value;
51
+ C.value = r, w.value = e - i * r, T.value = t - a * r, N(!1);
60
52
  }
61
53
  function L(e) {
62
- let t = _.value?.getBoundingClientRect(), n = t ? e.clientX - t.left : e.clientX, r = t ? e.clientY - t.top : e.clientY;
63
- x.value <= w.value * 1.05 ? P(n, r, Math.max(2, 1 / Math.max(w.value, .001))) : M();
54
+ let { w: t, h: n } = A();
55
+ I(t / 2, n / 2, e);
64
56
  }
65
- let R = !1, z = !1, B = 0, V = 0, H = null;
66
- function U(e) {
67
- e.button === 0 && (R = !0, z = !1, B = e.clientX, V = e.clientY, H = e.target, _.value?.setPointerCapture(e.pointerId));
57
+ function R(e) {
58
+ let t = y.value?.getBoundingClientRect();
59
+ I(t ? e.clientX - t.left : e.clientX, t ? e.clientY - t.top : e.clientY, e.deltaY < 0 ? 1.15 : 1 / 1.15);
68
60
  }
69
- function W(e) {
70
- if (!R) return;
71
- let t = e.clientX - B, n = e.clientY - V;
72
- !z && Math.abs(t) + Math.abs(n) > 3 && (z = !0), B = e.clientX, V = e.clientY, S.value += t, C.value += n, j(!1);
61
+ function z(e) {
62
+ let t = y.value?.getBoundingClientRect(), n = t ? e.clientX - t.left : e.clientX, r = t ? e.clientY - t.top : e.clientY;
63
+ C.value <= E.value * 1.05 ? I(n, r, Math.max(2, 1 / Math.max(E.value, .001))) : P();
73
64
  }
65
+ let B = !1, V = !1, H = 0, U = 0, W = null;
74
66
  function G(e) {
75
- if (R) {
76
- R = !1;
67
+ e.button === 0 && (B = !0, V = !1, H = e.clientX, U = e.clientY, W = e.target, y.value?.setPointerCapture(e.pointerId));
68
+ }
69
+ function K(e) {
70
+ if (!B) return;
71
+ let t = e.clientX - H, n = e.clientY - U;
72
+ !V && Math.abs(t) + Math.abs(n) > 3 && (V = !0), H = e.clientX, U = e.clientY, w.value += t, T.value += n, N(!1);
73
+ }
74
+ function q(e) {
75
+ if (B) {
76
+ B = !1;
77
77
  try {
78
- _.value?.releasePointerCapture(e.pointerId);
78
+ y.value?.releasePointerCapture(e.pointerId);
79
79
  } catch {}
80
- !z && H !== v.value && g("emptyClick");
80
+ !V && W !== b.value && v("emptyClick");
81
81
  }
82
82
  }
83
- function K() {
84
- let e = v.value;
85
- e && (y.value = e.naturalWidth || 1, b.value = e.naturalHeight || 1, M(), T.value = !0);
86
- }
87
- let q = null;
88
83
  function J() {
89
- T.value && M();
84
+ let e = b.value;
85
+ e && (x.value = e.naturalWidth || 1, S.value = e.naturalHeight || 1, P(), D.value = !0);
86
+ }
87
+ let Y = null;
88
+ function X() {
89
+ D.value && P();
90
90
  }
91
- return u(() => h.src, () => {
92
- T.value = !1, y.value = 0, b.value = 0, x.value = 1, S.value = 0, C.value = 0, w.value = 1, D();
91
+ return d(() => _.src, () => {
92
+ D.value = !1, x.value = 0, S.value = 0, C.value = 1, w.value = 0, T.value = 0, E.value = 1, k();
93
93
  }), o(() => {
94
- typeof ResizeObserver < "u" && _.value && (q = new ResizeObserver(J), q.observe(_.value));
95
- }), a(() => q?.disconnect()), p({
96
- zoomIn: () => F(1.2),
97
- zoomOut: () => F(1 / 1.2),
98
- fit: () => M()
94
+ typeof ResizeObserver < "u" && y.value && (Y = new ResizeObserver(X), Y.observe(y.value));
95
+ }), a(() => Y?.disconnect()), h({
96
+ zoomIn: () => L(1.2),
97
+ zoomOut: () => L(1 / 1.2),
98
+ fit: () => P()
99
99
  }), (a, o) => (s(), e("div", {
100
100
  ref_key: "stageEl",
101
- ref: _,
102
- class: r(["zi", { "zi--grab": x.value > w.value }]),
103
- onWheel: d(I, ["prevent"]),
104
- onPointerdown: U,
105
- onPointermove: W,
106
- onPointerup: G,
107
- onPointercancel: G,
108
- onDblclick: L
101
+ ref: y,
102
+ class: r(["zi", { "zi--grab": C.value > E.value }]),
103
+ onWheel: f(R, ["prevent"]),
104
+ onPointerdown: G,
105
+ onPointermove: K,
106
+ onPointerup: q,
107
+ onPointercancel: q,
108
+ onDblclick: z
109
109
  }, [t("img", {
110
110
  ref_key: "imgEl",
111
- ref: v,
112
- class: r(["zi__img", { "zi__img--ready": T.value }]),
111
+ ref: b,
112
+ class: r(["zi__img", { "zi__img--ready": D.value }]),
113
113
  src: n.src,
114
114
  alt: n.alt,
115
115
  draggable: "false",
116
- style: i({ transform: `translate(${S.value}px, ${C.value}px) scale(${x.value})` }),
117
- onLoad: K,
118
- onDragstart: o[0] ||= d(() => {}, ["prevent"])
119
- }, null, 46, f), t("div", {
116
+ style: i({ transform: `translate(${w.value}px, ${T.value}px) scale(${C.value})` }),
117
+ onLoad: J,
118
+ onDragstart: o[0] ||= f(() => {}, ["prevent"])
119
+ }, null, 46, p), t("div", {
120
120
  class: "zi__toolbar",
121
- onPointerdown: o[4] ||= d(() => {}, ["stop"]),
122
- onDblclick: o[5] ||= d(() => {}, ["stop"]),
123
- onWheel: o[6] ||= d(() => {}, ["stop"])
121
+ onPointerdown: o[4] ||= f(() => {}, ["stop"]),
122
+ onDblclick: o[5] ||= f(() => {}, ["stop"]),
123
+ onWheel: o[6] ||= f(() => {}, ["stop"])
124
124
  }, [
125
125
  t("button", {
126
126
  type: "button",
127
127
  class: "zi__tool",
128
128
  title: "缩小 (-)",
129
- onClick: o[1] ||= (e) => F(1 / 1.2)
129
+ onClick: o[1] ||= (e) => L(1 / 1.2)
130
130
  }, " − "),
131
131
  t("button", {
132
132
  type: "button",
133
133
  class: "zi__pct",
134
134
  title: "适应窗口 (0)",
135
- onClick: o[2] ||= (e) => M()
136
- }, l(E.value) + "% ", 1),
135
+ onClick: o[2] ||= (e) => P()
136
+ }, l(O.value) + "% ", 1),
137
137
  t("button", {
138
138
  type: "button",
139
139
  class: "zi__tool",
140
140
  title: "放大 (+)",
141
- onClick: o[3] ||= (e) => F(1.2)
141
+ onClick: o[3] ||= (e) => L(1.2)
142
142
  }, "+")
143
143
  ], 32)], 34));
144
144
  }
145
145
  });
146
146
  //#endregion
147
- export { p as default };
147
+ export { h as default };
148
148
 
149
149
  //# sourceMappingURL=ZoomImage.vue_vue_type_script_setup_true_lang.js.map