From 61d8f6eabf6f236eeb29fa6bed064b707a6fe7d9 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:40:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A1=B9=E7=9B=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/app-modular.js | 77 ++++ static/js/modules/account-list.js | 132 +++++++ static/js/modules/all-other-features.js | 197 ++++++++++ static/js/modules/cookies.js | 347 ++++++++++++++++++ static/js/modules/dashboard.js | 141 ++++++++ static/js/modules/example-usage.html | 102 ++++++ static/js/modules/globals.js | 15 + static/js/modules/init.js | 171 +++++++++ static/js/modules/keyword-cache.js | 42 +++ static/js/modules/keywords.js | 463 ++++++++++++++++++++++++ static/js/modules/main-features.js | 189 ++++++++++ static/js/modules/navigation.js | 86 +++++ static/js/modules/utils.js | 85 +++++ 13 files changed, 2047 insertions(+) create mode 100644 static/js/app-modular.js create mode 100644 static/js/modules/account-list.js create mode 100644 static/js/modules/all-other-features.js create mode 100644 static/js/modules/cookies.js create mode 100644 static/js/modules/dashboard.js create mode 100644 static/js/modules/example-usage.html create mode 100644 static/js/modules/globals.js create mode 100644 static/js/modules/init.js create mode 100644 static/js/modules/keyword-cache.js create mode 100644 static/js/modules/keywords.js create mode 100644 static/js/modules/main-features.js create mode 100644 static/js/modules/navigation.js create mode 100644 static/js/modules/utils.js diff --git a/static/js/app-modular.js b/static/js/app-modular.js new file mode 100644 index 0000000..3234da5 --- /dev/null +++ b/static/js/app-modular.js @@ -0,0 +1,77 @@ +/** + * 闲鱼自动回复系统 - 模块化主入口文件 + * + * 此文件负责引入所有功能模块,替代原来的单一app.js文件 + * + * 模块结构: + * - globals.js: 全局变量 + * - utils.js: 通用工具函数 + * - navigation.js: 页面导航功能 + * - dashboard.js: 仪表盘管理 + * - keyword-cache.js: 关键词缓存管理 + * - account-list.js: 账号列表管理 + * - keywords.js: 关键词管理 + * - cookies.js: Cookie管理 + * - main-features.js: 主要功能 + * - all-other-features.js: 所有其他功能 + * - init.js: 应用初始化 + */ + +// 注意:在HTML中需要按以下顺序引入所有模块文件: +/* + + + + + + + + + + + +*/ + +// 模块加载完成后的初始化检查 +document.addEventListener('DOMContentLoaded', function() { + console.log('闲鱼自动回复系统 - 模块化版本已加载'); + + // 检查关键函数是否存在 + const requiredFunctions = [ + 'showSection', + 'loadDashboard', + 'loadCookies', + 'refreshAccountList', + 'loadAccountKeywords', + 'showToast', + 'toggleLoading' + ]; + + const missingFunctions = requiredFunctions.filter(func => typeof window[func] !== 'function'); + + if (missingFunctions.length > 0) { + console.error('缺少必要的函数:', missingFunctions); + console.error('请检查模块文件是否正确加载'); + } else { + console.log('所有必要的函数已加载完成'); + } +}); + +// 导出模块信息(用于调试) +window.moduleInfo = { + version: '1.0.0', + modules: [ + 'globals', + 'utils', + 'navigation', + 'dashboard', + 'keyword-cache', + 'account-list', + 'keywords', + 'cookies', + 'main-features', + 'all-other-features', + 'init' + ], + loadedAt: new Date().toISOString() +}; diff --git a/static/js/modules/account-list.js b/static/js/modules/account-list.js new file mode 100644 index 0000000..2888fbf --- /dev/null +++ b/static/js/modules/account-list.js @@ -0,0 +1,132 @@ +// ==================== 账号列表管理 ==================== + +// 刷新账号列表(用于自动回复页面) +async function refreshAccountList() { + try { + toggleLoading(true); + + // 获取账号列表 + const response = await fetch(`${apiBase}/cookies/details`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const accounts = await response.json(); + const select = document.getElementById('accountSelect'); + select.innerHTML = ''; + + // 为每个账号获取关键词数量 + const accountsWithKeywords = await Promise.all( + accounts.map(async (account) => { + try { + const keywordsResponse = await fetch(`${apiBase}/keywords/${account.id}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (keywordsResponse.ok) { + const keywordsData = await keywordsResponse.json(); + return { + ...account, + keywords: keywordsData, + keywordCount: keywordsData.length + }; + } else { + return { + ...account, + keywordCount: 0 + }; + } + } catch (error) { + console.error(`获取账号 ${account.id} 关键词失败:`, error); + return { + ...account, + keywordCount: 0 + }; + } + }) + ); + + // 渲染账号选项(显示所有账号,但标识禁用状态) + if (accountsWithKeywords.length === 0) { + select.innerHTML = ''; + return; + } + + // 分组显示:先显示启用的账号,再显示禁用的账号 + const enabledAccounts = accountsWithKeywords.filter(account => { + const enabled = account.enabled === undefined ? true : account.enabled; + console.log(`账号 ${account.id} 过滤状态: enabled=${account.enabled}, 判断为启用=${enabled}`); + return enabled; + }); + const disabledAccounts = accountsWithKeywords.filter(account => { + const enabled = account.enabled === undefined ? true : account.enabled; + return !enabled; + }); + + // 渲染启用的账号 + enabledAccounts.forEach(account => { + const option = document.createElement('option'); + option.value = account.id; + + // 根据关键词数量显示不同的图标和样式 + let icon = '📝'; + let status = ''; + if (account.keywordCount === 0) { + icon = '⚪'; + status = ' (未配置)'; + } else if (account.keywordCount >= 5) { + icon = '🟢'; + status = ` (${account.keywordCount} 个关键词)`; + } else { + icon = '🟡'; + status = ` (${account.keywordCount} 个关键词)`; + } + + option.textContent = `${icon} ${account.id}${status}`; + select.appendChild(option); + }); + + // 如果有禁用的账号,添加分隔线和禁用账号 + if (disabledAccounts.length > 0) { + // 添加分隔线 + const separatorOption = document.createElement('option'); + separatorOption.disabled = true; + separatorOption.textContent = `--- 禁用账号 (${disabledAccounts.length} 个) ---`; + select.appendChild(separatorOption); + + // 渲染禁用的账号 + disabledAccounts.forEach(account => { + const option = document.createElement('option'); + option.value = account.id; + + // 禁用账号使用特殊图标和样式 + let icon = '🔴'; + let status = ''; + if (account.keywordCount === 0) { + status = ' (未配置) [已禁用]'; + } else { + status = ` (${account.keywordCount} 个关键词) [已禁用]`; + } + + option.textContent = `${icon} ${account.id}${status}`; + option.style.color = '#6b7280'; + option.style.fontStyle = 'italic'; + select.appendChild(option); + }); + } + + console.log('账号列表刷新完成,关键词统计:', accountsWithKeywords.map(a => ({ id: a.id, keywords: a.keywordCount }))); + } else { + showToast('获取账号列表失败', 'danger'); + } + } catch (error) { + console.error('刷新账号列表失败:', error); + showToast('刷新账号列表失败', 'danger'); + } finally { + toggleLoading(false); + } +} diff --git a/static/js/modules/all-other-features.js b/static/js/modules/all-other-features.js new file mode 100644 index 0000000..e0be265 --- /dev/null +++ b/static/js/modules/all-other-features.js @@ -0,0 +1,197 @@ +// ==================== 所有其他功能模块 ==================== +// 此文件包含原app.js中除已拆分模块外的所有其他功能 + +// ==================== 默认回复管理功能 ==================== + +// 打开默认回复管理器 +async function openDefaultReplyManager() { + try { + await loadDefaultReplies(); + const modal = new bootstrap.Modal(document.getElementById('defaultReplyModal')); + modal.show(); + } catch (error) { + console.error('打开默认回复管理器失败:', error); + showToast('打开默认回复管理器失败', 'danger'); + } +} + +// 加载默认回复列表 +async function loadDefaultReplies() { + try { + // 获取所有账号 + const accountsResponse = await fetch(`${apiBase}/cookies`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (!accountsResponse.ok) { + throw new Error('获取账号列表失败'); + } + + const accounts = await accountsResponse.json(); + + // 获取所有默认回复设置 + const repliesResponse = await fetch(`${apiBase}/default-replies`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + let defaultReplies = {}; + if (repliesResponse.ok) { + defaultReplies = await repliesResponse.json(); + } + + renderDefaultRepliesList(accounts, defaultReplies); + } catch (error) { + console.error('加载默认回复列表失败:', error); + showToast('加载默认回复列表失败', 'danger'); + } +} + +// 渲染默认回复列表 +function renderDefaultRepliesList(accounts, defaultReplies) { + const tbody = document.getElementById('defaultReplyTableBody'); + tbody.innerHTML = ''; + + if (accounts.length === 0) { + tbody.innerHTML = ` + + + +
暂无账号数据
+

