修复限制兑换显示文案

This commit is contained in:
yml2213
2026-08-01 10:03:02 +08:00
parent 72cf0e5fef
commit 947539f01e
+153 -107
View File
@@ -15,7 +15,6 @@ import {
type DouyuTaskAccountItem, type DouyuTaskAccountItem,
type DouyuTaskItem, type DouyuTaskItem,
} from '../api/modules'; } from '../api/modules';
import RealtimeLogPanel from '../components/RealtimeLogPanel';
import { usePermissions } from '../hooks/usePermissions'; import { usePermissions } from '../hooks/usePermissions';
import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
import { formatTime } from '../utils/time'; import { formatTime } from '../utils/time';
@@ -53,8 +52,6 @@ const ESPORTS_QUICK_ACTIONS = [
{ key: 'donate_esports_firework_gift', icon: <GiftOutlined /> }, { key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
]; ];
const ELITE_BIND_ACTION_KEYS = ['get_bind_qr', 'confirm_bind', 'query_game_name', 'query_change_bind_time'];
const ELITE_QUERY_ACTION_KEYS = ['query_points', 'query_limited_goods', 'query_gold_balance'];
const ELITE_TASK_TYPES = new Set([ const ELITE_TASK_TYPES = new Set([
'get_bind_qr', 'get_bind_qr',
'confirm_bind', 'confirm_bind',
@@ -98,26 +95,6 @@ const STATUS_LABELS: Record<string, string> = {
success: '成功', failed: '失败', error: '异常', stopped: '已停止', success: '成功', failed: '失败', error: '异常', stopped: '已停止',
}; };
const BIND_STATUS_LABELS: Record<string, string> = {
bind_qr_generated: '待扫码',
game_queried: '已识别角色',
game_not_bound: '未绑定',
bind_confirmed: '已绑定',
esports_bind_qr_generated: '电竞待绑定',
esports_role_queried: '已查电竞角色',
esports_bind_ready: '电竞待确认',
esports_role_switching: '电竞换角色',
esports_bind_confirming: '电竞绑定中',
esports_bound: '电竞已绑定',
esports_qr_created: '电竞待开通',
esports_opened: '电竞已开通',
esports_points_queried: '已查电竞积分',
esports_gift_donated: '已赠电竞礼物',
esports_goods_refreshed: '已刷电竞皮肤',
esports_goods_exchanged: '已兑电竞皮肤',
change_time_queried: '已查换绑',
};
function resultText(result: Record<string, unknown> | null | undefined, key: string): string { function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
const value = result?.[key]; const value = result?.[key];
return typeof value === 'string' ? value : ''; return typeof value === 'string' ? value : '';
@@ -224,6 +201,26 @@ function resultNumber(result: Record<string, unknown> | null | undefined, key: s
return null; return null;
} }
function limitedGoodsName(item: unknown): string {
if (!item || typeof item !== 'object') return '';
const record = item as Record<string, unknown>;
const value = record.commodityName ?? record.name;
return typeof value === 'string' ? value : '';
}
function limitedGoodsTaskText(task: DouyuTaskItem | null | undefined): string {
if (!task) return '';
if (task.message?.trim()) return task.message.trim();
const count = resultNumber(task.result, 'limited_count');
if (count == null) return '';
if (count <= 0) return '无限制商品';
const goods = task.result?.limited_goods;
const names = Array.isArray(goods)
? goods.map(limitedGoodsName).filter(Boolean).slice(0, 5)
: [];
return names.length ? `限兑 ${count} 个: ${names.join(', ')}` : `限兑 ${count}`;
}
function savedPositiveInteger(key: string, fallback = 1): number { function savedPositiveInteger(key: string, fallback = 1): number {
const value = Number(localStorage.getItem(key)); const value = Number(localStorage.getItem(key));
return Number.isInteger(value) && value >= 1 ? value : fallback; return Number.isInteger(value) && value >= 1 ? value : fallback;
@@ -322,6 +319,17 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return map; return map;
}, [isEsportsHandbook, tasks]); }, [isEsportsHandbook, tasks]);
const latestLimitedGoodsTaskByAccount = useMemo(() => {
const map = new Map<number, DouyuTaskItem>();
for (const task of tasks) {
if (task.task_type !== 'query_limited_goods') continue;
if (!['success', 'failed', 'error'].includes(task.status)) continue;
const previous = map.get(task.account_id);
if (!previous || task.id > previous.id) map.set(task.account_id, task);
}
return map;
}, [tasks]);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -355,7 +363,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [canConfig]); }, [canConfig, isEsportsHandbook]);
const loadTasks = useCallback(async () => { const loadTasks = useCallback(async () => {
try { try {
@@ -779,6 +787,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
// Account table // Account table
const accountColumns: TableProps<DouyuTaskAccountItem>['columns'] = [ const accountColumns: TableProps<DouyuTaskAccountItem>['columns'] = [
{
title: '#', width: 45,
render: (_, __, index) => <Text type="secondary">{index + 1}</Text>,
},
{ {
title: '账号', dataIndex: 'username', width: 150, title: '账号', dataIndex: 'username', width: 150,
render: (_, record) => ( render: (_, record) => (
@@ -822,6 +834,20 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
title: '鱼翅', dataIndex: 'gold_balance', width: 70, title: '鱼翅', dataIndex: 'gold_balance', width: 70,
render: (v: number | null) => v ?? <Text type="secondary">-</Text>, render: (v: number | null) => v ?? <Text type="secondary">-</Text>,
}, },
{
title: '限制兑换', dataIndex: 'exchange_balance', width: 150, ellipsis: true,
render: (_, record) => {
const latestLimitedTask = latestLimitedGoodsTaskByAccount.get(record.id);
const text = limitedGoodsTaskText(latestLimitedTask);
if (!text) return <Text type="secondary"></Text>;
if (latestLimitedTask?.status !== 'success') {
return <Text type="secondary">{text}</Text>;
}
const count = resultNumber(latestLimitedTask.result, 'limited_count');
if (count === 0 || text === '无限制商品') return <Tag color="green"></Tag>;
return <Text title={text}>{text}</Text>;
},
},
{ {
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130, title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
render: (_, record) => { render: (_, record) => {
@@ -874,30 +900,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return aw - bw; return aw - bw;
}, },
}, },
{
title: '状态', dataIndex: 'bind_status', width: 100,
render: (_, record) => {
const value = isEsportsHandbook ? record.esports_bind_status : record.bind_status;
return value
? <Tag>{BIND_STATUS_LABELS[value] || value}</Tag>
: <Text type="secondary">-</Text>;
},
},
]; ];
// Task table // 任务记录表
const taskColumns: TableProps<DouyuTaskItem>['columns'] = [ const taskColumns: TableProps<DouyuTaskItem>['columns'] = [
{ {
title: '账号', dataIndex: 'account_username', width: 120, title: '账号', dataIndex: 'account_username', width: 130,
render: (_, r) => r.account_nickname || r.account_username || r.account_uid, render: (_, r) => r.account_nickname || r.account_username || r.account_uid,
}, },
{ title: '任务', dataIndex: 'task_type', width: 140, render: (v) => taskTypes[v] || v },
{ {
title: '状态', dataIndex: 'status', width: 80, title: '结果', dataIndex: 'message', width: 300, ellipsis: true,
render: (v) => <Tag color={STATUS_COLORS[v]}>{STATUS_LABELS[v] || v}</Tag>,
},
{
title: '结果', dataIndex: 'message', ellipsis: true,
render: (_, r) => { render: (_, r) => {
if (['prepare_esports_bind', 'get_esports_bind_qr'].includes(r.task_type)) { if (['prepare_esports_bind', 'get_esports_bind_qr'].includes(r.task_type)) {
const roleName = resultText(r.result, 'role_name'); const roleName = resultText(r.result, 'role_name');
@@ -990,8 +1002,13 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
); );
}, },
}, },
{ title: '任务', dataIndex: 'task_type', width: 170, render: (v) => taskTypes[v] || v },
{ {
title: '时间', dataIndex: 'finished_at', width: 150, title: '状态', dataIndex: 'status', width: 80,
render: (v) => <Tag color={STATUS_COLORS[v]}>{STATUS_LABELS[v] || v}</Tag>,
},
{
title: '时间', dataIndex: 'finished_at', width: 170,
render: (_, r) => formatTime(r.finished_at || r.created_at), render: (_, r) => formatTime(r.finished_at || r.created_at),
}, },
]; ];
@@ -1059,10 +1076,73 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
gridTemplateColumns: '1fr 1fr', gridTemplateColumns: '1fr 1fr',
gap: 6, gap: 6,
}; };
const compactOperationGridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 8,
};
// 任务记录表:表头固定、表体内部滚动、分页器固定显示在底部
const taskRecordTableStyle = `
.douyu-task-record-table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.douyu-task-record-table .ant-spin-nested-loading,
.douyu-task-record-table .ant-spin-container {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.douyu-task-record-table .ant-table {
flex: 1;
min-height: 0;
font-size: 12px;
display: flex;
flex-direction: column;
}
.douyu-task-record-table .ant-table-container {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.douyu-task-record-table .ant-table-header {
flex-shrink: 0;
}
.douyu-task-record-table .ant-table-body {
flex: 1;
min-height: 0;
overflow: auto !important;
}
.douyu-task-record-table .ant-table-thead > tr > th,
.douyu-task-record-table .ant-table-tbody > tr > td {
padding: 4px 8px;
line-height: 20px;
}
.douyu-task-record-table .ant-typography {
font-size: 12px;
}
.douyu-task-record-table .ant-tag {
font-size: 11px;
line-height: 18px;
margin-inline-end: 0;
padding-inline: 5px;
}
.douyu-task-record-table .ant-pagination {
flex-shrink: 0;
margin: 0;
padding: 6px 4px 2px;
border-top: 1px solid #f0f0f0;
}
`;
return ( return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Header */} <style>{taskRecordTableStyle}</style>
{/* 头部 */}
<Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}> <Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}>
<div> <div>
<h2 style={{ margin: 0 }}>{handbookTitle}</h2> <h2 style={{ margin: 0 }}>{handbookTitle}</h2>
@@ -1075,19 +1155,19 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</Space> </Space>
</Space> </Space>
{/* Main grid */} {/* 主布局 */}
<div style={{ <div style={{
flex: 1, minHeight: 0, flex: 1, minHeight: 0,
display: 'grid', display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) minmax(320px, 380px)', gridTemplateColumns: 'minmax(0, 1fr) minmax(520px, 560px)',
gap: 12, overflow: 'hidden', gap: 12, overflow: 'hidden',
}}> }}>
{/* Left: accounts + tasks */} {/* 左侧账号 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
<Card <Card
size="small" title="账号" size="small" title="账号"
extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>} extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>}
style={{ flex: '1 1 0', minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }} styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }}
> >
<Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap> <Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
@@ -1108,7 +1188,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
<Table <Table
rowKey="id" rowKey="id"
size="small" size="small"
style={{ flex: 1, minHeight: 0 }} className="douyu-task-record-table"
loading={loading} loading={loading}
rowSelection={{ rowSelection={{
selectedRowKeys: selectedIds, selectedRowKeys: selectedIds,
@@ -1116,8 +1196,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
}} }}
columns={accountColumns} columns={accountColumns}
dataSource={filteredAccounts} dataSource={filteredAccounts}
pagination={{ pageSize: 15, showSizeChanger: false }} pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }}
scroll={{ y: 'max(180px, calc(100vh - 650px))' }}
onRow={(record) => ({ onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e), onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' }, style: { cursor: 'context-menu' },
@@ -1125,7 +1204,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
})} })}
/> />
</Card> </Card>
</div>
{/* 右侧:任务记录(弹性高度) + 操作区(自适应) */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
<Card <Card
size="small" title="任务记录" size="small" title="任务记录"
extra={ extra={
@@ -1134,25 +1216,31 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
<Tag color="error"> {visibleTasks.filter((t) => ['failed', 'error'].includes(t.status)).length}</Tag> <Tag color="error"> {visibleTasks.filter((t) => ['failed', 'error'].includes(t.status)).length}</Tag>
</Space> </Space>
} }
style={{ flex: '0 0 300px', minHeight: 260, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { flex: 1, minHeight: 0, padding: 8, overflow: 'hidden', display: 'flex', flexDirection: 'column' } }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
> >
<Table <Table
rowKey="id" size="small" columns={taskColumns} dataSource={visibleTasks} rowKey="id" size="small" columns={taskColumns} dataSource={visibleTasks}
style={{ flex: 1, minHeight: 0 }} className="douyu-task-record-table"
loading={loading} pagination={{ pageSize: 4, showSizeChanger: false, size: 'small' }} loading={loading}
scroll={{ x: 700, y: 150 }} pagination={{
pageSize: 15,
showSizeChanger: false,
size: 'small',
showLessItems: true,
showTotal: (total) => `${total}`,
}}
tableLayout="fixed"
scroll={{ x: 850 }}
/> />
</Card> </Card>
</div>
{/* Right: operations + logs */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, overflow: 'auto' }}>
<Card <Card
size="small" size="small"
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'} title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
extra={<Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>} extra={<Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>}
styles={{ body: { padding: 8 } }} style={{ flexShrink: 0, display: 'flex', flexDirection: 'column' }}
styles={{ body: { padding: 8, maxHeight: 260, overflow: 'auto' } }}
> >
<Space direction="vertical" style={{ width: '100%' }} size={8}> <Space direction="vertical" style={{ width: '100%' }} size={8}>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
@@ -1168,17 +1256,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</Space.Compact> </Space.Compact>
{isEsportsHandbook ? ( {isEsportsHandbook ? (
<> <div style={compactOperationGridStyle}>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<QrcodeOutlined />
<span></span>
</div>
<div style={actionGridStyle}>
{renderActionButton('prepare_esports_bind', 'primary')}
{renderActionButton('query_esports_game_name')}
</div>
</div>
<div style={sectionStyle}> <div style={sectionStyle}>
<div style={sectionTitleStyle}> <div style={sectionTitleStyle}>
<CreditCardOutlined /> <CreditCardOutlined />
@@ -1186,16 +1264,6 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</div> </div>
{renderActionButton('create_esports_qr', 'primary')} {renderActionButton('create_esports_qr', 'primary')}
</div> </div>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<SearchOutlined />
<span></span>
</div>
<div style={actionGridStyle}>
{renderActionButton('query_esports_points')}
{renderActionButton('query_gold_balance')}
</div>
</div>
<div style={sectionStyle}> <div style={sectionStyle}>
<div style={sectionTitleStyle}> <div style={sectionTitleStyle}>
<ShoppingOutlined /> <ShoppingOutlined />
@@ -1255,29 +1323,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</div> </div>
</Space> </Space>
</div> </div>
</> </div>
) : ( ) : (
<> <div style={compactOperationGridStyle}>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<QrcodeOutlined />
<span></span>
</div>
<div style={actionGridStyle}>
{ELITE_BIND_ACTION_KEYS.map((key) => renderActionButton(key))}
</div>
</div>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<SearchOutlined />
<span></span>
</div>
<div style={actionGridStyle}>
{ELITE_QUERY_ACTION_KEYS.map((key) => renderActionButton(key))}
</div>
</div>
<div style={sectionStyle}> <div style={sectionStyle}>
<div style={sectionTitleStyle}> <div style={sectionTitleStyle}>
<ShoppingOutlined /> <ShoppingOutlined />
@@ -1344,19 +1392,17 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</Space.Compact> </Space.Compact>
</Space> </Space>
</div> </div>
</> </div>
)} )}
<Text type="secondary" style={{ fontSize: 12 }}> <Text type="secondary" style={{ fontSize: 12 }}>
</Text> </Text>
</Space> </Space>
</Card> </Card>
<RealtimeLogPanel logs={logs.logs} connected={logs.connected} height={200} />
</div> </div>
</div> </div>
{/* Context menu */} {/* 右键菜单 */}
{contextMenu && ( {contextMenu && (
<div style={contextMenuStyle} onClick={(e) => e.stopPropagation()}> <div style={contextMenuStyle} onClick={(e) => e.stopPropagation()}>
<Card <Card