bee-front-components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/config/config.ts +28 -0
  2. package/lib/assets/images/empty.png +0 -0
  3. package/lib/components/BaseModal/index.d.ts +41 -0
  4. package/lib/components/BaseModal/index.js +133 -0
  5. package/lib/components/BaseModal/index.module.less +66 -0
  6. package/lib/components/Buttons/Button.d.ts +17 -0
  7. package/lib/components/Buttons/Button.js +34 -0
  8. package/lib/components/Buttons/ButtonPermission.d.ts +18 -0
  9. package/lib/components/Buttons/ButtonPermission.js +34 -0
  10. package/lib/components/Buttons/index.d.ts +3 -0
  11. package/lib/components/Buttons/index.js +3 -0
  12. package/lib/components/Buttons/index.module.less +21 -0
  13. package/lib/components/ContentHeader/index.d.ts +17 -0
  14. package/lib/components/ContentHeader/index.js +22 -0
  15. package/lib/components/ContentHeader/index.module.less +14 -0
  16. package/lib/components/Empty/index.d.ts +17 -0
  17. package/lib/components/Empty/index.js +23 -0
  18. package/lib/components/Empty/index.module.less +15 -0
  19. package/lib/components/ExcelExport/index.d.ts +28 -0
  20. package/lib/components/ExcelExport/index.js +60 -0
  21. package/lib/components/ExcelExport/index.module.less +61 -0
  22. package/lib/components/Modal/index.d.ts +26 -0
  23. package/lib/components/Modal/index.js +191 -0
  24. package/lib/components/Modal/index.module.less +79 -0
  25. package/lib/components/PageHeader/index.d.ts +43 -0
  26. package/lib/components/PageHeader/index.js +72 -0
  27. package/lib/components/PageHeader/index.module.less +38 -0
  28. package/lib/components/PdfViewer/index.d.ts +49 -0
  29. package/lib/components/PdfViewer/index.js +699 -0
  30. package/lib/components/QueryBar/TableQueryBar.d.ts +32 -0
  31. package/lib/components/QueryBar/TableQueryBar.js +115 -0
  32. package/lib/components/QueryBar/index.d.ts +25 -0
  33. package/lib/components/QueryBar/index.js +102 -0
  34. package/lib/components/QueryBar/index.module.less +58 -0
  35. package/lib/components/RemainDay/index.d.ts +22 -0
  36. package/lib/components/RemainDay/index.js +47 -0
  37. package/lib/components/RemainDay/index.module.less +42 -0
  38. package/lib/components/Scanner/hooks/useKeyboardScanner.d.ts +14 -0
  39. package/lib/components/Scanner/hooks/useKeyboardScanner.js +95 -0
  40. package/lib/components/Scanner/index.d.ts +60 -0
  41. package/lib/components/Scanner/index.js +318 -0
  42. package/lib/components/Scanner/index.module.less +264 -0
  43. package/lib/components/Scanner/utils/device.d.ts +2 -0
  44. package/lib/components/Scanner/utils/device.js +18 -0
  45. package/lib/components/StatusTab/index.d.ts +36 -0
  46. package/lib/components/StatusTab/index.js +118 -0
  47. package/lib/components/StatusTab/index.module.less +20 -0
  48. package/lib/components/StatusTag/index.d.ts +32 -0
  49. package/lib/components/StatusTag/index.js +142 -0
  50. package/lib/components/StatusTag/index.module.less +37 -0
  51. package/lib/components/StepProgress/index.d.ts +36 -0
  52. package/lib/components/StepProgress/index.js +84 -0
  53. package/lib/components/StepProgress/index.module.less +180 -0
  54. package/lib/components/Table/index.d.ts +44 -0
  55. package/lib/components/Table/index.js +135 -0
  56. package/lib/components/Table/index.module.less +24 -0
  57. package/lib/components/TableOperationColumn/index.d.ts +45 -0
  58. package/lib/components/TableOperationColumn/index.js +205 -0
  59. package/lib/components/TableOperationColumn/index.module.less +22 -0
  60. package/lib/components/Tabs/TabGroup.d.ts +9 -0
  61. package/lib/components/Tabs/TabGroup.js +12 -0
  62. package/lib/components/Tabs/TabPane.d.ts +9 -0
  63. package/lib/components/Tabs/TabPane.js +12 -0
  64. package/lib/components/Tabs/Tabs.d.ts +23 -0
  65. package/lib/components/Tabs/Tabs.js +42 -0
  66. package/lib/components/Tabs/index.d.ts +4 -0
  67. package/lib/components/Tabs/index.js +4 -0
  68. package/lib/components/Tabs/index.module.less +20 -0
  69. package/lib/components/Upload/BatchUpload/index.d.ts +42 -0
  70. package/lib/components/Upload/BatchUpload/index.js +140 -0
  71. package/lib/components/Upload/BatchUpload/index.module.less +10 -0
  72. package/lib/components/Upload/DraggerUpload/index.d.ts +42 -0
  73. package/lib/components/Upload/DraggerUpload/index.js +142 -0
  74. package/lib/components/Upload/DraggerUpload/index.module.less +30 -0
  75. package/lib/components/Upload/FileUpload.d.ts +36 -0
  76. package/lib/components/Upload/FileUpload.js +122 -0
  77. package/lib/components/Upload/index.d.ts +4 -0
  78. package/lib/components/Upload/index.js +4 -0
  79. package/lib/components/VipFlag/index.d.ts +10 -0
  80. package/lib/components/VipFlag/index.js +23 -0
  81. package/lib/components/VipFlag/index.module.less +20 -0
  82. package/lib/typings/blank.d.ts +0 -0
  83. package/lib/typings.d.ts +3 -0
  84. package/lib/utils/const.d.ts +21 -0
  85. package/lib/utils/const.js +22 -0
  86. package/lib/utils/index.d.ts +95 -0
  87. package/lib/utils/index.js +319 -0
  88. package/lib/utils/menuTab.d.ts +60 -0
  89. package/lib/utils/menuTab.js +122 -0
  90. package/lib/utils/utils.d.ts +61 -0
  91. package/lib/utils/utils.js +270 -0
  92. package/package.json +41 -0
