From a9c8aac48f0367483dda2ddd529d0467c0628222 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Sun, 14 Dec 2025 11:30:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=B4=A6=E5=8F=B7=E9=A1=B5=E9=97=AA?= =?UTF-8?q?=E7=83=81/=E6=B5=8F=E8=A7=88=E7=B1=BB=E5=9E=8B/=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E5=A4=8D=E5=88=B6/=E6=97=B6=E5=8C=BA=E7=BB=9F?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- UI_REFACTOR_FRONTEND.md | 2 +- admin-frontend/src/pages/StatsPage.vue | 2 +- admin-frontend/src/pages/SystemPage.vue | 8 +- admin-frontend/src/utils/datetime.js | 18 +- api_browser.py | 11 +- app-frontend/src/pages/AccountsPage.vue | 58 ++++- app-frontend/src/pages/SchedulesPage.vue | 14 +- app-frontend/src/pages/ScreenshotsPage.vue | 120 +++++++++- app.py | 85 +++++-- database.py | 128 +++++++--- email_service.py | 139 +++++++---- playwright_automation.py | 2 +- static/admin/.vite/manifest.json | 50 ++-- ...1QH6T.js => AnnouncementsPage-C63j6LV5.js} | 2 +- ...Page-C0sjJZrc.js => EmailPage-Bf6BbYPD.js} | 2 +- ...-B_IMe7WI.js => FeedbacksPage-mOUifait.js} | 2 +- ...sPage-Bvt31x2D.js => LogsPage-DVfeUO3d.js} | 2 +- ...ge-Bs4BEacx.js => PendingPage-BALptdIG.js} | 2 +- ...e-BA0VS3mc.js => SettingsPage-BKf3hQvU.js} | 2 +- static/admin/assets/StatsPage-B4w_lWWU.css | 1 - static/admin/assets/StatsPage-Bjh6A42Y.css | 1 + static/admin/assets/StatsPage-CyjgHApe.js | 1 - static/admin/assets/StatsPage-DjylIGTc.js | 1 + static/admin/assets/SystemPage-DC1VKbLw.css | 1 - static/admin/assets/SystemPage-DMxUhCvv.js | 16 -- static/admin/assets/SystemPage-b-S9OiVi.css | 1 + static/admin/assets/SystemPage-k6FhqNid.js | 16 ++ ...Page-DDSa1S98.js => UsersPage-XRSMHsqH.js} | 2 +- static/admin/assets/datetime-CpkTDmvr.js | 1 - static/admin/assets/datetime-ZCuLLiQt.js | 1 + .../{index-CrrNPCqw.js => index-C5w7EVNo.js} | 4 +- .../{tasks-C2mQL6Tj.js => tasks-BYcXDffp.js} | 2 +- .../{users-5QCWoNsI.js => users-Du3tLSHt.js} | 2 +- static/admin/index.html | 2 +- static/app/.vite/manifest.json | 40 ++-- static/app/assets/AccountsPage-CugNoiiP.js | 1 + static/app/assets/AccountsPage-D6CPzxlM.js | 1 - static/app/assets/AccountsPage-DAtYSxiO.css | 1 + static/app/assets/AccountsPage-OUqse8pw.css | 1 - ...Page-DY0hfuOv.js => LoginPage-8hxar7WW.js} | 2 +- ...e-CXbQlCO0.js => RegisterPage-B_EUWOuP.js} | 2 +- ...LXPca.js => ResetPasswordPage-BLEflhaq.js} | 2 +- static/app/assets/SchedulesPage-BbA-BJek.js | 1 + static/app/assets/SchedulesPage-DIc25kqH.js | 1 - static/app/assets/SchedulesPage-Gu_OsDUd.css | 1 - static/app/assets/SchedulesPage-s5SCMMmz.css | 1 + static/app/assets/ScreenshotsPage-BawNJkO2.js | 1 - .../app/assets/ScreenshotsPage-CRn_Qd8Q.css | 1 - static/app/assets/ScreenshotsPage-DCIxup8x.js | 1 + .../app/assets/ScreenshotsPage-DgLR6Xlu.css | 1 + ...GZLPnL.js => VerifyResultPage-QQKdIo1L.js} | 2 +- ...ounts-B4fckN3X.js => accounts-DuQjqW8V.js} | 2 +- .../{auth-DhZn_bRf.js => auth-C__02fQ5.js} | 2 +- .../{index-XYID9gNS.js => index-2JnZbEa5.js} | 4 +- static/app/index.html | 2 +- task_checkpoint.py | 224 +++++++++--------- templates/admin_legacy.html | 22 +- templates/index.html | 5 +- 59 files changed, 685 insertions(+), 339 deletions(-) rename static/admin/assets/{AnnouncementsPage-9I91QH6T.js => AnnouncementsPage-C63j6LV5.js} (98%) rename static/admin/assets/{EmailPage-C0sjJZrc.js => EmailPage-Bf6BbYPD.js} (99%) rename static/admin/assets/{FeedbacksPage-B_IMe7WI.js => FeedbacksPage-mOUifait.js} (97%) rename static/admin/assets/{LogsPage-Bvt31x2D.js => LogsPage-DVfeUO3d.js} (96%) rename static/admin/assets/{PendingPage-Bs4BEacx.js => PendingPage-BALptdIG.js} (95%) rename static/admin/assets/{SettingsPage-BA0VS3mc.js => SettingsPage-BKf3hQvU.js} (97%) delete mode 100644 static/admin/assets/StatsPage-B4w_lWWU.css create mode 100644 static/admin/assets/StatsPage-Bjh6A42Y.css delete mode 100644 static/admin/assets/StatsPage-CyjgHApe.js create mode 100644 static/admin/assets/StatsPage-DjylIGTc.js delete mode 100644 static/admin/assets/SystemPage-DC1VKbLw.css delete mode 100644 static/admin/assets/SystemPage-DMxUhCvv.js create mode 100644 static/admin/assets/SystemPage-b-S9OiVi.css create mode 100644 static/admin/assets/SystemPage-k6FhqNid.js rename static/admin/assets/{UsersPage-DDSa1S98.js => UsersPage-XRSMHsqH.js} (98%) delete mode 100644 static/admin/assets/datetime-CpkTDmvr.js create mode 100644 static/admin/assets/datetime-ZCuLLiQt.js rename static/admin/assets/{index-CrrNPCqw.js => index-C5w7EVNo.js} (99%) rename static/admin/assets/{tasks-C2mQL6Tj.js => tasks-BYcXDffp.js} (88%) rename static/admin/assets/{users-5QCWoNsI.js => users-Du3tLSHt.js} (91%) create mode 100644 static/app/assets/AccountsPage-CugNoiiP.js delete mode 100644 static/app/assets/AccountsPage-D6CPzxlM.js create mode 100644 static/app/assets/AccountsPage-DAtYSxiO.css delete mode 100644 static/app/assets/AccountsPage-OUqse8pw.css rename static/app/assets/{LoginPage-DY0hfuOv.js => LoginPage-8hxar7WW.js} (98%) rename static/app/assets/{RegisterPage-CXbQlCO0.js => RegisterPage-B_EUWOuP.js} (97%) rename static/app/assets/{ResetPasswordPage-CQpLXPca.js => ResetPasswordPage-BLEflhaq.js} (96%) create mode 100644 static/app/assets/SchedulesPage-BbA-BJek.js delete mode 100644 static/app/assets/SchedulesPage-DIc25kqH.js delete mode 100644 static/app/assets/SchedulesPage-Gu_OsDUd.css create mode 100644 static/app/assets/SchedulesPage-s5SCMMmz.css delete mode 100644 static/app/assets/ScreenshotsPage-BawNJkO2.js delete mode 100644 static/app/assets/ScreenshotsPage-CRn_Qd8Q.css create mode 100644 static/app/assets/ScreenshotsPage-DCIxup8x.js create mode 100644 static/app/assets/ScreenshotsPage-DgLR6Xlu.css rename static/app/assets/{VerifyResultPage-C-GZLPnL.js => VerifyResultPage-QQKdIo1L.js} (97%) rename static/app/assets/{accounts-B4fckN3X.js => accounts-DuQjqW8V.js} (93%) rename static/app/assets/{auth-DhZn_bRf.js => auth-C__02fQ5.js} (91%) rename static/app/assets/{index-XYID9gNS.js => index-2JnZbEa5.js} (99%) 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(() => { - +