From 9aa28f5b9e36a9d042f76dcf278e5bb453a34d4c Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Mon, 15 Dec 2025 21:39:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8A=A5=E8=A1=A8=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=92=8C=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-frontend/src/components/StatsCards.vue | 5 +- admin-frontend/src/layouts/AdminLayout.vue | 8 +- admin-frontend/src/pages/PendingPage.vue | 228 ------ admin-frontend/src/pages/ReportPage.vue | 687 ++++++++++++++++++ admin-frontend/src/pages/SystemPage.vue | 10 +- admin-frontend/src/pages/UsersPage.vue | 136 +++- admin-frontend/src/router/index.js | 7 +- app-frontend/src/pages/RegisterPage.vue | 2 +- database.py | 2 +- db/admin.py | 22 +- db/migrations.py | 26 + db/schema.py | 2 +- db/users.py | 8 +- routes/api_auth.py | 46 +- static/admin/.vite/manifest.json | 86 ++- .../assets/AnnouncementsPage-BoMMAWcs.js | 1 + .../assets/AnnouncementsPage-L5yVXHzU.js | 1 - static/admin/assets/EmailPage-VvSoafV1.js | 1 + static/admin/assets/EmailPage-qHIhKChc.js | 1 - ...-DPKEyWIv.js => FeedbacksPage-DSgG1B6b.js} | 2 +- static/admin/assets/LogsPage-D1qK6xGt.js | 1 + static/admin/assets/LogsPage-EUt-yBa-.js | 1 - static/admin/assets/PendingPage-C_mZDlzP.css | 1 - static/admin/assets/PendingPage-D8-nvUo0.js | 1 - static/admin/assets/ReportPage-BtciWYlz.js | 1 + static/admin/assets/ReportPage-Ds4jOTh9.css | 1 + ...e-8bHYJpQZ.js => SettingsPage-Bw9iP0hP.js} | 2 +- static/admin/assets/StatsPage-BumsyjN6.js | 1 + static/admin/assets/StatsPage-D_TFEGc9.js | 1 - static/admin/assets/SystemPage-BjTkcmTG.css | 1 + static/admin/assets/SystemPage-CbqDATVe.js | 22 - static/admin/assets/SystemPage-CdLP1b5Y.css | 1 - static/admin/assets/SystemPage-CosZ9Vtj.js | 22 + static/admin/assets/UsersPage-C6sG2ovw.js | 1 + static/admin/assets/UsersPage-COyIOdOo.js | 1 - static/admin/assets/UsersPage-CbiPbpuj.css | 1 + static/admin/assets/UsersPage-D2Xg1a62.css | 1 - static/admin/assets/datetime-ZCuLLiQt.js | 1 - static/admin/assets/email-DiVz51rK.js | 1 + ...{index-BIDpnzAs.css => index-C73IFBwi.css} | 2 +- .../{index-DMMQbxWA.js => index-Do26tg8I.js} | 4 +- static/admin/assets/taskSource-B7bFDyX8.js | 1 - static/admin/assets/taskSource-CFicR2zp.js | 1 + static/admin/assets/tasks-Bpfaxqqb.js | 1 + static/admin/assets/update-B-ZRn1LV.js | 1 + static/admin/assets/users-BKWiZCGY.js | 1 - static/admin/assets/users-CgASQeNW.js | 1 + static/admin/index.html | 4 +- static/app/.vite/manifest.json | 36 +- ...e-BRy_ANjo.js => AccountsPage-Ci46ulXO.js} | 2 +- ...Page-DQyjJkcn.js => LoginPage-CSQ5MWiZ.js} | 2 +- static/app/assets/RegisterPage-Bbjpgkc1.js | 1 - static/app/assets/RegisterPage-CVjBOq6i.css | 1 - static/app/assets/RegisterPage-DMaHEdd1.js | 1 + static/app/assets/RegisterPage-yylt2w7b.css | 1 + ...n5I3l.js => ResetPasswordPage-lLB2G9rF.js} | 2 +- ...-BUgXa5ne.js => SchedulesPage-BKXmBVuW.js} | 2 +- ..._eU28nf.js => ScreenshotsPage-4zEBaZjf.js} | 2 +- ...PsEifw.js => VerifyResultPage-BuyLCSRI.js} | 2 +- ...ounts-CDwCkd9q.js => accounts-nj5kn4js.js} | 2 +- .../{auth-U2Kna0qf.js => auth-B-8S3dtx.js} | 2 +- .../{index-BzB0auqv.js => index-oLrocmqc.js} | 4 +- static/app/index.html | 2 +- 63 files changed, 1018 insertions(+), 403 deletions(-) delete mode 100644 admin-frontend/src/pages/PendingPage.vue create mode 100644 admin-frontend/src/pages/ReportPage.vue create mode 100644 static/admin/assets/AnnouncementsPage-BoMMAWcs.js delete mode 100644 static/admin/assets/AnnouncementsPage-L5yVXHzU.js create mode 100644 static/admin/assets/EmailPage-VvSoafV1.js delete mode 100644 static/admin/assets/EmailPage-qHIhKChc.js rename static/admin/assets/{FeedbacksPage-DPKEyWIv.js => FeedbacksPage-DSgG1B6b.js} (95%) create mode 100644 static/admin/assets/LogsPage-D1qK6xGt.js delete mode 100644 static/admin/assets/LogsPage-EUt-yBa-.js delete mode 100644 static/admin/assets/PendingPage-C_mZDlzP.css delete mode 100644 static/admin/assets/PendingPage-D8-nvUo0.js create mode 100644 static/admin/assets/ReportPage-BtciWYlz.js create mode 100644 static/admin/assets/ReportPage-Ds4jOTh9.css rename static/admin/assets/{SettingsPage-8bHYJpQZ.js => SettingsPage-Bw9iP0hP.js} (95%) create mode 100644 static/admin/assets/StatsPage-BumsyjN6.js delete mode 100644 static/admin/assets/StatsPage-D_TFEGc9.js create mode 100644 static/admin/assets/SystemPage-BjTkcmTG.css delete mode 100644 static/admin/assets/SystemPage-CbqDATVe.js delete mode 100644 static/admin/assets/SystemPage-CdLP1b5Y.css create mode 100644 static/admin/assets/SystemPage-CosZ9Vtj.js create mode 100644 static/admin/assets/UsersPage-C6sG2ovw.js delete mode 100644 static/admin/assets/UsersPage-COyIOdOo.js create mode 100644 static/admin/assets/UsersPage-CbiPbpuj.css delete mode 100644 static/admin/assets/UsersPage-D2Xg1a62.css delete mode 100644 static/admin/assets/datetime-ZCuLLiQt.js create mode 100644 static/admin/assets/email-DiVz51rK.js rename static/admin/assets/{index-BIDpnzAs.css => index-C73IFBwi.css} (99%) rename static/admin/assets/{index-DMMQbxWA.js => index-Do26tg8I.js} (98%) delete mode 100644 static/admin/assets/taskSource-B7bFDyX8.js create mode 100644 static/admin/assets/taskSource-CFicR2zp.js create mode 100644 static/admin/assets/tasks-Bpfaxqqb.js create mode 100644 static/admin/assets/update-B-ZRn1LV.js delete mode 100644 static/admin/assets/users-BKWiZCGY.js create mode 100644 static/admin/assets/users-CgASQeNW.js rename static/app/assets/{AccountsPage-BRy_ANjo.js => AccountsPage-Ci46ulXO.js} (99%) rename static/app/assets/{LoginPage-DQyjJkcn.js => LoginPage-CSQ5MWiZ.js} (98%) delete mode 100644 static/app/assets/RegisterPage-Bbjpgkc1.js delete mode 100644 static/app/assets/RegisterPage-CVjBOq6i.css create mode 100644 static/app/assets/RegisterPage-DMaHEdd1.js create mode 100644 static/app/assets/RegisterPage-yylt2w7b.css rename static/app/assets/{ResetPasswordPage-qMRn5I3l.js => ResetPasswordPage-lLB2G9rF.js} (96%) rename static/app/assets/{SchedulesPage-BUgXa5ne.js => SchedulesPage-BKXmBVuW.js} (99%) rename static/app/assets/{ScreenshotsPage-C_eU28nf.js => ScreenshotsPage-4zEBaZjf.js} (99%) rename static/app/assets/{VerifyResultPage-xHPsEifw.js => VerifyResultPage-BuyLCSRI.js} (97%) rename static/app/assets/{accounts-CDwCkd9q.js => accounts-nj5kn4js.js} (93%) rename static/app/assets/{auth-U2Kna0qf.js => auth-B-8S3dtx.js} (91%) rename static/app/assets/{index-BzB0auqv.js => index-oLrocmqc.js} (99%) diff --git a/admin-frontend/src/components/StatsCards.vue b/admin-frontend/src/components/StatsCards.vue index 666b5b0..1359b5e 100644 --- a/admin-frontend/src/components/StatsCards.vue +++ b/admin-frontend/src/components/StatsCards.vue @@ -8,8 +8,8 @@ const props = defineProps({ const items = computed(() => [ { key: 'total_users', label: '总用户数' }, - { key: 'approved_users', label: '已审核' }, - { key: 'pending_users', label: '待审核' }, + { key: 'new_users_today', label: '今日注册' }, + { key: 'new_users_7d', label: '近7天注册' }, { key: 'total_accounts', label: '总账号数' }, { key: 'vip_users', label: 'VIP用户' }, ]) @@ -49,4 +49,3 @@ const items = computed(() => [ color: var(--app-muted); } - diff --git a/admin-frontend/src/layouts/AdminLayout.vue b/admin-frontend/src/layouts/AdminLayout.vue index e3e9934..64824c6 100644 --- a/admin-frontend/src/layouts/AdminLayout.vue +++ b/admin-frontend/src/layouts/AdminLayout.vue @@ -103,8 +103,8 @@ onBeforeUnmount(() => { }) const menuItems = [ - { path: '/pending', label: '待审核', icon: Document, badgeKey: 'pending' }, - { path: '/users', label: '用户', icon: User }, + { path: '/reports', label: '报表', icon: Document }, + { path: '/users', label: '用户', icon: User, badgeKey: 'resets' }, { path: '/feedbacks', label: '反馈', icon: ChatLineSquare, badgeKey: 'feedbacks' }, { path: '/stats', label: '统计', icon: DataAnalysis }, { path: '/logs', label: '任务日志', icon: List }, @@ -118,9 +118,7 @@ const activeMenu = computed(() => route.path) function badgeFor(item) { if (!item?.badgeKey) return 0 - if (item.badgeKey === 'pending') { - return Number(stats.value?.pending_users || 0) + Number(pendingResetsCount.value || 0) - } + if (item.badgeKey === 'resets') return Number(pendingResetsCount.value || 0) if (item.badgeKey === 'feedbacks') { return Number(pendingFeedbackCount.value || 0) } diff --git a/admin-frontend/src/pages/PendingPage.vue b/admin-frontend/src/pages/PendingPage.vue deleted file mode 100644 index 5d6df87..0000000 --- a/admin-frontend/src/pages/PendingPage.vue +++ /dev/null @@ -1,228 +0,0 @@ - - - - - diff --git a/admin-frontend/src/pages/ReportPage.vue b/admin-frontend/src/pages/ReportPage.vue new file mode 100644 index 0000000..477dc70 --- /dev/null +++ b/admin-frontend/src/pages/ReportPage.vue @@ -0,0 +1,687 @@ + + + + + diff --git a/admin-frontend/src/pages/SystemPage.vue b/admin-frontend/src/pages/SystemPage.vue index 1f43feb..abbe044 100644 --- a/admin-frontend/src/pages/SystemPage.vue +++ b/admin-frontend/src/pages/SystemPage.vue @@ -295,7 +295,7 @@ async function saveAutoApprove() { try { const res = await updateSystemConfig(payload) - ElMessage.success(res?.message || '自动审核配置已保存') + ElMessage.success(res?.message || '注册设置已保存') } catch { // handled by interceptor } @@ -438,12 +438,12 @@ onBeforeUnmount(stopUpdatePolling) -

注册自动审核

+

注册设置

- + -
开启后,新用户注册将自动通过审核,无需管理员手动审批。
+
开启后,新用户注册成功后将赠送下方设置的VIP天数(注册已默认无需审核)。
@@ -455,7 +455,7 @@ onBeforeUnmount(stopUpdatePolling)
- 保存自动审核配置 + 保存注册设置
diff --git a/admin-frontend/src/pages/UsersPage.vue b/admin-frontend/src/pages/UsersPage.vue index 24592f3..56c2693 100644 --- a/admin-frontend/src/pages/UsersPage.vue +++ b/admin-frontend/src/pages/UsersPage.vue @@ -4,21 +4,26 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { adminResetUserPassword, - approveUser, deleteUser, fetchAllUsers, + approveUser, rejectUser, removeUserVip, setUserVip, } from '../api/users' +import { approvePasswordReset, fetchPasswordResets, rejectPasswordReset } from '../api/passwordResets' import { parseSqliteDateTime } from '../utils/datetime' import { validatePasswordStrength } from '../utils/password' const refreshStats = inject('refreshStats', null) +const refreshNavBadges = inject('refreshNavBadges', null) const loading = ref(false) const users = ref([]) +const resetLoading = ref(false) +const passwordResets = ref([]) + function isVip(user) { const expire = user?.vip_expire_time if (!expire) return false @@ -38,9 +43,8 @@ function vipLabel(user) { } function statusMeta(status) { - if (status === 'approved') return { label: '已通过', type: 'success' } - if (status === 'rejected') return { label: '已拒绝', type: 'danger' } - return { label: '待审核', type: 'warning' } + if (status === 'rejected') return { label: '禁用', type: 'danger' } + return { label: '正常', type: 'success' } } async function loadUsers() { @@ -54,10 +58,27 @@ async function loadUsers() { } } -async function onApprove(row) { +async function loadResets() { + resetLoading.value = true try { - await ElMessageBox.confirm(`确定通过用户「${row.username}」的注册申请吗?`, '审核通过', { - confirmButtonText: '通过', + const list = await fetchPasswordResets() + passwordResets.value = Array.isArray(list) ? list : [] + } catch { + passwordResets.value = [] + } finally { + resetLoading.value = false + } +} + +async function refreshAll() { + await Promise.all([loadUsers(), loadResets()]) + await refreshNavBadges?.({ pendingResets: passwordResets.value.length }) +} + +async function onEnableUser(row) { + try { + await ElMessageBox.confirm(`确定启用用户「${row.username}」吗?启用后用户可正常登录。`, '启用用户', { + confirmButtonText: '启用', cancelButtonText: '取消', type: 'success', }) @@ -67,7 +88,7 @@ async function onApprove(row) { try { await approveUser(row.id) - ElMessage.success('用户审核通过') + ElMessage.success('用户已启用') await loadUsers() await refreshStats?.() } catch { @@ -75,10 +96,10 @@ async function onApprove(row) { } } -async function onReject(row) { +async function onDisableUser(row) { try { - await ElMessageBox.confirm(`确定拒绝用户「${row.username}」的注册申请吗?`, '拒绝申请', { - confirmButtonText: '拒绝', + await ElMessageBox.confirm(`确定禁用用户「${row.username}」吗?禁用后用户将无法登录。`, '禁用用户', { + confirmButtonText: '禁用', cancelButtonText: '取消', type: 'warning', }) @@ -88,7 +109,7 @@ async function onReject(row) { try { await rejectUser(row.id) - ElMessage.success('已拒绝用户') + ElMessage.success('用户已禁用') await loadUsers() await refreshStats?.() } catch { @@ -96,6 +117,48 @@ async function onReject(row) { } } +async function onApproveReset(row) { + try { + await ElMessageBox.confirm(`确定批准「${row.username}」的密码重置申请吗?`, '批准重置', { + confirmButtonText: '批准', + cancelButtonText: '取消', + type: 'success', + }) + } catch { + return + } + + try { + const res = await approvePasswordReset(row.id) + ElMessage.success(res?.message || '密码重置申请已批准') + await loadResets() + await refreshNavBadges?.({ pendingResets: passwordResets.value.length }) + } catch { + // handled by interceptor + } +} + +async function onRejectReset(row) { + try { + await ElMessageBox.confirm(`确定拒绝「${row.username}」的密码重置申请吗?`, '拒绝重置', { + confirmButtonText: '拒绝', + cancelButtonText: '取消', + type: 'warning', + }) + } catch { + return + } + + try { + const res = await rejectPasswordReset(row.id) + ElMessage.success(res?.message || '密码重置申请已拒绝') + await loadResets() + await refreshNavBadges?.({ pendingResets: passwordResets.value.length }) + } catch { + // handled by interceptor + } +} + async function onDelete(row) { try { await ElMessageBox.confirm( @@ -200,7 +263,7 @@ async function onResetPassword(row) { } } -onMounted(loadUsers) +onMounted(refreshAll)