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.id}
+
+ |
+
+
+ ${cookie.value || '未设置'}
+
+ |
+
+
+ ${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 @@
+
+
+
+
+
+ 闲鱼自动回复系统 - 模块化版本
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 = `
+
+
+ `;
+ 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 = `
+
+ `;
+
+ 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;
+ }
+}