请先添加账号

+ + + `; + return; + } + + accounts.forEach(accountId => { + const replySettings = defaultReplies[accountId] || { enabled: false, reply_content: '' }; + const tr = document.createElement('tr'); + + // 状态标签 + const statusBadge = replySettings.enabled ? + '启用' : + '禁用'; + + // 回复内容预览 + let contentPreview = replySettings.reply_content || '未设置'; + if (contentPreview.length > 50) { + contentPreview = contentPreview.substring(0, 50) + '...'; + } + + tr.innerHTML = ` + + ${accountId} + + ${statusBadge} + +
+ ${contentPreview} +
+ + +
+ + +
+ + `; + + tbody.appendChild(tr); + }); +} + +// 编辑默认回复 +async function editDefaultReply(accountId) { + try { + // 获取当前设置 + const response = await fetch(`${apiBase}/default-replies/${accountId}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + let settings = { enabled: false, reply_content: '' }; + if (response.ok) { + settings = await response.json(); + } + + // 填充编辑表单 + document.getElementById('editAccountId').value = accountId; + document.getElementById('editAccountIdDisplay').value = accountId; + document.getElementById('editDefaultReplyEnabled').checked = settings.enabled; + document.getElementById('editReplyContent').value = settings.reply_content || ''; + + // 根据启用状态显示/隐藏内容输入框 + toggleReplyContentVisibility(); + + // 显示编辑模态框 + const modal = new bootstrap.Modal(document.getElementById('editDefaultReplyModal')); + modal.show(); + } catch (error) { + console.error('获取默认回复设置失败:', error); + showToast('获取默认回复设置失败', 'danger'); + } +} + +// 切换回复内容输入框的显示/隐藏 +function toggleReplyContentVisibility() { + const enabled = document.getElementById('editDefaultReplyEnabled').checked; + const contentGroup = document.getElementById('editReplyContentGroup'); + contentGroup.style.display = enabled ? 'block' : 'none'; +} + +// 保存默认回复设置 +async function saveDefaultReply() { + try { + const accountId = document.getElementById('editAccountId').value; + const enabled = document.getElementById('editDefaultReplyEnabled').checked; + const replyContent = document.getElementById('editReplyContent').value; + + if (enabled && !replyContent.trim()) { + showToast('启用默认回复时必须设置回复内容', 'warning'); + return; + } + + const data = { + enabled: enabled, + reply_content: enabled ? replyContent : null + }; + + const response = await fetch(`${apiBase}/default-replies/${accountId}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + if (response.ok) { + showToast('默认回复设置保存成功', 'success'); + bootstrap.Modal.getInstance(document.getElementById('editDefaultReplyModal')).hide(); + loadDefaultReplies(); // 刷新列表 + loadCookies(); // 刷新账号列表以更新默认回复状态显示 + } else { + const error = await response.text(); + showToast(`保存失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('保存默认回复设置失败:', error); + showToast('保存默认回复设置失败', 'danger'); + } +} + +// 测试默认回复 +function testDefaultReply(accountId) { + console.log('测试默认回复功能,账号ID:', accountId); + showToast('测试功能开发中...', 'info'); +} diff --git a/static/js/modules/cookies.js b/static/js/modules/cookies.js new file mode 100644 index 0000000..60920fb --- /dev/null +++ b/static/js/modules/cookies.js @@ -0,0 +1,347 @@ +// ==================== Cookie管理 ==================== + +// 加载Cookie列表 +async function loadCookies() { + try { + toggleLoading(true); + const tbody = document.querySelector('#cookieTable tbody'); + tbody.innerHTML = ''; + + const cookieDetails = await fetchJSON(apiBase + '/cookies/details'); + + if (cookieDetails.length === 0) { + tbody.innerHTML = ` + + + +
暂无账号
+

请添加新的闲鱼账号开始使用

