app.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. 'use strict';
  2. (function () {
  3. const views = {
  4. crawl: document.getElementById('viewCrawl'),
  5. gen: document.getElementById('viewGen'),
  6. tasks: document.getElementById('viewTasks'),
  7. history: document.getElementById('viewHistory'),
  8. settings: document.getElementById('viewSettings'),
  9. };
  10. const navItems = document.querySelectorAll('.nav-item[data-view]');
  11. let settingsLoaded = false;
  12. let logTimer = null;
  13. let lastLogSignature = '';
  14. function showSettingsStatus(message, type = 'info') {
  15. const status = document.getElementById('settingsStatus');
  16. status.textContent = message;
  17. status.className = `badge ${type === 'success' ? 'badge-ok' : type === 'error' ? 'badge-fail' : 'badge-info'}`;
  18. }
  19. async function loadModels(selectedModel) {
  20. const select = document.getElementById('settingsModel');
  21. const response = await fetch('/api/models');
  22. if (!response.ok) throw new Error('模型列表加载失败');
  23. const data = await response.json();
  24. select.innerHTML = '';
  25. for (const model of data.models || []) {
  26. const option = document.createElement('option');
  27. option.value = typeof model === 'string' ? model : model.id;
  28. option.textContent = typeof model === 'string' ? model : (model.label || model.name || model.id);
  29. option.selected = option.value === selectedModel;
  30. select.appendChild(option);
  31. }
  32. }
  33. async function loadSettings() {
  34. showSettingsStatus('加载中…');
  35. const response = await fetch('/api/settings');
  36. if (!response.ok) throw new Error('设置加载失败');
  37. const settings = await response.json();
  38. document.getElementById('settingsApiKey').value = settings.apiKey || '';
  39. document.getElementById('settingsApiBase').value = settings.apiBase || '';
  40. document.getElementById('settingsRootDir').value = settings.rootDir || '';
  41. document.getElementById('pageSizeInput').value = settings.pageSize || 50;
  42. document.getElementById('delayInput').value = settings.delayMs ?? 300;
  43. const cookieInput = document.getElementById('settingsCookie');
  44. cookieInput.value = '';
  45. cookieInput.placeholder = settings.cookieConfigured
  46. ? '已配置,输入新 Cookie 可更换'
  47. : '登录 melody.shop.ele.me 后从 DevTools 复制';
  48. await loadModels(settings.model);
  49. showSettingsStatus('已同步');
  50. }
  51. async function saveSettings() {
  52. const button = document.getElementById('btnSaveSettings');
  53. button.disabled = true;
  54. showSettingsStatus('保存中…');
  55. try {
  56. const response = await fetch('/api/settings', {
  57. method: 'POST',
  58. headers: { 'Content-Type': 'application/json' },
  59. body: JSON.stringify({
  60. apiKey: document.getElementById('settingsApiKey').value.trim(),
  61. model: document.getElementById('settingsModel').value,
  62. rootDir: document.getElementById('settingsRootDir').value.trim(),
  63. pageSize: Number(document.getElementById('pageSizeInput').value),
  64. delayMs: Number(document.getElementById('delayInput').value),
  65. ...(document.getElementById('settingsCookie').value.trim()
  66. ? { cookie: document.getElementById('settingsCookie').value.trim() }
  67. : {}),
  68. }),
  69. });
  70. const data = await response.json();
  71. if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
  72. await loadSettings();
  73. showSettingsStatus('已保存', 'success');
  74. } catch (error) {
  75. showSettingsStatus(error.message, 'error');
  76. } finally {
  77. button.disabled = false;
  78. }
  79. }
  80. async function initSettings() {
  81. if (settingsLoaded) return;
  82. document.getElementById('btnSaveSettings').addEventListener('click', saveSettings);
  83. document.getElementById('btnClearLog').addEventListener('click', () => {
  84. clearLogs().catch(() => {});
  85. });
  86. try {
  87. await loadSettings();
  88. settingsLoaded = true;
  89. } catch (error) {
  90. showSettingsStatus(error.message, 'error');
  91. }
  92. }
  93. function escapeLogText(value) {
  94. const div = document.createElement('div');
  95. div.textContent = String(value ?? '');
  96. return div.innerHTML;
  97. }
  98. function renderLogs(logs) {
  99. const panel = document.getElementById('logPanel');
  100. if (!panel) return;
  101. const sourceMap = { task: '任务', system: '系统' };
  102. const levelMap = { info: 'INFO', success: 'OK', warn: 'WARN', error: 'FAIL' };
  103. panel.innerHTML = logs.map((log) => {
  104. const level = levelMap[log.level] || 'INFO';
  105. const levelClass = log.level === 'success' ? 'ok' : log.level || 'info';
  106. return `
  107. <div class="log-line">
  108. <span class="log-ts">${escapeLogText(log.time)}</span>
  109. <span class="log-source ${log.source === 'system' ? 'system' : 'task'}">${sourceMap[log.source] || log.source}</span>
  110. <span class="log-lvl ${levelClass}">${level}</span>
  111. <span class="log-msg">${escapeLogText(log.message)}</span>
  112. </div>`;
  113. }).join('');
  114. }
  115. async function refreshLogs() {
  116. const response = await fetch('/api/logs');
  117. if (!response.ok) return;
  118. const data = await response.json();
  119. const logs = (data.logs || []).sort((a, b) => b.seq - a.seq);
  120. const signature = logs.map((log) => `${log.seq}:${log.level}:${log.message}`).join('|');
  121. if (signature === lastLogSignature) return;
  122. lastLogSignature = signature;
  123. renderLogs(logs);
  124. }
  125. function startLogPolling() {
  126. if (logTimer) return;
  127. refreshLogs();
  128. logTimer = setInterval(refreshLogs, 1500);
  129. }
  130. async function clearLogs() {
  131. await fetch('/api/logs/clear', { method: 'POST' });
  132. lastLogSignature = '';
  133. await refreshLogs();
  134. }
  135. function switchView(name) {
  136. for (const [key, el] of Object.entries(views)) {
  137. el.classList.toggle('active', key === name);
  138. }
  139. navItems.forEach((n) => n.classList.toggle('active', n.dataset.view === name));
  140. if (name === 'crawl' && window.shanCrawlInit) window.shanCrawlInit();
  141. if (name === 'gen' && window.shanGenInit) window.shanGenInit();
  142. if (name === 'tasks' && window.shanTasksInit) window.shanTasksInit();
  143. if (name === 'history' && window.shanHistoryInit) window.shanHistoryInit();
  144. if (name === 'settings') initSettings();
  145. }
  146. navItems.forEach((n) => n.addEventListener('click', () => switchView(n.dataset.view)));
  147. switchView('crawl');
  148. startLogPolling();
  149. const splash = document.getElementById('splash');
  150. if (splash) {
  151. setTimeout(() => splash.classList.add('hide'), 1800);
  152. setTimeout(() => splash.remove(), 2400);
  153. }
  154. })();