Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 497ea36346 | |||
| c3b2aabc4d | |||
| 52acff8243 | |||
| 8c5e6bcef8 |
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { SorterResult } from 'antd/es/table/interface';
|
import type { SorterResult } from 'antd/es/table/interface';
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +35,7 @@ import UserRecordsDrawer from '@/components/UserRecordsDrawer';
|
|||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
const REWARD_MAX = 10000;
|
const REWARD_MAX = 10000;
|
||||||
|
const REVIEW_PAGE_POLL_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
// 截图是 app-server 的 /media 相对路径,本地由 :8770 提供(NEXT_PUBLIC_MEDIA_BASE);生产同域走 nginx 代理。
|
// 截图是 app-server 的 /media 相对路径,本地由 :8770 提供(NEXT_PUBLIC_MEDIA_BASE);生产同域走 nginx 代理。
|
||||||
const MEDIA_BASE = process.env.NEXT_PUBLIC_MEDIA_BASE || '';
|
const MEDIA_BASE = process.env.NEXT_PUBLIC_MEDIA_BASE || '';
|
||||||
@@ -196,6 +197,10 @@ export default function FeedbacksPage() {
|
|||||||
const filters: Record<string, unknown> = { ...applied, sort_by: sortBy, sort_order: sortOrder };
|
const filters: Record<string, unknown> = { ...applied, sort_by: sortBy, sort_order: sortOrder };
|
||||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||||
usePagedList<Feedback>('/admin/api/feedbacks', filters);
|
usePagedList<Feedback>('/admin/api/feedbacks', filters);
|
||||||
|
const reloadRef = useRef(reload);
|
||||||
|
useEffect(() => {
|
||||||
|
reloadRef.current = reload;
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
const canReview = canDo(['operator']);
|
const canReview = canDo(['operator']);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||||
@@ -213,18 +218,40 @@ export default function FeedbacksPage() {
|
|||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const [summary, setSummary] = useState<FeedbackSummary | null>(null);
|
const [summary, setSummary] = useState<FeedbackSummary | null>(null);
|
||||||
const loadSummary = async () => {
|
const loadSummary = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<FeedbackSummary>('/admin/api/feedbacks/summary');
|
const { data } = await api.get<FeedbackSummary>('/admin/api/feedbacks/summary');
|
||||||
setSummary(data);
|
setSummary(data);
|
||||||
} catch {
|
} 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<Feedback | null>(null);
|
const [drawerFb, setDrawerFb] = useState<Feedback | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|||||||
+29
-10
@@ -37,6 +37,8 @@ import type {
|
|||||||
WithdrawSummary,
|
WithdrawSummary,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
|
|
||||||
|
const REVIEW_BADGE_POLL_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
const { Sider, Header, Content } = Layout;
|
const { Sider, Header, Content } = Layout;
|
||||||
|
|
||||||
type NavItem = {
|
type NavItem = {
|
||||||
@@ -162,22 +164,39 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refreshSafely = (key: ReviewBadgeKey) => refresh(key).catch(() => {});
|
const refreshSafely = (key: ReviewBadgeKey) => refresh(key).catch(() => {});
|
||||||
void Promise.all([
|
const refreshAll = () => {
|
||||||
refreshSafely('/withdraws'),
|
void Promise.all([
|
||||||
refreshSafely('/invite-withdraws'),
|
refreshSafely('/withdraws'),
|
||||||
refreshSafely('/price-reports'),
|
refreshSafely('/invite-withdraws'),
|
||||||
refreshSafely('/feedbacks'),
|
refreshSafely('/price-reports'),
|
||||||
]);
|
refreshSafely('/feedbacks'),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshAll();
|
||||||
|
|
||||||
const onReviewCompleted = (event: Event) => {
|
const onReviewCompleted = (event: Event) => {
|
||||||
const key = (event as CustomEvent<ReviewBadgeKey>).detail;
|
const key = (event as CustomEvent<ReviewBadgeKey>).detail;
|
||||||
if (key) void refreshSafely(key);
|
if (key) void refreshSafely(key);
|
||||||
};
|
};
|
||||||
window.addEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted);
|
const onWindowFocus = () => refreshAll();
|
||||||
return () => {
|
const onVisibilityChange = () => {
|
||||||
window.removeEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted);
|
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; // 守卫期间不闪烁内容
|
if (!admin) return null; // 守卫期间不闪烁内容
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
@@ -30,6 +30,7 @@ import type { PriceReport, PriceReportSummary } from '@/lib/types';
|
|||||||
import UserRecordsDrawer from '@/components/UserRecordsDrawer';
|
import UserRecordsDrawer from '@/components/UserRecordsDrawer';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
const REVIEW_PAGE_POLL_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
// 后端 created_at/reviewed_at 存的是北京 wall-clock(naive,同 savings/comparison),
|
// 后端 created_at/reviewed_at 存的是北京 wall-clock(naive,同 savings/comparison),
|
||||||
// 直接本地解析、不加 Z(否则会差 8h)。
|
// 直接本地解析、不加 Z(否则会差 8h)。
|
||||||
@@ -150,6 +151,10 @@ export default function PriceReportsPage() {
|
|||||||
|
|
||||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||||
usePagedList<PriceReport>('/admin/api/price-reports', filters);
|
usePagedList<PriceReport>('/admin/api/price-reports', filters);
|
||||||
|
const reloadRef = useRef(reload);
|
||||||
|
useEffect(() => {
|
||||||
|
reloadRef.current = reload;
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
const sortOrderOf = (field: SortField) =>
|
const sortOrderOf = (field: SortField) =>
|
||||||
sortBy === field ? (sortOrder === 'asc' ? 'ascend' : 'descend') : null;
|
sortBy === field ? (sortOrder === 'asc' ? 'ascend' : 'descend') : null;
|
||||||
@@ -181,18 +186,40 @@ export default function PriceReportsPage() {
|
|||||||
setSelectedRowKeys((keys) => keys.filter((id) => visiblePendingIds.has(id)));
|
setSelectedRowKeys((keys) => keys.filter((id) => visiblePendingIds.has(id)));
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<PriceReportSummary>('/admin/api/price-reports/summary');
|
const { data } = await api.get<PriceReportSummary>('/admin/api/price-reports/summary');
|
||||||
setSummary(data);
|
setSummary(data);
|
||||||
} catch {
|
} 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 = () => {
|
const refreshAfterChange = () => {
|
||||||
reload();
|
reload();
|
||||||
loadSummary();
|
loadSummary();
|
||||||
|
|||||||
Reference in New Issue
Block a user