feat(douyu): 新增和平小店查询模块(绑定角色/商品列表/点券余额)
- activity_client: getIframeUrl 签发 code/sig,道聚城 getrole.out/recommend/balance 接口 (2026-08 起需 isCode=1 + authType=delegate + sAnchorId 才能通过 Livelink 校验) - 迁移 0016: accounts xpd_* 字段、douyu_config 小店配置(actAlias/actId/rid)、 douyu_xpd_goods_snapshot 商品快照表 - runner/service/router/schema 注册 query_xpd_role/refresh_xpd_goods/query_xpd_balance 三个任务,任务结果剥离 role.raw 防 openid 泄露 - 前端: 和平小店路由/菜单/DouyuTasksPage 三态/商品下拉/配置弹窗
This commit is contained in:
@@ -32,7 +32,7 @@ import { message } from '../utils/antdMessage';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type HandbookKind = 'elite' | 'esports';
|
||||
type HandbookKind = 'elite' | 'esports' | 'peace';
|
||||
|
||||
const TASK_STATUS_COLORS: Record<string, string> = {
|
||||
planned: 'default',
|
||||
@@ -83,6 +83,12 @@ const ESPORTS_QUICK_ACTIONS = [
|
||||
{ key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
|
||||
];
|
||||
|
||||
const PEACE_QUICK_ACTIONS = [
|
||||
{ key: 'query_xpd_role', icon: <SearchOutlined /> },
|
||||
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
|
||||
{ key: 'query_xpd_balance', icon: <SearchOutlined /> },
|
||||
];
|
||||
|
||||
const ELITE_TASK_TYPES = new Set([
|
||||
'get_bind_qr',
|
||||
'confirm_bind',
|
||||
@@ -115,6 +121,11 @@ const ESPORTS_TASK_TYPES = new Set([
|
||||
'donate_esports_chicken_gift',
|
||||
'donate_esports_firework_gift',
|
||||
]);
|
||||
const PEACE_TASK_TYPES = new Set([
|
||||
'query_xpd_role',
|
||||
'refresh_xpd_goods',
|
||||
'query_xpd_balance',
|
||||
]);
|
||||
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
||||
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
||||
const DOUYU_LAYOUT_MODE_STORAGE_KEY = 'douyu_task_layout_mode';
|
||||
@@ -201,6 +212,13 @@ function goodsLabel(item: DouyuGoodsItem): string {
|
||||
return `${item.name || item.commodity_id}${item.score ? ` / ${item.score}积分` : ''}${stockText}`;
|
||||
}
|
||||
|
||||
function xpdGoodsLabel(item: DouyuGoodsItem): string {
|
||||
const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
|
||||
const price = xpd.price != null ? ` / ${xpd.price}点券` : '';
|
||||
const left = xpd.goods_left != null ? ` / 剩${xpd.goods_left}` : '';
|
||||
return `${item.name || item.commodity_id}${price}${left}`;
|
||||
}
|
||||
|
||||
function formatWaitSeconds(seconds: number | null | undefined): string {
|
||||
if (seconds == null || Number.isNaN(Number(seconds))) return '';
|
||||
const total = Math.max(0, Math.floor(Number(seconds)));
|
||||
@@ -265,13 +283,22 @@ function savedPositiveInteger(key: string, fallback = 1): number {
|
||||
}
|
||||
|
||||
export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
|
||||
const isPeaceHandbook = handbook === 'peace';
|
||||
const isEsportsHandbook = handbook === 'esports';
|
||||
const handbookTitle = isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台';
|
||||
const handbookDescription = isEsportsHandbook
|
||||
? '电竞手册角色绑定与开通任务'
|
||||
: '精英宝典绑定、开通、积分与兑换任务';
|
||||
const activeTaskTypes = isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES;
|
||||
const quickActions = isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS;
|
||||
const handbookTitle = isPeaceHandbook
|
||||
? '和平小店工作台'
|
||||
: (isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台');
|
||||
const handbookDescription = isPeaceHandbook
|
||||
? '和平小店绑定角色、商品与点券余额查询'
|
||||
: (isEsportsHandbook
|
||||
? '电竞手册角色绑定与开通任务'
|
||||
: '精英宝典绑定、开通、积分与兑换任务');
|
||||
const activeTaskTypes = isPeaceHandbook
|
||||
? PEACE_TASK_TYPES
|
||||
: (isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES);
|
||||
const quickActions = isPeaceHandbook
|
||||
? PEACE_QUICK_ACTIONS
|
||||
: (isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS);
|
||||
const { can } = usePermissions();
|
||||
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
|
||||
// 工作台已导入账号 ID(localStorage 持久化,默认空白,用户手动导入/移除)
|
||||
@@ -462,7 +489,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
try {
|
||||
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
|
||||
workbenchIds.length > 0 ? douyuApi.listAccounts({ ids: workbenchIds.join(',') }) : Promise.resolve([]),
|
||||
isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(),
|
||||
isPeaceHandbook
|
||||
? douyuApi.listPeaceGoods()
|
||||
: (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
|
||||
douyuApi.listTasks(),
|
||||
canConfig ? douyuApi.getConfig() : Promise.resolve(null),
|
||||
douyuApi.taskTypes(),
|
||||
@@ -508,7 +537,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canConfig, isEsportsHandbook, workbenchIds]);
|
||||
}, [canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
if (tasksLoadingRef.current) return;
|
||||
@@ -1104,6 +1133,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{
|
||||
title: '游戏名', dataIndex: 'game_name', width: 170,
|
||||
render: (_, record) => {
|
||||
if (isPeaceHandbook) {
|
||||
return record.xpd_game_name
|
||||
? <Text ellipsis title={record.xpd_game_name}>{record.xpd_game_name}</Text>
|
||||
: <Text type="secondary">未查</Text>;
|
||||
}
|
||||
const queryTask = isEsportsHandbook
|
||||
? latestEsportsStateTaskByAccount.get(record.id) || null
|
||||
: latestQueryGameTaskByAccount.get(record.id) || null;
|
||||
@@ -1175,8 +1209,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: isEsportsHandbook ? '电竞积分' : '积分',
|
||||
dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
|
||||
title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
|
||||
dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
|
||||
width: 80,
|
||||
render: (v) => v ?? <Text type="secondary">-</Text>,
|
||||
},
|
||||
@@ -1201,6 +1235,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{
|
||||
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
|
||||
render: (_, record) => {
|
||||
if (isPeaceHandbook) return <Text type="secondary">-</Text>;
|
||||
const fromTask = latestChangeWaitByAccount.get(record.id);
|
||||
const canChangeTime = fromTask?.canChangeTime ?? (
|
||||
isEsportsHandbook ? record.esports_can_change_time : null
|
||||
@@ -1239,6 +1274,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
return <Text type="secondary">{text || '-'}</Text>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
if (isPeaceHandbook) return 0;
|
||||
const aw = isEsportsHandbook
|
||||
? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1
|
||||
: latestChangeWaitByAccount.get(a.id)?.wait
|
||||
@@ -1398,7 +1434,39 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
);
|
||||
const operationBody = (
|
||||
<>
|
||||
{isEsportsHandbook ? (
|
||||
{isPeaceHandbook ? (
|
||||
<div style={compactOperationGridStyle}>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<SearchOutlined />
|
||||
<span>小店查询</span>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||
{renderActionButton('query_xpd_role', 'primary')}
|
||||
{renderActionButton('query_xpd_balance', 'primary')}
|
||||
{renderActionButton('refresh_xpd_goods')}
|
||||
</Space>
|
||||
</div>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<ShoppingOutlined />
|
||||
<span>小店商品</span>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||
<Select
|
||||
size="small"
|
||||
value={selectedGoodsId || undefined}
|
||||
onChange={setSelectedGoodsId}
|
||||
options={goods.map((item) => ({ value: item.commodity_id, label: xpdGoodsLabel(item) }))}
|
||||
placeholder="选择小店商品"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
) : isEsportsHandbook ? (
|
||||
<div style={compactOperationGridStyle}>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
@@ -1690,7 +1758,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
</Card>
|
||||
<Card
|
||||
size="small"
|
||||
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
||||
title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
|
||||
extra={operationExtra}
|
||||
style={{ width: 340, flexShrink: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
styles={{ body: { padding: 8, flex: 1, minHeight: 0, overflowY: 'auto' } }}
|
||||
@@ -1702,7 +1770,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<>
|
||||
<Card
|
||||
size="small"
|
||||
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
||||
title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
|
||||
extra={operationExtra}
|
||||
style={{ flexShrink: 0, marginBottom: 12 }}
|
||||
styles={{ body: { padding: 8 } }}
|
||||
@@ -1788,12 +1856,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{ title: '标签', dataIndex: 'tag', width: 110, render: (v: string) => (v ? <Tag color="blue">{v}</Tag> : '-') },
|
||||
{
|
||||
title: '游戏名',
|
||||
dataIndex: isEsportsHandbook ? 'esports_game_name' : 'game_name',
|
||||
dataIndex: isPeaceHandbook ? 'xpd_game_name' : (isEsportsHandbook ? 'esports_game_name' : 'game_name'),
|
||||
render: (v) => v || '-',
|
||||
},
|
||||
{
|
||||
title: isEsportsHandbook ? '电竞积分' : '积分',
|
||||
dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
|
||||
title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
|
||||
dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
|
||||
render: (v) => v ?? '-',
|
||||
},
|
||||
]}
|
||||
@@ -1804,7 +1872,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
|
||||
{/* Config modal */}
|
||||
<Modal
|
||||
title={isEsportsHandbook ? '电竞手册配置' : '精英宝典配置'}
|
||||
title={isPeaceHandbook ? '和平小店配置' : (isEsportsHandbook ? '电竞手册配置' : '精英宝典配置')}
|
||||
open={configOpen}
|
||||
onCancel={() => setConfigOpen(false)}
|
||||
onOk={() => configFormValues && saveConfig(configFormValues)}
|
||||
@@ -1813,7 +1881,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
>
|
||||
{configFormValues && (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
{(isEsportsHandbook ? [
|
||||
{(isPeaceHandbook ? [
|
||||
{ label: '小店活动代号', key: 'xpd_act_alias' },
|
||||
{ label: '道聚城活动 ID', key: 'xpd_act_id' },
|
||||
{ label: '房间 ID', key: 'xpd_rid' },
|
||||
] : isEsportsHandbook ? [
|
||||
{ label: '电竞手册 manualID', key: 'esports_manual_id' },
|
||||
{ label: '电竞手册活动', key: 'esports_act_alias' },
|
||||
{ label: '房间 ID', key: 'room_id' },
|
||||
@@ -1840,7 +1912,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
/>
|
||||
</Space>
|
||||
))}
|
||||
{(isEsportsHandbook ? [
|
||||
{(isPeaceHandbook ? [] : isEsportsHandbook ? [
|
||||
{ label: '电竞手册金额(分)', key: 'esports_amount' },
|
||||
] : [
|
||||
{ label: '宝典金额(分)', key: 'elite_amount' },
|
||||
|
||||
Reference in New Issue
Block a user