card.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /**
  2. * 卡片渲染组件
  3. */
  4. class CardRenderer {
  5. /**
  6. * 渲染帖子卡片列表
  7. * @param {Array} notes - 帖子数组
  8. * @param {HTMLElement} container - 容器元素
  9. */
  10. static renderNotes(notes, container) {
  11. if (!notes || notes.length === 0) {
  12. container.innerHTML = `
  13. <div class="empty-state">
  14. <div class="empty-icon">📭</div>
  15. <p class="empty-text">该特征暂无搜索结果</p>
  16. </div>
  17. `;
  18. return;
  19. }
  20. container.innerHTML = notes.map(note => this.renderNoteCard(note)).join('');
  21. // 初始化轮播图
  22. this.initCarousels(container);
  23. }
  24. /**
  25. * 渲染单个帖子卡片
  26. * @param {Object} noteData - 帖子数据
  27. * @returns {string} HTML字符串
  28. */
  29. static renderNoteCard(noteData) {
  30. const note = noteData.note_card || noteData;
  31. // 提取数据
  32. const title = note.display_title || '无标题';
  33. const desc = note.desc || '';
  34. const user = note.user || {};
  35. const interactInfo = note.interact_info || {};
  36. const images = note.image_list || [];
  37. const timestamp = note.publish_timestamp;
  38. // 格式化时间
  39. const timeStr = this.formatTime(timestamp);
  40. // 格式化数字
  41. const likeCount = this.formatNumber(interactInfo.liked_count);
  42. const commentCount = this.formatNumber(interactInfo.comment_count);
  43. const collectCount = this.formatNumber(interactInfo.collected_count);
  44. const shareCount = this.formatNumber(interactInfo.shared_count);
  45. return `
  46. <div class="note-card">
  47. <!-- 卡片头部 -->
  48. <div class="card-header">
  49. <div class="card-user">
  50. <img class="card-avatar"
  51. src="${user.avatar || 'https://via.placeholder.com/40'}"
  52. alt="${user.nickname}"
  53. onerror="this.src='https://via.placeholder.com/40'">
  54. <div class="card-user-info">
  55. <div class="card-username">${user.nickname || '匿名用户'}</div>
  56. <div class="card-time">${timeStr}</div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- 图片轮播 -->
  61. ${images.length > 0 ? `
  62. <div class="card-carousel" data-carousel-container></div>
  63. ` : ''}
  64. <!-- 卡片内容 -->
  65. <div class="card-body">
  66. <h3 class="card-title">${this.escapeHtml(title)}</h3>
  67. <div class="card-desc" data-desc>
  68. ${this.escapeHtml(desc)}
  69. </div>
  70. ${desc.length > 150 ? `
  71. <span class="card-desc-toggle" data-desc-toggle>展开全文</span>
  72. ` : ''}
  73. </div>
  74. <!-- 卡片底部交互数据 -->
  75. <div class="card-stats">
  76. <div class="card-stat">
  77. <span class="card-stat-icon">❤️</span>
  78. <span>${likeCount}</span>
  79. </div>
  80. <div class="card-stat">
  81. <span class="card-stat-icon">💬</span>
  82. <span>${commentCount}</span>
  83. </div>
  84. <div class="card-stat">
  85. <span class="card-stat-icon">⭐</span>
  86. <span>${collectCount}</span>
  87. </div>
  88. <div class="card-stat">
  89. <span class="card-stat-icon">🔗</span>
  90. <span>${shareCount}</span>
  91. </div>
  92. </div>
  93. </div>
  94. `;
  95. }
  96. /**
  97. * 初始化所有轮播图
  98. * @param {HTMLElement} container - 容器元素
  99. */
  100. static initCarousels(container) {
  101. const carouselContainers = container.querySelectorAll('[data-carousel-container]');
  102. carouselContainers.forEach((carouselContainer, index) => {
  103. const card = carouselContainer.closest('.note-card');
  104. // 从容器中找到对应的图片数据
  105. // 这里需要从原始数据中获取,所以我们先存储在 data 属性中
  106. const carouselIndex = Array.from(container.querySelectorAll('[data-carousel-container]')).indexOf(carouselContainer);
  107. // 临时方案:从全局变量获取(在 renderNotes 时设置)
  108. if (window.__currentNotes && window.__currentNotes[carouselIndex]) {
  109. const note = window.__currentNotes[carouselIndex].note_card || window.__currentNotes[carouselIndex];
  110. const images = note.image_list || [];
  111. if (images.length > 0) {
  112. new Carousel(carouselContainer, images);
  113. }
  114. }
  115. });
  116. // 绑定展开/折叠事件
  117. this.bindDescToggle(container);
  118. }
  119. /**
  120. * 绑定描述文本展开/折叠
  121. * @param {HTMLElement} container - 容器元素
  122. */
  123. static bindDescToggle(container) {
  124. const toggles = container.querySelectorAll('[data-desc-toggle]');
  125. toggles.forEach(toggle => {
  126. toggle.addEventListener('click', () => {
  127. const desc = toggle.previousElementSibling;
  128. const isExpanded = desc.classList.contains('expanded');
  129. if (isExpanded) {
  130. desc.classList.remove('expanded');
  131. toggle.textContent = '展开全文';
  132. } else {
  133. desc.classList.add('expanded');
  134. toggle.textContent = '收起';
  135. }
  136. });
  137. });
  138. }
  139. /**
  140. * 格式化时间戳
  141. * @param {number} timestamp - Unix时间戳(秒)
  142. * @returns {string} 格式化的时间字符串
  143. */
  144. static formatTime(timestamp) {
  145. if (!timestamp) return '未知时间';
  146. const date = new Date(timestamp * 1000);
  147. const now = new Date();
  148. const diff = now - date;
  149. // 小于1分钟
  150. if (diff < 60 * 1000) {
  151. return '刚刚';
  152. }
  153. // 小于1小时
  154. if (diff < 60 * 60 * 1000) {
  155. const minutes = Math.floor(diff / (60 * 1000));
  156. return `${minutes}分钟前`;
  157. }
  158. // 小于24小时
  159. if (diff < 24 * 60 * 60 * 1000) {
  160. const hours = Math.floor(diff / (60 * 60 * 1000));
  161. return `${hours}小时前`;
  162. }
  163. // 小于7天
  164. if (diff < 7 * 24 * 60 * 60 * 1000) {
  165. const days = Math.floor(diff / (24 * 60 * 60 * 1000));
  166. return `${days}天前`;
  167. }
  168. // 显示具体日期
  169. const year = date.getFullYear();
  170. const month = String(date.getMonth() + 1).padStart(2, '0');
  171. const day = String(date.getDate()).padStart(2, '0');
  172. if (year === now.getFullYear()) {
  173. return `${month}-${day}`;
  174. }
  175. return `${year}-${month}-${day}`;
  176. }
  177. /**
  178. * 格式化数字
  179. * @param {number|string} num - 数字
  180. * @returns {string} 格式化的数字字符串
  181. */
  182. static formatNumber(num) {
  183. if (!num) return '0';
  184. const n = parseInt(num);
  185. if (n >= 10000) {
  186. return (n / 10000).toFixed(1) + 'w';
  187. }
  188. if (n >= 1000) {
  189. return (n / 1000).toFixed(1) + 'k';
  190. }
  191. return n.toString();
  192. }
  193. /**
  194. * HTML 转义
  195. * @param {string} text - 原始文本
  196. * @returns {string} 转义后的文本
  197. */
  198. static escapeHtml(text) {
  199. if (!text) return '';
  200. const div = document.createElement('div');
  201. div.textContent = text;
  202. return div.innerHTML;
  203. }
  204. /**
  205. * 显示加载状态
  206. * @param {HTMLElement} container - 容器元素
  207. */
  208. static showLoading(container) {
  209. container.innerHTML = '<div class="loading">加载中...</div>';
  210. }
  211. /**
  212. * 显示错误状态
  213. * @param {HTMLElement} container - 容器元素
  214. * @param {string} message - 错误消息
  215. */
  216. static showError(container, message) {
  217. container.innerHTML = `
  218. <div class="empty-state">
  219. <div class="empty-icon">❌</div>
  220. <p class="empty-text">${message}</p>
  221. </div>
  222. `;
  223. }
  224. }