diff --git a/admin-frontend/src/api/feedbacks.js b/admin-frontend/src/api/feedbacks.js index 4d6c8ba..92e4605 100644 --- a/admin-frontend/src/api/feedbacks.js +++ b/admin-frontend/src/api/feedbacks.js @@ -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 } - diff --git a/admin-frontend/src/layouts/AdminLayout.vue b/admin-frontend/src/layouts/AdminLayout.vue index bcc0941..8f50e56 100644 --- a/admin-frontend/src/layouts/AdminLayout.vue +++ b/admin-frontend/src/layouts/AdminLayout.vue @@ -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) { - {{ item.label }} + + {{ item.label }} + + {{ item.label }} @@ -133,7 +190,16 @@ async function go(path) { - + + + + @@ -145,7 +211,10 @@ async function go(path) { - {{ item.label }} + + {{ item.label }} + + {{ item.label }} @@ -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) { } } - diff --git a/admin-frontend/src/pages/FeedbacksPage.vue b/admin-frontend/src/pages/FeedbacksPage.vue index b1b2b19..3aacbf6 100644 --- a/admin-frontend/src/pages/FeedbacksPage.vue +++ b/admin-frontend/src/pages/FeedbacksPage.vue @@ -1,9 +1,11 @@ - + +