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