carousel.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * 图片轮播组件
  3. */
  4. class Carousel {
  5. constructor(container, images) {
  6. this.container = container;
  7. this.images = images || [];
  8. this.currentIndex = 0;
  9. this.init();
  10. }
  11. init() {
  12. if (this.images.length === 0) {
  13. this.container.innerHTML = '<div class="carousel-empty">暂无图片</div>';
  14. return;
  15. }
  16. this.render();
  17. this.bindEvents();
  18. }
  19. render() {
  20. const html = `
  21. <div class="carousel-images" data-carousel-track>
  22. ${this.images.map((img, index) => `
  23. <img
  24. class="carousel-image"
  25. src="${img}"
  26. alt="图片 ${index + 1}"
  27. loading="lazy"
  28. data-index="${index}"
  29. />
  30. `).join('')}
  31. </div>
  32. ${this.images.length > 1 ? `
  33. <div class="carousel-controls">
  34. <button class="carousel-btn carousel-prev" data-carousel-prev>‹</button>
  35. <button class="carousel-btn carousel-next" data-carousel-next>›</button>
  36. </div>
  37. <div class="carousel-indicators">
  38. ${this.images.map((_, index) => `
  39. <span
  40. class="carousel-indicator ${index === 0 ? 'active' : ''}"
  41. data-carousel-indicator="${index}"
  42. ></span>
  43. `).join('')}
  44. </div>
  45. ` : ''}
  46. `;
  47. this.container.innerHTML = html;
  48. }
  49. bindEvents() {
  50. if (this.images.length <= 1) return;
  51. // 上一张
  52. const prevBtn = this.container.querySelector('[data-carousel-prev]');
  53. if (prevBtn) {
  54. prevBtn.addEventListener('click', (e) => {
  55. e.stopPropagation();
  56. this.prev();
  57. });
  58. }
  59. // 下一张
  60. const nextBtn = this.container.querySelector('[data-carousel-next]');
  61. if (nextBtn) {
  62. nextBtn.addEventListener('click', (e) => {
  63. e.stopPropagation();
  64. this.next();
  65. });
  66. }
  67. // 指示器点击
  68. const indicators = this.container.querySelectorAll('[data-carousel-indicator]');
  69. indicators.forEach(indicator => {
  70. indicator.addEventListener('click', (e) => {
  71. e.stopPropagation();
  72. const index = parseInt(indicator.getAttribute('data-carousel-indicator'));
  73. this.goTo(index);
  74. });
  75. });
  76. // 图片点击打开模态框
  77. const imgElements = this.container.querySelectorAll('.carousel-image');
  78. imgElements.forEach(img => {
  79. img.addEventListener('click', () => {
  80. this.openModal(this.currentIndex);
  81. });
  82. });
  83. // 键盘快捷键
  84. document.addEventListener('keydown', (e) => {
  85. if (e.key === 'ArrowLeft') this.prev();
  86. if (e.key === 'ArrowRight') this.next();
  87. });
  88. }
  89. prev() {
  90. this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
  91. this.update();
  92. }
  93. next() {
  94. this.currentIndex = (this.currentIndex + 1) % this.images.length;
  95. this.update();
  96. }
  97. goTo(index) {
  98. this.currentIndex = index;
  99. this.update();
  100. }
  101. update() {
  102. const track = this.container.querySelector('[data-carousel-track]');
  103. if (track) {
  104. track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
  105. }
  106. // 更新指示器
  107. const indicators = this.container.querySelectorAll('[data-carousel-indicator]');
  108. indicators.forEach((indicator, index) => {
  109. indicator.classList.toggle('active', index === this.currentIndex);
  110. });
  111. }
  112. openModal(startIndex) {
  113. ImageModal.open(this.images, startIndex);
  114. }
  115. }
  116. /**
  117. * 图片查看器模态框
  118. */
  119. class ImageModal {
  120. static modal = null;
  121. static images = [];
  122. static currentIndex = 0;
  123. static init() {
  124. this.modal = document.getElementById('image-modal');
  125. if (!this.modal) return;
  126. const overlay = document.getElementById('modal-overlay');
  127. const closeBtn = document.getElementById('modal-close');
  128. const prevBtn = document.getElementById('modal-prev');
  129. const nextBtn = document.getElementById('modal-next');
  130. // 关闭
  131. overlay?.addEventListener('click', () => this.close());
  132. closeBtn?.addEventListener('click', () => this.close());
  133. // 切换
  134. prevBtn?.addEventListener('click', () => this.prev());
  135. nextBtn?.addEventListener('click', () => this.next());
  136. // 键盘
  137. document.addEventListener('keydown', (e) => {
  138. if (!this.modal?.classList.contains('active')) return;
  139. if (e.key === 'Escape') this.close();
  140. if (e.key === 'ArrowLeft') this.prev();
  141. if (e.key === 'ArrowRight') this.next();
  142. });
  143. }
  144. static open(images, startIndex = 0) {
  145. this.images = images;
  146. this.currentIndex = startIndex;
  147. this.show();
  148. }
  149. static show() {
  150. if (!this.modal) return;
  151. this.modal.classList.add('active');
  152. this.updateImage();
  153. document.body.style.overflow = 'hidden';
  154. }
  155. static close() {
  156. if (!this.modal) return;
  157. this.modal.classList.remove('active');
  158. document.body.style.overflow = '';
  159. }
  160. static prev() {
  161. this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
  162. this.updateImage();
  163. }
  164. static next() {
  165. this.currentIndex = (this.currentIndex + 1) % this.images.length;
  166. this.updateImage();
  167. }
  168. static updateImage() {
  169. const img = document.getElementById('modal-image');
  170. const caption = document.getElementById('modal-caption');
  171. if (img) {
  172. img.src = this.images[this.currentIndex];
  173. }
  174. if (caption) {
  175. caption.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
  176. }
  177. }
  178. }
  179. // 初始化模态框
  180. if (document.readyState === 'loading') {
  181. document.addEventListener('DOMContentLoaded', () => ImageModal.init());
  182. } else {
  183. ImageModal.init();
  184. }