images-viewer-js 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.
package/dist/index.js ADDED
@@ -0,0 +1,1813 @@
1
+ 'use strict';
2
+
3
+ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
4
+
5
+ function getDefaultExportFromCjs (x) {
6
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
7
+ }
8
+
9
+ var imageViewer = {exports: {}};
10
+
11
+ (function (module, exports) {
12
+ // image-viewer.js - 修复居中问题的版本
13
+ (function (global, factory) {
14
+ // UMD包装器,支持CommonJS、AMD和全局变量
15
+ {
16
+ // CommonJS/Node.js环境
17
+ module.exports = factory();
18
+ }
19
+ })(typeof self !== 'undefined' ? self : commonjsGlobal, function () {
20
+
21
+ // 节流函数
22
+ function throttle(func, delay) {
23
+ let timeoutId;
24
+ let lastExecTime = 0;
25
+ return function (...args) {
26
+ const currentTime = Date.now();
27
+ const timeSinceLastExec = currentTime - lastExecTime;
28
+
29
+ if (timeSinceLastExec > delay) {
30
+ func.apply(this, args);
31
+ lastExecTime = currentTime;
32
+ } else {
33
+ clearTimeout(timeoutId);
34
+ timeoutId = setTimeout(() => {
35
+ func.apply(this, args);
36
+ lastExecTime = currentTime;
37
+ }, delay - timeSinceLastExec);
38
+ }
39
+ };
40
+ }
41
+
42
+ class ImageViewer {
43
+ constructor(options) {
44
+ // 默认配置
45
+ this.defaultOptions = {
46
+ closeOnMaskClick: false,
47
+ loop: true,
48
+ buttons: {
49
+ zoomIn: true,
50
+ zoomOut: true,
51
+ rotateLeft: true,
52
+ rotateRight: true,
53
+ reset: true,
54
+ download: true,
55
+ fullscreen: true,
56
+ prev: true,
57
+ next: true,
58
+ close: true,
59
+ topClose: true,
60
+ thumbnails: true,
61
+ info: true,
62
+ originalSize: true,
63
+ },
64
+ imageInfo: {
65
+ visible: false,
66
+ showName: true,
67
+ showDimensions: true,
68
+ },
69
+ theme: {
70
+ // 背景相关
71
+ viewerBgColor: 'rgba(0, 0, 0, 0.4)',
72
+
73
+ // 工具栏相关(半透明浅灰,营造朦胧感)
74
+ toolbarBgColor: 'rgba(150, 150, 150, 0.7)',
75
+ toolbarBorderRadius: '30px',
76
+ toolbarPadding: '8px 12px',
77
+ toolbarBottom: '20px',
78
+
79
+ // 按钮相关(半透明中灰)
80
+ buttonBgColor: 'rgba(150, 150, 150, 0.7)',
81
+ buttonHoverBg: 'rgba(200, 200, 200, 0.4)',
82
+ buttonSize: '50px',
83
+ buttonFontSize: '20px',
84
+ buttonBorderRadius: '50%',
85
+
86
+ // 右上角关闭按钮
87
+ topCloseBtnSize: '44px',
88
+ topCloseBtnTop: '20px',
89
+ topCloseBtnRight: '20px',
90
+
91
+ // 信息栏相关(半透明浅灰)
92
+ infoBgColor: 'rgba(150, 150, 150, 0.7)',
93
+ infoBorderRadius: '12px',
94
+ infoPadding: '10px 15px',
95
+ infoFontSize: '13px',
96
+ infoTop: '70px',
97
+ infoLeft: '20px',
98
+
99
+ // 缩放指示器
100
+ zoomIndicatorBg: 'rgba(150, 150, 150, 0.7)',
101
+ zoomIndicatorBorderRadius: '18px',
102
+ zoomIndicatorPadding: '6px 12px',
103
+ zoomIndicatorFontSize: '14px',
104
+ zoomIndicatorTop: '20px',
105
+ zoomIndicatorLeft: '20px',
106
+
107
+ // 通用
108
+ activeColor: 'rgba(100, 150, 255, 0.8)',
109
+ textColor: 'rgba(255, 255, 255, 0.9)',
110
+ shadowColor: 'rgba(0, 0, 0, 0.2)',
111
+ transitionSpeed: '0.3s',
112
+ },
113
+ };
114
+
115
+ // 合并用户配置
116
+ this.options = {
117
+ ...this.defaultOptions,
118
+ ...options,
119
+ buttons: { ...this.defaultOptions.buttons, ...(options?.buttons || {}) },
120
+ imageInfo: { ...this.defaultOptions.imageInfo, ...(options?.imageInfo || {}) },
121
+ theme: { ...this.defaultOptions.theme, ...(options?.theme || {}) },
122
+ };
123
+
124
+ // 解析图片参数
125
+ this.parseImageOptions(options);
126
+ if (this.images.length === 0) {
127
+ throw new Error('未提供有效的图片URL');
128
+ }
129
+
130
+ // 初始化状态变量
131
+ this.currentIndex = 0;
132
+ this.scale = 1.0;
133
+ this.rotation = 0;
134
+ this.translateX = 0;
135
+ this.translateY = 0;
136
+ this.isDragging = false;
137
+ this.startX = 0;
138
+ this.startY = 0;
139
+ this.startTranslateX = 0;
140
+ this.startTranslateY = 0;
141
+ this.isFullscreen = false;
142
+ this.imageInfoVisible = this.options.imageInfo.visible;
143
+ this.imageMetadata = [];
144
+ this.loadedImages = new Map();
145
+
146
+ // 双击相关状态
147
+ this.lastTapTime = 0;
148
+ this.lastScale = 1.0;
149
+ this.lastTranslateX = 0;
150
+ this.lastTranslateY = 0;
151
+ this.hasPreviousState = false;
152
+ this.isToggledState = false;
153
+
154
+ // 触摸状态
155
+ this.touchState = {
156
+ isDragging: false,
157
+ isPinching: false,
158
+ initialDistance: null,
159
+ initialScale: null,
160
+ initialTranslateX: null,
161
+ initialTranslateY: null,
162
+ centerX: null,
163
+ centerY: null,
164
+ relativeCenterX: null,
165
+ relativeCenterY: null,
166
+ lastTouchTime: 0,
167
+ startX: 0,
168
+ startY: 0,
169
+ startTranslateX: 0,
170
+ startTranslateY: 0,
171
+ minScaleChange: 0.005,
172
+ scaleRatio: 1,
173
+ stabilizationThreshold: 3,
174
+ movementCount: 0,
175
+ };
176
+
177
+ // 事件监听器引用
178
+ this.eventListeners = new Map();
179
+
180
+ // 注入CSS样式
181
+ this.injectStyles();
182
+
183
+ // 创建优化的DOM元素
184
+ this.createOptimizedElements();
185
+
186
+ // 绑定事件
187
+ this.bindEvents();
188
+
189
+ // 预加载图片
190
+ this.preloadImages();
191
+
192
+ // 加载第一张图片
193
+ this.loadCurrentImage();
194
+
195
+ // 显示预览器
196
+ this.show();
197
+ }
198
+
199
+ // 注入CSS样式
200
+ injectStyles() {
201
+ const style = document.createElement('style');
202
+ style.id = 'image-viewer-styles';
203
+ style.textContent = `
204
+ :root {
205
+ /* 背景相关变量 */
206
+ --viewer-bg-color: ${this.options.theme.viewerBgColor};
207
+
208
+ /* 工具栏相关变量 */
209
+ --toolbar-bg-color: ${this.options.theme.toolbarBgColor};
210
+ --toolbar-border-radius: ${this.options.theme.toolbarBorderRadius};
211
+ --toolbar-padding: ${this.options.theme.toolbarPadding};
212
+ --toolbar-bottom: ${this.options.theme.toolbarBottom};
213
+
214
+ /* 按钮相关变量 */
215
+ --button-bg-color: ${this.options.theme.buttonBgColor};
216
+ --button-hover-bg: ${this.options.theme.buttonHoverBg};
217
+ --button-size: ${this.options.theme.buttonSize};
218
+ --button-font-size: ${this.options.theme.buttonFontSize};
219
+ --button-border-radius: ${this.options.theme.buttonBorderRadius};
220
+
221
+ /* 右上角关闭按钮变量 */
222
+ --top-close-btn-size: ${this.options.theme.topCloseBtnSize};
223
+ --top-close-btn-top: ${this.options.theme.topCloseBtnTop};
224
+ --top-close-btn-right: ${this.options.theme.topCloseBtnRight};
225
+
226
+ /* 信息栏相关变量 */
227
+ --info-bg-color: ${this.options.theme.infoBgColor};
228
+ --info-border-radius: ${this.options.theme.infoBorderRadius};
229
+ --info-padding: ${this.options.theme.infoPadding};
230
+ --info-font-size: ${this.options.theme.infoFontSize};
231
+ --info-top: ${this.options.theme.infoTop};
232
+ --info-left: ${this.options.theme.infoLeft};
233
+
234
+ /* 缩放指示器变量 */
235
+ --zoom-indicator-bg: ${this.options.theme.zoomIndicatorBg};
236
+ --zoom-indicator-border-radius: ${this.options.theme.zoomIndicatorBorderRadius};
237
+ --zoom-indicator-padding: ${this.options.theme.zoomIndicatorPadding};
238
+ --zoom-indicator-font-size: ${this.options.theme.zoomIndicatorFontSize};
239
+ --zoom-indicator-top: ${this.options.theme.zoomIndicatorTop};
240
+ --zoom-indicator-left: ${this.options.theme.zoomIndicatorLeft};
241
+
242
+ /* 通用变量 */
243
+ --active-color: ${this.options.theme.activeColor};
244
+ --text-color: ${this.options.theme.textColor};
245
+ --shadow-color: ${this.options.theme.shadowColor};
246
+ --transition-speed: ${this.options.theme.transitionSpeed};
247
+ }
248
+
249
+ .image-viewer-container {
250
+ position: fixed;
251
+ top: 0;
252
+ left: 0;
253
+ width: 100vw;
254
+ height: 100vh;
255
+ z-index: 9999;
256
+ opacity: 0;
257
+ transition: opacity var(--transition-speed) ease;
258
+ touch-action: none;
259
+ -webkit-user-select: none;
260
+ user-select: none;
261
+ display: none;
262
+ background-color: var(--viewer-bg-color);
263
+ }
264
+
265
+ /* 修复图片容器样式 - 确保居中 */
266
+ .image-viewer-image-container {
267
+ position: absolute;
268
+ top: 0;
269
+ left: 0;
270
+ width: 100%;
271
+ height: 100%;
272
+ z-index: 2;
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ overflow: hidden;
277
+ }
278
+
279
+ /* 修复图片样式 - 确保居中 */
280
+ .image-viewer-image {
281
+ position: relative;
282
+ object-fit: contain;
283
+ cursor: grab;
284
+ transition: transform 0.1s ease-out, opacity var(--transition-speed) ease;
285
+ transform-origin: center center;
286
+ opacity: 0;
287
+ box-shadow: 0 8px 25px var(--shadow-color);
288
+ border-radius: 4px;
289
+ user-select: none;
290
+ touch-action: none;
291
+ }
292
+
293
+ .image-viewer-image.loaded {
294
+ opacity: 1;
295
+ }
296
+
297
+ .image-viewer-image.dragging {
298
+ cursor: grabbing;
299
+ transition: none;
300
+ }
301
+
302
+ /* 加载指示器 */
303
+ .image-viewer-loading {
304
+ position: absolute;
305
+ top: 50%;
306
+ left: 50%;
307
+ transform: translate(-50%, -50%);
308
+ background-color: rgba(127, 127, 127, 0.7);
309
+ padding: 20px 30px;
310
+ border-radius: 10px;
311
+ display: flex;
312
+ align-items: center;
313
+ gap: 15px;
314
+ color: var(--text-color);
315
+ font-size: 18px;
316
+ opacity: 0;
317
+ pointer-events: none;
318
+ transition: opacity var(--transition-speed) ease;
319
+ z-index: 3;
320
+ }
321
+
322
+ .image-viewer-loading.active {
323
+ opacity: 1;
324
+ }
325
+
326
+ .image-viewer-loading-spinner {
327
+ width: 40px;
328
+ height: 40px;
329
+ border: 4px solid rgba(255, 255, 255, 0.2);
330
+ border-top-color: var(--active-color);
331
+ border-radius: 50%;
332
+ animation: imageViewerSpin 1s linear infinite;
333
+ }
334
+
335
+ @keyframes imageViewerSpin {
336
+ to {
337
+ transform: rotate(360deg);
338
+ }
339
+ }
340
+
341
+ /* 右上角关闭按钮样式 */
342
+ .image-viewer-top-close-btn {
343
+ position: absolute;
344
+ top: var(--top-close-btn-top);
345
+ right: var(--top-close-btn-right);
346
+ width: var(--top-close-btn-size);
347
+ height: var(--top-close-btn-size);
348
+ border-radius: 50%;
349
+ background-color: var(--button-bg-color);
350
+ color: var(--text-color);
351
+ border: none;
352
+ font-size: 20px;
353
+ cursor: pointer;
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: center;
357
+ transition: all var(--transition-speed);
358
+ z-index: 10;
359
+ backdrop-filter: blur(4px);
360
+ box-shadow: 0 2px 8px var(--shadow-color);
361
+ }
362
+
363
+ .image-viewer-top-close-btn:hover {
364
+ background-color: rgba(255, 50, 50, 0.3);
365
+ transform: scale(1.1);
366
+ }
367
+
368
+ /* 缩放指示器样式 */
369
+ .image-viewer-zoom-indicator {
370
+ position: absolute;
371
+ top: var(--zoom-indicator-top);
372
+ left: var(--zoom-indicator-left);
373
+ color: var(--text-color);
374
+ background-color: var(--zoom-indicator-bg);
375
+ padding: var(--zoom-indicator-padding);
376
+ border-radius: var(--zoom-indicator-border-radius);
377
+ font-size: var(--zoom-indicator-font-size);
378
+ z-index: 10;
379
+ min-width: 60px;
380
+ text-align: center;
381
+ backdrop-filter: blur(4px);
382
+ box-shadow: 0 2px 8px var(--shadow-color);
383
+ border: 1px solid rgba(255, 255, 255, 0.1);
384
+ }
385
+
386
+ /* 信息栏样式 */
387
+ .image-viewer-image-info {
388
+ position: absolute;
389
+ top: var(--info-top);
390
+ left: var(--info-left);
391
+ color: var(--text-color);
392
+ background-color: var(--info-bg-color);
393
+ padding: var(--info-padding);
394
+ border-radius: var(--info-border-radius);
395
+ font-size: var(--info-font-size);
396
+ z-index: 10;
397
+ max-width: calc(100% - 40px);
398
+ backdrop-filter: blur(4px);
399
+ box-shadow: 0 4px 12px var(--shadow-color);
400
+ display: none;
401
+ border: 1px solid rgba(255, 255, 255, 0.1);
402
+ }
403
+
404
+ .image-viewer-image-info.visible {
405
+ display: block;
406
+ animation: imageViewerFadeIn 0.3s ease;
407
+ }
408
+
409
+ .image-viewer-image-info p {
410
+ margin: 4px 0;
411
+ line-height: 1.4;
412
+ }
413
+
414
+ .image-viewer-image-info .info-label {
415
+ color: #ddd;
416
+ margin-right: 5px;
417
+ }
418
+
419
+ .image-viewer-shortcuts-title {
420
+ margin-top: 10px;
421
+ padding-top: 10px;
422
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
423
+ font-weight: bold;
424
+ margin-bottom: 5px;
425
+ }
426
+
427
+ @keyframes imageViewerFadeIn {
428
+ from {
429
+ opacity: 0;
430
+ transform: translateY(-10px);
431
+ }
432
+ to {
433
+ opacity: 1;
434
+ transform: translateY(0);
435
+ }
436
+ }
437
+
438
+ /* 图片计数器 */
439
+ .image-viewer-image-counter {
440
+ position: absolute;
441
+ top: 20px;
442
+ left: 50%;
443
+ transform: translateX(-50%);
444
+ color: var(--text-color);
445
+ background-color: var(--info-bg-color);
446
+ padding: 6px 12px;
447
+ border-radius: 18px;
448
+ font-size: 14px;
449
+ z-index: 10;
450
+ backdrop-filter: blur(4px);
451
+ box-shadow: 0 2px 8px var(--shadow-color);
452
+ border: 1px solid rgba(255, 255, 255, 0.1);
453
+ }
454
+
455
+ /* 导航按钮 */
456
+ .image-viewer-nav-buttons {
457
+ position: absolute;
458
+ top: 50%;
459
+ left: 0;
460
+ right: 0;
461
+ transform: translateY(-50%);
462
+ display: flex;
463
+ justify-content: space-between;
464
+ pointer-events: none;
465
+ z-index: 5;
466
+ padding: 0 10px;
467
+ }
468
+
469
+ .image-viewer-nav-btn {
470
+ width: var(--button-size);
471
+ height: var(--button-size);
472
+ border-radius: 50%;
473
+ background-color: var(--button-bg-color);
474
+ color: var(--text-color);
475
+ border: none;
476
+ font-size: 24px;
477
+ cursor: pointer;
478
+ display: flex;
479
+ align-items: center;
480
+ justify-content: center;
481
+ transition: all 0.2s;
482
+ pointer-events: auto;
483
+ opacity: 0.9;
484
+ backdrop-filter: blur(4px);
485
+ box-shadow: 0 2px 8px var(--shadow-color);
486
+ z-index: 6;
487
+ }
488
+
489
+ .image-viewer-nav-btn:hover {
490
+ background-color: var(--button-hover-bg);
491
+ opacity: 1;
492
+ transform: scale(1.1);
493
+ }
494
+
495
+ .image-viewer-nav-btn:disabled {
496
+ opacity: 0.3;
497
+ cursor: not-allowed;
498
+ transform: none;
499
+ }
500
+
501
+ /* 工具栏样式 */
502
+ .image-viewer-toolbar {
503
+ position: absolute;
504
+ bottom: var(--toolbar-bottom);
505
+ left: 50%;
506
+ transform: translateX(-50%);
507
+ background-color: var(--toolbar-bg-color);
508
+ backdrop-filter: blur(12px);
509
+ padding: var(--toolbar-padding);
510
+ border-radius: var(--toolbar-border-radius);
511
+ display: flex;
512
+ gap: 2px;
513
+ z-index: 10;
514
+ box-shadow: 0 6px 25px var(--shadow-color);
515
+ max-width: calc(100% - 40px);
516
+ overflow-x: auto;
517
+ overflow-y: hidden;
518
+ border: 1px solid rgba(255, 255, 255, 0.1);
519
+ scrollbar-width: none;
520
+ -ms-overflow-style: none;
521
+ -webkit-overflow-scrolling: touch;
522
+ }
523
+
524
+ .image-viewer-toolbar::-webkit-scrollbar {
525
+ display: none;
526
+ }
527
+
528
+ .image-viewer-tool-btn {
529
+ width: var(--button-size);
530
+ height: var(--button-size);
531
+ background-color: transparent;
532
+ border: none;
533
+ color: var(--text-color);
534
+ cursor: pointer;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ font-size: var(--button-font-size);
539
+ transition: all 0.2s;
540
+ flex-shrink: 0;
541
+ position: relative;
542
+ border-radius: var(--button-border-radius);
543
+ margin: 0 2px;
544
+ z-index: 11;
545
+ }
546
+
547
+ .image-viewer-tool-btn:hover {
548
+ background-color: var(--button-hover-bg);
549
+ transform: translateY(-2px);
550
+ box-shadow: 0 4px 10px var(--shadow-color);
551
+ }
552
+
553
+ .image-viewer-tool-btn:active {
554
+ background-color: rgba(255, 255, 255, 0.3);
555
+ transform: translateY(0);
556
+ }
557
+
558
+ .image-viewer-tool-btn:disabled {
559
+ opacity: 0.5;
560
+ cursor: not-allowed;
561
+ }
562
+
563
+ /* 缩略图容器 */
564
+ .image-viewer-thumbnails-container {
565
+ position: absolute;
566
+ bottom: 90px;
567
+ left: 50%;
568
+ transform: translateX(-50%);
569
+ padding: 10px 15px;
570
+ background-color: var(--toolbar-bg-color);
571
+ backdrop-filter: blur(8px);
572
+ border-radius: 12px;
573
+ display: flex;
574
+ gap: 10px;
575
+ overflow-x: auto;
576
+ scrollbar-width: none;
577
+ -ms-overflow-style: none;
578
+ z-index: 10;
579
+ box-shadow: 0 3px 15px var(--shadow-color);
580
+ max-width: calc(100% - 40px);
581
+ -webkit-overflow-scrolling: touch;
582
+ border: 1px solid rgba(255, 255, 255, 0.1);
583
+ }
584
+
585
+ .image-viewer-thumbnails-container::-webkit-scrollbar {
586
+ display: none;
587
+ }
588
+
589
+ .image-viewer-thumbnail-item {
590
+ width: 70px;
591
+ height: 45px;
592
+ border: 2px solid transparent;
593
+ border-radius: 6px;
594
+ overflow: hidden;
595
+ cursor: pointer;
596
+ flex-shrink: 0;
597
+ transition: all 0.2s;
598
+ z-index: 11;
599
+ }
600
+
601
+ .image-viewer-thumbnail-item img {
602
+ width: 100%;
603
+ height: 100%;
604
+ object-fit: cover;
605
+ }
606
+
607
+ .image-viewer-thumbnail-item.active {
608
+ border-color: var(--active-color);
609
+ transform: scale(1.05);
610
+ }
611
+
612
+ .image-viewer-thumbnail-item:hover {
613
+ transform: scale(1.05);
614
+ }
615
+
616
+ @media (max-width: 768px) {
617
+ .image-viewer-tool-btn {
618
+ width: 44px;
619
+ height: 44px;
620
+ font-size: 18px;
621
+ }
622
+
623
+ .image-viewer-toolbar {
624
+ padding: 6px 8px;
625
+ bottom: 15px;
626
+ border-radius: 25px;
627
+ max-width: 95%;
628
+ }
629
+
630
+ .image-viewer-thumbnails-container {
631
+ bottom: 80px;
632
+ padding: 8px 10px;
633
+ gap: 8px;
634
+ max-width: 95%;
635
+ }
636
+
637
+ .image-viewer-thumbnail-item {
638
+ width: 60px;
639
+ height: 40px;
640
+ }
641
+
642
+ .image-viewer-nav-btn {
643
+ width: 44px;
644
+ height: 44px;
645
+ font-size: 20px;
646
+ }
647
+
648
+ .image-viewer-top-close-btn {
649
+ width: 40px;
650
+ height: 40px;
651
+ top: 15px;
652
+ right: 15px;
653
+ }
654
+
655
+ .image-viewer-image-info {
656
+ font-size: 12px;
657
+ padding: 8px 12px;
658
+ }
659
+
660
+ .image-viewer-zoom-indicator,
661
+ .image-viewer-image-counter {
662
+ font-size: 12px;
663
+ }
664
+ }
665
+
666
+ @media (max-width: 480px) {
667
+ .image-viewer-thumbnails-container {
668
+ max-width: 95%;
669
+ padding: 6px 8px;
670
+ }
671
+
672
+ .image-viewer-thumbnail-item {
673
+ width: 50px;
674
+ height: 35px;
675
+ }
676
+
677
+ .image-viewer-toolbar {
678
+ max-width: 95%;
679
+ }
680
+ }
681
+ `;
682
+ document.head.appendChild(style);
683
+ }
684
+
685
+ // 创建优化的DOM结构
686
+ createOptimizedElements() {
687
+ // 主容器
688
+ this.container = document.createElement('div');
689
+ this.container.className = 'image-viewer-container';
690
+ document.body.appendChild(this.container);
691
+
692
+ // 图片容器 - 使用flex确保居中
693
+ this.imageContainer = document.createElement('div');
694
+ this.imageContainer.className = 'image-viewer-image-container';
695
+ this.container.appendChild(this.imageContainer);
696
+
697
+ // 图片元素
698
+ this.image = document.createElement('img');
699
+ this.image.className = 'image-viewer-image';
700
+ this.image.alt = '预览图片';
701
+ this.image.crossOrigin = 'anonymous';
702
+ this.imageContainer.appendChild(this.image);
703
+
704
+ // 加载指示器
705
+ this.loading = document.createElement('div');
706
+ this.loading.className = 'image-viewer-loading';
707
+ this.loading.innerHTML = `
708
+ <div class="image-viewer-loading-spinner"></div>
709
+ <div>加载中...</div>
710
+ `;
711
+ this.imageContainer.appendChild(this.loading);
712
+
713
+ // 右上角关闭按钮
714
+ if (this.options.buttons.topClose) {
715
+ this.topCloseBtn = document.createElement('button');
716
+ this.topCloseBtn.className = 'image-viewer-top-close-btn';
717
+ this.topCloseBtn.textContent = '×';
718
+ this.topCloseBtn.title = '关闭预览 (ESC)';
719
+ this.container.appendChild(this.topCloseBtn);
720
+ }
721
+
722
+ // 缩放比例显示元素
723
+ this.zoomIndicator = document.createElement('div');
724
+ this.zoomIndicator.className = 'image-viewer-zoom-indicator';
725
+ this.container.appendChild(this.zoomIndicator);
726
+
727
+ // 图片信息面板
728
+ if (this.options.buttons.info) {
729
+ this.imageInfoPanel = document.createElement('div');
730
+ this.imageInfoPanel.className = `image-viewer-image-info ${this.imageInfoVisible ? 'visible' : ''}`;
731
+ this.container.appendChild(this.imageInfoPanel);
732
+ }
733
+
734
+ // 图片计数器
735
+ if (this.images.length > 1) {
736
+ this.counter = document.createElement('div');
737
+ this.counter.className = 'image-viewer-image-counter';
738
+ this.container.appendChild(this.counter);
739
+ }
740
+
741
+ // 左右导航按钮
742
+ if (this.images.length > 1 && (this.options.buttons.prev || this.options.buttons.next)) {
743
+ this.createNavButtons();
744
+ }
745
+
746
+ // 底部工具栏
747
+ this.createToolbar();
748
+
749
+ // 缩略图导航
750
+ if (this.images.length > 1 && this.options.buttons.thumbnails) {
751
+ this.createThumbnails();
752
+ }
753
+ }
754
+
755
+ // 创建导航按钮
756
+ createNavButtons() {
757
+ const navContainer = document.createElement('div');
758
+ navContainer.className = 'image-viewer-nav-buttons';
759
+
760
+ navContainer.addEventListener('click', e => {
761
+ e.stopPropagation();
762
+ });
763
+
764
+ if (this.options.buttons.prev) {
765
+ this.prevBtn = document.createElement('button');
766
+ this.prevBtn.className = 'image-viewer-nav-btn image-viewer-prev-btn';
767
+ this.prevBtn.textContent = '←';
768
+ this.prevBtn.title = '上一张 (←)';
769
+ this.prevBtn.addEventListener('click', e => {
770
+ e.stopPropagation();
771
+ this.prevImage();
772
+ });
773
+ navContainer.appendChild(this.prevBtn);
774
+ }
775
+
776
+ if (this.options.buttons.next) {
777
+ this.nextBtn = document.createElement('button');
778
+ this.nextBtn.className = 'image-viewer-nav-btn image-viewer-next-btn';
779
+ this.nextBtn.textContent = '→';
780
+ this.nextBtn.title = '下一张 (→)';
781
+ this.nextBtn.addEventListener('click', e => {
782
+ e.stopPropagation();
783
+ this.nextImage();
784
+ });
785
+ navContainer.appendChild(this.nextBtn);
786
+ }
787
+
788
+ this.container.appendChild(navContainer);
789
+ }
790
+
791
+ // 创建工具栏
792
+ createToolbar() {
793
+ const toolbar = document.createElement('div');
794
+ toolbar.className = 'image-viewer-toolbar';
795
+
796
+ toolbar.addEventListener('click', e => {
797
+ e.stopPropagation();
798
+ });
799
+
800
+ // 导航按钮
801
+ if (this.images.length > 1) {
802
+ if (this.options.buttons.prev) {
803
+ this.toolbarPrevBtn = this.createToolButton('←', () => this.prevImage());
804
+ toolbar.appendChild(this.toolbarPrevBtn);
805
+ }
806
+
807
+ if (this.options.buttons.next) {
808
+ this.toolbarNextBtn = this.createToolButton('→', () => this.nextImage());
809
+ toolbar.appendChild(this.toolbarNextBtn);
810
+ }
811
+
812
+ const separator = document.createElement('div');
813
+ separator.style.width = '10px';
814
+ separator.style.flexShrink = '0';
815
+ toolbar.appendChild(separator);
816
+ }
817
+
818
+ // 缩放按钮
819
+ if (this.options.buttons.zoomOut) {
820
+ this.zoomOutBtn = this.createToolButton('−', () => this.zoom(-0.1));
821
+ toolbar.appendChild(this.zoomOutBtn);
822
+ }
823
+
824
+ if (this.options.buttons.zoomIn) {
825
+ this.zoomInBtn = this.createToolButton('+', () => this.zoom(0.1));
826
+ toolbar.appendChild(this.zoomInBtn);
827
+ }
828
+
829
+ // 旋转按钮
830
+ if (this.options.buttons.rotateLeft) {
831
+ this.rotateLeftBtn = this.createToolButton('↺', () => this.rotate(-90));
832
+ toolbar.appendChild(this.rotateLeftBtn);
833
+ }
834
+
835
+ if (this.options.buttons.rotateRight) {
836
+ this.rotateRightBtn = this.createToolButton('↻', () => this.rotate(90));
837
+ toolbar.appendChild(this.rotateRightBtn);
838
+ }
839
+
840
+ // 其他功能按钮
841
+ if (this.options.buttons.reset) {
842
+ this.resetBtn = this.createToolButton('⟳', () => this.resetTransform());
843
+ toolbar.appendChild(this.resetBtn);
844
+ }
845
+
846
+ if (this.options.buttons.originalSize) {
847
+ this.originalSizeBtn = this.createToolButton('1:1', () => this.showOriginalSize());
848
+ toolbar.appendChild(this.originalSizeBtn);
849
+ }
850
+
851
+ if (this.options.buttons.info) {
852
+ this.infoBtn = this.createToolButton('ⓘ', () => this.toggleImageInfo());
853
+ toolbar.appendChild(this.infoBtn);
854
+ }
855
+
856
+ if (this.options.buttons.download) {
857
+ this.downloadBtn = this.createToolButton('↓', () => this.downloadImage());
858
+ toolbar.appendChild(this.downloadBtn);
859
+ }
860
+
861
+ if (this.options.buttons.fullscreen) {
862
+ this.fullscreenBtn = this.createToolButton('⛶', () => this.toggleFullscreen());
863
+ toolbar.appendChild(this.fullscreenBtn);
864
+ }
865
+
866
+ if (this.options.buttons.close) {
867
+ this.closeBtn = this.createToolButton('×', () => this.close());
868
+ toolbar.appendChild(this.closeBtn);
869
+ }
870
+
871
+ this.container.appendChild(toolbar);
872
+ }
873
+
874
+ // 创建工具按钮
875
+ createToolButton(icon, onClick) {
876
+ const button = document.createElement('button');
877
+ button.className = 'image-viewer-tool-btn';
878
+
879
+ const iconSpan = document.createElement('span');
880
+ iconSpan.textContent = icon;
881
+
882
+ button.appendChild(iconSpan);
883
+
884
+ button.addEventListener('click', e => {
885
+ e.stopPropagation();
886
+ onClick();
887
+ });
888
+
889
+ return button;
890
+ }
891
+
892
+ // 创建缩略图
893
+ createThumbnails() {
894
+ const thumbContainer = document.createElement('div');
895
+ thumbContainer.className = 'image-viewer-thumbnails-container';
896
+
897
+ thumbContainer.addEventListener('click', e => {
898
+ e.stopPropagation();
899
+ });
900
+
901
+ this.images.forEach((url, index) => {
902
+ const thumbItem = document.createElement('div');
903
+ thumbItem.className = `image-viewer-thumbnail-item ${index === 0 ? 'active' : ''}`;
904
+ thumbItem.dataset.index = index;
905
+
906
+ const thumbImg = document.createElement('img');
907
+ thumbImg.src = url;
908
+ thumbImg.alt = `缩略图 ${index + 1}`;
909
+ thumbImg.crossOrigin = 'anonymous';
910
+
911
+ thumbImg.onerror = () => {
912
+ thumbImg.src =
913
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyIDJDMTEuMzcgMiAxMC43NyAyLjAzIDEwLjIzIDIuMDljLS41MSAwLS45Ni40NS0uOTYuOTZzLjQ1Ljk2Ljk2Ljk2Ljk2LS40NS45Ni0uOTYuNDUtLjk2Ljk2LS45NiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0yMCAxMkg0Yy0uNTUgMC0xIC40NS0xIDEgMCAuMTguMDIuMzUuMDcuNTFMMiAxOWMwLjU1IDAgMS0uNDUgMS0xVjVjMC0uNTUuNDUtMSAxLTEgMiAwIDMuOTggLjkgNS41IDIgMi42MyAxLjMgNC4xNyAzLjEgNS41IDMgMiAwIDUgMiA1IDV2MmMwIC41NS40NSAxIDEgMWgxNmMuNTUgMCAxLS40NSAxLTF2LTEyYzAtLjU1LS40NS0xLTEtMXptLTggMTRjLTQuNDIgMC04LTMuNTgtOC04czMuNTgtOCA4LTggOCAzLjU4IDggOFMzMC40MiAxNiAyNCAxNnoiLz48L3N2Zz4=';
914
+ };
915
+
916
+ thumbItem.appendChild(thumbImg);
917
+
918
+ thumbItem.addEventListener('click', e => {
919
+ e.stopPropagation();
920
+ const index = parseInt(thumbItem.dataset.index);
921
+ if (index !== this.currentIndex) {
922
+ this.currentIndex = index;
923
+ this.loadCurrentImage();
924
+ this.updateThumbnails();
925
+ }
926
+ });
927
+
928
+ thumbContainer.appendChild(thumbItem);
929
+ });
930
+
931
+ this.container.appendChild(thumbContainer);
932
+ }
933
+
934
+ // 更新图片变换 - 修复居中问题
935
+ updateImageTransform() {
936
+ // 使用绝对定位和transform来确保居中
937
+ const transform = `
938
+ translate(${this.translateX}px, ${this.translateY}px)
939
+ scale(${this.scale})
940
+ rotate(${this.rotation}deg)
941
+ `;
942
+
943
+ this.image.style.transform = transform;
944
+ }
945
+
946
+ // 调整图片大小以适应屏幕 - 修复居中问题
947
+ fitImageToScreen(imageWidth, imageHeight) {
948
+ this.scale = 1;
949
+ this.translateX = 0;
950
+ this.translateY = 0;
951
+
952
+ const containerWidth = this.imageContainer.clientWidth;
953
+ const containerHeight = this.imageContainer.clientHeight;
954
+
955
+ const angle = this.rotation % 360;
956
+ let effectiveWidth = imageWidth;
957
+ let effectiveHeight = imageHeight;
958
+
959
+ if (angle === 90 || angle === 270) {
960
+ effectiveWidth = imageHeight;
961
+ effectiveHeight = imageWidth;
962
+ }
963
+
964
+ if (effectiveWidth > containerWidth || effectiveHeight > containerHeight) {
965
+ const widthRatio = containerWidth / effectiveWidth;
966
+ const heightRatio = containerHeight / effectiveHeight;
967
+ this.scale = Math.min(widthRatio, heightRatio);
968
+ }
969
+
970
+ this.scale = Math.max(0.1, this.scale);
971
+
972
+ this.updateImageTransform();
973
+ this.updateZoomIndicator();
974
+ }
975
+
976
+ parseImageOptions(options) {
977
+ this.images = [];
978
+
979
+ if (typeof options === 'string') {
980
+ this.images = [options];
981
+ } else if (Array.isArray(options)) {
982
+ this.images = options.filter(url => typeof url === 'string' && url.trim() !== '');
983
+ } else if (typeof options === 'object') {
984
+ if (options.images && Array.isArray(options.images)) {
985
+ this.images = options.images.filter(url => typeof url === 'string' && url.trim() !== '');
986
+ }
987
+ }
988
+ }
989
+
990
+ preloadImages() {
991
+ this.images.forEach((url, index) => {
992
+ if (!this.loadedImages.has(url)) {
993
+ const img = new Image();
994
+ img.crossOrigin = 'anonymous';
995
+ img.src = url;
996
+ img.onload = () => {
997
+ this.loadedImages.set(url, img);
998
+ this.imageMetadata[index] = {
999
+ name: this.extractFileName(url),
1000
+ width: img.width,
1001
+ height: img.height,
1002
+ };
1003
+ };
1004
+ img.onerror = () => {
1005
+ console.error(`图片预加载失败: ${url}`);
1006
+ };
1007
+ }
1008
+ });
1009
+ }
1010
+
1011
+ extractFileName(url) {
1012
+ try {
1013
+ const urlObj = new URL(url);
1014
+ const pathParts = urlObj.pathname.split('/');
1015
+ let fileName = pathParts[pathParts.length - 1];
1016
+
1017
+ const queryIndex = fileName.indexOf('?');
1018
+ if (queryIndex > -1) {
1019
+ fileName = fileName.substring(0, queryIndex);
1020
+ }
1021
+
1022
+ return fileName || 'unknown-image';
1023
+ } catch (e) {
1024
+ return 'unknown-image';
1025
+ }
1026
+ }
1027
+
1028
+ loadCurrentImage() {
1029
+ const currentUrl = this.images[this.currentIndex];
1030
+ const isLoaded = this.loadedImages.has(currentUrl);
1031
+
1032
+ // 重置双击状态
1033
+ this.hasPreviousState = false;
1034
+ this.isToggledState = false;
1035
+
1036
+ // 更新计数器
1037
+ if (this.images.length > 1 && this.counter) {
1038
+ this.counter.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
1039
+ }
1040
+
1041
+ // 更新导航按钮状态
1042
+ this.updateNavButtons();
1043
+ // 更新缩略图状态
1044
+ this.updateThumbnails();
1045
+
1046
+ // 重置变换状态
1047
+ this.scale = 1.0;
1048
+ this.rotation = 0;
1049
+ this.translateX = 0;
1050
+ this.translateY = 0;
1051
+
1052
+ // 如果图片已加载,直接显示
1053
+ if (isLoaded) {
1054
+ // this.showLoading();
1055
+
1056
+ const cachedImg = this.loadedImages.get(currentUrl);
1057
+ const tempImg = new Image();
1058
+ tempImg.crossOrigin = 'anonymous';
1059
+ tempImg.src = cachedImg.src;
1060
+
1061
+ tempImg.onload = () => {
1062
+ this.image.src = tempImg.src;
1063
+ this.image.classList.add('loaded');
1064
+
1065
+ const metadata = this.imageMetadata[this.currentIndex];
1066
+ if (metadata) {
1067
+ this.fitImageToScreen(metadata.width, metadata.height);
1068
+ this.updateImageInfo();
1069
+ }
1070
+
1071
+ setTimeout(() => {
1072
+ this.hideLoading();
1073
+ }, 300);
1074
+ };
1075
+
1076
+ return;
1077
+ }
1078
+
1079
+ // 新图片加载流程
1080
+ this.showLoading();
1081
+ this.image.classList.remove('loaded');
1082
+
1083
+ const tempImg = new Image();
1084
+ tempImg.crossOrigin = 'anonymous';
1085
+ tempImg.src = currentUrl;
1086
+
1087
+ tempImg.onload = () => {
1088
+ this.loadedImages.set(currentUrl, tempImg);
1089
+ this.imageMetadata[this.currentIndex] = {
1090
+ name: this.extractFileName(currentUrl),
1091
+ width: tempImg.width,
1092
+ height: tempImg.height,
1093
+ };
1094
+
1095
+ this.image.src = tempImg.src;
1096
+ this.image.classList.add('loaded');
1097
+ this.fitImageToScreen(tempImg.width, tempImg.height);
1098
+ this.updateImageInfo();
1099
+
1100
+ setTimeout(() => {
1101
+ this.hideLoading();
1102
+ }, 300);
1103
+ };
1104
+
1105
+ tempImg.onerror = () => {
1106
+ this.hideLoading();
1107
+ alert('图片加载失败');
1108
+ };
1109
+ }
1110
+
1111
+ updateZoomIndicator() {
1112
+ const percentage = Math.round(this.scale * 100);
1113
+ this.zoomIndicator.textContent = `${percentage}%`;
1114
+ }
1115
+
1116
+ updateImageInfo() {
1117
+ if (!this.options.buttons.info || !this.imageInfoPanel) return;
1118
+
1119
+ const metadata = this.imageMetadata[this.currentIndex];
1120
+ if (!metadata) return;
1121
+
1122
+ let infoHtml = '';
1123
+
1124
+ if (this.options.imageInfo.showName) {
1125
+ infoHtml += `<p><span class="info-label">名称:</span> ${metadata.name}</p>`;
1126
+ }
1127
+
1128
+ if (this.options.imageInfo.showDimensions) {
1129
+ infoHtml += `<p><span class="info-label">尺寸:</span> ${metadata.width} × ${metadata.height} px</p>`;
1130
+ }
1131
+
1132
+ infoHtml += `
1133
+ <div class="image-viewer-shortcuts-title">快捷键</div>
1134
+ <p><span class="info-label">放大:</span> + / =</p>
1135
+ <p><span class="info-label">缩小:</span> -</p>
1136
+ <p><span class="info-label">上一张:</span> ←</p>
1137
+ <p><span class="info-label">下一张:</span> →</p>
1138
+ <p><span class="info-label">重置:</span> 0</p>
1139
+ <p><span class="info-label">全屏:</span> F</p>
1140
+ <p><span class="info-label">信息:</span> I</p>
1141
+ <p><span class="info-label">关闭:</span> ESC</p>
1142
+ `;
1143
+
1144
+ this.imageInfoPanel.innerHTML = infoHtml;
1145
+ }
1146
+
1147
+ toggleImageInfo() {
1148
+ if (!this.options.buttons.info || !this.imageInfoPanel) return;
1149
+
1150
+ this.imageInfoVisible = !this.imageInfoVisible;
1151
+ if (this.imageInfoVisible) {
1152
+ this.imageInfoPanel.classList.add('visible');
1153
+ } else {
1154
+ this.imageInfoPanel.classList.remove('visible');
1155
+ }
1156
+ }
1157
+
1158
+ updateNavButtons() {
1159
+ if (this.images.length <= 1) return;
1160
+
1161
+ const canGoPrev = this.options.loop ? true : this.currentIndex > 0;
1162
+ const canGoNext = this.options.loop ? true : this.currentIndex < this.images.length - 1;
1163
+
1164
+ if (this.prevBtn) this.prevBtn.disabled = !canGoPrev;
1165
+ if (this.nextBtn) this.nextBtn.disabled = !canGoNext;
1166
+ if (this.toolbarPrevBtn) this.toolbarPrevBtn.disabled = !canGoPrev;
1167
+ if (this.toolbarNextBtn) this.toolbarNextBtn.disabled = !canGoNext;
1168
+ }
1169
+
1170
+ updateThumbnails() {
1171
+ if (this.images.length <= 1) return;
1172
+
1173
+ document.querySelectorAll('.image-viewer-thumbnail-item').forEach(item => {
1174
+ item.classList.remove('active');
1175
+ });
1176
+
1177
+ const activeItem = document.querySelector(`.image-viewer-thumbnail-item[data-index="${this.currentIndex}"]`);
1178
+ if (activeItem) {
1179
+ activeItem.classList.add('active');
1180
+ activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
1181
+ }
1182
+ }
1183
+
1184
+ showLoading() {
1185
+ if (this.loading) {
1186
+ this.loading.classList.add('active');
1187
+ }
1188
+ }
1189
+
1190
+ hideLoading() {
1191
+ if (this.loading) {
1192
+ this.loading.classList.remove('active');
1193
+ }
1194
+ }
1195
+
1196
+ prevImage() {
1197
+ if (this.images.length <= 1) return;
1198
+
1199
+ let newIndex = this.currentIndex - 1;
1200
+ if (newIndex < 0) {
1201
+ newIndex = this.options.loop ? this.images.length - 1 : 0;
1202
+ }
1203
+
1204
+ if (newIndex !== this.currentIndex) {
1205
+ this.currentIndex = newIndex;
1206
+ this.loadCurrentImage();
1207
+ }
1208
+ }
1209
+
1210
+ nextImage() {
1211
+ if (this.images.length <= 1) return;
1212
+
1213
+ let newIndex = this.currentIndex + 1;
1214
+ if (newIndex >= this.images.length) {
1215
+ newIndex = this.options.loop ? 0 : this.images.length - 1;
1216
+ }
1217
+
1218
+ if (newIndex !== this.currentIndex) {
1219
+ this.currentIndex = newIndex;
1220
+ this.loadCurrentImage();
1221
+ }
1222
+ }
1223
+
1224
+ bindEvents() {
1225
+ // 关闭按钮事件
1226
+ if (this.topCloseBtn) {
1227
+ this.addEvent(this.topCloseBtn, 'click', () => this.close());
1228
+ }
1229
+
1230
+ // 点击遮罩关闭
1231
+ if (this.options.closeOnMaskClick) {
1232
+ this.addEvent(this.imageContainer, 'click', e => {
1233
+ if (e.target == this.imageContainer) this.close();
1234
+ });
1235
+ }
1236
+
1237
+ // 键盘事件
1238
+ this.addEvent(document, 'keydown', e => this.handleKeydown(e));
1239
+
1240
+ // 窗口大小改变事件
1241
+ const throttledResize = throttle(() => {
1242
+ this.handleResize();
1243
+ }, 300);
1244
+ this.addEvent(window, 'resize', throttledResize);
1245
+
1246
+ // 鼠标/触摸事件 - 直接绑定到图片
1247
+ this.bindDragEvents();
1248
+ this.bindTouchEvents();
1249
+ }
1250
+
1251
+ addEvent(element, event, handler, options) {
1252
+ element.addEventListener(event, handler, options);
1253
+ const key = `${event}-${Date.now()}-${Math.random()}`;
1254
+ this.eventListeners.set(key, { element, event, handler });
1255
+ }
1256
+
1257
+ removeAllEvents() {
1258
+ this.eventListeners.forEach(({ element, event, handler }) => {
1259
+ element.removeEventListener(event, handler);
1260
+ });
1261
+ this.eventListeners.clear();
1262
+ }
1263
+
1264
+ rotatePoint(x, y, angleDegrees) {
1265
+ const angle = (angleDegrees * Math.PI) / 180;
1266
+ const cos = Math.cos(angle);
1267
+ const sin = Math.sin(angle);
1268
+ return {
1269
+ x: x * cos - y * sin,
1270
+ y: x * sin + y * cos,
1271
+ };
1272
+ }
1273
+
1274
+ bindDragEvents() {
1275
+ // 鼠标按下 - 直接绑定到图片
1276
+ this.addEvent(this.image, 'mousedown', e => {
1277
+ if (e.button !== 0) return;
1278
+
1279
+ this.isDragging = true;
1280
+ this.image.classList.add('dragging');
1281
+ this.startX = e.clientX;
1282
+ this.startY = e.clientY;
1283
+ this.startTranslateX = this.translateX;
1284
+ this.startTranslateY = this.translateY;
1285
+ e.preventDefault();
1286
+ });
1287
+
1288
+ // 鼠标移动
1289
+ this.addEvent(document, 'mousemove', e => {
1290
+ if (!this.isDragging) return;
1291
+
1292
+ const deltaX = e.clientX - this.startX;
1293
+ const deltaY = e.clientY - this.startY;
1294
+
1295
+ const rotatedDelta = this.rotatePoint(deltaX, deltaY, -this.rotation);
1296
+
1297
+ this.translateX = this.startTranslateX + rotatedDelta.x;
1298
+ this.translateY = this.startTranslateY + rotatedDelta.y;
1299
+
1300
+ this.updateImageTransform();
1301
+ e.preventDefault();
1302
+ });
1303
+
1304
+ // 鼠标释放
1305
+ this.addEvent(document, 'mouseup', () => {
1306
+ if (this.isDragging) {
1307
+ this.isDragging = false;
1308
+ this.image.classList.remove('dragging');
1309
+ }
1310
+ });
1311
+
1312
+ // 鼠标离开窗口
1313
+ this.addEvent(document, 'mouseleave', () => {
1314
+ if (this.isDragging) {
1315
+ this.isDragging = false;
1316
+ this.image.classList.remove('dragging');
1317
+ }
1318
+ });
1319
+
1320
+ // 鼠标滚轮缩放
1321
+ this.addEvent(this.imageContainer, 'wheel', e => {
1322
+ e.preventDefault();
1323
+
1324
+ const rect = this.imageContainer.getBoundingClientRect();
1325
+ const mouseX = e.clientX - rect.left;
1326
+ const mouseY = e.clientY - rect.top;
1327
+
1328
+ const delta = e.deltaY > 0 ? -0.05 : 0.05;
1329
+ this.zoomAtPoint(delta, mouseX, mouseY);
1330
+ });
1331
+
1332
+ // 双击缩放
1333
+ this.addEvent(this.image, 'dblclick', e => {
1334
+ e.preventDefault();
1335
+
1336
+ const rect = this.imageContainer.getBoundingClientRect();
1337
+ const mouseX = e.clientX - rect.left;
1338
+ const mouseY = e.clientY - rect.top;
1339
+
1340
+ if (Math.abs(this.scale - 1.0) < 0.01) {
1341
+ if (this.hasPreviousState) {
1342
+ this.scale = this.lastScale;
1343
+ this.translateX = this.lastTranslateX;
1344
+ this.translateY = this.lastTranslateY;
1345
+ this.hasPreviousState = false;
1346
+ } else {
1347
+ this.lastScale = this.scale;
1348
+ this.lastTranslateX = this.translateX;
1349
+ this.lastTranslateY = this.translateY;
1350
+ this.hasPreviousState = true;
1351
+
1352
+ const targetScale = 1.5;
1353
+ const oldScale = this.scale;
1354
+ const scaleDiff = targetScale / oldScale;
1355
+ const containerWidth = this.imageContainer.clientWidth;
1356
+ const containerHeight = this.imageContainer.clientHeight;
1357
+
1358
+ this.translateX =
1359
+ this.translateX * scaleDiff + mouseX - containerWidth / 2 - scaleDiff * (mouseX - containerWidth / 2);
1360
+ this.translateY =
1361
+ this.translateY * scaleDiff + mouseY - containerHeight / 2 - scaleDiff * (mouseY - containerHeight / 2);
1362
+
1363
+ this.scale = targetScale;
1364
+ }
1365
+ } else {
1366
+ this.lastScale = this.scale;
1367
+ this.lastTranslateX = this.translateX;
1368
+ this.lastTranslateY = this.translateY;
1369
+ this.hasPreviousState = true;
1370
+
1371
+ const targetScale = 1.0;
1372
+ const oldScale = this.scale;
1373
+ const scaleDiff = targetScale / oldScale;
1374
+ const containerWidth = this.imageContainer.clientWidth;
1375
+ const containerHeight = this.imageContainer.clientHeight;
1376
+
1377
+ this.translateX =
1378
+ this.translateX * scaleDiff + mouseX - containerWidth / 2 - scaleDiff * (mouseX - containerWidth / 2);
1379
+ this.translateY =
1380
+ this.translateY * scaleDiff + mouseY - containerHeight / 2 - scaleDiff * (mouseY - containerHeight / 2);
1381
+
1382
+ this.scale = targetScale;
1383
+ }
1384
+
1385
+ this.updateImageTransform();
1386
+ this.updateZoomIndicator();
1387
+ });
1388
+ }
1389
+
1390
+ bindTouchEvents() {
1391
+ // 触摸开始
1392
+ this.addEvent(this.image, 'touchstart', e => {
1393
+ this.touchState.lastTouchTime = Date.now();
1394
+
1395
+ if (e.touches.length === 1) {
1396
+ if (!this.touchState.isPinching) {
1397
+ this.touchState.isDragging = true;
1398
+ this.touchState.startX = e.touches[0].clientX;
1399
+ this.touchState.startY = e.touches[0].clientY;
1400
+ this.touchState.startTranslateX = this.translateX;
1401
+ this.touchState.startTranslateY = this.translateY;
1402
+ this.image.classList.add('dragging');
1403
+ }
1404
+ } else if (e.touches.length === 2) {
1405
+ this.touchState.isPinching = true;
1406
+ this.touchState.isDragging = false;
1407
+ this.image.classList.remove('dragging');
1408
+
1409
+ const touch1 = e.touches[0];
1410
+ const touch2 = e.touches[1];
1411
+
1412
+ this.touchState.initialDistance = this.getDistance(touch1, touch2);
1413
+ this.touchState.initialScale = this.scale;
1414
+ this.touchState.initialTranslateX = this.translateX;
1415
+ this.touchState.initialTranslateY = this.translateY;
1416
+
1417
+ this.touchState.centerX = (touch1.clientX + touch2.clientX) / 2;
1418
+ this.touchState.centerY = (touch1.clientY + touch2.clientY) / 2;
1419
+
1420
+ const rect = this.imageContainer.getBoundingClientRect();
1421
+ const containerX = this.touchState.centerX - rect.left;
1422
+ const containerY = this.touchState.centerY - rect.top;
1423
+
1424
+ this.calculateRelativeCenter(containerX, containerY);
1425
+
1426
+ this.touchState.movementCount = 0;
1427
+ this.touchState.scaleRatio = 1;
1428
+ }
1429
+
1430
+ e.preventDefault();
1431
+ });
1432
+
1433
+ // 触摸移动
1434
+ this.addEvent(this.image, 'touchmove', e => {
1435
+ if (Date.now() - this.touchState.lastTouchTime < 16) {
1436
+ return;
1437
+ }
1438
+ this.touchState.lastTouchTime = Date.now();
1439
+
1440
+ if (e.touches.length === 1 && this.touchState.isDragging && !this.touchState.isPinching) {
1441
+ const deltaX = e.touches[0].clientX - this.touchState.startX;
1442
+ const deltaY = e.touches[0].clientY - this.touchState.startY;
1443
+
1444
+ const rotatedDelta = this.rotatePoint(deltaX, deltaY, -this.rotation);
1445
+
1446
+ this.touchState.movementCount++;
1447
+
1448
+ if (
1449
+ this.touchState.movementCount > this.touchState.stabilizationThreshold ||
1450
+ Math.abs(deltaX) > 5 ||
1451
+ Math.abs(deltaY) > 5
1452
+ ) {
1453
+ this.translateX = this.touchState.startTranslateX + rotatedDelta.x;
1454
+ this.translateY = this.touchState.startTranslateY + rotatedDelta.y;
1455
+ this.updateImageTransform();
1456
+ }
1457
+ } else if (e.touches.length === 2 && this.touchState.isPinching) {
1458
+ const touch1 = e.touches[0];
1459
+ const touch2 = e.touches[1];
1460
+
1461
+ const currentDistance = this.getDistance(touch1, touch2);
1462
+ this.touchState.scaleRatio = currentDistance / this.touchState.initialDistance;
1463
+ const newScale = this.touchState.initialScale * this.touchState.scaleRatio;
1464
+
1465
+ const minScale = 0.1;
1466
+ const maxScale = 5;
1467
+ const clampedScale = Math.max(minScale, Math.min(maxScale, newScale));
1468
+
1469
+ if (Math.abs(clampedScale - this.scale) > this.touchState.minScaleChange) {
1470
+ const scaleDiff = clampedScale / this.touchState.initialScale;
1471
+
1472
+ const rect = this.imageContainer.getBoundingClientRect();
1473
+ const containerWidth = rect.width;
1474
+ const containerHeight = rect.height;
1475
+
1476
+ this.translateX =
1477
+ this.touchState.initialTranslateX * scaleDiff +
1478
+ this.touchState.centerX -
1479
+ containerWidth / 2 -
1480
+ scaleDiff * (this.touchState.centerX - containerWidth / 2);
1481
+
1482
+ this.translateY =
1483
+ this.touchState.initialTranslateY * scaleDiff +
1484
+ this.touchState.centerY -
1485
+ containerHeight / 2 -
1486
+ scaleDiff * (this.touchState.centerY - containerHeight / 2);
1487
+
1488
+ this.scale = clampedScale;
1489
+ this.updateImageTransform();
1490
+ this.updateZoomIndicator();
1491
+ }
1492
+ }
1493
+
1494
+ e.preventDefault();
1495
+ });
1496
+
1497
+ // 触摸结束/取消
1498
+ this.addEvent(this.image, 'touchend', e => {
1499
+ if (e.touches.length === 0) {
1500
+ this.touchState.isDragging = false;
1501
+ this.touchState.isPinching = false;
1502
+ this.image.classList.remove('dragging');
1503
+ } else if (e.touches.length === 1 && this.touchState.isPinching) {
1504
+ this.touchState.isPinching = false;
1505
+ this.touchState.isDragging = true;
1506
+ this.touchState.startX = e.touches[0].clientX;
1507
+ this.touchState.startY = e.touches[0].clientY;
1508
+ this.touchState.startTranslateX = this.translateX;
1509
+ this.touchState.startTranslateY = this.translateY;
1510
+ this.image.classList.add('dragging');
1511
+ }
1512
+
1513
+ e.preventDefault();
1514
+ });
1515
+
1516
+ this.addEvent(this.image, 'touchcancel', () => {
1517
+ this.touchState.isDragging = false;
1518
+ this.touchState.isPinching = false;
1519
+ this.image.classList.remove('dragging');
1520
+ });
1521
+ }
1522
+
1523
+ getDistance(touch1, touch2) {
1524
+ const dx = touch1.clientX - touch2.clientX;
1525
+ const dy = touch1.clientY - touch2.clientY;
1526
+ return Math.sqrt(dx * dx + dy * dy);
1527
+ }
1528
+
1529
+ calculateRelativeCenter(x, y) {
1530
+ const metadata = this.imageMetadata[this.currentIndex];
1531
+ if (!metadata) return;
1532
+
1533
+ const containerWidth = this.imageContainer.clientWidth;
1534
+ const containerHeight = this.imageContainer.clientHeight;
1535
+ const containerCenterX = containerWidth / 2;
1536
+ const containerCenterY = containerHeight / 2;
1537
+
1538
+ const offsetX = x - containerCenterX - this.translateX;
1539
+ const offsetY = y - containerCenterY - this.translateY;
1540
+
1541
+ this.touchState.relativeCenterX = offsetX / this.scale;
1542
+ this.touchState.relativeCenterY = offsetY / this.scale;
1543
+ }
1544
+
1545
+ zoomAtPoint(delta, x, y) {
1546
+ const oldScale = this.scale;
1547
+ const maxScale = 5;
1548
+ const newScale = Math.max(0.1, Math.min(maxScale, this.scale + delta));
1549
+
1550
+ if (newScale === this.scale) return;
1551
+
1552
+ const scaleDiff = newScale / oldScale;
1553
+ const containerWidth = this.imageContainer.clientWidth;
1554
+ const containerHeight = this.imageContainer.clientHeight;
1555
+
1556
+ this.translateX = this.translateX * scaleDiff + x - containerWidth / 2 - scaleDiff * (x - containerWidth / 2);
1557
+ this.translateY = this.translateY * scaleDiff + y - containerHeight / 2 - scaleDiff * (y - containerHeight / 2);
1558
+
1559
+ this.scale = newScale;
1560
+ this.updateImageTransform();
1561
+ this.updateZoomIndicator();
1562
+ }
1563
+
1564
+ zoom(delta) {
1565
+ const containerWidth = this.imageContainer.clientWidth;
1566
+ const containerHeight = this.imageContainer.clientHeight;
1567
+ this.zoomAtPoint(delta, containerWidth / 2, containerHeight / 2);
1568
+ }
1569
+
1570
+ rotate(degrees) {
1571
+ const oldRotation = this.rotation;
1572
+ const newRotation = oldRotation + degrees;
1573
+
1574
+ if (oldRotation === newRotation) return;
1575
+
1576
+ const containerRect = this.imageContainer.getBoundingClientRect();
1577
+ const viewportCenterX = containerRect.width / 2;
1578
+ const viewportCenterY = containerRect.height / 2;
1579
+
1580
+ const currentCenterX = viewportCenterX + this.translateX;
1581
+ const currentCenterY = viewportCenterY + this.translateY;
1582
+
1583
+ this.rotation = newRotation;
1584
+
1585
+ this.translateX = 0;
1586
+ this.translateY = 0;
1587
+
1588
+ const metadata = this.imageMetadata[this.currentIndex];
1589
+ if (metadata) {
1590
+ const newBoundingBox = this.calculateBoundingBox(metadata.width, metadata.height, newRotation);
1591
+ const scaledWidth = newBoundingBox.width * this.scale;
1592
+ const scaledHeight = newBoundingBox.height * this.scale;
1593
+
1594
+ this.translateX = (currentCenterX - viewportCenterX) * (scaledWidth / (scaledWidth - this.translateX));
1595
+ this.translateY = (currentCenterY - viewportCenterY) * (scaledHeight / (scaledHeight - this.translateY));
1596
+ }
1597
+
1598
+ this.updateImageTransform();
1599
+ this.updateZoomIndicator();
1600
+ }
1601
+
1602
+ calculateBoundingBox(width, height, rotation) {
1603
+ const rad = (rotation * Math.PI) / 180;
1604
+ const absCos = Math.abs(Math.cos(rad));
1605
+ const absSin = Math.abs(Math.sin(rad));
1606
+
1607
+ return {
1608
+ width: width * absCos + height * absSin,
1609
+ height: width * absSin + height * absCos,
1610
+ };
1611
+ }
1612
+
1613
+ resetTransform() {
1614
+ this.rotation = 0;
1615
+ const metadata = this.imageMetadata[this.currentIndex];
1616
+ if (metadata) {
1617
+ this.fitImageToScreen(metadata.width, metadata.height);
1618
+ }
1619
+ }
1620
+
1621
+ showOriginalSize() {
1622
+ this.rotation = 0;
1623
+ this.scale = 1;
1624
+ this.translateX = 0;
1625
+ this.translateY = 0;
1626
+ this.updateImageTransform();
1627
+ this.updateZoomIndicator();
1628
+ }
1629
+
1630
+ downloadImage() {
1631
+ const currentUrl = this.images[this.currentIndex];
1632
+ const metadata = this.imageMetadata[this.currentIndex];
1633
+
1634
+ const img = new Image();
1635
+ img.crossOrigin = 'anonymous';
1636
+ img.onload = () => {
1637
+ try {
1638
+ const canvas = document.createElement('canvas');
1639
+ canvas.width = img.width;
1640
+ canvas.height = img.height;
1641
+
1642
+ const ctx = canvas.getContext('2d');
1643
+ ctx.drawImage(img, 0, 0);
1644
+
1645
+ const dataURL = canvas.toDataURL('image/jpeg');
1646
+
1647
+ const a = document.createElement('a');
1648
+ a.href = dataURL;
1649
+ a.download = metadata ? metadata.name : 'image.jpg';
1650
+ document.body.appendChild(a);
1651
+ a.click();
1652
+ document.body.removeChild(a);
1653
+ } catch (error) {
1654
+ console.error('图片下载失败:', error);
1655
+ this.downloadOriginalImage(currentUrl, metadata);
1656
+ }
1657
+ };
1658
+
1659
+ img.onerror = () => {
1660
+ this.downloadOriginalImage(currentUrl, metadata);
1661
+ };
1662
+
1663
+ img.src = currentUrl;
1664
+ }
1665
+
1666
+ downloadOriginalImage(url, metadata) {
1667
+ try {
1668
+ const a = document.createElement('a');
1669
+ a.href = url;
1670
+ a.download = metadata ? metadata.name : 'image.jpg';
1671
+ document.body.appendChild(a);
1672
+ a.click();
1673
+ document.body.removeChild(a);
1674
+ } catch (error) {
1675
+ console.error('原图下载失败:', error);
1676
+ alert('图片下载失败,可能是跨域限制导致的');
1677
+ }
1678
+ }
1679
+
1680
+ toggleFullscreen() {
1681
+ if (!document.fullscreenElement) {
1682
+ this.container.requestFullscreen().catch(err => {
1683
+ console.error(`全屏请求失败: ${err.message}`);
1684
+ });
1685
+ this.isFullscreen = true;
1686
+ } else {
1687
+ if (document.exitFullscreen) {
1688
+ document.exitFullscreen();
1689
+ this.isFullscreen = false;
1690
+ }
1691
+ }
1692
+ }
1693
+
1694
+ handleKeydown(e) {
1695
+ if (this.container.style.display !== 'block') return;
1696
+
1697
+ switch (e.key) {
1698
+ case 'Escape':
1699
+ this.close();
1700
+ e.preventDefault();
1701
+ break;
1702
+ case 'ArrowLeft':
1703
+ this.prevImage();
1704
+ e.preventDefault();
1705
+ break;
1706
+ case 'ArrowRight':
1707
+ this.nextImage();
1708
+ e.preventDefault();
1709
+ break;
1710
+ case '+':
1711
+ case '=':
1712
+ this.zoom(0.1);
1713
+ e.preventDefault();
1714
+ break;
1715
+ case '-':
1716
+ this.zoom(-0.1);
1717
+ e.preventDefault();
1718
+ break;
1719
+ case '0':
1720
+ this.resetTransform();
1721
+ e.preventDefault();
1722
+ break;
1723
+ case 'f':
1724
+ case 'F':
1725
+ this.toggleFullscreen();
1726
+ e.preventDefault();
1727
+ break;
1728
+ case 'i':
1729
+ case 'I':
1730
+ this.toggleImageInfo();
1731
+ e.preventDefault();
1732
+ break;
1733
+ }
1734
+ }
1735
+
1736
+ handleResize() {
1737
+ const metadata = this.imageMetadata[this.currentIndex];
1738
+ if (!metadata) return;
1739
+
1740
+ const containerWidth = this.imageContainer.clientWidth;
1741
+ const containerHeight = this.imageContainer.clientHeight;
1742
+
1743
+ // 计算旋转后的有效尺寸
1744
+ const angle = this.rotation % 360;
1745
+ let effectiveWidth = metadata.width;
1746
+ let effectiveHeight = metadata.height;
1747
+
1748
+ if (angle === 90 || angle === 270) {
1749
+ effectiveWidth = metadata.height;
1750
+ effectiveHeight = metadata.width;
1751
+ }
1752
+
1753
+ // 计算适合容器的缩放比例
1754
+ const fitScale = Math.min(containerWidth / effectiveWidth, containerHeight / effectiveHeight);
1755
+
1756
+ // 当前缩放后的图片尺寸
1757
+ const currentScaledWidth = effectiveWidth * this.scale;
1758
+ const currentScaledHeight = effectiveHeight * this.scale;
1759
+
1760
+ // 判断是否超出容器
1761
+ const isOverflowing = currentScaledWidth > containerWidth || currentScaledHeight > containerHeight;
1762
+
1763
+ let targetScale = this.scale;
1764
+
1765
+ if (isOverflowing) {
1766
+ // 图片超出容器,缩小到适合比例
1767
+ targetScale = Math.max(0.1, fitScale);
1768
+ } else if (fitScale >= 1.0) {
1769
+ // 图片小于100%且有足够空间,放大到100%
1770
+ targetScale = 1.0;
1771
+ } else {
1772
+ targetScale = fitScale;
1773
+ }
1774
+
1775
+ // 只有当变化显著时才更新(避免微小调整)
1776
+ if (Math.abs(targetScale - this.scale) > 0.01) {
1777
+ this.scale = targetScale;
1778
+ this.translateX = 0;
1779
+ this.translateY = 0;
1780
+ this.updateImageTransform();
1781
+ this.updateZoomIndicator();
1782
+ }
1783
+ }
1784
+
1785
+ show() {
1786
+ this.container.style.display = 'block';
1787
+ setTimeout(() => {
1788
+ this.container.style.opacity = '1';
1789
+ }, 10);
1790
+ }
1791
+
1792
+ close() {
1793
+ this.removeAllEvents();
1794
+
1795
+ this.container.style.opacity = '0';
1796
+ setTimeout(() => {
1797
+ this.container.style.display = 'none';
1798
+ const styles = document.getElementById('image-viewer-styles');
1799
+ if (styles) styles.remove();
1800
+ if (this.container) this.container.remove();
1801
+ }, 300);
1802
+ }
1803
+ }
1804
+
1805
+ // 返回ImageViewer类
1806
+ return ImageViewer;
1807
+ });
1808
+ } (imageViewer));
1809
+
1810
+ var imageViewerExports = imageViewer.exports;
1811
+ var index = /*@__PURE__*/getDefaultExportFromCjs(imageViewerExports);
1812
+
1813
+ module.exports = index;