@@ -0,0 +1,699 @@
1
+ import _Icon from "choerodon-ui/lib/icon";
2
+ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
3
+ /**
4
+ * @name PdfViewer 轻量 PDF 预览组件(基于 pdfjs-dist)
5
+ * @description
6
+ * - 多实例:每个实例独立 getDocument,各自拥有独立 worker,可左右分屏对比多份文档
7
+ * - 懒加载:IntersectionObserver 仅渲染可视区页面(可提前 rootMargin 预渲染),滚动顺滑
8
+ * - 文档级 LRU 缓存:同批次/委托单多份文档来回切换时秒开,LRU 上限避免 90+ 份大单场景内存爆炸
9
+ * - 联动:暴露 scrollToPage / onVisiblePageChange,可做多实例同步滚动
10
+ * - 缩放:右下角工具栏按钮(缩放/比例显示),或 Ctrl/Cmd + 滚轮 / 双指捏合(替代浏览器默认缩放),按锚点保持缩放前后内容位置
11
+ * - 定位:url 携带 #page=N 片段(如 xxx.pdf#page=3)时,文档加载完成后自动滚动到对应页
12
+ *
13
+ * @note worker 部署
14
+ * pdf.js 解析运行在 Web Worker 中,需要加载 pdf.worker.min.js:
15
+ * 请将 node_modules/pdfjs-dist/build/pdf.worker.min.js 复制到应用静态根目录(dist/),
16
+ * 或通过 <PdfViewer workerSrc="..." /> 指定可访问地址。
17
+ * 若 worker 加载失败,pdf.js 会自动降级为 fake worker(主线程解析),功能可用但会阻塞 UI。
18
+ */
19
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
20
+ import * as pdfjsNS from "pdfjs-dist";
21
+
22
+ // pdfjs-dist v2 是 AMD/UMD 构建,webpack 下 `import * as` 可能被包装成 { default: PDFJS },
23
+ // 统一取 default(不存在则用命名空间本体),避免 pdfjsLib.getDocument 解析为 undefined
24
+ const pdfjsLib = pdfjsNS.default || pdfjsNS;
25
+
26
+ /* ====================== 1. Worker 配置 ====================== */
27
+ // 基于 umi define 注入的 PUBLIC_URL 拼接(dev 为 "/",prod 部署时 BUILD_PUBLIC_URL 会替换成实际部署路径);
28
+ // public/ 下的 pdf.worker.min.js 会随构建自动拷贝到静态根目录
29
+ const DEFAULT_WORKER_SRC = `${process.env.PUBLIC_URL}pdf.worker.min.js`;
30
+ export function configurePdfWorker(src) {
31
+ pdfjsLib.GlobalWorkerOptions.workerSrc = src;
32
+ }
33
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
34
+ configurePdfWorker(DEFAULT_WORKER_SRC);
35
+ }
36
+
37
+ /* ====================== 2. 工具:LRU 缓存 ====================== */
38
+ class LRUCache {
39
+ constructor(max) {
40
+ this.max = max;
41
+ this.map = new Map();
42
+ }
43
+ has(key) {
44
+ return this.map.has(key);
45
+ }
46
+ get(key) {
47
+ const value = this.map.get(key);
48
+ if (value !== undefined) {
49
+ // 命中后刷新到末尾(最新)
50
+ this.map.delete(key);
51
+ this.map.set(key, value);
52
+ }
53
+ return value;
54
+ }
55
+ set(key, value) {
56
+ if (this.map.has(key)) {
57
+ this.map.delete(key);
58
+ }
59
+ this.map.set(key, value);
60
+ // 超出上限淘汰最久未用的
61
+ if (this.map.size > this.max) {
62
+ const oldest = this.map.keys().next().value;
63
+ if (oldest !== undefined) {
64
+ this.map.delete(oldest);
65
+ }
66
+ }
67
+ }
68
+ clear() {
69
+ this.map.clear();
70
+ }
71
+ }
72
+
73
+ /* ====================== 3. 文档级 LRU 缓存 ====================== */
74
+
75
+ const DOC_CACHE_MAX = 5;
76
+ const docCache = new LRUCache(DOC_CACHE_MAX);
77
+ // 懒加载:前 2 页强制渲染作为首屏兜底(IO 尚未触发时也不空白),其余页按可视区按需渲染,避免大文档全量渲染卡死
78
+ const FORCE_RENDER_PAGES = 2;
79
+ // 缩放范围与步进(Ctrl/Cmd + 滚轮 或 双指捏合 触发)
80
+ const MIN_SCALE = 0.5;
81
+ const MAX_SCALE = 4;
82
+ const ZOOM_STEP = 1.1;
83
+
84
+ /** 按 URL 获取(并缓存)PDFDocumentProxy,多实例共享同一解析结果 */
85
+ function getDocument(url) {
86
+ // 中文等非 ASCII 字符需百分号编码;encodeURI 对已编码序列幂等,不会二次编码
87
+ const encodedUrl = encodeURI(url);
88
+ const cached = docCache.get(encodedUrl);
89
+ if (cached) {
90
+ return Promise.resolve(cached);
91
+ }
92
+ const loadingTask = pdfjsLib.getDocument({
93
+ url: encodedUrl
94
+ });
95
+ return loadingTask.promise.then(doc => {
96
+ const item = {
97
+ doc,
98
+ loadingTask
99
+ };
100
+ docCache.set(encodedUrl, item);
101
+ return item;
102
+ });
103
+ }
104
+
105
+ /* ====================== 4. 单页渲染器 ====================== */
106
+
107
+ /**
108
+ * 单页:按页面宽高比自适应——纵向页宽度适配容器,横向页高度适配可视区(一页一屏,超宽回退宽度适配)。
109
+ * 懒加载:仅渲染可视页(前 2 页兜底强制渲染),页面对象就绪后立即渲染(page 为 state,异步就绪会正确触发重跑)。
110
+ */
111
+ const PageItem = /*#__PURE__*/React.memo(({
112
+ doc,
113
+ pageNum,
114
+ scale,
115
+ containerWidth,
116
+ containerHeight,
117
+ visible
118
+ }) => {
119
+ const canvasRef = useRef(null);
120
+ // 离屏 canvas(复用):渲染先画到离屏画布,完成后同一帧内一次性提交到可见 canvas,
121
+ // 避免"canvas.width 赋值清空画布 → 异步渲染空窗"导致的变黑闪烁
122
+ const offscreenRef = useRef(null);
123
+ const _useState = useState(null),
124
+ _useState2 = _slicedToArray(_useState, 2),
125
+ page = _useState2[0],
126
+ setPage = _useState2[1];
127
+ // 已渲染的尺寸指纹("宽x高"):缩放导致 display 变化时据此按新 scale 重新绘制,避免 canvas 被 CSS 拉伸而模糊
128
+ const renderedKeyRef = useRef("");
129
+ // 适配后的显示尺寸(逻辑像素)
130
+ const _useState3 = useState(null),
131
+ _useState4 = _slicedToArray(_useState3, 2),
132
+ display = _useState4[0],
133
+ setDisplay = _useState4[1];
134
+
135
+ // 获取页面对象,按宽高比计算适配后的显示尺寸
136
+ useEffect(() => {
137
+ let cancelled = false;
138
+ doc.getPage(pageNum).then(p => {
139
+ if (cancelled) {
140
+ return;
141
+ }
142
+ setPage(p);
143
+ const raw = p.getViewport({
144
+ scale: 1
145
+ });
146
+ // 容器宽度尚未测出时(首帧/弹窗时序)用默认 800 兜底,避免整页塌陷成空白
147
+ const cw = containerWidth > 0 ? containerWidth : 800;
148
+ const ch = containerHeight > 0 ? containerHeight : 0;
149
+ const ratio = raw.height / raw.width;
150
+ let w;
151
+ let h;
152
+ if (ch > 0 && ratio <= 1) {
153
+ // 横向页:优先高度适配(一页一屏),高度适配超宽则回退宽度适配
154
+ h = ch;
155
+ w = ch / ratio;
156
+ if (w > cw) {
157
+ w = cw;
158
+ h = cw * ratio;
159
+ }
160
+ } else {
161
+ // 纵向页(或容器高度未知):宽度适配,高度按比例
162
+ w = cw;
163
+ h = cw * ratio;
164
+ }
165
+ setDisplay({
166
+ w: Math.floor(w * scale),
167
+ h: Math.floor(h * scale)
168
+ });
169
+ });
170
+ return () => {
171
+ cancelled = true;
172
+ };
173
+ }, [doc, pageNum, scale, containerWidth, containerHeight]);
174
+
175
+ // 渲染:懒加载(前 2 页兜底强制渲染),页面对象 + 适配尺寸就绪 + 进入可视区后渲染;
176
+ // display 变化(缩放/尺寸变化)时尺寸指纹不同,会按新 scale 重新绘制。
177
+ // 离屏渲染 + 一次性提交:重渲染期间可见 canvas 保持旧内容,渲染完成瞬间切新内容,无"变黑"空窗
178
+ useEffect(() => {
179
+ if (!page || !display || !canvasRef.current) {
180
+ return;
181
+ }
182
+ const key = `${display.w}x${display.h}`;
183
+ if (renderedKeyRef.current === key) {
184
+ return;
185
+ }
186
+ // 第 3 页起仅渲染可视页(滚动进入可视区时此 effect 因 visible 变化重跑)
187
+ if (pageNum > FORCE_RENDER_PAGES && !visible) {
188
+ return;
189
+ }
190
+ const raw = page.getViewport({
191
+ scale: 1
192
+ });
193
+ const viewport = page.getViewport({
194
+ scale: display.w / raw.width
195
+ });
196
+ // 高清适配(pdf.js 官方 Viewer 同款做法):位图按 devicePixelRatio 放大、CSS 保持逻辑尺寸,
197
+ // 并传入匹配的 transform,让渲染器按高分辨率绘制,接近 Chrome 原生 PDF 查看器的清晰度
198
+ const outputScale = window.devicePixelRatio || 1;
199
+ const bw = Math.floor(viewport.width * outputScale);
200
+ const bh = Math.floor(viewport.height * outputScale);
201
+ const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;
202
+
203
+ // 1. 先渲染到离屏 canvas
204
+ let offscreen = offscreenRef.current;
205
+ if (!offscreen) {
206
+ offscreen = document.createElement("canvas");
207
+ offscreenRef.current = offscreen;
208
+ }
209
+ offscreen.width = bw;
210
+ offscreen.height = bh;
211
+ const offCtx = offscreen.getContext("2d", {
212
+ alpha: false
213
+ });
214
+ const renderTask = page.render({
215
+ canvasContext: offCtx,
216
+ viewport,
217
+ transform
218
+ });
219
+ renderTask.promise.then(() => {
220
+ renderedKeyRef.current = key;
221
+ // 2. 渲染完成,同一帧内提交到可见 canvas(设尺寸清空 + drawImage 同步完成,浏览器不插帧)
222
+ const canvas = canvasRef.current;
223
+ if (!canvas) {
224
+ return;
225
+ }
226
+ canvas.width = bw;
227
+ canvas.height = bh;
228
+ const ctx = canvas.getContext("2d", {
229
+ alpha: false
230
+ });
231
+ if (offscreen) {
232
+ ctx.drawImage(offscreen, 0, 0);
233
+ }
234
+ }).catch(err => {
235
+ // 渲染被取消(deps 变化/卸载触发 renderTask.cancel())是预期行为,忽略;其余错误抛给上层
236
+ if ((err === null || err === void 0 ? void 0 : err.name) !== "RenderingCancelledException") {
237
+ // console.error(err);
238
+ }
239
+ });
240
+ return () => {
241
+ renderTask.cancel();
242
+ };
243
+ }, [page, display, visible, pageNum]);
244
+ return /*#__PURE__*/React.createElement("div", {
245
+ style: {
246
+ width: (display === null || display === void 0 ? void 0 : display.w) || undefined,
247
+ height: (display === null || display === void 0 ? void 0 : display.h) || undefined,
248
+ position: "relative",
249
+ margin: "0 auto"
250
+ }
251
+ }, /*#__PURE__*/React.createElement("canvas", {
252
+ ref: canvasRef,
253
+ style: {
254
+ width: "100%",
255
+ height: "100%",
256
+ display: "block",
257
+ background: "#fff"
258
+ }
259
+ }));
260
+ });
261
+
262
+ /* ====================== 5. 主组件 ====================== */
263
+
264
+ const PdfViewer = /*#__PURE__*/forwardRef((props, ref) => {
265
+ const url = props.url,
266
+ workerSrc = props.workerSrc,
267
+ _props$scale = props.scale,
268
+ scale = _props$scale === void 0 ? 1 : _props$scale,
269
+ _props$height = props.height,
270
+ height = _props$height === void 0 ? "100%" : _props$height,
271
+ _props$gap = props.gap,
272
+ gap = _props$gap === void 0 ? 8 : _props$gap,
273
+ _props$lazy = props.lazy,
274
+ lazy = _props$lazy === void 0 ? true : _props$lazy,
275
+ onLoad = props.onLoad,
276
+ onError = props.onError,
277
+ onVisiblePageChange = props.onVisiblePageChange,
278
+ className = props.className;
279
+ const containerRef = useRef(null);
280
+ const wrapperRefs = useRef([]);
281
+ const docRef = useRef(null);
282
+ const lastReportedPageRef = useRef(0);
283
+ // URL 片段目标页码(如 xxx.pdf#page=3),文档加载后自动定位
284
+ const targetPageRef = useRef(0);
285
+ const _useState5 = useState(null),
286
+ _useState6 = _slicedToArray(_useState5, 2),
287
+ doc = _useState6[0],
288
+ setDoc = _useState6[1];
289
+ const _useState7 = useState(0),
290
+ _useState8 = _slicedToArray(_useState7, 2),
291
+ pageCount = _useState8[0],
292
+ setPageCount = _useState8[1];
293
+ const _useState9 = useState(null),
294
+ _useState0 = _slicedToArray(_useState9, 2),
295
+ error = _useState0[0],
296
+ setError = _useState0[1];
297
+ const _useState1 = useState(0),
298
+ _useState10 = _slicedToArray(_useState1, 2),
299
+ containerWidth = _useState10[0],
300
+ setContainerWidth = _useState10[1];
301
+ const _useState11 = useState(0),
302
+ _useState12 = _slicedToArray(_useState11, 2),
303
+ containerHeight = _useState12[0],
304
+ setContainerHeight = _useState12[1];
305
+ const _useState13 = useState(new Set()),
306
+ _useState14 = _slicedToArray(_useState13, 2),
307
+ visiblePages = _useState14[0],
308
+ setVisiblePages = _useState14[1];
309
+ // 同步 ref,避免 IO 回调闭包读到过期的 visiblePages
310
+ const visiblePagesRef = useRef(new Set());
311
+ // 内部缩放:初始取 props.scale,之后由 Ctrl/Cmd+滚轮 / 双指捏合接管(props.scale 仅作初始值)
312
+ const _useState15 = useState(scale),
313
+ _useState16 = _slicedToArray(_useState15, 2),
314
+ zoom = _useState16[0],
315
+ setZoom = _useState16[1];
316
+ const zoomRef = useRef(zoom);
317
+ zoomRef.current = zoom;
318
+
319
+ // worker 地址覆盖
320
+ useEffect(() => {
321
+ if (workerSrc) {
322
+ configurePdfWorker(workerSrc);
323
+ }
324
+ }, [workerSrc]);
325
+
326
+ // 容器宽高变化监听(自适应布局)
327
+ // 200ms 防抖:容器宽度过渡动画(如对比模式 50% 切换)期间每帧都触发 RO,若同步 setState
328
+ // 会导致几十页 PageItem 反复重算+重渲染(闪烁/卡顿),合并到动画结束后统一更新一次。
329
+ // 首次回调(observe 后异步触发,此时布局已稳定)立即应用尺寸,避免挂载空窗导致的“无→出文件”闪烁
330
+ useEffect(() => {
331
+ const container = containerRef.current;
332
+ if (!container) {
333
+ return;
334
+ }
335
+ let timer;
336
+ let first = true;
337
+ const applySize = () => {
338
+ const w = container.clientWidth;
339
+ const h = container.clientHeight;
340
+ if (w) {
341
+ setContainerWidth(w);
342
+ }
343
+ if (h) {
344
+ setContainerHeight(h);
345
+ }
346
+ };
347
+ // 挂载即测量一次
348
+ applySize();
349
+ const ro = new ResizeObserver(() => {
350
+ if (first) {
351
+ first = false;
352
+ applySize();
353
+ return;
354
+ }
355
+ if (timer) {
356
+ clearTimeout(timer);
357
+ }
358
+ timer = window.setTimeout(applySize, 200);
359
+ });
360
+ ro.observe(container);
361
+ return () => {
362
+ if (timer) {
363
+ clearTimeout(timer);
364
+ }
365
+ ro.disconnect();
366
+ };
367
+ }, []);
368
+
369
+ // 加载文档(走 LRU 缓存)
370
+ useEffect(() => {
371
+ let cancelled = false;
372
+ docRef.current = null;
373
+ setDoc(null);
374
+ setPageCount(0);
375
+ setError(null);
376
+ setVisiblePages(new Set());
377
+ visiblePagesRef.current = new Set();
378
+ lastReportedPageRef.current = 0;
379
+ if (!url) {
380
+ return;
381
+ }
382
+ // 解析 URL 片段中的目标页码(如 xxx.pdf#page=3);# 片段不参与请求,仅用于定位
383
+ const pageMatch = /#page=(\d+)/.exec(url);
384
+ targetPageRef.current = pageMatch ? Math.max(1, Number(pageMatch[1])) : 0;
385
+ getDocument(url).then(({
386
+ doc: d
387
+ }) => {
388
+ if (cancelled) {
389
+ return;
390
+ }
391
+ docRef.current = d;
392
+ setDoc(d);
393
+ setPageCount(d.numPages);
394
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad(d);
395
+ }).catch(err => {
396
+ if (cancelled) {
397
+ return;
398
+ }
399
+ setError(err);
400
+ onError === null || onError === void 0 ? void 0 : onError(err);
401
+ });
402
+ return () => {
403
+ cancelled = true;
404
+ };
405
+ }, [url]);
406
+
407
+ // 懒加载:IntersectionObserver 观察每页 wrapper,仅可视页触发渲染
408
+ useEffect(() => {
409
+ if (!lazy || !doc) {
410
+ return;
411
+ }
412
+ const container = containerRef.current;
413
+ if (!container) {
414
+ return;
415
+ }
416
+ const observer = new IntersectionObserver(entries => {
417
+ // 基于 ref 取最新集合,避免闭包过期导致可见页丢失
418
+ const next = new Set(visiblePagesRef.current);
419
+ entries.forEach(entry => {
420
+ const index = Number(entry.target.dataset.page);
421
+ if (entry.isIntersecting) {
422
+ next.add(index + 1);
423
+ } else {
424
+ next.delete(index + 1);
425
+ }
426
+ });
427
+ visiblePagesRef.current = next;
428
+ setVisiblePages(next);
429
+ const minPage = next.size ? Math.min(...next) : 0;
430
+ if (minPage && minPage !== lastReportedPageRef.current) {
431
+ lastReportedPageRef.current = minPage;
432
+ onVisiblePageChange === null || onVisiblePageChange === void 0 ? void 0 : onVisiblePageChange(minPage);
433
+ }
434
+ }, {
435
+ root: container,
436
+ rootMargin: "2rem 0"
437
+ } // 提前 2rem 预渲染,滚动更顺滑
438
+ );
439
+ // rAF 延迟观察,确保页面布局稳定后再建立可视区监听
440
+ const rafId = requestAnimationFrame(() => {
441
+ wrapperRefs.current.forEach(el => el && observer.observe(el));
442
+ });
443
+ return () => {
444
+ observer.disconnect();
445
+ cancelAnimationFrame(rafId);
446
+ };
447
+ }, [doc, lazy]);
448
+
449
+ // 统一缩放入口:factor 为期望倍率,anchorY 为锚点相对容器可视顶部的偏移;
450
+ // 缩放后按比例补偿 scrollTop,保持锚点处内容不漂移(页面高度随 scale 近似线性变化)
451
+ const applyZoom = useCallback((factor, anchorY) => {
452
+ const container = containerRef.current;
453
+ if (!container) {
454
+ return;
455
+ }
456
+ const current = zoomRef.current;
457
+ const nextZoom = Math.min(MAX_SCALE, Math.max(MIN_SCALE, current * factor));
458
+ if (nextZoom === current) {
459
+ return;
460
+ }
461
+ const actualFactor = nextZoom / current;
462
+ const contentY = container.scrollTop + anchorY;
463
+ setZoom(nextZoom);
464
+ // 等页面按新 scale 重排后再校正滚动位置(双 rAF 确保布局已更新)
465
+ requestAnimationFrame(() => {
466
+ requestAnimationFrame(() => {
467
+ container.scrollTop = Math.max(0, contentY * actualFactor - anchorY);
468
+ });
469
+ });
470
+ }, []);
471
+
472
+ // Ctrl/Cmd + 滚轮 与 双指捏合 缩放
473
+ // 用原生监听(passive: false):React 合成 wheel/touch 事件默认 passive,preventDefault 无法拦截浏览器默认缩放
474
+ useEffect(() => {
475
+ const container = containerRef.current;
476
+ if (!container) {
477
+ return;
478
+ }
479
+
480
+ // Ctrl/Cmd + 滚轮缩放,替代浏览器默认的页面缩放(Mac 触控板捏合即此路径)
481
+ const onWheel = e => {
482
+ if (!e.ctrlKey && !e.metaKey) {
483
+ return;
484
+ }
485
+ e.preventDefault();
486
+ const rect = container.getBoundingClientRect();
487
+ applyZoom(e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP, e.clientY - rect.top);
488
+ };
489
+
490
+ // 双指捏合缩放(单指不拦截,保留原生滚动)
491
+ let pinchStartDist = 0;
492
+ const onTouchStart = e => {
493
+ if (e.touches.length >= 2) {
494
+ e.preventDefault();
495
+ pinchStartDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
496
+ }
497
+ };
498
+ const onTouchMove = e => {
499
+ if (e.touches.length >= 2 && pinchStartDist > 0) {
500
+ e.preventDefault();
501
+ const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
502
+ const rect = container.getBoundingClientRect();
503
+ const anchorY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
504
+ // 以捏合开始时的距离为基准,避免逐事件累积误差;
505
+ // 倍率变化按 1/10 衰减,双指轻微移动不会导致缩放速度过猛
506
+ const factor = 1 + (dist / pinchStartDist - 1) / 10;
507
+ applyZoom(factor, anchorY);
508
+ }
509
+ };
510
+ const resetPinch = () => {
511
+ pinchStartDist = 0;
512
+ };
513
+ container.addEventListener("wheel", onWheel, {
514
+ passive: false
515
+ });
516
+ container.addEventListener("touchstart", onTouchStart, {
517
+ passive: false
518
+ });
519
+ container.addEventListener("touchmove", onTouchMove, {
520
+ passive: false
521
+ });
522
+ container.addEventListener("touchend", resetPinch);
523
+ container.addEventListener("touchcancel", resetPinch);
524
+ return () => {
525
+ container.removeEventListener("wheel", onWheel);
526
+ container.removeEventListener("touchstart", onTouchStart);
527
+ container.removeEventListener("touchmove", onTouchMove);
528
+ container.removeEventListener("touchend", resetPinch);
529
+ container.removeEventListener("touchcancel", resetPinch);
530
+ };
531
+ }, [applyZoom]);
532
+
533
+ // 右下角工具栏缩放:以可视区中心为锚点,缩放后中心内容不漂移
534
+ const zoomIn = useCallback(() => {
535
+ const container = containerRef.current;
536
+ if (container) {
537
+ applyZoom(ZOOM_STEP, container.clientHeight / 2);
538
+ }
539
+ }, [applyZoom]);
540
+ const zoomOut = useCallback(() => {
541
+ const container = containerRef.current;
542
+ if (container) {
543
+ applyZoom(1 / ZOOM_STEP, container.clientHeight / 2);
544
+ }
545
+ }, [applyZoom]);
546
+
547
+ // 滚动到指定页(容器内滚动,避免 scrollIntoView 影响整页)
548
+ const scrollToPage = useCallback(pageNum => {
549
+ const container = containerRef.current;
550
+ const wrapper = wrapperRefs.current[pageNum - 1];
551
+ if (!container || !wrapper) {
552
+ return;
553
+ }
554
+ container.scrollTop = wrapper.offsetTop - container.offsetTop;
555
+ }, []);
556
+
557
+ // url 携带 #page=N 时,文档加载且目标页高度就绪后自动定位。
558
+ // 懒加载下页面高度异步出现(getPage → setDisplay),故轮询等待目标页高度 > 0 再滚动,
559
+ // 超时(约 3s)放弃,避免目标页不存在(页码越界)时无限循环
560
+ useEffect(() => {
561
+ const target = targetPageRef.current;
562
+ if (!doc || target <= 0) {
563
+ return;
564
+ }
565
+ let tries = 0;
566
+ const timer = window.setInterval(() => {
567
+ const wrapper = wrapperRefs.current[target - 1];
568
+ if (wrapper && wrapper.offsetHeight > 0) {
569
+ scrollToPage(target);
570
+ window.clearInterval(timer);
571
+ return;
572
+ }
573
+ tries += 1;
574
+ if (tries > 30) {
575
+ window.clearInterval(timer);
576
+ }
577
+ }, 100);
578
+ return () => {
579
+ window.clearInterval(timer);
580
+ };
581
+ }, [doc, scrollToPage]);
582
+ const getCurrentPage = useCallback(() => {
583
+ return visiblePages.size ? Math.min(...visiblePages) : 0;
584
+ }, [visiblePages]);
585
+ useImperativeHandle(ref, () => ({
586
+ scrollToPage,
587
+ getCurrentPage,
588
+ getDocument: () => docRef.current
589
+ }), [scrollToPage, getCurrentPage]);
590
+
591
+ // 页面列表(占位高度由 PageItem 自算,wrapper 仅作滚动定位锚点)
592
+ const pageWrappers = useMemo(() => {
593
+ if (!doc || pageCount === 0) {
594
+ return null;
595
+ }
596
+ const items = [];
597
+ for (let i = 1; i <= pageCount; i++) {
598
+ items.push(/*#__PURE__*/React.createElement("div", {
599
+ key: i,
600
+ ref: el => {
601
+ wrapperRefs.current[i - 1] = el;
602
+ },
603
+ "data-page": i - 1,
604
+ style: {
605
+ marginBottom: gap
606
+ }
607
+ }, /*#__PURE__*/React.createElement(PageItem, {
608
+ doc: doc,
609
+ pageNum: i,
610
+ scale: zoom,
611
+ containerWidth: containerWidth,
612
+ containerHeight: containerHeight,
613
+ visible: visiblePages.has(i)
614
+ })));
615
+ }
616
+ return items;
617
+ }, [doc, pageCount, zoom, containerWidth, containerHeight, visiblePages, lazy, gap]);
618
+ return /*#__PURE__*/React.createElement("div", {
619
+ className: className,
620
+ style: {
621
+ position: "relative",
622
+ height,
623
+ background: "#f0f2f5"
624
+ }
625
+ }, /*#__PURE__*/React.createElement("div", {
626
+ ref: containerRef,
627
+ style: {
628
+ height: "100%",
629
+ overflow: "auto",
630
+ // 允许平移但双指缩放由组件接管,避免浏览器默认页面缩放
631
+ touchAction: "pan-x pan-y"
632
+ }
633
+ }, !doc && !error && /*#__PURE__*/React.createElement("div", {
634
+ style: {
635
+ padding: 32,
636
+ textAlign: "center",
637
+ color: "#999"
638
+ }
639
+ }, "\u52A0\u8F7D\u4E2D..."), error && /*#__PURE__*/React.createElement("div", {
640
+ style: {
641
+ padding: 32,
642
+ textAlign: "center",
643
+ color: "#f5222d"
644
+ }
645
+ }, "PDF \u52A0\u8F7D\u5931\u8D25\uFF1A", String((error === null || error === void 0 ? void 0 : error.message) || error)), pageWrappers), doc && /*#__PURE__*/React.createElement("div", {
646
+ style: {
647
+ position: "absolute",
648
+ right: 12,
649
+ bottom: 12,
650
+ display: "flex",
651
+ flexDirection: "column",
652
+ alignItems: "flex-end",
653
+ gap: 4,
654
+ userSelect: "none"
655
+ }
656
+ }, /*#__PURE__*/React.createElement("div", {
657
+ style: {
658
+ display: "inline-flex",
659
+ alignItems: "center",
660
+ gap: 4,
661
+ padding: "0 0.06rem",
662
+ background: "#fff",
663
+ borderRadius: 4,
664
+ boxShadow: "0 0.02rem 0.08rem rgba(0, 0, 0, 0.15)"
665
+ }
666
+ }, /*#__PURE__*/React.createElement(_Icon, {
667
+ type: "suoxiao1",
668
+ title: "\u7F29\u5C0F",
669
+ onClick: zoomOut,
670
+ style: {
671
+ cursor: "pointer",
672
+ fontSize: 16,
673
+ color: "#333"
674
+ }
675
+ }), /*#__PURE__*/React.createElement("span", {
676
+ style: {
677
+ minWidth: 40,
678
+ textAlign: "center",
679
+ fontSize: 12,
680
+ color: "#666"
681
+ }
682
+ }, Math.round(zoom * 100), "%"), /*#__PURE__*/React.createElement(_Icon, {
683
+ type: "fangda1",
684
+ title: "\u653E\u5927",
685
+ onClick: zoomIn,
686
+ style: {
687
+ cursor: "pointer",
688
+ fontSize: 16,
689
+ color: "#333"
690
+ }
691
+ })), /*#__PURE__*/React.createElement("span", {
692
+ style: {
693
+ fontSize: 12,
694
+ color: "#999"
695
+ }
696
+ }, "\u652F\u6301\u53CC\u6307 / Ctrl+\u6EDA\u8F6E\u7F29\u653E")));
697
+ });
698
+ PdfViewer.displayName = "PdfViewer";
699
+ export default PdfViewer;