history.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. 'use strict';
  2. (function () {
  3. const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="M7 10l5 5 5-5"/><path d="M4 21h16"/></svg>';
  4. function esc(value) {
  5. const div = document.createElement('div');
  6. div.textContent = String(value ?? '');
  7. return div.innerHTML;
  8. }
  9. const state = {
  10. initialized: false,
  11. loading: false,
  12. root: '',
  13. items: [],
  14. filter: 'all',
  15. keyword: '',
  16. };
  17. const FILTER_LABELS = {
  18. logo: 'LOGO',
  19. banner: '海报',
  20. signage: '招牌',
  21. sticker: '贴纸',
  22. dish: '菜品',
  23. other: '其他',
  24. };
  25. function formatBytes(size) {
  26. if (!Number.isFinite(size) || size <= 0) return '—';
  27. if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
  28. return `${(size / 1024 / 1024).toFixed(1)} MB`;
  29. }
  30. function formatTime(value) {
  31. if (!value) return '';
  32. const date = new Date(value);
  33. const pad = (n) => String(n).padStart(2, '0');
  34. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
  35. }
  36. function getFilteredItems() {
  37. const keyword = state.keyword.trim().toLowerCase();
  38. return state.items.filter((item) => {
  39. if (state.filter !== 'all' && item.kind !== state.filter) return false;
  40. if (!keyword) return true;
  41. return `${item.shopName} ${item.name} ${item.fileName}`.toLowerCase().includes(keyword);
  42. });
  43. }
  44. function renderSummary() {
  45. const summary = document.getElementById('historySummary');
  46. const filtered = getFilteredItems();
  47. if (state.loading) {
  48. summary.textContent = '正在加载历史作品…';
  49. return;
  50. }
  51. summary.textContent = state.items.length
  52. ? `共 ${state.items.length} 件作品 · 当前显示 ${filtered.length} 件`
  53. : '暂无历史作品';
  54. }
  55. function renderGallery() {
  56. const gallery = document.getElementById('historyGallery');
  57. const items = getFilteredItems();
  58. gallery.classList.toggle('empty', items.length === 0);
  59. if (!items.length) {
  60. gallery.innerHTML = `
  61. <div class="history-empty">
  62. <div class="history-empty-icon">
  63. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="9" cy="10" r="1.8"/><path d="M21 16l-5-5-6 6"/></svg>
  64. </div>
  65. <p>${state.loading ? '正在加载作品' : '暂无匹配的历史作品'}</p>
  66. <small>${state.loading ? '请稍候' : '生成完成的作品会自动出现在这里'}</small>
  67. </div>`;
  68. return;
  69. }
  70. gallery.innerHTML = '';
  71. items.forEach((item) => {
  72. const card = document.createElement('figure');
  73. card.className = 'creation-card';
  74. card.dataset.kind = item.kind;
  75. const frame = document.createElement('div');
  76. frame.className = 'creation-frame';
  77. if (item.kind === 'sticker') frame.classList.add('transparent-bg');
  78. const image = document.createElement('img');
  79. image.src = item.url;
  80. image.alt = `${item.shopName} ${item.name}`;
  81. image.loading = 'lazy';
  82. frame.appendChild(image);
  83. window.shanHoverZoom(frame, image.src, item.kind === 'sticker');
  84. const kind = document.createElement('span');
  85. kind.className = 'creation-kind';
  86. kind.textContent = FILTER_LABELS[item.kind] || '其他';
  87. frame.appendChild(kind);
  88. const download = document.createElement('a');
  89. download.className = 'result-download-icon';
  90. download.href = item.url;
  91. download.download = '';
  92. download.title = '下载原图';
  93. download.setAttribute('aria-label', '下载原图');
  94. download.innerHTML = ICON_DOWNLOAD;
  95. frame.appendChild(download);
  96. const caption = document.createElement('figcaption');
  97. const title = document.createElement('div');
  98. title.className = 'creation-title';
  99. title.title = item.name;
  100. title.textContent = item.name;
  101. const shop = document.createElement('div');
  102. shop.className = 'creation-shop';
  103. shop.title = item.shopName;
  104. shop.textContent = item.shopName;
  105. const meta = document.createElement('div');
  106. meta.className = 'creation-meta';
  107. meta.textContent = `${formatTime(item.modifiedAt)} · ${formatBytes(item.fileSizeBytes)}`;
  108. caption.append(title, shop, meta);
  109. card.append(frame, caption);
  110. gallery.appendChild(card);
  111. });
  112. }
  113. function render() {
  114. renderSummary();
  115. renderGallery();
  116. }
  117. async function loadHistory() {
  118. if (state.loading) return;
  119. state.loading = true;
  120. render();
  121. try {
  122. const rootResponse = await fetch('/api/default-root');
  123. const rootData = await rootResponse.json();
  124. state.root = rootData.root;
  125. const response = await fetch(`/api/creations?root=${encodeURIComponent(state.root)}&limit=600`);
  126. const data = await response.json();
  127. if (!response.ok) throw new Error(data.error || '历史作品加载失败');
  128. state.items = data.items || [];
  129. } catch (error) {
  130. state.items = [];
  131. renderGallery();
  132. document.getElementById('historySummary').textContent = error.message;
  133. } finally {
  134. state.loading = false;
  135. render();
  136. }
  137. }
  138. function bindEvents() {
  139. document.getElementById('btnRefreshHistory').addEventListener('click', loadHistory);
  140. document.getElementById('historySearch').addEventListener('input', (event) => {
  141. state.keyword = event.target.value;
  142. render();
  143. });
  144. document.getElementById('historyFilters').addEventListener('click', (event) => {
  145. const button = event.target.closest('[data-filter]');
  146. if (!button) return;
  147. state.filter = button.dataset.filter;
  148. document.querySelectorAll('#historyFilters .history-filter').forEach((item) => {
  149. item.classList.toggle('active', item === button);
  150. });
  151. render();
  152. });
  153. }
  154. window.shanHistoryInit = function () {
  155. if (!state.initialized) {
  156. state.initialized = true;
  157. bindEvents();
  158. }
  159. loadHistory();
  160. };
  161. })();