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