diff --git a/README.md b/README.md index e346e94..72826c9 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ certbot renew --dry-run ### 2. 定时任务 - **启用定时浏览**: 是/否 - **执行时间**: 02:00 (CST时间) -- **浏览类型**: 应读/注册前未读/未读 +- **浏览类型**: 应读/注册前未读 - **执行日期**: 周一到周日 ### 3. 代理配置 diff --git a/UI_REFACTOR_FRONTEND.md b/UI_REFACTOR_FRONTEND.md index cd85bd7..0e21ebe 100644 --- a/UI_REFACTOR_FRONTEND.md +++ b/UI_REFACTOR_FRONTEND.md @@ -85,7 +85,7 @@ - 批量启动:`POST /api/accounts/batch/start` - 批量停止:`POST /api/accounts/batch/stop` - 清空账号:`POST /api/accounts/clear` - - 批量参数:浏览类型(应读/未读/注册前未读)、截图开关、选中账号集合 + - 批量参数:浏览类型(应读/注册前未读)、截图开关、选中账号集合 - VIP 限制/提示: - 普通用户账号数量上限、批量能力/定时能力等(以现有逻辑为准) diff --git a/admin-frontend/src/pages/StatsPage.vue b/admin-frontend/src/pages/StatsPage.vue index 34f30f2..c2c70ea 100644 --- a/admin-frontend/src/pages/StatsPage.vue +++ b/admin-frontend/src/pages/StatsPage.vue @@ -63,7 +63,7 @@ let timer = null function recordUpdatedAt() { try { - lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' }) } catch { lastUpdatedAt.value = '' } diff --git a/admin-frontend/src/pages/SystemPage.vue b/admin-frontend/src/pages/SystemPage.vue index 974045b..45865f1 100644 --- a/admin-frontend/src/pages/SystemPage.vue +++ b/admin-frontend/src/pages/SystemPage.vue @@ -54,6 +54,11 @@ const scheduleWeekdayDisplay = computed(() => .join('、'), ) +function normalizeBrowseType(value) { + if (String(value) === '注册前未读') return '注册前未读' + return '应读' +} + async function loadAll() { loading.value = true try { @@ -65,7 +70,7 @@ async function loadAll() { scheduleEnabled.value = (system.schedule_enabled ?? 0) === 1 scheduleTime.value = system.schedule_time || '02:00' - scheduleBrowseType.value = system.schedule_browse_type || '应读' + scheduleBrowseType.value = normalizeBrowseType(system.schedule_browse_type) const weekdays = String(system.schedule_weekdays || '1,2,3,4,5,6,7') .split(',') @@ -279,7 +284,6 @@ onMounted(loadAll) - diff --git a/admin-frontend/src/utils/datetime.js b/admin-frontend/src/utils/datetime.js index 9ba2f81..c96d214 100644 --- a/admin-frontend/src/utils/datetime.js +++ b/admin-frontend/src/utils/datetime.js @@ -2,9 +2,22 @@ export function parseSqliteDateTime(value) { if (!value) return null if (value instanceof Date) return value - const str = String(value) + let str = String(value).trim() + if (!str) return null + + // "YYYY-MM-DD" -> "YYYY-MM-DDT00:00:00" + if (/^\d{4}-\d{2}-\d{2}$/.test(str)) str = `${str}T00:00:00` + // "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss" - const iso = str.includes('T') ? str : str.replace(' ', 'T') + let iso = str.includes('T') ? str : str.replace(' ', 'T') + + // SQLite 可能带微秒,Date 仅可靠支持到毫秒 + iso = iso.replace(/\.(\d{3})\d+/, '.$1') + + // 统一按北京时间解析(除非字符串本身已带时区) + const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso) + if (!hasTimezone) iso = `${iso}+08:00` + const date = new Date(iso) if (Number.isNaN(date.getTime())) return null return date @@ -14,4 +27,3 @@ export function formatDateTime(value) { if (!value) return '-' return String(value) } - diff --git a/api_browser.py b/api_browser.py index 1c6b376..4b7ee29 100755 --- a/api_browser.py +++ b/api_browser.py @@ -350,14 +350,13 @@ class APIBrowser: return result # 根据浏览类型确定 bz 参数 - # 网页实际选项: 0=注册前未读, 1=已读, 2=应读 - # 前端选项: 注册前未读, 应读, 未读, 已读 - if '注册前' in browse_type: + # 网页实际参数: 0=注册前未读, 2=应读(历史上曾存在 1=已读,但当前逻辑不再使用) + # 当前前端选项: 注册前未读、应读(默认应读) + browse_type_text = str(browse_type or "") + if '注册前' in browse_type_text: bz = 0 # 注册前未读 - elif browse_type == '已读': - bz = 1 # 已读 else: - bz = 2 # 应读、未读 都映射到 bz=2 + bz = 2 # 应读 self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...") diff --git a/app-frontend/src/pages/AccountsPage.vue b/app-frontend/src/pages/AccountsPage.vue index 86bd8fe..ab6bdf8 100644 --- a/app-frontend/src/pages/AccountsPage.vue +++ b/app-frontend/src/pages/AccountsPage.vue @@ -59,7 +59,6 @@ const editForm = reactive({ const browseTypeOptions = [ { label: '应读', value: '应读' }, - { label: '未读', value: '未读' }, { label: '注册前未读', value: '注册前未读' }, ] @@ -80,8 +79,13 @@ function normalizeAccountPayload(acc) { } function replaceAccounts(list) { - for (const key of Object.keys(accountsById)) delete accountsById[key] - for (const acc of list || []) normalizeAccountPayload(acc) + const nextList = Array.isArray(list) ? list : [] + const nextIds = new Set(nextList.map((acc) => String(acc?.id || ''))) + + for (const existingId of Object.keys(accountsById)) { + if (!nextIds.has(existingId)) delete accountsById[existingId] + } + for (const acc of nextList) normalizeAccountPayload(acc) } function ensureBrowseTypeDefaults() { @@ -138,8 +142,9 @@ function showRuntimeProgress(acc) { return true } -async function refreshStats() { - statsLoading.value = true +async function refreshStats(options = {}) { + const silent = Boolean(options?.silent) + if (!silent) statsLoading.value = true try { const data = await fetchRunStats() stats.today_completed = Number(data?.today_completed || 0) @@ -150,7 +155,7 @@ async function refreshStats() { } catch (e) { if (e?.response?.status === 401) window.location.href = '/login' } finally { - statsLoading.value = false + if (!silent) statsLoading.value = false } } @@ -456,6 +461,41 @@ function bindSocket() { let unbindSocket = null let statsTimer = null +const shouldPollStats = computed(() => { + // 仅在“真正执行中”才轮询(排队中不轮询,避免空转导致页面闪烁) + return accounts.value.some((acc) => { + if (!acc?.is_running) return false + const statusText = String(acc.status || '') + if (statusText.includes('排队')) return false + return true + }) +}) + +function stopStatsPolling() { + if (!statsTimer) return + window.clearInterval(statsTimer) + statsTimer = null +} + +function startStatsPolling() { + if (statsTimer) return + statsTimer = window.setInterval(() => refreshStats({ silent: true }), 10_000) +} + +function syncStatsPolling(prevRunning = null) { + const running = shouldPollStats.value + + // 任务从“运行中 -> 空闲”时,补拉一次统计以确保最终数据正确,再停止轮询 + if (prevRunning === true && running === false) refreshStats({ silent: true }).catch(() => {}) + + if (running) startStatsPolling() + else stopStatsPolling() +} + +watch(shouldPollStats, (running, prevRunning) => { + syncStatsPolling(prevRunning) +}) + onMounted(async () => { if (!userStore.vipInfo) { userStore.refreshVipInfo().catch(() => { @@ -467,12 +507,12 @@ onMounted(async () => { await refreshAccounts() await refreshStats() - statsTimer = window.setInterval(refreshStats, 10_000) + syncStatsPolling() }) onBeforeUnmount(() => { if (unbindSocket) unbindSocket() - if (statsTimer) window.clearInterval(statsTimer) + stopStatsPolling() }) @@ -565,7 +605,7 @@ onBeforeUnmount(() => { - +