feat: add xpd purchase records view

This commit is contained in:
yml2213
2026-08-08 21:41:44 +08:00
parent 948e7d7246
commit a342dddcb6
5 changed files with 300 additions and 2 deletions
+30
View File
@@ -1429,6 +1429,35 @@ class DouyuBatchRunner:
{"fragments": fragments, "role": role, "area_id": int(areaid)},
)
def _execute_query_xpd_purchase_records(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""查询和平小店道聚城购买记录。"""
client = self._client(cookie)
embed = client.xpd_embed_query(
act_alias=str(config["xpd_act_alias"]),
rid=str(config["xpd_rid"]),
)
result = client.xpd_purchase_records(
embed_query=embed["query"],
act_id=str(config["xpd_act_id"]),
)
account.xpd_bind_status = "xpd_purchase_records_queried"
account.updated_at = datetime.now(timezone.utc)
total = result.get("total") or len(result.get("records") or [])
self._mark_task(
db,
task,
"success",
f"小店兑换记录 {total}" if total else "暂无小店兑换记录",
result,
)
def _execute_exchange_xpd_goods(
self,
db: Session,
@@ -2821,6 +2850,7 @@ class DouyuBatchRunner:
"refresh_xpd_goods": self._execute_refresh_xpd_goods,
"query_xpd_balance": self._execute_query_xpd_balance,
"query_xpd_fragments": self._execute_query_xpd_fragments,
"query_xpd_purchase_records": self._execute_query_xpd_purchase_records,
"exchange_xpd_goods": self._execute_exchange_xpd_goods,
}.get(task.task_type)
if handler is None:
+1
View File
@@ -44,6 +44,7 @@ SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
"refresh_xpd_goods": "刷新小店商品列表",
"query_xpd_balance": "查询小店点券余额",
"query_xpd_fragments": "查询小店扭蛋碎片",
"query_xpd_purchase_records": "查询小店兑换记录",
"exchange_xpd_goods": "兑换小店商品",
}
+132 -2
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, theme,
Alert, Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, theme,
} from 'antd';
import type { TableProps } from 'antd';
import {
@@ -90,6 +90,7 @@ const PEACE_QUICK_ACTIONS = [
{ key: 'query_xpd_role', icon: <SearchOutlined /> },
{ key: 'query_xpd_balance', icon: <SearchOutlined /> },
{ key: 'query_xpd_fragments', icon: <SearchOutlined /> },
{ key: 'query_xpd_purchase_records', icon: <FieldTimeOutlined /> },
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
{ key: 'exchange_xpd_goods', icon: <ShoppingOutlined /> },
];
@@ -134,6 +135,7 @@ const PEACE_TASK_TYPES = new Set([
'refresh_xpd_goods',
'query_xpd_balance',
'query_xpd_fragments',
'query_xpd_purchase_records',
'exchange_xpd_goods',
]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
@@ -418,6 +420,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const [confirmFailTip, setConfirmFailTip] = useState<{ taskId: number; message: string } | null>(null);
const [confirmFailCountdown, setConfirmFailCountdown] = useState(0);
const [exchangePreview, setExchangePreview] = useState<{ task: DouyuTaskItem; url: string } | null>(null);
const [xpdPurchaseRecordsAccountId, setXpdPurchaseRecordsAccountId] = useState<number | null>(null);
const autoOpenQrReady = useRef(false);
const autoOpenEsportsBindReady = useRef(false);
const autoOpenPayReady = useRef(false);
@@ -566,6 +569,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return map;
}, [visibleTasks]);
const latestXpdPurchaseRecordsTaskByAccount = useMemo(() => {
const map = new Map<number, DouyuTaskItem>();
for (const task of tasks) {
if (task.task_type !== 'query_xpd_purchase_records') 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 () => {
setLoading(true);
try {
@@ -1540,6 +1553,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
);
},
},
...(isPeaceHandbook ? [{
title: '兑换记录', width: 100, align: 'center' as const,
render: (_: unknown, record: DouyuTaskAccountItem) => {
const task = latestXpdPurchaseRecordsTaskByAccount.get(record.id);
const running = task && ['planned', 'pending', 'running'].includes(task.status);
return (
<Button
size="small"
icon={<FieldTimeOutlined />}
loading={running}
onClick={() => {
setXpdPurchaseRecordsAccountId(record.id);
void startTask('query_xpd_purchase_records', [record.id]);
}}
>
</Button>
);
},
}] : []),
];
const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
@@ -1866,7 +1899,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
},
}}
tableLayout="fixed"
scroll={{ x: 1180, y: Math.max(120, accountTableAreaHeight - 80) }}
scroll={{ x: 1280, y: Math.max(120, accountTableAreaHeight - 80) }}
onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' },
@@ -1934,6 +1967,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
}
`;
const xpdPurchaseTask = xpdPurchaseRecordsAccountId == null
? null
: latestXpdPurchaseRecordsTaskByAccount.get(xpdPurchaseRecordsAccountId) || null;
const xpdPurchaseRecords = Array.isArray(xpdPurchaseTask?.result?.records)
? xpdPurchaseTask.result.records.filter(
(item): item is Record<string, unknown> => Boolean(item && typeof item === 'object'),
)
: [];
const xpdPurchaseAccount = accounts.find((account) => account.id === xpdPurchaseRecordsAccountId);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<style>{taskRecordTableStyle}</style>
@@ -2390,6 +2433,93 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
)}
</Modal>
{/* 和平小店兑换记录 Modal */}
<Modal
title={
<Space>
<FieldTimeOutlined />
<span></span>
</Space>
}
open={xpdPurchaseRecordsAccountId != null}
onCancel={() => setXpdPurchaseRecordsAccountId(null)}
footer={null}
width={980}
centered
>
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
{xpdPurchaseAccount?.nickname || xpdPurchaseAccount?.username || `#${xpdPurchaseRecordsAccountId}`}
{xpdPurchaseTask?.result?.total != null ? `,共 ${String(xpdPurchaseTask.result.total)}` : ''}
</Text>
{xpdPurchaseTask && ['failed', 'error'].includes(xpdPurchaseTask.status) ? (
<Alert
type="error"
showIcon
message={xpdPurchaseTask.message || '查询兑换记录失败'}
/>
) : (
<Table
rowKey={(record, index) => String(record.serial || `${record.buy_time || ''}-${index}`)}
size="small"
loading={!xpdPurchaseTask || ['planned', 'pending', 'running'].includes(xpdPurchaseTask.status)}
dataSource={xpdPurchaseRecords}
pagination={{ pageSize: 10, showLessItems: true }}
scroll={{ x: 850 }}
columns={[
{
title: '商品',
dataIndex: 'goods_name',
width: 240,
render: (value: unknown) => String(value || '-'),
},
{
title: '兑换时间',
dataIndex: 'buy_time',
width: 170,
render: (value: unknown) => String(value || '-'),
},
{
title: '支付时间',
dataIndex: 'pay_time',
width: 170,
render: (value: unknown) => String(value || '-'),
},
{
title: '价格',
width: 90,
render: (_: unknown, record: Record<string, unknown>) =>
record.pay_amount ?? record.price ?? '-',
},
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (value: unknown) => (
<Tag color={String(value) === '3' ? 'success' : 'default'}>
{String(value) === '3' ? '已完成' : String(value || '未知')}
</Tag>
),
},
{
title: '角色',
dataIndex: 'role_name',
width: 150,
ellipsis: true,
render: (value: unknown) => String(value || '-'),
},
{
title: '订单号',
dataIndex: 'serial',
width: 250,
ellipsis: true,
render: (value: unknown) => String(value || '-'),
},
]}
locale={{ emptyText: xpdPurchaseTask ? '暂无兑换记录' : '正在查询兑换记录...' }}
/>
)}
</Modal>
{/* 兑换结果图片预览 Modal */}
<Modal
title={