feat(weibo): 增加协作 CK 失效筛选与实时同步开关

This commit is contained in:
yml
2026-11-12 20:36:11 +08:00
parent aa978a808e
commit f3aab9dabb
6 changed files with 283 additions and 27 deletions
+1
View File
@@ -288,6 +288,7 @@ export interface WeiboCollabVerifyBatchResult {
order_no: string;
success: boolean;
message: string;
ck_status: string;
}>;
}
+169 -22
View File
@@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import {
Alert,
AutoComplete,
Badge,
Button,
Checkbox,
Col,
@@ -17,6 +18,7 @@ import {
Select,
Space,
Statistic,
Switch,
Table,
Tag,
Tooltip,
@@ -92,6 +94,16 @@ const STATUS_ORDER: WeiboCollabStatus[] = [
'void',
];
const REALTIME_PAUSED_STORAGE_KEY = 'weibo.collab.realtime-paused';
function readRealtimePaused(): boolean {
try {
return window.localStorage.getItem(REALTIME_PAUSED_STORAGE_KEY) === '1';
} catch {
return false;
}
}
const MANUAL_STATUS_OPTIONS = STATUS_ORDER.filter(
(item) => item !== 'weidou_error',
).map((value) => ({ value, label: STATUS_META[value].label }));
@@ -147,12 +159,14 @@ export default function WeiboCollabPage() {
const [copyingCk, setCopyingCk] = useState(false);
const [keyword, setKeyword] = useState('');
const [status, setStatus] = useState('');
const [ckStatus, setCkStatus] = useState('');
const [shop, setShop] = useState('');
const [ownerId, setOwnerId] = useState<number | null>(null);
const [goodsName, setGoodsName] = useState('');
const [includeVoid, setIncludeVoid] = useState(false);
const [archiveScope, setArchiveScope] = useState<'active' | 'archived' | 'all'>('active');
const [threshold, setThreshold] = useState(100);
const [realtimePaused, setRealtimePaused] = useState(readRealtimePaused);
const [form] = Form.useForm<OrderFormValues>();
const [editOpen, setEditOpen] = useState(false);
@@ -214,6 +228,7 @@ export default function WeiboCollabPage() {
page_size: pageSize,
archive_scope: archiveScope,
status: status || undefined,
ck_status: ckStatus || undefined,
shop: shop || undefined,
owner_id: ownerId ?? undefined,
goods_name: goodsName || undefined,
@@ -230,6 +245,7 @@ export default function WeiboCollabPage() {
}
}, [
archiveScope,
ckStatus,
goodsName,
includeVoid,
keyword,
@@ -253,12 +269,32 @@ export default function WeiboCollabPage() {
void loadOrders();
}, [loadOrders]);
useCollabRealtime({
enabled: true,
const { connected: realtimeConnected, reconnecting: realtimeReconnecting } =
useCollabRealtime({
enabled: !realtimePaused,
path: '/api/weibo/external-orders/ws',
onInvalidate: () => refreshAll(),
});
const toggleRealtimeSync = (nextEnabled: boolean) => {
const nextPaused = !nextEnabled;
setRealtimePaused(nextPaused);
try {
window.localStorage.setItem(
REALTIME_PAUSED_STORAGE_KEY,
nextPaused ? '1' : '0',
);
} catch {
// localStorage 不可写时仍保留本次会话内的开关状态。
}
if (nextPaused) {
message.info('已暂停实时同步,需要最新数据时请点「刷新」');
return;
}
message.success('已恢复实时同步');
refreshAll();
};
const accountOptions = accounts.map((item) => ({
value: item.id,
label: `${item.nickname || item.uid} · UID ${item.uid}${item.game_role_name ? ` · ${item.game_role_name}` : ''}`,
@@ -466,8 +502,22 @@ export default function WeiboCollabPage() {
if (!ids.length) return;
try {
const result = await weiboCollabApi.verifyBatch(ids);
const invalidCount = result.results.filter(
(item) => item.ck_status === 'invalid',
).length;
if (invalidCount > 0) {
setStatus('');
setCkStatus('invalid');
setPage(1);
void loadBase();
message.warning(`检测到 ${invalidCount} 个 CK 失效,已切换到 CK 失效列表`);
return;
}
if (result.success) message.success(`已校验 ${ids.length} 单`);
else message.warning(`校验完成,${result.results.filter((item) => !item.success).length} 单失败`);
else
message.warning(
`校验完成,${result.results.filter((item) => !item.success).length} 单失败`,
);
refreshAll();
} catch (error) {
message.error(getErrorMessage(error));
@@ -615,6 +665,18 @@ export default function WeiboCollabPage() {
[summary],
);
const applyStatusCard = (nextStatus: string) => {
setStatus(nextStatus);
setCkStatus('');
setPage(1);
};
const applyCkInvalidCard = () => {
setStatus('');
setCkStatus('invalid');
setPage(1);
};
const copySelectedUsernames = async () => {
const selectedOrders = orders.filter((row) => selectedIds.includes(row.id));
const values = Array.from(
@@ -894,26 +956,96 @@ export default function WeiboCollabPage() {
gap: 8,
}}
>
{statusCards.map((item) => (
<button
key={item.key || 'all'}
type="button"
onClick={() => {
setStatus(item.key);
setPage(1);
}}
style={{
border: `1px solid ${status === item.key ? token.colorPrimary : token.colorBorderSecondary}`,
background: status === item.key ? token.colorPrimaryBg : token.colorBgContainer,
borderRadius: 6,
padding: '10px 12px',
textAlign: 'left',
cursor: 'pointer',
}}
{statusCards.map((item) => {
const active = !ckStatus && status === item.key;
return (
<button
key={item.key || 'all'}
type="button"
onClick={() => applyStatusCard(item.key)}
style={{
border: `1px solid ${active ? token.colorPrimary : token.colorBorderSecondary}`,
background: active ? token.colorPrimaryBg : token.colorBgContainer,
borderRadius: 6,
padding: '10px 12px',
textAlign: 'left',
cursor: 'pointer',
}}
>
<Statistic
title={item.label}
value={item.value}
valueStyle={{ fontSize: 20 }}
/>
</button>
);
})}
<button
type="button"
onClick={applyCkInvalidCard}
style={{
border: `1px solid ${ckStatus === 'invalid' ? token.colorPrimary : token.colorBorderSecondary}`,
background:
ckStatus === 'invalid'
? token.colorPrimaryBg
: token.colorBgContainer,
borderRadius: 6,
padding: '10px 12px',
textAlign: 'left',
cursor: 'pointer',
}}
>
<Statistic
title="CK 失效"
value={summary?.ck_invalid ?? 0}
valueStyle={{ fontSize: 20, color: token.colorError }}
/>
</button>
<div
style={{
border: `1px solid ${token.colorBorderSecondary}`,
background: token.colorBgContainer,
borderRadius: 6,
padding: '10px 12px',
}}
>
<Space
size={6}
style={{ width: '100%', justifyContent: 'space-between' }}
>
<Statistic title={item.label} value={item.value} valueStyle={{ fontSize: 20 }} />
</button>
))}
<Text strong style={{ fontSize: 13 }}>
实时同步
</Text>
<Switch
size="small"
checked={!realtimePaused}
onChange={toggleRealtimeSync}
aria-label="实时同步开关"
/>
</Space>
<Badge
status={
realtimePaused
? 'default'
: realtimeConnected
? 'success'
: realtimeReconnecting
? 'warning'
: 'error'
}
text={
<Text type="secondary" style={{ fontSize: 12 }}>
{realtimePaused
? '已暂停,仅手动刷新'
: realtimeConnected
? '已连接,自动刷新'
: realtimeReconnecting
? '重连中'
: '未连接'}
</Text>
}
/>
</div>
</div>
<div
@@ -972,6 +1104,21 @@ export default function WeiboCollabPage() {
...STATUS_ORDER.map((item) => ({ value: item, label: STATUS_META[item].label })),
]}
/>
<Select
value={ckStatus || undefined}
onChange={(value) => {
setCkStatus(value || '');
setPage(1);
}}
allowClear
placeholder="CK 状态"
style={{ width: 130 }}
options={[
{ value: 'valid', label: ACCOUNT_CK_LABELS.valid },
{ value: 'invalid', label: ACCOUNT_CK_LABELS.invalid },
{ value: 'error', label: ACCOUNT_CK_LABELS.error },
]}
/>
<Select
value={archiveScope}
onChange={(value) => {