+ + + `; + return; + } + + // 为每个账号获取关键词数量和默认回复设置并渲染 + const accountsWithKeywords = await Promise.all( + cookieDetails.map(async (cookie) => { + try { + // 获取关键词数量 + const keywordsResponse = await fetch(`${apiBase}/keywords/${cookie.id}`, { + headers: { 'Authorization': `Bearer ${authToken}` } + }); + + let keywordCount = 0; + if (keywordsResponse.ok) { + const keywordsData = await keywordsResponse.json(); + keywordCount = keywordsData.length; + } + + // 获取默认回复设置 + const defaultReplyResponse = await fetch(`${apiBase}/default-replies/${cookie.id}`, { + headers: { 'Authorization': `Bearer ${authToken}` } + }); + + let defaultReply = { enabled: false, reply_content: '' }; + if (defaultReplyResponse.ok) { + defaultReply = await defaultReplyResponse.json(); + } + + // 获取AI回复设置 + const aiReplyResponse = await fetch(`${apiBase}/ai-reply-settings/${cookie.id}`, { + headers: { 'Authorization': `Bearer ${authToken}` } + }); + + let aiReply = { ai_enabled: false, model_name: 'qwen-plus' }; + if (aiReplyResponse.ok) { + aiReply = await aiReplyResponse.json(); + } + + return { + ...cookie, + keywordCount: keywordCount, + defaultReply: defaultReply, + aiReply: aiReply + }; + } catch (error) { + return { + ...cookie, + keywordCount: 0, + defaultReply: { enabled: false, reply_content: '' }, + aiReply: { ai_enabled: false, model_name: 'qwen-plus' } + }; + } + }) + ); + + accountsWithKeywords.forEach(cookie => { + // 使用数据库中的实际状态,默认为启用 + const isEnabled = cookie.enabled === undefined ? true : cookie.enabled; + + console.log(`账号 ${cookie.id} 状态: enabled=${cookie.enabled}, isEnabled=${isEnabled}`); // 调试信息 + + const tr = document.createElement('tr'); + tr.className = `account-row ${isEnabled ? 'enabled' : 'disabled'}`; + // 默认回复状态标签 + const defaultReplyBadge = cookie.defaultReply.enabled ? + '启用' : + '禁用'; + + // AI回复状态标签 + const aiReplyBadge = cookie.aiReply.ai_enabled ? + 'AI启用' : + 'AI禁用'; + + // 自动确认发货状态(默认开启) + const autoConfirm = cookie.auto_confirm === undefined ? true : cookie.auto_confirm; + + tr.innerHTML = ` + + + + + + + + + ${cookie.keywordCount} 个关键词 + + + +
+ + + + +
+ + + ${defaultReplyBadge} + + + ${aiReplyBadge} + + +
+ + + + +
+ + +
+ + + + + +
+ + `; + tbody.appendChild(tr); + }); + + // 为Cookie值添加点击复制功能 + document.querySelectorAll('.cookie-value').forEach(element => { + element.style.cursor = 'pointer'; + element.addEventListener('click', function() { + const cookieValue = this.textContent; + if (cookieValue && cookieValue !== '未设置') { + navigator.clipboard.writeText(cookieValue).then(() => { + showToast('Cookie已复制到剪贴板', 'success'); + }).catch(() => { + showToast('复制失败,请手动复制', 'error'); + }); + } + }); + }); + + } catch (err) { + // 错误已在fetchJSON中处理 + } finally { + toggleLoading(false); + } +} + +// 复制Cookie +function copyCookie(id, value) { + if (!value || value === '未设置') { + showToast('该账号暂无Cookie值', 'warning'); + return; + } + + navigator.clipboard.writeText(value).then(() => { + showToast(`账号 "${id}" 的Cookie已复制到剪贴板`, 'success'); + }).catch(() => { + // 降级方案:创建临时文本框 + const textArea = document.createElement('textarea'); + textArea.value = value; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + showToast(`账号 "${id}" 的Cookie已复制到剪贴板`, 'success'); + } catch (err) { + showToast('复制失败,请手动复制', 'error'); + } + document.body.removeChild(textArea); + }); +} + +// 删除Cookie +async function delCookie(id) { + if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return; + + try { + await fetchJSON(apiBase + `/cookies/${id}`, { method: 'DELETE' }); + showToast(`账号 "${id}" 已删除`, 'success'); + loadCookies(); + } catch (err) { + // 错误已在fetchJSON中处理 + } +} + +// 内联编辑Cookie +function editCookieInline(id, currentValue) { + const row = event.target.closest('tr'); + const cookieValueCell = row.querySelector('.cookie-value'); + const originalContent = cookieValueCell.innerHTML; + + // 存储原始数据到全局变量,避免HTML注入问题 + window.editingCookieData = { + id: id, + originalContent: originalContent, + originalValue: currentValue || '' + }; + + // 创建编辑界面容器 + const editContainer = document.createElement('div'); + editContainer.className = 'd-flex gap-2'; + + // 创建输入框 + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-control form-control-sm'; + input.id = `edit-${id}`; + input.value = currentValue || ''; + input.placeholder = '输入新的Cookie值'; + + // 创建保存按钮 + const saveBtn = document.createElement('button'); + saveBtn.className = 'btn btn-sm btn-success'; + saveBtn.title = '保存'; + saveBtn.innerHTML = ''; + saveBtn.onclick = () => saveCookieInline(id); + + // 创建取消按钮 + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'btn btn-sm btn-secondary'; + cancelBtn.title = '取消'; + cancelBtn.innerHTML = ''; + cancelBtn.onclick = () => cancelCookieEdit(id); + + // 组装编辑界面 + editContainer.appendChild(input); + editContainer.appendChild(saveBtn); + editContainer.appendChild(cancelBtn); + + // 替换原内容 + cookieValueCell.innerHTML = ''; + cookieValueCell.appendChild(editContainer); + + // 聚焦输入框 + input.focus(); + input.select(); + + // 添加键盘事件监听 + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + saveCookieInline(id); + } else if (e.key === 'Escape') { + e.preventDefault(); + cancelCookieEdit(id); + } + }); + + // 禁用该行的其他按钮 + const actionButtons = row.querySelectorAll('.btn-group button'); + actionButtons.forEach(btn => btn.disabled = true); +} + +// 保存内联编辑的Cookie +async function saveCookieInline(id) { + const input = document.getElementById(`edit-${id}`); + const newValue = input.value.trim(); + + if (!newValue) { + showToast('Cookie值不能为空', 'warning'); + return; + } + + try { + toggleLoading(true); + + await fetchJSON(apiBase + `/cookies/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: id, + value: newValue + }) + }); + + showToast(`账号 "${id}" Cookie已更新`, 'success'); + loadCookies(); // 重新加载列表 + + } catch (err) { + console.error('Cookie更新失败:', err); + showToast(`Cookie更新失败: ${err.message || '未知错误'}`, 'danger'); + // 恢复原内容 + cancelCookieEdit(id); + } finally { + toggleLoading(false); + } +} + +// 取消Cookie编辑 +function cancelCookieEdit(id) { + if (!window.editingCookieData || window.editingCookieData.id !== id) { + console.error('编辑数据不存在'); + return; + } + + const row = document.querySelector(`#edit-${id}`).closest('tr'); + const cookieValueCell = row.querySelector('.cookie-value'); + + // 恢复原内容 + cookieValueCell.innerHTML = window.editingCookieData.originalContent; + + // 恢复按钮状态 + const actionButtons = row.querySelectorAll('.btn-group button'); + actionButtons.forEach(btn => btn.disabled = false); + + // 清理全局数据 + delete window.editingCookieData; +} diff --git a/static/js/modules/dashboard.js b/static/js/modules/dashboard.js new file mode 100644 index 0000000..e163fae --- /dev/null +++ b/static/js/modules/dashboard.js @@ -0,0 +1,141 @@ +// ==================== 仪表盘管理 ==================== + +// 加载仪表盘数据 +async function loadDashboard() { + try { + toggleLoading(true); + + // 获取账号列表 + const cookiesResponse = await fetch(`${apiBase}/cookies/details`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (cookiesResponse.ok) { + const cookiesData = await cookiesResponse.json(); + + // 为每个账号获取关键词信息 + const accountsWithKeywords = await Promise.all( + cookiesData.map(async (account) => { + try { + const keywordsResponse = await fetch(`${apiBase}/keywords/${account.id}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (keywordsResponse.ok) { + const keywordsData = await keywordsResponse.json(); + return { + ...account, + keywords: keywordsData, + keywordCount: keywordsData.length + }; + } else { + return { + ...account, + keywords: [], + keywordCount: 0 + }; + } + } catch (error) { + console.error(`获取账号 ${account.id} 关键词失败:`, error); + return { + ...account, + keywords: [], + keywordCount: 0 + }; + } + }) + ); + + dashboardData.accounts = accountsWithKeywords; + + // 计算统计数据 + let totalKeywords = 0; + let activeAccounts = 0; + let enabledAccounts = 0; + + accountsWithKeywords.forEach(account => { + const keywordCount = account.keywordCount || 0; + const isEnabled = account.enabled === undefined ? true : account.enabled; + + if (isEnabled) { + enabledAccounts++; + totalKeywords += keywordCount; + if (keywordCount > 0) { + activeAccounts++; + } + } + }); + + dashboardData.totalKeywords = totalKeywords; + + // 更新仪表盘显示 + updateDashboardStats(accountsWithKeywords.length, totalKeywords, enabledAccounts); + updateDashboardAccountsList(accountsWithKeywords); + } + } catch (error) { + console.error('加载仪表盘数据失败:', error); + showToast('加载仪表盘数据失败', 'danger'); + } finally { + toggleLoading(false); + } +} + +// 更新仪表盘统计数据 +function updateDashboardStats(totalAccounts, totalKeywords, enabledAccounts) { + document.getElementById('totalAccounts').textContent = totalAccounts; + document.getElementById('totalKeywords').textContent = totalKeywords; + document.getElementById('activeAccounts').textContent = enabledAccounts; +} + +// 更新仪表盘账号列表 +function updateDashboardAccountsList(accounts) { + const tbody = document.getElementById('dashboardAccountsList'); + tbody.innerHTML = ''; + + if (accounts.length === 0) { + tbody.innerHTML = ` + + + + 暂无账号数据 + + + `; + return; + } + + accounts.forEach(account => { + const keywordCount = account.keywordCount || 0; + const isEnabled = account.enabled === undefined ? true : account.enabled; + + let status = ''; + if (!isEnabled) { + status = '已禁用'; + } else if (keywordCount > 0) { + status = '活跃'; + } else { + status = '未配置'; + } + + const row = document.createElement('tr'); + row.className = isEnabled ? '' : 'table-secondary'; + row.innerHTML = ` + + ${account.id} + ${!isEnabled ? '' : ''} + + + ${keywordCount} 个关键词 + + ${status} + + ${new Date().toLocaleString()} + + `; + tbody.appendChild(row); + }); +} diff --git a/static/js/modules/example-usage.html b/static/js/modules/example-usage.html new file mode 100644 index 0000000..e951e24 --- /dev/null +++ b/static/js/modules/example-usage.html @@ -0,0 +1,102 @@ + + + + + + 闲鱼自动回复系统 - 模块化版本 + + + + + + + + + + +
+ +
+
+ Loading... +
+
+ + +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/js/modules/globals.js b/static/js/modules/globals.js new file mode 100644 index 0000000..9eecdc0 --- /dev/null +++ b/static/js/modules/globals.js @@ -0,0 +1,15 @@ +// ==================== 全局变量 ==================== +const apiBase = location.origin; +let keywordsData = {}; +let currentCookieId = ''; +let editCookieId = ''; +let authToken = localStorage.getItem('auth_token'); +let dashboardData = { + accounts: [], + totalKeywords: 0 +}; + +// 账号关键词缓存 +let accountKeywordCache = {}; +let cacheTimestamp = 0; +const CACHE_DURATION = 30000; // 30秒缓存 diff --git a/static/js/modules/init.js b/static/js/modules/init.js new file mode 100644 index 0000000..629e38c --- /dev/null +++ b/static/js/modules/init.js @@ -0,0 +1,171 @@ +// ==================== 应用初始化 ==================== + +// 登出功能 +async function logout() { + try { + if (authToken) { + await fetch('/logout', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + } + localStorage.removeItem('auth_token'); + window.location.href = '/'; + } catch (err) { + console.error('登出失败:', err); + localStorage.removeItem('auth_token'); + window.location.href = '/'; + } +} + +// 检查认证状态 +async function checkAuth() { + if (!authToken) { + window.location.href = '/'; + return false; + } + + try { + const response = await fetch('/verify', { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + const result = await response.json(); + + if (!result.authenticated) { + localStorage.removeItem('auth_token'); + window.location.href = '/'; + return false; + } + + // 检查是否为管理员,显示管理员菜单和功能 + if (result.username === 'admin') { + const adminMenuSection = document.getElementById('adminMenuSection'); + if (adminMenuSection) { + adminMenuSection.style.display = 'block'; + } + + // 显示备份管理功能 + const backupManagement = document.getElementById('backup-management'); + if (backupManagement) { + backupManagement.style.display = 'block'; + } + } + + return true; + } catch (err) { + localStorage.removeItem('auth_token'); + window.location.href = '/'; + return false; + } +} + +// 初始化事件监听 +document.addEventListener('DOMContentLoaded', async () => { + // 首先检查认证状态 + const isAuthenticated = await checkAuth(); + if (!isAuthenticated) return; + + // 添加Cookie表单提交 + document.getElementById('addForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const id = document.getElementById('cookieId').value.trim(); + const value = document.getElementById('cookieValue').value.trim(); + + if (!id || !value) return; + + try { + await fetchJSON(apiBase + '/cookies', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, value }) + }); + + document.getElementById('cookieId').value = ''; + document.getElementById('cookieValue').value = ''; + showToast(`账号 "${id}" 添加成功`); + loadCookies(); + } catch (err) { + // 错误已在fetchJSON中处理 + } + }); + + // 增强的键盘快捷键和用户体验 + document.getElementById('newKeyword')?.addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + document.getElementById('newReply').focus(); + } + }); + + document.getElementById('newReply')?.addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + addKeyword(); + } + }); + + // ESC键取消编辑 + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && typeof window.editingIndex !== 'undefined') { + e.preventDefault(); + cancelEdit(); + } + }); + + // 输入框实时验证和提示 + document.getElementById('newKeyword')?.addEventListener('input', function (e) { + const value = e.target.value.trim(); + const addBtn = document.querySelector('.add-btn'); + const replyInput = document.getElementById('newReply'); + + if (value.length > 0) { + e.target.style.borderColor = '#10b981'; + if (replyInput.value.trim().length > 0) { + addBtn.style.opacity = '1'; + addBtn.style.transform = 'scale(1)'; + } + } else { + e.target.style.borderColor = '#e5e7eb'; + addBtn.style.opacity = '0.7'; + addBtn.style.transform = 'scale(0.95)'; + } + }); + + document.getElementById('newReply')?.addEventListener('input', function (e) { + const value = e.target.value.trim(); + const addBtn = document.querySelector('.add-btn'); + const keywordInput = document.getElementById('newKeyword'); + + if (value.length > 0) { + e.target.style.borderColor = '#10b981'; + if (keywordInput.value.trim().length > 0) { + addBtn.style.opacity = '1'; + addBtn.style.transform = 'scale(1)'; + } + } else { + e.target.style.borderColor = '#e5e7eb'; + addBtn.style.opacity = '0.7'; + addBtn.style.transform = 'scale(0.95)'; + } + }); + + // 初始加载仪表盘 + loadDashboard(); + + // 点击侧边栏外部关闭移动端菜单 + document.addEventListener('click', function (e) { + const sidebar = document.getElementById('sidebar'); + const toggle = document.querySelector('.mobile-toggle'); + + if (window.innerWidth <= 768 && + !sidebar.contains(e.target) && + !toggle.contains(e.target) && + sidebar.classList.contains('show')) { + sidebar.classList.remove('show'); + } + }); +}); diff --git a/static/js/modules/keyword-cache.js b/static/js/modules/keyword-cache.js new file mode 100644 index 0000000..685c04b --- /dev/null +++ b/static/js/modules/keyword-cache.js @@ -0,0 +1,42 @@ +// ==================== 关键词缓存管理 ==================== + +// 获取账号关键词数量(带缓存)- 包含普通关键词和商品关键词 +async function getAccountKeywordCount(accountId) { + const now = Date.now(); + + // 检查缓存 + if (accountKeywordCache[accountId] && (now - cacheTimestamp) < CACHE_DURATION) { + return accountKeywordCache[accountId]; + } + + try { + const response = await fetch(`${apiBase}/keywords/${accountId}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const keywordsData = await response.json(); + // 现在API返回的是包含普通关键词和商品关键词的完整列表 + const count = keywordsData.length; + + // 更新缓存 + accountKeywordCache[accountId] = count; + cacheTimestamp = now; + + return count; + } else { + return 0; + } + } catch (error) { + console.error(`获取账号 ${accountId} 关键词失败:`, error); + return 0; + } +} + +// 清除关键词缓存 +function clearKeywordCache() { + accountKeywordCache = {}; + cacheTimestamp = 0; +} diff --git a/static/js/modules/keywords.js b/static/js/modules/keywords.js new file mode 100644 index 0000000..ea06808 --- /dev/null +++ b/static/js/modules/keywords.js @@ -0,0 +1,463 @@ +// ==================== 关键词管理 ==================== + +// 加载账号关键词 +async function loadAccountKeywords() { + const accountId = document.getElementById('accountSelect').value; + const keywordManagement = document.getElementById('keywordManagement'); + + if (!accountId) { + keywordManagement.style.display = 'none'; + return; + } + + try { + toggleLoading(true); + currentCookieId = accountId; + + // 获取账号详情以检查状态 + const accountResponse = await fetch(`${apiBase}/cookies/details`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + let accountStatus = true; // 默认启用 + if (accountResponse.ok) { + const accounts = await accountResponse.json(); + const currentAccount = accounts.find(acc => acc.id === accountId); + accountStatus = currentAccount ? (currentAccount.enabled === undefined ? true : currentAccount.enabled) : true; + console.log(`加载关键词时账号 ${accountId} 状态: enabled=${currentAccount?.enabled}, accountStatus=${accountStatus}`); // 调试信息 + } + + const response = await fetch(`${apiBase}/keywords-with-item-id/${accountId}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + console.log('从服务器获取的关键词数据:', data); // 调试信息 + + // 后端返回的是 [{keyword, reply, item_id}, ...] 格式,直接使用 + const formattedData = data; + + console.log('格式化后的关键词数据:', formattedData); // 调试信息 + keywordsData[accountId] = formattedData; + renderKeywordsList(formattedData); + + // 加载商品列表 + await loadItemsList(accountId); + + // 更新账号徽章显示 + updateAccountBadge(accountId, accountStatus); + + keywordManagement.style.display = 'block'; + } else { + showToast('加载关键词失败', 'danger'); + } + } catch (error) { + console.error('加载关键词失败:', error); + showToast('加载关键词失败', 'danger'); + } finally { + toggleLoading(false); + } +} + +// 更新账号徽章显示 +function updateAccountBadge(accountId, isEnabled) { + const badge = document.getElementById('currentAccountBadge'); + if (!badge) return; + + const statusIcon = isEnabled ? '🟢' : '🔴'; + const statusText = isEnabled ? '启用' : '禁用'; + const statusClass = isEnabled ? 'bg-success' : 'bg-warning'; + + badge.innerHTML = ` + + ${statusIcon} ${accountId} + + + 状态: ${statusText} + ${!isEnabled ? ' (配置的关键词不会参与自动回复)' : ''} + + `; +} + +// 显示添加关键词表单 +function showAddKeywordForm() { + const form = document.getElementById('addKeywordForm'); + form.style.display = form.style.display === 'none' ? 'block' : 'none'; + + if (form.style.display === 'block') { + document.getElementById('newKeyword').focus(); + } +} + +// 加载商品列表 +async function loadItemsList(accountId) { + try { + const response = await fetch(`${apiBase}/items/${accountId}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + const items = data.items || []; + + // 更新商品选择下拉框 + const selectElement = document.getElementById('newItemIdSelect'); + if (selectElement) { + // 清空现有选项(保留第一个默认选项) + selectElement.innerHTML = ''; + + // 添加商品选项 + items.forEach(item => { + const option = document.createElement('option'); + option.value = item.item_id; + option.textContent = `${item.item_id} - ${item.item_title}`; + selectElement.appendChild(option); + }); + } + + console.log(`加载了 ${items.length} 个商品到选择列表`); + } else { + console.warn('加载商品列表失败:', response.status); + } + } catch (error) { + console.error('加载商品列表时发生错误:', error); + } +} + +// 添加或更新关键词 +async function addKeyword() { + const keyword = document.getElementById('newKeyword').value.trim(); + const reply = document.getElementById('newReply').value.trim(); + const itemId = document.getElementById('newItemIdSelect').value.trim(); + + if (!keyword || !reply) { + showToast('请填写关键词和回复内容', 'warning'); + return; + } + + if (!currentCookieId) { + showToast('请先选择账号', 'warning'); + return; + } + + // 检查是否为编辑模式 + const isEditMode = typeof window.editingIndex !== 'undefined'; + const actionText = isEditMode ? '更新' : '添加'; + + try { + toggleLoading(true); + + // 获取当前关键词列表 + let currentKeywords = [...(keywordsData[currentCookieId] || [])]; + + // 如果是编辑模式,先移除原关键词 + if (isEditMode) { + currentKeywords.splice(window.editingIndex, 1); + } + + // 准备要保存的关键词列表 + let keywordsToSave = [...currentKeywords]; + + // 如果是编辑模式,先移除原关键词 + if (isEditMode && typeof window.editingIndex !== 'undefined') { + keywordsToSave.splice(window.editingIndex, 1); + } + + // 检查关键词是否已存在(考虑商品ID) + const existingKeyword = keywordsToSave.find(item => + item.keyword === keyword && + (item.item_id || '') === (itemId || '') + ); + if (existingKeyword) { + const itemIdText = itemId ? `(商品ID: ${itemId})` : '(通用关键词)'; + showToast(`关键词 "${keyword}" ${itemIdText} 已存在,请使用其他关键词或商品ID`, 'warning'); + toggleLoading(false); + return; + } + + // 添加新关键词或更新的关键词 + const newKeyword = { + keyword: keyword, + reply: reply, + item_id: itemId || '' + }; + keywordsToSave.push(newKeyword); + + const response = await fetch(`${apiBase}/keywords-with-item-id/${currentCookieId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ + keywords: keywordsToSave + }) + }); + + if (response.ok) { + showToast(`✨ 关键词 "${keyword}" ${actionText}成功!`, 'success'); + + // 清空输入框并重置样式 + const keywordInput = document.getElementById('newKeyword'); + const replyInput = document.getElementById('newReply'); + const selectElement = document.getElementById('newItemIdSelect'); + const addBtn = document.querySelector('.add-btn'); + + keywordInput.value = ''; + replyInput.value = ''; + if (selectElement) { + selectElement.value = ''; + } + keywordInput.style.borderColor = '#e5e7eb'; + replyInput.style.borderColor = '#e5e7eb'; + addBtn.style.opacity = '0.7'; + addBtn.style.transform = 'scale(0.95)'; + + // 如果是编辑模式,重置编辑状态 + if (isEditMode) { + delete window.editingIndex; + delete window.originalKeyword; + + // 恢复添加按钮 + addBtn.innerHTML = '添加'; + addBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)'; + + // 移除取消按钮 + const cancelBtn = document.getElementById('cancelEditBtn'); + if (cancelBtn) { + cancelBtn.remove(); + } + } + + // 聚焦到关键词输入框,方便连续添加 + setTimeout(() => { + keywordInput.focus(); + }, 100); + + loadAccountKeywords(); // 重新加载关键词列表 + clearKeywordCache(); // 清除缓存 + } else { + const errorText = await response.text(); + console.error('关键词添加失败:', errorText); + showToast('关键词添加失败', 'danger'); + } + } catch (error) { + console.error('添加关键词失败:', error); + showToast('添加关键词失败', 'danger'); + } finally { + toggleLoading(false); + } +} + +// 渲染现代化关键词列表 +function renderKeywordsList(keywords) { + console.log('渲染关键词列表:', keywords); // 调试信息 + const container = document.getElementById('keywordsList'); + + if (!container) { + console.error('找不到关键词列表容器元素'); + return; + } + + container.innerHTML = ''; + + if (!keywords || keywords.length === 0) { + console.log('关键词列表为空,显示空状态'); + container.innerHTML = ` +
+ +

