diff --git a/src/app/(main)/feedbacks/page.tsx b/src/app/(main)/feedbacks/page.tsx index 4bb89ce..b68d046 100644 --- a/src/app/(main)/feedbacks/page.tsx +++ b/src/app/(main)/feedbacks/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { ColumnsType } from 'antd/es/table'; import type { SorterResult } from 'antd/es/table/interface'; import { @@ -35,6 +35,7 @@ import UserRecordsDrawer from '@/components/UserRecordsDrawer'; const { Text, Paragraph } = Typography; const { RangePicker } = DatePicker; const REWARD_MAX = 10000; +const REVIEW_PAGE_POLL_INTERVAL_MS = 60_000; // 截图是 app-server 的 /media 相对路径,本地由 :8770 提供(NEXT_PUBLIC_MEDIA_BASE);生产同域走 nginx 代理。 const MEDIA_BASE = process.env.NEXT_PUBLIC_MEDIA_BASE || ''; @@ -196,6 +197,10 @@ export default function FeedbacksPage() { const filters: Record = { ...applied, sort_by: sortBy, sort_order: sortOrder }; const { items, total, page, pageSize, loading, onChange: onPageChange, reload } = usePagedList('/admin/api/feedbacks', filters); + const reloadRef = useRef(reload); + useEffect(() => { + reloadRef.current = reload; + }, [reload]); const canReview = canDo(['operator']); const [selectedRowKeys, setSelectedRowKeys] = useState([]); @@ -213,18 +218,40 @@ export default function FeedbacksPage() { }, [items]); const [summary, setSummary] = useState(null); - const loadSummary = async () => { + const loadSummary = useCallback(async () => { try { const { data } = await api.get('/admin/api/feedbacks/summary'); setSummary(data); } catch { /* 统计失败不阻塞列表 */ } - }; - useEffect(() => { - loadSummary(); }, []); + const refreshPageData = useCallback(() => { + void reloadRef.current(); + void loadSummary(); + }, [loadSummary]); + + useEffect(() => { + void loadSummary(); + + const onWindowFocus = () => refreshPageData(); + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') refreshPageData(); + }; + const pollTimer = window.setInterval(() => { + if (document.visibilityState === 'visible') refreshPageData(); + }, REVIEW_PAGE_POLL_INTERVAL_MS); + + window.addEventListener('focus', onWindowFocus); + document.addEventListener('visibilitychange', onVisibilityChange); + return () => { + window.clearInterval(pollTimer); + window.removeEventListener('focus', onWindowFocus); + document.removeEventListener('visibilitychange', onVisibilityChange); + }; + }, [loadSummary, refreshPageData]); + // 审核/查看抽屉(采纳发金币 / 拒绝填原因 + 看该用户历史反馈) const [drawerFb, setDrawerFb] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 61c350f..24160dc 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -37,6 +37,8 @@ import type { WithdrawSummary, } from '@/lib/types'; +const REVIEW_BADGE_POLL_INTERVAL_MS = 60_000; + const { Sider, Header, Content } = Layout; type NavItem = { @@ -162,22 +164,39 @@ export default function MainLayout({ children }: { children: React.ReactNode }) }; const refreshSafely = (key: ReviewBadgeKey) => refresh(key).catch(() => {}); - void Promise.all([ - refreshSafely('/withdraws'), - refreshSafely('/invite-withdraws'), - refreshSafely('/price-reports'), - refreshSafely('/feedbacks'), - ]); + const refreshAll = () => { + void Promise.all([ + refreshSafely('/withdraws'), + refreshSafely('/invite-withdraws'), + refreshSafely('/price-reports'), + refreshSafely('/feedbacks'), + ]); + }; + + refreshAll(); const onReviewCompleted = (event: Event) => { const key = (event as CustomEvent).detail; if (key) void refreshSafely(key); }; - window.addEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); - return () => { - window.removeEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); + const onWindowFocus = () => refreshAll(); + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') refreshAll(); }; - }, [admin]); + const pollTimer = window.setInterval(() => { + if (document.visibilityState === 'visible') refreshAll(); + }, REVIEW_BADGE_POLL_INTERVAL_MS); + + window.addEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); + window.addEventListener('focus', onWindowFocus); + document.addEventListener('visibilitychange', onVisibilityChange); + return () => { + window.clearInterval(pollTimer); + window.removeEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); + window.removeEventListener('focus', onWindowFocus); + document.removeEventListener('visibilitychange', onVisibilityChange); + }; + }, [admin, pathname]); if (!admin) return null; // 守卫期间不闪烁内容 diff --git a/src/app/(main)/price-reports/page.tsx b/src/app/(main)/price-reports/page.tsx index 44d10d1..4ae1180 100644 --- a/src/app/(main)/price-reports/page.tsx +++ b/src/app/(main)/price-reports/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { App, Button, @@ -30,6 +30,7 @@ import type { PriceReport, PriceReportSummary } from '@/lib/types'; import UserRecordsDrawer from '@/components/UserRecordsDrawer'; const { Text } = Typography; +const REVIEW_PAGE_POLL_INTERVAL_MS = 60_000; // 后端 created_at/reviewed_at 存的是北京 wall-clock(naive,同 savings/comparison), // 直接本地解析、不加 Z(否则会差 8h)。 @@ -150,6 +151,10 @@ export default function PriceReportsPage() { const { items, total, page, pageSize, loading, onChange: onPageChange, reload } = usePagedList('/admin/api/price-reports', filters); + const reloadRef = useRef(reload); + useEffect(() => { + reloadRef.current = reload; + }, [reload]); const sortOrderOf = (field: SortField) => sortBy === field ? (sortOrder === 'asc' ? 'ascend' : 'descend') : null; @@ -181,18 +186,40 @@ export default function PriceReportsPage() { setSelectedRowKeys((keys) => keys.filter((id) => visiblePendingIds.has(id))); }, [items]); - const loadSummary = async () => { + const loadSummary = useCallback(async () => { try { const { data } = await api.get('/admin/api/price-reports/summary'); setSummary(data); } catch { /* 统计失败不阻塞列表 */ } - }; - useEffect(() => { - loadSummary(); }, []); + const refreshPageData = useCallback(() => { + void reloadRef.current(); + void loadSummary(); + }, [loadSummary]); + + useEffect(() => { + void loadSummary(); + + const onWindowFocus = () => refreshPageData(); + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') refreshPageData(); + }; + const pollTimer = window.setInterval(() => { + if (document.visibilityState === 'visible') refreshPageData(); + }, REVIEW_PAGE_POLL_INTERVAL_MS); + + window.addEventListener('focus', onWindowFocus); + document.addEventListener('visibilitychange', onVisibilityChange); + return () => { + window.clearInterval(pollTimer); + window.removeEventListener('focus', onWindowFocus); + document.removeEventListener('visibilitychange', onVisibilityChange); + }; + }, [loadSummary, refreshPageData]); + const refreshAfterChange = () => { reload(); loadSummary();