diff --git a/src/app/(main)/feedbacks/page.tsx b/src/app/(main)/feedbacks/page.tsx index f773e96..f94ba01 100644 --- a/src/app/(main)/feedbacks/page.tsx +++ b/src/app/(main)/feedbacks/page.tsx @@ -20,6 +20,7 @@ import type { Dayjs } from 'dayjs'; import { api } from '@/lib/api'; import { canDo } from '@/lib/auth'; import { formatWallTime } from '@/lib/format'; +import { refreshReviewBadge } from '@/lib/reviewBadge'; import { usePagedList } from '@/lib/usePagedList'; import type { Feedback, FeedbackSummary } from '@/lib/types'; import FeedbackHandleDrawer from './FeedbackHandleDrawer'; @@ -431,7 +432,11 @@ export default function FeedbacksPage() { feedback={drawerFb} open={drawerOpen} onClose={() => setDrawerOpen(false)} - onDone={() => { reload(); loadSummary(); }} + onDone={() => { + reload(); + loadSummary(); + refreshReviewBadge('/feedbacks'); + }} /> diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 7c81eb3..220c923 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -25,6 +25,10 @@ import { import { Avatar, Dropdown, Layout, Tooltip } from 'antd'; import { api } from '@/lib/api'; import { clearAuth, getAdmin, getToken, setAuth } from '@/lib/auth'; +import { + REVIEW_BADGE_REFRESH_EVENT, + type ReviewBadgeKey, +} from '@/lib/reviewBadge'; import type { AdminInfo, FeedbackSummary, @@ -40,8 +44,6 @@ type NavItem = { label: string; }; -type ReviewNavKey = '/withdraws' | '/price-reports' | '/feedbacks'; - type NavGroup = | (NavItem & { children?: never }) | { @@ -103,7 +105,7 @@ export default function MainLayout({ children }: { children: React.ReactNode }) const pathname = usePathname(); const [admin, setAdmin] = useState(null); const [collapsed, setCollapsed] = useState(false); - const [pendingReviewCounts, setPendingReviewCounts] = useState>>({}); + const [pendingReviewCounts, setPendingReviewCounts] = useState>>({}); useEffect(() => { const token = getToken(); @@ -124,46 +126,38 @@ export default function MainLayout({ children }: { children: React.ReactNode }) useEffect(() => { if (!admin) return; - let cancelled = false; const canAccess = (page: string) => admin.role === 'super_admin' || !admin.pages || admin.pages.includes(page); - const refresh = async () => { - const [withdraws, priceReports, feedbacks] = await Promise.all([ - canAccess('withdraws') - ? api.get('/admin/api/withdraws/summary') - .then(({ data }) => data.reviewing_count) - .catch(() => null) - : Promise.resolve(null), - canAccess('price-reports') - ? api.get('/admin/api/price-reports/summary') - .then(({ data }) => data.pending) - .catch(() => null) - : Promise.resolve(null), - canAccess('feedbacks') - ? api.get('/admin/api/feedbacks/summary') - .then(({ data }) => data.pending) - .catch(() => null) - : Promise.resolve(null), - ]); - if (cancelled) return; - setPendingReviewCounts((current) => ({ - ...current, - ...(withdraws == null ? {} : { '/withdraws': withdraws }), - ...(priceReports == null ? {} : { '/price-reports': priceReports }), - ...(feedbacks == null ? {} : { '/feedbacks': feedbacks }), - })); + const refresh = async (key: ReviewBadgeKey) => { + if (!canAccess(key.slice(1))) return; + let count: number; + if (key === '/withdraws') { + count = (await api.get('/admin/api/withdraws/summary')).data.reviewing_count; + } else if (key === '/price-reports') { + count = (await api.get('/admin/api/price-reports/summary')).data.pending; + } else { + count = (await api.get('/admin/api/feedbacks/summary')).data.pending; + } + setPendingReviewCounts((current) => ({ ...current, [key]: count })); }; - void refresh(); - const timer = window.setInterval(refresh, 30_000); - window.addEventListener('focus', refresh); - return () => { - cancelled = true; - window.clearInterval(timer); - window.removeEventListener('focus', refresh); + const refreshSafely = (key: ReviewBadgeKey) => refresh(key).catch(() => {}); + void Promise.all([ + refreshSafely('/withdraws'), + refreshSafely('/price-reports'), + refreshSafely('/feedbacks'), + ]); + + const onReviewCompleted = (event: Event) => { + const key = (event as CustomEvent).detail; + if (key) void refreshSafely(key); }; - }, [admin, pathname]); + window.addEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); + return () => { + window.removeEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); + }; + }, [admin]); if (!admin) return null; // 守卫期间不闪烁内容 @@ -184,7 +178,7 @@ export default function MainLayout({ children }: { children: React.ReactNode }) ); const reviewBadge = (key: string) => { - const count = pendingReviewCounts[key as ReviewNavKey] ?? 0; + const count = pendingReviewCounts[key as ReviewBadgeKey] ?? 0; if (count <= 0) return null; return ( { try { await api.post(`/admin/api/price-reports/${r.id}/approve`); + refreshReviewBadge('/price-reports'); message.success('已通过,已发放 1000 金币'); refreshAfterChange(); } catch (e) { @@ -220,6 +222,7 @@ export default function PriceReportsPage() { } try { await api.post(`/admin/api/price-reports/${rejecting.id}/reject`, { reason }); + refreshReviewBadge('/price-reports'); message.success('已拒绝'); setRejecting(null); setRejectReason(''); diff --git a/src/app/(main)/withdraws/page.tsx b/src/app/(main)/withdraws/page.tsx index 66b46e9..666e757 100644 --- a/src/app/(main)/withdraws/page.tsx +++ b/src/app/(main)/withdraws/page.tsx @@ -39,6 +39,7 @@ import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; import { formatUtcTime, utcDayjs, utcFromNow, yuan } from '@/lib/format'; +import { refreshReviewBadge } from '@/lib/reviewBadge'; import { usePagedList } from '@/lib/usePagedList'; import type { AuditLog, @@ -332,6 +333,7 @@ export default function WithdrawsPage() { onOk: async () => { try { await api.post(`/admin/api/withdraws/${o.out_bill_no}/approve`); + refreshReviewBadge('/withdraws'); message.success('已通过审核并发起打款'); await refreshAfterChange(o.out_bill_no); } catch (e) { @@ -362,6 +364,7 @@ export default function WithdrawsPage() { await api.post(`/admin/api/withdraws/${targets[0].out_bill_no}/reject`, { reason: trimmed }); message.success('已拒绝并退回余额'); } + refreshReviewBadge('/withdraws'); setRejecting(null); setBulkRejecting([]); setRejectReason(''); @@ -400,6 +403,7 @@ export default function WithdrawsPage() { const { data } = await api.post('/admin/api/withdraws/bulk/approve', { out_bill_nos: selectedReviewing.map((item) => item.out_bill_no), }); + refreshReviewBadge('/withdraws'); const text = bulkResultText('批量通过完成', data); if (data.failed) message.warning(text); else message.success(text); diff --git a/src/lib/reviewBadge.ts b/src/lib/reviewBadge.ts new file mode 100644 index 0000000..78e95d4 --- /dev/null +++ b/src/lib/reviewBadge.ts @@ -0,0 +1,10 @@ +export type ReviewBadgeKey = '/withdraws' | '/price-reports' | '/feedbacks'; + +export const REVIEW_BADGE_REFRESH_EVENT = 'review-badge-refresh'; + +export function refreshReviewBadge(key: ReviewBadgeKey) { + if (typeof window === 'undefined') return; + window.dispatchEvent( + new CustomEvent(REVIEW_BADGE_REFRESH_EVENT, { detail: key }), + ); +}