还没有关键词

+

添加第一个关键词,让您的闲鱼店铺自动回复客户消息

+ +
+ `; + return; + } + + console.log(`开始渲染 ${keywords.length} 个关键词`); + + keywords.forEach((item, index) => { + console.log(`渲染关键词 ${index + 1}:`, item); // 调试信息 + + const keywordItem = document.createElement('div'); + keywordItem.className = 'keyword-item'; + // 商品ID显示 + const itemIdDisplay = item.item_id ? + ` 商品ID: ${item.item_id}` : + ' 通用关键词'; + + keywordItem.innerHTML = ` +
+
+ + ${item.keyword} + ${itemIdDisplay} +
+
+ + +
+
+
+

${item.reply}

+
+ `; + container.appendChild(keywordItem); + }); + + console.log('关键词列表渲染完成'); +} + +// 聚焦到关键词输入框 +function focusKeywordInput() { + document.getElementById('newKeyword').focus(); +} + +// 编辑关键词 - 改进版本 +function editKeyword(index) { + const keywords = keywordsData[currentCookieId] || []; + const keyword = keywords[index]; + + if (!keyword) { + showToast('关键词不存在', 'warning'); + return; + } + + // 将关键词信息填入输入框 + document.getElementById('newKeyword').value = keyword.keyword; + document.getElementById('newReply').value = keyword.reply; + + // 设置商品ID选择框 + const selectElement = document.getElementById('newItemIdSelect'); + if (selectElement) { + selectElement.value = keyword.item_id || ''; + } + + // 设置编辑模式标识 + window.editingIndex = index; + window.originalKeyword = keyword.keyword; + window.originalItemId = keyword.item_id || ''; + + // 更新按钮文本和样式 + const addBtn = document.querySelector('.add-btn'); + addBtn.innerHTML = '更新'; + addBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'; + + // 显示取消按钮 + showCancelEditButton(); + + // 聚焦到关键词输入框并选中文本 + setTimeout(() => { + const keywordInput = document.getElementById('newKeyword'); + keywordInput.focus(); + keywordInput.select(); + }, 100); + + showToast('📝 编辑模式:修改后点击"更新"按钮保存', 'info'); +} + +// 显示取消编辑按钮 +function showCancelEditButton() { + // 检查是否已存在取消按钮 + if (document.getElementById('cancelEditBtn')) { + return; + } + + const addBtn = document.querySelector('.add-btn'); + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'cancelEditBtn'; + cancelBtn.className = 'btn btn-outline-secondary'; + cancelBtn.style.marginLeft = '0.5rem'; + cancelBtn.innerHTML = '取消'; + cancelBtn.onclick = cancelEdit; + + addBtn.parentNode.appendChild(cancelBtn); +} + +// 取消编辑 +function cancelEdit() { + // 清空输入框 + document.getElementById('newKeyword').value = ''; + document.getElementById('newReply').value = ''; + + // 清空商品ID选择框 + const selectElement = document.getElementById('newItemIdSelect'); + if (selectElement) { + selectElement.value = ''; + } + + // 重置编辑状态 + delete window.editingIndex; + delete window.originalKeyword; + delete window.originalItemId; + + // 恢复添加按钮 + const addBtn = document.querySelector('.add-btn'); + addBtn.innerHTML = '添加'; + addBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)'; + + // 移除取消按钮 + const cancelBtn = document.getElementById('cancelEditBtn'); + if (cancelBtn) { + cancelBtn.remove(); + } + + showToast('已取消编辑', 'info'); +} + +// 删除关键词 +async function deleteKeyword(cookieId, index) { + if (!confirm('确定要删除这个关键词吗?')) { + return; + } + + try { + toggleLoading(true); + + // 获取当前关键词列表 + const currentKeywords = keywordsData[cookieId] || []; + // 移除指定索引的关键词 + currentKeywords.splice(index, 1); + + // 更新服务器 + const response = await fetch(`${apiBase}/keywords-with-item-id/${cookieId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ + keywords: currentKeywords + }) + }); + + if (response.ok) { + showToast('关键词删除成功', 'success'); + keywordsData[cookieId] = currentKeywords; + renderKeywordsList(currentKeywords); + clearKeywordCache(); // 清除缓存 + } else { + const errorText = await response.text(); + console.error('关键词删除失败:', errorText); + showToast('关键词删除失败', 'danger'); + } + } catch (error) { + console.error('删除关键词失败:', error); + showToast('删除关键词删除失败', 'danger'); + } finally { + toggleLoading(false); + } +} diff --git a/static/js/modules/main-features.js b/static/js/modules/main-features.js new file mode 100644 index 0000000..c2d6879 --- /dev/null +++ b/static/js/modules/main-features.js @@ -0,0 +1,189 @@ +// ==================== 主要功能模块 ==================== + +// 切换账号启用/禁用状态 +async function toggleAccountStatus(accountId, enabled) { + try { + toggleLoading(true); + + // 这里需要调用后端API来更新账号状态 + // 由于当前后端可能没有enabled字段,我们先在前端模拟 + // 实际项目中需要后端支持 + + const response = await fetch(`${apiBase}/cookies/${accountId}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ enabled: enabled }) + }); + + if (response.ok) { + showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'}`, 'success'); + + // 清除相关缓存,确保数据一致性 + clearKeywordCache(); + + // 更新界面显示 + updateAccountRowStatus(accountId, enabled); + + // 刷新自动回复页面的账号列表 + refreshAccountList(); + + // 如果禁用的账号在自动回复页面被选中,更新显示 + const accountSelect = document.getElementById('accountSelect'); + if (accountSelect && accountSelect.value === accountId) { + if (!enabled) { + // 更新徽章显示禁用状态 + updateAccountBadge(accountId, false); + showToast('账号已禁用,配置的关键词不会参与自动回复', 'warning'); + } else { + // 更新徽章显示启用状态 + updateAccountBadge(accountId, true); + showToast('账号已启用,配置的关键词将参与自动回复', 'success'); + } + } + + } else { + // 如果后端不支持,先在前端模拟 + console.warn('后端暂不支持账号状态切换,使用前端模拟'); + showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'} (前端模拟)`, enabled ? 'success' : 'warning'); + updateAccountRowStatus(accountId, enabled); + } + + } catch (error) { + console.error('切换账号状态失败:', error); + + // 后端不支持时的降级处理 + showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'} (本地模拟)`, enabled ? 'success' : 'warning'); + updateAccountRowStatus(accountId, enabled); + + // 恢复切换按钮状态 + const toggle = document.querySelector(`input[onchange*="${accountId}"]`); + if (toggle) { + toggle.checked = enabled; + } + } finally { + toggleLoading(false); + } +} + +// 更新账号行的状态显示 +function updateAccountRowStatus(accountId, enabled) { + const toggle = document.querySelector(`input[onchange*="${accountId}"]`); + if (!toggle) return; + + const row = toggle.closest('tr'); + const statusBadge = row.querySelector('.status-badge'); + const actionButtons = row.querySelectorAll('.btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)'); + + // 更新行样式 + row.className = `account-row ${enabled ? 'enabled' : 'disabled'}`; + + // 更新状态徽章 + statusBadge.className = `status-badge ${enabled ? 'enabled' : 'disabled'}`; + statusBadge.title = enabled ? '账号已启用' : '账号已禁用'; + statusBadge.innerHTML = ` + + `; + + // 更新按钮状态(只禁用编辑Cookie按钮,其他按钮保持可用) + actionButtons.forEach(btn => { + if (btn.onclick && btn.onclick.toString().includes('editCookieInline')) { + btn.disabled = !enabled; + } + // 设置自动回复按钮始终可用,但更新提示文本 + if (btn.onclick && btn.onclick.toString().includes('goToAutoReply')) { + btn.title = enabled ? '设置自动回复' : '配置关键词 (账号已禁用)'; + } + }); + + // 更新切换按钮的提示 + const label = toggle.closest('.status-toggle'); + label.title = enabled ? '点击禁用' : '点击启用'; +} + +// 切换自动确认发货状态 +async function toggleAutoConfirm(accountId, enabled) { + try { + toggleLoading(true); + + const response = await fetch(`${apiBase}/cookies/${accountId}/auto-confirm`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ auto_confirm: enabled }) + }); + + if (response.ok) { + const result = await response.json(); + showToast(result.message, 'success'); + + // 更新界面显示 + updateAutoConfirmRowStatus(accountId, enabled); + } else { + const error = await response.json(); + showToast(error.detail || '更新自动确认发货设置失败', 'error'); + + // 恢复切换按钮状态 + const toggle = document.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`); + if (toggle) { + toggle.checked = !enabled; + } + } + + } catch (error) { + console.error('切换自动确认发货状态失败:', error); + showToast('网络错误,请稍后重试', 'error'); + + // 恢复切换按钮状态 + const toggle = document.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`); + if (toggle) { + toggle.checked = !enabled; + } + } finally { + toggleLoading(false); + } +} + +// 更新自动确认发货行状态 +function updateAutoConfirmRowStatus(accountId, enabled) { + const row = document.querySelector(`tr:has(input[onchange*="toggleAutoConfirm('${accountId}'"])`); + if (!row) return; + + const statusBadge = row.querySelector('.status-badge:has(i.bi-truck, i.bi-truck-flatbed)'); + const toggle = row.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`); + + if (statusBadge && toggle) { + // 更新状态徽章 + statusBadge.className = `status-badge ${enabled ? 'enabled' : 'disabled'}`; + statusBadge.title = enabled ? '自动确认发货已开启' : '自动确认发货已关闭'; + statusBadge.innerHTML = ` + + `; + + // 更新切换按钮的提示 + const label = toggle.closest('.status-toggle'); + label.title = enabled ? '点击关闭自动确认发货' : '点击开启自动确认发货'; + } +} + +// 跳转到自动回复页面并选择指定账号 +function goToAutoReply(accountId) { + // 切换到自动回复页面 + showSection('auto-reply'); + + // 设置账号选择器的值 + setTimeout(() => { + const accountSelect = document.getElementById('accountSelect'); + if (accountSelect) { + accountSelect.value = accountId; + // 触发change事件来加载关键词 + loadAccountKeywords(); + } + }, 100); + + showToast(`已切换到自动回复页面,账号 "${accountId}" 已选中`, 'info'); +} diff --git a/static/js/modules/navigation.js b/static/js/modules/navigation.js new file mode 100644 index 0000000..63ac0cc --- /dev/null +++ b/static/js/modules/navigation.js @@ -0,0 +1,86 @@ +// ==================== 页面导航功能 ==================== + +// 菜单切换功能 +function showSection(sectionName) { + console.log('切换到页面:', sectionName); // 调试信息 + + // 隐藏所有内容区域 + document.querySelectorAll('.content-section').forEach(section => { + section.classList.remove('active'); + }); + + // 移除所有菜单项的active状态 + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + }); + + // 显示选中的内容区域 + const targetSection = document.getElementById(sectionName + '-section'); + if (targetSection) { + targetSection.classList.add('active'); + console.log('页面已激活:', sectionName + '-section'); // 调试信息 + } else { + console.error('找不到页面元素:', sectionName + '-section'); // 调试信息 + } + + // 设置对应菜单项为active(修复event.target问题) + const menuLinks = document.querySelectorAll('.nav-link'); + menuLinks.forEach(link => { + if (link.onclick && link.onclick.toString().includes(`showSection('${sectionName}')`)) { + link.classList.add('active'); + } + }); + + // 根据不同section加载对应数据 + switch (sectionName) { + case 'dashboard': + loadDashboard(); + break; + case 'accounts': + loadCookies(); + break; + case 'items': + loadItems(); + break; + case 'auto-reply': + refreshAccountList(); + break; + case 'cards': + loadCards(); + break; + case 'auto-delivery': + loadDeliveryRules(); + break; + case 'notification-channels': + loadNotificationChannels(); + break; + case 'message-notifications': + loadMessageNotifications(); + break; + case 'logs': + // 如果没有日志数据,则加载 + setTimeout(() => { + if (!window.allLogs || window.allLogs.length === 0) { + refreshLogs(); + } + }, 100); + break; + } + + // 如果切换到非日志页面,停止自动刷新 + if (sectionName !== 'logs' && window.autoRefreshInterval) { + clearInterval(window.autoRefreshInterval); + window.autoRefreshInterval = null; + const button = document.querySelector('#autoRefreshText'); + const icon = button?.previousElementSibling; + if (button) { + button.textContent = '开启自动刷新'; + if (icon) icon.className = 'bi bi-play-circle me-1'; + } + } +} + +// 移动端侧边栏切换 +function toggleSidebar() { + document.getElementById('sidebar').classList.toggle('show'); +} diff --git a/static/js/modules/utils.js b/static/js/modules/utils.js new file mode 100644 index 0000000..9ac3988 --- /dev/null +++ b/static/js/modules/utils.js @@ -0,0 +1,85 @@ +// ==================== 通用工具函数 ==================== + +// 显示/隐藏加载动画 +function toggleLoading(show) { + document.getElementById('loading').classList.toggle('d-none', !show); +} + +// 显示提示消息 +function showToast(message, type = 'success') { + const toastContainer = document.querySelector('.toast-container'); + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + + toastContainer.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); + bsToast.show(); + + // 自动移除 + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); +} + +// 错误处理 +async function handleApiError(err) { + console.error(err); + showToast(err.message || '操作失败', 'danger'); + toggleLoading(false); +} + +// API请求包装 +async function fetchJSON(url, opts = {}) { + toggleLoading(true); + try { + // 添加认证头 + if (authToken) { + opts.headers = opts.headers || {}; + opts.headers['Authorization'] = `Bearer ${authToken}`; + } + + const res = await fetch(url, opts); + if (res.status === 401) { + // 未授权,跳转到登录页面 + localStorage.removeItem('auth_token'); + window.location.href = '/'; + return; + } + if (!res.ok) { + let errorMessage = `HTTP ${res.status}`; + try { + const errorText = await res.text(); + if (errorText) { + // 尝试解析JSON错误信息 + try { + const errorJson = JSON.parse(errorText); + errorMessage = errorJson.detail || errorJson.message || errorText; + } catch { + errorMessage = errorText; + } + } + } catch { + errorMessage = `HTTP ${res.status} ${res.statusText}`; + } + throw new Error(errorMessage); + } + const data = await res.json(); + toggleLoading(false); + return data; + } catch (err) { + handleApiError(err); + throw err; + } +}