feat(huya): 协作台账增加 WebSocket 实时同步
- 后端新增进程内事件总线与 /api/huya/external-orders/ws\n- 所有写接口成功后发布轻量失效事件\n- 前端自动重连、静默刷新、断线兜底轮询
This commit is contained in:
@@ -42,9 +42,18 @@ import { message } from '../utils/antdMessage';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { copyTextToClipboard } from '../utils/clipboard';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useCollabRealtime, type CollabRealtimeEvent } from '../hooks/useCollabRealtime';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
function hasTextInputFocus(): boolean {
|
||||
const active = document.activeElement;
|
||||
return (
|
||||
active instanceof HTMLElement &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)
|
||||
);
|
||||
}
|
||||
|
||||
const STATUS_META: Record<HuyaCollabStatus, { label: string; color: string }> = {
|
||||
pending: { label: '待处理', color: 'default' },
|
||||
// 人工核对过「探针读到的绑定角色」与「主页截图」一致
|
||||
@@ -292,6 +301,7 @@ export default function HuyaCollabPage() {
|
||||
const [giftNames, setGiftNames] = useState<string[]>([]);
|
||||
const [shopModalOpen, setShopModalOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<HuyaCollabOrderQuery>({ include_void: false });
|
||||
const [realtimePending, setRealtimePending] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
|
||||
const [detail, setDetail] = useState<HuyaCollabOrder | null>(null);
|
||||
@@ -347,22 +357,24 @@ export default function HuyaCollabPage() {
|
||||
const [ckExport, setCkExport] = useState<HuyaCollabCkExportResult | null>(null);
|
||||
const [ckExportFormat, setCkExportFormat] = useState<'plain' | 'with_order'>('plain');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const load = useCallback(async (options?: { silent?: boolean; withMeta?: boolean }) => {
|
||||
const silent = options?.silent === true;
|
||||
const withMeta = options?.withMeta !== false;
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const [list, nextSummary, nextOwners, nextShops, nextGiftNames] = await Promise.all([
|
||||
huyaApi.listCollabOrders({ ...filters, page, page_size: pageSize }),
|
||||
huyaApi.collabOrdersSummary(),
|
||||
huyaApi.collabOrderOwners(),
|
||||
huyaApi.listCollabShops(),
|
||||
huyaApi.collabOrderGifts(),
|
||||
withMeta ? huyaApi.collabOrderOwners() : Promise.resolve(null),
|
||||
withMeta ? huyaApi.listCollabShops() : Promise.resolve(null),
|
||||
withMeta ? huyaApi.collabOrderGifts() : Promise.resolve(null),
|
||||
]);
|
||||
setRows(list.items);
|
||||
setTotal(list.total);
|
||||
setSummary(nextSummary);
|
||||
setOwners(nextOwners);
|
||||
setShops(nextShops);
|
||||
setGiftNames(nextGiftNames);
|
||||
if (nextOwners) setOwners(nextOwners);
|
||||
if (nextShops) setShops(nextShops);
|
||||
if (nextGiftNames) setGiftNames(nextGiftNames);
|
||||
// 选中是「当前页」的概念:刷新后剔除已经不在这页上的行,避免点到别人的脏 id。
|
||||
setSelectedIds((ids) => ids.filter((id) => list.items.some((row) => row.id === id)));
|
||||
} catch (e: unknown) {
|
||||
@@ -374,6 +386,56 @@ export default function HuyaCollabPage() {
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
const realtimeEnabled = can('huya:collab:view');
|
||||
const collabModalOpen = Boolean(
|
||||
createOpen || detail || statusAction || batchStatusOpen || ckTarget || ckExportOpen || shopModalOpen,
|
||||
);
|
||||
|
||||
const handleRealtimeInvalidate = useCallback((event: CollabRealtimeEvent) => {
|
||||
if (!realtimeEnabled) return;
|
||||
if (collabModalOpen || hasTextInputFocus()) {
|
||||
setRealtimePending(true);
|
||||
return;
|
||||
}
|
||||
void load({ silent: true, withMeta: event.scope !== 'orders' });
|
||||
}, [realtimeEnabled, collabModalOpen, load]);
|
||||
|
||||
const {
|
||||
connected: realtimeConnected,
|
||||
reconnecting: realtimeReconnecting,
|
||||
} = useCollabRealtime({
|
||||
enabled: realtimeEnabled,
|
||||
onInvalidate: handleRealtimeInvalidate,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!realtimePending || collabModalOpen) return undefined;
|
||||
let cancelled = false;
|
||||
const flush = () => {
|
||||
if (cancelled || collabModalOpen || hasTextInputFocus()) return;
|
||||
setRealtimePending(false);
|
||||
void load({ silent: true, withMeta: true });
|
||||
};
|
||||
const timer = window.setTimeout(flush, 300);
|
||||
const handleFocusOut = () => window.setTimeout(flush, 0);
|
||||
window.addEventListener('focusout', handleFocusOut);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
window.removeEventListener('focusout', handleFocusOut);
|
||||
};
|
||||
}, [realtimePending, collabModalOpen, load]);
|
||||
|
||||
// WebSocket 断开时用低频轮询兜底;连接正常时完全不轮询。
|
||||
useEffect(() => {
|
||||
if (!realtimeEnabled || realtimeConnected) return undefined;
|
||||
const timer = window.setInterval(() => {
|
||||
if (document.visibilityState !== 'visible' || collabModalOpen) return;
|
||||
void load({ silent: true, withMeta: false });
|
||||
}, 30_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [realtimeEnabled, realtimeConnected, collabModalOpen, load]);
|
||||
|
||||
const applyUpdated = useCallback((updated: HuyaCollabOrder) => {
|
||||
setRows((list) => list.map((row) => (row.id === updated.id ? updated : row)));
|
||||
setDetail((current) => (current && current.id === updated.id ? updated : current));
|
||||
@@ -1325,6 +1387,14 @@ export default function HuyaCollabPage() {
|
||||
{/* 第二行:动作(固定在这一行;勾选后加「(N)」只是按钮变宽,不会把列表顶下去) */}
|
||||
<Space wrap style={{ width: '100%' }}>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void load()} disabled={loading}>刷新</Button>
|
||||
{realtimeEnabled && (
|
||||
<Tooltip title={realtimeConnected ? '协作台账实时同步已连接' : '实时连接已断开,正在自动重连'}>
|
||||
<Tag color={realtimeConnected ? 'success' : realtimeReconnecting ? 'warning' : 'default'}>
|
||||
{realtimeConnected ? '实时同步' : realtimeReconnecting ? '实时重连中' : '实时离线'}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{realtimePending && <Tag color="processing">有更新待同步</Tag>}
|
||||
{canEdit && (
|
||||
<Button icon={<SafetyCertificateOutlined />} disabled={selectedIds.length === 0 || busy}
|
||||
onClick={() => void verifySelected()}>
|
||||
|
||||
Reference in New Issue
Block a user