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) {