perf(admin): lazy routes and nav badges

This commit is contained in:
2025-12-13 21:13:57 +08:00
parent 235ba28cc8
commit 49bc8b83b1
32 changed files with 328 additions and 68 deletions

View File

@@ -5,6 +5,11 @@ export async function fetchFeedbacks(status = '') {
return data
}
export async function fetchFeedbackStats() {
const { data } = await api.get('/feedbacks', { params: { limit: 1, offset: 0 } })
return data?.stats
}
export async function replyFeedback(feedbackId, reply) {
const { data } = await api.post(`/feedbacks/${feedbackId}/reply`, { reply })
return data
@@ -19,4 +24,3 @@ export async function deleteFeedback(feedbackId) {
const { data } = await api.delete(`/feedbacks/${feedbackId}`)
return data
}

View File

@@ -15,6 +15,8 @@ import {
} from '@element-plus/icons-vue'
import { api } from '../api/client'
import { fetchFeedbackStats } from '../api/feedbacks'
import { fetchPasswordResets } from '../api/passwordResets'
import { fetchSystemStats } from '../api/stats'
import StatsCards from '../components/StatsCards.vue'
@@ -35,8 +37,46 @@ async function refreshStats() {
}
}
const loadingBadges = ref(false)
const pendingResetsCount = ref(0)
const pendingFeedbackCount = ref(0)
let badgeTimer
async function refreshNavBadges(partial = null) {
if (partial && typeof partial === 'object') {
if (Object.prototype.hasOwnProperty.call(partial, 'pendingResets')) {
pendingResetsCount.value = Number(partial.pendingResets || 0)
}
if (Object.prototype.hasOwnProperty.call(partial, 'pendingFeedbacks')) {
pendingFeedbackCount.value = Number(partial.pendingFeedbacks || 0)
}
return
}
if (loadingBadges.value) return
loadingBadges.value = true
try {
const [resetsResult, feedbackResult] = await Promise.allSettled([
fetchPasswordResets(),
fetchFeedbackStats(),
])
if (resetsResult.status === 'fulfilled') {
pendingResetsCount.value = Array.isArray(resetsResult.value) ? resetsResult.value.length : 0
}
if (feedbackResult.status === 'fulfilled') {
pendingFeedbackCount.value = Number(feedbackResult.value?.pending || 0)
}
} finally {
loadingBadges.value = false
}
}
provide('refreshStats', refreshStats)
provide('adminStats', stats)
provide('refreshNavBadges', refreshNavBadges)
const isMobile = ref(false)
const drawerOpen = ref(false)
@@ -53,16 +93,19 @@ onMounted(async () => {
syncIsMobile()
await refreshStats()
await refreshNavBadges()
badgeTimer = window.setInterval(refreshNavBadges, 60_000)
})
onBeforeUnmount(() => {
mediaQuery?.removeEventListener?.('change', syncIsMobile)
window.clearInterval(badgeTimer)
})
const menuItems = [
{ path: '/pending', label: '待审核', icon: Document },
{ path: '/pending', label: '待审核', icon: Document, badgeKey: 'pending' },
{ path: '/users', label: '用户', icon: User },
{ path: '/feedbacks', label: '反馈', icon: ChatLineSquare },
{ path: '/feedbacks', label: '反馈', icon: ChatLineSquare, badgeKey: 'feedbacks' },
{ path: '/stats', label: '统计', icon: DataAnalysis },
{ path: '/logs', label: '任务日志', icon: List },
{ path: '/announcements', label: '公告', icon: Bell },
@@ -73,6 +116,17 @@ const menuItems = [
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 === 'feedbacks') {
return Number(pendingFeedbackCount.value || 0)
}
return 0
}
async function logout() {
try {
await ElMessageBox.confirm('确定退出管理员登录吗?', '退出登录', {
@@ -108,7 +162,10 @@ async function go(path) {
<el-menu :default-active="activeMenu" class="aside-menu" router @select="go">
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
<el-badge v-if="badgeFor(item) > 0" :value="badgeFor(item)" :max="99" class="menu-badge">
<span class="menu-label">{{ item.label }}</span>
</el-badge>
<span v-else class="menu-label">{{ item.label }}</span>
</el-menu-item>
</el-menu>
</el-aside>
@@ -133,7 +190,16 @@ async function go(path) {
<el-main class="layout-main">
<StatsCards :stats="stats" :loading="loadingStats" />
<RouterView />
<Suspense>
<template #default>
<RouterView />
</template>
<template #fallback>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="fallback-card">
<el-skeleton :rows="5" animated />
</el-card>
</template>
</Suspense>
</el-main>
</el-container>
@@ -145,7 +211,10 @@ async function go(path) {
<el-menu :default-active="activeMenu" class="aside-menu" router @select="go">
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
<el-badge v-if="badgeFor(item) > 0" :value="badgeFor(item)" :max="99" class="menu-badge">
<span class="menu-label">{{ item.label }}</span>
</el-badge>
<span v-else class="menu-label">{{ item.label }}</span>
</el-menu-item>
</el-menu>
</el-drawer>
@@ -185,6 +254,22 @@ async function go(path) {
border-right: none;
}
.menu-label {
display: inline-flex;
align-items: center;
min-width: 0;
}
.menu-badge {
display: inline-flex;
align-items: center;
}
.fallback-card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.layout-header {
display: flex;
align-items: center;
@@ -238,4 +323,3 @@ async function go(path) {
}
}
</style>

View File

@@ -1,9 +1,11 @@
<script setup>
import { onMounted, ref } from 'vue'
import { inject, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { closeFeedback, deleteFeedback, fetchFeedbacks, replyFeedback } from '../api/feedbacks'
const refreshNavBadges = inject('refreshNavBadges', null)
const loading = ref(false)
const statusFilter = ref('')
const stats = ref({ total: 0, pending: 0, replied: 0, closed: 0 })
@@ -35,6 +37,8 @@ async function load() {
} finally {
loading.value = false
}
await refreshNavBadges?.({ pendingFeedbacks: stats.value.pending || 0 })
}
async function onReply(row) {
@@ -253,4 +257,3 @@ onMounted(load)
gap: 8px;
}
</style>

View File

@@ -7,6 +7,7 @@ import { approvePasswordReset, fetchPasswordResets, rejectPasswordReset } from '
import { parseSqliteDateTime } from '../utils/datetime'
const refreshStats = inject('refreshStats', null)
const refreshNavBadges = inject('refreshNavBadges', null)
const pendingUsers = ref([])
const passwordResets = ref([])
@@ -46,6 +47,7 @@ async function loadResets() {
async function refreshAll() {
await Promise.all([loadPending(), loadResets()])
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
}
async function onApproveUser(row) {
@@ -105,6 +107,7 @@ async function onApproveReset(row) {
const res = await approvePasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已批准')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}
@@ -125,6 +128,7 @@ async function onRejectReset(row) {
const res = await rejectPasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已拒绝')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}

View File

@@ -2,15 +2,15 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import AdminLayout from '../layouts/AdminLayout.vue'
import PendingPage from '../pages/PendingPage.vue'
import UsersPage from '../pages/UsersPage.vue'
import FeedbacksPage from '../pages/FeedbacksPage.vue'
import StatsPage from '../pages/StatsPage.vue'
import LogsPage from '../pages/LogsPage.vue'
import AnnouncementsPage from '../pages/AnnouncementsPage.vue'
import EmailPage from '../pages/EmailPage.vue'
import SystemPage from '../pages/SystemPage.vue'
import SettingsPage from '../pages/SettingsPage.vue'
const PendingPage = () => import('../pages/PendingPage.vue')
const UsersPage = () => import('../pages/UsersPage.vue')
const FeedbacksPage = () => import('../pages/FeedbacksPage.vue')
const StatsPage = () => import('../pages/StatsPage.vue')
const LogsPage = () => import('../pages/LogsPage.vue')
const AnnouncementsPage = () => import('../pages/AnnouncementsPage.vue')
const EmailPage = () => import('../pages/EmailPage.vue')
const SystemPage = () => import('../pages/SystemPage.vue')
const SettingsPage = () => import('../pages/SettingsPage.vue')
const routes = [
{
@@ -37,4 +37,3 @@ const router = createRouter({
})
export default router

View File

@@ -1,11 +1,155 @@
{
"_datetime-CpkTDmvr.js": {
"file": "assets/datetime-CpkTDmvr.js",
"name": "datetime"
},
"_tasks-BUxA_MMn.js": {
"file": "assets/tasks-BUxA_MMn.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_users-DVl5a2To.js": {
"file": "assets/users-DVl5a2To.js",
"name": "users",
"imports": [
"index.html"
]
},
"index.html": {
"file": "assets/index-D-MDwNCD.js",
"file": "assets/index-CCJGmygT.js",
"name": "index",
"src": "index.html",
"isEntry": true,
"dynamicImports": [
"src/pages/PendingPage.vue",
"src/pages/UsersPage.vue",
"src/pages/FeedbacksPage.vue",
"src/pages/StatsPage.vue",
"src/pages/LogsPage.vue",
"src/pages/AnnouncementsPage.vue",
"src/pages/EmailPage.vue",
"src/pages/SystemPage.vue",
"src/pages/SettingsPage.vue"
],
"css": [
"assets/index-C2CkOw_I.css"
"assets/index-lm5BCraY.css"
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-CXFfpdyD.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html"
],
"css": [
"assets/AnnouncementsPage-CjcC-aWD.css"
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-D5rz9N2M.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html"
],
"css": [
"assets/EmailPage-Dk6eRUoe.css"
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-zx0MksLD.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html"
],
"css": [
"assets/FeedbacksPage-BKNQYWPz.css"
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-DnqHdnu7.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-DVl5a2To.js",
"_tasks-BUxA_MMn.js",
"index.html"
],
"css": [
"assets/LogsPage-R-XyhzDW.css"
]
},
"src/pages/PendingPage.vue": {
"file": "assets/PendingPage-DDGug1ac.js",
"name": "PendingPage",
"src": "src/pages/PendingPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-DVl5a2To.js",
"index.html",
"_datetime-CpkTDmvr.js"
],
"css": [
"assets/PendingPage-C_mZDlzP.css"
]
},
"src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-BNOqaz0O.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html"
],
"css": [
"assets/SettingsPage-DGdwb4W2.css"
]
},
"src/pages/StatsPage.vue": {
"file": "assets/StatsPage-CfWiD1Ty.js",
"name": "StatsPage",
"src": "src/pages/StatsPage.vue",
"isDynamicEntry": true,
"imports": [
"_tasks-BUxA_MMn.js",
"index.html"
],
"css": [
"assets/StatsPage-kYXPdoa5.css"
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-Di4QNzPH.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html"
],
"css": [
"assets/SystemPage-DC1VKbLw.css"
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-zxqUvIyG.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-DVl5a2To.js",
"_datetime-CpkTDmvr.js",
"index.html"
],
"css": [
"assets/UsersPage-D2Xg1a62.css"
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-a7b3418e]{display:flex;flex-direction:column;gap:12px}.card[data-v-a7b3418e]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-a7b3418e]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-a7b3418e]{margin-top:10px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-a7b3418e]{overflow-x:auto}.ellipsis[data-v-a7b3418e]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-a7b3418e]{display:flex;flex-wrap:wrap;gap:8px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-3d6e76c6]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-3d6e76c6]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-3d6e76c6]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-head[data-v-3d6e76c6]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-3d6e76c6]{margin:0;font-size:14px;font-weight:800}.help[data-v-3d6e76c6]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-3d6e76c6]{overflow-x:auto}.stat-card[data-v-3d6e76c6]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-3d6e76c6]{font-size:20px;font-weight:900;line-height:1.1}.stat-label[data-v-3d6e76c6]{margin-top:6px;font-size:12px;color:var(--app-muted)}.ok[data-v-3d6e76c6]{color:#047857}.err[data-v-3d6e76c6]{color:#b91c1c}.sub-stats[data-v-3d6e76c6]{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.ellipsis[data-v-3d6e76c6]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-3d6e76c6]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-3d6e76c6]{font-size:12px}.dialog-actions[data-v-3d6e76c6]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-3d6e76c6]{flex:1}

View File

@@ -0,0 +1 @@
.page-stack[data-v-97c1e509]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-97c1e509]{display:flex;gap:10px;align-items:center}.card[data-v-97c1e509],.stat-card[data-v-97c1e509]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-value[data-v-97c1e509]{font-size:20px;font-weight:800;line-height:1.1}.stat-label[data-v-97c1e509]{margin-top:6px;font-size:12px;color:var(--app-muted)}.warn[data-v-97c1e509]{color:#b45309}.ok[data-v-97c1e509]{color:#047857}.table-wrap[data-v-97c1e509]{overflow-x:auto}.ellipsis[data-v-97c1e509]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-97c1e509]{display:flex;flex-wrap:wrap;gap:8px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-a7a68d16]{display:flex;flex-direction:column;gap:12px}.card[data-v-a7a68d16]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.filters[data-v-a7a68d16]{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.table-wrap[data-v-a7a68d16]{overflow-x:auto}.ellipsis[data-v-a7a68d16]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-a7a68d16]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-a7a68d16]{font-size:12px}

View File

@@ -0,0 +1 @@
.page-stack[data-v-f2aa6820]{display:flex;flex-direction:column;gap:12px}.card[data-v-f2aa6820]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-f2aa6820]{margin:0 0 12px;font-size:14px;font-weight:800}.table-wrap[data-v-f2aa6820]{overflow-x:auto}.user-cell[data-v-f2aa6820]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}

View File

@@ -0,0 +1 @@
import{f as E,a as I,r as A}from"./users-DVl5a2To.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-CCJGmygT.js";import{p as L}from"./datetime-CpkTDmvr.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};

View File

@@ -0,0 +1 @@
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-CCJGmygT.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};

View File

@@ -0,0 +1 @@
.page-stack[data-v-2f4b840f]{display:flex;flex-direction:column;gap:12px}.card[data-v-2f4b840f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-2f4b840f]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-2f4b840f]{margin-top:10px;font-size:12px;color:var(--app-muted)}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-cdfd4595]{display:flex;flex-direction:column;gap:12px}.metric-card[data-v-cdfd4595],.card[data-v-cdfd4595]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.metric-label[data-v-cdfd4595]{font-size:12px;color:var(--app-muted)}.metric-value[data-v-cdfd4595]{margin-top:6px;font-size:18px;font-weight:800}.metric-sub[data-v-cdfd4595]{margin-top:4px;font-size:12px}.section-head[data-v-cdfd4595]{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}.section-title[data-v-cdfd4595]{margin:0;font-size:14px;font-weight:800}.count-row[data-v-cdfd4595]{margin-bottom:10px}.count-card[data-v-cdfd4595]{border-radius:10px;border:1px solid var(--app-border)}.count-card.ok[data-v-cdfd4595]{background:#10b98114}.count-card.warn[data-v-cdfd4595]{background:#f59e0b14}.count-value[data-v-cdfd4595]{font-size:22px;font-weight:900;line-height:1.1}.count-label[data-v-cdfd4595]{margin-top:4px;font-size:12px;color:var(--app-muted)}.sub-title[data-v-cdfd4595]{margin-top:14px;margin-bottom:8px;font-size:13px;font-weight:800}.empty[data-v-cdfd4595]{padding:10px 0}.task-list[data-v-cdfd4595]{display:flex;flex-direction:column;gap:8px}.task-item[data-v-cdfd4595]{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:10px 12px;border-radius:10px;border:1px solid var(--app-border);background:#fff}.task-item.queue[data-v-cdfd4595]{background:#f59e0b0f}.task-line[data-v-cdfd4595]{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.task-line2[data-v-cdfd4595]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:12px}.task-user[data-v-cdfd4595]{font-weight:600}.task-account[data-v-cdfd4595]{font-weight:700;color:#2563eb}.dot[data-v-cdfd4595]{width:8px;height:8px;border-radius:999px;display:inline-block}.task-status[data-v-cdfd4595]{font-weight:700}.task-right[data-v-cdfd4595]{font-size:12px;font-weight:700;color:#10b981;white-space:nowrap}.task-right.warn[data-v-cdfd4595]{color:#f59e0b}.stat-grid[data-v-cdfd4595]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stat-box[data-v-cdfd4595]{border-radius:12px;border:1px solid var(--app-border);padding:12px}.stat-box.ok[data-v-cdfd4595]{background:#10b98114}.stat-box.err[data-v-cdfd4595]{background:#ef444414}.stat-box.info[data-v-cdfd4595]{background:#3b82f614}.stat-box.info2[data-v-cdfd4595]{background:#06b6d414}.stat-name[data-v-cdfd4595]{font-size:12px;font-weight:800;margin-bottom:6px}.stat-row[data-v-cdfd4595]{display:flex;align-items:baseline;gap:8px}.stat-big[data-v-cdfd4595]{font-size:20px;font-weight:900}.stat-row2[data-v-cdfd4595]{margin-top:6px;font-size:12px}

View File

@@ -0,0 +1 @@
.page-stack[data-v-6af756b3]{display:flex;flex-direction:column;gap:12px}.card[data-v-6af756b3]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-6af756b3]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-6af756b3]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-6af756b3]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-e62c5723]{display:flex;flex-direction:column;gap:12px}.card[data-v-e62c5723]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.table-wrap[data-v-e62c5723]{overflow-x:auto}.user-block[data-v-e62c5723]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-e62c5723]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-e62c5723]{font-size:12px}.vip-sub[data-v-e62c5723]{font-size:12px;color:#7c3aed}.actions[data-v-e62c5723]{display:flex;flex-wrap:wrap;gap:8px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
function i(t){if(!t)return null;if(t instanceof Date)return t;const e=String(t),r=e.includes("T")?e:e.replace(" ","T"),n=new Date(r);return Number.isNaN(n.getTime())?null:n}export{i as p};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{B as a}from"./index-CCJGmygT.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{e as a,o as b,r as c,i as d,f as e,c as f};

View File

@@ -0,0 +1 @@
import{B as a}from"./index-CCJGmygT.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};

View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-D-MDwNCD.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C2CkOw_I.css">
<script type="module" crossorigin src="./assets/index-CCJGmygT.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-lm5BCraY.css">
</head>
<body>
<div id="app"></div>