// ================================
// 全局变量和配置
// ================================
const apiBase = location.origin;
let keywordsData = {};
let currentCookieId = '';
let editCookieId = '';
let authToken = localStorage.getItem('auth_token');
let dashboardData = {
accounts: [],
totalKeywords: 0,
totalItems: 0
};
let pendingAccountManagementFocusId = '';
let aboutDiagnosticsAccounts = [];
let aboutDiagnosticsInitialized = false;
let dashboardRuntimeRetryTimer = null;
let aboutRuntimeRetryTimer = null;
let lastDashboardRuntimeRetryAt = 0;
let lastAboutRuntimeRetryAt = 0;
const DASHBOARD_ANNOUNCEMENT_DISMISS_PREFIX = 'dashboard_announcement_dismissed_';
let dashboardAnnouncementState = {
current: null,
history: []
};
// 账号关键词缓存
let accountKeywordCache = {};
let cacheTimestamp = 0;
const CACHE_DURATION = 30000; // 30秒缓存
// 商品列表搜索和分页相关变量
let allItemsData = []; // 存储所有商品数据
let filteredItemsData = []; // 存储过滤后的商品数据
let currentItemsPage = 1; // 当前页码
let itemsPerPage = 20; // 每页显示数量
let totalItemsPages = 0; // 总页数
let currentSearchKeyword = ''; // 当前搜索关键词
// 订单列表搜索和分页相关变量
let allOrdersData = []; // 存储所有订单数据
let filteredOrdersData = []; // 存储过滤后的订单数据
let currentOrdersPage = 1; // 当前页码
let ordersPerPage = 20; // 每页显示数量
let totalOrdersPages = 0; // 总页数
let currentOrderSearchKeyword = ''; // 当前搜索关键词
let ordersStreamAbortController = null;
let ordersStreamReconnectTimer = null;
let ordersStreamRetryCount = 0;
let ordersStreamShouldRun = false;
let orderHistorySyncModalInstance = null;
let orderHistorySyncPollingTimer = null;
let activeOrderHistorySyncJobId = '';
let orderHistorySyncNotifiedJobId = '';
let orderHistorySyncAccounts = [];
let loadingRequestCount = 0;
let loadingShowTimer = null;
const LOADING_SHOW_DELAY = 120;
// ================================
// 通用功能 - 菜单切换和导航
// ================================
function showSection(sectionName) {
console.log('切换到页面:', sectionName); // 调试信息
// 获取并校验目标内容区域
const targetSection = document.getElementById(sectionName + '-section');
if (!targetSection) {
console.error('找不到页面元素:', sectionName + '-section'); // 调试信息
return;
}
// 如果已经是当前页面,避免重复切换导致闪烁
if (targetSection.classList.contains('active')) {
return;
}
// 仅切换当前激活页面和目标页面,避免“先全关再全开”造成白闪
const currentActiveSection = document.querySelector('.content-section.active');
if (currentActiveSection) {
currentActiveSection.classList.remove('active');
}
targetSection.classList.add('active');
console.log('页面已激活:', sectionName + '-section'); // 调试信息
// 仅处理侧边栏菜单 active,避免影响内容区域 tab 的 .nav-link
document.querySelectorAll('#sidebar .sidebar-nav .nav-link').forEach(link => {
link.classList.remove('active');
});
const activeMenuLink = document.querySelector(`#sidebar .nav-item[data-menu-id="${sectionName}"] .nav-link`);
if (activeMenuLink) {
activeMenuLink.classList.add('active');
}
// 根据不同section加载对应数据
switch(sectionName) {
case 'dashboard': // 【仪表盘菜单】
loadDashboard();
break;
case 'accounts': // 【账号管理菜单】
loadCookies();
break;
case 'items': // 【商品管理菜单】
loadItems();
initItemsSearch(); // 确保搜索功能已初始化
break;
case 'items-reply': // 【商品回复管理菜单】
loadItemsReplay();
break;
case 'orders': // 【订单管理菜单】
loadOrders();
break;
case 'auto-reply': // 【自动回复菜单】
refreshAccountList();
break;
case 'cards': // 【卡券管理菜单】
loadCards();
break;
case 'auto-delivery': // 【自动发货菜单】
loadDeliveryRules();
break;
case 'notification-channels': // 【通知渠道菜单】
loadNotificationChannels();
break;
case 'message-notifications': // 【消息通知菜单】
loadMessageNotifications();
loadNotificationTemplates();
break;
case 'system-settings': // 【系统设置菜单】
loadSystemSettings();
initMenuManagement();
break;
case 'logs': // 【日志管理菜单】
// 自动加载系统日志
setTimeout(() => {
// 检查是否在正确的页面并且元素存在
const systemLogContainer = document.getElementById('systemLogContainer');
if (systemLogContainer) {
console.log('首次进入日志页面,自动加载日志...');
loadSystemLogs();
}
}, 100);
break;
case 'risk-control-logs': // 【风控日志菜单】
// 自动加载风控日志
setTimeout(() => {
const riskLogContainer = document.getElementById('riskLogContainer');
if (riskLogContainer) {
console.log('首次进入风控日志页面,自动加载日志...');
loadRiskControlLogs();
loadCookieFilterOptions();
}
}, 100);
break;
case 'user-management': // 【用户管理菜单】
loadUserManagement();
break;
case 'online-im': // 【在线客服菜单】
loadOnlineIm();
break;
case 'data-management': // 【数据管理菜单】
loadDataManagement();
break;
}
if (sectionName !== 'orders') {
stopOrdersStream();
}
// 如果切换到非日志页面,停止自动刷新
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';
}
}
if (sectionName !== 'dashboard' && dashboardRuntimeRetryTimer) {
clearTimeout(dashboardRuntimeRetryTimer);
dashboardRuntimeRetryTimer = null;
}
if (sectionName !== 'accounts' && aboutRuntimeRetryTimer) {
clearTimeout(aboutRuntimeRetryTimer);
aboutRuntimeRetryTimer = null;
}
}
// 移动端侧边栏切换
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('show');
}
// 侧边栏折叠切换
function toggleSidebarCollapse() {
const sidebar = document.getElementById('sidebar');
const body = document.body;
sidebar.classList.toggle('collapsed');
body.classList.toggle('sidebar-collapsed');
// 保存状态到 localStorage
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
}
// 初始化侧边栏折叠状态
function initSidebarCollapse() {
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
if (isCollapsed) {
const sidebar = document.getElementById('sidebar');
const body = document.body;
if (sidebar) {
sidebar.classList.add('collapsed');
body.classList.add('sidebar-collapsed');
}
}
}
// ================================
// 暗色模式功能
// ================================
// 检测系统是否为暗色模式
function isSystemDarkMode() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// 更新主题图标
function updateDarkModeIcon(mode) {
const icon = document.getElementById('darkModeIcon');
if (!icon) return;
// 清除所有可能的图标类
icon.classList.remove('bi-moon-fill', 'bi-sun-fill', 'bi-circle-half');
if (mode === 'auto') {
icon.classList.add('bi-circle-half');
} else if (mode === 'dark') {
icon.classList.add('bi-sun-fill');
} else {
icon.classList.add('bi-moon-fill');
}
}
// 应用主题
function applyDarkMode(mode) {
const html = document.documentElement;
let shouldBeDark = false;
if (mode === 'auto') {
shouldBeDark = isSystemDarkMode();
} else if (mode === 'dark') {
shouldBeDark = true;
}
if (shouldBeDark) {
html.setAttribute('data-theme', 'dark');
} else {
html.removeAttribute('data-theme');
}
updateDarkModeIcon(mode);
}
// 切换暗色模式(三态切换:light → dark → auto)
function toggleDarkMode() {
const currentMode = localStorage.getItem('darkMode') || 'light';
let nextMode;
if (currentMode === 'light') {
nextMode = 'dark';
} else if (currentMode === 'dark') {
nextMode = 'auto';
} else {
nextMode = 'light';
}
localStorage.setItem('darkMode', nextMode);
applyDarkMode(nextMode);
// 显示提示
const modeNames = {
'light': '浅色模式',
'dark': '深色模式',
'auto': '跟随系统'
};
showToast(`已切换至${modeNames[nextMode]}`, 'info');
}
// 初始化暗色模式
function initDarkMode() {
const savedMode = localStorage.getItem('darkMode') || 'light';
applyDarkMode(savedMode);
// 监听系统主题变化
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const currentMode = localStorage.getItem('darkMode') || 'light';
if (currentMode === 'auto') {
applyDarkMode('auto');
}
});
}
}
// ================================
// 【仪表盘菜单】相关功能
// ================================
async function fetchDashboardResource(path, fallbackValue) {
try {
const response = await fetch(`${apiBase}${path}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
return fallbackValue;
}
return await response.json();
} catch (error) {
console.error(`加载仪表盘资源失败: ${path}`, error);
return fallbackValue;
}
}
async function enrichDashboardAccounts(accounts) {
const scheduledTaskData = await fetchDashboardResource('/scheduled-tasks', { success: false, tasks: [] });
const scheduledTasks = scheduledTaskData && scheduledTaskData.success ? (scheduledTaskData.tasks || []) : [];
return Promise.all(accounts.map(async (account) => {
const [keywordsData, defaultReplyData, aiReplyData] = await Promise.all([
fetchDashboardResource(`/keywords/${encodeURIComponent(account.id)}`, []),
fetchDashboardResource(`/default-replies/${encodeURIComponent(account.id)}`, { enabled: false, reply_content: '' }),
fetchDashboardResource(`/ai-reply-settings/${encodeURIComponent(account.id)}`, { ai_enabled: false, model_name: 'qwen-plus' })
]);
return {
...account,
keywords: Array.isArray(keywordsData) ? keywordsData : [],
keywordCount: Array.isArray(keywordsData) ? keywordsData.length : 0,
defaultReply: defaultReplyData || { enabled: false, reply_content: '' },
aiReply: aiReplyData || { ai_enabled: false, model_name: 'qwen-plus' },
polishSchedule: getPolishScheduledTask(scheduledTasks, account.id)
};
}));
}
function getDashboardAnnouncementDismissKey(id) {
return `${DASHBOARD_ANNOUNCEMENT_DISMISS_PREFIX}${String(id || '').trim()}`;
}
function normalizeDashboardAnnouncementState(payload) {
return {
current: payload?.current || null,
history: Array.isArray(payload?.history) ? payload.history : []
};
}
function isDashboardAnnouncementDismissed(announcement) {
const announcementId = String(announcement?.id || '').trim();
if (!announcementId) {
return false;
}
return localStorage.getItem(getDashboardAnnouncementDismissKey(announcementId)) === 'true';
}
function dismissDashboardAnnouncement(announcement) {
const announcementId = String(announcement?.id || '').trim();
if (announcementId) {
localStorage.setItem(getDashboardAnnouncementDismissKey(announcementId), 'true');
}
renderDashboardAnnouncement();
}
function handleDashboardAnnouncementAction(announcement) {
const actionType = String(announcement?.action_type || '').trim().toLowerCase();
if (!actionType) {
return;
}
if (actionType === 'changelog') {
showChangelogModal();
return;
}
if (actionType === 'update') {
performHotUpdate();
return;
}
if (actionType === 'url') {
const targetUrl = String(announcement?.action_url || '').trim();
if (targetUrl) {
window.open(targetUrl, '_blank', 'noopener,noreferrer');
}
}
}
function getDashboardAnnouncementLevelText(level) {
const normalizedLevel = String(level || '').trim().toLowerCase();
if (normalizedLevel === 'success') return '成功';
if (normalizedLevel === 'warning') return '提醒';
if (normalizedLevel === 'danger') return '重要';
return '公告';
}
function getDashboardAnnouncementStatusText(status) {
const normalizedStatus = String(status || '').trim().toLowerCase();
if (normalizedStatus === 'active') return '当前生效';
if (normalizedStatus === 'scheduled') return '尚未生效';
if (normalizedStatus === 'expired') return '已结束';
if (normalizedStatus === 'disabled') return '未启用';
return '历史记录';
}
function getDashboardAnnouncementDisplayTime(announcement) {
const timeValue = String(
announcement?.published_at
|| announcement?.start_at
|| announcement?.end_at
|| ''
).trim();
if (!timeValue) {
return '未设置时间';
}
return formatDateTime(timeValue);
}
function showDashboardAnnouncementHistoryModal() {
const history = Array.isArray(dashboardAnnouncementState.history) ? dashboardAnnouncementState.history : [];
if (!history.length) {
showToast('暂无公告记录', 'info');
return;
}
const modalId = 'dashboardAnnouncementHistoryModal';
const existingModal = document.getElementById(modalId);
if (existingModal) {
existingModal.remove();
}
const historyHtml = history.map((announcement, index) => {
const level = ['info', 'success', 'warning', 'danger'].includes(String(announcement?.level || '').trim().toLowerCase())
? String(announcement.level || '').trim().toLowerCase()
: 'info';
const status = String(announcement?.status || '').trim().toLowerCase() || 'disabled';
const title = String(announcement?.title || '').trim() || '未命名公告';
const message = String(announcement?.message || '').trim() || '暂无内容';
const actionText = String(announcement?.action_type ? (announcement?.action_text || '') : '').trim();
const timeText = getDashboardAnnouncementDisplayTime(announcement);
const currentBadge = announcement?.is_current
? '当前'
: '';
return `
${escapeHtml(title)}
${currentBadge}
${escapeHtml(getDashboardAnnouncementLevelText(level))}
${escapeHtml(getDashboardAnnouncementStatusText(status))}
${escapeHtml(timeText)}
${actionText ? `
` : ''}
${escapeHtml(message)}
`;
}).join('');
document.body.insertAdjacentHTML('beforeend', `
`);
const modalElement = document.getElementById(modalId);
if (!modalElement) {
return;
}
modalElement.querySelectorAll('[data-announcement-history-action-index]').forEach(button => {
button.addEventListener('click', () => {
const index = Number(button.getAttribute('data-announcement-history-action-index'));
const announcement = Number.isFinite(index) ? history[index] : null;
if (!announcement) {
return;
}
const modalInstance = bootstrap.Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
}
setTimeout(() => {
handleDashboardAnnouncementAction(announcement);
}, 120);
});
});
modalElement.addEventListener('hidden.bs.modal', () => {
modalElement.remove();
}, { once: true });
const modal = new bootstrap.Modal(modalElement);
modal.show();
}
function renderDashboardAnnouncement() {
const slot = document.getElementById('dashboardAnnouncementSlot');
if (!slot) return;
const currentAnnouncement = dashboardAnnouncementState.current;
if (!currentAnnouncement || isDashboardAnnouncementDismissed(currentAnnouncement)) {
slot.style.display = 'none';
slot.innerHTML = '';
return;
}
const level = ['info', 'success', 'warning', 'danger'].includes(String(currentAnnouncement.level || '').trim().toLowerCase())
? String(currentAnnouncement.level || '').trim().toLowerCase()
: 'info';
const title = String(currentAnnouncement.title || '').trim();
const message = String(currentAnnouncement.message || '').trim();
const actionText = String(currentAnnouncement.action_type ? (currentAnnouncement.action_text || '') : '').trim();
const dismissible = currentAnnouncement.dismissible !== false;
slot.style.display = '';
slot.innerHTML = `
${actionText ? `` : ''}
${dismissible ? `
` : ''}
`;
const openButton = document.getElementById('dashboardAnnouncementOpenBtn');
if (openButton) {
openButton.onclick = () => showDashboardAnnouncementHistoryModal();
}
const actionButton = document.getElementById('dashboardAnnouncementActionBtn');
if (actionButton) {
actionButton.onclick = () => handleDashboardAnnouncementAction(currentAnnouncement);
}
const closeButton = document.getElementById('dashboardAnnouncementCloseBtn');
if (closeButton) {
closeButton.onclick = () => dismissDashboardAnnouncement(currentAnnouncement);
}
}
async function loadDashboardAnnouncement() {
const result = await fetchDashboardResource('/api/announcement', { success: false, current: null, history: [] });
dashboardAnnouncementState = normalizeDashboardAnnouncementState(result?.success ? result : null);
renderDashboardAnnouncement();
}
function renderDashboardSummaryCard(label, value, tone = 'primary', details = []) {
const detailMarkup = Array.isArray(details) && details.length ? `
${details.map(([detailLabel, detailValue]) => `
${escapeHtml(detailLabel)}
${escapeHtml(detailValue)}
`).join('')}
` : '';
return `
${escapeHtml(value)}
${detailMarkup}
`;
}
function renderDashboardAccountMetric(label, value, tone = 'off') {
return `
${escapeHtml(label)}
${escapeHtml(value)}
`;
}
function isRuntimeStatusHealthy(runtimeStatus) {
return Boolean(
runtimeStatus?.running
&& runtimeStatus.ws_ready
&& runtimeStatus.session_ready
&& runtimeStatus.has_current_token
&& runtimeStatus.message_stream_ready
);
}
function getRuntimeStatusRecentAnchor(runtimeStatus) {
const normalizedRuntimeStatus = runtimeStatus || {};
const timestampKeys = [
'state_last_changed_at',
'last_successful_connection_at',
'last_heartbeat_response_at',
'session_keepalive_at',
'token_last_refreshed_at',
'last_message_received_at',
];
const timestamps = timestampKeys
.map(key => Number(normalizedRuntimeStatus[key] || 0))
.filter(value => Number.isFinite(value) && value > 0);
return timestamps.length ? Math.max(...timestamps) : 0;
}
function shouldAutoRetryRuntimeStatus(runtimeStatus) {
if (!runtimeStatus?.running) {
return false;
}
const connectionState = String(runtimeStatus.connection_state || '').trim();
if (connectionState === 'connecting' || connectionState === 'reconnecting') {
return true;
}
if (isRuntimeStatusHealthy(runtimeStatus)) {
return false;
}
const recentAnchor = getRuntimeStatusRecentAnchor(runtimeStatus);
if (!recentAnchor) {
return false;
}
return ((Date.now() / 1000) - recentAnchor) <= 90;
}
function getMessageStreamRuntimeDisplay(runtimeStatus) {
const normalizedRuntimeStatus = runtimeStatus || {};
const explicitStatus = String(normalizedRuntimeStatus.message_stream_status || '').trim();
const explicitNote = String(normalizedRuntimeStatus.message_stream_note || '').trim();
const connectionState = String(normalizedRuntimeStatus.connection_state || '').trim();
let status = explicitStatus;
if (!status) {
if (!normalizedRuntimeStatus.running) {
status = 'not_running';
} else if (connectionState === 'connecting' || connectionState === 'reconnecting') {
status = 'recovering';
} else if (connectionState !== 'connected' || normalizedRuntimeStatus.ws_ready === false) {
status = 'connection_unready';
} else if (normalizedRuntimeStatus.message_stream_ready) {
status = 'watching';
} else {
status = 'connection_unready';
}
}
let note = explicitNote;
if (!note) {
if (!normalizedRuntimeStatus.running) {
note = '账号实例未启动,业务消息流尚未建立';
} else if (status === 'recovering') {
note = '连接正在恢复,业务消息流状态将在重连稳定后更新';
} else if (status === 'connection_unready') {
note = '连接未就绪,业务消息流状态待 WebSocket 恢复后更新';
} else if (status === 'watching') {
note = '当前连接尚未收到非心跳业务包';
} else {
note = '业务消息流状态等待更多运行时数据';
}
}
return { status, note };
}
function scheduleDashboardRuntimeAutoRetry(accounts) {
if (dashboardRuntimeRetryTimer) {
clearTimeout(dashboardRuntimeRetryTimer);
dashboardRuntimeRetryTimer = null;
}
if (!document.getElementById('dashboard-section')?.classList.contains('active')) {
return;
}
if (!Array.isArray(accounts) || !accounts.some(account => shouldAutoRetryRuntimeStatus(account.runtime_status))) {
return;
}
if (Date.now() - lastDashboardRuntimeRetryAt < 15000) {
return;
}
const hasTransientState = accounts.some(account => {
const connectionState = String(account?.runtime_status?.connection_state || '').trim();
return connectionState === 'connecting' || connectionState === 'reconnecting';
});
const delay = hasTransientState ? 3500 : 5000;
dashboardRuntimeRetryTimer = setTimeout(() => {
dashboardRuntimeRetryTimer = null;
if (!document.getElementById('dashboard-section')?.classList.contains('active')) {
return;
}
lastDashboardRuntimeRetryAt = Date.now();
refreshDashboardRuntimeSnapshots();
}, delay);
}
function scheduleAboutRuntimeAutoRetry(accountId, runtimeStatus) {
if (aboutRuntimeRetryTimer) {
clearTimeout(aboutRuntimeRetryTimer);
aboutRuntimeRetryTimer = null;
}
const normalizedAccountId = String(accountId || '').trim();
if (!normalizedAccountId) {
return;
}
if (!document.getElementById('accounts-section')?.classList.contains('active')) {
return;
}
if (!shouldAutoRetryRuntimeStatus(runtimeStatus)) {
return;
}
if (Date.now() - lastAboutRuntimeRetryAt < 12000) {
return;
}
const connectionState = String(runtimeStatus?.connection_state || '').trim();
const delay = (connectionState === 'connecting' || connectionState === 'reconnecting') ? 3000 : 5000;
aboutRuntimeRetryTimer = setTimeout(() => {
aboutRuntimeRetryTimer = null;
if (!document.getElementById('accounts-section')?.classList.contains('active')) {
return;
}
if (getAboutSelectedAccountId() !== normalizedAccountId) {
return;
}
lastAboutRuntimeRetryAt = Date.now();
loadAboutRuntimeStatus(normalizedAccountId);
}, delay);
}
function renderDashboardAccountRuntimeSnapshot(runtimeStatus) {
const normalizedRuntimeStatus = runtimeStatus || {};
const connectionState = normalizedRuntimeStatus.connection_state || 'not_running';
const keepaliveDisplayStatus = normalizedRuntimeStatus.session_keepalive_display_status || normalizedRuntimeStatus.session_keepalive_status || '';
const tokenStatus = normalizedRuntimeStatus.token_refresh_status || '';
const messageStreamDisplay = getMessageStreamRuntimeDisplay(normalizedRuntimeStatus);
const messageStreamStatus = messageStreamDisplay.status;
const connectionText = getAboutStatusText('connection', connectionState) || '未运行';
const connectionTone = getAboutStatusVariant('connection', connectionState);
const keepaliveText = keepaliveDisplayStatus
? (getAboutStatusText('keepalive', keepaliveDisplayStatus) || keepaliveDisplayStatus)
: (normalizedRuntimeStatus.running ? '未执行' : '未运行');
const keepaliveTone = keepaliveDisplayStatus
? getAboutStatusVariant('keepalive', keepaliveDisplayStatus)
: 'secondary';
const tokenText = tokenStatus
? (getAboutStatusText('token', tokenStatus) || tokenStatus)
: (normalizedRuntimeStatus.running ? '未刷新' : '未运行');
const tokenTone = tokenStatus
? getAboutStatusVariant('token', tokenStatus)
: 'secondary';
const messageStreamText = messageStreamStatus
? (getAboutStatusText('stream', messageStreamStatus) || messageStreamStatus)
: (normalizedRuntimeStatus.running ? '观察中' : '未运行');
const messageStreamTone = messageStreamStatus
? getAboutStatusVariant('stream', messageStreamStatus)
: 'secondary';
const runningHealthy = isRuntimeStatusHealthy(normalizedRuntimeStatus);
const summaryText = !normalizedRuntimeStatus.running
? '未运行'
: (runningHealthy ? '运行正常' : '部分异常');
const summaryTone = !normalizedRuntimeStatus.running
? 'secondary'
: (runningHealthy ? 'success' : 'warning');
const items = [
{ label: '连接', text: connectionText, tone: connectionTone },
{ label: '保活', text: keepaliveText, tone: keepaliveTone },
{ label: 'Token', text: tokenText, tone: tokenTone },
{ label: '消息流', text: messageStreamText, tone: messageStreamTone }
];
return `
${escapeHtml(summaryText)}
${items.map(item => {
const detailText = `${item.label}: ${item.text}`;
return `
${escapeHtml(item.label)}
`;
}).join('')}
`;
}
function renderDashboardAccountCard(account) {
const isEnabled = account.enabled === undefined ? true : account.enabled;
const keywordCount = account.keywordCount || 0;
const defaultReplyEnabled = Boolean(account.defaultReply?.enabled);
const aiReplyEnabled = Boolean(account.aiReply?.ai_enabled);
const autoConfirmEnabled = account.auto_confirm === undefined ? true : Boolean(account.auto_confirm);
const autoCommentEnabled = Boolean(account.auto_comment);
const hasCredentials = Boolean(account.username) && Boolean(account.has_password);
const hasPartialCredentials = !hasCredentials && (Boolean(account.username) || Boolean(account.has_password));
const pauseDuration = account.pause_duration === 0 ? '不暂停' : `${account.pause_duration || 10} 分钟`;
const polishSchedule = account.polishSchedule;
const remarkText = account.remark || '';
let replyModeText = '未开启';
let replyModeTone = 'off';
if (aiReplyEnabled && defaultReplyEnabled) {
replyModeText = 'AI + 默认';
replyModeTone = 'info';
} else if (aiReplyEnabled) {
replyModeText = 'AI 回复';
replyModeTone = 'info';
} else if (defaultReplyEnabled) {
replyModeText = '默认回复';
replyModeTone = 'on';
}
let polishScheduleMetricText = '未设置';
let polishScheduleTone = 'off';
if (polishSchedule) {
if (polishSchedule.enabled) {
const displayHour = formatPolishScheduleHour(polishSchedule.delay_minutes ?? polishSchedule.run_hour);
polishScheduleMetricText = `${displayHour}`;
polishScheduleTone = 'info';
} else {
const displayHour = formatPolishScheduleHour(polishSchedule.delay_minutes ?? polishSchedule.run_hour);
polishScheduleMetricText = `${displayHour} 未开`;
polishScheduleTone = 'warn';
}
} else if (isEnabled) {
polishScheduleMetricText = '未设置';
polishScheduleTone = 'off';
}
const metrics = [
renderDashboardAccountMetric('关键词', keywordCount > 0 ? `${keywordCount} 个` : '未配置', keywordCount > 0 ? 'on' : 'off'),
renderDashboardAccountMetric('回复模式', replyModeText, replyModeTone),
renderDashboardAccountMetric('定时擦亮', polishScheduleMetricText, polishScheduleTone)
].join('');
const runtimeSnapshot = renderDashboardAccountRuntimeSnapshot(account.runtime_status);
const secondarySummary = [
{
label: '关键词',
icon: 'chat-left-text-fill',
tone: keywordCount > 0 ? 'on' : 'off'
},
{
label: '自动发货',
icon: 'lightning-charge-fill',
tone: autoConfirmEnabled ? 'on' : 'off'
},
{
label: '自动好评',
icon: 'chat-heart-fill',
tone: autoCommentEnabled ? 'on' : 'off'
},
{
label: '账密',
icon: hasPartialCredentials ? 'exclamation-triangle-fill' : 'shield-lock-fill',
tone: hasCredentials ? 'info' : (hasPartialCredentials ? 'warn' : 'off')
},
{
label: '暂停',
value: pauseDuration,
icon: 'clock-history',
tone: 'neutral'
}
].map(({ label, value = '', icon, tone }) => `
${escapeHtml(label)}
${value ? `${escapeHtml(value)}` : ''}
`).join('');
return `
${escapeHtml(account.id)}
${remarkText ? `` : ''}
${secondarySummary}
${isEnabled ? '启用中' : '已禁用'}
${metrics}
${runtimeSnapshot}
`;
}
function renderDashboardAccountOverview(accounts, totalItems = 0) {
const summary = document.getElementById('dashboardAccountSummary');
const enabledContainer = document.getElementById('dashboardEnabledAccounts');
const disabledContainer = document.getElementById('dashboardDisabledAccounts');
const enabledHint = document.getElementById('dashboardEnabledAccountsHint');
const disabledHint = document.getElementById('dashboardDisabledAccountsHint');
if (!summary || !enabledContainer || !disabledContainer || !enabledHint || !disabledHint) {
return;
}
const enabledAccounts = accounts.filter(account => account.enabled === undefined ? true : account.enabled);
const disabledAccounts = accounts.filter(account => !(account.enabled === undefined ? true : account.enabled));
const activeKeywordAccounts = enabledAccounts.filter(account => (account.keywordCount || 0) > 0).length;
const totalKeywords = enabledAccounts.reduce((sum, account) => sum + (account.keywordCount || 0), 0);
summary.innerHTML = [
['全部账号', String(accounts.length), 'primary', []],
['已启用 / 已禁用', `${enabledAccounts.length} / ${disabledAccounts.length}`, 'success', []],
['关键词总数', String(totalKeywords), 'info', []],
['商品总数', String(totalItems), 'muted', []]
].map(([label, value, tone, details]) => renderDashboardSummaryCard(label, value, tone, details)).join('');
enabledHint.textContent = `${enabledAccounts.length} 个账号`;
disabledHint.textContent = disabledAccounts.length ? `${disabledAccounts.length} 个账号待恢复` : '暂无禁用账号';
const sortAccounts = (items) => [...items].sort((a, b) => {
const keywordDiff = (b.keywordCount || 0) - (a.keywordCount || 0);
if (keywordDiff !== 0) {
return keywordDiff;
}
return String(a.id || '').localeCompare(String(b.id || ''), 'zh-Hans-CN');
});
enabledContainer.innerHTML = enabledAccounts.length
? sortAccounts(enabledAccounts).map(renderDashboardAccountCard).join('')
: '暂无启用账号
';
disabledContainer.innerHTML = disabledAccounts.length
? sortAccounts(disabledAccounts).map(renderDashboardAccountCard).join('')
: '暂无禁用账号
';
}
// 加载仪表盘数据
async function loadDashboard() {
try {
toggleLoading(true);
loadDashboardAnnouncement();
// 获取账号列表
const cookiesResponse = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (cookiesResponse.ok) {
const cookiesData = await cookiesResponse.json();
const accountsWithKeywords = await enrichDashboardAccounts(cookiesData);
dashboardData.accounts = accountsWithKeywords;
dashboardData.totalKeywords = accountsWithKeywords.reduce((sum, account) => {
const isEnabled = account.enabled === undefined ? true : account.enabled;
return sum + (isEnabled ? (account.keywordCount || 0) : 0);
}, 0);
// 加载商品总数
const totalItems = await loadItemsCount();
dashboardData.totalItems = totalItems;
// 加载订单看板数据
const orderMetrics = await loadOrderDashboardMetrics();
// 加载销售额摘要数据
await loadSalesSummary();
// 加载销售额图表数据(默认显示最近1周)
await loadSalesChart('week');
// 更新仪表盘显示
renderDashboardAccountOverview(accountsWithKeywords, totalItems);
scheduleDashboardRuntimeAutoRetry(accountsWithKeywords);
await loadDashboardDeliveryLogs();
}
} catch (error) {
console.error('加载仪表盘数据失败:', error);
showToast('加载仪表盘数据失败', 'danger');
} finally {
toggleLoading(false);
}
}
async function refreshDashboardRuntimeSnapshots() {
if (!dashboardData.accounts.length) {
return;
}
try {
const cookieDetails = await fetchJSON(`${apiBase}/cookies/details`);
const runtimeStatusMap = new Map(
(Array.isArray(cookieDetails) ? cookieDetails : []).map(cookie => [String(cookie.id), cookie.runtime_status || null])
);
dashboardData.accounts = dashboardData.accounts.map(account => {
const accountId = String(account.id || '');
if (!runtimeStatusMap.has(accountId)) {
return account;
}
return {
...account,
runtime_status: runtimeStatusMap.get(accountId),
};
});
renderDashboardAccountOverview(dashboardData.accounts, dashboardData.totalItems || 0);
scheduleDashboardRuntimeAutoRetry(dashboardData.accounts);
} catch (error) {
console.error('刷新仪表盘运行态失败:', error);
}
}
// 加载商品总数
async function loadItemsCount() {
try {
const response = await fetch(`${apiBase}/items`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取商品列表失败');
}
const data = await response.json();
const items = Array.isArray(data.items) ? data.items : [];
return items.length;
} catch (error) {
console.error('加载商品总数失败:', error);
return 0;
}
}
// 加载仪表盘订单指标
async function loadOrderDashboardMetrics() {
const defaultMetrics = {
totalOrders: 0,
totalSalesAmount: 0,
completionRate: 0,
todayOrders: 0
};
try {
const token = localStorage.getItem('auth_token');
const response = await fetch('/api/orders', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (!data.success) {
console.error('加载订单数量失败:', data.message);
updateDashboardOrderMetrics(defaultMetrics);
return defaultMetrics;
}
const orders = Array.isArray(data.data) ? data.data : [];
const totalOrders = orders.length;
let totalSalesAmount = 0;
let completedOrders = 0;
let completionEligibleOrders = 0;
let todayOrders = 0;
orders.forEach(order => {
const normalizedStatus = normalizeOrderStatus(order?.order_status);
const parsedAmount = parseOrderAmount(order);
if (isSalesEligibleOrder(normalizedStatus) && parsedAmount !== null) {
totalSalesAmount += parsedAmount;
}
if (isCompletionEligibleOrder(normalizedStatus)) {
completionEligibleOrders++;
if (isCompletedOrder(normalizedStatus)) {
completedOrders++;
}
}
if (isTodayOrder(getEffectiveOrderSalesTime(order))) {
todayOrders++;
}
});
const metrics = {
totalOrders,
totalSalesAmount,
completionRate: completionEligibleOrders > 0 ? (completedOrders / completionEligibleOrders) * 100 : 0,
todayOrders
};
updateDashboardOrderMetrics(metrics);
return metrics;
} catch (error) {
console.error('加载订单数量失败:', error);
updateDashboardOrderMetrics(defaultMetrics);
return defaultMetrics;
}
}
// 销售额摘要定时刷新定时器
let salesSummaryRefreshTimer = null;
// 加载销售额摘要数据
async function loadSalesSummary() {
const todaySalesEl = document.getElementById('dashboardTodaySales');
const weekSalesEl = document.getElementById('dashboardWeekSales');
const monthSalesEl = document.getElementById('dashboardMonthSales');
const updateTimeEl = document.getElementById('dashboardSalesUpdateTime');
// 显示加载状态
showSalesLoadingState(todaySalesEl);
showSalesLoadingState(weekSalesEl);
showSalesLoadingState(monthSalesEl);
try {
const token = localStorage.getItem('auth_token');
const response = await fetch('/api/sales/summary', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success && data.data) {
updateDashboardSalesMetrics(data.data);
} else {
showSalesErrorState(todaySalesEl, '获取失败');
showSalesErrorState(weekSalesEl, '获取失败');
showSalesErrorState(monthSalesEl, '获取失败');
}
} catch (error) {
console.error('加载销售额摘要失败:', error);
showSalesErrorState(todaySalesEl, '加载失败');
showSalesErrorState(weekSalesEl, '加载失败');
showSalesErrorState(monthSalesEl, '加载失败');
}
// 启动定时刷新(每5分钟刷新一次)
startSalesSummaryRefreshTimer();
}
// 显示销售额加载状态
function showSalesLoadingState(element) {
if (element) {
element.innerHTML = '加载中...';
}
}
// 显示销售额错误状态
function showSalesErrorState(element, message) {
if (element) {
element.innerHTML = `${message}`;
}
}
// 格式化销售额显示(带千分位分隔符)
function formatSalesAmount(amount) {
return amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
// 更新销售额指标
function updateDashboardSalesMetrics(metrics) {
const todaySalesEl = document.getElementById('dashboardTodaySales');
const weekSalesEl = document.getElementById('dashboardWeekSales');
const monthSalesEl = document.getElementById('dashboardMonthSales');
const updateTimeEl = document.getElementById('dashboardSalesUpdateTime');
if (todaySalesEl) {
todaySalesEl.innerHTML = `¥${formatSalesAmount(metrics.today_sales)}`;
}
if (weekSalesEl) {
weekSalesEl.innerHTML = `¥${formatSalesAmount(metrics.week_sales)}`;
}
if (monthSalesEl) {
monthSalesEl.innerHTML = `¥${formatSalesAmount(metrics.month_sales)}`;
}
if (updateTimeEl) {
updateTimeEl.textContent = metrics.update_time;
}
}
// 启动销售额摘要定时刷新
function startSalesSummaryRefreshTimer() {
// 清除现有定时器
if (salesSummaryRefreshTimer) {
clearInterval(salesSummaryRefreshTimer);
}
// 每5分钟刷新一次
salesSummaryRefreshTimer = setInterval(async () => {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
clearInterval(salesSummaryRefreshTimer);
return;
}
const response = await fetch('/api/sales/summary', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success && data.data) {
updateDashboardSalesMetrics(data.data);
}
} catch (error) {
console.error('定时刷新销售额摘要失败:', error);
}
}, 5 * 60 * 1000); // 5分钟
}
// 停止销售额摘要定时刷新
function stopSalesSummaryRefreshTimer() {
if (salesSummaryRefreshTimer) {
clearInterval(salesSummaryRefreshTimer);
salesSummaryRefreshTimer = null;
}
}
// 销售额图表实例
let salesChartInstance = null;
let currentChartPeriod = null;
let salesDateRangeOutsideClickBound = false;
// 显示图表加载状态
function showChartLoading() {
const chartContainer = document.querySelector('.chart-container');
if (!chartContainer) return;
// 添加加载遮罩
let loadingOverlay = chartContainer.querySelector('.chart-loading-overlay');
if (!loadingOverlay) {
loadingOverlay = document.createElement('div');
loadingOverlay.className = 'chart-loading-overlay';
loadingOverlay.innerHTML = `
`;
chartContainer.style.position = 'relative';
chartContainer.appendChild(loadingOverlay);
}
loadingOverlay.style.display = 'flex';
}
// 隐藏图表加载状态
function hideChartLoading() {
const loadingOverlay = document.querySelector('.chart-loading-overlay');
if (loadingOverlay) {
loadingOverlay.style.display = 'none';
}
}
// 更新按钮激活状态
function updateChartButtonState(activePeriod) {
const buttons = document.querySelectorAll('.sales-period-button');
buttons.forEach(btn => {
const btnPeriod = btn.dataset.period;
const isActive = btnPeriod === activePeriod;
btn.classList.toggle('is-active', isActive);
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
}
// 加载销售额图表数据
async function loadSalesChart(period) {
showChartLoading();
updateChartButtonState(period);
setDateRangePickerVisible(false);
try {
const token = localStorage.getItem('auth_token');
let startDate, endDate;
const now = new Date();
if (period === 'week') {
startDate = new Date(now);
startDate.setDate(now.getDate() - 6);
} else if (period === 'month') {
startDate = new Date(now);
startDate.setMonth(now.getMonth() - 1);
}
const startDateStr = startDate.toISOString().split('T')[0];
const endDateStr = now.toISOString().split('T')[0];
const response = await fetch(`/api/sales?start_date=${startDateStr}&end_date=${endDateStr}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success && data.data) {
currentChartPeriod = period;
renderSalesChart(data.data.sales, period);
}
} catch (error) {
console.error('加载销售额图表数据失败:', error);
showToast('加载销售额数据失败', 'danger');
} finally {
hideChartLoading();
}
}
// 加载自定义日期范围的销售额数据
async function loadCustomSalesChart() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
showToast('请选择开始和结束日期', 'warning');
return;
}
if (new Date(startDate) > new Date(endDate)) {
showToast('开始日期不能晚于结束日期', 'warning');
return;
}
showChartLoading();
updateChartButtonState('custom');
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/sales?start_date=${startDate}&end_date=${endDate}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success && data.data) {
currentChartPeriod = 'custom';
renderSalesChart(data.data.sales, 'custom');
}
} catch (error) {
console.error('加载自定义销售额数据失败:', error);
showToast('加载销售额数据失败', 'danger');
} finally {
hideChartLoading();
}
}
function setDateRangePickerVisible(visible) {
const dateRangePicker = document.getElementById('dateRangePicker');
const customButton = document.querySelector('.sales-period-button[data-period="custom"]');
const timeRangeSelector = document.querySelector('.time-range-selector');
if (!dateRangePicker) {
return;
}
dateRangePicker.hidden = !visible;
if (timeRangeSelector) {
timeRangeSelector.classList.toggle('is-open', visible);
}
if (customButton) {
customButton.setAttribute('aria-expanded', visible ? 'true' : 'false');
}
if (!salesDateRangeOutsideClickBound) {
document.addEventListener('click', event => {
const control = document.querySelector('.time-range-selector');
const picker = document.getElementById('dateRangePicker');
if (!control || !picker || picker.hidden) {
return;
}
if (!control.contains(event.target)) {
setDateRangePickerVisible(false);
updateChartButtonState(currentChartPeriod || 'week');
}
});
document.addEventListener('keydown', event => {
const picker = document.getElementById('dateRangePicker');
if (event.key === 'Escape' && picker && !picker.hidden) {
setDateRangePickerVisible(false);
updateChartButtonState(currentChartPeriod || 'week');
}
});
salesDateRangeOutsideClickBound = true;
}
}
// 切换日期选择器显示
function toggleDateRangePicker() {
const dateRangePicker = document.getElementById('dateRangePicker');
if (!dateRangePicker) {
return;
}
const willShow = dateRangePicker.hidden;
setDateRangePickerVisible(willShow);
if (willShow) {
updateChartButtonState('custom');
return;
}
updateChartButtonState(currentChartPeriod || 'week');
}
// 渲染销售额图表
function renderSalesChart(salesData, period) {
const ctx = document.getElementById('salesChart').getContext('2d');
// 准备数据
const labels = salesData.map(item => item.date);
const data = salesData.map(item => item.amount);
// 创建渐变填充
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(0, 'rgba(0, 123, 255, 0.3)');
gradient.addColorStop(0.5, 'rgba(0, 123, 255, 0.15)');
gradient.addColorStop(1, 'rgba(0, 123, 255, 0.02)');
// 如果图表已存在,使用平滑更新
if (salesChartInstance) {
// 使用动画更新数据
salesChartInstance.data.labels = labels;
salesChartInstance.data.datasets[0].data = data;
salesChartInstance.data.datasets[0].backgroundColor = gradient;
// 更新标题
salesChartInstance.options.plugins.title.text = getChartTitle(period);
// 平滑过渡更新
salesChartInstance.update('active');
return;
}
// 创建新图表
salesChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '销售额',
data: data,
borderColor: '#007bff',
backgroundColor: gradient,
borderWidth: 3,
tension: 0.4,
cubicInterpolationMode: 'monotone',
fill: true,
pointBackgroundColor: '#007bff',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 5,
pointHoverRadius: 7,
pointHoverBackgroundColor: '#0056b3',
pointHoverBorderColor: '#fff',
pointHoverBorderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 750,
easing: 'easeInOutQuart'
},
transitions: {
active: {
animation: {
duration: 750,
easing: 'easeInOutQuart'
}
}
},
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 15,
font: {
size: 13,
weight: '500'
}
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#007bff',
borderWidth: 1,
padding: 12,
displayColors: true,
callbacks: {
label: function(context) {
return `销售额: ¥${context.parsed.y.toFixed(2)}`;
}
}
},
title: {
display: true,
text: getChartTitle(period),
font: {
size: 16,
weight: '600'
},
padding: {
bottom: 15
}
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '日期',
font: {
size: 12,
weight: '500'
}
},
grid: {
display: false
},
ticks: {
font: {
size: 11
}
}
},
y: {
display: true,
title: {
display: true,
text: '销售额 (¥)',
font: {
size: 12,
weight: '500'
}
},
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
drawBorder: false
},
ticks: {
callback: function(value) {
return '¥' + value;
},
font: {
size: 11
}
}
}
}
}
});
}
// 获取图表标题
function getChartTitle(period) {
if (period === 'week') {
return '最近1周销售额趋势';
} else if (period === 'month') {
return '最近1月销售额趋势';
} else {
return '自定义时间范围销售额趋势';
}
}
function parseOrderAmount(order) {
const amountCandidates = [
order?.amount,
order?.total_amount,
order?.order_amount,
order?.pay_amount,
order?.price
];
for (const amount of amountCandidates) {
if (amount === undefined || amount === null || amount === '') continue;
const normalized = String(amount).replace(/[^\d.-]/g, '');
if (!normalized || normalized === '-' || normalized === '.' || normalized === '-.') {
continue;
}
const numericAmount = parseFloat(normalized);
if (!Number.isNaN(numericAmount)) {
return numericAmount;
}
}
return null;
}
function formatOrderAmountDisplay(rawAmount) {
if (rawAmount === undefined || rawAmount === null) {
return '-';
}
const amountText = String(rawAmount).trim();
if (!amountText) {
return '-';
}
// 已包含货币符号时直接展示,避免重复拼接
if (/[¥¥$]/.test(amountText)) {
return amountText;
}
return `¥${amountText}`;
}
function normalizeOrderStatus(status) {
const value = String(status || '').toLowerCase();
const aliasMap = {
success: 'completed',
finished: 'completed',
pending_delivery: 'pending_ship',
partial_success: 'partial_success',
partial_pending_finalize: 'partial_pending_finalize',
delivered: 'shipped',
closed: 'cancelled',
refunded: 'cancelled',
canceled: 'cancelled'
};
return aliasMap[value] || value || 'unknown';
}
function isCompletedOrder(normalizedStatus) {
return normalizedStatus === 'completed';
}
function isSalesEligibleOrder(normalizedStatus) {
const salesEligibleStatuses = ['pending_ship', 'partial_success', 'partial_pending_finalize', 'shipped', 'completed'];
return salesEligibleStatuses.includes(normalizedStatus);
}
function isCompletionEligibleOrder(normalizedStatus) {
const completionEligibleStatuses = ['pending_ship', 'partial_success', 'partial_pending_finalize', 'shipped', 'completed', 'cancelled', 'refunding', 'refund_cancelled'];
return completionEligibleStatuses.includes(normalizedStatus);
}
function parseUtcDateTime(dateString) {
if (!dateString) return null;
if (dateString instanceof Date) {
return Number.isNaN(dateString.getTime()) ? null : dateString;
}
const raw = String(dateString).trim();
if (!raw) return null;
const normalized = raw.includes('T') ? raw : raw.replace(' ', 'T');
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(normalized);
const parsed = new Date(hasTimezone ? normalized : `${normalized}Z`);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
const beijingMinuteFormatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
hourCycle: 'h23'
});
const beijingDateFormatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const beijingSecondFormatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
hourCycle: 'h23'
});
function formatBeijingDateTime(dateString) {
const date = parseUtcDateTime(dateString);
if (!date) return '--';
const parts = {};
beijingMinuteFormatter.formatToParts(date).forEach(part => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
});
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`;
}
function formatBeijingDateTimeWithSeconds(dateInput) {
const date = parseUtcDateTime(dateInput);
if (!date) return '--';
const parts = {};
beijingSecondFormatter.formatToParts(date).forEach(part => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
});
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
}
function getBeijingDateKey(dateInput) {
const date = parseUtcDateTime(dateInput);
if (!date) return '';
const parts = {};
beijingDateFormatter.formatToParts(date).forEach(part => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
});
return `${parts.year}-${parts.month}-${parts.day}`;
}
function getEffectiveOrderSalesTime(order) {
const platformPaidAt = String(order?.platform_paid_at || '').trim();
if (platformPaidAt) return platformPaidAt;
const platformCreatedAt = String(order?.platform_created_at || '').trim();
if (platformCreatedAt) return platformCreatedAt;
const createdAt = String(order?.created_at || '').trim();
return createdAt || null;
}
function formatAboutRuntimeTime(displayValue, rawTimestamp) {
const displayText = typeof displayValue === 'string' ? displayValue.trim() : '';
if (displayText) {
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/.test(displayText)) {
return displayText.replace('T', ' ');
}
const normalizedDisplay = formatBeijingDateTimeWithSeconds(displayText);
if (normalizedDisplay !== '--') {
return normalizedDisplay;
}
return displayText;
}
const numericTimestamp = Number(rawTimestamp);
if (!Number.isFinite(numericTimestamp) || numericTimestamp <= 0) {
return '暂无记录';
}
const millis = numericTimestamp > 1e12 ? numericTimestamp : numericTimestamp * 1000;
return formatBeijingDateTimeWithSeconds(new Date(millis));
}
function isTodayOrder(createdAt) {
const orderDateKey = getBeijingDateKey(createdAt);
if (!orderDateKey) return false;
return orderDateKey === getBeijingDateKey(new Date());
}
function updateDashboardOrderMetrics(metrics) {
const totalOrdersEl = document.getElementById('dashboardOrderTotal');
const salesAmountEl = document.getElementById('dashboardSalesAmount');
const completionRateEl = document.getElementById('dashboardCompletionRate');
const todayOrdersEl = document.getElementById('dashboardTodayOrders');
if (totalOrdersEl) {
totalOrdersEl.textContent = metrics.totalOrders;
}
if (salesAmountEl) {
salesAmountEl.textContent = `¥${metrics.totalSalesAmount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}`;
}
if (completionRateEl) {
completionRateEl.textContent = `${metrics.completionRate.toFixed(1)}%`;
}
if (todayOrdersEl) {
todayOrdersEl.textContent = metrics.todayOrders;
}
}
// 更新仪表盘统计数据
function openAccountManagement(accountId) {
pendingAccountManagementFocusId = accountId || '';
const accountsSection = document.getElementById('accounts-section');
if (accountsSection && accountsSection.classList.contains('active')) {
loadCookies();
return;
}
showSection('accounts');
}
function focusPendingAccountManagementRow() {
if (!pendingAccountManagementFocusId) {
return;
}
const rows = document.querySelectorAll('#cookieTable tbody tr[data-account-id]');
const targetRow = Array.from(rows).find(row => row.dataset.accountId === pendingAccountManagementFocusId);
if (!targetRow) {
return;
}
pendingAccountManagementFocusId = '';
targetRow.classList.add('dashboard-account-focus');
targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.setTimeout(() => targetRow.classList.remove('dashboard-account-focus'), 2200);
}
async function loadDashboardDeliveryLogs() {
const tbody = document.getElementById('dashboardDeliveryLogsList');
if (!tbody) return;
try {
const response = await fetch(`${apiBase}/delivery-logs/recent?limit=20`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const logs = Array.isArray(data.logs) ? data.logs : [];
renderDashboardDeliveryLogs(logs);
} catch (error) {
console.error('加载仪表盘发货日志失败:', error);
tbody.innerHTML = `
|
发货日志加载失败
|
`;
}
}
function renderDashboardDeliveryLogs(logs) {
const tbody = document.getElementById('dashboardDeliveryLogsList');
if (!tbody) return;
tbody.innerHTML = '';
if (!logs.length) {
tbody.innerHTML = `
|
暂无发货日志
|
`;
return;
}
logs.forEach(log => {
const normalizedStatus = String(log.status || '').toLowerCase();
const isSuccess = normalizedStatus === 'success';
const isSkipped = normalizedStatus === 'skipped';
const statusBadge = isSuccess
? '成功'
: (isSkipped
? '已跳过'
: '失败');
const matchModeLabelMap = {
no_spec_match: '无规格',
one_spec_exact: '一组规格',
one_spec_fallback_no_spec: '单规兜底',
two_spec_exact: '两组规格',
blocked_no_rule: '无规则',
blocked_no_spec_parsed: '缺少规格',
blocked_multiple_no_spec_rules: '多规则阻断',
blocked_rule_mode_mismatch: '模式不一致'
};
const specModeLabelMap = {
no_spec: '无规格',
one_spec: '一组规格',
two_spec: '两组规格',
spec_enabled: '已开规格'
};
function buildBadge(text, className) {
return `${escapeHtml(text)}`;
}
let matchBadge = buildBadge(matchModeLabelMap[log.match_mode] || (log.match_mode || '未知'), 'bg-secondary');
if (log.match_mode === 'one_spec_exact' || log.match_mode === 'two_spec_exact') {
matchBadge = buildBadge(matchModeLabelMap[log.match_mode], 'bg-primary');
} else if (log.match_mode === 'one_spec_fallback_no_spec') {
matchBadge = buildBadge(matchModeLabelMap[log.match_mode], 'bg-info text-dark');
} else if (log.match_mode === 'no_spec_match') {
matchBadge = buildBadge(matchModeLabelMap[log.match_mode], 'bg-warning text-dark');
} else if (String(log.match_mode || '').startsWith('blocked_')) {
matchBadge = buildBadge(matchModeLabelMap[log.match_mode] || log.match_mode, 'bg-danger');
}
const specModes = [log.order_spec_mode, log.rule_spec_mode, log.item_config_mode].filter(Boolean);
const uniqueSpecLabels = [...new Set(specModes.map(mode => specModeLabelMap[mode] || mode))];
const hasEnabledSpecMode = specModes.some(mode => ['one_spec', 'two_spec', 'spec_enabled'].includes(mode));
const hasNoSpecMode = specModes.some(mode => mode === 'no_spec');
let specModeTitle = '';
if (log.match_mode === 'blocked_rule_mode_mismatch') {
specModeTitle = uniqueSpecLabels.join(' / ') || '规格不一致';
} else if (log.match_mode === 'two_spec_exact' || specModes.includes('two_spec')) {
specModeTitle = '两组规格';
} else if (log.match_mode === 'one_spec_exact' || log.match_mode === 'one_spec_fallback_no_spec' || specModes.includes('one_spec')) {
specModeTitle = '一组规格';
} else if (log.match_mode === 'no_spec_match' || hasNoSpecMode) {
specModeTitle = '无规格';
} else if (specModes.includes('spec_enabled')) {
specModeTitle = '已开规格';
}
let specSummary = '-';
if (log.match_mode === 'blocked_rule_mode_mismatch') {
specSummary = `${buildBadge('规格不一致', 'bg-warning text-dark')}`;
} else if (hasEnabledSpecMode || ['one_spec_exact', 'one_spec_fallback_no_spec', 'two_spec_exact'].includes(log.match_mode)) {
specSummary = `${buildBadge('已开规格', 'bg-info text-dark')}`;
} else if (hasNoSpecMode || log.match_mode === 'no_spec_match') {
specSummary = `${buildBadge('未开规格', 'bg-secondary')}`;
}
const ruleText = log.rule_keyword
? `${escapeHtml(log.rule_keyword)}
`
: '未命中规则';
const channelText = log.channel === 'manual' ? '手动' : '自动';
const channelBadgeClass = log.channel === 'manual' ? 'dashboard-delivery-channel-manual' : 'dashboard-delivery-channel-auto';
const reasonText = isSuccess
? (log.reason || '发货成功')
: (isSkipped
? (log.reason || '已跳过重复发货')
: (log.reason || '未知失败原因'));
const tr = document.createElement('tr');
tr.innerHTML = `
${escapeHtml(formatDateTime(log.created_at || ''))} |
${escapeHtml(log.order_id || '-')} |
${statusBadge} |
${ruleText} |
${matchBadge} |
${specSummary} |
${escapeHtml(channelText)}
|
${escapeHtml(reasonText)} |
`;
tbody.appendChild(tr);
});
}
// 获取账号关键词数量(带缓存)- 包含普通关键词和商品关键词
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;
}
// ================================
// 【自动回复菜单】相关功能
// ================================
// 刷新账号列表(用于自动回复页面)
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);
}
}
// 只刷新关键词列表(不重新加载商品列表等其他数据)
async function refreshKeywordsList() {
if (!currentCookieId) {
console.warn('没有选中的账号,无法刷新关键词列表');
return;
}
try {
const response = await fetch(`${apiBase}/keywords-with-item-id/${currentCookieId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
console.log('刷新关键词列表,从服务器获取的数据:', data);
// 更新缓存数据
keywordsData[currentCookieId] = data;
// 只重新渲染关键词列表
renderKeywordsList(data);
// 清除关键词缓存
clearKeywordCache();
} else {
console.error('刷新关键词列表失败:', response.status);
showToast('刷新关键词列表失败', 'danger');
}
} catch (error) {
console.error('刷新关键词列表失败:', error);
showToast('刷新关键词列表失败', 'danger');
}
}
// 加载账号关键词
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, type, image_url}, ...] 格式,直接使用
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 keywordInput = document.getElementById('newKeyword').value.trim();
const reply = document.getElementById('newReply').value.trim();
const selectElement = document.getElementById('newItemIdSelect');
const selectedOptions = Array.from(selectElement.selectedOptions);
if (!keywordInput) {
showToast('请填写关键词', 'warning');
return;
}
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
// 检查是否为编辑模式
const isEditMode = typeof window.editingIndex !== 'undefined';
const actionText = isEditMode ? '更新' : '添加';
try {
toggleLoading(true);
// 解析多个关键词(支持竖线、换行符分隔)
const keywords = keywordInput
.split(/[\|\n]/)
.map(k => k.trim())
.filter(k => k.length > 0);
if (keywords.length === 0) {
showToast('请填写有效的关键词', 'warning');
toggleLoading(false);
return;
}
// 获取选中的商品ID列表
let itemIds = selectedOptions
.map(opt => opt.value)
.filter(id => id !== ''); // 过滤掉空值(通用关键词选项)
// 如果没有选中任何商品,或者选中了空值,则作为通用关键词
if (itemIds.length === 0) {
itemIds = [''];
}
// 获取当前关键词列表
let currentKeywords = [...(keywordsData[currentCookieId] || [])];
// 如果是编辑模式,先移除原关键词
if (isEditMode) {
currentKeywords.splice(window.editingIndex, 1);
}
// 准备要保存的关键词列表(只包含文本类型的关键字)
let textKeywords = currentKeywords.filter(item => (item.type || 'text') === 'text');
// 如果是编辑模式,先移除原关键词
if (isEditMode && typeof window.editingIndex !== 'undefined') {
// 需要重新计算在文本关键字中的索引
const originalKeyword = keywordsData[currentCookieId][window.editingIndex];
const textIndex = textKeywords.findIndex(item =>
item.keyword === originalKeyword.keyword &&
(item.item_id || '') === (originalKeyword.item_id || '')
);
if (textIndex !== -1) {
textKeywords.splice(textIndex, 1);
}
}
// 检查关键词是否已存在(考虑商品ID,检查所有类型的关键词)
// 在编辑模式下,需要排除正在编辑的关键词本身
let allKeywords = keywordsData[currentCookieId] || [];
if (isEditMode && typeof window.editingIndex !== 'undefined') {
// 创建一个副本,排除正在编辑的关键词
allKeywords = allKeywords.filter((item, index) => index !== window.editingIndex);
}
// 检查重复关键词
const duplicates = [];
for (const keyword of keywords) {
for (const itemId of itemIds) {
const existingKeyword = allKeywords.find(item =>
item.keyword === keyword &&
(item.item_id || '') === (itemId || '')
);
if (existingKeyword) {
const itemIdText = itemId ? `(商品ID: ${itemId})` : '(通用关键词)';
const typeText = existingKeyword.type === 'image' ? '图片' : '文本';
duplicates.push(`"${keyword}" ${itemIdText}`);
}
}
}
if (duplicates.length > 0) {
showToast(`以下关键词已存在:\n${duplicates.join('\n')}\n请修改后重试`, 'warning');
toggleLoading(false);
return;
}
// 展开添加多个关键词和多个商品ID的组合
for (const keyword of keywords) {
for (const itemId of itemIds) {
const newKeyword = {
keyword: keyword,
reply: reply,
item_id: itemId || ''
};
textKeywords.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: textKeywords
})
});
if (response.ok) {
const totalAdded = keywords.length * itemIds.length;
const keywordText = keywords.length > 1 ? `${keywords.length}个关键词` : `"${keywords[0]}"`;
const itemText = itemIds.length > 1 ? `${itemIds.length}个商品` : (itemIds[0] ? '指定商品' : '通用');
showToast(`✨ ${keywordText} ${actionText}成功!(共${totalAdded}条配置,应用于${itemText})`, 'success');
// 清空输入框并重置样式
const keywordInputEl = document.getElementById('newKeyword');
const replyInput = document.getElementById('newReply');
const selectElement = document.getElementById('newItemIdSelect');
const addBtn = document.querySelector('.add-btn');
keywordInputEl.value = '';
replyInput.value = '';
if (selectElement) {
// 清除所有选中项
Array.from(selectElement.options).forEach(opt => opt.selected = false);
}
keywordInputEl.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(() => {
keywordInputEl.focus();
}, 100);
// 只刷新关键词列表,不重新加载整个界面
await refreshKeywordsList();
} else {
try {
const errorData = await response.json();
const errorMessage = errorData.detail || '关键词添加失败';
console.error('关键词添加失败:', errorMessage);
// 检查是否是重复关键词的错误
if (errorMessage.includes('关键词已存在') || errorMessage.includes('关键词重复') || errorMessage.includes('UNIQUE constraint')) {
showToast(`❌ 关键词重复:${errorMessage}`, 'warning');
} else {
showToast(`❌ ${errorMessage}`, 'danger');
}
} catch (parseError) {
// 如果无法解析JSON,使用原始文本
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;
}
// 按回复内容和类型分组
const groups = groupKeywordsByReply(keywords);
console.log(`开始渲染 ${groups.length} 个分组,共 ${keywords.length} 个关键词`);
groups.forEach((group, groupIndex) => {
const groupItem = document.createElement('div');
groupItem.className = 'keyword-group-item';
const isImageType = group.type === 'image';
const typeBadge = isImageType ?
' 图片' :
' 文本';
// 回复内容显示
let replyDisplay = '';
if (isImageType) {
const imageUrl = group.reply || group.image_url || '';
replyDisplay = `
回复图片:
点击图片查看大图
`;
} else {
replyDisplay = `
回复内容:
${group.reply || '(空回复,不自动回复)'}
`;
}
// 关键词列表
const keywordsList = group.keywords.map((kw, kwIndex) => `
${kw}
`).join('');
// 商品列表
const itemsList = group.items.map((itemInfo, itemIndex) => {
const itemName = getItemName(itemInfo.item_id, itemInfo.item_title);
const displayText = itemInfo.item_id ?
`${itemInfo.item_id} - ${itemName}` :
'通用关键词(所有商品)';
const icon = itemInfo.item_id ? 'bi-box' : 'bi-globe';
return `
${displayText}
`;
}).join('');
groupItem.innerHTML = `
${replyDisplay}
`;
container.appendChild(groupItem);
});
console.log('关键词列表渲染完成');
}
// 按回复内容分组关键词
function groupKeywordsByReply(keywords) {
const groupMap = new Map();
keywords.forEach((item, index) => {
// 使用回复内容+类型+图片URL作为分组键
const key = `${item.type || 'text'}:${item.reply || ''}:${item.image_url || ''}`;
if (!groupMap.has(key)) {
groupMap.set(key, {
id: `group_${groupMap.size}`,
type: item.type || 'text',
reply: item.reply || '',
image_url: item.image_url || '',
keywords: [],
items: [],
indices: [] // 保存原始索引
});
}
const group = groupMap.get(key);
// 添加关键词(去重)
if (!group.keywords.includes(item.keyword)) {
group.keywords.push(item.keyword);
}
// 添加商品(去重)
const itemId = item.item_id || '';
const existingItem = group.items.find(i => (i.item_id || '') === itemId);
if (!existingItem) {
group.items.push({
item_id: itemId,
item_title: item.item_title || '', // 添加商品名称
indices: [index]
});
} else {
existingItem.indices.push(index);
}
// 记录原始索引
group.indices.push(index);
});
return Array.from(groupMap.values());
}
// 获取商品名称(截取前30个字符)
function getItemName(itemId, itemTitle) {
if (!itemId) return '';
// 优先使用传入的商品名称
if (itemTitle && itemTitle.trim()) {
const name = itemTitle.trim();
// 截取前30个字符
return name.length > 30 ? name.substring(0, 30) + '...' : name;
}
// 从商品列表中查找商品名称
const itemsSelect = document.getElementById('newItemIdSelect');
if (itemsSelect) {
const option = Array.from(itemsSelect.options).find(opt => opt.value === itemId);
if (option && option.textContent) {
// 提取商品名称(格式:itemId - 商品名称)
const parts = option.textContent.split(' - ');
if (parts.length > 1) {
const name = parts.slice(1).join(' - ');
// 截取前30个字符
return name.length > 30 ? name.substring(0, 30) + '...' : name;
}
}
}
return '未知商品';
}
// 聚焦到关键词输入框
function focusKeywordInput() {
document.getElementById('newKeyword').focus();
}
// 编辑分组回复内容(就地编辑)
function editGroupReply(groupIndex) {
const keywords = keywordsData[currentCookieId] || [];
const groups = groupKeywordsByReply(keywords);
const group = groups[groupIndex];
if (!group) {
showToast('找不到关键词分组', 'warning');
return;
}
const container = document.getElementById(`reply-display-${groupIndex}`);
if (!container) return;
// 转义HTML用于textarea
const replyText = group.reply || '';
container.innerHTML = `
回复内容:
`;
// 聚焦并将光标移到末尾
const textarea = document.getElementById(`reply-edit-input-${groupIndex}`);
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
// 取消编辑分组回复
function cancelGroupReplyEdit(groupIndex) {
const keywords = keywordsData[currentCookieId] || [];
renderKeywordsList(keywords);
}
// 保存分组回复内容
async function saveGroupReply(groupIndex) {
const keywords = keywordsData[currentCookieId] || [];
const groups = groupKeywordsByReply(keywords);
const group = groups[groupIndex];
if (!group) {
showToast('找不到关键词分组', 'warning');
return;
}
const textarea = document.getElementById(`reply-edit-input-${groupIndex}`);
if (!textarea) return;
const newReply = textarea.value.trim();
// 更新所有属于该分组的关键词回复内容
const updatedKeywords = keywords.map((item, index) => {
if (group.indices.includes(index)) {
return { ...item, reply: newReply };
}
return item;
});
// 提取文本类型的关键词用于保存
const textKeywords = updatedKeywords.filter(item => (item.type || 'text') === 'text');
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/keywords-with-item-id/${currentCookieId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
keywords: textKeywords
})
});
if (response.ok) {
showToast(`回复内容已更新(影响${group.indices.length}条配置)`, 'success');
await refreshKeywordsList();
} else {
const errorText = await response.text();
console.error('更新回复内容失败:', errorText);
showToast('更新回复内容失败', 'danger');
}
} catch (error) {
console.error('更新回复内容失败:', error);
showToast('更新回复内容失败', 'danger');
} finally {
toggleLoading(false);
}
}
// 编辑关键词 - 改进版本
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);
// 使用新的删除API
const response = await fetch(`${apiBase}/keywords/${cookieId}/${index}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('关键词删除成功', 'success');
// 只刷新关键词列表,不重新加载整个界面
await refreshKeywordsList();
} else {
const errorText = await response.text();
console.error('关键词删除失败:', errorText);
showToast('关键词删除失败', 'danger');
}
} catch (error) {
console.error('删除关键词失败:', error);
showToast('删除关键词删除失败', 'danger');
} finally {
toggleLoading(false);
}
}
// 删除特定关键词(删除该关键词在所有商品中的配置)
async function deleteSpecificKeyword(groupId, keywordIndex) {
const keywords = keywordsData[currentCookieId] || [];
const groups = groupKeywordsByReply(keywords);
const group = groups.find(g => g.id === groupId);
if (!group) {
showToast('找不到关键词分组', 'warning');
return;
}
const targetKeyword = group.keywords[keywordIndex];
if (!confirm(`确定要删除关键词 "${targetKeyword}" 在所有商品中的配置吗?`)) {
return;
}
try {
toggleLoading(true);
// 找到所有需要删除的索引(从后往前删除,避免索引变化)
const indicesToDelete = [];
keywords.forEach((item, index) => {
if (item.keyword === targetKeyword &&
(item.type || 'text') === group.type &&
(item.reply || '') === group.reply &&
(item.image_url || '') === group.image_url) {
indicesToDelete.push(index);
}
});
// 从后往前删除
indicesToDelete.sort((a, b) => b - a);
for (const index of indicesToDelete) {
const response = await fetch(`${apiBase}/keywords/${currentCookieId}/${index}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('删除失败');
}
}
showToast(`✅ 关键词 "${targetKeyword}" 已删除(${indicesToDelete.length}条配置)`, 'success');
await refreshKeywordsList();
} catch (error) {
console.error('删除关键词失败:', error);
showToast('删除关键词失败', 'danger');
} finally {
toggleLoading(false);
}
}
// 删除特定商品的配置(删除该商品下所有关键词的配置)
async function deleteSpecificItem(groupId, itemIndex) {
const keywords = keywordsData[currentCookieId] || [];
const groups = groupKeywordsByReply(keywords);
const group = groups.find(g => g.id === groupId);
if (!group) {
showToast('找不到关键词分组', 'warning');
return;
}
const targetItem = group.items[itemIndex];
const itemId = targetItem.item_id || '';
const itemName = itemId ? `商品 ${itemId} - ${getItemName(itemId, targetItem.item_title)}` : '通用关键词(所有商品)';
if (!confirm(`确定要删除 "${itemName}" 的所有关键词配置吗?\n将删除该商品下的 ${group.keywords.length} 个关键词。`)) {
return;
}
try {
toggleLoading(true);
// 找到所有需要删除的索引
const indicesToDelete = [];
keywords.forEach((item, index) => {
if ((item.item_id || '') === itemId &&
(item.type || 'text') === group.type &&
(item.reply || '') === group.reply &&
(item.image_url || '') === group.image_url) {
indicesToDelete.push(index);
}
});
// 从后往前删除
indicesToDelete.sort((a, b) => b - a);
for (const index of indicesToDelete) {
const response = await fetch(`${apiBase}/keywords/${currentCookieId}/${index}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('删除失败');
}
}
showToast(`✅ ${itemName} 的配置已删除(${indicesToDelete.length}条)`, 'success');
await refreshKeywordsList();
} catch (error) {
console.error('删除商品配置失败:', error);
showToast('删除商品配置失败', 'danger');
} finally {
toggleLoading(false);
}
}
// 显示/隐藏加载动画
function toggleLoading(show) {
const loadingEl = document.getElementById('loading');
if (!loadingEl) return;
if (show) {
loadingRequestCount += 1;
if (loadingRequestCount === 1) {
if (loadingShowTimer) {
clearTimeout(loadingShowTimer);
}
loadingShowTimer = setTimeout(() => {
if (loadingRequestCount > 0) {
loadingEl.classList.remove('d-none');
}
loadingShowTimer = null;
}, LOADING_SHOW_DELAY);
}
return;
}
if (loadingRequestCount > 0) {
loadingRequestCount -= 1;
}
if (loadingRequestCount === 0) {
if (loadingShowTimer) {
clearTimeout(loadingShowTimer);
loadingShowTimer = null;
}
loadingEl.classList.add('d-none');
}
}
// ================================
// 通用工具函数
// ================================
// 显示提示消息
function showToast(message, type = 'success') {
// 将 'error' 类型映射为 'danger',因为 Bootstrap 使用 'danger' 作为错误类型
if (type === 'error') {
type = 'danger';
}
let toastContainer = document.querySelector('.toast-container');
// 如果 toast 容器不存在,创建一个
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
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');
const toastRow = document.createElement('div');
toastRow.className = 'd-flex';
const toastBody = document.createElement('div');
toastBody.className = 'toast-body';
toastBody.style.whiteSpace = 'pre-line';
toastBody.textContent = String(message ?? '');
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.className = 'btn-close btn-close-white me-2 m-auto';
closeButton.setAttribute('data-bs-dismiss', 'toast');
closeButton.setAttribute('aria-label', 'Close');
toastRow.appendChild(toastBody);
toastRow.appendChild(closeButton);
toast.appendChild(toastRow);
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 5000 }); // 增加显示时间到5秒
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;
}
}
// ================================
// 账号保活诊断
// ================================
function getAboutDiagnosticsElements() {
return {
accountSelect: document.getElementById('aboutDiagnosticsAccount'),
accountMeta: document.getElementById('aboutDiagnosticsAccountMeta'),
refreshButton: document.getElementById('aboutDiagnosticsRefreshBtn'),
keepaliveButton: document.getElementById('aboutDiagnosticsKeepaliveBtn'),
historyButton: document.getElementById('aboutDiagnosticsHistoryBtn'),
conversationInput: document.getElementById('aboutDiagnosticsConversationId'),
statusContainer: document.getElementById('aboutDiagnosticsStatus'),
historyContainer: document.getElementById('aboutConversationHistory'),
};
}
function getAboutSelectedAccountId() {
return document.getElementById('aboutDiagnosticsAccount')?.value?.trim() || '';
}
function getAboutStatusText(type, value) {
const normalized = String(value || '').trim();
if (!normalized) {
return '暂无';
}
const maps = {
connection: {
connected: '已连接',
reconnecting: '重连中',
connecting: '连接中',
disconnected: '未连接',
failed: '失败',
closed: '已关闭',
not_running: '未运行',
unknown: '未知',
},
keepalive: {
started: '执行中',
success: '成功',
recovered: '已恢复',
auth_failed: '鉴权失败',
api_failed: '接口失败',
network_failed: '网络异常',
response_parse_failed: '响应解析失败',
exception: '执行异常',
},
token: {
started: '执行中',
success: '成功',
skipped_cooldown: '冷却跳过',
manual_refresh_active: '手动刷新进行中',
manual_refresh_browser_stabilizing: '浏览器稳定中',
post_slider_session_settling: '滑块后稳定中',
restarted_after_cookie_refresh: '已触发重连',
captcha_max_retries_exceeded: '滑块重试超限',
token_expired_recovery_failed: '过期恢复失败',
token_refresh_failed: '刷新失败',
token_refresh_exception: '刷新异常',
token_init_failed: '初始化失败',
token_missing_after_refresh: '刷新后无 Token',
token_missing: '无 Token',
failed: '失败',
},
stream: {
healthy: '正常',
recovered: '已恢复',
warming_up: '预热中',
watching: '观察中',
recovering: '恢复中',
suspected_stale: '疑似停滞',
connection_unready: '连接未就绪',
not_running: '未运行',
},
};
return maps[type]?.[normalized] || normalized;
}
function getAboutStatusVariant(type, value) {
const normalized = String(value || '').trim();
if (!normalized) {
return 'secondary';
}
if (type === 'connection') {
if (normalized === 'connected') return 'success';
if (normalized === 'connecting' || normalized === 'reconnecting') return 'warning';
if (normalized === 'failed') return 'danger';
if (normalized === 'not_running' || normalized === 'disconnected' || normalized === 'closed') return 'secondary';
return 'info';
}
if (type === 'stream') {
if (normalized === 'healthy' || normalized === 'recovered') return 'success';
if (normalized === 'warming_up' || normalized === 'watching' || normalized === 'recovering') return 'info';
if (normalized === 'suspected_stale') return 'warning';
if (normalized === 'connection_unready' || normalized === 'not_running') return 'secondary';
return 'secondary';
}
if (normalized === 'success' || normalized === 'recovered') return 'success';
if (normalized === 'started' || normalized === 'connecting' || normalized === 'reconnecting') return 'info';
if (normalized.includes('failed') || normalized.includes('exception') || normalized.includes('error')) return 'danger';
if (normalized.includes('skipped') || normalized.includes('retry') || normalized.includes('restarted')) return 'warning';
return 'secondary';
}
function buildAboutStatusBadge(type, value) {
const text = getAboutStatusText(type, value);
const variant = getAboutStatusVariant(type, value);
return `${escapeHtml(text)}`;
}
function buildAboutMetaCard({ label, value, supporting = '' }) {
return `
${escapeHtml(label)}
${escapeHtml(value)}
${supporting ? `
${escapeHtml(supporting)}
` : ''}
`;
}
function buildAboutRuntimeStatusItem({ label, value, note = '', tone = '', richValue = false, accent = '', icon = '' }) {
return `
${icon ? `` : ''}
${escapeHtml(label)}
${richValue ? value : escapeHtml(value)}
${note ? `
${escapeHtml(note)}
` : ''}
`;
}
function buildAboutRuntimeMetaItem(label, value) {
return `
${escapeHtml(label)}
${escapeHtml(value)}
`;
}
function buildAboutReadinessValue(items) {
const normalizedItems = Array.isArray(items) ? items : [];
const totalCount = normalizedItems.length;
const readyCount = normalizedItems.filter(item => item.ready).length;
const progressPercent = totalCount
? Math.max(0, Math.min(100, Math.round((readyCount / totalCount) * 100)))
: 0;
const pendingLabels = normalizedItems
.filter(item => !item.ready)
.map(item => item.label);
let summaryNote = '暂无链路状态';
if (totalCount > 0 && pendingLabels.length === 0) {
summaryNote = '四条关键链路均已就绪';
} else if (totalCount > 0 && pendingLabels.length === totalCount) {
summaryNote = '四条关键链路均未就绪';
} else if (pendingLabels.length > 0) {
summaryNote = `待处理:${pendingLabels.join(' / ')}`;
}
return `
${readyCount}
/ ${totalCount}
关键链路已就绪
${progressPercent}% 就绪
${normalizedItems.map(item => `
${escapeHtml(item.label)}
${item.ready ? '已就绪' : '未就绪'}
`).join('')}
${escapeHtml(summaryNote)}
`;
}
function renderAboutAccountMeta(account) {
const { accountMeta } = getAboutDiagnosticsElements();
if (!accountMeta) return;
if (!account) {
accountMeta.innerHTML = '';
return;
}
const metaParts = [
buildAboutMetaCard({
label: '账号 ID',
value: account.id,
}),
buildAboutMetaCard({
label: '登录名',
value: account.username || '未设置用户名',
supporting: account.username ? '用于账号识别与后续 Cookie 刷新' : '建议补充用户名,便于后续维护',
}),
buildAboutMetaCard({
label: '备注',
value: account.remark || '未设置备注',
supporting: account.remark ? '' : '可在账号管理中补充备注',
}),
];
accountMeta.innerHTML = metaParts.join('');
}
function renderAboutDiagnosticsPlaceholder(container, icon, title, subtitle) {
if (!container) return;
container.innerHTML = `
${escapeHtml(title)}
${escapeHtml(subtitle)}
`;
}
function renderAboutRuntimePlaceholder(title, subtitle) {
const { statusContainer } = getAboutDiagnosticsElements();
renderAboutDiagnosticsPlaceholder(statusContainer, 'hdd-network', title, subtitle);
}
function renderAboutHistoryPlaceholder(title, subtitle) {
const { historyContainer } = getAboutDiagnosticsElements();
renderAboutDiagnosticsPlaceholder(historyContainer, 'clock-history', title, subtitle);
}
function getAboutRuntimeOverview(runtimeStatus, readinessCount = 0) {
if (!runtimeStatus?.running) {
return {
tone: 'danger',
title: '实例未启动',
note: '轻保活和历史消息查询都依赖账号实例,当前应先启动实例。',
};
}
if (runtimeStatus?.connection_state === 'connecting' || runtimeStatus?.connection_state === 'reconnecting') {
return {
tone: 'info',
title: '连接正在恢复',
note: '主链路还在波动,先观察连接状态与最近消息时间是否继续推进。',
};
}
if (!runtimeStatus?.ws_ready || !runtimeStatus?.session_ready || !runtimeStatus?.has_current_token || !runtimeStatus?.message_stream_ready) {
return {
tone: 'warning',
title: `${readinessCount} / 4 关键链路已就绪`,
note: '链路部分可用,优先处理未就绪项,再观察保活与消息链路。',
};
}
return {
tone: 'success',
title: '链路稳定可用',
note: '连接、轻保活、Token 与业务消息流四条主信号都处于正常状态。',
};
}
function renderAboutRuntimeStatus(runtimeStatus) {
const { statusContainer } = getAboutDiagnosticsElements();
if (!statusContainer) return;
if (!runtimeStatus) {
renderAboutRuntimePlaceholder('暂无运行态', '当前账号还没有可用的运行态信息。');
return;
}
const lastConnectionDisplay = formatAboutRuntimeTime(
runtimeStatus.last_successful_connection_at_display,
runtimeStatus.last_successful_connection_at
);
const keepaliveDisplay = formatAboutRuntimeTime(
runtimeStatus.session_keepalive_at_display,
runtimeStatus.session_keepalive_at
);
const tokenRefreshDisplay = formatAboutRuntimeTime(
runtimeStatus.token_last_refreshed_at_display,
runtimeStatus.token_last_refreshed_at
);
const lastMessageDisplay = formatAboutRuntimeTime(
runtimeStatus.last_message_received_at_display,
runtimeStatus.last_message_received_at
);
const stateChangedDisplay = formatAboutRuntimeTime(
runtimeStatus.state_last_changed_at_display,
runtimeStatus.state_last_changed_at
);
const messageStreamDisplay = getMessageStreamRuntimeDisplay(runtimeStatus);
const messageStreamStatus = messageStreamDisplay.status;
const readinessItems = [
{ label: '实例', ready: !!runtimeStatus.running },
{ label: 'WS', ready: !!runtimeStatus.ws_ready },
{ label: 'Session', ready: !!runtimeStatus.session_ready },
{ label: 'Token', ready: !!runtimeStatus.has_current_token },
{ label: '业务流', ready: !!runtimeStatus.message_stream_ready },
];
const readinessSignalItems = readinessItems.slice(1);
const readinessSignalCount = readinessSignalItems.filter(item => item.ready).length;
const overview = getAboutRuntimeOverview(runtimeStatus, readinessSignalCount);
const connectionTone = getAboutStatusVariant('connection', runtimeStatus.connection_state);
const keepaliveDisplayStatus = runtimeStatus.session_keepalive_display_status || runtimeStatus.session_keepalive_status;
const keepaliveTone = getAboutStatusVariant('keepalive', keepaliveDisplayStatus);
const tokenTone = getAboutStatusVariant('token', runtimeStatus.token_refresh_status);
const messageStreamTone = getAboutStatusVariant('stream', messageStreamStatus);
const readinessTone = readinessSignalItems.every(item => item.ready)
? 'success'
: readinessSignalItems.some(item => item.ready)
? 'warning'
: 'danger';
statusContainer.innerHTML = `
${escapeHtml(overview.title)}
${escapeHtml(overview.note)}
${buildAboutRuntimeStatusItem({
label: '连接状态',
value: buildAboutStatusBadge('connection', runtimeStatus.connection_state),
note: `最近连接成功:${lastConnectionDisplay}`,
tone: connectionTone,
richValue: true,
accent: 'connection',
icon: 'hdd-network',
})}
${buildAboutRuntimeStatusItem({
label: '轻保活状态',
value: buildAboutStatusBadge('keepalive', keepaliveDisplayStatus),
note: runtimeStatus.session_keepalive_display_note
? `最近执行:${keepaliveDisplay} · ${runtimeStatus.session_keepalive_display_note}`
: `最近执行:${keepaliveDisplay}`,
tone: keepaliveTone,
richValue: true,
accent: 'keepalive',
icon: 'heart-pulse',
})}
${buildAboutRuntimeStatusItem({
label: 'Token 刷新状态',
value: buildAboutStatusBadge('token', runtimeStatus.token_refresh_status),
note: `最近刷新:${tokenRefreshDisplay}`,
tone: tokenTone,
richValue: true,
accent: 'token',
icon: 'key',
})}
${buildAboutRuntimeStatusItem({
label: '业务消息流',
value: buildAboutStatusBadge('stream', messageStreamStatus),
note: messageStreamDisplay.note,
tone: messageStreamTone,
richValue: true,
accent: 'readiness',
icon: 'broadcast-pin',
})}
${buildAboutRuntimeMetaItem('最近收到消息', lastMessageDisplay)}
${buildAboutRuntimeMetaItem('状态变化时间', stateChangedDisplay)}
`;
}
function getAboutHistoryMessageText(message) {
if (message == null) {
return '空消息';
}
if (typeof message === 'string') {
return message;
}
if (typeof message?.text?.text === 'string' && message.text.text.trim()) {
return message.text.text;
}
if (typeof message?.raw === 'string' && message.raw.trim()) {
return message.raw;
}
try {
return JSON.stringify(message, null, 2);
} catch (error) {
return String(message);
}
}
function getAboutHistorySenderInitial(senderName) {
const normalized = String(senderName || '').trim();
if (!normalized) {
return 'U';
}
return normalized.charAt(0).toUpperCase();
}
function renderAboutConversationHistory(messages, meta = {}) {
const { historyContainer } = getAboutDiagnosticsElements();
if (!historyContainer) return;
if (!Array.isArray(messages) || messages.length === 0) {
renderAboutHistoryPlaceholder('未查询到历史消息', '确认会话 ID 是否正确,以及该账号实例是否正在运行。');
return;
}
const summaryText = `共查询到 ${messages.length} 条消息`;
const conversationIdText = meta.conversationId ? `会话 ID: ${meta.conversationId}` : '';
historyContainer.innerHTML = `
${escapeHtml(summaryText)}
${conversationIdText ? `${escapeHtml(conversationIdText)}` : ''}
${messages.map((item, index) => {
const senderName = item?.send_user_name || '未知用户';
const senderId = item?.send_user_id || '-';
const senderInitial = getAboutHistorySenderInitial(senderName);
const messageText = getAboutHistoryMessageText(item?.message);
const rawText = typeof item?.message === 'object'
? (() => {
try {
return JSON.stringify(item.message, null, 2);
} catch (error) {
return messageText;
}
})()
: messageText;
return `
${escapeHtml(messageText)}
${rawText !== messageText ? `
查看原始内容
${escapeHtml(rawText)}
` : ''}
`;
}).join('')}
`;
}
function populateAboutAccountOptions(accounts) {
const { accountSelect } = getAboutDiagnosticsElements();
if (!accountSelect) return;
if (!Array.isArray(accounts) || accounts.length === 0) {
accountSelect.innerHTML = '';
accountSelect.disabled = true;
return;
}
accountSelect.disabled = false;
accountSelect.innerHTML = `
${accounts.map(account => {
const runningSuffix = account.runtime_status?.running ? ' · 运行中' : '';
return ``;
}).join('')}
`;
}
async function loadAboutRuntimeStatus(accountId = '') {
const normalizedAccountId = String(accountId || getAboutSelectedAccountId()).trim();
if (!normalizedAccountId) {
renderAboutAccountMeta(null);
renderAboutRuntimePlaceholder('请选择账号', '选择账号后会显示当前连接状态、轻保活结果和最近活动时间。');
return;
}
const selectedAccount = aboutDiagnosticsAccounts.find(account => account.id === normalizedAccountId) || null;
renderAboutAccountMeta(selectedAccount);
renderAboutRuntimeStatus(selectedAccount?.runtime_status || null);
try {
const result = await fetchJSON(`${apiBase}/cookies/${encodeURIComponent(normalizedAccountId)}/runtime-status`);
const runtimeStatus = result?.runtime_status || null;
const targetAccount = aboutDiagnosticsAccounts.find(account => account.id === normalizedAccountId);
if (targetAccount) {
targetAccount.runtime_status = runtimeStatus;
renderAboutAccountMeta(targetAccount);
}
renderAboutRuntimeStatus(runtimeStatus);
scheduleAboutRuntimeAutoRetry(normalizedAccountId, runtimeStatus);
} catch (error) {
console.error('加载账号运行态失败:', error);
}
}
async function loadAboutDiagnostics() {
initAboutDiagnosticsEvents();
try {
const previousAccountId = getAboutSelectedAccountId();
const accounts = await fetchJSON(`${apiBase}/cookies/details`);
aboutDiagnosticsAccounts = Array.isArray(accounts) ? accounts : [];
populateAboutAccountOptions(aboutDiagnosticsAccounts);
const { accountSelect } = getAboutDiagnosticsElements();
if (!accountSelect || aboutDiagnosticsAccounts.length === 0) {
renderAboutAccountMeta(null);
renderAboutRuntimePlaceholder('暂无账号', '请先在账号管理中添加闲鱼账号。');
renderAboutHistoryPlaceholder('暂无历史消息', '请先添加账号并确保实例已启动。');
return;
}
const nextAccountId = aboutDiagnosticsAccounts.some(account => account.id === previousAccountId)
? previousAccountId
: (aboutDiagnosticsAccounts.find(account => account.runtime_status?.running)?.id || aboutDiagnosticsAccounts[0]?.id || '');
accountSelect.value = nextAccountId;
await loadAboutRuntimeStatus(nextAccountId);
} catch (error) {
console.error('加载账号保活诊断失败:', error);
}
}
async function refreshAboutDiagnosticsStatus() {
const { refreshButton } = getAboutDiagnosticsElements();
const accountId = getAboutSelectedAccountId();
if (!accountId) {
showToast('请先选择账号', 'warning');
return;
}
const originalHtml = refreshButton?.innerHTML;
if (refreshButton) {
refreshButton.disabled = true;
refreshButton.innerHTML = '刷新中...';
}
try {
await loadAboutRuntimeStatus(accountId);
showToast(`账号 "${accountId}" 运行态已刷新`, 'success');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.innerHTML = originalHtml;
}
}
}
async function triggerAboutSessionKeepalive() {
const { keepaliveButton } = getAboutDiagnosticsElements();
const accountId = getAboutSelectedAccountId();
if (!accountId) {
showToast('请先选择账号', 'warning');
return;
}
const originalHtml = keepaliveButton?.innerHTML;
if (keepaliveButton) {
keepaliveButton.disabled = true;
keepaliveButton.innerHTML = '执行中...';
}
try {
const result = await fetchJSON(`${apiBase}/cookies/${encodeURIComponent(accountId)}/session-keepalive`, {
method: 'POST',
});
const targetAccount = aboutDiagnosticsAccounts.find(account => account.id === accountId);
if (targetAccount) {
targetAccount.runtime_status = result?.runtime_status || null;
renderAboutAccountMeta(targetAccount);
}
renderAboutRuntimeStatus(result?.runtime_status || null);
showToast(result?.message || '轻保活已执行', result?.success ? 'success' : 'warning');
} catch (error) {
console.error('执行轻保活失败:', error);
} finally {
if (keepaliveButton) {
keepaliveButton.disabled = false;
keepaliveButton.innerHTML = originalHtml;
}
}
}
async function loadAboutConversationHistory() {
const { historyButton, conversationInput } = getAboutDiagnosticsElements();
const accountId = getAboutSelectedAccountId();
const conversationId = conversationInput?.value?.trim() || '';
if (!accountId) {
showToast('请先选择账号', 'warning');
return;
}
if (!conversationId) {
showToast('请输入会话 ID', 'warning');
return;
}
const originalHtml = historyButton?.innerHTML;
if (historyButton) {
historyButton.disabled = true;
historyButton.innerHTML = '查询中...';
}
renderAboutHistoryPlaceholder('正在查询历史消息', '请稍候,系统正在尝试拉取最近的会话消息。');
try {
const result = await fetchJSON(
`${apiBase}/cookies/${encodeURIComponent(accountId)}/conversations/${encodeURIComponent(conversationId)}/history`
);
renderAboutConversationHistory(result?.messages || [], {
conversationId: result?.conversation_id || conversationId,
});
showToast(`账号 "${accountId}" 历史消息查询完成`, 'success');
} catch (error) {
console.error('查询历史消息失败:', error);
renderAboutHistoryPlaceholder('历史消息查询失败', error?.message || '请稍后重试。');
} finally {
if (historyButton) {
historyButton.disabled = false;
historyButton.innerHTML = originalHtml;
}
}
}
function initAboutDiagnosticsEvents() {
if (aboutDiagnosticsInitialized) {
return;
}
const {
accountSelect,
refreshButton,
keepaliveButton,
historyButton,
conversationInput,
} = getAboutDiagnosticsElements();
accountSelect?.addEventListener('change', async () => {
renderAboutHistoryPlaceholder('暂无历史消息', '切换账号后,请重新输入会话 ID 并查询历史消息。');
await loadAboutRuntimeStatus(accountSelect.value);
});
refreshButton?.addEventListener('click', refreshAboutDiagnosticsStatus);
keepaliveButton?.addEventListener('click', triggerAboutSessionKeepalive);
historyButton?.addEventListener('click', loadAboutConversationHistory);
conversationInput?.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
loadAboutConversationHistory();
}
});
aboutDiagnosticsInitialized = true;
}
// ================================
// 【账号管理菜单】相关功能
// ================================
// 加载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'}`;
tr.dataset.accountId = cookie.id;
// 默认回复状态标签
const defaultReplyBadge = cookie.defaultReply.enabled ?
'启用' :
'禁用';
// AI回复状态标签
const aiReplyBadge = cookie.aiReply.ai_enabled ?
'AI启用' :
'AI禁用';
// 自动确认发货状态(默认开启)
const autoConfirm = cookie.auto_confirm === undefined ? true : cookie.auto_confirm;
// 自动好评状态(默认关闭)
const autoComment = cookie.auto_comment === undefined ? false : cookie.auto_comment;
tr.innerHTML = `
${cookie.id}
|
${cookie.value || '未设置'}
|
${cookie.keywordCount} 个关键词
|
|
${defaultReplyBadge}
|
${aiReplyBadge}
|
|
|
|
${cookie.pause_duration === 0 ? '不暂停' : (cookie.pause_duration || 10) + '分钟'}
|
|
`;
tbody.appendChild(tr);
});
// 为Cookie值添加点击复制功能
document.querySelectorAll('.cookie-value').forEach(element => {
element.style.cursor = 'pointer';
element.addEventListener('click', function() {
const row = this.closest('tr');
const cookieId = row?.querySelector('.cookie-id strong')?.textContent;
if (cookieId) {
copyCookie(cookieId);
}
});
});
// 重新初始化工具提示
initTooltips();
focusPendingAccountManagementRow();
} catch (err) {
// 错误已在fetchJSON中处理
} finally {
toggleLoading(false);
if (document.getElementById('accounts-section')?.classList.contains('active')) {
loadAboutDiagnostics();
}
}
}
// 复制Cookie
async function copyCookie(id) {
try {
const details = await fetchJSON(`${apiBase}/cookie/${encodeURIComponent(id)}/details?include_secrets=true`);
const value = details?.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);
});
} catch (error) {
console.error('获取Cookie详情失败:', error);
showToast('获取Cookie详情失败,请稍后重试', 'danger');
}
}
// 一键擦亮
async function polishAccountItems(accountId) {
toggleLoading(true);
showToast('正在擦亮所有商品,请稍候...', 'info');
try {
const response = await fetch(`${apiBase}/accounts/${encodeURIComponent(accountId)}/polish-items`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
if (data.success) {
showToast(`擦亮完成: ${data.polished}/${data.total} 个商品成功`, 'success');
} else {
showToast(`擦亮失败: ${data.message}`, 'danger');
}
} catch (error) {
showToast(`擦亮请求异常: ${error.message}`, 'danger');
} finally {
toggleLoading(false);
}
}
// 刷新真实Cookie
async function refreshRealCookie(cookieId) {
if (!cookieId) {
showToast('缺少账号ID', 'warning');
return;
}
// 获取当前cookie值
try {
const currentCookie = await fetchJSON(`${apiBase}/cookie/${encodeURIComponent(cookieId)}/details?include_secrets=true`);
if (!currentCookie || !currentCookie.value) {
showToast('未找到有效的Cookie信息', 'warning');
return;
}
// 确认操作
if (!confirm(`确定要刷新账号 "${cookieId}" 的真实Cookie吗?\n\n此操作将使用当前Cookie访问闲鱼IM界面获取最新的真实Cookie。`)) {
return;
}
// 显示加载状态
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.disabled = true;
button.innerHTML = '';
// 调用刷新API
const response = await fetch(`${apiBase}/qr-login/refresh-cookies`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
qr_cookies: currentCookie.value,
cookie_id: cookieId
})
});
const result = await response.json();
if (result.success) {
showToast(`账号 "${cookieId}" 真实Cookie刷新成功`, 'success');
// 刷新账号列表以显示更新后的cookie
loadCookies();
} else {
showToast(`真实Cookie刷新失败: ${result.message}`, 'danger');
}
} catch (error) {
console.error('刷新真实Cookie失败:', error);
showToast(`刷新真实Cookie失败: ${error.message || '未知错误'}`, 'danger');
} finally {
// 恢复按钮状态
const button = event.target.closest('button');
if (button) {
button.disabled = false;
button.innerHTML = '';
}
}
}
// 显示冷却状态
async function showCooldownStatus(cookieId) {
if (!cookieId) {
showToast('缺少账号ID', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/qr-login/cooldown-status/${cookieId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const { remaining_time, cooldown_duration, is_in_cooldown, remaining_minutes, remaining_seconds } = result;
let statusMessage = `账号: ${cookieId}\n`;
statusMessage += `冷却时长: ${cooldown_duration / 60}分钟\n`;
if (is_in_cooldown) {
statusMessage += `冷却状态: 进行中\n`;
statusMessage += `剩余时间: ${remaining_minutes}分${remaining_seconds}秒\n\n`;
statusMessage += `在冷却期间,_refresh_cookies_via_browser 方法将被跳过。\n\n`;
statusMessage += `是否要重置冷却时间?`;
if (confirm(statusMessage)) {
await resetCooldownTime(cookieId);
}
} else {
statusMessage += `冷却状态: 无冷却\n`;
statusMessage += `可以正常执行 _refresh_cookies_via_browser 方法`;
alert(statusMessage);
}
} else {
showToast(`获取冷却状态失败: ${result.message}`, 'danger');
}
} catch (error) {
console.error('获取冷却状态失败:', error);
showToast(`获取冷却状态失败: ${error.message || '未知错误'}`, 'danger');
}
}
// 重置冷却时间
async function resetCooldownTime(cookieId) {
if (!cookieId) {
showToast('缺少账号ID', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/qr-login/reset-cooldown/${cookieId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const previousTime = result.previous_remaining_time || 0;
const previousMinutes = Math.floor(previousTime / 60);
const previousSeconds = previousTime % 60;
let message = `账号 "${cookieId}" 的扫码登录冷却时间已重置`;
if (previousTime > 0) {
message += `\n原剩余时间: ${previousMinutes}分${previousSeconds}秒`;
}
showToast(message, 'success');
} else {
showToast(`重置冷却时间失败: ${result.message}`, 'danger');
}
} catch (error) {
console.error('重置冷却时间失败:', error);
showToast(`重置冷却时间失败: ${error.message || '未知错误'}`, 'danger');
}
}
// 删除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
async function editCookieInline(id, currentValue) {
try {
toggleLoading(true);
// 获取账号详细信息
const details = await fetchJSON(apiBase + `/cookie/${id}/details?include_secrets=true`);
// 打开编辑模态框
openAccountEditModal(details);
} catch (err) {
console.error('获取账号详情失败:', err);
showToast(`获取账号详情失败: ${err.message || '未知错误'}`, 'danger');
} finally {
toggleLoading(false);
}
}
// 打开账号编辑模态框
async function openAccountEditModal(accountData) {
// 设置模态框数据
document.getElementById('accountEditId').value = accountData.id;
document.getElementById('editAccountCookie').value = accountData.value || '';
document.getElementById('editAccountUsername').value = accountData.username || '';
document.getElementById('editAccountPassword').value = accountData.password || '';
document.getElementById('editAccountShowBrowser').checked = accountData.show_browser || false;
// 显示账号ID
document.getElementById('accountEditIdDisplay').textContent = accountData.id;
// 加载代理配置
try {
const proxyData = await fetchJSON(apiBase + `/cookie/${accountData.id}/proxy?include_secret=true`);
if (proxyData && proxyData.data) {
document.getElementById('editProxyType').value = proxyData.data.proxy_type || 'none';
document.getElementById('editProxyHost').value = proxyData.data.proxy_host || '';
document.getElementById('editProxyPort').value = proxyData.data.proxy_port || '';
document.getElementById('editProxyUser').value = proxyData.data.proxy_user || '';
document.getElementById('editProxyPass').value = proxyData.data.proxy_pass || '';
} else {
// 设置默认值
document.getElementById('editProxyType').value = 'none';
document.getElementById('editProxyHost').value = '';
document.getElementById('editProxyPort').value = '';
document.getElementById('editProxyUser').value = '';
document.getElementById('editProxyPass').value = '';
}
// 更新代理字段显示状态
toggleProxyFields();
} catch (err) {
console.error('加载代理配置失败:', err);
// 设置默认值
document.getElementById('editProxyType').value = 'none';
toggleProxyFields();
}
// 打开模态框
const modal = new bootstrap.Modal(document.getElementById('accountEditModal'));
modal.show();
// 初始化模态框中的 tooltips
setTimeout(() => {
initTooltips();
}, 100);
}
// 切换代理配置字段显示
function toggleProxyFields() {
const proxyType = document.getElementById('editProxyType').value;
const showProxy = proxyType !== 'none';
document.getElementById('proxyHostGroup').style.display = showProxy ? 'block' : 'none';
document.getElementById('proxyPortGroup').style.display = showProxy ? 'block' : 'none';
document.getElementById('proxyAuthGroup').style.display = showProxy ? 'flex' : 'none';
}
// 保存账号编辑
async function saveAccountEdit() {
const id = document.getElementById('accountEditId').value;
const cookie = document.getElementById('editAccountCookie').value.trim();
const username = document.getElementById('editAccountUsername').value.trim();
const password = document.getElementById('editAccountPassword').value.trim();
const showBrowser = document.getElementById('editAccountShowBrowser').checked;
// 代理配置
const proxyType = document.getElementById('editProxyType').value;
const proxyHost = document.getElementById('editProxyHost').value.trim();
const proxyPort = parseInt(document.getElementById('editProxyPort').value) || 0;
const proxyUser = document.getElementById('editProxyUser').value.trim();
const proxyPass = document.getElementById('editProxyPass').value.trim();
if (!cookie) {
showToast('Cookie值不能为空', 'warning');
return;
}
// 如果选择了代理,验证必要字段
if (proxyType !== 'none') {
if (!proxyHost) {
showToast('请输入代理服务器地址', 'warning');
return;
}
if (!proxyPort || proxyPort <= 0) {
showToast('请输入有效的代理端口', 'warning');
return;
}
}
try {
toggleLoading(true);
// 保存账号基本信息
await fetchJSON(apiBase + `/cookie/${id}/account-info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
value: cookie,
username: username,
password: password,
show_browser: showBrowser
})
});
// 保存代理配置
await fetchJSON(apiBase + `/cookie/${id}/proxy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
proxy_type: proxyType,
proxy_host: proxyHost,
proxy_port: proxyPort,
proxy_user: proxyUser,
proxy_pass: proxyPass
})
});
showToast(`账号 "${id}" 信息已更新`, 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('accountEditModal'));
modal.hide();
// 重新加载账号列表
loadCookies();
} catch (err) {
console.error('保存账号信息失败:', err);
showToast(`保存失败: ${err.message || '未知错误'}`, 'danger');
} finally {
toggleLoading(false);
}
}
// 保存内联编辑的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;
}
// 切换账号启用/禁用状态
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 ? '点击关闭自动确认发货' : '点击开启自动确认发货';
}
}
// 切换自动好评状态
async function toggleAutoComment(accountId, enabled) {
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/cookies/${accountId}/auto-comment`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ auto_comment: enabled })
});
if (response.ok) {
const result = await response.json();
showToast(result.message, 'success');
// 更新界面显示
updateAutoCommentRowStatus(accountId, enabled);
} else {
const error = await response.json();
showToast(error.detail || '更新自动好评设置失败', 'error');
// 恢复切换按钮状态
const toggle = document.querySelector(`input[onchange*="toggleAutoComment('${accountId}'"]`);
if (toggle) {
toggle.checked = !enabled;
}
}
} catch (error) {
console.error('切换自动好评状态失败:', error);
showToast('网络错误,请稍后重试', 'error');
// 恢复切换按钮状态
const toggle = document.querySelector(`input[onchange*="toggleAutoComment('${accountId}'"]`);
if (toggle) {
toggle.checked = !enabled;
}
} finally {
toggleLoading(false);
}
}
// 更新自动好评行状态
function updateAutoCommentRowStatus(accountId, enabled) {
const row = document.querySelector(`tr:has(input[onchange*="toggleAutoComment('${accountId}'"])`);
if (!row) return;
const statusBadge = row.querySelector('.status-badge:has(i.bi-star, i.bi-star-fill)');
const toggle = row.querySelector(`input[onchange*="toggleAutoComment('${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 ? '点击关闭自动好评' : '点击开启自动好评';
}
}
// 当前编辑的好评模板账号ID
let currentCommentTemplateAccountId = null;
// 显示好评模板管理弹窗
async function showCommentTemplates(accountId) {
currentCommentTemplateAccountId = accountId;
try {
toggleLoading(true);
// 获取好评模板列表
const response = await fetch(`${apiBase}/cookies/${accountId}/comment-templates`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取好评模板列表失败');
}
const data = await response.json();
const templates = data.templates || [];
// 生成模板列表HTML
let templatesHtml = '';
if (templates.length === 0) {
templatesHtml = '暂无好评模板,请添加
';
} else {
templatesHtml = templates.map(template => `
${escapeHtml(template.name)}
${template.is_active ? '使用中' : ''}
${escapeHtml(template.content)}
${!template.is_active ? `` : ''}
`).join('');
}
// 显示模态框
const modalHtml = `
`;
// 检查模态框是否已存在
const existingModalEl = document.getElementById('commentTemplatesModal');
if (existingModalEl) {
// 模态框已存在,只更新模板列表内容
const templatesList = existingModalEl.querySelector('#templatesList');
if (templatesList) {
templatesList.innerHTML = templatesHtml;
}
// 隐藏添加和编辑表单
const addForm = existingModalEl.querySelector('#addTemplateForm');
const editForm = existingModalEl.querySelector('#editTemplateForm');
if (addForm) addForm.style.display = 'none';
if (editForm) editForm.style.display = 'none';
} else {
// 模态框不存在,创建新的
// 先清理可能残留的遮罩层
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
// 添加新模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('commentTemplatesModal'));
modal.show();
}
} catch (error) {
console.error('获取好评模板失败:', error);
showToast('获取好评模板失败: ' + error.message, 'error');
} finally {
toggleLoading(false);
}
}
// 显示添加模板表单
function showAddCommentTemplateForm() {
document.getElementById('addTemplateForm').style.display = 'block';
document.getElementById('editTemplateForm').style.display = 'none';
document.getElementById('newTemplateName').value = '';
document.getElementById('newTemplateContent').value = '';
document.getElementById('newTemplateActive').checked = false;
}
// 隐藏添加模板表单
function hideAddCommentTemplateForm() {
document.getElementById('addTemplateForm').style.display = 'none';
}
// 添加好评模板
async function addCommentTemplate() {
const name = document.getElementById('newTemplateName').value.trim();
const content = document.getElementById('newTemplateContent').value.trim();
const isActive = document.getElementById('newTemplateActive').checked;
if (!name) {
showToast('请输入模板名称', 'warning');
return;
}
if (!content) {
showToast('请输入好评内容', 'warning');
return;
}
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/cookies/${currentCommentTemplateAccountId}/comment-templates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
name: name,
content: content,
is_active: isActive
})
});
if (response.ok) {
showToast('添加好评模板成功', 'success');
toggleLoading(false);
// 刷新模板列表
await showCommentTemplates(currentCommentTemplateAccountId);
return;
} else {
const error = await response.json();
showToast(error.detail || '添加好评模板失败', 'error');
}
} catch (error) {
console.error('添加好评模板失败:', error);
showToast('网络错误,请稍后重试', 'error');
}
toggleLoading(false);
}
// 编辑好评模板
function editCommentTemplate(templateId, name, content) {
document.getElementById('addTemplateForm').style.display = 'none';
document.getElementById('editTemplateForm').style.display = 'block';
document.getElementById('editTemplateId').value = templateId;
document.getElementById('editTemplateName').value = name;
document.getElementById('editTemplateContent').value = content;
}
// 隐藏编辑模板表单
function hideEditCommentTemplateForm() {
document.getElementById('editTemplateForm').style.display = 'none';
}
// 保存编辑的好评模板
async function saveEditCommentTemplate() {
const templateId = document.getElementById('editTemplateId').value;
const name = document.getElementById('editTemplateName').value.trim();
const content = document.getElementById('editTemplateContent').value.trim();
if (!name) {
showToast('请输入模板名称', 'warning');
return;
}
if (!content) {
showToast('请输入好评内容', 'warning');
return;
}
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/cookies/${currentCommentTemplateAccountId}/comment-templates/${templateId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
name: name,
content: content
})
});
if (response.ok) {
showToast('更新好评模板成功', 'success');
toggleLoading(false);
// 刷新模板列表
await showCommentTemplates(currentCommentTemplateAccountId);
return;
} else {
const error = await response.json();
showToast(error.detail || '更新好评模板失败', 'error');
}
} catch (error) {
console.error('更新好评模板失败:', error);
showToast('网络错误,请稍后重试', 'error');
}
toggleLoading(false);
}
// 删除好评模板
async function deleteCommentTemplate(accountId, templateId) {
if (!confirm('确定要删除此好评模板吗?')) {
return;
}
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/cookies/${accountId}/comment-templates/${templateId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('删除好评模板成功', 'success');
toggleLoading(false);
// 刷新模板列表
await showCommentTemplates(accountId);
return;
} else {
const error = await response.json();
showToast(error.detail || '删除好评模板失败', 'error');
}
} catch (error) {
console.error('删除好评模板失败:', error);
showToast('网络错误,请稍后重试', 'error');
}
toggleLoading(false);
}
// 激活好评模板
async function activateCommentTemplate(accountId, templateId) {
try {
toggleLoading(true);
const response = await fetch(`${apiBase}/cookies/${accountId}/comment-templates/${templateId}/activate`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('已切换使用此模板', 'success');
toggleLoading(false);
// 刷新模板列表
await showCommentTemplates(accountId);
return;
} else {
const error = await response.json();
showToast(error.detail || '切换模板失败', 'error');
}
} catch (error) {
console.error('切换模板失败:', error);
showToast('网络错误,请稍后重试', 'error');
}
toggleLoading(false);
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 跳转到自动回复页面并选择指定账号
function goToAutoReply(accountId) {
// 切换到自动回复页面
showSection('auto-reply');
// 设置账号选择器的值
setTimeout(() => {
const accountSelect = document.getElementById('accountSelect');
if (accountSelect) {
accountSelect.value = accountId;
// 触发change事件来加载关键词
loadAccountKeywords();
}
}, 100);
showToast(`已切换到自动回复页面,账号 "${accountId}" 已选中`, 'info');
}
// 登出功能
async function logout() {
// 停止销售额摘要定时刷新
stopSalesSummaryRefreshTimer();
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.is_admin === true) {
const adminMenuSection = document.getElementById('adminMenuSection');
if (adminMenuSection) {
adminMenuSection.style.display = 'block';
}
// 显示备份管理功能
const backupManagement = document.getElementById('backup-management');
if (backupManagement) {
backupManagement.style.display = 'block';
}
// 显示系统重启功能
const systemRestartBtn = document.getElementById('system-restart-btn');
if (systemRestartBtn) {
systemRestartBtn.style.display = 'inline-block';
}
const dashboardHotUpdateGroup = document.getElementById('dashboardHotUpdateGroup');
if (dashboardHotUpdateGroup) {
dashboardHotUpdateGroup.style.display = 'inline-flex';
}
// 显示登录与注册设置
const loginInfoSettings = document.getElementById('login-info-settings');
if (loginInfoSettings) {
loginInfoSettings.style.display = 'flex';
}
}
return true;
} catch (err) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return false;
}
}
// 初始化事件监听
document.addEventListener('DOMContentLoaded', async () => {
// 首先检查认证状态
const isAuthenticated = await checkAuth();
if (!isAuthenticated) return;
// 初始化侧边栏折叠状态
initSidebarCollapse();
// 初始化暗色模式
initDarkMode();
// 初始化账号保活诊断事件
initAboutDiagnosticsEvents();
// 加载系统版本号
loadSystemVersion();
// 加载防抖延迟设置
loadDebounceDelay();
// 启动验证会话监控
startCaptchaSessionMonitor();
// 添加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中处理
}
});
// 添加账号密码登录表单提交
const passwordLoginForm = document.getElementById('passwordLoginFormElement');
if (passwordLoginForm) {
passwordLoginForm.addEventListener('submit', handlePasswordLogin);
}
// 增强的键盘快捷键和用户体验
// textarea 中 Enter 允许换行,Ctrl+Enter 提交
document.getElementById('newKeyword')?.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
addKeyword();
}
});
document.getElementById('newReply')?.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.ctrlKey) {
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';
// 只要关键词有内容就可以添加,不需要回复内容
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 keywordInput = document.getElementById('newKeyword');
// 回复内容可以为空,只需要关键词有内容即可
if (value.length > 0) {
e.target.style.borderColor = '#10b981';
} else {
e.target.style.borderColor = '#e5e7eb';
}
// 按钮状态只依赖关键词是否有内容
const addBtn = document.querySelector('.add-btn');
if (keywordInput.value.trim().length > 0) {
addBtn.style.opacity = '1';
addBtn.style.transform = 'scale(1)';
} else {
addBtn.style.opacity = '0.7';
addBtn.style.transform = 'scale(0.95)';
}
});
// 初始加载仪表盘
loadDashboard();
// 加载菜单设置并应用
loadMenuSettings();
// 初始化图片关键词事件监听器
initImageKeywordEventListeners();
// 初始化卡券图片文件选择器
initCardImageFileSelector();
// 初始化编辑卡券图片文件选择器
initEditCardImageFileSelector();
// 初始化工具提示
initTooltips();
// 初始化商品搜索功能
initItemsSearch();
// 初始化商品搜索界面功能
initItemSearch();
// 点击侧边栏外部关闭移动端菜单
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');
}
});
});
// ==================== 默认回复管理功能 ====================
// 打开默认回复管理器
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: '', reply_once: false };
const tr = document.createElement('tr');
// 状态标签
const statusBadge = replySettings.enabled ?
'启用' :
'禁用';
// 只回复一次标签
const replyOnceBadge = replySettings.reply_once ?
'是' :
'否';
// 回复内容预览
let contentPreview = replySettings.reply_content || '未设置';
if (contentPreview.length > 50) {
contentPreview = contentPreview.substring(0, 50) + '...';
}
tr.innerHTML = `
${accountId}
|
${statusBadge} |
${replyOnceBadge} |
${contentPreview}
|
${replySettings.reply_once ? `
` : ''}
|
`;
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: '', reply_once: false };
if (response.ok) {
settings = await response.json();
}
// 填充编辑表单
document.getElementById('editDefaultReplyAccountId').value = accountId;
document.getElementById('editDefaultReplyAccountIdDisplay').value = accountId;
document.getElementById('editDefaultReplyEnabled').checked = settings.enabled;
document.getElementById('editReplyContent').value = settings.reply_content || '';
document.getElementById('editReplyOnce').checked = settings.reply_once || false;
// 根据启用状态显示/隐藏内容输入框
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('editDefaultReplyAccountId').value;
const enabled = document.getElementById('editDefaultReplyEnabled').checked;
const replyContent = document.getElementById('editReplyContent').value;
const replyOnce = document.getElementById('editReplyOnce').checked;
if (enabled && !replyContent.trim()) {
showToast('启用默认回复时必须设置回复内容', 'warning');
return;
}
const data = {
enabled: enabled,
reply_content: enabled ? replyContent : null,
reply_once: replyOnce
};
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) {
showToast('测试功能开发中...', 'info');
}
// 清空默认回复记录
async function clearDefaultReplyRecords(accountId) {
if (!confirm(`确定要清空账号 "${accountId}" 的默认回复记录吗?\n\n清空后,该账号将可以重新对之前回复过的对话进行默认回复。`)) {
return;
}
try {
const response = await fetch(`${apiBase}/default-replies/${accountId}/clear-records`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
showToast(`账号 "${accountId}" 的默认回复记录已清空`, 'success');
loadDefaultReplies(); // 刷新列表
} else {
const error = await response.text();
showToast(`清空失败: ${error}`, 'danger');
}
} catch (error) {
console.error('清空默认回复记录失败:', error);
showToast('清空默认回复记录失败', 'danger');
}
}
// ==================== AI回复配置相关函数 ====================
// 配置AI回复
async function configAIReply(accountId) {
try {
// 获取当前AI回复设置
const settings = await fetchJSON(`${apiBase}/ai-reply-settings/${accountId}`);
// 填充表单
document.getElementById('aiConfigAccountId').value = accountId;
document.getElementById('aiConfigAccountIdDisplay').value = accountId;
document.getElementById('aiReplyEnabled').checked = settings.ai_enabled;
// 处理模型名称
const modelSelect = document.getElementById('aiModelName');
const customModelInput = document.getElementById('customModelName');
const modelName = settings.model_name;
// 检查是否是预设模型
const presetModels = ['deepseek-v3.2', 'kimi-k2.5', 'qwen3-max-2026-01-23', 'qwen3.5-plus', 'gpt-4o-mini', 'gpt-4o'];
if (presetModels.includes(modelName)) {
modelSelect.value = modelName;
customModelInput.style.display = 'none';
customModelInput.value = '';
} else {
// 自定义模型
modelSelect.value = 'custom';
customModelInput.style.display = 'block';
customModelInput.value = modelName;
}
document.getElementById('aiBaseUrl').value = settings.base_url;
const normalizedApiType = settings.api_type === 'dashscope' ? '' : (settings.api_type || '');
document.getElementById('aiApiType').value = normalizedApiType;
document.getElementById('aiApiKey').value = settings.api_key;
document.getElementById('maxDiscountPercent').value = settings.max_discount_percent;
document.getElementById('maxDiscountAmount').value = settings.max_discount_amount;
document.getElementById('maxBargainRounds').value = settings.max_bargain_rounds;
// 解析自定义提示词 JSON,填入三个独立文本框
let prompts = {};
if (settings.custom_prompts) {
try { prompts = JSON.parse(settings.custom_prompts); } catch (e) { prompts = {}; }
}
document.getElementById('promptPrice').value = prompts.price || '';
document.getElementById('promptTech').value = prompts.tech || '';
document.getElementById('promptDefault').value = prompts.default || '';
// 切换设置显示状态
toggleAIReplySettings();
updateApiUrlPreview();
await loadAIPresets();
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('aiReplyConfigModal'));
modal.show();
} catch (error) {
console.error('获取AI回复设置失败:', error);
showToast('获取AI回复设置失败', 'danger');
}
}
// 更新API请求地址预览
function updateApiUrlPreview() {
const baseUrl = (document.getElementById('aiBaseUrl').value || '').replace(/\/+$/, '');
const apiType = document.getElementById('aiApiType').value;
const preview = document.getElementById('apiUrlPreview');
if (!preview || !baseUrl) {
if (preview) preview.textContent = '';
return;
}
const pathMap = {
'openai': '/v1/chat/completions',
'openai_responses': '/v1/responses',
'anthropic': '/v1/messages',
'azure_openai': '/chat/completions',
'ollama': '/v1/chat/completions',
'gemini': '',
};
let path = pathMap[apiType];
if (path === undefined) {
// 自动识别 — 默认 chat/completions
path = '/v1/chat/completions';
}
if (!path) {
// Gemini 地址格式特殊,不追加路径
preview.textContent = '请求端点预览: ' + baseUrl;
} else if (apiType === 'azure_openai') {
// Azure 不自动加 /v1
const url = baseUrl.includes('/chat/completions') ? baseUrl : baseUrl + path;
preview.textContent = '请求端点预览: ' + url;
} else {
const base = baseUrl.endsWith('/v1') ? baseUrl : baseUrl + '/v1';
const suffix = path.replace('/v1', '');
preview.textContent = '请求端点预览: ' + base + suffix;
}
}
// 切换AI回复设置显示
function toggleAIReplySettings() {
const enabled = document.getElementById('aiReplyEnabled').checked;
const settingsDiv = document.getElementById('aiReplySettings');
const bargainSettings = document.getElementById('bargainSettings');
const promptSettings = document.getElementById('promptSettings');
const testArea = document.getElementById('testArea');
if (enabled) {
settingsDiv.style.display = 'block';
bargainSettings.style.display = 'block';
promptSettings.style.display = 'block';
testArea.style.display = 'block';
} else {
settingsDiv.style.display = 'none';
bargainSettings.style.display = 'none';
promptSettings.style.display = 'none';
testArea.style.display = 'none';
}
}
// 保存AI回复配置
async function saveAIReplyConfig() {
try {
const accountId = document.getElementById('aiConfigAccountId').value;
const enabled = document.getElementById('aiReplyEnabled').checked;
// 如果启用AI回复,验证必填字段
if (enabled) {
const apiKey = document.getElementById('aiApiKey').value.trim();
if (!apiKey) {
showToast('请输入API密钥', 'warning');
return;
}
}
// 获取模型名称
let modelName = document.getElementById('aiModelName').value;
if (modelName === 'custom') {
const customModelName = document.getElementById('customModelName').value.trim();
if (!customModelName) {
showToast('请输入自定义模型名称', 'warning');
return;
}
modelName = customModelName;
}
// 从三个文本框组装自定义提示词 JSON
const promptsObj = {};
const priceVal = document.getElementById('promptPrice').value.trim();
const techVal = document.getElementById('promptTech').value.trim();
const defaultVal = document.getElementById('promptDefault').value.trim();
if (priceVal) promptsObj.price = priceVal;
if (techVal) promptsObj.tech = techVal;
if (defaultVal) promptsObj.default = defaultVal;
const customPromptsJson = Object.keys(promptsObj).length > 0 ? JSON.stringify(promptsObj) : '';
// 构建设置对象
const settings = {
ai_enabled: enabled,
model_name: modelName,
api_key: document.getElementById('aiApiKey').value,
base_url: document.getElementById('aiBaseUrl').value,
api_type: document.getElementById('aiApiType').value,
max_discount_percent: parseInt(document.getElementById('maxDiscountPercent').value),
max_discount_amount: parseInt(document.getElementById('maxDiscountAmount').value),
max_bargain_rounds: parseInt(document.getElementById('maxBargainRounds').value),
custom_prompts: customPromptsJson
};
// 保存设置
const response = await fetch(`${apiBase}/ai-reply-settings/${accountId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(settings)
});
if (response.ok) {
showToast('AI回复配置保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('aiReplyConfigModal')).hide();
loadCookies(); // 刷新账号列表以更新AI回复状态显示
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('保存AI回复配置失败:', error);
showToast('保存AI回复配置失败', 'danger');
}
}
// 测试AI回复
async function testAIReply() {
const testBtn = document.querySelector('[onclick="testAIReply()"]');
if (testBtn && testBtn.disabled) return;
if (testBtn) { testBtn.disabled = true; testBtn.textContent = '测试中...'; }
try {
const accountId = document.getElementById('aiConfigAccountId').value;
const testMessage = document.getElementById('testMessage').value.trim();
const testItemPrice = document.getElementById('testItemPrice').value;
if (!testMessage) {
showToast('请输入测试消息', 'warning');
return;
}
// 构建测试数据
const testData = {
message: testMessage,
item_title: '测试商品',
item_price: parseFloat(testItemPrice) || 100,
item_desc: '这是一个用于测试AI回复功能的商品'
};
// 显示加载状态
const testResult = document.getElementById('testResult');
const testReplyContent = document.getElementById('testReplyContent');
testResult.style.display = 'block';
testReplyContent.innerHTML = ' 正在生成AI回复...';
// 调用测试API
const response = await fetch(`${apiBase}/ai-reply-test/${accountId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(testData)
});
if (response.ok) {
const result = await response.json();
testReplyContent.innerHTML = result.reply;
showToast('AI回复测试成功', 'success');
} else {
const error = await response.text();
testReplyContent.innerHTML = `测试失败: ${error}`;
showToast(`测试失败: ${error}`, 'danger');
}
} catch (error) {
console.error('测试AI回复失败:', error);
const testReplyContent = document.getElementById('testReplyContent');
testReplyContent.innerHTML = `测试失败: ${error.message}`;
showToast('测试AI回复失败', 'danger');
} finally {
if (testBtn) { testBtn.disabled = false; testBtn.textContent = '测试回复'; }
}
}
// 切换自定义模型输入框的显示/隐藏
function toggleCustomModelInput() {
const modelSelect = document.getElementById('aiModelName');
const customModelInput = document.getElementById('customModelName');
if (modelSelect.value === 'custom') {
customModelInput.style.display = 'block';
customModelInput.focus();
} else {
customModelInput.style.display = 'none';
customModelInput.value = '';
}
}
// -------------------- AI配置预设功能 --------------------
let _aiPresets = []; // 缓存预设数据,避免依赖 option dataset
async function loadAIPresets() {
try {
const presets = await fetchJSON(`${apiBase}/ai-config-presets`);
_aiPresets = presets || [];
const select = document.getElementById('aiPresetSelect');
const deleteBtn = document.getElementById('deletePresetBtn');
select.innerHTML = '';
_aiPresets.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.preset_name;
select.appendChild(opt);
});
// 尝试自动匹配当前表单值对应的预设
_autoSelectMatchingPreset();
deleteBtn.style.display = select.value ? '' : 'none';
} catch (e) {
console.error('加载AI配置预设失败:', e);
}
}
function _autoSelectMatchingPreset() {
const select = document.getElementById('aiPresetSelect');
const modelSelect = document.getElementById('aiModelName');
const customModelInput = document.getElementById('customModelName');
const curModel = modelSelect.value === 'custom' ? customModelInput.value : modelSelect.value;
const curKey = document.getElementById('aiApiKey').value;
const curUrl = document.getElementById('aiBaseUrl').value;
const curApiType = document.getElementById('aiApiType').value;
const match = _aiPresets.find(p => {
const presetApiType = p.api_type === 'dashscope' ? '' : (p.api_type || '');
return p.model_name === curModel && p.api_key === curKey && p.base_url === curUrl && presetApiType === curApiType;
});
select.value = match ? match.id : '';
}
function loadAIPreset() {
const select = document.getElementById('aiPresetSelect');
const deleteBtn = document.getElementById('deletePresetBtn');
const presetId = select.value;
if (!presetId) {
deleteBtn.style.display = 'none';
return;
}
deleteBtn.style.display = '';
const preset = _aiPresets.find(p => String(p.id) === presetId);
if (!preset) return;
// 填充模型
const modelSelect = document.getElementById('aiModelName');
const customModelInput = document.getElementById('customModelName');
const builtinModels = Array.from(modelSelect.options).map(o => o.value).filter(v => v && v !== 'custom');
if (builtinModels.includes(preset.model_name)) {
modelSelect.value = preset.model_name;
customModelInput.style.display = 'none';
customModelInput.value = '';
} else {
modelSelect.value = 'custom';
customModelInput.style.display = 'block';
customModelInput.value = preset.model_name;
}
document.getElementById('aiBaseUrl').value = preset.base_url;
document.getElementById('aiApiKey').value = preset.api_key;
const normalizedPresetApiType = preset.api_type === 'dashscope' ? '' : (preset.api_type || '');
document.getElementById('aiApiType').value = normalizedPresetApiType;
updateApiUrlPreview();
showToast(`已切换到预设「${preset.preset_name}」`, 'success');
}
async function saveCurrentAsPreset() {
const name = prompt('请输入预设名称:');
if (!name || !name.trim()) return;
const modelSelect = document.getElementById('aiModelName');
const customModelInput = document.getElementById('customModelName');
const modelName = modelSelect.value === 'custom' ? customModelInput.value : modelSelect.value;
const apiKey = document.getElementById('aiApiKey').value;
const baseUrl = document.getElementById('aiBaseUrl').value;
if (!modelName) {
showToast('请先选择或输入模型名称', 'warning');
return;
}
try {
await fetchJSON(`${apiBase}/ai-config-presets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
preset_name: name.trim(),
model_name: modelName,
api_key: apiKey,
base_url: baseUrl,
api_type: document.getElementById('aiApiType').value
})
});
showToast('预设保存成功', 'success');
await loadAIPresets();
// 自动选中刚保存的预设
const select = document.getElementById('aiPresetSelect');
const saved = _aiPresets.find(p => p.preset_name === name.trim());
if (saved) {
select.value = saved.id;
document.getElementById('deletePresetBtn').style.display = '';
}
} catch (e) {
console.error('保存预设失败:', e);
showToast('保存预设失败', 'danger');
}
}
async function deleteSelectedPreset() {
const select = document.getElementById('aiPresetSelect');
const presetId = select.value;
if (!presetId) return;
const preset = _aiPresets.find(p => String(p.id) === presetId);
if (!preset) return;
if (!confirm(`确定删除预设「${preset.preset_name}」吗?`)) return;
try {
await fetchJSON(`${apiBase}/ai-config-presets/${presetId}`, {
method: 'DELETE'
});
showToast('预设已删除', 'success');
await loadAIPresets();
} catch (e) {
console.error('删除预设失败:', e);
showToast('删除预设失败', 'danger');
}
}
// 监听默认回复启用状态变化
document.addEventListener('DOMContentLoaded', function() {
const enabledCheckbox = document.getElementById('editDefaultReplyEnabled');
if (enabledCheckbox) {
enabledCheckbox.addEventListener('change', toggleReplyContentVisibility);
}
});
// ================================
// 【外发配置菜单】相关功能
// ================================
// 外发配置类型配置
const outgoingConfigs = {
smtp: {
title: 'SMTP邮件配置',
description: '配置SMTP服务器用于发送注册验证码等邮件通知',
icon: 'bi-envelope-fill',
color: 'primary',
fields: [
{
id: 'smtp_server',
label: 'SMTP服务器',
type: 'text',
placeholder: 'smtp.qq.com',
required: true,
help: '邮箱服务商的SMTP服务器地址,如:smtp.qq.com、smtp.gmail.com'
},
{
id: 'smtp_port',
label: 'SMTP端口',
type: 'number',
placeholder: '587',
required: true,
help: '通常为587(TLS)或465(SSL)'
},
{
id: 'smtp_user',
label: '发件邮箱',
type: 'email',
placeholder: 'your-email@qq.com',
required: true,
help: '用于发送邮件的邮箱地址'
},
{
id: 'smtp_password',
label: '邮箱密码/授权码',
type: 'password',
placeholder: '输入密码或授权码',
required: true,
help: '邮箱密码或应用专用密码(QQ邮箱需要授权码)'
},
{
id: 'smtp_from',
label: '发件人显示名(可选)',
type: 'text',
placeholder: '闲鱼管理系统',
required: false,
help: '邮件发件人显示的名称,留空则使用邮箱地址'
},
{
id: 'smtp_use_tls',
label: '启用TLS',
type: 'select',
options: [
{ value: 'true', text: '是' },
{ value: 'false', text: '否' }
],
required: true,
help: '是否启用TLS加密(推荐开启)'
},
{
id: 'smtp_use_ssl',
label: '启用SSL',
type: 'select',
options: [
{ value: 'true', text: '是' },
{ value: 'false', text: '否' }
],
required: true,
help: '是否启用SSL加密(与TLS二选一)'
}
]
}
};
// ================================
// 【通知渠道菜单】相关功能
// ================================
// 通知渠道类型配置
const channelTypeConfigs = {
qq: {
title: 'QQ通知',
description: '需要添加QQ号 3607695896 为好友才能正常接收消息通知',
icon: 'bi-chat-dots-fill',
color: 'primary',
fields: [
{
id: 'qq_number',
label: '接收QQ号码',
type: 'text',
placeholder: '输入QQ号码',
required: true,
help: '用于接收通知消息的QQ号码'
}
]
},
dingtalk: {
title: '钉钉通知',
description: '请设置钉钉机器人Webhook URL,支持自定义机器人和群机器人',
icon: 'bi-bell-fill',
color: 'info',
fields: [
{
id: 'webhook_url',
label: '钉钉机器人Webhook URL',
type: 'url',
placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=...',
required: true,
help: '钉钉机器人的Webhook地址'
},
{
id: 'secret',
label: '加签密钥(可选)',
type: 'text',
placeholder: '输入加签密钥',
required: false,
help: '如果机器人开启了加签验证,请填写密钥'
}
]
},
feishu: {
title: '飞书通知',
description: '请设置飞书机器人Webhook URL,支持自定义机器人和群机器人',
icon: 'bi-chat-square-text-fill',
color: 'warning',
fields: [
{
id: 'webhook_url',
label: '飞书机器人Webhook URL',
type: 'url',
placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/...',
required: true,
help: '飞书机器人的Webhook地址'
},
{
id: 'secret',
label: '签名密钥(可选)',
type: 'text',
placeholder: '输入签名密钥',
required: false,
help: '如果机器人开启了签名验证,请填写密钥'
}
]
},
bark: {
title: 'Bark通知',
description: 'iOS推送通知服务,支持自建服务器和官方服务器',
icon: 'bi-phone-fill',
color: 'dark',
fields: [
{
id: 'device_key',
label: '设备密钥',
type: 'text',
placeholder: '输入Bark设备密钥',
required: true,
help: 'Bark应用中显示的设备密钥'
},
{
id: 'server_url',
label: '服务器地址(可选)',
type: 'url',
placeholder: 'https://api.day.app',
required: false,
help: '自建Bark服务器地址,留空使用官方服务器'
},
{
id: 'title',
label: '通知标题(可选)',
type: 'text',
placeholder: '闲鱼管理系统通知',
required: false,
help: '推送通知的标题'
},
{
id: 'sound',
label: '提示音(可选)',
type: 'text',
placeholder: 'default',
required: false,
help: '通知提示音,如:alarm, anticipate, bell等'
},
{
id: 'group',
label: '分组(可选)',
type: 'text',
placeholder: 'xianyu',
required: false,
help: '通知分组名称,用于归类消息'
}
]
},
email: {
title: '邮件通知',
description: '通过SMTP服务器发送邮件通知,支持各种邮箱服务商',
icon: 'bi-envelope-fill',
color: 'success',
fields: [
{
id: 'smtp_server',
label: 'SMTP服务器',
type: 'text',
placeholder: 'smtp.gmail.com',
required: true,
help: '邮箱服务商的SMTP服务器地址'
},
{
id: 'smtp_port',
label: 'SMTP端口',
type: 'number',
placeholder: '587',
required: true,
help: '通常为587(TLS)或465(SSL)'
},
{
id: 'email_user',
label: '发送邮箱',
type: 'email',
placeholder: 'your-email@gmail.com',
required: true,
help: '用于发送通知的邮箱地址'
},
{
id: 'email_password',
label: '邮箱密码/授权码',
type: 'password',
placeholder: '输入密码或授权码',
required: true,
help: '邮箱密码或应用专用密码'
},
{
id: 'recipient_email',
label: '接收邮箱',
type: 'email',
placeholder: 'recipient@example.com',
required: true,
help: '用于接收通知的邮箱地址'
}
]
},
webhook: {
title: 'Webhook通知',
description: '通过HTTP POST请求发送通知到自定义的Webhook地址',
icon: 'bi-link-45deg',
color: 'warning',
fields: [
{
id: 'webhook_url',
label: 'Webhook URL',
type: 'url',
placeholder: 'https://your-server.com/webhook',
required: true,
help: '接收通知的Webhook地址'
},
{
id: 'http_method',
label: 'HTTP方法',
type: 'select',
options: [
{ value: 'POST', text: 'POST' },
{ value: 'PUT', text: 'PUT' }
],
required: true,
help: '发送请求使用的HTTP方法'
},
{
id: 'headers',
label: '自定义请求头(可选)',
type: 'textarea',
placeholder: '{"Authorization": "Bearer token", "Content-Type": "application/json"}',
required: false,
help: 'JSON格式的自定义请求头'
}
]
},
wechat: {
title: '微信通知',
description: '通过企业微信机器人发送通知消息',
icon: 'bi-wechat',
color: 'success',
fields: [
{
id: 'webhook_url',
label: '企业微信机器人Webhook URL',
type: 'url',
placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...',
required: true,
help: '企业微信群机器人的Webhook地址'
}
]
},
telegram: {
title: 'Telegram通知',
description: '通过Telegram机器人发送通知消息(需要海外服务器)',
icon: 'bi-telegram',
color: 'primary',
fields: [
{
id: 'bot_token',
label: 'Bot Token',
type: 'text',
placeholder: '123456789:ABCdefGHIjklMNOpqrsTUVwxyz',
required: true,
help: '从@BotFather获取的机器人Token'
},
{
id: 'chat_id',
label: 'Chat ID',
type: 'text',
placeholder: '123456789 或 @channel_name',
required: true,
help: '接收消息的用户ID或频道名'
}
]
}
};
// 显示添加渠道模态框
function showAddChannelModal(type) {
const config = channelTypeConfigs[type];
if (!config) {
showToast('不支持的通知渠道类型', 'danger');
return;
}
// 设置模态框标题和描述
document.getElementById('addChannelModalTitle').textContent = `添加${config.title}`;
document.getElementById('channelTypeDescription').innerHTML = config.description;
document.getElementById('channelType').value = type;
// 生成配置字段
const fieldsContainer = document.getElementById('channelConfigFields');
fieldsContainer.innerHTML = '';
config.fields.forEach(field => {
const fieldHtml = generateFieldHtml(field, 'add_');
fieldsContainer.insertAdjacentHTML('beforeend', fieldHtml);
});
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('addChannelModal'));
modal.show();
}
// 生成表单字段HTML
function generateFieldHtml(field, prefix) {
const fieldId = prefix + field.id;
let inputHtml = '';
switch (field.type) {
case 'select':
inputHtml = `';
break;
case 'textarea':
inputHtml = ``;
break;
default:
inputHtml = ``;
}
return `
${inputHtml}
${field.help ? `${field.help}` : ''}
`;
}
// 保存通知渠道
async function saveNotificationChannel() {
const type = document.getElementById('channelType').value;
const name = document.getElementById('channelName').value;
const enabled = document.getElementById('channelEnabled').checked;
const form = document.getElementById('addChannelForm');
if (!name.trim()) {
showToast('请输入渠道名称', 'warning');
return;
}
const config = channelTypeConfigs[type];
if (!config) {
showToast('无效的渠道类型', 'danger');
return;
}
// 收集配置数据
const configData = {};
let hasError = false;
config.fields.forEach(field => {
const element = form ? form.querySelector(`#add_${field.id}`) : null;
if (!element) {
showToast(`找不到${field.label}输入框`, 'danger');
hasError = true;
return;
}
const value = element.value.trim();
if (field.required && !value) {
showToast(`请填写${field.label}`, 'warning');
hasError = true;
return;
}
if (value) {
configData[field.id] = value;
}
});
if (hasError) return;
try {
const response = await fetch(`${apiBase}/notification-channels`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
type: type,
config: JSON.stringify(configData),
enabled: enabled
})
});
if (response.ok) {
showToast('通知渠道添加成功', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('addChannelModal'));
modal.hide();
loadNotificationChannels();
} else {
const error = await response.text();
showToast(`添加失败: ${error}`, 'danger');
}
} catch (error) {
console.error('添加通知渠道失败:', error);
showToast('添加通知渠道失败', 'danger');
}
}
// 加载通知渠道列表
async function loadNotificationChannels() {
try {
const response = await fetch(`${apiBase}/notification-channels`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取通知渠道失败');
}
const channels = await response.json();
renderNotificationChannels(channels);
} catch (error) {
console.error('加载通知渠道失败:', error);
showToast('加载通知渠道失败', 'danger');
}
}
// 渲染通知渠道列表
function renderNotificationChannels(channels) {
const tbody = document.getElementById('channelsTableBody');
tbody.innerHTML = '';
if (channels.length === 0) {
tbody.innerHTML = `
暂无通知渠道
点击上方按钮添加通知渠道
|
`;
return;
}
channels.forEach(channel => {
const tr = document.createElement('tr');
const statusBadge = channel.enabled ?
'启用' :
'禁用';
// 获取渠道类型配置(处理类型映射)
let channelType = channel.type;
if (channelType === 'ding_talk') {
channelType = 'dingtalk'; // 兼容旧的类型名
} else if (channelType === 'lark') {
channelType = 'feishu'; // 兼容lark类型名
}
const typeConfig = channelTypeConfigs[channelType];
const typeDisplay = typeConfig ? typeConfig.title : channel.type;
const typeColor = typeConfig ? typeConfig.color : 'secondary';
// 解析并显示配置信息
let configDisplay = '';
try {
const configData = JSON.parse(channel.config || '{}');
const configEntries = Object.entries(configData);
if (configEntries.length > 0) {
configDisplay = configEntries.map(([key, value]) => {
// 隐藏敏感信息
if (key.includes('password') || key.includes('token') || key.includes('secret')) {
return `${key}: ****`;
}
// 截断过长的值
const displayValue = value.length > 30 ? value.substring(0, 30) + '...' : value;
return `${key}: ${displayValue}`;
}).join('
');
} else {
configDisplay = channel.config || '无配置';
}
} catch (e) {
// 兼容旧格式
configDisplay = channel.config || '无配置';
if (configDisplay.length > 30) {
configDisplay = configDisplay.substring(0, 30) + '...';
}
}
tr.innerHTML = `
${channel.id} |
${channel.name}
|
${typeDisplay} |
${configDisplay} |
${statusBadge} |
|
`;
tbody.appendChild(tr);
});
}
// 删除通知渠道
async function deleteNotificationChannel(channelId) {
if (!confirm('确定要删除这个通知渠道吗?')) {
return;
}
try {
const response = await fetch(`${apiBase}/notification-channels/${channelId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('通知渠道删除成功', 'success');
loadNotificationChannels();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除通知渠道失败:', error);
showToast('删除通知渠道失败', 'danger');
}
}
// 编辑通知渠道
async function editNotificationChannel(channelId) {
try {
// 获取渠道详情
const response = await fetch(`${apiBase}/notification-channels`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取通知渠道失败');
}
const channels = await response.json();
const channel = channels.find(c => c.id === channelId);
if (!channel) {
showToast('通知渠道不存在', 'danger');
return;
}
// 处理类型映射
let channelType = channel.type;
if (channelType === 'ding_talk') {
channelType = 'dingtalk'; // 兼容旧的类型名
} else if (channelType === 'lark') {
channelType = 'feishu'; // 兼容lark类型名
}
const config = channelTypeConfigs[channelType];
if (!config) {
showToast('不支持的渠道类型', 'danger');
return;
}
// 填充基本信息
document.getElementById('editChannelId').value = channel.id;
document.getElementById('editChannelType').value = channelType; // 使用映射后的类型
document.getElementById('editChannelName').value = channel.name;
document.getElementById('editChannelEnabled').checked = channel.enabled;
// 解析配置数据
let configData = {};
try {
configData = JSON.parse(channel.config || '{}');
} catch (e) {
// 兼容旧格式(直接字符串)
if (channel.type === 'qq') {
configData = { qq_number: channel.config };
} else if (channel.type === 'dingtalk' || channel.type === 'ding_talk') {
configData = { webhook_url: channel.config };
} else if (channel.type === 'feishu' || channel.type === 'lark') {
configData = { webhook_url: channel.config };
} else if (channel.type === 'bark') {
configData = { device_key: channel.config };
} else {
configData = { config: channel.config };
}
}
// 生成编辑字段
const fieldsContainer = document.getElementById('editChannelConfigFields');
fieldsContainer.innerHTML = '';
config.fields.forEach(field => {
const fieldHtml = generateFieldHtml(field, 'edit_');
fieldsContainer.insertAdjacentHTML('beforeend', fieldHtml);
// 填充现有值
const element = document.getElementById('edit_' + field.id);
if (element && configData[field.id]) {
element.value = configData[field.id];
}
});
// 显示编辑模态框
const modal = new bootstrap.Modal(document.getElementById('editChannelModal'));
modal.show();
} catch (error) {
console.error('编辑通知渠道失败:', error);
showToast('编辑通知渠道失败', 'danger');
}
}
// 更新通知渠道
async function updateNotificationChannel() {
const channelId = document.getElementById('editChannelId').value;
const type = document.getElementById('editChannelType').value;
const name = document.getElementById('editChannelName').value;
const enabled = document.getElementById('editChannelEnabled').checked;
if (!name.trim()) {
showToast('请输入渠道名称', 'warning');
return;
}
const config = channelTypeConfigs[type];
if (!config) {
showToast('无效的渠道类型', 'danger');
return;
}
// 收集配置数据
const configData = {};
let hasError = false;
config.fields.forEach(field => {
const element = document.getElementById('edit_' + field.id);
const value = element.value.trim();
if (field.required && !value) {
showToast(`请填写${field.label}`, 'warning');
hasError = true;
return;
}
if (value) {
configData[field.id] = value;
}
});
if (hasError) return;
try {
const response = await fetch(`${apiBase}/notification-channels/${channelId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
config: JSON.stringify(configData),
enabled: enabled
})
});
if (response.ok) {
showToast('通知渠道更新成功', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('editChannelModal'));
modal.hide();
loadNotificationChannels();
} else {
const error = await response.text();
showToast(`更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('更新通知渠道失败:', error);
showToast('更新通知渠道失败', 'danger');
}
}
// ================================
// 【通知模板配置】相关功能
// ================================
// 通知模板预览数据
const templatePreviewData = {
message: {
account_id: 'test_account',
buyer_name: '张三',
buyer_id: '123456789',
item_id: '987654321',
chat_id: 'chat_001',
message: '你好,这个商品还有吗?',
time: new Date().toLocaleString('zh-CN')
},
token_refresh: {
account_id: 'test_account',
time: new Date().toLocaleString('zh-CN'),
error_message: 'Token已过期,需要重新登录',
verification_url: 'https://example.com/verify'
},
delivery: {
account_id: 'test_account',
buyer_name: '李四',
buyer_id: '234567890',
item_id: '876543210',
chat_id: 'chat_002',
result: '发货成功',
time: new Date().toLocaleString('zh-CN')
},
slider_success: {
account_id: 'test_account',
time: new Date().toLocaleString('zh-CN'),
status_text: 'cookies已自动更新到数据库'
},
face_verify: {
account_id: 'test_account',
time: new Date().toLocaleString('zh-CN'),
verification_action: '请点击验证链接完成验证:',
verification_url: 'https://passport.goofish.com/mini_login.htm?example=test',
verification_type: '身份验证'
},
password_login_success: {
account_id: 'test_account',
time: new Date().toLocaleString('zh-CN'),
cookie_count: '30'
},
cookie_refresh_success: {
account_id: 'test_account',
time: new Date().toLocaleString('zh-CN'),
cookie_count: '30'
}
};
// 加载通知模板
async function loadNotificationTemplates() {
try {
// 重置tab状态,确保只显示第一个tab
const tabContent = document.getElementById('notificationTemplateTabContent');
if (tabContent) {
// 重置所有tab-pane
tabContent.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
// 激活第一个tab-pane
const firstPane = tabContent.querySelector('#message-template');
if (firstPane) {
firstPane.classList.add('show', 'active');
}
// 重置所有tab按钮
const tabList = document.getElementById('notificationTemplateTabs');
if (tabList) {
tabList.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
link.setAttribute('aria-selected', 'false');
});
const firstTab = tabList.querySelector('#message-template-tab');
if (firstTab) {
firstTab.classList.add('active');
firstTab.setAttribute('aria-selected', 'true');
}
}
}
const response = await fetch(`${apiBase}/notification-templates`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取通知模板失败');
}
const data = await response.json();
const templates = data.templates || [];
// 加载每个模板到编辑器
templates.forEach(template => {
const editor = document.getElementById(`${template.type}-template-editor`);
if (editor) {
editor.value = template.template;
updateTemplatePreview(template.type);
}
});
// 如果没有模板数据,加载默认模板
['message', 'token_refresh', 'delivery', 'slider_success', 'face_verify'].forEach(async (type) => {
const editor = document.getElementById(`${type}-template-editor`);
if (editor && !editor.value) {
await loadDefaultTemplate(type);
}
});
showToast('通知模板加载成功', 'success');
} catch (error) {
console.error('加载通知模板失败:', error);
showToast('加载通知模板失败', 'danger');
}
}
// 加载默认模板
async function loadDefaultTemplate(templateType) {
try {
const response = await fetch(`${apiBase}/notification-templates/${templateType}/default`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const editor = document.getElementById(`${templateType}-template-editor`);
if (editor) {
editor.value = data.template;
updateTemplatePreview(templateType);
}
}
} catch (error) {
console.error(`加载默认模板失败 (${templateType}):`, error);
}
}
// 保存通知模板
async function saveNotificationTemplate(templateType) {
try {
const editor = document.getElementById(`${templateType}-template-editor`);
if (!editor) {
showToast('编辑器不存在', 'danger');
return;
}
const template = editor.value;
if (!template.trim()) {
showToast('模板内容不能为空', 'warning');
return;
}
const response = await fetch(`${apiBase}/notification-templates/${templateType}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ template })
});
if (!response.ok) {
throw new Error('保存模板失败');
}
showToast('模板保存成功', 'success');
} catch (error) {
console.error('保存通知模板失败:', error);
showToast('保存模板失败', 'danger');
}
}
// 重置通知模板
async function resetNotificationTemplate(templateType) {
if (!confirm('确定要恢复默认模板吗?当前修改将会丢失。')) {
return;
}
try {
const response = await fetch(`${apiBase}/notification-templates/${templateType}/reset`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('重置模板失败');
}
const data = await response.json();
const editor = document.getElementById(`${templateType}-template-editor`);
if (editor && data.template) {
editor.value = data.template.template;
updateTemplatePreview(templateType);
}
showToast('模板已恢复默认', 'success');
} catch (error) {
console.error('重置通知模板失败:', error);
showToast('重置模板失败', 'danger');
}
}
// 插入模板变量
function insertTemplateVariable(templateType, variable) {
const editor = document.getElementById(`${templateType}-template-editor`);
if (!editor) return;
const start = editor.selectionStart;
const end = editor.selectionEnd;
const text = editor.value;
editor.value = text.substring(0, start) + variable + text.substring(end);
editor.selectionStart = editor.selectionEnd = start + variable.length;
editor.focus();
updateTemplatePreview(templateType);
}
// 更新模板预览
function updateTemplatePreview(templateType) {
const editor = document.getElementById(`${templateType}-template-editor`);
const preview = document.getElementById(`${templateType}-template-preview`);
if (!editor || !preview) return;
let template = editor.value;
const data = templatePreviewData[templateType] || {};
// 替换变量
for (const [key, value] of Object.entries(data)) {
template = template.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
preview.textContent = template;
}
// 发送测试通知
async function testNotificationTemplate(templateType) {
const editor = document.getElementById(`${templateType}-template-editor`);
if (!editor) {
showToast('编辑器不存在', 'danger');
return;
}
const template = editor.value;
if (!template.trim()) {
showToast('模板内容不能为空', 'warning');
return;
}
// 显示发送中提示
showToast('正在发送测试通知...', 'info');
try {
const response = await fetch(`${apiBase}/notification-templates/test`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_type: templateType,
template: template
})
});
const data = await response.json();
if (response.ok) {
showToast(data.message || '测试通知发送成功', 'success');
if (data.failed_channels && data.failed_channels.length > 0) {
console.warn('部分渠道发送失败:', data.failed_channels);
}
} else {
showToast(data.detail || '测试通知发送失败', 'danger');
}
} catch (error) {
console.error('发送测试通知失败:', error);
showToast('发送测试通知失败', 'danger');
}
}
// ================================
// 【消息通知菜单】相关功能
// ================================
// 加载消息通知配置
async function loadMessageNotifications() {
try {
// 获取所有账号
const accountsResponse = await fetch(`${apiBase}/cookies`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!accountsResponse.ok) {
throw new Error('获取账号列表失败');
}
const accounts = await accountsResponse.json();
// 获取所有通知配置
const notificationsResponse = await fetch(`${apiBase}/message-notifications`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
let notifications = {};
if (notificationsResponse.ok) {
notifications = await notificationsResponse.json();
}
renderMessageNotifications(accounts, notifications);
} catch (error) {
console.error('加载消息通知配置失败:', error);
showToast('加载消息通知配置失败', 'danger');
}
}
// 渲染消息通知配置
function renderMessageNotifications(accounts, notifications) {
const tbody = document.getElementById('notificationsTableBody');
tbody.innerHTML = '';
if (accounts.length === 0) {
tbody.innerHTML = `
暂无账号数据
请先添加账号
|
`;
return;
}
accounts.forEach(accountId => {
const accountNotifications = notifications[accountId] || [];
const tr = document.createElement('tr');
let channelsList = '';
if (accountNotifications.length > 0) {
channelsList = accountNotifications.map(n =>
`${n.channel_name}`
).join('');
} else {
channelsList = '未配置';
}
const status = accountNotifications.some(n => n.enabled) ?
'启用' :
'禁用';
tr.innerHTML = `
${accountId} |
${channelsList} |
${status} |
${accountNotifications.length > 0 ? `
` : ''}
|
`;
tbody.appendChild(tr);
});
}
// 配置账号通知
async function configAccountNotification(accountId) {
try {
// 获取所有通知渠道
const channelsResponse = await fetch(`${apiBase}/notification-channels`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!channelsResponse.ok) {
throw new Error('获取通知渠道失败');
}
const channels = await channelsResponse.json();
if (channels.length === 0) {
showToast('请先添加通知渠道', 'warning');
return;
}
// 获取当前账号的通知配置
const notificationResponse = await fetch(`${apiBase}/message-notifications/${accountId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
let currentNotifications = [];
if (notificationResponse.ok) {
currentNotifications = await notificationResponse.json();
}
// 填充表单
document.getElementById('configAccountId').value = accountId;
document.getElementById('displayAccountId').value = accountId;
// 填充通知渠道选项
const channelSelect = document.getElementById('notificationChannel');
channelSelect.innerHTML = '';
// 获取当前配置的第一个通知渠道(如果存在)
const currentNotification = currentNotifications.length > 0 ? currentNotifications[0] : null;
channels.forEach(channel => {
if (channel.enabled) {
const option = document.createElement('option');
option.value = channel.id;
option.textContent = `${channel.name} (${channel.config})`;
if (currentNotification && currentNotification.channel_id === channel.id) {
option.selected = true;
}
channelSelect.appendChild(option);
}
});
// 设置启用状态
document.getElementById('notificationEnabled').checked =
currentNotification ? currentNotification.enabled : true;
// 显示配置模态框
const modal = new bootstrap.Modal(document.getElementById('configNotificationModal'));
modal.show();
} catch (error) {
console.error('配置账号通知失败:', error);
showToast('配置账号通知失败', 'danger');
}
}
// 删除账号通知配置
async function deleteAccountNotification(accountId) {
if (!confirm(`确定要删除账号 ${accountId} 的通知配置吗?`)) {
return;
}
try {
const response = await fetch(`${apiBase}/message-notifications/account/${accountId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('通知配置删除成功', 'success');
loadMessageNotifications();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除通知配置失败:', error);
showToast('删除通知配置失败', 'danger');
}
}
// 保存账号通知配置
async function saveAccountNotification() {
const accountId = document.getElementById('configAccountId').value;
const channelId = document.getElementById('notificationChannel').value;
const enabled = document.getElementById('notificationEnabled').checked;
if (!channelId) {
showToast('请选择通知渠道', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/message-notifications/${accountId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel_id: parseInt(channelId),
enabled: enabled
})
});
if (response.ok) {
showToast('通知配置保存成功', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('configNotificationModal'));
modal.hide();
loadMessageNotifications();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('保存通知配置失败:', error);
showToast('保存通知配置失败', 'danger');
}
}
// ================================
// 【卡券管理菜单】相关功能
// ================================
// 加载卡券列表
async function loadCards() {
try {
const response = await fetch(`${apiBase}/cards`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const cards = await response.json();
renderCardsList(cards);
updateCardsStats(cards);
} else {
showToast('加载卡券列表失败', 'danger');
}
} catch (error) {
console.error('加载卡券列表失败:', error);
showToast('加载卡券列表失败', 'danger');
}
}
// 渲染卡券列表
function renderCardsList(cards) {
const tbody = document.getElementById('cardsTableBody');
if (cards.length === 0) {
tbody.innerHTML = `
暂无卡券数据
点击"添加卡券"开始创建您的第一个卡券
|
`;
return;
}
tbody.innerHTML = '';
cards.forEach(card => {
const tr = document.createElement('tr');
// 类型标签
let typeBadge = '';
switch(card.type) {
case 'api':
typeBadge = 'API接口';
break;
case 'yifan_api':
typeBadge = '亦凡卡劵API';
break;
case 'text':
typeBadge = '固定文字';
break;
case 'data':
typeBadge = '批量数据';
break;
case 'image':
typeBadge = '图片';
break;
}
// 状态标签
const statusBadge = card.enabled ?
'启用' :
'禁用';
// 数据量显示
let dataCount = '-';
if (card.type === 'data' && card.data_content) {
const lines = card.data_content.split('\n').filter(line => line.trim());
dataCount = lines.length;
} else if (card.type === 'api') {
dataCount = '∞';
} else if (card.type === 'text') {
dataCount = '1';
} else if (card.type === 'image') {
dataCount = '1';
}
// 延时时间显示
const delayDisplay = card.delay_seconds > 0 ?
`${card.delay_seconds}秒` :
'立即';
// 规格信息显示
let specDisplay = '普通卡券';
if (card.is_multi_spec && card.spec_name && card.spec_value) {
let specInfo = `${card.spec_name}: ${card.spec_value}`;
if (card.spec_name_2 && card.spec_value_2) {
specInfo += `
${card.spec_name_2}: ${card.spec_value_2}`;
}
specDisplay = `${specInfo}`;
}
tr.innerHTML = `
${card.name}
${card.description ? `${card.description}` : ''}
|
${typeBadge} |
${specDisplay} |
${dataCount} |
${delayDisplay} |
${statusBadge} |
${formatDateTime(card.created_at)}
|
|
`;
tbody.appendChild(tr);
});
}
// 更新卡券统计
function updateCardsStats(cards) {
const totalCards = cards.length;
const apiCards = cards.filter(card => card.type === 'api').length;
const textCards = cards.filter(card => card.type === 'text').length;
const dataCards = cards.filter(card => card.type === 'data').length;
document.getElementById('totalCards').textContent = totalCards;
document.getElementById('apiCards').textContent = apiCards;
document.getElementById('textCards').textContent = textCards;
document.getElementById('dataCards').textContent = dataCards;
}
// 显示添加卡券模态框
function showAddCardModal() {
document.getElementById('addCardForm').reset();
toggleCardTypeFields();
const modal = new bootstrap.Modal(document.getElementById('addCardModal'));
modal.show();
}
// 切换卡券类型字段显示
function toggleCardTypeFields() {
const cardType = document.getElementById('cardType')?.value || 'text';
// 安全地设置元素显示状态
const setDisplay = (id, condition) => {
const element = document.getElementById(id);
if (element) {
element.style.display = condition ? 'block' : 'none';
}
};
setDisplay('apiFields', cardType === 'api');
setDisplay('yifanApiFields', cardType === 'yifan_api');
setDisplay('textFields', cardType === 'text');
setDisplay('dataFields', cardType === 'data');
setDisplay('imageFields', cardType === 'image');
// 如果是API类型,初始化API方法监听
if (cardType === 'api') {
toggleApiParamsHelp();
// 添加API方法变化监听
const apiMethodSelect = document.getElementById('apiMethod');
if (apiMethodSelect) {
apiMethodSelect.removeEventListener('change', toggleApiParamsHelp);
apiMethodSelect.addEventListener('change', toggleApiParamsHelp);
}
}
}
// 切换API参数提示显示
function toggleApiParamsHelp() {
const apiMethodElement = document.getElementById('apiMethod');
if (!apiMethodElement) return;
const apiMethod = apiMethodElement.value;
const postParamsHelp = document.getElementById('postParamsHelp');
if (postParamsHelp) {
postParamsHelp.style.display = apiMethod === 'POST' ? 'block' : 'none';
// 如果显示参数提示,添加点击事件
if (apiMethod === 'POST') {
initParamClickHandlers('apiParams', 'postParamsHelp');
}
}
}
// 初始化参数点击处理器
function initParamClickHandlers(textareaId, containerId) {
const container = document.getElementById(containerId);
const textarea = document.getElementById(textareaId);
if (!container || !textarea) return;
// 移除现有的点击事件监听器
const paramNames = container.querySelectorAll('.param-name');
paramNames.forEach(paramName => {
paramName.removeEventListener('click', handleParamClick);
});
// 添加新的点击事件监听器
paramNames.forEach(paramName => {
paramName.addEventListener('click', function() {
handleParamClick(this, textarea);
});
});
}
// 处理参数点击事件
function handleParamClick(paramElement, textarea) {
const paramName = paramElement.textContent.trim();
const paramValue = `{${paramName}}`;
try {
// 获取当前textarea的值
let currentValue = textarea.value.trim();
// 如果当前值为空或不是有效的JSON,创建新的JSON对象
if (!currentValue || currentValue === '{}') {
const newJson = {};
newJson[paramName] = paramValue;
textarea.value = JSON.stringify(newJson, null, 2);
} else {
// 尝试解析现有的JSON
let jsonObj;
try {
jsonObj = JSON.parse(currentValue);
} catch (e) {
// 如果解析失败,创建新的JSON对象
jsonObj = {};
}
// 添加新参数
jsonObj[paramName] = paramValue;
// 更新textarea
textarea.value = JSON.stringify(jsonObj, null, 2);
}
// 触发change事件
textarea.dispatchEvent(new Event('change'));
// 显示成功提示
showToast(`已添加参数: ${paramName}`, 'success');
} catch (error) {
console.error('添加参数时出错:', error);
showToast('添加参数失败', 'danger');
}
}
// 切换多规格字段显示
function toggleMultiSpecFields() {
const isMultiSpec = document.getElementById('isMultiSpec').checked;
document.getElementById('multiSpecFields').style.display = isMultiSpec ? 'block' : 'none';
}
// 初始化卡券图片文件选择器
function initCardImageFileSelector() {
const fileInput = document.getElementById('cardImageFile');
if (fileInput) {
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
showToast('❌ 请选择图片文件,当前文件类型:' + file.type, 'warning');
e.target.value = '';
hideCardImagePreview();
return;
}
// 验证文件大小(5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('❌ 图片文件大小不能超过 5MB,当前文件大小:' + (file.size / 1024 / 1024).toFixed(1) + 'MB', 'warning');
e.target.value = '';
hideCardImagePreview();
return;
}
// 验证图片尺寸
validateCardImageDimensions(file, e.target);
} else {
hideCardImagePreview();
}
});
}
}
// 验证卡券图片尺寸
function validateCardImageDimensions(file, inputElement) {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = function() {
const width = this.naturalWidth;
const height = this.naturalHeight;
// 释放对象URL
URL.revokeObjectURL(url);
// 检查图片尺寸
const maxDimension = 4096;
const maxPixels = 8 * 1024 * 1024; // 8M像素
const totalPixels = width * height;
if (width > maxDimension || height > maxDimension) {
showToast(`❌ 图片尺寸过大:${width}x${height},最大允许:${maxDimension}x${maxDimension}像素`, 'warning');
inputElement.value = '';
hideCardImagePreview();
return;
}
if (totalPixels > maxPixels) {
showToast(`❌ 图片像素总数过大:${(totalPixels / 1024 / 1024).toFixed(1)}M像素,最大允许:8M像素`, 'warning');
inputElement.value = '';
hideCardImagePreview();
return;
}
// 尺寸检查通过,显示预览和提示信息
showCardImagePreview(file);
// 如果图片较大,提示会被压缩
if (width > 2048 || height > 2048) {
showToast(`ℹ️ 图片尺寸较大(${width}x${height}),上传时将自动压缩以优化性能`, 'info');
} else {
showToast(`✅ 图片尺寸合适(${width}x${height}),可以上传`, 'success');
}
};
img.onerror = function() {
URL.revokeObjectURL(url);
showToast('❌ 无法读取图片文件,请选择有效的图片', 'warning');
inputElement.value = '';
hideCardImagePreview();
};
img.src = url;
}
// 显示卡券图片预览
function showCardImagePreview(file) {
const reader = new FileReader();
reader.onload = function(e) {
const previewContainer = document.getElementById('cardImagePreview');
const previewImg = document.getElementById('cardPreviewImg');
previewImg.src = e.target.result;
previewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
}
// 隐藏卡券图片预览
function hideCardImagePreview() {
const previewContainer = document.getElementById('cardImagePreview');
if (previewContainer) {
previewContainer.style.display = 'none';
}
}
// 初始化编辑卡券图片文件选择器
function initEditCardImageFileSelector() {
const fileInput = document.getElementById('editCardImageFile');
if (fileInput) {
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
showToast('❌ 请选择图片文件,当前文件类型:' + file.type, 'warning');
e.target.value = '';
hideEditCardImagePreview();
return;
}
// 验证文件大小(5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('❌ 图片文件大小不能超过 5MB,当前文件大小:' + (file.size / 1024 / 1024).toFixed(1) + 'MB', 'warning');
e.target.value = '';
hideEditCardImagePreview();
return;
}
// 验证图片尺寸
validateEditCardImageDimensions(file, e.target);
} else {
hideEditCardImagePreview();
}
});
}
}
// 验证编辑卡券图片尺寸
function validateEditCardImageDimensions(file, inputElement) {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = function() {
const width = this.naturalWidth;
const height = this.naturalHeight;
URL.revokeObjectURL(url);
// 检查尺寸限制
if (width > 4096 || height > 4096) {
showToast(`❌ 图片尺寸过大(${width}x${height}),最大支持 4096x4096 像素`, 'warning');
inputElement.value = '';
hideEditCardImagePreview();
return;
}
// 显示图片预览
showEditCardImagePreview(file);
// 如果图片较大,提示会被压缩
if (width > 2048 || height > 2048) {
showToast(`ℹ️ 图片尺寸较大(${width}x${height}),上传时将自动压缩以优化性能`, 'info');
} else {
showToast(`✅ 图片尺寸合适(${width}x${height}),可以上传`, 'success');
}
};
img.onerror = function() {
URL.revokeObjectURL(url);
showToast('❌ 无法读取图片文件,请选择有效的图片', 'warning');
inputElement.value = '';
hideEditCardImagePreview();
};
img.src = url;
}
// 显示编辑卡券图片预览
function showEditCardImagePreview(file) {
const reader = new FileReader();
reader.onload = function(e) {
const previewImg = document.getElementById('editCardPreviewImg');
const previewContainer = document.getElementById('editCardImagePreview');
if (previewImg && previewContainer) {
previewImg.src = e.target.result;
previewContainer.style.display = 'block';
}
};
reader.readAsDataURL(file);
}
// 隐藏编辑卡券图片预览
function hideEditCardImagePreview() {
const previewContainer = document.getElementById('editCardImagePreview');
if (previewContainer) {
previewContainer.style.display = 'none';
}
}
// 切换编辑多规格字段显示
function toggleEditMultiSpecFields() {
const checkbox = document.getElementById('editIsMultiSpec');
const fieldsDiv = document.getElementById('editMultiSpecFields');
if (!checkbox) {
console.error('编辑多规格开关元素未找到');
return;
}
if (!fieldsDiv) {
console.error('编辑多规格字段容器未找到');
return;
}
const isMultiSpec = checkbox.checked;
const displayStyle = isMultiSpec ? 'block' : 'none';
console.log('toggleEditMultiSpecFields - 多规格状态:', isMultiSpec);
console.log('toggleEditMultiSpecFields - 设置显示样式:', displayStyle);
fieldsDiv.style.display = displayStyle;
// 验证设置是否生效
console.log('toggleEditMultiSpecFields - 实际显示样式:', fieldsDiv.style.display);
}
// 清空添加卡券表单
function clearAddCardForm() {
try {
// 安全地清空表单字段
const setElementValue = (id, value) => {
const element = document.getElementById(id);
if (element) {
if (element.type === 'checkbox') {
element.checked = value;
} else {
element.value = value;
}
} else {
console.warn(`Element with id '${id}' not found`);
}
};
const setElementDisplay = (id, display) => {
const element = document.getElementById(id);
if (element) {
element.style.display = display;
} else {
console.warn(`Element with id '${id}' not found`);
}
};
// 清空基本字段
setElementValue('cardName', '');
setElementValue('cardType', 'text');
setElementValue('cardDescription', '');
setElementValue('cardDelaySeconds', '0');
setElementValue('isMultiSpec', false);
setElementValue('specName', '');
setElementValue('specValue', '');
setElementValue('specName2', '');
setElementValue('specValue2', '');
// 隐藏多规格字段
setElementDisplay('multiSpecFields', 'none');
// 清空类型相关字段
setElementValue('textContent', '');
setElementValue('dataContent', '');
setElementValue('apiUrl', '');
setElementValue('apiMethod', 'GET');
setElementValue('apiHeaders', '');
setElementValue('apiParams', '');
setElementValue('apiTimeout', '10');
setElementValue('yifanUserId', '');
setElementValue('yifanUserKey', '');
setElementValue('yifanGoodsId', '');
setElementValue('yifanCallbackUrl', '');
setElementValue('yifanRequireAccount', false);
// 重置字段显示
toggleCardTypeFields();
} catch (error) {
console.error('清空表单时出错:', error);
}
}
// 保存卡券
async function saveCard() {
try {
const cardType = document.getElementById('cardType').value;
const cardName = document.getElementById('cardName').value;
if (!cardType || !cardName) {
showToast('请填写必填字段', 'warning');
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('isMultiSpec').checked;
const specName = document.getElementById('specName').value;
const specValue = document.getElementById('specValue').value;
const specName2 = document.getElementById('specName2').value;
const specValue2 = document.getElementById('specValue2').value;
// 调试日志
console.log('[DEBUG] 创建卡券 - isMultiSpec:', isMultiSpec);
console.log('[DEBUG] 创建卡券 - specName:', specName);
console.log('[DEBUG] 创建卡券 - specValue:', specValue);
console.log('[DEBUG] 创建卡券 - specName2:', specName2);
console.log('[DEBUG] 创建卡券 - specValue2:', specValue2);
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格1名称和规格1值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('cardDescription').value,
delay_seconds: parseInt(document.getElementById('cardDelaySeconds').value) || 0,
enabled: true,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null,
spec_name_2: isMultiSpec ? specName2 : null,
spec_value_2: isMultiSpec ? specValue2 : null
};
// 调试日志 - 显示完整的 cardData
console.log('[DEBUG] 创建卡券 - 发送的 cardData:', JSON.stringify(cardData, null, 2));
// 根据类型添加特定配置
switch(cardType) {
case 'api':
// 验证和解析JSON字段
let headers = '{}';
let params = '{}';
try {
const headersInput = document.getElementById('apiHeaders').value.trim();
if (headersInput) {
JSON.parse(headersInput); // 验证JSON格式
headers = headersInput;
}
} catch (e) {
showToast('请求头格式错误,请输入有效的JSON', 'warning');
return;
}
try {
const paramsInput = document.getElementById('apiParams').value.trim();
if (paramsInput) {
JSON.parse(paramsInput); // 验证JSON格式
params = paramsInput;
}
} catch (e) {
showToast('请求参数格式错误,请输入有效的JSON', 'warning');
return;
}
cardData.api_config = {
url: document.getElementById('apiUrl').value,
method: document.getElementById('apiMethod').value,
timeout: parseInt(document.getElementById('apiTimeout').value),
headers: headers,
params: params
};
break;
case 'yifan_api':
// 验证必填字段
const yifanUserId = document.getElementById('yifanUserId').value.trim();
const yifanUserKey = document.getElementById('yifanUserKey').value.trim();
const yifanGoodsId = document.getElementById('yifanGoodsId').value.trim();
if (!yifanUserId || !yifanUserKey || !yifanGoodsId) {
showToast('请填写商户ID、商户KEY和商品ID', 'warning');
return;
}
// 亦凡API配置也存储在api_config字段中
cardData.api_config = {
user_id: yifanUserId,
user_key: yifanUserKey,
goods_id: yifanGoodsId,
callback_url: document.getElementById('yifanCallbackUrl').value.trim(),
require_account: document.getElementById('yifanRequireAccount').checked
};
break;
case 'text':
cardData.text_content = document.getElementById('textContent').value;
break;
case 'data':
cardData.data_content = document.getElementById('dataContent').value;
break;
case 'image':
// 处理图片上传
const imageFile = document.getElementById('cardImageFile').files[0];
if (!imageFile) {
showToast('请选择图片文件', 'warning');
return;
}
// 上传图片
const formData = new FormData();
formData.append('image', imageFile);
const uploadResponse = await fetch(`${apiBase}/upload-image`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (!uploadResponse.ok) {
const errorData = await uploadResponse.json();
showToast(`图片上传失败: ${errorData.detail || '未知错误'}`, 'danger');
return;
}
const uploadResult = await uploadResponse.json();
cardData.image_url = uploadResult.image_url;
break;
}
// 获取"生成对应发货规则"开关状态
const generateDeliveryRule = document.getElementById('generateDeliveryRule').checked;
const response = await fetch(`${apiBase}/cards`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...cardData,
generate_delivery_rule: generateDeliveryRule
})
});
if (response.ok) {
showToast('卡券保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('addCardModal')).hide();
// 清空表单
clearAddCardForm();
loadCards();
} else {
let errorMessage = '保存失败';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.detail || errorMessage;
} catch (e) {
// 如果不是JSON格式,尝试获取文本
try {
const errorText = await response.text();
errorMessage = errorText || errorMessage;
} catch (e2) {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
}
showToast(`保存失败: ${errorMessage}`, 'danger');
}
} catch (error) {
console.error('保存卡券失败:', error);
showToast(`网络错误: ${error.message}`, 'danger');
}
}
// ================================
// 【自动发货菜单】相关功能
// ================================
// 加载发货规则列表
async function loadDeliveryRules() {
try {
const response = await fetch(`${apiBase}/delivery-rules`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const rules = await response.json();
renderDeliveryRulesList(rules);
updateDeliveryStats(rules);
// 同时加载卡券列表用于下拉选择
loadCardsForSelect();
} else {
showToast('加载发货规则失败', 'danger');
}
} catch (error) {
console.error('加载发货规则失败:', error);
showToast('加载发货规则失败', 'danger');
}
}
// 渲染发货规则列表
function renderDeliveryRulesList(rules) {
const tbody = document.getElementById('deliveryRulesTableBody');
if (rules.length === 0) {
tbody.innerHTML = `
暂无发货规则
点击"添加规则"开始配置自动发货规则
|
`;
return;
}
tbody.innerHTML = '';
rules.forEach(rule => {
const tr = document.createElement('tr');
// 状态标签
const statusBadge = rule.enabled ?
'启用' :
'禁用';
// 卡券类型标签
let cardTypeBadge = '未知';
if (rule.card_type) {
switch(rule.card_type) {
case 'api':
cardTypeBadge = 'API接口';
break;
case 'yifan_api':
cardTypeBadge = '亦凡卡劵API';
break;
case 'text':
cardTypeBadge = '固定文字';
break;
case 'data':
cardTypeBadge = '批量数据';
break;
case 'image':
cardTypeBadge = '图片';
break;
}
}
tr.innerHTML = `
${rule.keyword}
${rule.description ? `${rule.description}` : ''}
|
${rule.card_name || '未知卡券'}
${rule.is_multi_spec && rule.spec_name && rule.spec_value ?
` ${rule.spec_name}: ${rule.spec_value}${rule.spec_name_2 && rule.spec_value_2 ? ` ${rule.spec_name_2}: ${rule.spec_value_2}` : ''}` :
''}
|
${cardTypeBadge} |
${statusBadge} |
${rule.delivery_times || 0}
|
|
`;
tbody.appendChild(tr);
});
}
// 更新发货统计
async function updateDeliveryStats(rules) {
const totalRules = rules.length;
const activeRules = rules.filter(rule => rule.enabled).length;
const totalDeliveries = rules.reduce((sum, rule) => sum + (rule.delivery_times || 0), 0);
document.getElementById('totalRules').textContent = totalRules;
document.getElementById('activeRules').textContent = activeRules;
document.getElementById('totalDeliveries').textContent = totalDeliveries;
// 刷新今日发货统计
await refreshTodayDeliveryCount();
}
// 刷新今日发货统计(独立函数,可在发货后单独调用)
async function refreshTodayDeliveryCount() {
try {
const response = await fetch(`${apiBase}/delivery-rules/stats`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const stats = await response.json();
const todayEl = document.getElementById('todayDeliveries');
if (todayEl) {
todayEl.textContent = stats.today_delivery_count || 0;
}
}
} catch (error) {
console.error('获取今日发货统计失败:', error);
}
}
// 显示添加发货规则模态框
function showAddDeliveryRuleModal() {
document.getElementById('addDeliveryRuleForm').reset();
loadCardsForSelect(); // 加载卡券选项
const modal = new bootstrap.Modal(document.getElementById('addDeliveryRuleModal'));
modal.show();
}
// 加载卡券列表用于下拉选择
async function loadCardsForSelect() {
try {
const response = await fetch(`${apiBase}/cards`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const cards = await response.json();
const select = document.getElementById('selectedCard');
// 清空现有选项
select.innerHTML = '';
cards.forEach(card => {
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
let typeText;
switch(card.type) {
case 'api':
typeText = 'API';
break;
case 'text':
typeText = '固定文字';
break;
case 'data':
typeText = '批量数据';
break;
case 'image':
typeText = '图片';
break;
default:
typeText = '未知类型';
}
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
let specInfo = `${card.spec_name}:${card.spec_value}`;
if (card.spec_name_2 && card.spec_value_2) {
specInfo += `, ${card.spec_name_2}:${card.spec_value_2}`;
}
displayText += ` [${specInfo}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
}
} catch (error) {
console.error('加载卡券选项失败:', error);
}
}
// 保存发货规则
async function saveDeliveryRule() {
try {
const keyword = document.getElementById('productKeyword').value;
const cardId = document.getElementById('selectedCard').value;
const deliveryCount = document.getElementById('deliveryCount').value || 1;
const enabled = document.getElementById('ruleEnabled').checked;
const description = document.getElementById('ruleDescription').value;
if (!keyword || !cardId) {
showToast('请填写必填字段', 'warning');
return;
}
const ruleData = {
keyword: keyword,
card_id: parseInt(cardId),
delivery_count: parseInt(deliveryCount),
enabled: enabled,
description: description
};
const response = await fetch(`${apiBase}/delivery-rules`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(ruleData)
});
if (response.ok) {
showToast('发货规则保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('addDeliveryRuleModal')).hide();
loadDeliveryRules();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('保存发货规则失败:', error);
showToast('保存发货规则失败', 'danger');
}
}
// 编辑卡券
async function editCard(cardId) {
try {
// 获取卡券详情
const response = await fetch(`${apiBase}/cards/${cardId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const card = await response.json();
// 填充编辑表单
document.getElementById('editCardId').value = card.id;
document.getElementById('editCardName').value = card.name;
document.getElementById('editCardType').value = card.type;
document.getElementById('editCardDescription').value = card.description || '';
document.getElementById('editCardDelaySeconds').value = card.delay_seconds || 0;
document.getElementById('editCardEnabled').checked = card.enabled;
// 填充多规格字段
const isMultiSpec = card.is_multi_spec || false;
document.getElementById('editIsMultiSpec').checked = isMultiSpec;
document.getElementById('editSpecName').value = card.spec_name || '';
document.getElementById('editSpecValue').value = card.spec_value || '';
document.getElementById('editSpecName2').value = card.spec_name_2 || '';
document.getElementById('editSpecValue2').value = card.spec_value_2 || '';
// 添加调试日志
console.log('编辑卡券 - 多规格状态:', isMultiSpec);
console.log('编辑卡券 - 规格1名称:', card.spec_name);
console.log('编辑卡券 - 规格1值:', card.spec_value);
console.log('编辑卡券 - 规格2名称:', card.spec_name_2);
console.log('编辑卡券 - 规格2值:', card.spec_value_2);
// 根据类型填充特定字段
if (card.type === 'api' && card.api_config) {
document.getElementById('editApiUrl').value = card.api_config.url || '';
document.getElementById('editApiMethod').value = card.api_config.method || 'GET';
document.getElementById('editApiTimeout').value = card.api_config.timeout || 10;
document.getElementById('editApiHeaders').value = card.api_config.headers || '{}';
document.getElementById('editApiParams').value = card.api_config.params || '{}';
} else if (card.type === 'yifan_api' && card.api_config) {
document.getElementById('editYifanUserId').value = card.api_config.user_id || '';
document.getElementById('editYifanUserKey').value = card.api_config.user_key || '';
document.getElementById('editYifanGoodsId').value = card.api_config.goods_id || '';
document.getElementById('editYifanCallbackUrl').value = card.api_config.callback_url || '';
document.getElementById('editYifanRequireAccount').checked = card.api_config.require_account || false;
} else if (card.type === 'text') {
document.getElementById('editTextContent').value = card.text_content || '';
} else if (card.type === 'data') {
document.getElementById('editDataContent').value = card.data_content || '';
} else if (card.type === 'image') {
// 处理图片类型
const currentImagePreview = document.getElementById('editCurrentImagePreview');
const currentImg = document.getElementById('editCurrentImg');
const noImageText = document.getElementById('editNoImageText');
if (card.image_url) {
// 显示当前图片
currentImg.src = card.image_url;
currentImagePreview.style.display = 'block';
noImageText.style.display = 'none';
} else {
// 没有图片
currentImagePreview.style.display = 'none';
noImageText.style.display = 'block';
}
// 清空文件选择器和预览
document.getElementById('editCardImageFile').value = '';
document.getElementById('editCardImagePreview').style.display = 'none';
}
// 显示对应的字段
toggleEditCardTypeFields();
// 使用延迟调用确保DOM更新完成后再显示多规格字段
setTimeout(() => {
console.log('延迟调用 toggleEditMultiSpecFields');
toggleEditMultiSpecFields();
// 验证多规格字段是否正确显示
const multiSpecElement = document.getElementById('editMultiSpecFields');
const isChecked = document.getElementById('editIsMultiSpec').checked;
console.log('多规格元素存在:', !!multiSpecElement);
console.log('多规格开关状态:', isChecked);
console.log('多规格字段显示状态:', multiSpecElement ? multiSpecElement.style.display : 'element not found');
}, 100);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editCardModal'));
modal.show();
} else {
showToast('获取卡券详情失败', 'danger');
}
} catch (error) {
console.error('获取卡券详情失败:', error);
showToast('获取卡券详情失败', 'danger');
}
}
// 切换编辑卡券类型字段显示
function toggleEditCardTypeFields() {
const cardType = document.getElementById('editCardType').value;
document.getElementById('editApiFields').style.display = cardType === 'api' ? 'block' : 'none';
document.getElementById('editYifanApiFields').style.display = cardType === 'yifan_api' ? 'block' : 'none';
document.getElementById('editTextFields').style.display = cardType === 'text' ? 'block' : 'none';
document.getElementById('editDataFields').style.display = cardType === 'data' ? 'block' : 'none';
document.getElementById('editImageFields').style.display = cardType === 'image' ? 'block' : 'none';
// 如果是API类型,初始化API方法监听
if (cardType === 'api') {
toggleEditApiParamsHelp();
// 添加API方法变化监听
const editApiMethodSelect = document.getElementById('editApiMethod');
if (editApiMethodSelect) {
editApiMethodSelect.removeEventListener('change', toggleEditApiParamsHelp);
editApiMethodSelect.addEventListener('change', toggleEditApiParamsHelp);
}
}
}
// 切换编辑API参数提示显示
function toggleEditApiParamsHelp() {
const apiMethod = document.getElementById('editApiMethod').value;
const editPostParamsHelp = document.getElementById('editPostParamsHelp');
if (editPostParamsHelp) {
editPostParamsHelp.style.display = apiMethod === 'POST' ? 'block' : 'none';
// 如果显示参数提示,添加点击事件
if (apiMethod === 'POST') {
initParamClickHandlers('editApiParams', 'editPostParamsHelp');
}
}
}
// 更新卡券
async function updateCard() {
try {
const cardId = document.getElementById('editCardId').value;
const cardType = document.getElementById('editCardType').value;
const cardName = document.getElementById('editCardName').value;
if (!cardType || !cardName) {
showToast('请填写必填字段', 'warning');
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('editIsMultiSpec').checked;
const specName = document.getElementById('editSpecName').value;
const specValue = document.getElementById('editSpecValue').value;
const specName2 = document.getElementById('editSpecName2').value;
const specValue2 = document.getElementById('editSpecValue2').value;
// 调试日志
console.log('[DEBUG] 更新卡券 - isMultiSpec:', isMultiSpec);
console.log('[DEBUG] 更新卡券 - specName:', specName);
console.log('[DEBUG] 更新卡券 - specValue:', specValue);
console.log('[DEBUG] 更新卡券 - specName2:', specName2);
console.log('[DEBUG] 更新卡券 - specValue2:', specValue2);
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格1名称和规格1值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('editCardDescription').value,
delay_seconds: parseInt(document.getElementById('editCardDelaySeconds').value) || 0,
enabled: document.getElementById('editCardEnabled').checked,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null,
spec_name_2: isMultiSpec ? specName2 : null,
spec_value_2: isMultiSpec ? specValue2 : null
};
// 调试日志 - 显示完整的 cardData
console.log('[DEBUG] 发送的 cardData:', JSON.stringify(cardData, null, 2));
// 根据类型添加特定配置
switch(cardType) {
case 'api':
// 验证和解析JSON字段
let headers = '{}';
let params = '{}';
try {
const headersInput = document.getElementById('editApiHeaders').value.trim();
if (headersInput) {
JSON.parse(headersInput);
headers = headersInput;
}
} catch (e) {
showToast('请求头格式错误,请输入有效的JSON', 'warning');
return;
}
try {
const paramsInput = document.getElementById('editApiParams').value.trim();
if (paramsInput) {
JSON.parse(paramsInput);
params = paramsInput;
}
} catch (e) {
showToast('请求参数格式错误,请输入有效的JSON', 'warning');
return;
}
cardData.api_config = {
url: document.getElementById('editApiUrl').value,
method: document.getElementById('editApiMethod').value,
timeout: parseInt(document.getElementById('editApiTimeout').value),
headers: headers,
params: params
};
break;
case 'yifan_api':
// 验证必填字段
const editYifanUserId = document.getElementById('editYifanUserId').value.trim();
const editYifanUserKey = document.getElementById('editYifanUserKey').value.trim();
const editYifanGoodsId = document.getElementById('editYifanGoodsId').value.trim();
if (!editYifanUserId || !editYifanUserKey || !editYifanGoodsId) {
showToast('请填写商户ID、商户KEY和商品ID', 'warning');
return;
}
// 亦凡API配置也存储在api_config字段中
cardData.api_config = {
user_id: editYifanUserId,
user_key: editYifanUserKey,
goods_id: editYifanGoodsId,
callback_url: document.getElementById('editYifanCallbackUrl').value.trim(),
require_account: document.getElementById('editYifanRequireAccount').checked
};
break;
case 'text':
cardData.text_content = document.getElementById('editTextContent').value;
break;
case 'data':
cardData.data_content = document.getElementById('editDataContent').value;
break;
case 'image':
// 处理图片类型 - 如果有新图片则上传,否则保持原有图片
const imageFile = document.getElementById('editCardImageFile').files[0];
if (imageFile) {
// 有新图片,需要上传
await updateCardWithImage(cardId, cardData, imageFile);
return; // 提前返回,因为上传图片是异步的
}
// 没有新图片,保持原有配置,继续正常更新流程
break;
}
const response = await fetch(`${apiBase}/cards/${cardId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(cardData)
});
if (response.ok) {
showToast('卡券更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editCardModal')).hide();
loadCards();
} else {
const error = await response.text();
showToast(`更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('更新卡券失败:', error);
showToast('更新卡券失败', 'danger');
}
}
// 更新带图片的卡券
async function updateCardWithImage(cardId, cardData, imageFile) {
try {
// 创建FormData对象
const formData = new FormData();
// 添加图片文件
formData.append('image', imageFile);
// 添加卡券数据
Object.keys(cardData).forEach(key => {
if (cardData[key] !== null && cardData[key] !== undefined) {
if (typeof cardData[key] === 'object') {
formData.append(key, JSON.stringify(cardData[key]));
} else {
formData.append(key, cardData[key]);
}
}
});
const response = await fetch(`${apiBase}/cards/${cardId}/image`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`
// 不设置Content-Type,让浏览器自动设置multipart/form-data
},
body: formData
});
if (response.ok) {
showToast('卡券更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editCardModal')).hide();
loadCards();
} else {
const error = await response.text();
showToast(`更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('更新带图片的卡券失败:', error);
showToast('更新卡券失败', 'danger');
}
}
// 测试卡券(占位函数)
function testCard(cardId) {
showToast('测试功能开发中...', 'info');
}
// 删除卡券
async function deleteCard(cardId) {
if (confirm('确定要删除这个卡券吗?删除后无法恢复!')) {
try {
const response = await fetch(`${apiBase}/cards/${cardId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('卡券删除成功', 'success');
loadCards();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除卡券失败:', error);
showToast('删除卡券失败', 'danger');
}
}
}
// 编辑发货规则
async function editDeliveryRule(ruleId) {
try {
// 获取发货规则详情
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const rule = await response.json();
// 填充编辑表单
document.getElementById('editRuleId').value = rule.id;
document.getElementById('editProductKeyword').value = rule.keyword;
document.getElementById('editDeliveryCount').value = rule.delivery_count || 1;
document.getElementById('editRuleEnabled').checked = rule.enabled;
document.getElementById('editRuleDescription').value = rule.description || '';
// 加载卡券选项并设置当前选中的卡券
await loadCardsForEditSelect();
document.getElementById('editSelectedCard').value = rule.card_id;
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editDeliveryRuleModal'));
modal.show();
} else {
showToast('获取发货规则详情失败', 'danger');
}
} catch (error) {
console.error('获取发货规则详情失败:', error);
showToast('获取发货规则详情失败', 'danger');
}
}
// 加载卡券列表用于编辑时的下拉选择
async function loadCardsForEditSelect() {
try {
const response = await fetch(`${apiBase}/cards`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const cards = await response.json();
const select = document.getElementById('editSelectedCard');
// 清空现有选项
select.innerHTML = '';
cards.forEach(card => {
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
let typeText;
switch(card.type) {
case 'api':
typeText = 'API';
break;
case 'text':
typeText = '固定文字';
break;
case 'data':
typeText = '批量数据';
break;
case 'image':
typeText = '图片';
break;
default:
typeText = '未知类型';
}
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
let specInfo = `${card.spec_name}:${card.spec_value}`;
if (card.spec_name_2 && card.spec_value_2) {
specInfo += `, ${card.spec_name_2}:${card.spec_value_2}`;
}
displayText += ` [${specInfo}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
}
} catch (error) {
console.error('加载卡券选项失败:', error);
}
}
// 更新发货规则
async function updateDeliveryRule() {
try {
const ruleId = document.getElementById('editRuleId').value;
const keyword = document.getElementById('editProductKeyword').value;
const cardId = document.getElementById('editSelectedCard').value;
const deliveryCount = document.getElementById('editDeliveryCount').value || 1;
const enabled = document.getElementById('editRuleEnabled').checked;
const description = document.getElementById('editRuleDescription').value;
if (!keyword || !cardId) {
showToast('请填写必填字段', 'warning');
return;
}
const ruleData = {
keyword: keyword,
card_id: parseInt(cardId),
delivery_count: parseInt(deliveryCount),
enabled: enabled,
description: description
};
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(ruleData)
});
if (response.ok) {
showToast('发货规则更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editDeliveryRuleModal')).hide();
loadDeliveryRules();
} else {
const error = await response.text();
showToast(`更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('更新发货规则失败:', error);
showToast('更新发货规则失败', 'danger');
}
}
// 测试发货规则(占位函数)
function testDeliveryRule(ruleId) {
showToast('测试功能开发中...', 'info');
}
// 删除发货规则
async function deleteDeliveryRule(ruleId) {
if (confirm('确定要删除这个发货规则吗?删除后无法恢复!')) {
try {
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('发货规则删除成功', 'success');
loadDeliveryRules();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除发货规则失败:', error);
showToast('删除发货规则失败', 'danger');
}
}
}
// ==================== 系统设置功能 ====================
// 加载用户设置
async function loadUserSettings() {
try {
const response = await fetch(`${apiBase}/user-settings`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
// 设置主题颜色
if (settings.theme_color && settings.theme_color.value) {
const color = settings.theme_color.value;
const picker = document.getElementById('themeColorPicker');
const hex = document.getElementById('themeColorHex');
if (picker) picker.value = color;
if (hex) hex.value = color;
applyThemeColor(color);
updatePresetSelection(color);
} else {
localStorage.removeItem('themeColor');
}
}
} catch (error) {
console.error('加载用户设置失败:', error);
}
}
// 应用主题颜色(支持任意十六进制颜色)
function applyThemeColor(color) {
if (!color || !color.startsWith('#')) return;
document.documentElement.style.setProperty('--primary-color', color);
// 计算hover颜色(稍微深一点)
const hoverColor = adjustBrightness(color, -20);
document.documentElement.style.setProperty('--primary-hover', hoverColor);
// 计算浅色版本(用于某些UI元素)
const lightColor = adjustBrightness(color, 40);
document.documentElement.style.setProperty('--primary-light', lightColor);
// 缓存主题色,供页面首次渲染前预应用,避免刷新闪回默认蓝色
localStorage.setItem('themeColor', color);
}
// 调整颜色亮度
function adjustBrightness(hex, percent) {
const num = parseInt(hex.replace("#", ""), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
}
// 更新预设颜色按钮选中状态
function updatePresetSelection(selectedColor) {
document.querySelectorAll('.color-preset').forEach(btn => {
if (btn.dataset.color === selectedColor) {
btn.style.border = '2px solid #333';
btn.style.boxShadow = '0 0 0 2px #fff, 0 0 0 4px #333';
} else {
btn.style.border = '2px solid transparent';
btn.style.boxShadow = 'none';
}
});
}
// ==================== 菜单管理功能 ====================
// 菜单项配置(默认顺序)
const DEFAULT_MENU_ITEMS = [
{ id: 'dashboard', name: '仪表盘', icon: 'bi-speedometer2', required: true },
{ id: 'accounts', name: '账号管理', icon: 'bi-person-circle', required: false },
{ id: 'items', name: '商品管理', icon: 'bi-box-seam', required: false },
{ id: 'orders', name: '订单管理', icon: 'bi-receipt-cutoff', required: false },
{ id: 'auto-reply', name: '自动回复', icon: 'bi-chat-left-text', required: false },
{ id: 'items-reply', name: '指定商品回复', icon: 'bi-chat-left-text', required: false },
{ id: 'cards', name: '卡券管理', icon: 'bi-credit-card', required: false },
{ id: 'auto-delivery', name: '自动发货', icon: 'bi-truck', required: false },
{ id: 'notification-channels', name: '通知渠道', icon: 'bi-bell', required: false },
{ id: 'message-notifications', name: '消息通知', icon: 'bi-chat-dots', required: false },
{ id: 'online-im', name: '在线客服', icon: 'bi-headset', required: false },
{ id: 'system-settings', name: '系统设置', icon: 'bi-gear', required: true },
{ id: 'about', name: '关于', icon: 'bi-info-circle', required: true }
];
// 当前菜单设置
let menuSettings = {}; // 显示/隐藏设置
let menuOrder = []; // 菜单顺序
let draggedItem = null; // 当前拖拽的元素
// 获取排序后的菜单项
function getSortedMenuItems() {
if (menuOrder.length === 0) {
return [...DEFAULT_MENU_ITEMS];
}
// 按保存的顺序排列
const sorted = [];
menuOrder.forEach(id => {
const item = DEFAULT_MENU_ITEMS.find(m => m.id === id);
if (item) sorted.push(item);
});
// 添加可能遗漏的新菜单项
DEFAULT_MENU_ITEMS.forEach(item => {
if (!sorted.find(m => m.id === item.id)) {
sorted.push(item);
}
});
return sorted;
}
// 初始化菜单管理UI
function initMenuManagement() {
const container = document.getElementById('menuManagementList');
if (!container) return;
const sortedItems = getSortedMenuItems();
container.innerHTML = sortedItems.map(item => `
`).join('');
// 绑定拖拽事件
initDragAndDrop();
}
// 初始化拖拽功能
function initDragAndDrop() {
const container = document.getElementById('menuManagementList');
if (!container) return;
const items = container.querySelectorAll('.menu-sort-item');
items.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
item.addEventListener('drop', handleDrop);
});
}
function handleDragStart(e) {
draggedItem = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
document.querySelectorAll('.menu-sort-item').forEach(item => {
item.classList.remove('drag-over');
});
draggedItem = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
if (this !== draggedItem) {
this.classList.add('drag-over');
}
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
if (draggedItem !== this) {
const container = document.getElementById('menuManagementList');
const items = Array.from(container.querySelectorAll('.menu-sort-item'));
const draggedIndex = items.indexOf(draggedItem);
const targetIndex = items.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedItem, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedItem, this);
}
}
this.classList.remove('drag-over');
return false;
}
// 获取当前菜单顺序
function getCurrentMenuOrder() {
const container = document.getElementById('menuManagementList');
if (!container) return [];
const items = container.querySelectorAll('.menu-sort-item');
return Array.from(items).map(item => item.dataset.menuId);
}
// 保存菜单设置(包括顺序和显示/隐藏)
async function saveMenuSettings() {
// 获取显示/隐藏设置
const visibility = {};
DEFAULT_MENU_ITEMS.forEach(item => {
if (!item.required) {
const checkbox = document.getElementById(`menu-${item.id}`);
if (checkbox) {
visibility[item.id] = checkbox.checked;
}
}
});
// 获取顺序
const order = getCurrentMenuOrder();
try {
// 保存显示设置
await fetch(`${apiBase}/user-settings/menu_visibility`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
value: JSON.stringify(visibility),
description: '菜单显示设置'
})
});
// 保存顺序设置
await fetch(`${apiBase}/user-settings/menu_order`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
value: JSON.stringify(order),
description: '菜单顺序设置'
})
});
menuSettings = visibility;
menuOrder = order;
applyMenuSettings();
showToast('菜单设置保存成功', 'success');
} catch (error) {
console.error('保存菜单设置失败:', error);
showToast('保存菜单设置失败', 'danger');
}
}
// 重置菜单设置
async function resetMenuSettings() {
try {
// 重置显示设置
await fetch(`${apiBase}/user-settings/menu_visibility`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
value: JSON.stringify({}),
description: '菜单显示设置'
})
});
// 重置顺序设置
await fetch(`${apiBase}/user-settings/menu_order`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
value: JSON.stringify([]),
description: '菜单顺序设置'
})
});
menuSettings = {};
menuOrder = [];
// 重新初始化UI
initMenuManagement();
applyMenuSettings();
showToast('菜单设置已恢复默认', 'success');
} catch (error) {
console.error('重置菜单设置失败:', error);
showToast('重置菜单设置失败', 'danger');
}
}
// 应用菜单设置(顺序和显示/隐藏)
function applyMenuSettings() {
const sidebar = document.querySelector('.sidebar-nav');
if (!sidebar) return;
const sortedItems = getSortedMenuItems();
// 按顺序重新排列侧边栏菜单(普通菜单项使用 0-99)
sortedItems.forEach((item, index) => {
const menuItem = sidebar.querySelector(`.nav-item[data-menu-id="${item.id}"]`);
if (menuItem) {
// 设置显示/隐藏
if (!item.required) {
const isVisible = menuSettings[item.id] !== false;
menuItem.style.display = isVisible ? '' : 'none';
}
// 设置顺序(通过CSS order属性)
menuItem.style.order = index;
}
});
// 确保管理员菜单区块在普通菜单之后(order: 100)
const adminSection = document.getElementById('adminMenuSection');
if (adminSection) {
adminSection.style.order = 100;
}
// 底部分隔符和登出按钮在最后(order: 200+)
const dividers = sidebar.querySelectorAll('.nav-divider');
dividers.forEach((divider, idx) => {
// 跳过管理员区块内的分隔符
if (!divider.closest('#adminMenuSection')) {
divider.style.order = 200 + idx;
}
});
// 登出按钮(没有data-menu-id的nav-item)在最后
const logoutItem = sidebar.querySelector('.nav-item:not([data-menu-id])');
if (logoutItem) {
logoutItem.style.order = 999;
}
}
// 兼容旧函数名
function applyMenuVisibility() {
applyMenuSettings();
}
// 加载菜单设置
async function loadMenuSettings() {
try {
const response = await fetch(`${apiBase}/user-settings`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
// 加载显示设置
if (settings.menu_visibility && settings.menu_visibility.value) {
try {
menuSettings = JSON.parse(settings.menu_visibility.value);
} catch (e) {
menuSettings = {};
}
}
// 加载顺序设置
if (settings.menu_order && settings.menu_order.value) {
try {
menuOrder = JSON.parse(settings.menu_order.value);
} catch (e) {
menuOrder = [];
}
}
applyMenuSettings();
}
} catch (error) {
console.error('加载菜单设置失败:', error);
}
}
// 主题表单提交处理
document.addEventListener('DOMContentLoaded', function() {
// 颜色选择器同步
const themeColorPicker = document.getElementById('themeColorPicker');
const themeColorHex = document.getElementById('themeColorHex');
if (themeColorPicker && themeColorHex) {
themeColorPicker.addEventListener('input', function() {
themeColorHex.value = this.value;
applyThemeColor(this.value);
updatePresetSelection(this.value);
});
themeColorHex.addEventListener('input', function() {
if (/^#[0-9A-Fa-f]{6}$/.test(this.value)) {
themeColorPicker.value = this.value;
applyThemeColor(this.value);
updatePresetSelection(this.value);
}
});
}
// 预设颜色按钮点击
document.querySelectorAll('.color-preset').forEach(btn => {
btn.addEventListener('click', function() {
const color = this.dataset.color;
if (themeColorPicker) themeColorPicker.value = color;
if (themeColorHex) themeColorHex.value = color;
applyThemeColor(color);
updatePresetSelection(color);
});
});
const themeForm = document.getElementById('themeForm');
if (themeForm) {
themeForm.addEventListener('submit', async function(e) {
e.preventDefault();
const themeColor = document.getElementById('themeColorHex')?.value || '#4f46e5';
try {
await fetch(`${apiBase}/user-settings/theme_color`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
value: themeColor,
description: '主题颜色'
})
});
applyThemeColor(themeColor);
showToast('主题设置保存成功', 'success');
} catch (error) {
console.error('主题设置失败:', error);
showToast('主题设置失败', 'danger');
}
});
}
// 密码表单提交处理
const passwordForm = document.getElementById('passwordForm');
if (passwordForm) {
passwordForm.addEventListener('submit', async function(e) {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('新密码和确认密码不匹配', 'warning');
return;
}
if (newPassword.length < 6) {
showToast('新密码长度至少6位', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/change-admin-password`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
showToast('密码更新成功,请重新登录', 'success');
passwordForm.reset();
// 3秒后跳转到登录页面
setTimeout(() => {
localStorage.removeItem('auth_token');
window.location.href = '/login.html';
}, 3000);
} else {
showToast(`密码更新失败: ${result.message}`, 'danger');
}
} else {
const error = await response.text();
showToast(`密码更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('密码更新失败:', error);
showToast('密码更新失败', 'danger');
}
});
}
// 页面加载时加载用户设置(仅在已登录时)
if (authToken) {
loadUserSettings();
}
});
// ==================== 备份管理功能 ====================
// 下载数据库备份
async function downloadDatabaseBackup() {
try {
showToast('正在准备数据库备份,请稍候...', 'info');
const response = await fetch(`${apiBase}/admin/backup/download`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
// 获取文件名
const contentDisposition = response.headers.get('content-disposition');
let filename = 'xianyu_backup.db';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showToast('数据库备份下载成功', 'success');
} else {
const error = await response.text();
showToast(`下载失败: ${error}`, 'danger');
}
} catch (error) {
console.error('下载数据库备份失败:', error);
showToast('下载数据库备份失败', 'danger');
}
}
// 上传数据库备份
async function uploadDatabaseBackup() {
const fileInput = document.getElementById('databaseFile');
const file = fileInput.files[0];
if (!file) {
showToast('请选择数据库文件', 'warning');
return;
}
if (!file.name.endsWith('.db')) {
showToast('只支持.db格式的数据库文件', 'warning');
return;
}
// 文件大小检查(限制100MB)
if (file.size > 100 * 1024 * 1024) {
showToast('数据库文件大小不能超过100MB', 'warning');
return;
}
if (!confirm('恢复数据库将完全替换当前所有数据,包括所有用户、Cookie、卡券等信息。\n\n此操作不可撤销!\n\n确定要继续吗?')) {
return;
}
try {
showToast('正在上传并恢复数据库,请稍候...', 'info');
const formData = new FormData();
formData.append('backup_file', file);
const response = await fetch(`${apiBase}/admin/backup/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (response.ok) {
const result = await response.json();
showToast(`数据库恢复成功!包含 ${result.user_count} 个用户`, 'success');
// 清空文件选择
fileInput.value = '';
// 提示用户刷新页面
setTimeout(() => {
if (confirm('数据库已恢复,建议刷新页面以加载新数据。是否立即刷新?')) {
window.location.reload();
}
}, 2000);
} else {
const error = await response.json();
showToast(`恢复失败: ${error.detail}`, 'danger');
}
} catch (error) {
console.error('上传数据库备份失败:', error);
showToast('上传数据库备份失败', 'danger');
}
}
// 导出备份(JSON格式,兼容旧版本)
async function exportBackup() {
try {
showToast('正在导出备份,请稍候...', 'info');
const response = await fetch(`${apiBase}/backup/export`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const backupData = await response.json();
// 生成文件名
const now = new Date();
const timestamp = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
const filename = `xianyu_backup_${timestamp}.json`;
// 创建下载链接
const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showToast('备份导出成功', 'success');
} else {
const error = await response.text();
showToast(`导出失败: ${error}`, 'danger');
}
} catch (error) {
console.error('导出备份失败:', error);
showToast('导出备份失败', 'danger');
}
}
// 导入备份
async function importBackup() {
const fileInput = document.getElementById('backupFile');
const file = fileInput.files[0];
if (!file) {
showToast('请选择备份文件', 'warning');
return;
}
if (!file.name.endsWith('.json')) {
showToast('只支持JSON格式的备份文件', 'warning');
return;
}
if (!confirm('导入备份将覆盖当前所有数据,确定要继续吗?')) {
return;
}
try {
showToast('正在导入备份,请稍候...', 'info');
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${apiBase}/backup/import`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (response.ok) {
showToast('备份导入成功!正在刷新数据...', 'success');
// 清空文件选择
fileInput.value = '';
// 清除前端缓存
clearKeywordCache();
// 延迟一下再刷新数据,确保后端缓存已更新
setTimeout(async () => {
try {
// 如果当前在关键字管理页面,重新加载数据
if (currentCookieId) {
await loadAccountKeywords();
}
// 刷新仪表盘数据
if (document.getElementById('dashboard-section').classList.contains('active')) {
await loadDashboard();
}
// 刷新账号列表
if (document.getElementById('accounts-section').classList.contains('active')) {
await loadCookies();
}
showToast('数据刷新完成!', 'success');
} catch (error) {
console.error('刷新数据失败:', error);
showToast('备份导入成功,但数据刷新失败,请手动刷新页面', 'warning');
}
}, 1000);
} else {
const error = await response.text();
showToast(`导入失败: ${error}`, 'danger');
}
} catch (error) {
console.error('导入备份失败:', error);
showToast('导入备份失败', 'danger');
}
}
// 刷新系统缓存
async function reloadSystemCache() {
try {
showToast('正在刷新系统缓存...', 'info');
const response = await fetch(`${apiBase}/system/reload-cache`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
showToast('系统缓存刷新成功!关键字等数据已更新', 'success');
// 清除前端缓存
clearKeywordCache();
// 如果当前在关键字管理页面,重新加载数据
if (currentCookieId) {
setTimeout(() => {
loadAccountKeywords();
}, 500);
}
} else {
const error = await response.text();
showToast(`刷新缓存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('刷新系统缓存失败:', error);
showToast('刷新系统缓存失败', 'danger');
}
}
// 重启系统 - 显示确认对话框
function restartSystem() {
// 使用 Bootstrap 模态框进行二次确认
const modalHtml = `
确定要重启系统吗?
重启期间系统将暂时不可用,所有账号任务将重新启动。
`;
// 移除已存在的模态框
const existingModal = document.getElementById('restartConfirmModal');
if (existingModal) {
existingModal.remove();
}
// 添加模态框到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('restartConfirmModal'));
modal.show();
}
// 执行重启系统
async function doRestartSystem() {
// 关闭确认模态框
const confirmModal = bootstrap.Modal.getInstance(document.getElementById('restartConfirmModal'));
if (confirmModal) {
confirmModal.hide();
}
try {
showToast('正在重启系统...', 'info');
const response = await fetch('/api/update/restart', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
showToast('系统正在重启,请稍候刷新页面...', 'success');
// 5秒后自动刷新页面
setTimeout(() => {
window.location.reload();
}, 5000);
} else {
const error = await response.json();
showToast(`重启失败: ${error.detail || error.message || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('重启系统失败:', error);
showToast('重启系统失败,请检查网络连接', 'danger');
}
}
// ================================
// 【商品管理菜单】相关功能
// ================================
// 切换商品多规格状态
async function toggleItemMultiSpec(cookieId, itemId, isMultiSpec) {
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}/multi-spec`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
is_multi_spec: isMultiSpec
})
});
if (response.ok) {
showToast(`${isMultiSpec ? '开启' : '关闭'}多规格成功`, 'success');
// 刷新商品列表
await refreshItemsData();
} else {
const errorData = await response.json();
throw new Error(errorData.error || '操作失败');
}
} catch (error) {
console.error('切换多规格状态失败:', error);
showToast(`切换多规格状态失败: ${error.message}`, 'danger');
}
}
// 切换商品多数量发货状态
async function toggleItemMultiQuantityDelivery(cookieId, itemId, multiQuantityDelivery) {
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}/multi-quantity-delivery`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
multi_quantity_delivery: multiQuantityDelivery
})
});
if (response.ok) {
showToast(`${multiQuantityDelivery ? '开启' : '关闭'}多数量发货成功`, 'success');
// 刷新商品列表
await refreshItemsData();
} else {
const errorData = await response.json();
throw new Error(errorData.error || '操作失败');
}
} catch (error) {
console.error('切换多数量发货状态失败:', error);
showToast(`切换多数量发货状态失败: ${error.message}`, 'danger');
}
}
// 加载商品列表
async function loadItems() {
try {
// 先加载Cookie列表用于筛选
await loadCookieFilter('itemCookieFilter');
// 加载商品列表
await refreshItemsData();
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 只刷新商品数据,不重新加载筛选器
async function refreshItemsData() {
try {
const selectedCookie = document.getElementById('itemCookieFilter').value;
if (selectedCookie) {
await loadItemsByCookie();
} else {
await loadAllItems();
}
} catch (error) {
console.error('刷新商品数据失败:', error);
showToast('刷新商品数据失败', 'danger');
}
}
// 加载Cookie筛选选项
async function loadCookieFilter(id) {
try {
const response = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const accounts = await response.json();
const select = document.getElementById(id);
// 保存当前选择的值
const currentValue = select.value;
// 清空现有选项(保留"所有账号")
select.innerHTML = '';
if (accounts.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = '❌ 暂无账号';
option.disabled = true;
select.appendChild(option);
return;
}
// 分组显示:先显示启用的账号,再显示禁用的账号
const enabledAccounts = accounts.filter(account => {
const enabled = account.enabled === undefined ? true : account.enabled;
return enabled;
});
const disabledAccounts = accounts.filter(account => {
const enabled = account.enabled === undefined ? true : account.enabled;
return !enabled;
});
// 添加启用的账号
enabledAccounts.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = `🟢 ${account.id}`;
select.appendChild(option);
});
// 添加禁用的账号
if (disabledAccounts.length > 0) {
// 添加分隔线
if (enabledAccounts.length > 0) {
const separator = document.createElement('option');
separator.value = '';
separator.textContent = '────────────────';
separator.disabled = true;
select.appendChild(separator);
}
disabledAccounts.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = `🔴 ${account.id} (已禁用)`;
select.appendChild(option);
});
}
// 恢复之前选择的值
if (currentValue) {
select.value = currentValue;
}
}
} catch (error) {
console.error('加载Cookie列表失败:', error);
showToast('加载账号列表失败', 'danger');
}
}
// 加载所有商品
async function loadAllItems() {
try {
const response = await fetch(`${apiBase}/items`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayItems(data.items);
} else {
throw new Error('获取商品列表失败');
}
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 按Cookie加载商品
async function loadItemsByCookie() {
const cookieId = document.getElementById('itemCookieFilter').value;
if (!cookieId) {
await loadAllItems();
return;
}
try {
const response = await fetch(`${apiBase}/items/cookie/${encodeURIComponent(cookieId)}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayItems(data.items);
} else {
throw new Error('获取商品列表失败');
}
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 显示商品列表
function displayItems(items) {
// 存储所有商品数据
allItemsData = items || [];
// 应用搜索过滤
applyItemsFilter();
// 显示当前页数据
displayCurrentPageItems();
// 更新分页控件
updateItemsPagination();
}
// 应用搜索过滤
function applyItemsFilter() {
const searchKeyword = currentSearchKeyword.toLowerCase().trim();
if (!searchKeyword) {
filteredItemsData = [...allItemsData];
} else {
filteredItemsData = allItemsData.filter(item => {
const title = (item.item_title || '').toLowerCase();
const detail = getItemDetailText(item.item_detail || '').toLowerCase();
return title.includes(searchKeyword) || detail.includes(searchKeyword);
});
}
// 重置到第一页
currentItemsPage = 1;
// 计算总页数
totalItemsPages = Math.ceil(filteredItemsData.length / itemsPerPage);
// 更新搜索统计
updateItemsSearchStats();
}
// 获取商品详情的纯文本内容
function getItemDetailText(itemDetail) {
if (!itemDetail) return '';
try {
// 尝试解析JSON
const detail = JSON.parse(itemDetail);
if (detail.content) {
return detail.content;
}
return itemDetail;
} catch (e) {
// 如果不是JSON格式,直接返回原文本
return itemDetail;
}
}
// 显示当前页的商品数据
function displayCurrentPageItems() {
const tbody = document.getElementById('itemsTableBody');
if (!filteredItemsData || filteredItemsData.length === 0) {
tbody.innerHTML = '| 暂无商品数据 |
';
resetItemsSelection();
return;
}
// 计算当前页的数据范围
const startIndex = (currentItemsPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentPageItems = filteredItemsData.slice(startIndex, endIndex);
const itemsHtml = currentPageItems.map(item => {
// 处理商品标题显示
let itemTitleDisplay = item.item_title || '未设置';
if (itemTitleDisplay.length > 30) {
itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...';
}
// 处理商品详情显示
let itemDetailDisplay = '未设置';
if (item.item_detail) {
const detailText = getItemDetailText(item.item_detail);
itemDetailDisplay = detailText.substring(0, 50) + (detailText.length > 50 ? '...' : '');
}
// 多规格状态显示
const isMultiSpec = item.is_multi_spec;
const multiSpecDisplay = isMultiSpec ?
'多规格' :
'普通';
// 多数量发货状态显示
const isMultiQuantityDelivery = item.multi_quantity_delivery;
const multiQuantityDeliveryDisplay = isMultiQuantityDelivery ?
'已开启' :
'已关闭';
return `
|
|
${escapeHtml(item.cookie_id)} |
${escapeHtml(item.item_id)} |
${escapeHtml(itemTitleDisplay)} |
${escapeHtml(itemDetailDisplay)} |
${escapeHtml(item.item_price || '未设置')} |
${multiSpecDisplay} |
${multiQuantityDeliveryDisplay} |
${formatDateTime(item.updated_at)} |
|
`;
}).join('');
// 更新表格内容
tbody.innerHTML = itemsHtml;
// 重置选择状态
resetItemsSelection();
}
// 重置商品选择状态
function resetItemsSelection() {
const selectAllCheckbox = document.getElementById('selectAllItems');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateBatchDeleteButton();
}
// 商品搜索过滤函数
function filterItems() {
const searchInput = document.getElementById('itemSearchInput');
currentSearchKeyword = searchInput ? searchInput.value : '';
// 应用过滤
applyItemsFilter();
// 显示当前页数据
displayCurrentPageItems();
// 更新分页控件
updateItemsPagination();
}
// 更新搜索统计信息
function updateItemsSearchStats() {
const statsElement = document.getElementById('itemSearchStats');
const statsTextElement = document.getElementById('itemSearchStatsText');
if (!statsElement || !statsTextElement) return;
if (currentSearchKeyword) {
statsTextElement.textContent = `搜索"${currentSearchKeyword}",找到 ${filteredItemsData.length} 个商品`;
statsElement.style.display = 'block';
} else {
statsElement.style.display = 'none';
}
}
// 更新分页控件
function updateItemsPagination() {
const paginationElement = document.getElementById('itemsPagination');
const pageInfoElement = document.getElementById('itemsPageInfo');
const totalPagesElement = document.getElementById('itemsTotalPages');
const pageInputElement = document.getElementById('itemsPageInput');
if (!paginationElement) return;
// 分页控件总是显示
paginationElement.style.display = 'block';
// 更新页面信息
const startIndex = (currentItemsPage - 1) * itemsPerPage + 1;
const endIndex = Math.min(currentItemsPage * itemsPerPage, filteredItemsData.length);
if (pageInfoElement) {
pageInfoElement.textContent = `显示第 ${startIndex}-${endIndex} 条,共 ${filteredItemsData.length} 条记录`;
}
if (totalPagesElement) {
totalPagesElement.textContent = totalItemsPages;
}
if (pageInputElement) {
pageInputElement.value = currentItemsPage;
pageInputElement.max = totalItemsPages;
}
// 更新分页按钮状态
updateItemsPaginationButtons();
}
// 更新分页按钮状态
function updateItemsPaginationButtons() {
const firstPageBtn = document.getElementById('itemsFirstPage');
const prevPageBtn = document.getElementById('itemsPrevPage');
const nextPageBtn = document.getElementById('itemsNextPage');
const lastPageBtn = document.getElementById('itemsLastPage');
if (firstPageBtn) firstPageBtn.disabled = currentItemsPage <= 1;
if (prevPageBtn) prevPageBtn.disabled = currentItemsPage <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentItemsPage >= totalItemsPages;
if (lastPageBtn) lastPageBtn.disabled = currentItemsPage >= totalItemsPages;
}
// 跳转到指定页面
function goToItemsPage(page) {
if (page < 1 || page > totalItemsPages) return;
currentItemsPage = page;
displayCurrentPageItems();
updateItemsPagination();
}
// 处理页面输入框的回车事件
function handleItemsPageInput(event) {
if (event.key === 'Enter') {
const pageInput = event.target;
const page = parseInt(pageInput.value);
if (page >= 1 && page <= totalItemsPages) {
goToItemsPage(page);
} else {
pageInput.value = currentItemsPage;
}
}
}
// 改变每页显示数量
function changeItemsPageSize() {
const pageSizeSelect = document.getElementById('itemsPageSize');
if (!pageSizeSelect) return;
itemsPerPage = parseInt(pageSizeSelect.value);
// 重新计算总页数
totalItemsPages = Math.ceil(filteredItemsData.length / itemsPerPage);
// 调整当前页码,确保不超出范围
if (currentItemsPage > totalItemsPages) {
currentItemsPage = Math.max(1, totalItemsPages);
}
// 重新显示数据
displayCurrentPageItems();
updateItemsPagination();
}
// 初始化商品搜索功能
let itemsSearchInitialized = false; // 标记是否已初始化
function initItemsSearch() {
// 避免重复初始化
if (itemsSearchInitialized) return;
// 初始化分页大小
const pageSizeSelect = document.getElementById('itemsPageSize');
if (pageSizeSelect) {
itemsPerPage = parseInt(pageSizeSelect.value) || 20;
pageSizeSelect.addEventListener('change', changeItemsPageSize);
}
// 初始化搜索输入框事件监听器
const searchInput = document.getElementById('itemSearchInput');
if (searchInput) {
// 使用防抖来避免频繁搜索
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
filterItems();
}, 300); // 300ms 防抖延迟
});
// 标记已初始化
itemsSearchInitialized = true;
console.log('商品搜索功能已初始化');
}
// 初始化页面输入框事件监听器
const pageInput = document.getElementById('itemsPageInput');
if (pageInput) {
pageInput.addEventListener('keydown', handleItemsPageInput);
}
}
// 刷新商品列表
async function refreshItems() {
await refreshItemsData();
showToast('本地商品列表已刷新', 'success');
}
// 获取商品信息
async function getAllItemsFromAccount() {
const cookieSelect = document.getElementById('itemCookieFilter');
const selectedCookieId = cookieSelect.value;
const pageNumber = parseInt(document.getElementById('pageNumber').value) || 1;
if (!selectedCookieId) {
showToast('请先选择一个账号', 'warning');
return;
}
if (pageNumber < 1) {
showToast('页码必须大于0', 'warning');
return;
}
// 显示加载状态
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '同步中...';
button.disabled = true;
try {
const response = await fetch(`${apiBase}/items/get-by-page`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
cookie_id: selectedCookieId,
page_number: pageNumber,
page_size: 20
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
showToast(`成功同步第${pageNumber}页 ${data.current_count} 个商品,最新详情已更新`, 'success');
// 刷新商品列表(保持筛选器选择)
await refreshItemsData();
} else {
showToast(data.message || '同步商品信息失败', 'danger');
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('同步商品信息失败:', error);
showToast('同步商品信息失败', 'danger');
} finally {
// 恢复按钮状态
button.innerHTML = originalText;
button.disabled = false;
}
}
// 获取所有页商品信息
async function getAllItemsFromAccountAll() {
const cookieSelect = document.getElementById('itemCookieFilter');
const selectedCookieId = cookieSelect.value;
if (!selectedCookieId) {
showToast('请先选择一个账号', 'warning');
return;
}
// 显示加载状态
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '同步中...';
button.disabled = true;
try {
const response = await fetch(`${apiBase}/items/get-all-from-account`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
cookie_id: selectedCookieId
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
const message = data.total_pages ?
`成功同步 ${data.total_count} 个商品(共${data.total_pages}页),最新详情已更新` :
`成功同步商品信息,最新详情已更新`;
showToast(message, 'success');
// 刷新商品列表(保持筛选器选择)
await refreshItemsData();
} else {
showToast(data.message || '同步商品信息失败', 'danger');
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('同步商品信息失败:', error);
showToast('同步商品信息失败', 'danger');
} finally {
// 恢复按钮状态
button.innerHTML = originalText;
button.disabled = false;
}
}
// 编辑商品详情
async function editItem(cookieId, itemId) {
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const item = data.item;
// 填充表单
document.getElementById('editItemCookieId').value = item.cookie_id;
document.getElementById('editItemId').value = item.item_id;
document.getElementById('editItemCookieIdDisplay').value = item.cookie_id;
document.getElementById('editItemIdDisplay').value = item.item_id;
document.getElementById('editItemDetail').value = item.item_detail || '';
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
modal.show();
} else {
throw new Error('获取商品详情失败');
}
} catch (error) {
console.error('获取商品详情失败:', error);
showToast('获取商品详情失败', 'danger');
}
}
// 保存商品详情
async function saveItemDetail() {
const cookieId = document.getElementById('editItemCookieId').value;
const itemId = document.getElementById('editItemId').value;
const itemDetail = document.getElementById('editItemDetail').value.trim();
if (!itemDetail) {
showToast('请输入商品详情', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
item_detail: itemDetail
})
});
if (response.ok) {
showToast('商品详情更新成功', 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('editItemModal'));
modal.hide();
// 刷新列表(保持筛选器选择)
await refreshItemsData();
} else {
const error = await response.text();
showToast(`更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('更新商品详情失败:', error);
showToast('更新商品详情失败', 'danger');
}
}
// 删除商品信息
async function deleteItem(cookieId, itemId, itemTitle) {
try {
// 确认删除
const confirmed = confirm(`确定要删除商品信息吗?\n\n商品ID: ${itemId}\n商品标题: ${itemTitle || '未设置'}\n\n此操作不可撤销!`);
if (!confirmed) {
return;
}
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('商品信息删除成功', 'success');
// 刷新列表(保持筛选器选择)
await refreshItemsData();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除商品信息失败:', error);
showToast('删除商品信息失败', 'danger');
}
}
// 批量删除商品信息
async function batchDeleteItems() {
try {
// 获取所有选中的复选框
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
if (checkboxes.length === 0) {
showToast('请选择要删除的商品', 'warning');
return;
}
// 确认删除
const confirmed = confirm(`确定要删除选中的 ${checkboxes.length} 个商品信息吗?\n\n此操作不可撤销!`);
if (!confirmed) {
return;
}
// 构造删除列表
const itemsToDelete = Array.from(checkboxes).map(checkbox => {
const row = checkbox.closest('tr');
return {
cookie_id: checkbox.dataset.cookieId,
item_id: checkbox.dataset.itemId
};
});
const response = await fetch(`${apiBase}/items/batch`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ items: itemsToDelete })
});
if (response.ok) {
const result = await response.json();
showToast(`批量删除完成: 成功 ${result.success_count} 个,失败 ${result.failed_count} 个`, 'success');
// 刷新列表(保持筛选器选择)
await refreshItemsData();
} else {
const error = await response.text();
showToast(`批量删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('批量删除商品信息失败:', error);
showToast('批量删除商品信息失败', 'danger');
}
}
// 全选/取消全选
function toggleSelectAll(selectAllCheckbox) {
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateBatchDeleteButton();
}
// 更新全选状态
function updateSelectAllState() {
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]');
const checkedCheckboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
const selectAllCheckbox = document.getElementById('selectAllItems');
if (checkboxes.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes.length === checkboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes.length > 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateBatchDeleteButton();
}
// 更新批量删除按钮状态
function updateBatchDeleteButton() {
const checkedCheckboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
const batchDeleteBtn = document.getElementById('batchDeleteBtn');
if (checkedCheckboxes.length > 0) {
batchDeleteBtn.disabled = false;
batchDeleteBtn.innerHTML = ` 批量删除 (${checkedCheckboxes.length})`;
} else {
batchDeleteBtn.disabled = true;
batchDeleteBtn.innerHTML = ' 批量删除';
}
}
function toggleSelectAllItemReplies(selectAllCheckbox) {
const checkboxes = document.querySelectorAll('input[name="itemReplyCheckbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateItemReplyBatchDeleteButton();
}
function updateItemReplySelectAllState() {
const checkboxes = document.querySelectorAll('input[name="itemReplyCheckbox"]');
const checkedCheckboxes = document.querySelectorAll('input[name="itemReplyCheckbox"]:checked');
const selectAllCheckbox = document.getElementById('selectAllItemReplies');
if (!selectAllCheckbox) return;
if (checkboxes.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes.length === checkboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes.length > 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateItemReplyBatchDeleteButton();
}
function updateItemReplyBatchDeleteButton() {
const checkedCheckboxes = document.querySelectorAll('input[name="itemReplyCheckbox"]:checked');
const batchDeleteBtn = document.getElementById('batchDeleteItemRepliesBtn');
if (!batchDeleteBtn) return;
if (checkedCheckboxes.length > 0) {
batchDeleteBtn.disabled = false;
batchDeleteBtn.innerHTML = ` 批量删除 (${checkedCheckboxes.length})`;
} else {
batchDeleteBtn.disabled = true;
batchDeleteBtn.innerHTML = ' 批量删除';
}
}
// 格式化日期时间
function formatDateTime(dateString) {
const date = parseUtcDateTime(dateString);
return date ? date.toLocaleString('zh-CN') : '未知';
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ================================
// 【商品回复管理菜单】相关功能
// ================================
// 加载商品回复列表
async function loadItemsReplay() {
try {
// 先加载Cookie列表用于筛选
await loadCookieFilter('itemReplayCookieFilter');
await loadCookieFilterPlus('editReplyCookieIdSelect');
// 加载商品列表
await refreshItemsReplayData();
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 只刷新商品回复数据,不重新加载筛选器
async function refreshItemsReplayData() {
try {
const selectedCookie = document.getElementById('itemReplayCookieFilter').value;
if (selectedCookie) {
await loadItemsReplayByCookie();
} else {
await loadAllItemReplays();
}
} catch (error) {
console.error('刷新商品数据失败:', error);
showToast('刷新商品数据失败', 'danger');
}
}
// 加载Cookie筛选选项添加弹框中使用
async function loadCookieFilterPlus(id) {
try {
const response = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const accounts = await response.json();
const select = document.getElementById(id);
// 保存当前选择的值
const currentValue = select.value;
// 清空现有选项(保留"所有账号")
select.innerHTML = '';
if (accounts.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = '❌ 暂无账号';
option.disabled = true;
select.appendChild(option);
return;
}
// 分组显示:先显示启用的账号,再显示禁用的账号
const enabledAccounts = accounts.filter(account => {
const enabled = account.enabled === undefined ? true : account.enabled;
return enabled;
});
const disabledAccounts = accounts.filter(account => {
const enabled = account.enabled === undefined ? true : account.enabled;
return !enabled;
});
// 添加启用的账号
enabledAccounts.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = `🟢 ${account.id}`;
select.appendChild(option);
});
// 添加禁用的账号
if (disabledAccounts.length > 0) {
// 添加分隔线
if (enabledAccounts.length > 0) {
const separator = document.createElement('option');
separator.value = '';
separator.textContent = '────────────────';
separator.disabled = true;
select.appendChild(separator);
}
disabledAccounts.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = `🔴 ${account.id} (已禁用)`;
select.appendChild(option);
});
}
// 恢复之前选择的值
if (currentValue) {
select.value = currentValue;
}
}
} catch (error) {
console.error('加载Cookie列表失败:', error);
showToast('加载账号列表失败', 'danger');
}
}
// 刷新商品回复列表
async function refreshItemReplayS() {
await refreshItemsReplayData();
showToast('商品列表已刷新', 'success');
}
// 加载所有商品回复
async function loadAllItemReplays() {
try {
const response = await fetch(`${apiBase}/itemReplays`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayItemReplays(data.items);
} else {
throw new Error('获取商品列表失败');
}
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 按Cookie加载商品回复
async function loadItemsReplayByCookie() {
const cookieId = document.getElementById('itemReplayCookieFilter').value;
if (!cookieId) {
await loadAllItemReplays();
return;
}
try {
const response = await fetch(`${apiBase}/itemReplays/cookie/${encodeURIComponent(cookieId)}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayItemReplays(data.items);
} else {
throw new Error('获取商品列表失败');
}
} catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 显示商品回复列表
function displayItemReplays(items) {
const tbody = document.getElementById('itemReplaysTableBody');
if (!items || items.length === 0) {
tbody.innerHTML = '| 暂无商品数据 |
';
// 重置选择状态
const selectAllCheckbox = document.getElementById('selectAllItemReplies');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateItemReplyBatchDeleteButton();
return;
}
const itemsHtml = items.map(item => {
// 处理商品标题显示
let itemTitleDisplay = item.item_title || '未设置';
if (itemTitleDisplay.length > 30) {
itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...';
}
// 处理商品详情显示
let itemDetailDisplay = '未设置';
if (item.item_detail) {
try {
// 尝试解析JSON并提取有用信息
const detail = JSON.parse(item.item_detail);
if (detail.content) {
itemDetailDisplay = detail.content.substring(0, 50) + (detail.content.length > 50 ? '...' : '');
} else {
// 如果是纯文本或其他格式,直接显示前50个字符
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
}
} catch (e) {
// 如果不是JSON格式,直接显示前50个字符
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
}
}
return `
|
|
${escapeHtml(item.cookie_id)} |
${escapeHtml(item.item_id)} |
${escapeHtml(itemTitleDisplay)} |
${escapeHtml(itemDetailDisplay)} |
${escapeHtml(item.reply_content)} |
${formatDateTime(item.updated_at)} |
|
`;
}).join('');
// 更新表格内容
tbody.innerHTML = itemsHtml;
// 重置选择状态
const selectAllCheckbox = document.getElementById('selectAllItemReplies');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateItemReplyBatchDeleteButton();
}
// 显示添加弹框
async function showItemReplayEdit(){
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editItemReplyModal'));
document.getElementById('editReplyCookieIdSelect').value = '';
document.getElementById('editReplyItemIdSelect').value = '';
document.getElementById('editReplyItemIdSelect').disabled = true
document.getElementById('editItemReplyContent').value = '';
document.getElementById('itemReplayTitle').textContent = '添加商品回复';
modal.show();
}
// 当账号变化时加载对应商品
async function onCookieChangeForReply() {
const cookieId = document.getElementById('editReplyCookieIdSelect').value;
const itemSelect = document.getElementById('editReplyItemIdSelect');
itemSelect.innerHTML = '';
if (!cookieId) {
itemSelect.disabled = true; // 禁用选择框
return;
} else {
itemSelect.disabled = false; // 启用选择框
}
const response = await fetch(`${apiBase}/items/cookie/${encodeURIComponent(cookieId)}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
try {
if (response.ok) {
const data = await response.json();
data.items.forEach(item => {
const opt = document.createElement('option');
opt.value = item.item_id;
opt.textContent = `${item.item_id} - ${item.item_title || '无标题'}`;
itemSelect.appendChild(opt);
});
} else {
throw new Error('获取商品列表失败');
}
}catch (error) {
console.error('加载商品列表失败:', error);
showToast('加载商品列表失败', 'danger');
}
}
// 编辑商品回复
async function editItemReply(cookieId, itemId) {
try {
const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
document.getElementById('itemReplayTitle').textContent = '编辑商品回复';
// 填充表单
document.getElementById('editReplyCookieIdSelect').value = data.cookie_id;
let res = await onCookieChangeForReply()
document.getElementById('editReplyItemIdSelect').value = data.item_id;
document.getElementById('editItemReplyContent').value = data.reply_content || '';
} else if (response.status === 404) {
// 如果没有记录,则填充空白内容(用于添加)
// document.getElementById('editReplyCookieIdSelect').value = data.cookie_id;
// document.getElementById('editReplyItemIdSelect').value = data.item_id;
// document.getElementById('editItemReplyContent').value = data.reply_content || '';
} else {
throw new Error('获取商品回复失败');
}
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editItemReplyModal'));
modal.show();
} catch (error) {
console.error('获取商品回复失败:', error);
showToast('获取商品回复失败', 'danger');
}
}
// 保存商品回复
async function saveItemReply() {
const cookieId = document.getElementById('editReplyCookieIdSelect').value;
const itemId = document.getElementById('editReplyItemIdSelect').value;
const replyContent = document.getElementById('editItemReplyContent').value.trim();
console.log(cookieId)
console.log(itemId)
console.log(replyContent)
if (!cookieId) {
showToast('请选择账号', 'warning');
return;
}
if (!itemId) {
showToast('请选择商品', 'warning');
return;
}
if (!replyContent) {
showToast('请输入商品回复内容', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
reply_content: replyContent
})
});
if (response.ok) {
showToast('商品回复保存成功', 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('editItemReplyModal'));
modal.hide();
// 可选:刷新数据
await refreshItemsReplayData?.();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('保存商品回复失败:', error);
showToast('保存商品回复失败', 'danger');
}
}
// 删除商品回复
async function deleteItemReply(cookieId, itemId, itemTitle) {
try {
const confirmed = confirm(`确定要删除该商品的自动回复吗?\n\n商品ID: ${itemId}\n商品标题: ${itemTitle || '未设置'}\n\n此操作不可撤销!`);
if (!confirmed) return;
const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('商品回复删除成功', 'success');
await loadItemsReplayByCookie?.(); // 如果你有刷新商品列表的函数
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除商品回复失败:', error);
showToast('删除商品回复失败', 'danger');
}
}
// 批量删除商品回复
async function batchDeleteItemReplies() {
try {
const checkboxes = document.querySelectorAll('input[name="itemReplyCheckbox"]:checked');
if (checkboxes.length === 0) {
showToast('请选择要删除回复的商品', 'warning');
return;
}
const confirmed = confirm(`确定要删除选中商品的自动回复吗?\n共 ${checkboxes.length} 个商品\n\n此操作不可撤销!`);
if (!confirmed) return;
const itemsToDelete = Array.from(checkboxes).map(checkbox => ({
cookie_id: checkbox.dataset.cookieId,
item_id: checkbox.dataset.itemId
}));
const response = await fetch(`${apiBase}/item-reply/batch`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ items: itemsToDelete })
});
if (response.ok) {
const result = await response.json();
showToast(`批量删除回复完成: 成功 ${result.success_count} 个,失败 ${result.failed_count} 个`, 'success');
await loadItemsReplayByCookie?.();
} else {
const error = await response.text();
showToast(`批量删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('批量删除商品回复失败:', error);
showToast('批量删除商品回复失败', 'danger');
}
}
// ================================
// 【日志管理菜单】相关功能
// ================================
window.autoRefreshInterval = null;
window.allLogs = [];
window.filteredLogs = [];
// 刷新日志
async function refreshLogs() {
try {
const logLinesElement = document.getElementById('logLines');
if (!logLinesElement) {
console.warn('logLines 元素不存在');
showToast('页面元素缺失,请刷新页面', 'warning');
return;
}
const lines = logLinesElement.value;
const response = await fetch(`${apiBase}/logs?lines=${lines}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
window.allLogs = data.logs || [];
window.filteredLogs = window.allLogs; // 不再过滤,直接显示所有日志
displayLogs();
updateLogStats();
showToast('日志已刷新', 'success');
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('刷新日志失败:', error);
showToast(`刷新日志失败: ${error.message}`, 'danger');
}
}
// 显示日志
function displayLogs() {
const container = document.getElementById('logContainer');
// 检查容器是否存在
if (!container) {
// 只在特定页面显示警告,避免在其他页面产生无用的警告
const currentPath = window.location.pathname;
if (currentPath.includes('log') || currentPath.includes('admin')) {
console.warn('logContainer 元素不存在,无法显示日志');
}
return;
}
if (!window.filteredLogs || window.filteredLogs.length === 0) {
container.innerHTML = `
`;
return;
}
const logsHtml = window.filteredLogs.map(log => {
const timestamp = formatLogTimestamp(log.timestamp);
const levelClass = log.level || 'INFO';
return `
${timestamp}
[${log.level}]
${log.source}:
${escapeHtml(log.message)}
`;
}).join('');
container.innerHTML = logsHtml;
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 格式化日志时间戳
function formatLogTimestamp(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
}
// 更新日志统计信息
function updateLogStats() {
const logCountElement = document.getElementById('logCount');
const lastUpdateElement = document.getElementById('lastUpdate');
if (logCountElement) {
const count = window.filteredLogs ? window.filteredLogs.length : 0;
logCountElement.textContent = `${count} 条日志`;
}
if (lastUpdateElement) {
lastUpdateElement.textContent = new Date().toLocaleTimeString('zh-CN');
}
}
// 清空日志显示
function clearLogsDisplay() {
window.allLogs = [];
window.filteredLogs = [];
document.getElementById('logContainer').innerHTML = `
`;
updateLogStats();
showToast('日志显示已清空', 'info');
}
// 切换自动刷新
function toggleAutoRefresh() {
const button = document.querySelector('#autoRefreshText');
const icon = button.previousElementSibling;
if (window.autoRefreshInterval) {
// 停止自动刷新
clearInterval(window.autoRefreshInterval);
window.autoRefreshInterval = null;
button.textContent = '开启自动刷新';
icon.className = 'bi bi-play-circle me-1';
showToast('自动刷新已停止', 'info');
} else {
// 开启自动刷新
window.autoRefreshInterval = setInterval(refreshLogs, 5000); // 每5秒刷新一次
button.textContent = '停止自动刷新';
icon.className = 'bi bi-pause-circle me-1';
showToast('自动刷新已开启(每5秒)', 'success');
// 立即刷新一次
refreshLogs();
}
}
// 清空服务器日志
async function clearLogsServer() {
if (!confirm('确定要清空服务器端的所有日志吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch(`${apiBase}/logs/clear`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.allLogs = [];
window.filteredLogs = [];
displayLogs();
updateLogStats();
showToast('服务器日志已清空', 'success');
} else {
showToast(data.message || '清空失败', 'danger');
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('清空服务器日志失败:', error);
showToast('清空服务器日志失败', 'danger');
}
}
// 显示日志统计信息
async function showLogStats() {
try {
const response = await fetch(`${apiBase}/logs/stats`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
const stats = data.stats;
let statsHtml = `
总体统计
- 总日志数: ${stats.total_logs}
- 最大容量: ${stats.max_capacity}
- 使用率: ${((stats.total_logs / stats.max_capacity) * 100).toFixed(1)}%
级别分布
`;
for (const [level, count] of Object.entries(stats.level_counts || {})) {
const percentage = ((count / stats.total_logs) * 100).toFixed(1);
statsHtml += `- ${level}: ${count} (${percentage}%)
`;
}
statsHtml += `
来源分布
`;
const sources = Object.entries(stats.source_counts || {});
sources.forEach(([source, count], index) => {
if (index % 2 === 0) statsHtml += '
';
const percentage = ((count / stats.total_logs) * 100).toFixed(1);
statsHtml += `- ${source}: ${count} (${percentage}%)
`;
if (index % 2 === 1 || index === sources.length - 1) statsHtml += '
';
});
statsHtml += `
`;
// 显示模态框
const modalHtml = `
`;
// 移除旧的模态框
const oldModal = document.getElementById('logStatsModal');
if (oldModal) oldModal.remove();
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('logStatsModal'));
modal.show();
} else {
showToast(data.message || '获取统计信息失败', 'danger');
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('获取日志统计失败:', error);
showToast('获取日志统计失败', 'danger');
}
}
// ==================== 导入导出功能 ====================
// 导出关键词
async function exportKeywords() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/keywords-export/${currentCookieId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 根据当前账号是否有数据来设置文件名和提示
const currentKeywords = keywordsData[currentCookieId] || [];
const hasData = currentKeywords.length > 0;
if (hasData) {
a.download = `keywords_${currentCookieId}_${new Date().getTime()}.xlsx`;
showToast('关键词导出成功!', 'success');
} else {
a.download = `keywords_template_${currentCookieId}_${new Date().getTime()}.xlsx`;
showToast('导入模板导出成功!模板中包含示例数据供参考', 'success');
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} else {
const error = await response.json();
showToast(`导出失败: ${error.detail}`, 'error');
}
} catch (error) {
console.error('导出关键词失败:', error);
showToast('导出关键词失败', 'error');
}
}
// 显示导入模态框
function showImportModal() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
const modal = new bootstrap.Modal(document.getElementById('importKeywordsModal'));
modal.show();
}
// 导入关键词
async function importKeywords() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
const fileInput = document.getElementById('importFileInput');
const file = fileInput.files[0];
if (!file) {
showToast('请选择要导入的Excel文件', 'warning');
return;
}
try {
// 显示进度条
const progressDiv = document.getElementById('importProgress');
const progressBar = progressDiv.querySelector('.progress-bar');
progressDiv.style.display = 'block';
progressBar.style.width = '30%';
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${apiBase}/keywords-import/${currentCookieId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
progressBar.style.width = '70%';
if (response.ok) {
const result = await response.json();
progressBar.style.width = '100%';
setTimeout(() => {
progressDiv.style.display = 'none';
progressBar.style.width = '0%';
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('importKeywordsModal'));
modal.hide();
// 清空文件输入
fileInput.value = '';
// 重新加载关键词列表
loadAccountKeywords(currentCookieId);
showToast(`导入成功!新增: ${result.added}, 更新: ${result.updated}`, 'success');
}, 500);
} else {
const error = await response.json();
progressDiv.style.display = 'none';
progressBar.style.width = '0%';
showToast(`导入失败: ${error.detail}`, 'error');
}
} catch (error) {
console.error('导入关键词失败:', error);
document.getElementById('importProgress').style.display = 'none';
document.querySelector('#importProgress .progress-bar').style.width = '0%';
showToast('导入关键词失败', 'error');
}
}
// ========================= 账号添加相关函数 =========================
// 切换手动输入表单显示/隐藏
function toggleManualInput() {
const manualForm = document.getElementById('manualInputForm');
const passwordForm = document.getElementById('passwordLoginForm');
const refreshForm = document.getElementById('refreshCookieForm');
if (manualForm.style.display === 'none') {
// 隐藏账号密码登录表单
if (passwordForm) {
passwordForm.style.display = 'none';
}
// 隐藏刷新Cookie表单
if (refreshForm) {
refreshForm.style.display = 'none';
}
manualForm.style.display = 'block';
// 清空表单
document.getElementById('addForm').reset();
} else {
manualForm.style.display = 'none';
}
}
// 切换账号密码登录表单显示/隐藏
function togglePasswordLogin() {
const passwordForm = document.getElementById('passwordLoginForm');
const manualForm = document.getElementById('manualInputForm');
const refreshForm = document.getElementById('refreshCookieForm');
if (passwordForm.style.display === 'none') {
// 隐藏手动输入表单
if (manualForm) {
manualForm.style.display = 'none';
}
// 隐藏刷新Cookie表单
if (refreshForm) {
refreshForm.style.display = 'none';
}
passwordForm.style.display = 'block';
// 清空表单
document.getElementById('passwordLoginFormElement').reset();
} else {
passwordForm.style.display = 'none';
}
}
// 切换刷新Cookie表单显示/隐藏
function toggleRefreshCookieForm() {
const refreshForm = document.getElementById('refreshCookieForm');
const manualForm = document.getElementById('manualInputForm');
const passwordForm = document.getElementById('passwordLoginForm');
if (refreshForm.style.display === 'none') {
// 隐藏其他表单
if (manualForm) {
manualForm.style.display = 'none';
}
if (passwordForm) {
passwordForm.style.display = 'none';
}
refreshForm.style.display = 'block';
// 清空表单
document.getElementById('refreshCookieFormElement').reset();
document.getElementById('refreshCookieAccountStatus').innerHTML = '请先选择账号';
// 加载账号列表到下拉框
loadRefreshCookieAccountList();
} else {
refreshForm.style.display = 'none';
}
}
// 加载账号列表到刷新Cookie下拉框
async function loadRefreshCookieAccountList() {
const select = document.getElementById('refreshCookieAccountSelect');
select.innerHTML = '';
try {
const response = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (data && data.length > 0) {
data.forEach(cookie => {
const option = document.createElement('option');
option.value = cookie.id;
// 显示账号ID和是否配置了用户名密码
const hasCredentials = cookie.username && cookie.has_password ? '(已配置账密)' : '(未配置账密)';
option.textContent = `${cookie.id} ${hasCredentials}`;
option.dataset.hasCredentials = cookie.username && cookie.has_password ? 'true' : 'false';
option.dataset.username = cookie.username || '';
select.appendChild(option);
});
}
} catch (error) {
console.error('加载账号列表失败:', error);
showToast('加载账号列表失败', 'danger');
}
}
// 刷新Cookie账号选择变化时显示状态
document.addEventListener('DOMContentLoaded', function() {
const select = document.getElementById('refreshCookieAccountSelect');
if (select) {
select.addEventListener('change', function() {
const statusDiv = document.getElementById('refreshCookieAccountStatus');
const selectedOption = this.options[this.selectedIndex];
if (this.value) {
const hasCredentials = selectedOption.dataset.hasCredentials === 'true';
const username = selectedOption.dataset.username;
if (hasCredentials) {
statusDiv.innerHTML = `已配置用户名: ${username}`;
} else {
statusDiv.innerHTML = `未配置用户名和密码,无法刷新`;
}
} else {
statusDiv.innerHTML = '请先选择账号';
}
});
}
// 绑定刷新Cookie表单提交事件
const refreshForm = document.getElementById('refreshCookieFormElement');
if (refreshForm) {
refreshForm.addEventListener('submit', handleRefreshCookie);
}
});
// 处理刷新Cookie表单提交
async function handleRefreshCookie(event) {
event.preventDefault();
const select = document.getElementById('refreshCookieAccountSelect');
const cookieId = select.value;
const selectedOption = select.options[select.selectedIndex];
const showBrowser = document.getElementById('refreshCookieShowBrowser').checked;
if (!cookieId) {
showToast('请选择要刷新的账号', 'warning');
return;
}
const hasCredentials = selectedOption.dataset.hasCredentials === 'true';
if (!hasCredentials) {
showToast('该账号未配置用户名和密码,无法刷新Cookie', 'danger');
return;
}
// 显示loading
toggleLoading(true);
try {
// 调用密码登录API刷新Cookie
const response = await fetch(`${apiBase}/password-login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
account_id: cookieId,
refresh_mode: true, // 标记为刷新模式
show_browser: showBrowser
})
});
const data = await response.json();
if (data.session_id) {
// 开始轮询检查登录状态
showToast('正在验证账号并刷新Cookie,请稍候...', 'info');
startRefreshCookiePolling(data.session_id, cookieId);
} else {
toggleLoading(false);
showToast(data.message || '启动刷新失败', 'danger');
}
} catch (error) {
toggleLoading(false);
console.error('刷新Cookie失败:', error);
showToast('刷新Cookie失败: ' + error.message, 'danger');
}
}
// 更新刷新Cookie状态显示
function updateRefreshCookieStatus(message) {
const statusDiv = document.getElementById('refreshCookieAccountStatus');
if (statusDiv) {
statusDiv.innerHTML = `${message}`;
}
}
// 轮询检查刷新Cookie状态
let refreshCookieCheckInterval = null;
let refreshCookiePollingState = {
sessionId: null,
cookieId: null,
inFlight: false,
completed: false
};
function stopRefreshCookiePolling(sessionId = refreshCookiePollingState.sessionId) {
if (sessionId && refreshCookiePollingState.sessionId && refreshCookiePollingState.sessionId !== sessionId) {
return;
}
if (refreshCookieCheckInterval) {
clearInterval(refreshCookieCheckInterval);
refreshCookieCheckInterval = null;
}
refreshCookiePollingState.completed = true;
}
function startRefreshCookiePolling(sessionId, cookieId) {
// 清除之前的轮询
stopRefreshCookiePolling();
refreshCookiePollingState = {
sessionId,
cookieId,
inFlight: false,
completed: false
};
let checkCount = 0;
const maxChecks = 120; // 最多检查120次,每次2秒,共4分钟
const pollRefreshCookieStatus = async () => {
if (refreshCookiePollingState.completed || refreshCookiePollingState.inFlight || refreshCookiePollingState.sessionId !== sessionId) {
return;
}
refreshCookiePollingState.inFlight = true;
checkCount++;
if (checkCount > maxChecks) {
stopRefreshCookiePolling(sessionId);
closePasswordLoginQRModal();
toggleLoading(false);
showToast('刷新Cookie超时,请重试', 'warning');
refreshCookiePollingState.inFlight = false;
return;
}
try {
const response = await fetch(`${apiBase}/password-login/check/${sessionId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (refreshCookiePollingState.sessionId !== sessionId || refreshCookiePollingState.completed) {
return;
}
console.log('刷新Cookie状态检查:', data); // 调试日志
switch (data.status) {
case 'processing':
// 处理中,更新状态显示
updateRefreshCookieStatus('正在登录中,请稍候...');
break;
case 'verification_required':
// 需要身份验证,显示验证截图或链接
updateRefreshCookieStatus(`需要${getPasswordLoginVerificationTypeLabel(data.verification_type)},请查看弹出的验证窗口`);
// 使用账号密码登录的验证显示函数
showPasswordLoginQRCode(
data.screenshot_path || data.verification_url || data.qr_code_url,
data.screenshot_path,
data.verification_type
);
break;
case 'success':
stopRefreshCookiePolling(sessionId);
const passwordLoginQRModal = document.getElementById('passwordLoginQRModal');
if (passwordLoginQRModal && passwordLoginQRModal.classList.contains('show')) {
setPasswordLoginQRModalStatus('验证已完成,正在刷新账号状态...');
await new Promise(resolve => setTimeout(resolve, 400));
}
closePasswordLoginQRModal();
toggleLoading(false);
showToast(`账号 ${cookieId} Cookie刷新成功!`, 'success');
// 隐藏表单
document.getElementById('refreshCookieForm').style.display = 'none';
// 刷新账号列表
loadCookies();
break;
case 'failed':
case 'error':
case 'not_found':
case 'forbidden':
stopRefreshCookiePolling(sessionId);
closePasswordLoginQRModal();
toggleLoading(false);
showToast(`刷新失败: ${data.message || data.error || '未知错误'}`, 'danger');
break;
}
} catch (error) {
console.error('检查刷新状态失败:', error);
} finally {
if (refreshCookiePollingState.sessionId === sessionId) {
refreshCookiePollingState.inFlight = false;
}
}
};
refreshCookieCheckInterval = setInterval(pollRefreshCookieStatus, 2000);
pollRefreshCookieStatus();
}
// ========================= 账号密码登录相关函数 =========================
let passwordLoginCheckInterval = null;
let passwordLoginSessionId = null;
let passwordLoginPollingState = {
sessionId: null,
inFlight: false,
completed: false
};
// 处理账号密码登录表单提交
async function handlePasswordLogin(event) {
event.preventDefault();
const accountId = document.getElementById('passwordLoginAccountId').value.trim();
const account = document.getElementById('passwordLoginAccount').value.trim();
const password = document.getElementById('passwordLoginPassword').value;
const showBrowser = document.getElementById('passwordLoginShowBrowser').checked;
if (!accountId || !account || !password) {
showToast('请填写完整的登录信息', 'warning');
return;
}
// 禁用提交按钮,显示加载状态
const submitBtn = event.target.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '登录中...';
try {
const response = await fetch(`${apiBase}/password-login`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
account_id: accountId,
account: account,
password: password,
show_browser: showBrowser
})
});
const data = await response.json();
if (response.ok && data.success && data.session_id) {
passwordLoginSessionId = data.session_id;
// 开始轮询检查登录状态
startPasswordLoginCheck();
} else {
showToast(data.message || '登录失败,请检查账号密码是否正确', 'danger');
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
} catch (error) {
console.error('账号密码登录失败:', error);
showToast('网络错误,请重试', 'danger');
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
}
// 开始检查账号密码登录状态
function startPasswordLoginCheck() {
clearPasswordLoginCheck();
passwordLoginPollingState = {
sessionId: passwordLoginSessionId,
inFlight: false,
completed: false
};
passwordLoginCheckInterval = setInterval(checkPasswordLoginStatus, 2000); // 每2秒检查一次
checkPasswordLoginStatus();
}
// 检查账号密码登录状态
async function checkPasswordLoginStatus() {
if (!passwordLoginSessionId || passwordLoginPollingState.completed || passwordLoginPollingState.inFlight) return;
const sessionId = passwordLoginSessionId;
passwordLoginPollingState.inFlight = true;
try {
const response = await fetch(`${apiBase}/password-login/check/${sessionId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
if (passwordLoginPollingState.sessionId !== sessionId || passwordLoginPollingState.completed) {
return;
}
console.log('账号密码登录状态检查:', data); // 调试日志
switch (data.status) {
case 'processing':
// 处理中,继续等待
break;
case 'verification_required':
// 需要身份验证,显示验证截图或链接
showPasswordLoginQRCode(
data.screenshot_path || data.verification_url || data.qr_code_url,
data.screenshot_path,
data.verification_type
);
// 继续监控(人脸认证后需要继续等待登录完成)
break;
case 'success':
// 登录成功
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
handlePasswordLoginSuccess(data);
break;
case 'failed':
// 登录失败
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
handlePasswordLoginFailure(data);
break;
case 'not_found':
case 'forbidden':
case 'error':
// 错误情况
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
closePasswordLoginQRModal();
showToast(data.message || '登录检查失败', 'danger');
resetPasswordLoginForm();
break;
}
} else {
// 响应不OK时也尝试解析错误消息
try {
const errorData = await response.json();
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
closePasswordLoginQRModal();
showToast(errorData.message || '登录检查失败', 'danger');
resetPasswordLoginForm();
} catch (e) {
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
closePasswordLoginQRModal();
showToast('登录检查失败,请重试', 'danger');
resetPasswordLoginForm();
}
}
} catch (error) {
console.error('检查账号密码登录状态失败:', error);
passwordLoginPollingState.completed = true;
clearPasswordLoginCheck();
closePasswordLoginQRModal();
showToast('网络错误,请重试', 'danger');
resetPasswordLoginForm();
} finally {
if (passwordLoginPollingState.sessionId === sessionId) {
passwordLoginPollingState.inFlight = false;
}
}
}
function getPasswordLoginVerificationTypeLabel(verificationType) {
const normalized = String(verificationType || '').trim();
const labelMap = {
face_verify: '人脸验证',
sms_verify: '短信验证',
qr_verify: '二维码验证',
unknown: '身份验证'
};
return labelMap[normalized] || normalized || '身份验证';
}
// 显示账号密码登录验证
function showPasswordLoginQRCode(verificationUrl, screenshotPath, verificationType) {
// 使用现有的二维码登录模态框
let modal = document.getElementById('passwordLoginQRModal');
if (!modal) {
// 如果模态框不存在,创建一个
createPasswordLoginQRModal();
modal = document.getElementById('passwordLoginQRModal');
}
// 更新模态框标题
const modalTitle = document.getElementById('passwordLoginQRModalLabel');
if (modalTitle) {
modalTitle.innerHTML = '闲鱼验证';
}
// 获取或创建模态框实例
let modalInstance = bootstrap.Modal.getInstance(modal);
if (!modalInstance) {
modalInstance = new bootstrap.Modal(modal);
}
modalInstance.show();
// 隐藏加载容器
const qrContainer = document.getElementById('passwordLoginQRContainer');
if (qrContainer) {
qrContainer.style.display = 'none';
}
// 优先显示截图,如果没有截图则显示链接
const screenshotImg = document.getElementById('passwordLoginScreenshotImg');
const linkButton = document.getElementById('passwordLoginVerificationLink');
const statusText = document.getElementById('passwordLoginQRStatusText');
const verificationTypeLabel = getPasswordLoginVerificationTypeLabel(verificationType);
if (screenshotPath) {
// 显示截图
if (screenshotImg) {
screenshotImg.src = `${normalizeStaticAssetPath(screenshotPath)}?t=${new Date().getTime()}`;
screenshotImg.style.display = 'block';
screenshotImg.alt = `${verificationTypeLabel}截图`;
}
// 隐藏链接按钮
if (linkButton) {
linkButton.style.display = 'none';
}
// 更新状态文本
if (statusText) {
statusText.textContent = verificationTypeLabel === '二维码验证'
? '需要闲鱼二维码验证,请使用手机闲鱼APP扫描下方二维码完成验证'
: `需要闲鱼${verificationTypeLabel},请根据下方验证信息在手机闲鱼APP中完成操作`;
}
} else if (verificationUrl) {
// 隐藏截图
if (screenshotImg) {
screenshotImg.style.display = 'none';
}
// 显示链接按钮
if (linkButton) {
linkButton.href = verificationUrl;
linkButton.style.display = 'inline-block';
}
// 更新状态文本
if (statusText) {
statusText.textContent = `服务端已保持原始会话;如${verificationTypeLabel}入口暂未显示,可使用下方兜底入口`;
}
} else {
// 都没有,显示等待
if (screenshotImg) {
screenshotImg.style.display = 'none';
}
if (linkButton) {
linkButton.style.display = 'none';
}
if (statusText) {
statusText.textContent = `需要闲鱼${verificationTypeLabel},请等待验证信息...`;
}
}
}
function closePasswordLoginQRModal() {
const modalElement = document.getElementById('passwordLoginQRModal');
if (!modalElement) {
return;
}
const modalTitle = document.getElementById('passwordLoginQRModalLabel');
if (modalTitle) {
modalTitle.innerHTML = '闲鱼验证';
}
const screenshotImg = document.getElementById('passwordLoginScreenshotImg');
if (screenshotImg) {
screenshotImg.src = '';
screenshotImg.style.display = 'none';
}
const linkButton = document.getElementById('passwordLoginVerificationLink');
if (linkButton) {
linkButton.href = '#';
linkButton.style.display = 'none';
}
const statusText = document.getElementById('passwordLoginQRStatusText');
if (statusText) {
statusText.textContent = '需要闲鱼身份验证,请等待验证信息...';
}
const modalInstance = bootstrap.Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
}
}
function setPasswordLoginQRModalStatus(message) {
const statusText = document.getElementById('passwordLoginQRStatusText');
if (statusText) {
statusText.textContent = message;
}
}
// 创建账号密码登录二维码模态框
function createPasswordLoginQRModal() {
const modalHtml = `
需要闲鱼身份验证,请等待验证信息...
验证完成后,系统将自动检测并继续登录流程
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
// 处理账号密码登录成功
function handlePasswordLoginSuccess(data) {
// 关闭二维码模态框
closePasswordLoginQRModal();
showToast(`账号 ${data.account_id} 登录成功!`, 'success');
// 隐藏表单
togglePasswordLogin();
// 刷新账号列表
loadCookies();
// 重置表单
resetPasswordLoginForm();
}
// 处理账号密码登录失败
function handlePasswordLoginFailure(data) {
console.log('账号密码登录失败,错误数据:', data); // 调试日志
// 关闭二维码模态框
closePasswordLoginQRModal();
// 优先使用 message,如果没有则使用 error 字段
const errorMessage = data.message || data.error || '登录失败,请检查账号密码是否正确';
console.log('显示错误消息:', errorMessage); // 调试日志
showToast(errorMessage, 'danger'); // 使用 'danger' 而不是 'error',因为 Bootstrap 使用 'danger' 作为错误类型
// 重置表单
resetPasswordLoginForm();
}
// 清理账号密码登录检查
function clearPasswordLoginCheck() {
if (passwordLoginCheckInterval) {
clearInterval(passwordLoginCheckInterval);
passwordLoginCheckInterval = null;
}
}
// 重置账号密码登录表单
function resetPasswordLoginForm() {
passwordLoginSessionId = null;
clearPasswordLoginCheck();
passwordLoginPollingState = {
sessionId: null,
inFlight: false,
completed: false
};
const submitBtn = document.querySelector('#passwordLoginFormElement button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '开始登录';
}
}
// ========================= 扫码登录相关函数 =========================
let qrCodeCheckInterval = null;
let qrCodeSessionId = null;
let qrCodeModalEventsBound = false;
let qrCodeVerificationState = {
renderKey: '',
toastShown: false,
inFlight: false,
completed: false,
activeSessionId: null
};
function normalizeStaticAssetPath(path) {
if (!path) {
return '';
}
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) {
return path;
}
return path.startsWith('/') ? path : `/${path}`;
}
function resetQRCodeVerificationState() {
qrCodeVerificationState.renderKey = '';
qrCodeVerificationState.toastShown = false;
qrCodeVerificationState.inFlight = false;
qrCodeVerificationState.completed = false;
qrCodeVerificationState.activeSessionId = null;
}
function closeQRCodeLoginModal(delay = 3000) {
setTimeout(() => {
const modalElement = document.getElementById('qrCodeLoginModal');
if (!modalElement) {
loadCookies();
return;
}
const modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement);
modal.hide();
loadCookies();
}, delay);
}
function initializeQRCodeLoginModal() {
const modalElement = document.getElementById('qrCodeLoginModal');
if (!modalElement || qrCodeModalEventsBound) {
return modalElement;
}
modalElement.addEventListener('shown.bs.modal', function () {
generateQRCode();
});
modalElement.addEventListener('hidden.bs.modal', function () {
clearQRCodeCheck();
});
qrCodeModalEventsBound = true;
return modalElement;
}
// 显示扫码登录模态框
function showQRCodeLogin() {
const modalElement = initializeQRCodeLoginModal();
if (!modalElement) {
showToast('扫码登录弹窗未找到,请刷新页面重试', 'danger');
return;
}
const modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement);
modal.show();
}
// 刷新二维码(兼容旧函数名)
async function refreshQRCode() {
await generateQRCode();
}
// 生成二维码
async function generateQRCode() {
try {
resetQRCodeVerificationState();
showQRCodeLoading();
const response = await fetch(`${apiBase}/qr-login/generate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
qrCodeSessionId = data.session_id;
qrCodeVerificationState.activeSessionId = data.session_id;
showQRCodeImage(data.qr_code_url);
startQRCodeCheck();
} else {
showQRCodeError(data.message || '生成二维码失败');
}
} else {
showQRCodeError('生成二维码失败');
}
} catch (error) {
console.error('生成二维码失败:', error);
showQRCodeError('网络错误,请重试');
}
}
// 显示二维码加载状态
function showQRCodeLoading() {
resetQRCodeVerificationState();
document.getElementById('qrCodeContainer').style.display = 'block';
document.getElementById('qrCodeImage').style.display = 'none';
document.getElementById('statusText').textContent = '正在生成二维码,请耐心等待...';
document.getElementById('statusSpinner').style.display = 'none';
// 隐藏验证容器
const verificationContainer = document.getElementById('verificationContainer');
if (verificationContainer) {
verificationContainer.style.display = 'none';
}
}
// 显示二维码图片
function showQRCodeImage(qrCodeUrl) {
document.getElementById('qrCodeContainer').style.display = 'none';
document.getElementById('qrCodeImage').style.display = 'block';
document.getElementById('qrCodeImg').src = qrCodeUrl;
document.getElementById('statusText').textContent = '等待扫码...';
document.getElementById('statusSpinner').style.display = 'none';
}
// 显示二维码错误
function showQRCodeError(message) {
document.getElementById('qrCodeContainer').innerHTML = `
`;
document.getElementById('qrCodeImage').style.display = 'none';
document.getElementById('statusText').textContent = '生成失败';
document.getElementById('statusSpinner').style.display = 'none';
}
// 开始检查二维码状态
function startQRCodeCheck() {
if (qrCodeCheckInterval) {
clearInterval(qrCodeCheckInterval);
}
document.getElementById('statusSpinner').style.display = 'inline-block';
document.getElementById('statusText').textContent = '等待扫码...';
qrCodeCheckInterval = setInterval(checkQRCodeStatus, 2000); // 每2秒检查一次
}
// 检查二维码状态
async function checkQRCodeStatus() {
if (!qrCodeSessionId || qrCodeVerificationState.inFlight || qrCodeVerificationState.completed) return;
const requestSessionId = qrCodeSessionId;
qrCodeVerificationState.inFlight = true;
try {
const response = await fetch(`${apiBase}/qr-login/check/${requestSessionId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (requestSessionId !== qrCodeVerificationState.activeSessionId || qrCodeVerificationState.completed) {
return;
}
if (response.ok) {
const data = await response.json();
if (requestSessionId !== qrCodeVerificationState.activeSessionId || qrCodeVerificationState.completed) {
return;
}
switch (data.status) {
case 'waiting':
document.getElementById('statusText').textContent = '等待扫码...';
break;
case 'scanned':
document.getElementById('statusText').textContent = '已扫码,请在手机上确认...';
break;
case 'confirmed':
document.getElementById('statusText').textContent = '已确认,正在获取Cookie...';
break;
case 'success':
qrCodeVerificationState.completed = true;
document.getElementById('statusText').textContent = '登录成功!';
document.getElementById('statusSpinner').style.display = 'none';
clearQRCodeCheck();
handleQRCodeSuccess(data);
break;
case 'error':
qrCodeVerificationState.completed = true;
document.getElementById('statusText').textContent = '登录失败';
document.getElementById('statusSpinner').style.display = 'none';
clearQRCodeCheck();
showToast(data.message || '扫码登录失败', 'danger');
break;
case 'expired':
document.getElementById('statusText').textContent = '二维码已过期';
document.getElementById('statusSpinner').style.display = 'none';
clearQRCodeCheck();
showQRCodeError('二维码已过期,请刷新重试');
break;
case 'cancelled':
document.getElementById('statusText').textContent = '用户取消登录';
document.getElementById('statusSpinner').style.display = 'none';
clearQRCodeCheck();
break;
case 'verification_required':
document.getElementById('statusText').textContent = '需要闲鱼验证,系统正在等待验证完成...';
document.getElementById('statusSpinner').style.display = 'inline-block';
showVerificationRequired(data);
break;
case 'processing':
document.getElementById('statusText').textContent = '正在处理中...';
// 继续轮询,不清理检查
break;
case 'already_processed':
qrCodeVerificationState.completed = true;
document.getElementById('statusText').textContent = '登录已完成';
document.getElementById('statusSpinner').style.display = 'none';
clearQRCodeCheck();
handleQRCodeSuccess(data);
break;
}
}
} catch (error) {
console.error('检查二维码状态失败:', error);
} finally {
qrCodeVerificationState.inFlight = false;
}
}
// 显示需要验证的提示
function showVerificationRequired(data) {
const screenshotPath = data.screenshot_path || '';
const verificationUrl = data.verification_url || '';
const renderKey = `${screenshotPath}|${verificationUrl}`;
if (qrCodeVerificationState.renderKey === renderKey && renderKey) {
return;
}
qrCodeVerificationState.renderKey = renderKey;
// 隐藏二维码区域
document.getElementById('qrCodeContainer').style.display = 'none';
document.getElementById('qrCodeImage').style.display = 'none';
let verificationHtml = `
账号需要闲鱼验证
检测到账号存在风控,系统已在服务端保持原始会话并等待验证完成
验证步骤:
1. 使用手机闲鱼 APP 扫描下方二维码并完成验证
2. 保持当前弹窗打开,系统会自动继续登录流程
3. 如果二维码暂未出现,请稍等几秒,页面会自动刷新显示
`;
if (screenshotPath) {
verificationHtml = `
账号需要闲鱼验证
检测到账号存在风控,系统已在服务端保持原始会话并生成验证二维码
请使用手机闲鱼 APP 扫描下方二维码完成验证:
验证步骤:
1. 使用手机闲鱼 APP 扫描上方二维码并完成验证
2. 保持当前弹窗打开,系统会自动继续登录流程
3. 如果二维码失效,请关闭弹窗后重新发起扫码登录
`;
} else if (verificationUrl) {
verificationHtml = `
账号需要闲鱼验证
系统正在准备验证二维码,当前先保留一个兜底链接
系统仍会继续尝试在当前会话中生成二维码并自动完成后续登录。
`;
}
// 创建验证提示容器
let verificationContainer = document.getElementById('verificationContainer');
if (!verificationContainer) {
verificationContainer = document.createElement('div');
verificationContainer.id = 'verificationContainer';
document.querySelector('#qrCodeLoginModal .modal-body').appendChild(verificationContainer);
}
verificationContainer.innerHTML = verificationHtml;
verificationContainer.style.display = 'block';
// 显示Toast提示
if (!qrCodeVerificationState.toastShown) {
showToast('账号需要闲鱼验证,请使用当前页面展示的二维码完成验证', 'warning');
qrCodeVerificationState.toastShown = true;
}
}
// 处理扫码成功
function handleQRCodeSuccess(data) {
if (data.account_info) {
const {
account_id,
is_new_account,
real_cookie_refreshed,
fallback_reason,
cookie_length,
token_prewarmed,
task_restarted,
warning_message
} = data.account_info;
// 构建成功消息
let successMessage = '';
if (is_new_account) {
successMessage = `新账号添加成功!账号ID: ${account_id}`;
} else {
successMessage = `账号Cookie已更新!账号ID: ${account_id}`;
}
// 添加cookie长度信息
if (cookie_length) {
successMessage += `\nCookie长度: ${cookie_length}`;
}
// 添加真实cookie获取状态信息
if (real_cookie_refreshed === true) {
if (task_restarted === false) {
successMessage += '\n✅ 真实Cookie已获取';
if (warning_message) {
successMessage += `\n⚠️ ${warning_message}`;
}
document.getElementById('statusText').textContent = '登录完成,但账号任务尚未切换';
showToast(successMessage, 'warning');
} else if (token_prewarmed === false) {
successMessage += '\n✅ 真实Cookie获取并保存成功';
if (warning_message) {
successMessage += `\n⚠️ ${warning_message}`;
}
document.getElementById('statusText').textContent = '登录完成,账号任务已切换,Token将在后台继续初始化';
showToast(successMessage, 'warning');
} else {
successMessage += '\n✅ 真实Cookie获取并保存成功';
document.getElementById('statusText').textContent = '登录成功!真实Cookie已获取并保存';
showToast(successMessage, 'success');
}
} else if (real_cookie_refreshed === false) {
successMessage += '\n⚠️ 真实Cookie获取失败,已保存原始扫码Cookie';
if (fallback_reason) {
successMessage += `\n原因: ${fallback_reason}`;
}
document.getElementById('statusText').textContent = '登录成功,但使用原始Cookie';
showToast(successMessage, 'warning');
} else {
// 兼容旧版本,没有真实cookie刷新信息
document.getElementById('statusText').textContent = '登录成功!';
showToast(successMessage, 'success');
}
closeQRCodeLoginModal(3000);
return;
}
document.getElementById('statusText').textContent = '登录成功!';
showToast(data.message || '扫码登录已完成,账号信息已同步', 'success');
closeQRCodeLoginModal(1500);
}
// 清理二维码检查
function clearQRCodeCheck() {
if (qrCodeCheckInterval) {
clearInterval(qrCodeCheckInterval);
qrCodeCheckInterval = null;
}
qrCodeSessionId = null;
resetQRCodeVerificationState();
}
// 刷新二维码
function refreshQRCode() {
clearQRCodeCheck();
generateQRCode();
}
// ==================== 图片关键词管理功能 ====================
// 显示添加图片关键词模态框
function showAddImageKeywordModal() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
// 加载商品列表到图片关键词模态框
loadItemsListForImageKeyword();
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('addImageKeywordModal'));
modal.show();
// 清空表单
document.getElementById('imageKeyword').value = '';
const imageSelectElement = document.getElementById('imageItemIdSelect');
if (imageSelectElement) {
// 清除所有选中项
Array.from(imageSelectElement.options).forEach(opt => opt.selected = false);
}
document.getElementById('imageFile').value = '';
hideImagePreview();
}
// 为图片关键词模态框加载商品列表
async function loadItemsListForImageKeyword() {
try {
const response = await fetch(`${apiBase}/items/${currentCookieId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const items = data.items || [];
// 更新商品选择下拉框
const selectElement = document.getElementById('imageItemIdSelect');
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);
}
}
// 处理图片文件选择事件监听器
function initImageKeywordEventListeners() {
const imageFileInput = document.getElementById('imageFile');
if (imageFileInput && !imageFileInput.hasEventListener) {
imageFileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
showToast('请选择图片文件', 'warning');
e.target.value = '';
hideImagePreview();
return;
}
// 验证文件大小(5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('❌ 图片文件大小不能超过 5MB,当前文件大小:' + (file.size / 1024 / 1024).toFixed(1) + 'MB', 'warning');
e.target.value = '';
hideImagePreview();
return;
}
// 验证图片尺寸
validateImageDimensions(file, e.target);
} else {
hideImagePreview();
}
});
imageFileInput.hasEventListener = true;
}
}
// 验证图片尺寸
function validateImageDimensions(file, inputElement) {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = function() {
const width = this.naturalWidth;
const height = this.naturalHeight;
// 释放对象URL
URL.revokeObjectURL(url);
// 检查图片尺寸
const maxDimension = 4096;
const maxPixels = 8 * 1024 * 1024; // 8M像素
const totalPixels = width * height;
if (width > maxDimension || height > maxDimension) {
showToast(`❌ 图片尺寸过大:${width}x${height},最大允许:${maxDimension}x${maxDimension}像素`, 'warning');
inputElement.value = '';
hideImagePreview();
return;
}
if (totalPixels > maxPixels) {
showToast(`❌ 图片像素总数过大:${(totalPixels / 1024 / 1024).toFixed(1)}M像素,最大允许:8M像素`, 'warning');
inputElement.value = '';
hideImagePreview();
return;
}
// 尺寸检查通过,显示预览和提示信息
showImagePreview(file);
// 如果图片较大,提示会被压缩
if (width > 2048 || height > 2048) {
showToast(`ℹ️ 图片尺寸较大(${width}x${height}),上传时将自动压缩以优化性能`, 'info');
} else {
showToast(`✅ 图片尺寸合适(${width}x${height}),可以上传`, 'success');
}
};
img.onerror = function() {
URL.revokeObjectURL(url);
showToast('❌ 无法读取图片文件,请选择有效的图片', 'warning');
inputElement.value = '';
hideImagePreview();
};
img.src = url;
}
// 显示图片预览
function showImagePreview(file) {
const reader = new FileReader();
reader.onload = function(e) {
const previewContainer = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
previewImg.src = e.target.result;
previewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
}
// 隐藏图片预览
function hideImagePreview() {
const previewContainer = document.getElementById('imagePreview');
if (previewContainer) {
previewContainer.style.display = 'none';
}
}
// 添加图片关键词
async function addImageKeyword() {
const keywordInput = document.getElementById('imageKeyword').value.trim();
const selectElement = document.getElementById('imageItemIdSelect');
const selectedOptions = Array.from(selectElement.selectedOptions);
const fileInput = document.getElementById('imageFile');
const file = fileInput.files[0];
if (!keywordInput) {
showToast('请填写关键词', 'warning');
return;
}
if (!file) {
showToast('请选择图片文件', 'warning');
return;
}
// 解析多个关键词(支持竖线、换行符分隔)
const keywords = keywordInput
.split(/[\|\n]/)
.map(k => k.trim())
.filter(k => k.length > 0);
if (keywords.length === 0) {
showToast('请填写有效的关键词', 'warning');
return;
}
// 获取选中的商品ID列表
let itemIds = selectedOptions
.map(opt => opt.value)
.filter(id => id !== ''); // 过滤掉空值(通用关键词选项)
// 如果没有选中任何商品,或者选中了空值,则作为通用关键词
if (itemIds.length === 0) {
itemIds = [''];
}
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
try {
toggleLoading(true);
// 检查重复关键词
const allKeywords = keywordsData[currentCookieId] || [];
const duplicates = [];
for (const keyword of keywords) {
for (const itemId of itemIds) {
const existingKeyword = allKeywords.find(item =>
item.keyword === keyword &&
(item.item_id || '') === (itemId || '')
);
if (existingKeyword) {
const itemIdText = itemId ? `(商品ID: ${itemId})` : '(通用关键词)';
duplicates.push(`"${keyword}" ${itemIdText}`);
}
}
}
if (duplicates.length > 0) {
showToast(`以下关键词已存在:\n${duplicates.join('\n')}\n请修改后重试`, 'warning');
toggleLoading(false);
return;
}
const totalCount = keywords.length * itemIds.length;
// 第一步:先上传一次图片获取URL
const formData = new FormData();
formData.append('image', file);
const uploadResponse = await fetch(`${apiBase}/upload-image`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (!uploadResponse.ok) {
const errorData = await uploadResponse.json().catch(() => ({}));
showToast(`❌ 图片上传失败: ${errorData.detail || '请检查后重试'}`, 'danger');
toggleLoading(false);
return;
}
const uploadResult = await uploadResponse.json();
const imageUrl = uploadResult.image_url;
if (!imageUrl) {
showToast('❌ 图片上传失败:未获取到图片URL', 'danger');
toggleLoading(false);
return;
}
// 第二步:使用批量API添加所有关键词
const batchResponse = await fetch(`${apiBase}/keywords/${currentCookieId}/image-batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
image_url: imageUrl,
keywords: keywords,
item_ids: itemIds
})
});
if (batchResponse.ok) {
const result = await batchResponse.json();
const successCount = result.success_count || 0;
const failCount = result.fail_count || 0;
if (successCount > 0) {
const keywordText = keywords.length > 1 ? `${keywords.length}个关键词` : `"${keywords[0]}"`;
const itemText = itemIds.length > 1 ? `${itemIds.length}个商品` : (itemIds[0] ? '指定商品' : '通用');
if (failCount === 0) {
showToast(`✨ ${keywordText} 添加成功!(共${totalCount}条配置,应用于${itemText})`, 'success');
} else {
showToast(`⚠️ 部分添加成功:成功${successCount}条,失败${failCount}条`, 'warning');
}
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('addImageKeywordModal'));
modal.hide();
// 只刷新关键词列表,不重新加载整个界面
await refreshKeywordsList();
} else {
showToast('❌ 所有图片关键词添加失败,请检查后重试', 'danger');
}
} else {
const errorData = await batchResponse.json().catch(() => ({}));
showToast(`❌ 添加图片关键词失败: ${errorData.detail || '请检查后重试'}`, 'danger');
}
} catch (error) {
console.error('添加图片关键词失败:', error);
showToast('添加图片关键词失败', 'danger');
} finally {
toggleLoading(false);
}
}
// 显示图片模态框
function showImageModal(imageUrl) {
// 创建模态框HTML
const modalHtml = `
`;
// 移除已存在的模态框
const existingModal = document.getElementById('imageViewModal');
if (existingModal) {
existingModal.remove();
}
// 添加新模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('imageViewModal'));
modal.show();
// 模态框关闭后移除DOM元素
document.getElementById('imageViewModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
// 编辑图片关键词(不允许修改)
function editImageKeyword(index) {
showToast('图片关键词不允许修改,请删除后重新添加', 'warning');
}
// 修改导出关键词函数,使用后端导出API
async function exportKeywords() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
try {
toggleLoading(true);
// 使用后端导出API
const response = await fetch(`${apiBase}/keywords-export/${currentCookieId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
// 获取文件blob
const blob = await response.blob();
// 从响应头获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let fileName = `关键词数据_${currentCookieId}_${new Date().toISOString().slice(0, 10)}.xlsx`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename\*=UTF-8''(.+)/);
if (fileNameMatch) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
// 清理
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showToast('✅ 关键词导出成功', 'success');
} else {
const errorText = await response.text();
console.error('导出关键词失败:', errorText);
showToast('导出关键词失败', 'danger');
}
} catch (error) {
console.error('导出关键词失败:', error);
showToast('导出关键词失败', 'danger');
} finally {
toggleLoading(false);
}
}
// ==================== 备注管理功能 ====================
// 编辑备注
function editRemark(cookieId, currentRemark) {
console.log('editRemark called:', cookieId, currentRemark); // 调试信息
const remarkCell = document.querySelector(`[data-cookie-id="${cookieId}"] .remark-display`);
if (!remarkCell) {
console.log('remarkCell not found'); // 调试信息
return;
}
// 创建输入框
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm';
input.value = currentRemark || '';
input.placeholder = '请输入备注...';
input.style.fontSize = '0.875rem';
input.maxLength = 100; // 限制备注长度
// 保存原始内容和原始值
const originalContent = remarkCell.innerHTML;
const originalValue = currentRemark || '';
// 标记是否已经进行了编辑
let hasChanged = false;
let isProcessing = false; // 防止重复处理
// 替换为输入框
remarkCell.innerHTML = '';
remarkCell.appendChild(input);
// 监听输入变化
input.addEventListener('input', () => {
hasChanged = input.value.trim() !== originalValue;
});
// 保存函数
const saveRemark = async () => {
console.log('saveRemark called, isProcessing:', isProcessing, 'hasChanged:', hasChanged); // 调试信息
if (isProcessing) return; // 防止重复调用
const newRemark = input.value.trim();
console.log('newRemark:', newRemark, 'originalValue:', originalValue); // 调试信息
// 如果没有变化,直接恢复显示
if (!hasChanged || newRemark === originalValue) {
console.log('No changes detected, restoring original content'); // 调试信息
remarkCell.innerHTML = originalContent;
return;
}
isProcessing = true;
try {
const response = await fetch(`${apiBase}/cookies/${cookieId}/remark`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ remark: newRemark })
});
if (response.ok) {
// 更新显示
remarkCell.innerHTML = `
`;
showToast('备注更新成功', 'success');
} else {
const errorData = await response.json();
showToast(`备注更新失败: ${errorData.detail || '未知错误'}`, 'danger');
// 恢复原始内容
remarkCell.innerHTML = originalContent;
}
} catch (error) {
console.error('更新备注失败:', error);
showToast('备注更新失败', 'danger');
// 恢复原始内容
remarkCell.innerHTML = originalContent;
} finally {
isProcessing = false;
}
};
// 取消函数
const cancelEdit = () => {
if (isProcessing) return;
remarkCell.innerHTML = originalContent;
};
// 延迟绑定blur事件,避免立即触发
setTimeout(() => {
input.addEventListener('blur', saveRemark);
}, 100);
// 绑定键盘事件
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveRemark();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
// 聚焦并选中文本
input.focus();
input.select();
}
// 编辑暂停时间
function editPauseDuration(cookieId, currentDuration) {
console.log('editPauseDuration called:', cookieId, currentDuration); // 调试信息
const pauseCell = document.querySelector(`[data-cookie-id="${cookieId}"] .pause-duration-display`);
if (!pauseCell) {
console.log('pauseCell not found'); // 调试信息
return;
}
// 创建输入框
const input = document.createElement('input');
input.type = 'number';
input.className = 'form-control form-control-sm';
input.value = currentDuration !== undefined ? currentDuration : 10;
input.placeholder = '请输入暂停时间...';
input.style.fontSize = '0.875rem';
input.min = 0;
input.max = 60;
input.step = 1;
// 保存原始内容和原始值
const originalContent = pauseCell.innerHTML;
const originalValue = currentDuration !== undefined ? currentDuration : 10;
// 标记是否已经进行了编辑
let hasChanged = false;
let isProcessing = false; // 防止重复处理
// 替换为输入框
pauseCell.innerHTML = '';
pauseCell.appendChild(input);
// 监听输入变化
input.addEventListener('input', () => {
const newValue = input.value === '' ? 10 : parseInt(input.value);
hasChanged = newValue !== originalValue;
});
// 保存函数
const savePauseDuration = async () => {
console.log('savePauseDuration called, isProcessing:', isProcessing, 'hasChanged:', hasChanged); // 调试信息
if (isProcessing) return; // 防止重复调用
const newDuration = input.value === '' ? 10 : parseInt(input.value);
console.log('newDuration:', newDuration, 'originalValue:', originalValue); // 调试信息
// 验证范围
if (isNaN(newDuration) || newDuration < 0 || newDuration > 60) {
showToast('暂停时间必须在0-60分钟之间(0表示不暂停)', 'warning');
input.focus();
return;
}
// 如果没有变化,直接恢复显示
if (!hasChanged || newDuration === originalValue) {
console.log('No changes detected, restoring original content'); // 调试信息
pauseCell.innerHTML = originalContent;
return;
}
isProcessing = true;
try {
const response = await fetch(`${apiBase}/cookies/${cookieId}/pause-duration`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ pause_duration: newDuration })
});
if (response.ok) {
// 更新显示
pauseCell.innerHTML = `
${newDuration === 0 ? '不暂停' : newDuration + '分钟'}
`;
showToast('暂停时间更新成功', 'success');
} else {
const errorData = await response.json();
showToast(`暂停时间更新失败: ${errorData.detail || '未知错误'}`, 'danger');
// 恢复原始内容
pauseCell.innerHTML = originalContent;
}
} catch (error) {
console.error('更新暂停时间失败:', error);
showToast('暂停时间更新失败', 'danger');
// 恢复原始内容
pauseCell.innerHTML = originalContent;
} finally {
isProcessing = false;
}
};
// 取消函数
const cancelEdit = () => {
if (isProcessing) return;
pauseCell.innerHTML = originalContent;
};
// 延迟绑定blur事件,避免立即触发
setTimeout(() => {
input.addEventListener('blur', savePauseDuration);
}, 100);
// 绑定键盘事件
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
savePauseDuration();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
// 聚焦并选中文本
input.focus();
input.select();
}
// ==================== 工具提示初始化 ====================
// 初始化工具提示
function initTooltips() {
// 初始化所有工具提示
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// ==================== 系统设置功能 ====================
// 加载系统设置
async function loadSystemSettings() {
console.log('加载系统设置');
// 通过验证接口获取用户信息(更可靠)
try {
const response = await fetch(`${apiBase}/verify`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
const isAdmin = result.is_admin === true;
console.log('用户信息:', result, '是否管理员:', isAdmin);
// 显示/隐藏管理员专用设置(仅管理员可见)
const apiSecuritySettings = document.getElementById('api-security-settings');
const loginInfoSettings = document.getElementById('login-info-settings');
const outgoingConfigs = document.getElementById('outgoing-configs');
const backupManagement = document.getElementById('backup-management');
const systemRestartBtn = document.getElementById('system-restart-btn');
const dashboardHotUpdateGroup = document.getElementById('dashboardHotUpdateGroup');
if (apiSecuritySettings) {
apiSecuritySettings.style.display = isAdmin ? 'block' : 'none';
}
if (loginInfoSettings) {
loginInfoSettings.style.display = isAdmin ? 'flex' : 'none';
}
if (outgoingConfigs) {
outgoingConfigs.style.display = isAdmin ? 'block' : 'none';
}
if (backupManagement) {
backupManagement.style.display = isAdmin ? 'block' : 'none';
}
if (systemRestartBtn) {
systemRestartBtn.style.display = isAdmin ? 'inline-block' : 'none';
}
if (dashboardHotUpdateGroup) {
dashboardHotUpdateGroup.style.display = isAdmin ? 'inline-flex' : 'none';
}
// 如果是管理员,加载所有管理员设置
if (isAdmin) {
refreshHotUpdatePreferencesMenu();
await loadAPISecuritySettings();
await loadRegistrationSettings();
await loadLoginInfoSettings();
await loadOutgoingConfigs();
}
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 出错时隐藏管理员功能
const loginInfoSettings = document.getElementById('login-info-settings');
const dashboardHotUpdateGroup = document.getElementById('dashboardHotUpdateGroup');
if (loginInfoSettings) {
loginInfoSettings.style.display = 'none';
}
if (dashboardHotUpdateGroup) {
dashboardHotUpdateGroup.style.display = 'none';
}
}
}
// 加载API安全设置
async function loadAPISecuritySettings() {
try {
const response = await fetch('/system-settings', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
// 加载QQ回复消息秘钥
const qqReplySecretKey = settings.qq_reply_secret_key || '';
const qqReplySecretKeyInput = document.getElementById('qqReplySecretKey');
if (qqReplySecretKeyInput) {
qqReplySecretKeyInput.value = qqReplySecretKey;
}
}
} catch (error) {
console.error('加载API安全设置失败:', error);
showToast('加载API安全设置失败', 'danger');
}
}
// 加载防抖延迟设置
async function loadDebounceDelay() {
try {
const response = await fetch('/system-settings', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
const val = settings.message_debounce_delay;
const input = document.getElementById('debounceDelay');
if (input && val) {
input.value = parseInt(val) || 3;
}
}
} catch (error) {
console.error('加载防抖延迟设置失败:', error);
}
}
// 保存防抖延迟设置
async function saveDebounceDelay() {
const input = document.getElementById('debounceDelay');
if (!input) return;
const val = parseInt(input.value);
if (isNaN(val) || val < 1 || val > 10) {
showToast('防抖延迟需在1-10秒之间', 'warning');
return;
}
try {
const response = await fetch('/system-settings/message_debounce_delay', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
key: 'message_debounce_delay',
value: String(val),
description: '消息防抖延迟时间(秒)'
})
});
if (response.ok) {
showToast('防抖延迟已保存', 'success');
} else {
showToast('保存防抖延迟失败', 'danger');
}
} catch (error) {
console.error('保存防抖延迟失败:', error);
showToast('保存防抖延迟失败', 'danger');
}
}
// 切换密码可见性
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
const icon = document.getElementById(inputId + '-icon');
if (input && icon) {
if (input.type === 'password') {
input.type = 'text';
icon.className = 'bi bi-eye-slash';
} else {
input.type = 'password';
icon.className = 'bi bi-eye';
}
}
}
// 生成随机秘钥
function generateRandomSecretKey() {
// 生成32位随机字符串
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = 'xianyu_qq_';
for (let i = 0; i < 24; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
const qqReplySecretKeyInput = document.getElementById('qqReplySecretKey');
if (qqReplySecretKeyInput) {
qqReplySecretKeyInput.value = result;
showToast('随机秘钥已生成', 'success');
}
}
// 更新QQ回复消息秘钥
async function updateQQReplySecretKey() {
const qqReplySecretKey = document.getElementById('qqReplySecretKey').value.trim();
if (!qqReplySecretKey) {
showToast('请输入QQ回复消息API秘钥', 'warning');
return;
}
if (qqReplySecretKey.length < 8) {
showToast('秘钥长度至少需要8位字符', 'warning');
return;
}
try {
const response = await fetch('/system-settings/qq_reply_secret_key', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
value: qqReplySecretKey,
description: 'QQ回复消息API秘钥'
})
});
if (response.ok) {
showToast('QQ回复消息API秘钥更新成功', 'success');
// 显示状态信息
const statusDiv = document.getElementById('qqReplySecretStatus');
const statusText = document.getElementById('qqReplySecretStatusText');
if (statusDiv && statusText) {
statusText.textContent = `秘钥已更新,长度: ${qqReplySecretKey.length} 位`;
statusDiv.style.display = 'block';
// 3秒后隐藏状态
setTimeout(() => {
statusDiv.style.display = 'none';
}, 3000);
}
} else {
const errorData = await response.json();
showToast(`更新失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('更新QQ回复消息秘钥失败:', error);
showToast('更新QQ回复消息秘钥失败', 'danger');
}
}
// 加载外发配置
async function loadOutgoingConfigs() {
try {
const response = await fetch('/system-settings', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
// 渲染外发配置界面
renderOutgoingConfigs(settings);
}
} catch (error) {
console.error('加载外发配置失败:', error);
showToast('加载外发配置失败', 'danger');
}
}
// 渲染外发配置界面
function renderOutgoingConfigs(settings) {
const container = document.getElementById('outgoing-configs');
if (!container) return;
let html = '';
// 渲染SMTP配置
const smtpConfig = outgoingConfigs.smtp;
html += `
${smtpConfig.description}
`;
html += '
';
container.innerHTML = html;
// 绑定表单提交事件
const form = document.getElementById('smtp-config-form');
if (form) {
form.addEventListener('submit', saveOutgoingConfigs);
}
}
// 生成外发配置字段HTML
function generateOutgoingFieldHtml(field, value) {
switch (field.type) {
case 'select':
let options = '';
field.options.forEach(option => {
const selected = value === option.value ? 'selected' : '';
options += ``;
});
return ``;
case 'password':
return ``;
case 'number':
return ``;
case 'email':
return ``;
default:
return ``;
}
}
// 保存外发配置
async function saveOutgoingConfigs(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const configs = {};
// 收集表单数据
for (let [key, value] of formData.entries()) {
configs[key] = value;
}
try {
// 逐个保存配置项
for (const [key, value] of Object.entries(configs)) {
const response = await fetch(`/system-settings/${key}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
key: key,
value: value,
description: `SMTP配置 - ${key}`
})
});
if (!response.ok) {
throw new Error(`保存${key}失败`);
}
}
showToast('外发配置保存成功', 'success');
// 重新加载配置
await loadOutgoingConfigs();
} catch (error) {
console.error('保存外发配置失败:', error);
showToast('保存外发配置失败: ' + error.message, 'danger');
}
}
// 加载注册设置
async function loadRegistrationSettings() {
try {
const response = await fetch('/registration-status');
if (response.ok) {
const data = await response.json();
const checkbox = document.getElementById('registrationEnabled');
if (checkbox) {
checkbox.checked = data.enabled;
}
}
} catch (error) {
console.error('加载注册设置失败:', error);
showToast('加载注册设置失败', 'danger');
}
}
// 加载默认登录信息设置
async function loadLoginInfoSettings() {
try {
const response = await fetch('/system-settings', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
const checkbox = document.getElementById('showDefaultLoginInfo');
const captchaCheckbox = document.getElementById('loginCaptchaEnabled');
if (checkbox && settings.show_default_login_info !== undefined) {
checkbox.checked = settings.show_default_login_info === 'true';
}
if (captchaCheckbox && settings.login_captcha_enabled !== undefined) {
captchaCheckbox.checked = settings.login_captcha_enabled === 'true';
} else if (captchaCheckbox) {
// 默认开启
captchaCheckbox.checked = true;
}
}
} catch (error) {
console.error('加载登录信息设置失败:', error);
showToast('加载登录信息设置失败', 'danger');
}
}
// 更新登录与注册设置
async function updateLoginInfoSettings() {
const registrationCheckbox = document.getElementById('registrationEnabled');
const checkbox = document.getElementById('showDefaultLoginInfo');
const captchaCheckbox = document.getElementById('loginCaptchaEnabled');
const statusDiv = document.getElementById('loginInfoStatus');
const statusText = document.getElementById('loginInfoStatusText');
try {
let messages = [];
// 更新用户注册设置
if (registrationCheckbox) {
const regEnabled = registrationCheckbox.checked;
const regResponse = await fetch('/registration-settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ enabled: regEnabled })
});
if (regResponse.ok) {
messages.push(regEnabled ? '用户注册已开启' : '用户注册已关闭');
} else {
const errorData = await regResponse.json();
showToast(`更新注册设置失败: ${errorData.detail || '未知错误'}`, 'danger');
return;
}
}
// 更新显示默认登录信息设置
if (checkbox) {
const enabled = checkbox.checked;
const response = await fetch('/login-info-settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ enabled: enabled })
});
if (response.ok) {
messages.push(enabled ? '默认登录信息显示已开启' : '默认登录信息显示已关闭');
} else {
const errorData = await response.json();
showToast(`更新默认登录信息设置失败: ${errorData.detail || '未知错误'}`, 'danger');
return;
}
}
// 更新登录验证码设置
if (captchaCheckbox) {
const captchaEnabled = captchaCheckbox.checked;
const captchaResponse = await fetch('/login-captcha-settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ enabled: captchaEnabled })
});
if (captchaResponse.ok) {
messages.push(captchaEnabled ? '登录验证码已开启' : '登录验证码已关闭');
} else {
const errorData = await captchaResponse.json();
showToast(`更新登录验证码设置失败: ${errorData.detail || '未知错误'}`, 'danger');
return;
}
}
// 显示成功消息
const message = messages.join(',');
showToast('设置保存成功', 'success');
// 显示状态信息
if (statusDiv && statusText) {
statusText.textContent = message;
statusDiv.style.display = 'block';
// 3秒后隐藏状态信息
setTimeout(() => {
statusDiv.style.display = 'none';
}, 3000);
}
} catch (error) {
console.error('更新登录信息设置失败:', error);
showToast('更新登录信息设置失败', 'danger');
}
}
// ================================
// 订单管理功能
// ================================
function isOrdersSectionActive() {
const section = document.getElementById('orders-section');
return !!section && section.classList.contains('active');
}
function stopOrdersStream() {
ordersStreamShouldRun = false;
if (ordersStreamReconnectTimer) {
clearTimeout(ordersStreamReconnectTimer);
ordersStreamReconnectTimer = null;
}
if (ordersStreamAbortController) {
ordersStreamAbortController.abort();
ordersStreamAbortController = null;
}
}
window.addEventListener('pagehide', stopOrdersStream);
function scheduleOrdersStreamReconnect() {
if (!ordersStreamShouldRun || !isOrdersSectionActive()) return;
if (ordersStreamReconnectTimer) return;
const retryDelay = Math.min(10000, [1000, 2000, 5000, 10000][Math.min(ordersStreamRetryCount, 3)]);
ordersStreamReconnectTimer = setTimeout(() => {
ordersStreamReconnectTimer = null;
startOrdersStream();
}, retryDelay);
}
function handleOrdersStreamEvent(eventName, payloadText) {
if (!payloadText) return;
if (eventName === 'ping' || eventName === 'stream.ready') return;
try {
const payload = JSON.parse(payloadText);
if (eventName === 'order.updated' && payload.order) {
applyRealtimeOrderUpdate(payload.order);
}
} catch (error) {
console.error('解析订单实时事件失败:', error, payloadText);
}
}
function applyRealtimeOrderUpdate(order) {
if (!order || !order.order_id) return;
const existingIndex = allOrdersData.findIndex(item => item.order_id === order.order_id);
if (existingIndex === -1) {
refreshOrdersData();
return;
}
allOrdersData[existingIndex] = {
...allOrdersData[existingIndex],
...order,
};
filterOrders(false);
}
async function consumeOrdersStream(response, controller) {
if (!response.body) {
throw new Error('订单实时流不可用');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (controller.signal.aborted) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split(/\r?\n\r?\n/);
buffer = chunks.pop() || '';
chunks.forEach(chunk => {
let eventName = 'message';
const dataLines = [];
chunk.split(/\r?\n/).forEach(line => {
if (line.startsWith('event:')) {
eventName = line.slice(6).trim();
} else if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trimStart());
}
});
handleOrdersStreamEvent(eventName, dataLines.join('\n'));
});
}
}
async function startOrdersStream() {
if (!authToken || !isOrdersSectionActive()) return;
if (ordersStreamAbortController) return;
ordersStreamShouldRun = true;
if (ordersStreamReconnectTimer) {
clearTimeout(ordersStreamReconnectTimer);
ordersStreamReconnectTimer = null;
}
const controller = new AbortController();
ordersStreamAbortController = controller;
try {
const response = await fetch(`${apiBase}/api/orders/stream`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Accept': 'text/event-stream'
},
cache: 'no-store',
signal: controller.signal
});
if (response.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
if (!response.ok) {
throw new Error(`订单实时流连接失败: HTTP ${response.status}`);
}
ordersStreamRetryCount = 0;
await consumeOrdersStream(response, controller);
} catch (error) {
if (!controller.signal.aborted) {
ordersStreamRetryCount += 1;
console.error('订单实时流异常:', error);
scheduleOrdersStreamReconnect();
}
} finally {
if (ordersStreamAbortController === controller) {
ordersStreamAbortController = null;
}
if (!controller.signal.aborted && ordersStreamShouldRun && isOrdersSectionActive()) {
scheduleOrdersStreamReconnect();
}
}
}
// 加载订单列表
async function loadOrders() {
try {
// 先加载Cookie列表用于筛选
await loadOrderCookieFilter();
// 加载订单列表
await refreshOrdersData();
startOrdersStream();
} catch (error) {
console.error('加载订单列表失败:', error);
showToast('加载订单列表失败', 'danger');
}
}
// 只刷新订单数据,不重新加载筛选器
async function refreshOrdersData() {
try {
await loadAllOrders();
} catch (error) {
console.error('刷新订单数据失败:', error);
showToast('刷新订单数据失败', 'danger');
}
}
// 加载Cookie筛选选项
async function loadOrderCookieFilter() {
try {
const select = document.getElementById('orderCookieFilter');
const previousValue = select ? select.value : '';
const accounts = await fetchOrderSyncAccounts(true);
if (select) {
renderOrderAccountOptions(select, accounts, { includeAllOption: true });
if (previousValue && accounts.some(account => account.id === previousValue)) {
select.value = previousValue;
}
}
} catch (error) {
console.error('加载Cookie选项失败:', error);
}
}
// 加载所有订单
async function loadAllOrders() {
try {
const response = await fetch(`${apiBase}/api/orders`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (data.success) {
allOrdersData = data.data || [];
// 历史同步后优先按平台下单时间排序,回退到入库时间
allOrdersData.sort((a, b) => {
const bTime = parseUtcDateTime(getOrderPrimarySortTime(b))?.getTime() || 0;
const aTime = parseUtcDateTime(getOrderPrimarySortTime(a))?.getTime() || 0;
return bTime - aTime;
});
// 应用当前筛选条件
filterOrders(false);
} else {
console.error('加载订单失败:', data.message);
showToast('加载订单数据失败: ' + data.message, 'danger');
}
} catch (error) {
console.error('加载订单失败:', error);
showToast('加载订单数据失败,请检查网络连接', 'danger');
}
}
// 根据Cookie加载订单
async function loadOrdersByCookie() {
filterOrders(false);
}
// 筛选订单
function filterOrders(resetPage = true) {
const searchKeyword = document.getElementById('orderSearchInput')?.value.toLowerCase() || '';
const statusFilter = document.getElementById('orderStatusFilter')?.value || '';
const cookieFilter = document.getElementById('orderCookieFilter')?.value || '';
const normalizedStatusFilter = statusFilter ? normalizeOrderStatus(statusFilter) : '';
filteredOrdersData = allOrdersData.filter(order => {
// 搜索关键词筛选(订单ID、商品ID、买家ID、买家昵称)
const matchesSearch = !searchKeyword ||
(order.order_id && order.order_id.toLowerCase().includes(searchKeyword)) ||
(order.item_id && order.item_id.toLowerCase().includes(searchKeyword)) ||
(order.buyer_id && order.buyer_id.toLowerCase().includes(searchKeyword)) ||
(order.buyer_nick && order.buyer_nick.toLowerCase().includes(searchKeyword));
const matchesCookie = !cookieFilter || order.cookie_id === cookieFilter;
const matchesStatus = !normalizedStatusFilter || normalizeOrderStatus(order.order_status) === normalizedStatusFilter;
return matchesSearch && matchesCookie && matchesStatus;
});
currentOrderSearchKeyword = searchKeyword;
if (resetPage) {
currentOrdersPage = 1; // 重置到第一页
}
updateOrdersDisplay();
}
// 更新订单显示
function updateOrdersDisplay() {
const computedTotalPages = filteredOrdersData.length === 0 ? 0 : Math.ceil(filteredOrdersData.length / ordersPerPage);
if (computedTotalPages === 0) {
currentOrdersPage = 1;
} else {
currentOrdersPage = Math.min(currentOrdersPage, computedTotalPages);
}
displayOrders();
updateOrdersPagination();
updateOrdersSearchStats();
}
// 显示订单列表
function displayOrders() {
const tbody = document.getElementById('ordersTableBody');
if (!tbody) return;
if (filteredOrdersData.length === 0) {
tbody.innerHTML = `
|
${currentOrderSearchKeyword ? '没有找到匹配的订单' : '暂无订单数据'}
|
`;
return;
}
// 计算分页
totalOrdersPages = Math.ceil(filteredOrdersData.length / ordersPerPage);
const startIndex = (currentOrdersPage - 1) * ordersPerPage;
const endIndex = startIndex + ordersPerPage;
const pageOrders = filteredOrdersData.slice(startIndex, endIndex);
// 生成表格行
tbody.innerHTML = pageOrders.map(order => createOrderRow(order)).join('');
}
// 创建订单行HTML
function createOrderRow(order) {
const statusClass = getOrderStatusClass(order.order_status);
const statusText = getOrderStatusText(order.order_status);
const normalizedStatus = normalizeOrderStatus(order.order_status);
const orderId = escapeHtml(order.order_id || '');
const itemId = escapeHtml(order.item_id || '-');
const buyerId = escapeHtml(order.buyer_id || '-');
const buyerNick = escapeHtml(order.buyer_nick || '-');
const cookieId = escapeHtml(order.cookie_id || '-');
const specName = escapeHtml(order.spec_name || '');
const specValue = escapeHtml(order.spec_value || '');
const specName2 = escapeHtml(order.spec_name_2 || '');
const specValue2 = escapeHtml(order.spec_value_2 || '');
const quantity = escapeHtml(order.quantity || '-');
const amountDisplay = escapeHtml(formatOrderAmountDisplay(order.amount));
// 判断是否可以手动发货(允许多次发货,除了交易关闭的订单)
const canDeliver = !['cancelled', 'refunding'].includes(normalizedStatus);
let specHtml = '-';
if (order.spec_name && order.spec_value) {
specHtml = `${specName}:
${specValue}`;
if (order.spec_name_2 && order.spec_value_2) {
specHtml += `
${specName2}:
${specValue2}`;
}
}
return `
|
|
${orderId}
|
${itemId}
|
${buyerId}
|
${buyerNick}
|
${specHtml}
|
${quantity} |
${amountDisplay}
|
${escapeHtml(statusText)}
|
${cookieId}
|
|
`;
}
// 获取订单状态样式类
function getOrderStatusClass(status) {
const normalizedStatus = normalizeOrderStatus(status);
const statusMap = {
'processing': 'bg-warning text-dark',
'pending_payment': 'bg-warning text-dark',
'pending_ship': 'bg-info text-white',
'partial_success': 'bg-primary-subtle text-primary-emphasis',
'partial_pending_finalize': 'bg-warning-subtle text-warning-emphasis',
'shipped': 'bg-primary text-white',
'completed': 'bg-success text-white',
'success': 'bg-success text-white',
'refunding': 'bg-warning text-dark',
'refund_cancelled': 'bg-info text-dark',
'cancelled': 'bg-secondary text-white',
'unknown': 'bg-secondary text-white'
};
return statusMap[normalizedStatus] || statusMap[status] || 'bg-secondary text-white';
}
// 获取订单状态文本
function getOrderStatusText(status) {
const normalizedStatus = normalizeOrderStatus(status);
const statusMap = {
'processing': '处理中',
'pending_payment': '待付款',
'pending_ship': '待发货',
'partial_success': '部分发货',
'partial_pending_finalize': '部分待收尾',
'shipped': '已发货',
'completed': '交易成功',
'success': '交易成功',
'refunding': '申请退款中',
'refund_cancelled': '退款已撤销',
'cancelled': '交易关闭',
'unknown': '未知'
};
return statusMap[normalizedStatus] || statusMap[status] || status || '未知';
}
// 更新订单分页
function updateOrdersPagination() {
const pageInfo = document.getElementById('ordersPageInfo');
const pageInput = document.getElementById('ordersPageInput');
const totalPagesSpan = document.getElementById('ordersTotalPages');
if (pageInfo) {
const startIndex = (currentOrdersPage - 1) * ordersPerPage + 1;
const endIndex = Math.min(currentOrdersPage * ordersPerPage, filteredOrdersData.length);
pageInfo.textContent = `显示第 ${startIndex}-${endIndex} 条,共 ${filteredOrdersData.length} 条记录`;
}
if (pageInput) {
pageInput.value = currentOrdersPage;
}
if (totalPagesSpan) {
totalPagesSpan.textContent = totalOrdersPages;
}
// 更新分页按钮状态
const firstPageBtn = document.getElementById('ordersFirstPage');
const prevPageBtn = document.getElementById('ordersPrevPage');
const nextPageBtn = document.getElementById('ordersNextPage');
const lastPageBtn = document.getElementById('ordersLastPage');
if (firstPageBtn) firstPageBtn.disabled = currentOrdersPage === 1;
if (prevPageBtn) prevPageBtn.disabled = currentOrdersPage === 1;
if (nextPageBtn) nextPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0;
if (lastPageBtn) lastPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0;
}
// 更新搜索统计信息
function updateOrdersSearchStats() {
const searchStats = document.getElementById('orderSearchStats');
const searchStatsText = document.getElementById('orderSearchStatsText');
if (searchStats && searchStatsText) {
if (currentOrderSearchKeyword) {
searchStatsText.textContent = `搜索 "${currentOrderSearchKeyword}" 找到 ${filteredOrdersData.length} 个结果`;
searchStats.style.display = 'block';
} else {
searchStats.style.display = 'none';
}
}
}
// 跳转到指定页面
function goToOrdersPage(page) {
if (page < 1 || page > totalOrdersPages) return;
currentOrdersPage = page;
updateOrdersDisplay();
}
// 初始化订单搜索功能
function initOrdersSearch() {
// 初始化分页大小
const pageSizeSelect = document.getElementById('ordersPageSize');
if (pageSizeSelect) {
ordersPerPage = parseInt(pageSizeSelect.value) || 20;
pageSizeSelect.addEventListener('change', changeOrdersPageSize);
}
// 初始化搜索输入框事件监听器
const searchInput = document.getElementById('orderSearchInput');
if (searchInput) {
// 使用防抖来避免频繁搜索
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
filterOrders();
}, 300); // 300ms 防抖延迟
});
}
// 初始化页面输入框事件监听器
const pageInput = document.getElementById('ordersPageInput');
if (pageInput) {
pageInput.addEventListener('keydown', handleOrdersPageInput);
}
}
// 处理分页大小变化
function changeOrdersPageSize() {
const pageSizeSelect = document.getElementById('ordersPageSize');
if (pageSizeSelect) {
ordersPerPage = parseInt(pageSizeSelect.value) || 20;
currentOrdersPage = 1; // 重置到第一页
updateOrdersDisplay();
}
}
// 处理页面输入
function handleOrdersPageInput(event) {
if (event.key === 'Enter') {
const pageInput = document.getElementById('ordersPageInput');
if (pageInput) {
const page = parseInt(pageInput.value);
if (page >= 1 && page <= totalOrdersPages) {
goToOrdersPage(page);
} else {
pageInput.value = currentOrdersPage; // 恢复当前页码
showToast('页码超出范围', 'warning');
}
}
}
}
// 刷新订单列表
async function refreshOrders() {
await refreshOrdersData();
showToast('订单列表已刷新', 'success');
}
function getOrderPrimarySortTime(order) {
const platformCreatedAt = String(order?.platform_created_at || '').trim();
if (platformCreatedAt) {
return platformCreatedAt;
}
const createdAt = String(order?.created_at || '').trim();
return createdAt || null;
}
function getRelativeBeijingDateInputValue(offsetDays = 0) {
return getBeijingDateKey(new Date(Date.now() + offsetDays * 24 * 60 * 60 * 1000));
}
async function fetchOrderSyncAccounts(forceRefresh = false) {
if (!forceRefresh && orderHistorySyncAccounts.length > 0) {
return orderHistorySyncAccounts;
}
const response = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error(`获取账号列表失败: HTTP ${response.status}`);
}
const accounts = await response.json();
orderHistorySyncAccounts = Array.isArray(accounts) ? accounts : [];
return orderHistorySyncAccounts;
}
function formatOrderAccountLabel(account) {
const accountId = String(account?.id || '').trim();
const remark = String(account?.remark || '').trim();
if (remark) {
return `${remark} (${accountId})`;
}
return accountId || '未命名账号';
}
function renderOrderAccountOptions(select, accounts, options = {}) {
if (!select) return;
const {
includeAllOption = false,
allOptionLabel = '所有账号',
} = options;
const previousValue = select.value;
select.innerHTML = includeAllOption ? `` : '';
(accounts || []).forEach(account => {
const accountId = String(account?.id || '').trim();
if (!accountId) return;
const option = document.createElement('option');
option.value = accountId;
option.textContent = formatOrderAccountLabel(account);
select.appendChild(option);
});
if (previousValue && Array.from(select.options).some(option => option.value === previousValue)) {
select.value = previousValue;
}
}
function resetOrderHistorySyncProgress() {
renderOrderHistorySyncJob({
status: 'idle',
message: '选择账号和日期范围后即可开始同步。',
request: {},
accounts_total: 0,
accounts_completed: 0,
orders_discovered: 0,
orders_processed: 0,
orders_saved: 0,
orders_skipped: 0,
orders_failed: 0,
matched_orders: 0,
warnings: [],
});
}
function setOrderHistorySyncFormDisabled(disabled) {
[
'orderHistorySyncCookieId',
'orderHistorySyncStartDate',
'orderHistorySyncEndDate',
'orderHistorySyncMaxOrders',
'orderHistorySyncFetchDetails',
].forEach(id => {
const element = document.getElementById(id);
if (element) {
element.disabled = disabled;
}
});
const startBtn = document.getElementById('orderHistorySyncStartBtn');
const cancelBtn = document.getElementById('orderHistorySyncCancelBtn');
if (startBtn) {
startBtn.disabled = disabled;
startBtn.innerHTML = disabled
? '同步中'
: ' 开始同步';
}
if (cancelBtn) {
cancelBtn.style.display = disabled ? '' : 'none';
cancelBtn.disabled = false;
}
}
function stopOrderHistorySyncPolling() {
if (orderHistorySyncPollingTimer) {
clearTimeout(orderHistorySyncPollingTimer);
orderHistorySyncPollingTimer = null;
}
}
function scheduleOrderHistorySyncPolling(jobId) {
stopOrderHistorySyncPolling();
orderHistorySyncPollingTimer = setTimeout(() => {
fetchOrderHistorySyncStatus(jobId).catch(error => {
console.error('轮询历史订单同步状态失败:', error);
});
}, 2000);
}
function getOrderHistorySyncStatusMeta(job) {
const status = String(job?.status || '').toLowerCase();
const statusMap = {
idle: { label: '待命', badgeClass: 'bg-secondary text-white', progressClass: 'bg-secondary', title: '未开始' },
pending: { label: '排队中', badgeClass: 'bg-secondary text-white', progressClass: 'bg-secondary', title: '等待执行' },
running: { label: '进行中', badgeClass: 'bg-primary text-white', progressClass: 'bg-primary', title: '同步中' },
completed: { label: '已完成', badgeClass: 'bg-success text-white', progressClass: 'bg-success', title: '同步完成' },
failed: { label: '失败', badgeClass: 'bg-danger text-white', progressClass: 'bg-danger', title: '同步失败' },
cancelled: { label: '已取消', badgeClass: 'bg-warning text-dark', progressClass: 'bg-warning', title: '同步已取消' },
};
return statusMap[status] || statusMap.idle;
}
function renderOrderHistorySyncJob(job) {
const statusMeta = getOrderHistorySyncStatusMeta(job);
const request = job?.request || {};
const accountsTotal = Number(job?.accounts_total || 0);
const accountsCompleted = Number(job?.accounts_completed || 0);
const ordersDiscovered = Number(job?.orders_discovered || 0);
const matchedOrders = Number(job?.matched_orders || 0);
const ordersSaved = Number(job?.orders_saved || 0);
const ordersFailed = Number(job?.orders_failed || 0);
const ordersProcessed = Number(job?.orders_processed || 0);
const ordersSkipped = Number(job?.orders_skipped || 0);
const warnings = Array.isArray(job?.warnings) ? job.warnings : [];
const statusText = document.getElementById('orderHistorySyncStatusText');
const messageText = document.getElementById('orderHistorySyncMessageText');
const statusBadge = document.getElementById('orderHistorySyncStatusBadge');
const progressBar = document.getElementById('orderHistorySyncProgressBar');
const accountsStat = document.getElementById('orderHistorySyncAccountsStat');
const discoveredStat = document.getElementById('orderHistorySyncDiscoveredStat');
const matchedStat = document.getElementById('orderHistorySyncMatchedStat');
const savedStat = document.getElementById('orderHistorySyncSavedStat');
const metaText = document.getElementById('orderHistorySyncMetaText');
const currentText = document.getElementById('orderHistorySyncCurrentText');
const warningsWrap = document.getElementById('orderHistorySyncWarningsWrap');
const warningsContainer = document.getElementById('orderHistorySyncWarnings');
const cookieSelect = document.getElementById('orderHistorySyncCookieId');
const startDateInput = document.getElementById('orderHistorySyncStartDate');
const endDateInput = document.getElementById('orderHistorySyncEndDate');
const maxOrdersInput = document.getElementById('orderHistorySyncMaxOrders');
const fetchDetailsInput = document.getElementById('orderHistorySyncFetchDetails');
if (cookieSelect && Object.prototype.hasOwnProperty.call(request, 'cookie_id')) {
cookieSelect.value = request.cookie_id || '';
}
if (startDateInput && request.start_date) {
startDateInput.value = request.start_date;
}
if (endDateInput && request.end_date) {
endDateInput.value = request.end_date;
}
if (maxOrdersInput && request.max_orders) {
maxOrdersInput.value = String(request.max_orders);
}
if (fetchDetailsInput && Object.prototype.hasOwnProperty.call(request, 'fetch_details')) {
fetchDetailsInput.checked = Boolean(request.fetch_details);
}
if (statusText) {
statusText.textContent = statusMeta.title;
}
if (messageText) {
messageText.textContent = job?.message || '选择账号和日期范围后即可开始同步。';
}
if (statusBadge) {
statusBadge.className = `badge ${statusMeta.badgeClass}`;
statusBadge.textContent = statusMeta.label;
}
let progressPercent = 0;
const status = String(job?.status || '').toLowerCase();
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
progressPercent = 100;
} else if (accountsTotal > 0) {
const accountProgress = accountsCompleted / accountsTotal;
const orderProgress = matchedOrders > 0 ? (ordersProcessed / matchedOrders) : 0;
progressPercent = Math.max(accountProgress, orderProgress) * 100;
} else if (status === 'pending') {
progressPercent = 8;
}
if (progressBar) {
progressBar.className = `progress-bar ${statusMeta.progressClass}`;
progressBar.style.width = `${Math.max(0, Math.min(100, progressPercent))}%`;
}
if (accountsStat) {
accountsStat.textContent = `${accountsCompleted} / ${accountsTotal}`;
}
if (discoveredStat) {
discoveredStat.textContent = String(ordersDiscovered);
}
if (matchedStat) {
matchedStat.textContent = String(matchedOrders);
}
if (savedStat) {
savedStat.textContent = `${ordersSaved} / ${ordersFailed}`;
}
const requestParts = [
request.cookie_id ? `账号 ${request.cookie_id}` : '全部账号',
request.max_orders ? `最多同步 ${request.max_orders} 单` : '',
request.fetch_details === false ? '仅基础信息' : '含订单详情',
request.start_date && request.end_date ? `时间范围 ${request.start_date} 至 ${request.end_date}` : '',
].filter(Boolean);
const metaParts = [
requestParts.join(' · '),
job?.started_at ? `开始于 ${job.started_at}` : '',
job?.finished_at ? `结束于 ${job.finished_at}` : '',
].filter(Boolean);
if (metaText) {
metaText.textContent = metaParts.join(' · ') || '尚未开始任务';
}
const currentParts = [];
if (job?.current_account) {
currentParts.push(`当前账号: ${job.current_account}`);
}
if (job?.current_order_id) {
currentParts.push(`当前订单: ${job.current_order_id}`);
}
if (ordersProcessed > 0 || ordersSkipped > 0) {
currentParts.push(`已处理 ${ordersProcessed} 单,跳过 ${ordersSkipped} 单`);
}
if (currentText) {
if (matchedOrders > 0 && ordersProcessed > 0) {
currentParts.unshift(`范围内进度: ${ordersProcessed} / ${matchedOrders}`);
}
currentText.textContent = currentParts.join(' · ');
}
if (warningsWrap && warningsContainer) {
if (warnings.length > 0) {
warningsWrap.style.display = '';
warningsContainer.innerHTML = warnings.map(message => `
${escapeHtml(message)}
`).join('');
} else {
warningsWrap.style.display = 'none';
warningsContainer.innerHTML = '';
}
}
setOrderHistorySyncFormDisabled(status === 'pending' || status === 'running');
}
async function openOrderHistorySyncModal() {
try {
const modalElement = document.getElementById('orderHistorySyncModal');
if (!modalElement) return;
orderHistorySyncModalInstance = bootstrap.Modal.getOrCreateInstance(modalElement);
const accounts = await fetchOrderSyncAccounts(true);
const select = document.getElementById('orderHistorySyncCookieId');
renderOrderAccountOptions(select, accounts, { includeAllOption: true });
const pageFilterValue = document.getElementById('orderCookieFilter')?.value || '';
const startDateInput = document.getElementById('orderHistorySyncStartDate');
const endDateInput = document.getElementById('orderHistorySyncEndDate');
const maxOrdersInput = document.getElementById('orderHistorySyncMaxOrders');
const fetchDetailsInput = document.getElementById('orderHistorySyncFetchDetails');
if (startDateInput && !startDateInput.value) {
startDateInput.value = getRelativeBeijingDateInputValue(-30);
}
if (endDateInput && !endDateInput.value) {
endDateInput.value = getRelativeBeijingDateInputValue(0);
}
if (maxOrdersInput && !maxOrdersInput.value) {
maxOrdersInput.value = '120';
}
if (fetchDetailsInput && !activeOrderHistorySyncJobId) {
fetchDetailsInput.checked = true;
}
if (select && !activeOrderHistorySyncJobId) {
select.value = pageFilterValue || '';
}
if (activeOrderHistorySyncJobId) {
try {
await fetchOrderHistorySyncStatus(activeOrderHistorySyncJobId, { silentToast: true });
} catch (error) {
if (activeOrderHistorySyncJobId) {
throw error;
}
}
}
if (!activeOrderHistorySyncJobId) {
resetOrderHistorySyncProgress();
}
orderHistorySyncModalInstance.show();
} catch (error) {
console.error('打开历史订单同步弹窗失败:', error);
showToast('加载历史同步配置失败', 'danger');
}
}
async function startOrderHistorySync() {
try {
const cookieId = document.getElementById('orderHistorySyncCookieId')?.value || '';
const startDate = document.getElementById('orderHistorySyncStartDate')?.value || '';
const endDate = document.getElementById('orderHistorySyncEndDate')?.value || '';
const maxOrders = parseInt(document.getElementById('orderHistorySyncMaxOrders')?.value || '120', 10);
const fetchDetails = Boolean(document.getElementById('orderHistorySyncFetchDetails')?.checked);
if (!startDate || !endDate) {
showToast('请选择开始日期和结束日期', 'warning');
return;
}
if (startDate > endDate) {
showToast('开始日期不能晚于结束日期', 'warning');
return;
}
if (!Number.isFinite(maxOrders) || maxOrders < 1 || maxOrders > 500) {
showToast('最多同步单数需在 1 到 500 之间', 'warning');
return;
}
const startBtn = document.getElementById('orderHistorySyncStartBtn');
if (startBtn) {
startBtn.disabled = true;
startBtn.innerHTML = '创建任务中';
}
const response = await fetch(`${apiBase}/api/orders/history-sync`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
cookie_id: cookieId || null,
start_date: startDate,
end_date: endDate,
max_orders: maxOrders,
fetch_details: fetchDetails,
})
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.success || !result.data) {
throw new Error(result.detail || result.message || '创建历史订单同步任务失败');
}
activeOrderHistorySyncJobId = result.data.job_id;
orderHistorySyncNotifiedJobId = '';
renderOrderHistorySyncJob(result.data);
scheduleOrderHistorySyncPolling(activeOrderHistorySyncJobId);
showToast('历史订单同步已开始', 'success');
} catch (error) {
console.error('创建历史订单同步任务失败:', error);
showToast(error.message || '创建历史订单同步任务失败', 'danger');
setOrderHistorySyncFormDisabled(false);
} finally {
const startBtn = document.getElementById('orderHistorySyncStartBtn');
if (startBtn && !startBtn.disabled) {
startBtn.innerHTML = ' 开始同步';
}
}
}
async function fetchOrderHistorySyncStatus(jobId, options = {}) {
if (!jobId) return null;
const { silentToast = false } = options;
const response = await fetch(`${apiBase}/api/orders/history-sync/${jobId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.success || !result.data) {
if (response.status === 404) {
activeOrderHistorySyncJobId = '';
stopOrderHistorySyncPolling();
resetOrderHistorySyncProgress();
}
throw new Error(result.detail || result.message || '获取历史订单同步状态失败');
}
const job = result.data;
activeOrderHistorySyncJobId = job.job_id || activeOrderHistorySyncJobId;
renderOrderHistorySyncJob(job);
const status = String(job?.status || '').toLowerCase();
if (status === 'pending' || status === 'running') {
scheduleOrderHistorySyncPolling(job.job_id);
} else {
stopOrderHistorySyncPolling();
const startBtn = document.getElementById('orderHistorySyncStartBtn');
if (startBtn) {
startBtn.innerHTML = ' 开始同步';
}
if (!silentToast && orderHistorySyncNotifiedJobId !== job.job_id) {
orderHistorySyncNotifiedJobId = job.job_id;
if (status === 'completed') {
showToast(job.message || '历史订单同步完成', 'success');
} else if (status === 'failed') {
showToast(job.error || job.message || '历史订单同步失败', 'danger');
} else if (status === 'cancelled') {
showToast(job.message || '历史订单同步已取消', 'warning');
}
await refreshOrdersData();
}
}
return job;
}
async function cancelOrderHistorySync() {
if (!activeOrderHistorySyncJobId) {
showToast('当前没有可取消的历史同步任务', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/api/orders/history-sync/${activeOrderHistorySyncJobId}/cancel`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.success || !result.data) {
throw new Error(result.detail || result.message || '取消历史订单同步失败');
}
stopOrderHistorySyncPolling();
renderOrderHistorySyncJob(result.data);
orderHistorySyncNotifiedJobId = result.data.job_id || orderHistorySyncNotifiedJobId;
const startBtn = document.getElementById('orderHistorySyncStartBtn');
if (startBtn) {
startBtn.innerHTML = ' 开始同步';
}
showToast(result.data.message || '历史订单同步已取消', 'warning');
await refreshOrdersData();
} catch (error) {
console.error('取消历史订单同步失败:', error);
showToast(error.message || '取消历史订单同步失败', 'danger');
}
}
// 清空订单筛选条件
function clearOrderFilters() {
const searchInput = document.getElementById('orderSearchInput');
const statusFilter = document.getElementById('orderStatusFilter');
const cookieFilter = document.getElementById('orderCookieFilter');
if (searchInput) searchInput.value = '';
if (statusFilter) statusFilter.value = '';
if (cookieFilter) cookieFilter.value = '';
filterOrders();
showToast('筛选条件已清空', 'info');
}
// 显示订单详情
async function showOrderDetail(orderId) {
try {
const order = allOrdersData.find(o => o.order_id === orderId);
if (!order) {
showToast('订单不存在', 'warning');
return;
}
// 创建模态框内容
const safeOrderId = escapeHtml(order.order_id || '');
const safeItemId = escapeHtml(order.item_id || '未知');
const safeBuyerId = escapeHtml(order.buyer_id || '未知');
const safeBuyerNick = escapeHtml(order.buyer_nick || '未知');
const safeCookieId = escapeHtml(order.cookie_id || '未知');
const safeSpecName = escapeHtml(order.spec_name || '无');
const safeSpecValue = escapeHtml(order.spec_value || '无');
const safeSpecName2 = escapeHtml(order.spec_name_2 || '无');
const safeSpecValue2 = escapeHtml(order.spec_value_2 || '无');
const safeQuantity = escapeHtml(order.quantity || '1');
const safeAmount = escapeHtml(formatOrderAmountDisplay(order.amount));
const safePlatformCreatedAt = escapeHtml(formatBeijingDateTimeWithSeconds(order.platform_created_at));
const safePlatformPaidAt = escapeHtml(formatBeijingDateTimeWithSeconds(order.platform_paid_at));
const safePlatformCompletedAt = escapeHtml(formatBeijingDateTimeWithSeconds(order.platform_completed_at));
const safeCreatedAt = escapeHtml(formatBeijingDateTimeWithSeconds(order.created_at));
const safeUpdatedAt = escapeHtml(formatBeijingDateTimeWithSeconds(order.updated_at));
const safeStatusText = escapeHtml(getOrderStatusText(order.order_status));
const modalContent = `
基本信息
| 订单ID | ${safeOrderId} |
| 商品ID | ${safeItemId} |
| 买家ID | ${safeBuyerId} |
| 买家昵称 | ${safeBuyerNick} |
| Cookie账号 | ${safeCookieId} |
| 订单状态 | ${safeStatusText} |
商品信息
| 规格1名称 | ${safeSpecName} |
| 规格1值 | ${safeSpecValue} |
| 规格2名称 | ${safeSpecName2} |
| 规格2值 | ${safeSpecValue2} |
| 数量 | ${safeQuantity} |
| 金额 | ${safeAmount} |
时间信息
| 平台下单时间 | ${safePlatformCreatedAt} |
| 平台付款时间 | ${safePlatformPaidAt} |
| 平台完成时间 | ${safePlatformCompletedAt} |
| 入库时间 | ${safeCreatedAt} |
| 更新时间 | ${safeUpdatedAt} |
`;
// 移除已存在的模态框
const existingModal = document.getElementById('orderDetailModal');
if (existingModal) {
existingModal.remove();
}
// 添加新模态框到页面
document.body.insertAdjacentHTML('beforeend', modalContent);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('orderDetailModal'));
modal.show();
// 异步加载商品详情
if (order.item_id) {
loadItemDetailForOrder(order.item_id, order.cookie_id);
}
} catch (error) {
console.error('显示订单详情失败:', error);
showToast('显示订单详情失败', 'danger');
}
}
// 为订单加载商品详情
async function loadItemDetailForOrder(itemId, cookieId) {
try {
const token = localStorage.getItem('auth_token');
// 尝试从数据库获取商品信息
let response = await fetch(`${apiBase}/items/${cookieId}/${itemId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const content = document.getElementById('itemDetailContent');
if (!content) return;
if (response.ok) {
const data = await response.json();
const item = data.item;
const safeTitle = escapeHtml(item.item_title || '商品标题未知');
const safeDescription = escapeHtml(item.item_description || '暂无描述');
const safeCategory = escapeHtml(item.item_category || '未知');
const safePrice = escapeHtml(item.item_price || '未知');
const safeDetail = escapeHtml(item.item_detail || '');
content.innerHTML = `
${safeTitle}
${safeDescription}
分类:${safeCategory}
价格:${safePrice}
${item.item_detail ? `
` : ''}
`;
} else {
content.innerHTML = `
无法获取商品详情信息
`;
}
} catch (error) {
console.error('加载商品详情失败:', error);
const content = document.getElementById('itemDetailContent');
if (content) {
content.innerHTML = `
加载商品详情失败:${escapeHtml(error.message || '未知错误')}
`;
}
}
}
// 删除订单
async function deleteOrder(orderId) {
try {
const confirmed = confirm(`确定要删除订单吗?\n\n订单ID: ${orderId}\n\n此操作不可撤销!`);
if (!confirmed) {
return;
}
const response = await fetch(`${apiBase}/api/orders/${orderId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
showToast('订单删除成功', 'success');
// 刷新列表
await refreshOrdersData();
} else {
const error = await response.text();
showToast(`删除失败: ${error}`, 'danger');
}
} catch (error) {
console.error('删除订单失败:', error);
showToast('删除订单失败', 'danger');
}
}
// 批量删除订单
async function batchDeleteOrders() {
const checkboxes = document.querySelectorAll('.order-checkbox:checked');
if (checkboxes.length === 0) {
showToast('请先选择要删除的订单', 'warning');
return;
}
const orderIds = Array.from(checkboxes).map(cb => cb.value);
const confirmed = confirm(`确定要删除选中的 ${orderIds.length} 个订单吗?\n\n此操作不可撤销!`);
if (!confirmed) return;
try {
let successCount = 0;
let failCount = 0;
for (const orderId of orderIds) {
try {
const response = await fetch(`${apiBase}/api/orders/${orderId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
failCount++;
}
}
if (successCount > 0) {
showToast(`成功删除 ${successCount} 个订单${failCount > 0 ? `,${failCount} 个失败` : ''}`,
failCount > 0 ? 'warning' : 'success');
await refreshOrdersData();
} else {
showToast('批量删除失败', 'danger');
}
} catch (error) {
console.error('批量删除订单失败:', error);
showToast('批量删除订单失败', 'danger');
}
}
// 手动发货订单
async function manualDeliverOrder(orderId) {
try {
const confirmed = confirm(`确定要手动发货此订单吗?\n\n订单ID: ${orderId}\n\n系统将根据发货规则自动匹配发货内容并发送给买家。`);
if (!confirmed) {
return;
}
showToast('正在执行发货...', 'info');
const response = await fetch(`${apiBase}/api/orders/${orderId}/deliver`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok) {
if (result.delivered) {
showToast(`发货成功!\n${result.message}`, 'success');
// 刷新今日发货统计
refreshTodayDeliveryCount();
} else {
showToast(`发货失败: ${result.message}`, 'warning');
}
// 刷新订单列表
await refreshOrdersData();
} else {
showToast(`发货失败: ${result.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('手动发货失败:', error);
showToast('手动发货失败: ' + error.message, 'danger');
}
}
// 刷新订单状态
async function refreshOrderStatus(orderId) {
try {
showToast('正在刷新订单状态...', 'info');
const response = await fetch(`${apiBase}/api/orders/${orderId}/refresh`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok) {
if (result.updated) {
showToast(`订单状态已更新: ${getOrderStatusText(result.new_status)}`, 'success');
} else {
showToast(result.message || '订单状态无变化', 'info');
}
// 刷新订单列表
await refreshOrdersData();
} else {
showToast(`刷新失败: ${result.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('刷新订单状态失败:', error);
showToast('刷新订单状态失败: ' + error.message, 'danger');
}
}
// 切换全选订单
function toggleSelectAllOrders(checkbox) {
const orderCheckboxes = document.querySelectorAll('.order-checkbox');
orderCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateOrderBatchButtons();
}
// 更新批量操作按钮状态
function updateOrderBatchButtons() {
const checkboxes = document.querySelectorAll('.order-checkbox:checked');
const batchDeleteBtn = document.getElementById('batchDeleteOrdersBtn');
const batchRefreshBtn = document.getElementById('batchRefreshOrdersBtn');
const hasSelection = checkboxes.length > 0;
if (batchDeleteBtn) {
batchDeleteBtn.disabled = !hasSelection;
}
if (batchRefreshBtn) {
batchRefreshBtn.disabled = !hasSelection;
}
}
// 批量刷新订单状态
async function batchRefreshOrders() {
const checkboxes = document.querySelectorAll('.order-checkbox:checked');
if (checkboxes.length === 0) {
showToast('请先选择要刷新的订单', 'warning');
return;
}
const orderIds = Array.from(checkboxes).map(cb => cb.value);
const confirmed = confirm(`确定要刷新选中的 ${orderIds.length} 个订单状态吗?\n\n这可能需要一些时间...`);
if (!confirmed) return;
showToast(`正在刷新 ${orderIds.length} 个订单状态...`, 'info');
let successCount = 0;
let failCount = 0;
for (const orderId of orderIds) {
try {
const response = await fetch(`${apiBase}/api/orders/${orderId}/refresh`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`刷新订单 ${orderId} 失败:`, error);
failCount++;
}
}
// 刷新订单列表
await refreshOrdersData();
if (failCount === 0) {
showToast(`成功刷新 ${successCount} 个订单状态`, 'success');
} else {
showToast(`刷新完成: ${successCount} 成功, ${failCount} 失败`, 'warning');
}
}
// 页面加载完成后初始化订单搜索功能
document.addEventListener('DOMContentLoaded', function() {
// 延迟初始化,确保DOM完全加载
setTimeout(() => {
initOrdersSearch();
const orderHistorySyncModal = document.getElementById('orderHistorySyncModal');
if (orderHistorySyncModal) {
orderHistorySyncModal.addEventListener('hidden.bs.modal', () => {
stopOrderHistorySyncPolling();
});
}
// 绑定复选框变化事件
document.addEventListener('change', function(e) {
if (e.target.classList.contains('order-checkbox')) {
updateOrderBatchButtons();
}
});
document.addEventListener('click', function(e) {
const actionButton = e.target.closest('.order-action-btn');
if (!actionButton) return;
const orderId = actionButton.dataset.orderId;
const action = actionButton.dataset.orderAction;
if (!orderId || !action) return;
if (action === 'deliver') {
manualDeliverOrder(orderId);
} else if (action === 'refresh') {
refreshOrderStatus(orderId);
} else if (action === 'detail') {
showOrderDetail(orderId);
} else if (action === 'delete') {
deleteOrder(orderId);
}
});
}, 100);
});
// ================================
// 用户管理功能
// ================================
// 加载用户管理页面
async function loadUserManagement() {
console.log('加载用户管理页面');
// 检查管理员权限
try {
const response = await fetch(`${apiBase}/verify`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
if (!result.is_admin) {
showToast('您没有权限访问用户管理功能', 'danger');
showSection('dashboard'); // 跳转回仪表盘
return;
}
} else {
showToast('权限验证失败', 'danger');
return;
}
} catch (error) {
console.error('权限验证失败:', error);
showToast('权限验证失败', 'danger');
return;
}
// 加载数据
await loadUserSystemStats();
await loadUsers();
}
// 加载用户系统统计信息
async function loadUserSystemStats() {
try {
const token = localStorage.getItem('auth_token');
// 获取用户统计
const usersResponse = await fetch('/admin/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (usersResponse.ok) {
const usersData = await usersResponse.json();
document.getElementById('totalUsers').textContent = usersData.users.length;
}
// 获取Cookie统计
const cookiesResponse = await fetch(`${apiBase}/admin/data/cookies`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (cookiesResponse.ok) {
const cookiesData = await cookiesResponse.json();
document.getElementById('totalUserCookies').textContent = cookiesData.data ? cookiesData.data.length : 0;
}
// 获取卡券统计
const cardsResponse = await fetch(`${apiBase}/admin/data/cards`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (cardsResponse.ok) {
const cardsData = await cardsResponse.json();
document.getElementById('totalUserCards').textContent = cardsData.data ? cardsData.data.length : 0;
}
} catch (error) {
console.error('加载系统统计失败:', error);
}
}
// 加载用户列表
async function loadUsers() {
const loadingDiv = document.getElementById('loadingUsers');
const usersListDiv = document.getElementById('usersList');
const noUsersDiv = document.getElementById('noUsers');
// 显示加载状态
loadingDiv.style.display = 'block';
usersListDiv.style.display = 'none';
noUsersDiv.style.display = 'none';
try {
const token = localStorage.getItem('auth_token');
const response = await fetch('/admin/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
loadingDiv.style.display = 'none';
if (data.users && data.users.length > 0) {
usersListDiv.style.display = 'block';
displayUsers(data.users);
} else {
noUsersDiv.style.display = 'block';
}
} else {
throw new Error('获取用户列表失败');
}
} catch (error) {
console.error('加载用户列表失败:', error);
loadingDiv.style.display = 'none';
noUsersDiv.style.display = 'block';
showToast('加载用户列表失败', 'danger');
}
}
// 显示用户列表
function displayUsers(users) {
const usersListDiv = document.getElementById('usersList');
usersListDiv.innerHTML = '';
users.forEach(user => {
const userCard = createUserCard(user);
usersListDiv.appendChild(userCard);
});
}
// 创建用户卡片
function createUserCard(user) {
const col = document.createElement('div');
col.className = 'col-md-6 col-lg-4 mb-3';
// 使用is_admin字段判断是否为管理员
const isAdmin = user.is_admin === true;
const badgeClass = isAdmin ? 'bg-danger' : 'bg-primary';
const badgeText = isAdmin ? '管理员' : '普通用户';
// 获取当前登录用户的ID
let currentUserId = null;
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
currentUserId = userInfo.user_id;
} catch (e) {
console.error('解析用户信息失败:', e);
}
const isSelf = user.id === currentUserId;
col.innerHTML = `
${user.username}
${badgeText}
${user.email || '未设置邮箱'}
注册时间:${formatDateTime(user.created_at)}
Cookie数: ${user.cookie_count || 0} |
卡券数: ${user.card_count || 0}
${!isSelf ? `
` : `
当前用户
`}
`;
return col;
}
// 切换用户管理员状态
async function toggleUserAdmin(userId, username, setAdmin) {
const action = setAdmin ? '设置为管理员' : '取消管理员权限';
if (!confirm(`确定要将用户 "${username}" ${action}吗?`)) {
return;
}
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/users/${userId}/admin-status?is_admin=${setAdmin}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
showToast(data.message || `用户已${action}`, 'success');
// 刷新用户列表
await loadUsers();
} else {
const errorData = await response.json();
showToast(`操作失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('更新用户权限失败:', error);
showToast('更新用户权限失败', 'danger');
}
}
// 全局变量用于存储当前要删除的用户信息
let currentDeleteUserId = null;
let currentDeleteUserName = null;
let deleteUserModal = null;
// 删除用户
function deleteUser(userId, username) {
// 存储要删除的用户信息
currentDeleteUserId = userId;
currentDeleteUserName = username;
// 初始化模态框(如果还没有初始化)
if (!deleteUserModal) {
deleteUserModal = new bootstrap.Modal(document.getElementById('deleteUserModal'));
}
// 显示确认模态框
deleteUserModal.show();
}
// 确认删除用户
async function confirmDeleteUser() {
if (!currentDeleteUserId) return;
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/users/${currentDeleteUserId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
deleteUserModal.hide();
showToast(data.message || '用户删除成功', 'success');
// 刷新页面数据
await loadUserSystemStats();
await loadUsers();
} else {
const errorData = await response.json();
showToast(`删除失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('删除用户失败:', error);
showToast('删除用户失败', 'danger');
} finally {
// 清理状态
currentDeleteUserId = null;
currentDeleteUserName = null;
}
}
// 刷新用户列表
async function refreshUsers() {
await loadUserSystemStats();
await loadUsers();
showToast('用户列表已刷新', 'success');
}
// ================================
// 数据管理功能
// ================================
// 全局变量
let currentTable = '';
let currentData = [];
// 表的中文描述
const tableDescriptions = {
'users': '用户表',
'cookies': 'Cookie账号表',
'cookie_status': 'Cookie状态表',
'keywords': '关键字表',
'item_replay': '指定商品回复表',
'default_replies': '默认回复表',
'default_reply_records': '默认回复记录表',
'ai_reply_settings': 'AI回复设置表',
'ai_conversations': 'AI对话历史表',
'ai_item_cache': 'AI商品信息缓存表',
'item_info': '商品信息表',
'message_notifications': '消息通知表',
'cards': '卡券表',
'delivery_rules': '发货规则表',
'notification_channels': '通知渠道表',
'user_settings': '用户设置表',
'system_settings': '系统设置表',
'email_verifications': '邮箱验证表',
'captcha_codes': '验证码表',
'orders': '订单表'
};
// 加载数据管理页面
async function loadDataManagement() {
console.log('加载数据管理页面');
// 检查管理员权限
try {
const response = await fetch(`${apiBase}/verify`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
if (!result.is_admin) {
showToast('您没有权限访问数据管理功能', 'danger');
showSection('dashboard'); // 跳转回仪表盘
return;
}
} else {
showToast('权限验证失败', 'danger');
return;
}
} catch (error) {
console.error('权限验证失败:', error);
showToast('权限验证失败', 'danger');
return;
}
// 重置状态
currentTable = '';
currentData = [];
// 重置界面
showNoTableSelected();
// 重置表格选择器
const tableSelect = document.getElementById('tableSelect');
if (tableSelect) {
tableSelect.value = '';
}
}
// 显示未选择表格状态
function showNoTableSelected() {
document.getElementById('loadingTable').style.display = 'none';
document.getElementById('noTableSelected').style.display = 'block';
document.getElementById('noTableData').style.display = 'none';
document.getElementById('tableContainer').style.display = 'none';
// 重置统计信息
document.getElementById('recordCount').textContent = '-';
document.getElementById('tableTitle').innerHTML = ' 数据表';
// 禁用按钮
document.getElementById('clearBtn').disabled = true;
}
// 显示加载状态
function showLoading() {
document.getElementById('loadingTable').style.display = 'block';
document.getElementById('noTableSelected').style.display = 'none';
document.getElementById('noTableData').style.display = 'none';
document.getElementById('tableContainer').style.display = 'none';
}
// 显示无数据状态
function showNoData() {
document.getElementById('loadingTable').style.display = 'none';
document.getElementById('noTableSelected').style.display = 'none';
document.getElementById('noTableData').style.display = 'block';
document.getElementById('tableContainer').style.display = 'none';
}
// 加载表数据
async function loadTableData() {
const tableSelect = document.getElementById('tableSelect');
const selectedTable = tableSelect.value;
if (!selectedTable) {
showNoTableSelected();
return;
}
currentTable = selectedTable;
showLoading();
const token = localStorage.getItem('auth_token');
try {
const response = await fetch(`/admin/data/${selectedTable}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success) {
currentData = data.data;
displayTableData(data.data, data.columns);
updateTableInfo(selectedTable, data.data.length);
} else {
showToast('加载数据失败: ' + data.message, 'danger');
showNoData();
}
} catch (error) {
console.error('加载数据失败:', error);
showToast('加载数据失败', 'danger');
showNoData();
}
}
// 显示表格数据
function displayTableData(data, columns) {
if (!data || data.length === 0) {
showNoData();
return;
}
// 显示表格容器
document.getElementById('loadingTable').style.display = 'none';
document.getElementById('noTableSelected').style.display = 'none';
document.getElementById('noTableData').style.display = 'none';
document.getElementById('tableContainer').style.display = 'block';
// 生成表头(添加操作列)
const tableHeaders = document.getElementById('tableHeaders');
const headerHtml = columns.map(col => `${col} | `).join('') + '操作 | ';
tableHeaders.innerHTML = headerHtml;
// 生成表格内容(添加删除按钮)
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = data.map((row, index) => {
const dataCells = columns.map(col => {
let value = row[col];
if (value === null || value === undefined) {
value = 'NULL';
} else if (typeof value === 'string' && value.length > 50) {
value = `${escapeHtml(value.substring(0, 50))}...`;
} else {
value = escapeHtml(String(value));
}
return `${value} | `;
}).join('');
// 添加操作列(删除按钮)
const recordId = row.id || row.user_id || index;
const actionCell = `
| `;
return `${dataCells}${actionCell}
`;
}).join('');
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 更新表格信息
function updateTableInfo(tableName, recordCount) {
const description = tableDescriptions[tableName] || tableName;
document.getElementById('tableTitle').innerHTML = ` ${description}`;
document.getElementById('recordCount').textContent = recordCount;
// 启用清空按钮
document.getElementById('clearBtn').disabled = false;
}
// 刷新表格数据
function refreshTableData() {
if (currentTable) {
loadTableData();
showToast('数据已刷新', 'success');
} else {
showToast('请先选择数据表', 'warning');
}
}
// 导出表格数据
async function exportTableData() {
if (!currentTable || !currentData || currentData.length === 0) {
showToast('没有可导出的数据', 'warning');
return;
}
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/data/${currentTable}/export`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${currentTable}_${new Date().toISOString().slice(0, 10)}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showToast('数据导出成功', 'success');
} else {
showToast('导出失败', 'danger');
}
} catch (error) {
console.error('导出数据失败:', error);
showToast('导出数据失败', 'danger');
}
}
// 清空表格数据
async function clearTableData() {
if (!currentTable) {
showToast('请先选择数据表', 'warning');
return;
}
const description = tableDescriptions[currentTable] || currentTable;
const confirmed = confirm(`确定要清空 "${description}" 的所有数据吗?\n\n此操作不可撤销!`);
if (!confirmed) return;
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/data/${currentTable}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
showToast(data.message || '数据清空成功', 'success');
// 重新加载数据
loadTableData();
} else {
const errorData = await response.json();
showToast(`清空失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('清空数据失败:', error);
showToast('清空数据失败', 'danger');
}
}
// 删除记录相关变量
let currentDeleteId = null;
let deleteRecordModal = null;
// 初始化删除记录模态框
function initDeleteRecordModal() {
if (!deleteRecordModal) {
deleteRecordModal = new bootstrap.Modal(document.getElementById('deleteRecordModal'));
}
}
// 通过索引删除记录
function deleteRecordByIndex(index) {
console.log('deleteRecordByIndex被调用,index:', index);
console.log('currentData:', currentData);
console.log('当前currentTable:', currentTable);
if (!currentData || index >= currentData.length) {
console.error('无效的索引或数据不存在');
showToast('删除失败:数据不存在', 'danger');
return;
}
const record = currentData[index];
console.log('获取到的record:', record);
deleteRecord(record, index);
}
// 删除记录
function deleteRecord(record, index) {
console.log('deleteRecord被调用');
console.log('record:', record);
console.log('index:', index);
console.log('当前currentTable:', currentTable);
initDeleteRecordModal();
// 尝试多种方式获取记录ID
currentDeleteId = record.id || record.user_id || record.cookie_id || record.keyword_id ||
record.card_id || record.item_id || record.order_id || index;
console.log('设置currentDeleteId为:', currentDeleteId);
console.log('record的所有字段:', Object.keys(record));
console.log('record的所有值:', record);
// 显示记录信息
const deleteRecordInfo = document.getElementById('deleteRecordInfo');
deleteRecordInfo.innerHTML = '';
Object.keys(record).forEach(key => {
const div = document.createElement('div');
div.innerHTML = `${key}: ${record[key] || '-'}`;
deleteRecordInfo.appendChild(div);
});
deleteRecordModal.show();
}
// 确认删除记录
async function confirmDeleteRecord() {
console.log('confirmDeleteRecord被调用');
console.log('currentDeleteId:', currentDeleteId);
console.log('currentTable:', currentTable);
if (!currentDeleteId || !currentTable) {
console.error('缺少必要参数:', { currentDeleteId, currentTable });
showToast('删除失败:缺少必要参数', 'danger');
return;
}
try {
const token = localStorage.getItem('auth_token');
const url = `/admin/data/${currentTable}/${currentDeleteId}`;
console.log('发送删除请求到:', url);
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('删除响应状态:', response.status);
if (response.ok) {
const data = await response.json();
console.log('删除成功响应:', data);
deleteRecordModal.hide();
showToast(data.message || '删除成功', 'success');
loadTableData(); // 重新加载数据
} else {
const errorData = await response.json();
console.error('删除失败响应:', errorData);
showToast(`删除失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('删除记录失败:', error);
showToast('删除记录失败: ' + error.message, 'danger');
}
}
// ================================
// 系统日志管理功能
// ================================
let logAutoRefreshInterval = null;
let currentLogLevel = '';
// 加载系统日志
async function loadSystemLogs() {
const token = localStorage.getItem('auth_token');
const lines = document.getElementById('logLines').value;
const level = currentLogLevel;
const loadingDiv = document.getElementById('loadingSystemLogs');
const logContainer = document.getElementById('systemLogContainer');
const noLogsDiv = document.getElementById('noSystemLogs');
loadingDiv.style.display = 'block';
logContainer.style.display = 'none';
noLogsDiv.style.display = 'none';
let url = `/admin/logs?lines=${lines}`;
if (level) {
url += `&level=${level}`;
}
try {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
loadingDiv.style.display = 'none';
if (data.logs && data.logs.length > 0) {
displaySystemLogs(data.logs);
updateLogInfo(data);
logContainer.style.display = 'block';
} else {
noLogsDiv.style.display = 'block';
}
// 更新最后更新时间
document.getElementById('logLastUpdate').textContent =
'最后更新: ' + new Date().toLocaleTimeString('zh-CN');
} catch (error) {
console.error('加载日志失败:', error);
loadingDiv.style.display = 'none';
noLogsDiv.style.display = 'block';
showToast('加载日志失败', 'danger');
}
}
// 显示系统日志
function displaySystemLogs(logs) {
const logContainer = document.getElementById('systemLogContainer');
logContainer.innerHTML = '';
// 反转日志数组,让最新的日志显示在最上面
const reversedLogs = [...logs].reverse();
reversedLogs.forEach(log => {
const logLine = document.createElement('div');
logLine.className = 'log-entry';
// 根据日志级别添加颜色类
if (log.includes('| INFO |')) {
logLine.classList.add('INFO');
} else if (log.includes('| WARNING |')) {
logLine.classList.add('WARNING');
} else if (log.includes('| ERROR |')) {
logLine.classList.add('ERROR');
} else if (log.includes('| DEBUG |')) {
logLine.classList.add('DEBUG');
} else if (log.includes('| CRITICAL |')) {
logLine.classList.add('CRITICAL');
}
logLine.textContent = log;
logContainer.appendChild(logLine);
});
// 自动滚动到顶部(显示最新日志)
scrollLogToTop();
}
// 更新日志信息
function updateLogInfo(data) {
document.getElementById('logFileName').textContent = data.log_file || '-';
document.getElementById('logDisplayLines').textContent = data.total_lines || '-';
}
// 按级别过滤日志
function filterLogsByLevel(level) {
currentLogLevel = level;
// 更新过滤按钮状态
document.querySelectorAll('.filter-badge').forEach(badge => {
badge.classList.remove('active');
});
document.querySelector(`[data-level="${level}"]`).classList.add('active');
// 更新当前过滤显示
const filterText = level ? level.toUpperCase() : '全部';
document.getElementById('logCurrentFilter').textContent = filterText;
// 重新加载日志
loadSystemLogs();
}
// 切换日志自动刷新
function toggleLogAutoRefresh() {
const autoRefresh = document.getElementById('autoRefreshLogs');
const label = document.getElementById('autoRefreshLogLabel');
const icon = document.getElementById('autoRefreshLogIcon');
if (autoRefresh.checked) {
// 开启自动刷新
logAutoRefreshInterval = setInterval(loadSystemLogs, 5000); // 每5秒刷新
label.textContent = '开启 (5s)';
icon.style.display = 'inline';
icon.classList.add('auto-refresh-indicator');
} else {
// 关闭自动刷新
if (logAutoRefreshInterval) {
clearInterval(logAutoRefreshInterval);
logAutoRefreshInterval = null;
}
label.textContent = '关闭';
icon.style.display = 'none';
icon.classList.remove('auto-refresh-indicator');
}
}
// 滚动到日志顶部
function scrollLogToTop() {
const logContainer = document.getElementById('systemLogContainer');
logContainer.scrollTop = 0;
}
// 滚动到日志底部
function scrollLogToBottom() {
const logContainer = document.getElementById('systemLogContainer');
logContainer.scrollTop = logContainer.scrollHeight;
}
// 打开日志导出模态框
function openLogExportModal() {
const modalElement = document.getElementById('exportLogModal');
if (!modalElement) {
console.warn('未找到导出日志模态框元素');
return;
}
resetLogFileModalState();
const modal = new bootstrap.Modal(modalElement);
modal.show();
loadLogFileList();
}
function resetLogFileModalState() {
const loading = document.getElementById('logFileLoading');
const list = document.getElementById('logFileList');
const empty = document.getElementById('logFileEmpty');
const error = document.getElementById('logFileError');
if (loading) loading.classList.remove('d-none');
if (list) list.innerHTML = '';
if (empty) empty.classList.add('d-none');
if (error) {
error.classList.add('d-none');
error.textContent = '';
}
}
async function loadLogFileList() {
const token = localStorage.getItem('auth_token');
const loading = document.getElementById('logFileLoading');
const list = document.getElementById('logFileList');
const empty = document.getElementById('logFileEmpty');
const error = document.getElementById('logFileError');
if (!loading || !list || !empty || !error) {
console.warn('日志文件列表元素缺失');
return;
}
loading.classList.remove('d-none');
list.innerHTML = '';
empty.classList.add('d-none');
error.classList.add('d-none');
error.textContent = '';
try {
const response = await fetch(`${apiBase}/admin/log-files`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
loading.classList.add('d-none');
if (!response.ok) {
const message = await response.text();
error.classList.remove('d-none');
error.textContent = `加载日志文件失败: ${message || response.status}`;
return;
}
const data = await response.json();
if (!data.success) {
error.classList.remove('d-none');
error.textContent = data.message || '加载日志文件失败';
return;
}
const files = data.files || [];
if (files.length === 0) {
empty.classList.remove('d-none');
return;
}
files.forEach(file => {
const item = document.createElement('div');
item.className = 'list-group-item d-flex justify-content-between align-items-start flex-wrap gap-3';
const info = document.createElement('div');
info.className = 'me-auto';
const title = document.createElement('div');
title.className = 'fw-semibold';
title.textContent = file.name || '未知文件';
const meta = document.createElement('div');
meta.className = 'small text-muted';
const sizeText = typeof file.size === 'number' ? formatFileSize(file.size) : '未知大小';
const timeText = file.modified_at ? formatLogTimestamp(file.modified_at) : '-';
meta.textContent = `大小: ${sizeText} · 更新时间: ${timeText}`;
info.appendChild(title);
info.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'd-flex align-items-center gap-2';
const downloadBtn = document.createElement('button');
downloadBtn.type = 'button';
downloadBtn.className = 'btn btn-sm btn-outline-primary';
downloadBtn.innerHTML = '下载';
downloadBtn.onclick = () => downloadLogFile(file.name, downloadBtn);
actions.appendChild(downloadBtn);
item.appendChild(info);
item.appendChild(actions);
list.appendChild(item);
});
} catch (err) {
console.error('加载日志文件失败:', err);
loading.classList.add('d-none');
error.classList.remove('d-none');
error.textContent = '加载日志文件失败,请稍后重试';
}
}
function refreshLogFileList() {
resetLogFileModalState();
loadLogFileList();
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
if (!Number.isFinite(bytes)) return '未知大小';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
const size = bytes / Math.pow(1024, index);
return `${size.toFixed(index === 0 ? 0 : 2)} ${units[index]}`;
}
function formatLogTimestamp(isoString) {
const date = new Date(isoString);
if (Number.isNaN(date.getTime())) {
return '-';
}
return date.toLocaleString('zh-CN', { hour12: false });
}
async function downloadLogFile(fileName, buttonEl) {
if (!fileName) {
showToast('日志文件名无效', 'warning');
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('请先登录后再导出日志', 'warning');
return;
}
let originalHtml = '';
if (buttonEl) {
originalHtml = buttonEl.innerHTML;
buttonEl.disabled = true;
buttonEl.innerHTML = '下载中...';
}
try {
const response = await fetch(`${apiBase}/admin/logs/export?file=${encodeURIComponent(fileName)}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
const message = await response.text();
showToast(`日志下载失败: ${message || response.status}`, 'danger');
return;
}
let downloadName = fileName;
const contentDisposition = response.headers.get('content-disposition');
if (contentDisposition) {
const match = contentDisposition.match(/filename="?([^"]+)"?/i);
if (match && match[1]) {
downloadName = decodeURIComponent(match[1]);
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = downloadName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(url);
showToast('日志下载成功', 'success');
} catch (error) {
console.error('下载日志文件失败:', error);
showToast('下载日志文件失败,请稍后重试', 'danger');
} finally {
if (buttonEl) {
buttonEl.disabled = false;
buttonEl.innerHTML = originalHtml || '下载';
}
}
}
// ================================
// 风控日志管理功能
// ================================
let currentRiskLogStatus = '';
let currentRiskLogOffset = 0;
const riskLogLimit = 100;
let currentRiskSliderStatsRequestId = 0;
function getRiskSliderStatsRange() {
const activeButton = document.querySelector('#riskSliderRangeFilter .risk-slider-range-btn.is-active');
return activeButton?.dataset.range || 'all';
}
function getRiskSliderStatsRangeLabel(rangeValue = 'all') {
switch (String(rangeValue || '').trim().toLowerCase()) {
case 'today':
return '当日';
case '7d':
return '近 7 天';
default:
return '所有';
}
}
function onRiskSliderRangeChange(rangeValue = 'all') {
document.querySelectorAll('#riskSliderRangeFilter .risk-slider-range-btn').forEach((button) => {
button.classList.toggle('is-active', button.dataset.range === rangeValue);
});
const cookieId = document.getElementById('riskLogCookieFilter')?.value || '';
loadRiskControlSliderStats(cookieId);
}
function setRiskControlSliderStatsLoading(scopeLabel = '全部账号') {
const scopeElement = document.getElementById('riskSliderScope');
const successRateElement = document.getElementById('riskSliderSuccessRate');
const attemptCountElement = document.getElementById('riskSliderAttemptCount');
const successCountElement = document.getElementById('riskSliderSuccessCount');
const failureCountElement = document.getElementById('riskSliderFailureCount');
const recentSuccessElement = document.getElementById('riskSliderRecentSuccess');
const recentFailureElement = document.getElementById('riskSliderRecentFailure');
if (scopeElement) scopeElement.textContent = scopeLabel;
if (successRateElement) successRateElement.textContent = '--';
if (attemptCountElement) attemptCountElement.textContent = '统计中...';
if (successCountElement) successCountElement.textContent = '--';
if (failureCountElement) failureCountElement.textContent = '--';
if (recentSuccessElement) recentSuccessElement.textContent = '--';
if (recentFailureElement) recentFailureElement.textContent = '--';
}
function renderRiskControlSliderStats(stats = {}) {
const scopeElement = document.getElementById('riskSliderScope');
const successRateElement = document.getElementById('riskSliderSuccessRate');
const attemptCountElement = document.getElementById('riskSliderAttemptCount');
const successCountElement = document.getElementById('riskSliderSuccessCount');
const failureCountElement = document.getElementById('riskSliderFailureCount');
const recentSuccessElement = document.getElementById('riskSliderRecentSuccess');
const recentFailureElement = document.getElementById('riskSliderRecentFailure');
const totalSessions = Number(stats.total_sessions ?? stats.total_attempts ?? 0);
const successCount = Number(stats.success_count || 0);
const failureCount = Number(stats.failure_count || 0);
const processingCount = Number(stats.processing_count || 0);
const completedSessions = Number(stats.completed_sessions || (successCount + failureCount));
const successRate = Number.isFinite(Number(stats.success_rate)) ? Number(stats.success_rate).toFixed(1) : '0.0';
const hasData = Boolean(stats.has_data || totalSessions > 0);
const recentSuccessText = formatBeijingDateTime(stats.recent_success);
const recentFailureText = formatBeijingDateTime(stats.recent_failure);
const rangeLabel = stats.range_label || getRiskSliderStatsRangeLabel(stats.selected_range || getRiskSliderStatsRange());
let attemptSummary = stats.summary_text || '暂无滑块验证记录';
if (hasData) {
if (rangeLabel === '所有') {
attemptSummary = `累计滑块相关记录 ${totalSessions} 次`;
} else {
attemptSummary = `${rangeLabel}滑块相关记录 ${totalSessions} 次`;
}
if (processingCount > 0) {
attemptSummary += `,进行中 ${processingCount} 次`;
}
}
if (scopeElement) scopeElement.textContent = stats.scope_label || '全部账号';
if (successRateElement) successRateElement.textContent = completedSessions > 0 ? `${successRate}%` : '--';
if (attemptCountElement) attemptCountElement.textContent = attemptSummary;
if (successCountElement) successCountElement.textContent = String(successCount);
if (failureCountElement) failureCountElement.textContent = String(failureCount);
if (recentSuccessElement) recentSuccessElement.textContent = recentSuccessText;
if (recentFailureElement) recentFailureElement.textContent = recentFailureText;
}
async function loadRiskControlSliderStats(cookieId = '') {
const token = localStorage.getItem('auth_token');
const scopeLabel = cookieId || '全部账号';
const rangeValue = getRiskSliderStatsRange();
const rangeLabel = getRiskSliderStatsRangeLabel(rangeValue);
const requestId = ++currentRiskSliderStatsRequestId;
setRiskControlSliderStatsLoading(scopeLabel);
try {
const params = new URLSearchParams();
if (cookieId) {
params.set('cookie_id', cookieId);
}
params.set('range_key', rangeValue);
const url = `/admin/slider-verification-stats?${params.toString()}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (requestId !== currentRiskSliderStatsRequestId) {
return;
}
if (response.ok && data.success) {
renderRiskControlSliderStats(data.data || {});
return;
}
renderRiskControlSliderStats({
scope_label: scopeLabel,
total_sessions: 0,
success_count: 0,
failure_count: 0,
processing_count: 0,
completed_sessions: 0,
success_rate: 0,
recent_success: '--',
recent_failure: '--',
summary_text: rangeValue === 'all' ? '暂无滑块验证记录' : `${rangeLabel}暂无滑块验证记录`,
selected_range: rangeValue,
range_label: rangeLabel,
has_data: false
});
} catch (error) {
console.error('加载滑块验证统计失败:', error);
if (requestId !== currentRiskSliderStatsRequestId) {
return;
}
renderRiskControlSliderStats({
scope_label: scopeLabel,
total_sessions: 0,
success_count: 0,
failure_count: 0,
processing_count: 0,
completed_sessions: 0,
success_rate: 0,
recent_success: '--',
recent_failure: '--',
summary_text: rangeValue === 'all' ? '暂无滑块验证记录' : `${rangeLabel}暂无滑块验证记录`,
selected_range: rangeValue,
range_label: rangeLabel,
has_data: false
});
}
}
function getRiskLogFilters() {
return {
cookieId: document.getElementById('riskLogCookieFilter')?.value || '',
eventType: document.getElementById('riskLogEventTypeFilter')?.value || '',
triggerScene: document.getElementById('riskLogTriggerSceneFilter')?.value || '',
dateFrom: document.getElementById('riskLogDateFrom')?.value || '',
dateTo: document.getElementById('riskLogDateTo')?.value || '',
sessionId: (document.getElementById('riskLogSessionFilter')?.value || '').trim(),
processingStatus: currentRiskLogStatus,
limit: parseInt(document.getElementById('riskLogLimit')?.value, 10) || 100,
};
}
function hasActiveRiskLogFilters(filters = {}) {
return Boolean(
filters.cookieId ||
filters.processingStatus ||
filters.eventType ||
filters.triggerScene ||
filters.dateFrom ||
filters.dateTo ||
filters.sessionId
);
}
async function fetchRiskControlLogsPage(token, {
cookieId = '',
processingStatus = '',
eventType = '',
triggerScene = '',
dateFrom = '',
dateTo = '',
sessionId = '',
resultCode = '',
limit = 100,
offset = 0,
} = {}) {
const params = new URLSearchParams({
limit: String(limit),
offset: String(offset),
});
if (cookieId) params.set('cookie_id', cookieId);
if (processingStatus) params.set('processing_status', processingStatus);
if (eventType) params.set('event_type', eventType);
if (triggerScene) params.set('trigger_scene', triggerScene);
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
if (sessionId) params.set('session_id', sessionId);
if (resultCode) params.set('result_code', resultCode);
const response = await fetch(`/admin/risk-control-logs?${params.toString()}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}
function needsClientSideRiskLogFilter(logs, processingStatus) {
if (!processingStatus || !Array.isArray(logs) || logs.length === 0) {
return false;
}
return logs.some(log => String(log.processing_status || '') !== processingStatus);
}
async function fetchRiskControlLogsWithClientFilter(token, {
cookieId = '',
processingStatus = '',
eventType = '',
triggerScene = '',
dateFrom = '',
dateTo = '',
sessionId = '',
resultCode = '',
limit = 100,
offset = 0,
} = {}) {
const batchSize = 500;
let fetchOffset = 0;
let total = 0;
const matchedLogs = [];
while (true) {
const pageData = await fetchRiskControlLogsPage(token, {
cookieId,
eventType,
triggerScene,
dateFrom,
dateTo,
sessionId,
resultCode,
limit: batchSize,
offset: fetchOffset
});
const pageLogs = Array.isArray(pageData.data) ? pageData.data : [];
total = pageData.total || total || pageLogs.length;
matchedLogs.push(...pageLogs.filter(log => String(log.processing_status || '') === processingStatus));
fetchOffset += pageLogs.length;
if (pageLogs.length === 0 || fetchOffset >= total) {
break;
}
}
return {
success: true,
data: matchedLogs.slice(offset, offset + limit),
total: matchedLogs.length,
limit,
offset,
filter_mode: 'client'
};
}
// 加载风控日志
async function loadRiskControlLogs(offset = 0) {
const token = localStorage.getItem('auth_token');
const filters = getRiskLogFilters();
const cookieId = filters.cookieId;
const limit = filters.limit;
currentRiskLogOffset = offset;
loadRiskControlSliderStats(cookieId);
const loadingDiv = document.getElementById('loadingRiskLogs');
const logContainer = document.getElementById('riskLogContainer');
const noLogsDiv = document.getElementById('noRiskLogs');
loadingDiv.style.display = 'block';
logContainer.style.display = 'none';
noLogsDiv.style.display = 'none';
try {
let data = await fetchRiskControlLogsPage(token, {
...filters,
offset,
});
if (needsClientSideRiskLogFilter(data.data, filters.processingStatus)) {
data = await fetchRiskControlLogsWithClientFilter(token, {
...filters,
offset,
});
}
loadingDiv.style.display = 'none';
if (data.success && data.data && data.data.length > 0) {
displayRiskControlLogs(data.data);
updateRiskLogInfo(data);
updateRiskLogPagination(data);
logContainer.style.display = 'block';
} else {
noLogsDiv.style.display = 'block';
updateRiskLogInfo({total: 0, data: []});
updateRiskLogPagination({total: 0});
}
} catch (error) {
console.error('加载风控日志失败:', error);
loadingDiv.style.display = 'none';
noLogsDiv.style.display = 'block';
updateRiskLogPagination({total: 0});
const countElement = document.getElementById('riskLogCount');
const paginationInfo = document.getElementById('riskLogPaginationInfo');
if (countElement) {
countElement.textContent = '加载失败';
}
if (paginationInfo) {
paginationInfo.textContent = '风控日志加载失败,请重试';
}
showToast('加载风控日志失败', 'danger');
}
}
// 显示风控日志
function getRiskEventCategoryMeta(eventType) {
const normalizedType = String(eventType || '').trim();
if (normalizedType === 'unknown') {
return {
label: '身份验证',
className: 'risk-event-category-trigger'
};
}
if (['slider_captcha', 'face_verify', 'sms_verify', 'qr_verify', 'token_expired'].includes(normalizedType)) {
return {
label: '风控触发',
className: 'risk-event-category-trigger'
};
}
if (normalizedType === 'cookie_refresh') {
return {
label: 'Cookie刷新',
className: 'risk-event-category-refresh'
};
}
if (normalizedType === 'password_error') {
return {
label: '登录异常',
className: 'risk-event-category-error'
};
}
return {
label: normalizedType || '-',
className: 'risk-event-category-neutral'
};
}
function getRiskTriggerSceneLabel(triggerScene) {
const normalizedScene = String(triggerScene || '').trim();
const sceneLabels = {
token_refresh: 'Token刷新',
auto_cookie_refresh: '自动Cookie刷新',
manual_password_refresh: '手动账密刷新',
manual_qr_refresh: '手动扫码刷新',
password_login: '密码登录',
qr_login: '扫码登录'
};
return sceneLabels[normalizedScene] || normalizedScene || '-';
}
function formatRiskDuration(durationMs) {
const value = Number(durationMs);
if (!Number.isFinite(value) || value <= 0) {
return '--';
}
if (value < 1000) {
return `${Math.round(value)} ms`;
}
if (value < 60000) {
return `${(value / 1000).toFixed(1)} s`;
}
return `${(value / 60000).toFixed(1)} min`;
}
function formatRiskSessionId(sessionId, sessionDisplay = '') {
const text = String(sessionId || '').trim();
if (text) {
return text;
}
const fallback = String(sessionDisplay || '').trim();
return fallback || '--';
}
function renderRiskLogSummaryCell(log) {
const descriptionText = log.event_description_display || log.event_description || '-';
const description = escapeHtml(descriptionText);
const resultCode = log.result_code
? `结果代码: ${escapeHtml(log.result_code)}
`
: '';
return `
${description}
${resultCode}
`;
}
function renderRiskLogOutcomeCell(log) {
const processingResultText = log.processing_result_display || log.processing_result || '';
const errorMessageText = log.error_message_display || log.error_message || '';
const processingResult = processingResultText
? `${escapeHtml(processingResultText)}
`
: '';
const errorMessage = errorMessageText
? `${escapeHtml(errorMessageText)}
`
: '';
const fallbackText = !processingResult && !errorMessage
? '-'
: '';
return `
${processingResult}
${errorMessage}
${fallbackText}
`;
}
function displayRiskControlLogs(logs) {
const tableBody = document.getElementById('riskLogTableBody');
tableBody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
// 格式化时间
const createdAt = formatDateTime(log.created_at);
// 状态标签
let statusBadge = '';
switch(log.processing_status) {
case 'processing':
statusBadge = '处理中';
break;
case 'success':
statusBadge = '成功';
break;
case 'failed':
statusBadge = '失败';
break;
default:
statusBadge = '未知';
}
const eventCategory = getRiskEventCategoryMeta(log.event_type);
const eventCategoryBadge = `
${escapeHtml(eventCategory.label)}
`;
const triggerSceneLabel = getRiskTriggerSceneLabel(log.trigger_scene);
const triggerSceneBadge = `
${escapeHtml(triggerSceneLabel)}
`;
const sessionIdDisplay = formatRiskSessionId(log.session_id, log.session_display);
const sessionTitle = escapeHtml(log.session_id || log.session_display || '-');
const durationText = formatRiskDuration(log.duration_ms);
row.innerHTML = `
${createdAt} |
${escapeHtml(log.cookie_id || '-')} |
${eventCategoryBadge} |
${triggerSceneBadge} |
${statusBadge} |
${renderRiskLogSummaryCell(log)} |
${renderRiskLogOutcomeCell(log)} |
${escapeHtml(durationText)} |
${escapeHtml(sessionIdDisplay)} |
|
`;
tableBody.appendChild(row);
});
}
// 更新风控日志信息
function updateRiskLogInfo(data) {
const countElement = document.getElementById('riskLogCount');
const paginationInfo = document.getElementById('riskLogPaginationInfo');
const hasFilters = hasActiveRiskLogFilters(getRiskLogFilters());
const total = data.total || 0;
const currentCount = data.data ? data.data.length : 0;
if (countElement) {
countElement.textContent = hasFilters ? `筛选结果: ${total} 条` : `总计: ${total} 条`;
}
if (paginationInfo) {
if (currentCount === 0 || total === 0) {
paginationInfo.textContent = hasFilters ? `显示第 0-0 条,匹配 0 条记录` : '显示第 0-0 条,共 0 条记录';
return;
}
const start = currentRiskLogOffset + 1;
const end = Math.min(currentRiskLogOffset + currentCount, total);
paginationInfo.textContent = hasFilters
? `显示第 ${start}-${end} 条,匹配 ${total} 条记录`
: `显示第 ${start}-${end} 条,共 ${total} 条记录`;
}
}
// 更新风控日志分页
function updateRiskLogPagination(data) {
const pagination = document.getElementById('riskLogPagination');
const limit = parseInt(document.getElementById('riskLogLimit').value);
const total = data.total || 0;
const totalPages = Math.ceil(total / limit);
const currentPage = Math.floor(currentRiskLogOffset / limit) + 1;
pagination.innerHTML = '';
if (totalPages <= 1) return;
// 上一页
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `上一页`;
pagination.appendChild(prevLi);
// 页码
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `${i}`;
pagination.appendChild(li);
}
// 下一页
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `下一页`;
pagination.appendChild(nextLi);
}
// 按状态过滤风控日志
function filterRiskLogsByStatus(status) {
currentRiskLogStatus = status;
// 更新过滤按钮状态
document.querySelectorAll('.filter-badge[data-status]').forEach(badge => {
badge.classList.remove('active');
});
const activeBadge = document.querySelector(`.filter-badge[data-status="${status}"]`);
if (activeBadge) {
activeBadge.classList.add('active');
}
// 重新加载日志
loadRiskControlLogs(0);
}
// 加载账号筛选选项
async function loadCookieFilterOptions() {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch('/admin/cookies', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const select = document.getElementById('riskLogCookieFilter');
// 清空现有选项,保留"全部账号"
select.innerHTML = '';
if (data.success && data.cookies) {
data.cookies.forEach(cookie => {
const option = document.createElement('option');
option.value = cookie.cookie_id;
// 优先显示备注,其次显示用户名,都没有则不显示括号
const displayName = cookie.nickname || cookie.username || '';
option.textContent = displayName ? `${cookie.cookie_id} (${displayName})` : cookie.cookie_id;
select.appendChild(option);
});
}
}
} catch (error) {
console.error('加载账号选项失败:', error);
}
}
// 删除风控日志记录
async function deleteRiskControlLog(logId) {
if (!confirm('确定要删除这条风控日志记录吗?')) {
return;
}
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/risk-control-logs/${logId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
loadRiskControlLogs(currentRiskLogOffset);
} else {
showToast(data.message || '删除失败', 'danger');
}
} catch (error) {
console.error('删除风控日志失败:', error);
showToast('删除失败', 'danger');
}
}
// 清空风控日志
async function clearRiskControlLogs() {
if (!confirm('确定要清空所有风控日志吗?此操作不可恢复!')) {
return;
}
try {
const token = localStorage.getItem('auth_token');
// 调用后端批量清空接口(管理员)
const response = await fetch('/admin/data/risk_control_logs', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
showToast('风控日志已清空', 'success');
loadRiskControlLogs(0);
} else {
showToast(data.detail || data.message || '清空失败', 'danger');
}
} catch (error) {
console.error('清空风控日志失败:', error);
showToast('清空失败', 'danger');
}
}
// ================================
// 商品搜索功能
// ================================
let searchResultsData = [];
let currentSearchPage = 1;
let searchPageSize = 20;
let totalSearchPages = 0;
// 初始化商品搜索功能
function initItemSearch() {
const searchForm = document.getElementById('itemSearchForm');
if (searchForm) {
searchForm.addEventListener('submit', handleItemSearch);
}
}
// 处理商品搜索
async function handleItemSearch(event) {
event.preventDefault();
const keyword = document.getElementById('searchKeyword').value.trim();
const totalPages = parseInt(document.getElementById('searchTotalPages').value) || 1;
const pageSize = parseInt(document.getElementById('searchPageSize').value) || 20;
if (!keyword) {
showToast('请输入搜索关键词', 'warning');
return;
}
// 显示搜索状态
showSearchStatus(true);
hideSearchResults();
try {
// 检查是否有有效的cookies账户
const cookiesCheckResponse = await fetch('/cookies/check', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (cookiesCheckResponse.ok) {
const cookiesData = await cookiesCheckResponse.json();
if (!cookiesData.hasValidCookies) {
showToast('搜索失败:系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。', 'warning');
showSearchStatus(false);
return;
}
}
const token = localStorage.getItem('auth_token');
// 启动会话检查器(在搜索过程中检查是否有验证会话)
let sessionChecker = null;
let checkCount = 0;
const maxChecks = 30; // 最多检查30次(30秒)
let isSearchCompleted = false; // 标记搜索是否完成
sessionChecker = setInterval(async () => {
// 如果搜索已完成,停止检查
if (isSearchCompleted) {
if (sessionChecker) {
clearInterval(sessionChecker);
sessionChecker = null;
}
return;
}
try {
checkCount++;
const checkResponse = await fetch('/api/captcha/sessions');
const checkData = await checkResponse.json();
if (checkData.sessions && checkData.sessions.length > 0) {
for (const session of checkData.sessions) {
if (!session.completed) {
console.log(`🎨 检测到验证会话: ${session.session_id}`);
if (sessionChecker) {
clearInterval(sessionChecker);
sessionChecker = null;
}
// 确保监控已启动
if (typeof startCaptchaSessionMonitor === 'function') {
startCaptchaSessionMonitor();
}
// 弹出验证窗口
if (typeof showCaptchaVerificationModal === 'function') {
showCaptchaVerificationModal(session.session_id);
showToast('🎨 检测到滑块验证,请完成验证', 'warning');
// 停止搜索时的会话检查器,因为已经弹窗了,由弹窗的监控接管
if (sessionChecker) {
clearInterval(sessionChecker);
sessionChecker = null;
console.log('✅ 已弹窗,停止搜索时的会话检查器');
}
} else {
// 如果函数未定义,使用备用方案
console.error('showCaptchaVerificationModal 未定义,使用备用方案');
window.location.href = `/api/captcha/control/${session.session_id}`;
}
return;
}
}
}
// 如果检查次数超过限制,停止检查
if (checkCount >= maxChecks) {
if (sessionChecker) {
clearInterval(sessionChecker);
sessionChecker = null;
}
}
} catch (error) {
console.error('检查验证会话失败:', error);
}
}, 1000); // 每秒检查一次
// 使用 Promise 包装,以便使用 finally
const fetchPromise = fetch('/items/search_multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
keyword: keyword,
total_pages: totalPages
})
});
// 请求完成后,停止会话检查器
fetchPromise.finally(() => {
isSearchCompleted = true;
if (sessionChecker) {
clearInterval(sessionChecker);
sessionChecker = null;
console.log('✅ 搜索完成,已停止会话检查器');
}
});
const response = await fetchPromise;
console.log('API响应状态:', response.status);
if (response.ok) {
const data = await response.json();
console.log('API返回的完整数据:', data);
// 检查是否需要滑块验证
if (data.need_captcha || data.status === 'need_verification') {
console.log('检测到需要滑块验证');
showSearchStatus(false);
// 显示滑块验证模态框
const sessionId = data.session_id || 'default';
const modal = showCaptchaVerificationModal(sessionId);
try {
// 等待用户完成验证
await checkCaptchaCompletion(modal, sessionId);
// 验证成功,显示搜索状态并重新发起搜索请求
showSearchStatus(true);
document.getElementById('searchProgress').textContent = '验证成功,继续搜索商品...';
// 重新发起搜索请求
const retryResponse = await fetch('/items/search_multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
keyword: keyword,
total_pages: totalPages
})
});
if (retryResponse.ok) {
const retryData = await retryResponse.json();
// 再次检查是否需要验证(理论上不应该再需要)
if (retryData.need_captcha || retryData.status === 'need_verification') {
showSearchStatus(false);
showToast('验证后仍需要滑块,请联系管理员', 'danger');
return;
}
// 处理搜索结果
searchResultsData = retryData.data || [];
console.log('验证后搜索结果:', searchResultsData);
console.log('searchResultsData长度:', searchResultsData.length);
searchPageSize = pageSize;
currentSearchPage = 1;
totalSearchPages = Math.ceil(searchResultsData.length / searchPageSize);
if (retryData.error) {
showToast(`搜索完成,但遇到问题: ${retryData.error}`, 'warning');
}
showSearchStatus(false);
displaySearchResults();
updateSearchStats(retryData);
} else {
const retryError = await retryResponse.json();
showSearchStatus(false);
showToast(`验证后搜索失败: ${retryError.detail || '未知错误'}`, 'danger');
showNoSearchResults();
}
} catch (error) {
console.error('滑块验证失败:', error);
showSearchStatus(false);
showToast('滑块验证失败或超时', 'danger');
showNoSearchResults();
}
return;
}
// 正常搜索结果(无需验证)
// 修复字段名:使用data.data而不是data.items
searchResultsData = data.data || [];
console.log('设置searchResultsData:', searchResultsData);
console.log('searchResultsData长度:', searchResultsData.length);
console.log('完整响应数据:', data);
searchPageSize = pageSize;
currentSearchPage = 1;
totalSearchPages = Math.ceil(searchResultsData.length / searchPageSize);
if (data.error) {
showToast(`搜索完成,但遇到问题: ${data.error}`, 'warning');
}
showSearchStatus(false);
// 确保显示搜索结果
if (searchResultsData.length > 0) {
displaySearchResults();
updateSearchStats(data);
} else {
console.warn('搜索结果为空,显示无结果提示');
showNoSearchResults();
}
} else {
const errorData = await response.json();
showSearchStatus(false);
showToast(`搜索失败: ${errorData.detail || '未知错误'}`, 'danger');
showNoSearchResults();
}
} catch (error) {
console.error('搜索商品失败:', error);
showSearchStatus(false);
showToast('搜索商品失败', 'danger');
showNoSearchResults();
}
}
// 显示搜索状态
function showSearchStatus(isSearching) {
const statusDiv = document.getElementById('searchStatus');
const progressDiv = document.getElementById('searchProgress');
if (isSearching) {
statusDiv.style.display = 'block';
progressDiv.textContent = '正在搜索商品数据...';
} else {
statusDiv.style.display = 'none';
}
}
// 隐藏搜索结果
function hideSearchResults() {
document.getElementById('searchResults').style.display = 'none';
document.getElementById('searchResultStats').style.display = 'none';
document.getElementById('noSearchResults').style.display = 'none';
}
// 显示搜索结果
function displaySearchResults() {
if (searchResultsData.length === 0) {
showNoSearchResults();
return;
}
const startIndex = (currentSearchPage - 1) * searchPageSize;
const endIndex = startIndex + searchPageSize;
const pageItems = searchResultsData.slice(startIndex, endIndex);
const container = document.getElementById('searchResultsContainer');
container.innerHTML = '';
pageItems.forEach(item => {
const itemCard = createItemCard(item);
container.appendChild(itemCard);
});
updateSearchPagination();
document.getElementById('searchResults').style.display = 'block';
}
// 创建商品卡片
function createItemCard(item) {
console.log('createItemCard被调用,item数据:', item);
console.log('item的所有字段:', Object.keys(item));
const col = document.createElement('div');
col.className = 'col-md-6 col-lg-4 col-xl-3 mb-4';
// 修复字段映射:使用main_image而不是image_url
const imageUrl = item.main_image || item.image_url || 'https://via.placeholder.com/200x200?text=图片加载失败';
const wantCount = item.want_count || 0;
console.log('处理后的数据:', {
title: item.title,
price: item.price,
seller_name: item.seller_name,
imageUrl: imageUrl,
wantCount: wantCount,
url: item.item_url || item.url
});
col.innerHTML = `
${escapeHtml(item.title.length > 50 ? item.title.substring(0, 50) + '...' : item.title)}
${escapeHtml(item.price)}
${escapeHtml(item.seller_name)}
${wantCount > 0 ? `
${wantCount}人想要
` : ''}
`;
return col;
}
// 更新搜索统计
function updateSearchStats(data) {
document.getElementById('totalItemsFound').textContent = searchResultsData.length;
document.getElementById('totalPagesSearched').textContent = data.total_pages || 0;
document.getElementById('currentDisplayPage').textContent = currentSearchPage;
document.getElementById('totalDisplayPages').textContent = totalSearchPages;
document.getElementById('searchResultStats').style.display = 'block';
}
// 更新搜索分页
function updateSearchPagination() {
const paginationContainer = document.getElementById('searchPagination');
paginationContainer.innerHTML = '';
if (totalSearchPages <= 1) return;
const pagination = document.createElement('nav');
pagination.innerHTML = `
`;
paginationContainer.appendChild(pagination);
}
// 生成搜索分页页码
function generateSearchPageNumbers() {
let pageNumbers = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, currentSearchPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalSearchPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pageNumbers += `
${i}
`;
}
return pageNumbers;
}
// 切换搜索页面
function changeSearchPage(page) {
if (page < 1 || page > totalSearchPages || page === currentSearchPage) return;
currentSearchPage = page;
displaySearchResults();
updateSearchStats({ total_pages: document.getElementById('totalPagesSearched').textContent });
}
// 显示无搜索结果
function showNoSearchResults() {
document.getElementById('noSearchResults').style.display = 'block';
document.getElementById('searchResults').style.display = 'none';
document.getElementById('searchResultStats').style.display = 'none';
}
// 导出搜索结果
function exportSearchResults() {
if (searchResultsData.length === 0) {
showToast('没有可导出的搜索结果', 'warning');
return;
}
try {
// 准备导出数据
const exportData = searchResultsData.map(item => ({
'商品标题': item.title,
'价格': item.price,
'卖家': item.seller_name,
'想要人数': item.want_count || 0,
'商品链接': item.url,
'图片链接': item.image_url
}));
// 转换为CSV格式
const headers = Object.keys(exportData[0]);
const csvContent = [
headers.join(','),
...exportData.map(row => headers.map(header => `"${row[header] || ''}"`).join(','))
].join('\n');
// 创建下载链接
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `商品搜索结果_${new Date().toISOString().slice(0, 10)}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('搜索结果导出成功', 'success');
} catch (error) {
console.error('导出搜索结果失败:', error);
showToast('导出搜索结果失败', 'danger');
}
}
// ================================
// 版本管理功能
// ================================
// 默认版本号(当无法读取 version.txt 时使用)
const DEFAULT_VERSION = 'v1.9.2';
// 当前本地版本号(动态从 version.txt 读取)
let LOCAL_VERSION = DEFAULT_VERSION;
// 缓存远程版本信息
let remoteVersionInfo = null;
const HOT_UPDATE_STORAGE_KEYS = {
autoCheckDisabled: 'hot_update_auto_check_disabled',
ignoredVersion: 'hot_update_ignored_version'
};
function isHotUpdateAutoCheckEnabled() {
return localStorage.getItem(HOT_UPDATE_STORAGE_KEYS.autoCheckDisabled) !== 'true';
}
function setHotUpdateAutoCheckEnabled(enabled) {
localStorage.setItem(HOT_UPDATE_STORAGE_KEYS.autoCheckDisabled, enabled ? 'false' : 'true');
}
function getIgnoredHotUpdateVersion() {
return localStorage.getItem(HOT_UPDATE_STORAGE_KEYS.ignoredVersion) || '';
}
function setIgnoredHotUpdateVersion(version) {
if (version) {
localStorage.setItem(HOT_UPDATE_STORAGE_KEYS.ignoredVersion, version);
}
}
function getHotUpdateTargetVersion(updateInfo = remoteVersionInfo) {
return updateInfo?.new_version || (updateInfo?.has_update ? updateInfo?.version : '') || '';
}
function shouldSuppressHotUpdateHint(updateInfo = remoteVersionInfo) {
const targetVersion = getHotUpdateTargetVersion(updateInfo);
return !isHotUpdateAutoCheckEnabled() || (!!targetVersion && getIgnoredHotUpdateVersion() === targetVersion);
}
function refreshHotUpdateButtonState(updateInfo = remoteVersionInfo) {
const dashboardHotUpdateGroup = document.getElementById('dashboardHotUpdateGroup');
const dashboardHotUpdateBtn = document.getElementById('dashboardHotUpdateBtn');
const dashboardHotUpdateMenuBtn = document.getElementById('dashboardHotUpdateMenuBtn');
if (!dashboardHotUpdateGroup || !dashboardHotUpdateBtn || !dashboardHotUpdateMenuBtn) return;
dashboardHotUpdateBtn.disabled = false;
dashboardHotUpdateBtn.innerHTML = '检查更新';
dashboardHotUpdateMenuBtn.disabled = false;
dashboardHotUpdateGroup.classList.remove('has-update', 'is-loading');
const hasUpdate = Boolean(updateInfo && (updateInfo.has_update || updateInfo.new_version));
if (!hasUpdate || shouldSuppressHotUpdateHint(updateInfo)) {
return;
}
dashboardHotUpdateGroup.classList.add('has-update');
dashboardHotUpdateBtn.innerHTML = `有新版本 ${getHotUpdateTargetVersion(updateInfo)}`;
}
function updateHotUpdatePreferenceStatus(message = '', type = 'info') {
if (message) {
showToast(message, type === 'success' ? 'success' : 'info');
}
}
function refreshHotUpdatePreferencesMenu() {
const autoCheckToggle = document.getElementById('dashboardHotUpdateAutoCheckToggle');
const ignoredVersionHint = document.getElementById('dashboardHotUpdatePreferenceHint');
const clearIgnoredBtn = document.getElementById('dashboardClearIgnoredVersionBtn');
const ignoredVersion = getIgnoredHotUpdateVersion();
if (autoCheckToggle) {
autoCheckToggle.textContent = isHotUpdateAutoCheckEnabled() ? '关闭自动检查' : '开启自动检查';
}
if (ignoredVersionHint) {
const autoCheckText = isHotUpdateAutoCheckEnabled() ? '自动检查:已开启' : '自动检查:已关闭';
ignoredVersionHint.textContent = ignoredVersion
? `${autoCheckText} · 已忽略 ${ignoredVersion}`
: `${autoCheckText} · 当前未忽略任何版本`;
}
if (clearIgnoredBtn) {
clearIgnoredBtn.disabled = !ignoredVersion;
}
}
function toggleHotUpdateAutoCheck() {
const nextEnabled = !isHotUpdateAutoCheckEnabled();
setHotUpdateAutoCheckEnabled(nextEnabled);
refreshHotUpdatePreferencesMenu();
refreshHotUpdateButtonState();
updateHotUpdatePreferenceStatus(
nextEnabled
? '自动检查更新已开启,当前浏览器进入系统时会自动检测'
: '自动检查更新已关闭,仍可手动点击“检查更新”',
'success'
);
}
function clearIgnoredUpdateVersion(showFeedback = true) {
localStorage.removeItem(HOT_UPDATE_STORAGE_KEYS.ignoredVersion);
refreshHotUpdatePreferencesMenu();
refreshHotUpdateButtonState();
if (showFeedback) {
updateHotUpdatePreferenceStatus('已清除忽略版本设置', 'success');
}
}
// 本地版本历史(远程服务禁用时使用)
const LOCAL_VERSION_HISTORY = {
version: 'v1.9.2',
intro: '本系统仅供个人学习研究使用,请勿用于商业用途。如有问题或建议,欢迎反馈。',
versionHistory: [
{
version: 'v1.9.2',
date: '2026-04-10',
updates: [
'【修复】运行态总览统一按 WS / Session / Token / 业务流 四条主链路统计,避免出现 1 / 5 与 0 / 4 混用',
'【修复】运行态优先读取账号真实活跃实例,临时 XianyuLive 实例不再注册到全局实例表,减少业务消息流误判未就绪',
'【优化】账号详情运行态总览调整为左侧四个状态卡、右侧链路就绪摘要卡,桌面端信息分区更清晰',
'【优化】业务消息流补充连接未就绪与恢复中的兜底展示,运行态短时异常时前端自动重试刷新更平滑'
]
},
{
version: 'v1.9.1',
date: '2026-04-10',
updates: [
'【新功能】新增业务消息流看门狗,区分心跳包与真实业务包,长时间只有心跳时会主动关闭旧 WebSocket 并触发重连',
'【新功能】账号运行态新增消息流诊断字段,补充最近非心跳业务包、同步包、真实买家消息与假在线重连时间,便于识别“连接已通但消息停滞”',
'【优化】仪表盘账号卡片和账号详情页新增“消息流”状态,并将链路就绪判断扩展到业务消息流',
'【优化】前端对连接中、重连中和短时异常状态增加自动重试刷新,减少运行态展示滞后'
]
},
{
version: 'v1.9.0',
date: '2026-04-08',
updates: [
'【新功能】升级账号保活链路与账号诊断能力,账号页按实际链路展示 WS / Session / Token / 轻保活 等运行状态,仪表盘账号卡片新增运行态摘要',
'【新功能】重写历史订单同步并切换卖家工作台接口,补齐订单平台时间字段链路,支持在前端查看同步入口、状态面板与任务进度',
'【修复】收紧历史订单同步时间范围与数量限制,降低大范围同步导致的异常与超时风险',
'【修复】收紧登录表单识别,找不到账号框/密码框时先复检已登录态和验证页;验证类型不明时不再默认按人脸处理,前后端展示与通知统一按实际验证类型显示',
'【修复】修复账号重新启用后资料被清空的问题,避免恢复启用时覆盖已有配置',
'【优化】调整自动回复优先级顺序,减少多规则命中时的回复偏差',
'【新功能】接入 GitHub 公告栏,仪表盘支持展示公告横幅并可点击查看历史公告记录'
]
},
{
version: 'v1.8.4',
date: '2026-04-05',
updates: [
'【修复】修复订单详情规格解析失败导致自动发货被阻断的问题(by @82762294)',
'【优化】滑块验证统计新增当日 / 7天 / 所有范围筛选,统计卡片文案与交互更清晰',
'【优化】风控日志“处理结果”展示简化,移除前端元数据展开信息,排查更直观',
'【优化】账号管理入口与说明文案更新,明确扫码登录、账密登录、手动刷新 Cookie 与导入 Cookie 的使用场景'
]
},
{
version: 'v1.8.3',
date: '2026-04-05',
updates: [
'【修复】修复有头模式白屏:完整反检测脚本会覆盖浏览器核心API导致页面无法渲染,有头模式改用轻量反检测脚本',
'【修复】修复自动刷新Session过期导致滑块连败:自动Token刷新改用干净上下文,避免持久化上下文中过期Session数据导致风控升级',
'【修复】修复刷新模式登录状态假象:新增服务端Session有效性验证,过期时自动清除Cookie并重新登录',
'【修复】修复持久化上下文页面异常:无登录iframe且无已登录态时自动清除Cookie和缓存并重新加载',
'【优化】Token预检新增最多3次渐进重试,应对密码登录Cookie在服务端生效延迟',
'【优化】滑块策略权重调整,降低低成功率conservative策略权重,提高standard策略权重',
'【优化】滑块第3次及以后重试优先使用学习参数加大抖动变体,增加重试间隔降低反爬触发风险',
'【优化】密码登录复用完整浏览器画像配置,与captcha验证流程保持一致,自动刷新路径同步启用策略学习'
]
},
{
version: 'v1.8.2',
date: '2026-04-04',
updates: [
'【修复】修复 Token 刷新循环因 last_token_refresh_status 属性未初始化导致崩溃的问题',
'【修复】修复手动刷新认证预检因 asyncio 局部变量遮蔽导致 UnboundLocalError 的问题'
]
},
{
version: 'v1.8.1',
date: '2026-04-03',
updates: [
'【修复】滑块恢复与令牌刷新链路更稳定,滑块成功后会及时回写有效会话 Cookie,并保护关键会话字段不被不完整快照覆盖',
'【修复】手动刷新后的任务交接与初始化鉴权恢复,新增 Token 预检、交接恢复窗口、恢复锁和鉴权失败冷静期,减少 WebSocket 已连通但因 Token 获取失败反复重试',
'【修复】统一通知派发路径并收口验证通知,修正推送冷却、人脸验证通知类型/文案/模板渲染,以及定时刷新误报',
'【优化】账密登录与手动刷新流程里的滑块验证也会写入风控日志和滑块统计,风控排查口径更完整',
'【修复】取消订单后的系统卡片不再覆盖真实 buyer_id,避免订单买家信息被异常值污染',
'【优化】多数量纯文本卡券消息支持批量合并发送,减少重复刷屏,卡券发货提示更简洁'
]
},
{
version: 'v1.8.0',
date: '2026-04-01',
updates: [
'【新功能】风控日志升级为结构化会话链路,覆盖滑块验证、Token 过期、账密登录和扫码刷新,支持统一追踪结果、场景与脱敏元数据',
'【优化】风控看板重构为结构化会话统计,新增更准确的滑块成功/失败会话统计、筛选能力与响应式详情展示,排查风控更直观',
'【优化】滑块验证反检测全面增强,加入稳定指纹配置、拟人轨迹、Cookie 预热与多轮重试策略,提升验证通过率与稳定性',
'【修复】手动刷新、扫码登录与密码登录流程增加互斥保护、失败退避和状态收口,减少刷新互踩、扫码回滚误判与登录风控残留',
'【修复】扫码/密码登录链路补强浏览器侧 Cookie 稳定化、前置登录态校验和人脸/滑块兜底判断,登录成功判定更可靠',
'【优化】图片上传新增错误类型追踪,调用方可按错误原因给出更准确的提示与处理',
'【修复】商品管理区分“同步商品”和“刷新列表”,同步指定页/所有页时会强制拉取已有商品的最新详情,避免本地缓存长期陈旧'
]
},
{
version: 'v1.7.5',
date: '2026-03-24',
updates: [
'【修复】修复扫码登录遇到人脸验证时直接返回外部链接导致验证会话丢失的问题,改为在服务端保持原始会话并生成验证二维码',
'【修复】修复扫码成功后仍可能再次进入滑块验证的问题,新增真实 Cookie 合并与首次 Token 预热保护',
'【优化】优化扫码风控状态收口,增加浏览器侧兜底判定,验证完成后可更稳定进入登录成功',
'【优化】优化扫码登录前端提示,减少重复提示并统一验证过程中的状态反馈'
]
},
{
version: 'v1.7.4',
date: '2026-03-22',
updates: [
'【修复】收紧订单号提取规则,避免普通消息中的 messageId 被误识别为订单号并生成处理中假订单',
'【修复】统一销售统计口径并跳过空金额/脏金额订单,修复销售额卡片获取失败的问题',
'【优化】重构仪表盘账号概览、订单数据看板、销售趋势与发货日志展示,关键信息更清晰易读',
'【优化】发货日志拆分规则、匹配结果、触发方式和规格状态列,并简化规格状态显示便于快速排查'
]
},
{
version: 'v1.7.3',
date: '2026-03-21',
updates: [
'【修复】热更新清单改为优先读取上一版 Release 资产中的 update_files.json,避免 deleted_files 丢失',
'【修复】修正同版本下热更新可能回滚清单生成脚本的问题,补齐删除清单并完善后续版本生成逻辑'
]
},
{
version: 'v1.7.2',
date: '2026-03-20',
updates: [
'【新功能】账号列表新增商品一键擦亮入口,可批量执行当前在售商品擦亮',
'【新功能】新增每日定时擦亮任务,支持按账号配置启用状态、执行时段与随机延迟',
'【优化】后台新增定时任务调度与执行结果记录,便于查看下次执行时间和最近运行情况',
'【优化】管理端补充擦亮相关操作入口与设置弹窗,日常运营更方便',
'【优化】账号管理页调整列表列宽与仪表盘赞助按钮样式,提升界面可读性与交互一致性'
]
},
{
version: 'v1.7.1',
date: '2026-03-19',
updates: [
'【修复】订单规格识别改为优先读取结构化订单响应中的 skuInfo 和数量,减少页面元素缺失导致的漏识别',
'【修复】正文兜底不再把标题、描述等冒号文案误判为第二规格,避免单规格订单被错误识别为双规格',
'【优化】正文规格过滤只保留更像真实 SKU 字段的候选,进一步过滤时间、广告文案和无关文本',
'【修复】订单明确解析为单规格时自动清空历史残留的第二规格字段,避免旧脏数据继续影响发货匹配',
'【优化】多规格商品复用缓存时要求金额、状态和主规格同时有效,降低脏缓存复用风险'
]
},
{
version: 'v1.7.0',
date: '2026-03-19',
updates: [
'【修复】系统消息状态优先级与消息分流逻辑重构,阻止订单状态回退并减少系统通知噪音',
'【修复】扩展订单消息的订单号提取来源,增强 sid 兜底查单与近邻回退,减少简化消息和终态待处理消息漏单',
'【优化】订单详情优先采用结构化状态信号,补刷增加状态门控与冷却,降低误判和高频补刷',
'【修复】无规格商品规则匹配与 sid 兜底发货链路收紧,降低串单和误发风险',
'【修复】小刀订单新增成功证据持久化,在缺少完整待发货卡片时也能继续自动发货兜底',
'【修复】闲鱼币抵扣订单金额识别,优先保留或推导真实实付金额,避免把原价误判为成交价',
'【优化】发货日志新增“已跳过”状态,并过滤成功后的重复 skipped 记录,日志展示更清晰'
]
},
{
version: 'v1.6.1',
date: '2026-03-13',
updates: [
'【修复】简化消息buyer_id误提取:区分sid与PNM格式消息,不再将会话ID误提取为buyer_id,避免防串单校验误判',
'【修复】无效buyer_id污染订单数据:新增buyer_id可信度校验,识别unknown_user等无效占位符,防串单时自动跳过',
'【修复】数据库buyer_id被占位符覆盖:更新订单时跳过无效buyer_id覆盖已有有效值,新增订单时自动净化为空值',
'【新功能】PNM格式消息解析:从message[4]提取buyer_id、买家昵称和商品ID,提升订单信息完整度',
'【修复】订单详情锁事件循环不匹配:Web API调用时自动重建锁,避免跨事件循环死锁',
'【优化】统一buyer_id/user_id默认值:各处unknown_user占位符改为None,避免脏数据进入订单与发货链路'
]
},
{
version: 'v1.6.0',
date: '2026-03-12',
updates: [
'【修复】通知渠道邮件表单使用独立字段标识,避免与系统 SMTP 配置冲突后保存时误提示“请填写SMTP服务器”',
'【优化】通知渠道邮件标题与正文抬头统一为“闲鱼管理系统通知”,并精简验证码邮件文案',
'【优化】系统品牌文案统一调整为“闲鱼管理系统”,同步更新登录页、注册页、浏览器标题、API 文档与统计服务说明',
'【修复】风控日志记录数徽标文字垂直居中,处理状态筛选恢复可用,筛选结果与条数统计保持一致',
'【优化】风控验证通知文案调整为“自动回复功能暂时无法使用”,避免系统名与功能名混淆'
]
},
{
version: 'v1.5.9',
date: '2026-03-11',
updates: [
'【修复】买家昵称过滤系统文案,避免订单和发货日志写入错误昵称',
'【修复】小刀订单商品归属增加回退校验,避免缓存未命中时误跳过自动发货',
'【修复】连续下单场景下旧关单消息串到新订单的问题',
'【修复】sid 简化消息将已处理订单误报为未找到订单的问题',
'【优化】前端时间显示与销售统计统一按北京时间口径处理'
]
},
{
version: 'v1.5.8',
date: '2026-03-11',
updates: [
'【新功能】热更新弹窗新增“本次跳过”和“忽略此版本”,支持按版本跳过当前更新提示',
'【优化】仪表盘检查更新入口改为按钮组,新增自动检查开关和忽略版本管理,设置仅当前浏览器生效'
]
},
{
version: 'v1.5.7',
date: '2026-03-11',
updates: [
'【修复】单规格订单自动发货改为优先精确匹配,精确规则未命中时支持降级到普通关键字规则',
'【优化】单规格降级兜底仅在唯一命中一条普通规则时放行,避免多规则误发',
'【优化】发货日志新增“单规兜底”标签,便于区分精确命中和普通规则兜底'
]
},
{
version: 'v1.5.6',
date: '2026-03-11',
updates: [
'【修复】补发 captcha_control.html 热更新资源,避免 v1.5.5 下该文件因 Release 资源未刷新而反复提示更新',
'【优化】热更新检查日志改为汇总输出,不再逐文件打印“文件已是最新”'
]
},
{
version: 'v1.5.5',
date: '2026-03-11',
updates: [
'【新功能】热更新清单改为自动扫描 Python、HTML、静态资源和前端源码文件,无需手动维护白名单',
'【新功能】新增发版预检查脚本,可在发布前检查版本号、改名/删除文件和未跟踪热更新文件',
'【新功能】热更新支持按清单删除旧文件,删除前会自动备份,降低改名和清理残留文件的风险',
'【优化】update_files.json 改为由 GitHub Actions 自动生成并上传到 Release,仓库内不再手动维护'
]
},
{
version: 'v1.5.4',
date: '2026-03-10',
updates: [
'【修复】补充 .gitattributes 行尾规则,统一 Python、JS、HTML、CSS 等热更新相关文本文件使用 LF',
'【修复】避免因 CRLF/LF 行尾差异导致同版本下仍被误判为可热更新文件'
]
},
{
version: 'v1.5.3',
date: '2026-03-10',
updates: [
'【修复】热更新执行权限改为按管理员身份校验,不再强依赖用户名必须为 admin',
'【修复】前端更新失败提示优先展示后端 detail 信息,避免只显示“未知错误”'
]
},
{
version: 'v1.5.2',
date: '2026-03-10',
updates: [
'【新功能】GitHub Actions 在创建 Release 前自动生成并上传 update_files.json,无需手动维护更新清单',
'【优化】热更新检测前会实时刷新本地版本号,本地版本变更后无需重启服务即可重新检查更新',
'【修复】热更新执行权限改为按管理员身份判断,不再强依赖用户名必须为 admin',
'【修复】前端更新失败提示补充后端 detail 信息,避免只显示“未知错误”'
]
},
{
version: 'v1.5.1',
date: '2026-03-10',
updates: [
'【新功能】接入 GitHub Releases 在线更新,支持从最新 Release 读取 update_files.json 检查热更新',
'【新功能】仪表盘版本区新增管理员可见的“检查更新”入口,可直接执行热更新',
'【优化】更新清单解析兼容 GitHub 资产返回 application/octet-stream 的场景,避免检查更新失败',
'【优化】版本区样式统一为 badge 视觉,并修复版本号与更新入口的垂直居中显示',
'【新功能】新增 GitHub Actions 自动发布工作流,push 到 main 且版本变化后可自动创建 tag 和 Release'
]
},
{
version: 'v1.5.0',
date: '2026-03-10',
updates: [
'【新功能】Cookie、密码等敏感字段使用 Fernet 加密存储,启动时自动迁移历史明文数据',
'【新功能】多数量发货收尾状态机,消息发送与卡密消费/确认发货分阶段提交,避免脏数据',
'【新功能】批量数据卡密预占机制,发货前预占、发送后确认,启动时自动恢复过期预占',
'【新功能】发货进度追踪表,支持多数量订单分单元进度查询与状态聚合',
'【新功能】订单事件中心 OrderEventHub,按用户广播订单更新,支持 SSE 实时流推送',
'【新功能】仪表盘新增销售额统计面板与趋势曲线图,支持当日销售额显示及自动刷新(by @Mangor2021)',
'【新功能】添加卡券时可自动生成对应的发货规则(by @Mangor2021)',
'【优化】新增 partial_success(部分发货)和 partial_pending_finalize(部分待收尾)中间状态',
'【优化】退款撤销回退增强,新增 pre_refund_status 字段持久化退款前状态,支持跨重启回退',
'【优化】新增外部状态合并保护,防止粗粒度状态覆盖内部精细发货进度',
'【优化】规格识别容错增强,过滤备案信息、时间戳、URL 等误识别字段',
'【优化】订单缓存复用条件扩展为金额+状态+规格综合判断,减少不必要的浏览器抓取',
'【优化】账号列表接口不再返回完整 Cookie 和密码原文,改为脱敏展示',
'【优化】销售额数据按用户账号隔离,修复多用户场景下数据串读',
'【优化】发货日志记录拼接规格模式上下文,便于排查',
'【优化】新增 message_hash + 强关联键精准消息匹配框架',
'【优化】发货成功后激活订单级延迟锁,防止短时间内重复发货',
'【优化】销售额曲线变化增加平滑过渡动画,时间按钮改为属性匹配(by @Mangor2021)',
'【修复】自动确认发货 Session 跨事件循环复用导致 timeout 错误,改为每次创建独立 Session',
'【修复】自动确认发货请求沿用主实例 HTTP 代理配置',
'【修复】前端 showToast 从 innerHTML 改为 DOM 构建,防止 XSS 注入',
'【修复】账号编辑与默认回复模态框 DOM ID 冲突导致数据串写',
'【修复】复制 Cookie 改为按需 API 获取,列表页不再暴露原文',
'【修复】前端订单状态筛选项与后端状态体系对齐',
'【修复】批量删除按钮默认 disabled,全选复选框 ID 修正',
'【修复】增加系统消息过滤关键字,修复商品信息变更后误触发自动回复(by @Mangor2021)'
]
},
{
version: 'v1.3.4',
date: '2026-03-03',
updates: [
'【优化】无规格商品自动发货改为单次详情尝试并强制按普通规则匹配,避免误识别规格干扰',
'【优化】规格商品在缺失规格时新增“唯一规则安全兜底”,仅唯一命中时放行,提升单规格场景成功率',
'【修复】规格匹配失败后的普通规则兜底查询补充 user_id 过滤,避免跨账号规则误命中',
'【修复】补充 pending_payment 内部状态映射,减少未映射状态告警'
]
},
{
version: 'v1.3.3',
date: '2026-03-03',
updates: [
'【优化】增强订单详情解析稳定性,新增刷新重试、文本兜底与金额多选择器提取,降低偶发规格/金额缺失',
'【优化】新增结构化解析日志 ORDER_DETAIL_PARSE_ALERT / ORDER_DETAIL_PARSE_RECOVERED,便于快速排查异常账号与订单',
'【优化】避免空值和 unknown 状态覆盖已有有效订单字段,减少后续发货链路受脏数据影响',
'【修复】SQL日志敏感参数统一脱敏(password/proxy_pass/smtp_password/admin_password_hash)',
'【修复】默认管理员初始化日志移除明文密码提示',
'【修复】订单金额前端显示优化,避免重复货币符号并统一空值显示'
]
},
{
version: 'v1.3.2',
date: '2026-03-02',
updates: [
'【新功能】仪表盘新增发货日志面板,与账号详情5:5并排展示,支持查看最近发货事件',
'【新功能】新增发货日志接口 /delivery-logs/recent,支持按用户读取最近发货日志',
'【优化】自动发货与手动发货统一记录真实发货事件,包含规则关键词、匹配模式(精确/兜底)、渠道(自动/手动)与失败原因',
'【优化】新增 delivery_logs 数据表与索引,提升发货日志可追溯性与查询效率',
'【修复】自动确认发货失败后改为直接阻断发货,避免异常订单继续下发卡密',
'【修复】简化消息路径取消重复确认,统一由 _auto_delivery 执行一次确认,降低漏发风险',
'【修复】小刀流程调整为两阶段:待刀成仅免拼,成功小刀待发货才自动发货',
'【修复】自动发货关键字仅允许系统消息触发,并加强 sid 兜底订单一致性校验'
]
},
{
version: 'v1.3.1',
date: '2026-03-02',
updates: [
'【新功能】AI回复配置新增API类型能力,支持OpenAI Chat/Responses、Gemini、Anthropic、Azure OpenAI、Ollama',
'【修复】修复DashScope兼容模式被误判为百炼应用导致报错“未找到app_id”的问题',
'【优化】AI配置预设支持api_type维度,保存/切换/自动匹配更准确',
'【优化】AI回复配置弹窗全量重构,按连接层/策略层/语义层/验证层分区并适配移动端与暗色模式',
'【优化】下线API类型中的“DashScope(百炼应用)”入口,历史值自动映射为自动识别',
'【优化】提示词三个输入框高度统一,提升编辑体验',
'【优化】浏览器标题统一为“闲鱼管理系统”'
]
},
{
version: 'v1.3.0',
date: '2026-03-01',
updates: [
'【新功能】回复延迟配置:账号列表卡片头部新增回复延迟设置控件,支持界面配置防抖延迟时间(1-10秒),修改后实时生效无需重启',
'【优化】系统消息过滤:优化系统消息过滤关键字,改为部分匹配方式,避免因符号、空格差异导致漏匹配(PR #4 by @Mangor2021)',
'【优化】系统消息过滤:进一步优化关键字精确度,避免误匹配买家正常消息(如"已发货"改为"你已发货")',
'【优化】账号列表:表格内容居中显示,调整列宽分配,提升整体布局美观度'
]
},
{
version: 'v1.2.9',
date: '2026-02-26',
updates: [
'【新功能】仪表盘新增订单数据看板,展示订单总数、销售总金额、订单完成率、当日订单数',
'【优化】仪表盘统计卡片将“总订单数”调整为“商品总数”,并同步更新图标与统计逻辑',
'【优化】统一订单完成率统计口径(分子:交易成功;分母:待发货+已发货+交易成功+交易关闭)',
'【优化】新增订单状态归一化兼容(success/finished、pending_ship/delivered/cancelled)并统一展示',
'【修复】将退款中状态文案明确为“申请退款中”,并修正手动发货按钮禁用条件'
]
},
{
version: 'v1.2.8',
date: '2026-02-26',
updates: [
'【修复】优化侧边栏切换逻辑,避免切换菜单时主内容区出现白屏闪烁',
'【修复】优化全局loading遮罩显示策略(延迟展示+并发计数),降低仪表盘和账号管理切换时的闪白感',
'【优化】暗色模式可读性增强:提升账号管理扫码按钮提示文案与仪表盘总账号图标的对比度',
'【修复】恢复loading出现时的鼠标悬停焦点表现,避免交互反馈丢失',
'【修复】修复页面刷新时短暂回退默认蓝色主题的问题,首屏优先应用缓存主题色'
]
},
{
version: 'v1.2.7',
date: '2026-02-21',
updates: [
'【新功能】AI配置预设:支持保存/切换/删除常用的API配置组合(模型、密钥、地址),一键切换不同AI服务',
'【修复】修复OpenAI兼容API的base_url缺少/v1后缀导致请求404的问题'
]
},
{
version: 'v1.2.6',
date: '2026-02-18',
updates: [
'【优化】AI回复配置:修复模型下拉框HTML标签错误,更新可用模型列表(新增deepseek-v3.2、kimi-k2.5等)',
'【优化】自定义提示词:从单JSON输入改为议价/技术/一般三个独立输入框,操作更直观',
'【优化】关键词输入:输入框改为多行文本域,支持竖线和换行分隔批量添加',
'【新功能】关键词回复内容支持就地编辑,无需重新添加即可修改回复文本',
'【优化】暗色模式全面适配:关键词管理、账号管理、扫码登录弹窗、全局滚动条',
'【修复】关键词输入区域布局错乱问题'
]
},
{
version: 'v1.2.5',
date: '2026-02-12',
updates: [
'【新功能】风控日志新增令牌过期、Cookie刷新等事件类型,支持7种状态显示',
'【优化】滑块验证异常和导入失败事件同步写入风控日志数据库'
]
},
{
version: 'v1.2.4',
date: '2026-02-08',
updates: [
'【新功能】优化验证类型检测,精确区分人脸/短信/二维码/账密错误',
'【新功能】新增 {verification_type} 模板变量',
'【新功能】风控日志支持多种事件类型',
'【修复】修复密码登录时 db_manager 变量作用域问题',
'【修复】移除通知中的【闲鱼通知】前缀'
]
},
{
version: 'v1.2.3',
date: '2026-02-08',
updates: [
'【新功能】新增通知模板自定义功能,支持7种通知类型',
'【新功能】暗色模式新增跟随系统选项',
'【修复】修复飞书通知签名验证失败的问题',
'【修复】修复通知内容重复显示账号ID和时间的问题'
]
},
{
version: 'v1.2.2',
date: '2026-01-29',
updates: [
'【修复】修复下单时买家昵称提取错误的问题',
'【修复】修复点击导航链接会刷新页面的问题',
'【修复】修复暗色模式刷新页面闪烁问题',
'【修复】修复递归搜索误提取tradeId等非商品ID的问题',
'【修复】修复订单管理商品ID提取错误的问题'
]
},
{
version: 'v1.2.1',
date: '2026-01-28',
updates: [
'【新功能】新增暗色模式支持,可在系统设置中切换主题',
'【新功能】下单时自动获取并保存买家昵称'
]
},
{
version: 'v1.2.0',
date: '2026-01-28',
updates: [
'【优化】大幅优化滑块验证重试策略',
'【优化】缩短滑块验证重试等待时间'
]
},
{
version: 'v1.1.9',
date: '2026-01-28',
updates: [
'【修复】修复交易关闭时订单状态不更新的问题'
]
},
{
version: 'v1.1.8',
date: '2026-01-28',
updates: [
'【优化】优化滑块验证策略',
'【新功能】添加滑块验证优化代码'
]
},
{
version: 'v1.1.7',
date: '2026-01-28',
updates: [
'【菜单管理】新增拖拽排序功能',
'【菜单管理】按住拖动图标可调整菜单顺序',
'【菜单管理】菜单顺序自动保存到用户配置',
'【版本信息】点击版本号可查看更新日志',
'【侧边栏】使用CSS order属性实现菜单重排序',
'【修复】修复菜单排序后管理员功能和登出按钮位置错乱的问题'
]
},
{
version: 'v1.1.6',
date: '2026-01-27',
updates: [
'【菜单管理】新增侧边栏菜单显示/隐藏功能',
'【菜单管理】在系统设置中可自定义显示哪些菜单项',
'【菜单管理】仪表盘和系统设置为必选项,其他菜单可自由开关',
'【菜单管理】设置自动保存到用户配置,刷新后保持'
]
},
{
version: 'v1.1.5',
date: '2026-01-27',
updates: [
'【主题设置】新增主题颜色自定义功能',
'【主题设置】提供9种预设颜色(靛蓝、紫罗兰、蓝色、青色、绿色、橙色、红色、粉色、灰色)',
'【主题设置】支持颜色选择器自定义任意颜色',
'【主题设置】支持直接输入颜色代码',
'【系统设置】主题设置界面简化,操作更直观',
'【系统设置】系统重启按钮移至页面标题栏右侧'
]
},
{
version: 'v1.1.4',
date: '2026-01-27',
updates: [
'【订单管理】新增买家昵称列,方便识别买家身份',
'【订单管理】订单搜索支持按买家昵称搜索',
'【自动补全】买家发消息时自动补全历史订单昵称',
'【订单详情】弹窗中显示买家昵称信息'
]
},
{
version: 'v1.1.3',
date: '2026-01-27',
updates: [
'【系统设置】优化"登录与注册设置"卡片布局',
'【系统设置】增大各选项间距,提升视觉体验',
'【系统设置】保存按钮使用大号样式并占满卡片宽度',
'【系统设置】状态提示移至按钮上方显示'
]
},
{
version: 'v1.1.2',
date: '2026-01-27',
updates: [
'【在线客服】修复页面底部白色空白区域问题',
'【系统设置】重新组织页面布局(两行两列)',
'【系统设置】合并"注册设置"到"登录与注册设置"',
'【侧边栏】优化折叠功能'
]
},
{
version: 'v1.1.1',
date: '2026-01-27',
updates: [
'【在线客服】优化账号密码显示布局,移至选择框旁边',
'【在线客服】刷新账号列表时自动重置账号密码显示',
'【API】cookies/details接口新增返回password字段',
'【UI】添加favicon图标,更新登录注册页面Logo',
'【配置】默认端口从8080修改为8090'
]
},
{
version: 'v1.1.0',
date: '2026-01-25',
updates: [
'添加登录页面验证码开关功能',
'优化订单管理功能',
'添加手动发货和刷新订单状态功能',
'完善双规格自动发货功能',
'修复自动发货模块语法错误导致账号无法启动的问题',
'添加手动重启功能'
]
},
{
version: 'v1.0.0',
date: '2026-01-24',
updates: [
'闲鱼管理系统初始版本'
]
}
]
};
/**
* 加载系统版本号并检查更新
*/
async function loadSystemVersion() {
try {
// 先从 version.txt 动态读取本地版本号
try {
const versionResponse = await fetch('/static/version.txt?t=' + Date.now());
if (versionResponse.ok) {
LOCAL_VERSION = (await versionResponse.text()).trim();
currentSystemVersion = LOCAL_VERSION;
}
} catch (e) {
console.warn('无法读取本地版本文件,使用默认版本');
LOCAL_VERSION = DEFAULT_VERSION;
}
// 显示当前本地版本
document.getElementById('versionNumber').textContent = LOCAL_VERSION;
// 添加点击事件,显示更新日志
const systemVersionBadge = document.getElementById('systemVersion');
if (systemVersionBadge) {
systemVersionBadge.style.cursor = 'pointer';
systemVersionBadge.title = '点击查看更新日志';
systemVersionBadge.onclick = () => showChangelogModal();
}
refreshHotUpdateButtonState();
if (!isHotUpdateAutoCheckEnabled()) {
return;
}
// 调用后端检查更新(复用热更新接口)
try {
const checkResult = await checkHotUpdate();
if (checkResult && checkResult.has_update) {
refreshHotUpdateButtonState(checkResult);
}
} catch (e) {
console.warn('版本检查失败:', e.message);
}
} catch (error) {
console.error('版本加载失败:', error);
document.getElementById('versionNumber').textContent = '未知';
}
}
/**
* 获取更新信息(使用缓存或本地版本历史)
*/
async function getUpdateInfo() {
// 如果已有缓存的远程版本信息,映射为前端期望的字段格式
if (remoteVersionInfo) {
return {
version: remoteVersionInfo.new_version || remoteVersionInfo.version,
updates: remoteVersionInfo.changelog || remoteVersionInfo.updates,
description: remoteVersionInfo.description,
releaseDate: remoteVersionInfo.release_date || remoteVersionInfo.releaseDate,
downloadUrl: remoteVersionInfo.downloadUrl,
altDownloadUrl: remoteVersionInfo.altDownloadUrl,
installMethods: remoteVersionInfo.installMethods,
notice: remoteVersionInfo.notice
};
}
// 使用本地版本历史作为兜底
remoteVersionInfo = LOCAL_VERSION_HISTORY;
return remoteVersionInfo;
}
/**
* 显示更新信息(点击"有更新"标签时调用)
*/
async function showUpdateInfo(newVersion) {
const updateInfo = await getUpdateInfo();
if (!updateInfo) return;
// 构建更新内容列表
let updateList = '';
if (updateInfo.updates && updateInfo.updates.length > 0) {
updateList = updateInfo.updates.map(item => `${item}`).join('');
}
// 构建安装方式区域
let installSection = '';
if (updateInfo.installMethods && updateInfo.installMethods.length > 0) {
installSection = updateInfo.installMethods.map(method => {
let content = '';
// 如果有步骤说明(如Docker安装)
if (method.steps && method.steps.length > 0) {
content = `
${method.steps.map(step => `
${step}
`).join('')}
`;
}
// 如果有下载链接(如EXE下载)
if (method.downloads && method.downloads.length > 0) {
content = `
`;
}
return `
${method.name}
${method.description ? `${method.description}` : ''}
${content}
`;
}).join('');
}
// 兼容旧格式:构建下载按钮(如果有下载地址)
let downloadSection = '';
if (!installSection && updateInfo.downloadUrl) {
downloadSection = `
`;
}
// 兼容旧格式:构建备用下载地址
let altDownloadSection = '';
if (!installSection && updateInfo.altDownloadUrl) {
altDownloadSection = `
`;
}
const modalHtml = `
`;
// 移除已存在的模态框
const existingModal = document.getElementById('updateModal');
if (existingModal) {
existingModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
modal.show();
}
// =============================================================================
// 最新权益弹窗功能
// =============================================================================
/**
* 显示更新日志弹窗
*/
function showChangelogModal() {
const changelogContent = document.getElementById('changelogContent');
if (!changelogContent) return;
// 从 LOCAL_VERSION_HISTORY 统一读取,避免维护两份数据
const prefixTypeMap = {
'新功能': 'feature',
'优化': 'optimize',
'修复': 'fix'
};
const changelog = LOCAL_VERSION_HISTORY.versionHistory.map(v => ({
version: v.version,
date: v.date,
changes: v.updates.map(text => {
let type = 'feature';
let cleanText = text;
const match = text.match(/^【(.+?)】(.+)$/);
if (match) {
if (prefixTypeMap[match[1]]) {
type = prefixTypeMap[match[1]];
cleanText = match[2];
} else {
// 模块名前缀(如【菜单管理】),保留完整文本
type = 'feature';
cleanText = text;
}
}
return { type, text: cleanText };
})
}));
// 生成HTML
const html = changelog.map(release => {
const changesHtml = release.changes.map(change => {
let icon, color;
switch (change.type) {
case 'feature':
icon = 'bi-plus-circle-fill';
color = '#28a745';
break;
case 'optimize':
icon = 'bi-arrow-up-circle-fill';
color = '#17a2b8';
break;
case 'fix':
icon = 'bi-wrench';
color = '#dc3545';
break;
default:
icon = 'bi-dot';
color = '#6c757d';
}
return `
${change.text}
`;
}).join('');
return `
${release.version}
${release.date}
${changesHtml}
`;
}).join('');
changelogContent.innerHTML = html;
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('changelogModal'));
modal.show();
}
/**
* 显示最新权益弹窗
*/
async function showBenefitsModal() {
try {
// 获取权益信息(使用缓存或重新请求)
const benefitsData = await getBenefitsInfo();
if (!benefitsData || !benefitsData.benefits || benefitsData.benefits.length === 0) {
showToast('暂无权益信息', 'info');
return;
}
// 构建权益列表
const benefitsList = benefitsData.benefits.map(benefit => `
${benefit.name}
${benefit.description || ''}
`).join('');
const modalHtml = `
`;
// 移除已存在的模态框
const existingModal = document.getElementById('benefitsModal');
if (existingModal) {
existingModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('benefitsModal'));
modal.show();
} catch (error) {
console.error('显示权益弹窗失败:', error);
showToast('获取权益信息失败', 'danger');
}
}
/**
* 获取权益信息(使用缓存或重新请求)
*/
async function getBenefitsInfo() {
// 如果已有缓存的远程版本信息并包含权益,直接使用
if (remoteVersionInfo && remoteVersionInfo.benefits) {
return remoteVersionInfo;
}
// 从远程获取权益信息
try {
const response = await fetch('http://116.196.116.76/version.php', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
showToast('获取权益信息失败: 网络错误', 'danger');
return null;
}
const result = await response.json();
if (result.error || !result.success) {
showToast('获取权益信息失败: ' + (result.message || '未知错误'), 'danger');
return null;
}
remoteVersionInfo = result.data;
return remoteVersionInfo;
} catch (error) {
console.error('获取权益信息失败:', error);
showToast('获取权益信息失败: ' + error.message, 'danger');
return null;
}
}
// =============================================================================
// 滑块验证相关函数
// =============================================================================
// 会话监控相关变量
let captchaSessionMonitor = null;
let activeCaptchaModal = null;
let monitoredSessions = new Set();
// 开始监控验证会话
function startCaptchaSessionMonitor() {
if (captchaSessionMonitor) {
console.log('⚠️ 会话监控已在运行中');
return; // 已经在监控中
}
console.log('🔍 开始监控验证会话...');
let checkCount = 0;
captchaSessionMonitor = setInterval(async () => {
try {
checkCount++;
const response = await fetch('/api/captcha/sessions');
const data = await response.json();
// 每10次检查输出一次日志
if (checkCount % 10 === 0) {
console.log(`🔍 监控检查 #${checkCount}: 活跃会话数=${data.count || 0}`);
}
if (data.sessions && data.sessions.length > 0) {
console.log('📋 当前活跃会话:', data.sessions);
for (const session of data.sessions) {
// 如果会话已完成或不存在,从监控列表中移除
if (session.completed || !session.has_websocket) {
if (monitoredSessions.has(session.session_id)) {
console.log(`✅ 会话已完成或已关闭: ${session.session_id}`);
monitoredSessions.delete(session.session_id);
}
continue;
}
// 如果发现新的会话(未完成且未被监控),立即弹出窗口
if (!monitoredSessions.has(session.session_id)) {
console.log(`✨ 检测到新的验证会话: ${session.session_id}`);
monitoredSessions.add(session.session_id);
// 自动弹出验证窗口
showCaptchaVerificationModal(session.session_id);
showToast('🎨 检测到滑块验证,请完成验证', 'warning');
}
}
}
// 如果没有活跃会话且没有监控中的会话,停止监控
if ((!data.sessions || data.sessions.length === 0) && monitoredSessions.size === 0) {
console.log('✅ 没有活跃会话且没有监控中的会话,停止全局监控');
stopCaptchaSessionMonitor();
}
} catch (error) {
console.error('监控验证会话失败:', error);
}
}, 1000); // 每秒检查一次
console.log('✅ 会话监控已启动');
}
// 停止监控验证会话
function stopCaptchaSessionMonitor() {
if (captchaSessionMonitor) {
clearInterval(captchaSessionMonitor);
captchaSessionMonitor = null;
monitoredSessions.clear();
console.log('⏹️ 停止监控验证会话');
}
}
// 手动测试会话监控(用于调试)
async function testCaptchaSessionMonitor() {
try {
console.log('🧪 测试会话监控...');
const response = await fetch('/api/captcha/sessions');
const data = await response.json();
console.log('📊 API响应:', data);
return data;
} catch (error) {
console.error('❌ 测试失败:', error);
return null;
}
}
// 手动弹出验证窗口(用于调试)
function testShowCaptchaModal(sessionId = 'default') {
console.log(`🧪 手动弹出验证窗口: ${sessionId}`);
showCaptchaVerificationModal(sessionId);
}
// 暴露到全局,方便调试和使用
window.testCaptchaSessionMonitor = testCaptchaSessionMonitor;
window.testShowCaptchaModal = testShowCaptchaModal;
window.startCaptchaSessionMonitor = startCaptchaSessionMonitor;
window.stopCaptchaSessionMonitor = stopCaptchaSessionMonitor;
window.showCaptchaVerificationModal = showCaptchaVerificationModal;
// 显示滑块验证模态框
function showCaptchaVerificationModal(sessionId = 'default') {
// 如果已经有活跃的弹窗,不重复弹出
if (activeCaptchaModal) {
console.log('已有活跃的验证窗口,不重复弹出');
return activeCaptchaModal;
}
const modal = new bootstrap.Modal(document.getElementById('captchaVerifyModal'), {
backdrop: 'static',
keyboard: false
});
const iframe = document.getElementById('captchaIframe');
const loadingIndicator = document.getElementById('captchaLoadingIndicator');
// 获取服务器地址
const serverUrl = window.location.origin;
// 重置 iframe
iframe.style.display = 'none';
loadingIndicator.style.display = 'block';
// 设置 iframe 源(嵌入模式)
iframe.src = `${serverUrl}/api/captcha/control/${sessionId}?embed=1`;
// iframe 加载完成后隐藏加载指示器
iframe.onload = function() {
loadingIndicator.style.display = 'none';
iframe.style.display = 'block';
};
// 显示模态框
modal.show();
activeCaptchaModal = modal;
// 自动启动验证完成监控
startCheckCaptchaCompletion(modal, sessionId);
// 监听模态框关闭事件
document.getElementById('captchaVerifyModal').addEventListener('hidden.bs.modal', () => {
activeCaptchaModal = null;
// 从监控列表中移除
monitoredSessions.delete(sessionId);
// 如果没有其他监控中的会话,停止全局监控
if (monitoredSessions.size === 0) {
stopCaptchaSessionMonitor();
console.log('✅ 弹窗关闭,已停止全局监控');
}
}, { once: true });
// 返回 modal 实例用于后续控制
return modal;
}
// 启动验证完成监控(自动模式)
function startCheckCaptchaCompletion(modal, sessionId) {
let checkInterval = null;
let isClosed = false;
const closeModal = () => {
if (isClosed) return;
isClosed = true;
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
// 从监控列表中移除
monitoredSessions.delete(sessionId);
// 如果没有其他监控中的会话,停止全局监控
if (monitoredSessions.size === 0) {
stopCaptchaSessionMonitor();
console.log('✅ 所有验证已完成,已停止全局监控');
}
modal.hide();
activeCaptchaModal = null;
showToast('✅ 滑块验证成功!', 'success');
console.log(`✅ 验证完成: ${sessionId}`);
};
checkInterval = setInterval(async () => {
try {
const response = await fetch(`/api/captcha/status/${sessionId}`);
const data = await response.json();
console.log(`检查验证状态: ${sessionId}`, data);
// 如果验证完成,或者会话不存在(已关闭),都视为完成
if (data.completed || (data.session_exists === false && data.success)) {
closeModal();
return;
}
} catch (error) {
console.error('检查验证状态失败:', error);
// 如果API调用失败,可能是会话已关闭,也视为完成
if (error.message && error.message.includes('404')) {
closeModal();
}
}
}, 1000); // 每秒检查一次
// 5分钟超时
setTimeout(() => {
if (!isClosed && checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
if (activeCaptchaModal) {
modal.hide();
activeCaptchaModal = null;
showToast('❌ 验证超时,请重试', 'danger');
}
}
}, 300000);
// 模态框关闭时停止检查
document.getElementById('captchaVerifyModal').addEventListener('hidden.bs.modal', () => {
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
isClosed = true;
}, { once: true });
}
// 检查验证是否完成(Promise模式,兼容旧代码)
async function checkCaptchaCompletion(modal, sessionId) {
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
const response = await fetch(`/api/captcha/status/${sessionId}`);
const data = await response.json();
if (data.completed) {
clearInterval(checkInterval);
resolve(true);
}
} catch (error) {
console.error('检查验证状态失败:', error);
}
}, 1000);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('验证超时'));
}, 300000);
document.getElementById('captchaVerifyModal').addEventListener('hidden.bs.modal', () => {
clearInterval(checkInterval);
}, { once: true });
});
}
// ========================= 验证截图相关功能 =========================
// 显示验证截图
async function showFaceVerification(accountId) {
try {
toggleLoading(true);
// 获取该账号的验证截图
const response = await fetch(`${apiBase}/face-verification/screenshot/${accountId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取验证截图失败');
}
const data = await response.json();
toggleLoading(false);
if (!data.success) {
showToast(data.message || '未找到验证截图', 'warning');
return;
}
// 使用与密码登录相同的弹窗显示验证截图
showAccountFaceVerificationModal(accountId, data.screenshot);
} catch (error) {
toggleLoading(false);
console.error('获取验证截图失败:', error);
showToast('获取验证截图失败: ' + error.message, 'danger');
}
}
// 显示账号列表的验证截图弹窗(使用与密码登录相同的样式)
function showAccountFaceVerificationModal(accountId, screenshot) {
// 复用密码登录的弹窗
let modal = document.getElementById('passwordLoginQRModal');
if (!modal) {
createPasswordLoginQRModal();
modal = document.getElementById('passwordLoginQRModal');
}
// 更新模态框标题
const modalTitle = document.getElementById('passwordLoginQRModalLabel');
if (modalTitle) {
modalTitle.innerHTML = `账号验证 - 账号 ${accountId}`;
}
// 显示截图
const screenshotImg = document.getElementById('passwordLoginScreenshotImg');
const linkButton = document.getElementById('passwordLoginVerificationLink');
const statusText = document.getElementById('passwordLoginQRStatusText');
if (screenshotImg) {
screenshotImg.src = `${screenshot.path}?t=${new Date().getTime()}`;
screenshotImg.style.display = 'block';
screenshotImg.alt = '验证截图';
}
// 隐藏链接按钮
if (linkButton) {
linkButton.style.display = 'none';
}
// 更新状态文本
if (statusText) {
statusText.innerHTML = `请根据下方验证截图在手机闲鱼APP中完成验证
创建时间: ${screenshot.created_time_str}`;
}
// 获取或创建模态框实例
let modalInstance = bootstrap.Modal.getInstance(modal);
if (!modalInstance) {
modalInstance = new bootstrap.Modal(modal);
}
// 显示弹窗
modalInstance.show();
// 注意:截图删除由后端在验证完成或失败时自动处理,前端不需要手动删除
}
// 注:人脸验证弹窗已复用密码登录的 passwordLoginQRModal,不再需要单独的弹窗
/**
* 显示版本信息弹窗
*/
async function showVersionInfo(version) {
// 尝试获取远程版本信息
const versionInfo = await getUpdateInfo();
// 构建项目介绍
const intro = versionInfo?.intro || '此版本为本人利用业余时间开发,功能可能不完善,欢迎大家提出建议和bug,我会尽快修复。此版本纯粹免费,没有任何收费项目,请大家放心使用。如果大家觉得这个项目对你有帮助,可以请我喝杯咖啡,支持我继续开发。';
// 构建版本历史
let versionHistoryHtml = '';
if (versionInfo?.versionHistory && versionInfo.versionHistory.length > 0) {
versionHistoryHtml = versionInfo.versionHistory.map((item, index) => {
const isLatest = index === 0;
const bgClass = isLatest ? 'background: linear-gradient(135deg, #e8f5e9, #c8e6c9);' : 'background: #f8f9fa;';
const borderColor = isLatest ? 'border-left: 4px solid #28a745;' : 'border-left: 4px solid #dee2e6;';
const badgeStyle = isLatest ? 'background: linear-gradient(135deg, #28a745, #20c997); color: #fff;' : 'background: #6c757d; color: #fff;';
return `
${item.version}
${isLatest ? '最新' : ''}
${item.date ? `
${item.date}` : ''}
${item.updates.map(u => `- ${u}
`).join('')}
`;
}).join('');
} else {
// 兜底:使用默认的版本历史
versionHistoryHtml = `
`;
}
const modalHtml = `
`;
// 移除旧的弹窗(如果存在)
const oldModal = document.getElementById('versionInfoModal');
if (oldModal) {
oldModal.remove();
}
// 添加新弹窗到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示弹窗
const modal = document.getElementById('versionInfoModal');
const modalInstance = new bootstrap.Modal(modal);
modalInstance.show();
// 弹窗关闭后删除DOM元素
modal.addEventListener('hidden.bs.modal', function () {
modal.remove();
});
}
// =============================================================================
// 热更新功能
// =============================================================================
/**
* 检查热更新
* 调用后端API检查是否有可用的文件更新
*/
async function checkHotUpdate() {
try {
const response = await fetch('/api/update/check', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
console.warn('热更新检查请求失败:', response.status);
return null;
}
const result = await response.json();
if (!result.success) {
console.warn('热更新检查返回错误:', result.message);
return null;
}
if (result.data) {
remoteVersionInfo = result.data;
}
return result.data;
} catch (error) {
console.error('热更新检查失败:', error);
return null;
}
}
/**
* 执行热更新
* 下载并安装所有可用更新
*/
async function performHotUpdate() {
setHotUpdateButtonsLoading();
try {
// 先检查是否有更新
const checkResult = await checkHotUpdate();
if (!checkResult) {
showToast('检查更新失败,请稍后重试', 'danger');
resetHotUpdateBtn();
return;
}
if (!checkResult.has_update) {
showToast('已是最新版本,无需更新', 'info');
resetHotUpdateBtn();
return;
}
// 显示确认对话框
const dialogAction = await showHotUpdateConfirmDialog(checkResult);
if (dialogAction !== 'confirm') {
if (dialogAction === 'ignore') {
const ignoredVersion = getHotUpdateTargetVersion(checkResult);
setIgnoredHotUpdateVersion(ignoredVersion);
refreshHotUpdatePreferencesMenu();
refreshHotUpdateButtonState(checkResult);
updateHotUpdatePreferenceStatus(`已忽略版本 ${ignoredVersion}`, 'success');
}
resetHotUpdateBtn();
return;
}
// 显示更新进度
showHotUpdateProgress();
// 执行更新
const response = await fetch('/api/update/apply', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
}
});
const result = await response.json();
// 关闭进度弹窗
closeHotUpdateProgress();
if (result.success && result.data.success) {
// 更新成功
const updateData = result.data;
const updatedCount = updateData.updated_files?.length || 0;
const deletedCount = updateData.deleted_files?.length || 0;
if (updateData.needs_restart) {
// 需要重启
showHotUpdateRestartDialog(updateData);
} else {
// 不需要重启,刷新页面即可
showToast(`更新成功!更新 ${updatedCount} 个文件,删除 ${deletedCount} 个旧文件`, 'success');
// 3秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
}
} else {
showToast('更新失败: ' + (result.detail || result.message || result.data?.message || '未知错误'), 'danger');
}
} catch (error) {
console.error('热更新执行失败:', error);
showToast('更新失败: ' + error.message, 'danger');
closeHotUpdateProgress();
} finally {
resetHotUpdateBtn();
}
}
/**
* 重置热更新按钮状态
*/
function resetHotUpdateBtn() {
const hotUpdateBtn = document.getElementById('hotUpdateBtn');
if (hotUpdateBtn) {
hotUpdateBtn.disabled = false;
hotUpdateBtn.innerHTML = '一键热更新';
}
refreshHotUpdateButtonState();
}
function setHotUpdateButtonsLoading() {
const hotUpdateBtn = document.getElementById('hotUpdateBtn');
if (hotUpdateBtn) {
hotUpdateBtn.disabled = true;
hotUpdateBtn.innerHTML = '检查更新中...';
}
const dashboardHotUpdateGroup = document.getElementById('dashboardHotUpdateGroup');
const dashboardHotUpdateBtn = document.getElementById('dashboardHotUpdateBtn');
const dashboardHotUpdateMenuBtn = document.getElementById('dashboardHotUpdateMenuBtn');
if (dashboardHotUpdateBtn) {
dashboardHotUpdateBtn.disabled = true;
dashboardHotUpdateBtn.innerHTML = '检查更新中...';
}
if (dashboardHotUpdateMenuBtn) {
dashboardHotUpdateMenuBtn.disabled = true;
}
if (dashboardHotUpdateGroup) {
dashboardHotUpdateGroup.classList.add('is-loading');
}
}
/**
* 显示热更新确认对话框
*/
async function showHotUpdateConfirmDialog(updateInfo) {
return new Promise((resolve) => {
const filesInfo = updateInfo.files && updateInfo.files.length > 0
? updateInfo.files.map(f => `${f.path} ${f.requires_restart ? '需重启' : ''}`).join('')
: '本次无新增或覆盖文件';
const deletedFilesInfo = updateInfo.deleted_files && updateInfo.deleted_files.length > 0
? updateInfo.deleted_files.map(f => `${f.path} ${f.requires_restart ? '需重启' : ''}`).join('')
: '';
const totalSizeKB = (updateInfo.total_size / 1024).toFixed(2);
const deletedCount = updateInfo.deleted_files_count || 0;
const deleteSection = deletedCount > 0 ? `
` : '';
const modalHtml = `
当前版本
${updateInfo.current_version}
目标版本
${updateInfo.new_version}
更新文件数
${updateInfo.files_count} 个
删除旧文件数
${deletedCount} 个
下载大小
${totalSizeKB} KB
${deleteSection}
提示:更新和删除前都会自动备份原文件,如遇问题可恢复。
`;
// 移除已存在的模态框
const existingModal = document.getElementById('hotUpdateConfirmModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modalElement = document.getElementById('hotUpdateConfirmModal');
const modal = new bootstrap.Modal(modalElement);
let resolved = false;
const finish = (action) => {
if (resolved) return;
resolved = true;
modal.hide();
resolve(action);
};
// 绑定按钮事件
document.getElementById('hotUpdateConfirmBtn').onclick = () => {
finish('confirm');
};
document.getElementById('hotUpdateCancelBtn').onclick = () => {
finish('skip');
};
document.getElementById('hotUpdateIgnoreBtn').onclick = () => {
finish('ignore');
};
modalElement.addEventListener('hidden.bs.modal', () => {
modalElement.remove();
if (!resolved) {
resolved = true;
resolve('skip');
}
});
modal.show();
});
}
/**
* 显示热更新进度
*/
function showHotUpdateProgress() {
const modalHtml = `
Loading...
正在更新...
正在下载更新文件
`;
// 移除已存在的模态框
const existingModal = document.getElementById('hotUpdateProgressModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('hotUpdateProgressModal'));
modal.show();
}
/**
* 关闭热更新进度
*/
function closeHotUpdateProgress() {
const modal = document.getElementById('hotUpdateProgressModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) {
bsModal.hide();
}
setTimeout(() => modal.remove(), 300);
}
}
/**
* 显示需要重启的对话框
*/
function showHotUpdateRestartDialog(updateData) {
const modalHtml = `
更新成功!
共更新 ${updateData.updated_files.length} 个文件到版本 ${updateData.new_version}
注意:部分更新的文件需要重启应用才能生效。
`;
// 移除已存在的模态框
const existingModal = document.getElementById('hotUpdateRestartModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('hotUpdateRestartModal'));
modal.show();
}
/**
* 重启应用
*/
async function restartApplication() {
try {
showToast('正在重启应用...', 'info');
const response = await fetch('/api/update/restart', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
}
});
const result = await response.json();
if (result.success) {
showToast('应用正在重启,页面将在5秒后自动刷新...', 'success');
// 5秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 5000);
} else {
showToast('重启失败: ' + result.message, 'danger');
}
} catch (error) {
console.error('重启应用失败:', error);
showToast('重启失败: ' + error.message, 'danger');
}
}
// 添加CSS动画
const hotUpdateStyle = document.createElement('style');
hotUpdateStyle.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin {
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(hotUpdateStyle);
// ==================== 在线客服IM功能 ====================
// 存储IM账号数据
let imAccountsData = [];
/**
* 加载IM账号列表
*/
async function loadImAccountList() {
try {
// 重置账号密码显示为默认值
const usernameEl = document.getElementById('imDisplayUsername');
const passwordEl = document.getElementById('imDisplayPassword');
if (usernameEl) usernameEl.textContent = '-';
if (passwordEl) passwordEl.textContent = '-';
const response = await fetch(`${apiBase}/cookies/details`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
imAccountsData = data || [];
const select = document.getElementById('imAccountSelect');
if (select) {
select.innerHTML = '';
imAccountsData.forEach(account => {
const option = document.createElement('option');
option.value = account.id;
option.textContent = account.id;
// 仅缓存非敏感账号信息,敏感字段按需拉取
option.dataset.username = account.username || '';
select.appendChild(option);
});
}
} else {
console.error('加载IM账号列表失败:', response.status);
}
} catch (error) {
console.error('加载IM账号列表失败:', error);
}
}
/**
* 账号选择变化时的处理
*/
async function fetchImAccountDetails(accountId) {
const response = await fetch(`${apiBase}/cookie/${encodeURIComponent(accountId)}/details?include_secrets=true`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error('获取账号详情失败');
}
return await response.json();
}
/**
* 账号选择变化时的处理
*/
async function onImAccountChange() {
const select = document.getElementById('imAccountSelect');
const usernameEl = document.getElementById('imDisplayUsername');
const passwordEl = document.getElementById('imDisplayPassword');
if (!select) return;
const selectedOption = select.selectedOptions[0];
if (selectedOption && selectedOption.value) {
const username = selectedOption.dataset.username || '未配置';
if (usernameEl) usernameEl.textContent = username;
if (passwordEl) {
passwordEl.textContent = '加载中...';
}
try {
const details = await fetchImAccountDetails(selectedOption.value);
const password = details.password || '';
if (passwordEl) {
passwordEl.textContent = password ? '••••••••' : '未配置';
}
} catch (error) {
if (passwordEl) {
passwordEl.textContent = '获取失败';
}
console.error('获取IM账号详情失败:', error);
showToast('获取账号密码失败,请稍后重试', 'warning');
}
} else {
// 未选择账号时显示默认值
if (usernameEl) usernameEl.textContent = '-';
if (passwordEl) passwordEl.textContent = '-';
}
}
/**
* 复制账号到剪贴板
*/
async function copyImUsername() {
const usernameEl = document.getElementById('imDisplayUsername');
const username = usernameEl ? usernameEl.textContent : '';
if (!username || username === '未配置' || username === '-') {
showToast('账号未配置', 'warning');
return;
}
try {
await navigator.clipboard.writeText(username);
showToast('账号已复制', 'success');
} catch (error) {
fallbackCopy(username, '账号已复制', '复制失败');
}
}
/**
* 复制密码到剪贴板
*/
async function copyImPassword() {
const select = document.getElementById('imAccountSelect');
if (!select || !select.value) {
showToast('请先选择账号', 'warning');
return;
}
let password = '';
try {
const details = await fetchImAccountDetails(select.value);
password = details.password || '';
} catch (error) {
console.error('获取密码失败:', error);
showToast('获取密码失败,请稍后重试', 'danger');
return;
}
if (!password || password === '未配置') {
showToast('密码未配置', 'warning');
return;
}
try {
await navigator.clipboard.writeText(password);
showToast('密码已复制', 'success');
} catch (error) {
fallbackCopy(password, '密码已复制', '复制失败');
}
}
/**
* 降级复制方案
*/
function fallbackCopy(text, successMsg, failMsg) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showToast(successMsg, 'success');
} catch (e) {
showToast(failMsg, 'danger');
}
document.body.removeChild(textArea);
}
/**
* 复制账号密码到剪贴板(保留兼容)
*/
async function copyImAccountInfo() {
const select = document.getElementById('imAccountSelect');
if (!select || !select.value) {
showToast('请先选择一个账号', 'warning');
return;
}
const selectedOption = select.selectedOptions[0];
const username = selectedOption.dataset.username || '';
let password = '';
try {
const details = await fetchImAccountDetails(select.value);
password = details.password || '';
} catch (error) {
console.error('获取账号密码失败:', error);
showToast('获取账号密码失败,请稍后重试', 'danger');
return;
}
if (!username && !password) {
showToast('该账号未配置用户名和密码', 'warning');
return;
}
const copyText = `账号:${username}\n密码:${password}`;
try {
await navigator.clipboard.writeText(copyText);
showToast('账号密码已复制到剪贴板', 'success');
} catch (error) {
fallbackCopy(copyText, '账号密码已复制到剪贴板', '复制失败,请手动复制');
}
}
/**
* 刷新IM iframe
*/
function refreshImIframe() {
const iframe = document.getElementById('goofishImIframe');
if (iframe) {
iframe.src = iframe.src;
showToast('页面已刷新', 'success');
}
}
/**
* 在新窗口打开闲鱼IM
*/
function openGoofishImNewWindow() {
window.open('https://www.goofish.com/im', '_blank');
}
/**
* 兼容旧版函数名
*/
function openGoofishIm() {
openGoofishImNewWindow();
}
/**
* 加载在线客服页面
*/
function loadOnlineIm() {
loadImAccountList();
// 延迟加载 iframe,避免页面加载时直接加载闲鱼导致跳转问题
const iframe = document.getElementById('goofishImIframe');
if (iframe && iframe.src === 'about:blank') {
const realSrc = iframe.dataset.src || 'https://www.goofish.com/im';
iframe.src = realSrc;
}
}
// ==================== 定时擦亮任务管理 ====================
const POLISH_SCHEDULE_RANDOM_MINUTES = 10;
async function loadScheduledTasks() {
try {
const data = await fetchJSON(`${apiBase}/scheduled-tasks`);
if (data.success) {
return data.tasks || [];
}
showToast(`加载定时任务失败: ${data.message || '未知错误'}`, 'danger');
return [];
} catch (error) {
console.error('加载定时任务失败:', error);
return [];
}
}
async function createScheduledTask(accountId, runHour, enabled = true) {
return fetchJSON(`${apiBase}/scheduled-tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
account_id: accountId,
run_hour: runHour,
enabled,
random_delay_max: POLISH_SCHEDULE_RANDOM_MINUTES
})
});
}
async function updateScheduledTask(taskId, payload) {
return fetchJSON(`${apiBase}/scheduled-tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
}
function getPolishScheduledTask(tasks, accountId) {
const matchedTasks = tasks
.filter(task => task.account_id === accountId && task.task_type === 'item_polish')
.sort((a, b) => Number(Boolean(b.enabled)) - Number(Boolean(a.enabled)) || Number(b.id) - Number(a.id));
return matchedTasks[0] || null;
}
function formatPolishScheduleHour(hour) {
const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
return `${String(safeHour).padStart(2, '0')}:00`;
}
function getPolishScheduleDescription(taskOrHour, randomDelayMax = POLISH_SCHEDULE_RANDOM_MINUTES) {
const runHour = typeof taskOrHour === 'object' && taskOrHour !== null
? (taskOrHour.delay_minutes ?? taskOrHour.run_hour ?? 0)
: taskOrHour;
const safeRandomDelay = typeof taskOrHour === 'object' && taskOrHour !== null
? (taskOrHour.random_delay_max ?? randomDelayMax)
: randomDelayMax;
return `每日 ${formatPolishScheduleHour(runHour)} 后随机 0-${safeRandomDelay} 分钟擦亮一次`;
}
function closePolishScheduleModal() {
const modalElement = document.getElementById('polishScheduleModal');
if (!modalElement) return;
const modalInstance = bootstrap.Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
} else {
modalElement.remove();
}
}
function refreshPolishScheduleModalState() {
const enabledInput = document.getElementById('polishScheduleEnabled');
const hourSelect = document.getElementById('polishScheduleHour');
const hint = document.getElementById('polishScheduleHint');
if (!enabledInput || !hourSelect || !hint) return;
const enabled = enabledInput.checked;
const runHour = parseInt(hourSelect.value, 10);
hint.className = `alert ${enabled ? 'alert-info' : 'alert-secondary'} py-2 mb-3`;
hint.textContent = enabled
? getPolishScheduleDescription(runHour)
: `当前已关闭,保存后会记住 ${formatPolishScheduleHour(runHour)} 的设置,但不会自动执行`;
}
async function openPolishScheduleModal(accountId) {
try {
const tasks = await loadScheduledTasks();
const task = getPolishScheduledTask(tasks, accountId);
const runHour = Number.isFinite(Number(task?.delay_minutes)) ? Number(task.delay_minutes) : 8;
const enabled = task ? Boolean(task.enabled) : true;
const hourOptions = Array.from({ length: 24 }, (_, hour) => `
`).join('');
const statusText = task ? (task.enabled ? '已开启' : '未开启') : '保存后启用';
const nextRunText = task ? (task.enabled ? (task.next_run_at || '保存后生成') : '已关闭') : '保存后生成';
const lastRunText = task?.last_run_at || '暂无记录';
const existingModal = document.getElementById('polishScheduleModal');
if (existingModal) {
existingModal.remove();
}
const modalHtml = `
${getPolishScheduleDescription(runHour)}
当前状态:${statusText}
下次执行:${nextRunText}
上次执行:${lastRunText}
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modalElement = document.getElementById('polishScheduleModal');
const modalInstance = new bootstrap.Modal(modalElement);
modalElement.addEventListener('hidden.bs.modal', function () {
modalElement.remove();
});
document.getElementById('polishScheduleEnabled').addEventListener('change', refreshPolishScheduleModalState);
document.getElementById('polishScheduleHour').addEventListener('change', refreshPolishScheduleModalState);
refreshPolishScheduleModalState();
modalInstance.show();
} catch (error) {
console.error('打开定时擦亮设置失败:', error);
}
}
async function savePolishSchedule() {
const accountId = document.getElementById('polishScheduleAccountId')?.value;
const taskId = parseInt(document.getElementById('polishScheduleTaskId')?.value || '', 10);
const enabled = document.getElementById('polishScheduleEnabled')?.checked;
const runHour = parseInt(document.getElementById('polishScheduleHour')?.value || '', 10);
if (!accountId) {
showToast('缺少账号ID', 'warning');
return;
}
if (!Number.isInteger(runHour) || runHour < 0 || runHour > 23) {
showToast('请选择有效的擦亮时间', 'warning');
return;
}
try {
let data;
if (taskId) {
data = await updateScheduledTask(taskId, {
run_hour: runHour,
enabled,
random_delay_max: POLISH_SCHEDULE_RANDOM_MINUTES
});
} else {
data = await createScheduledTask(accountId, runHour, enabled);
}
if (!data.success) {
showToast(`保存失败: ${data.message || '未知错误'}`, 'danger');
return;
}
const successMessage = enabled
? `${accountId} 已设置为 ${getPolishScheduleDescription(runHour)}`
: `${accountId} 已保存 ${formatPolishScheduleHour(runHour)} 的定时擦亮时间,当前为关闭状态`;
showToast(successMessage, 'success');
closePolishScheduleModal();
} catch (error) {
console.error('保存定时擦亮设置失败:', error);
}
}