fix(admin): 审核完成后刷新菜单徽标

This commit is contained in:
linkeyu
2026-07-22 09:47:54 +08:00
parent 374ac1fd90
commit c3b77479a0
5 changed files with 55 additions and 39 deletions
+6 -1
View File
@@ -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');
}}
/>
<UserRecordsDrawer<Feedback>
+32 -38
View File
@@ -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<AdminInfo | null>(null);
const [collapsed, setCollapsed] = useState(false);
const [pendingReviewCounts, setPendingReviewCounts] = useState<Partial<Record<ReviewNavKey, number>>>({});
const [pendingReviewCounts, setPendingReviewCounts] = useState<Partial<Record<ReviewBadgeKey, number>>>({});
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<WithdrawSummary>('/admin/api/withdraws/summary')
.then(({ data }) => data.reviewing_count)
.catch(() => null)
: Promise.resolve(null),
canAccess('price-reports')
? api.get<PriceReportSummary>('/admin/api/price-reports/summary')
.then(({ data }) => data.pending)
.catch(() => null)
: Promise.resolve(null),
canAccess('feedbacks')
? api.get<FeedbackSummary>('/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<WithdrawSummary>('/admin/api/withdraws/summary')).data.reviewing_count;
} else if (key === '/price-reports') {
count = (await api.get<PriceReportSummary>('/admin/api/price-reports/summary')).data.pending;
} else {
count = (await api.get<FeedbackSummary>('/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<ReviewBadgeKey>).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 (
<span
+3
View File
@@ -22,6 +22,7 @@ import dayjs from 'dayjs';
import { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth';
import { mediaUrl } from '@/lib/media';
import { refreshReviewBadge } from '@/lib/reviewBadge';
import { usePagedList } from '@/lib/usePagedList';
import type { PriceReport, PriceReportSummary } from '@/lib/types';
import UserRecordsDrawer from '@/components/UserRecordsDrawer';
@@ -202,6 +203,7 @@ export default function PriceReportsPage() {
onOk: async () => {
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('');
+4
View File
@@ -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<WithdrawBulkResult>('/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);
+10
View File
@@ -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<ReviewBadgeKey>(REVIEW_BADGE_REFRESH_EVENT, { detail: key }),
);
}