diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index 84c5845..4b6b542 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -1105,3 +1105,44 @@ class DouyuActivityClient: ) info = self._xpd_parse_var(response.text, "banlanceInfo") return {"balance": self._xpd_int(info.get("balance")), "raw": info} + + def xpd_fragments( + self, + *, + embed_query: dict[str, str], + act_id: str, + openid: str, + roleid: str, + plat: str, + areaid: str = "1", + ) -> dict[str, Any]: + """查询和平小店角色扭蛋碎片数量(gamecoin jb2)。""" + params = { + "_jsvar": "jbInfos", + "_service": "pay.idip.gamecoin.get", + "_app_id": "2123", + "acctype": "ttpp", + "areaid": str(areaid), + "eventid": "", + "interwork": "0", + "openid": openid, + "openkey": "openkey", + "partition": "0", + "pay_token": "", + "plat": str(plat), + "plat_pc": "0", + "roleid": roleid, + "_biz_code": "cjm", + "_act_id": act_id, + "coin_type": "1", + "_sid": "6", + } + response = self._request( + "get", + self.XPD_BALANCE_API, + source="查询小店扭蛋碎片", + params=params, + headers=self._xpd_daoju_headers(), + ) + info = self._xpd_parse_var(response.text, "jbInfos") + return {"fragments": self._xpd_int(info.get("jb2")), "raw": info} diff --git a/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py b/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py new file mode 100644 index 0000000..2b8a06d --- /dev/null +++ b/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py @@ -0,0 +1,35 @@ +"""斗鱼和平小店账号新增扭蛋碎片字段 + +Revision ID: 20260807_0017 +Revises: 20260806_0016 +Create Date: 2026-08-07 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "20260807_0017" +down_revision: Union[str, None] = "20260806_0016" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + if not sa.inspect(bind).has_table("accounts"): + return + columns = {column["name"] for column in sa.inspect(bind).get_columns("accounts")} + if "xpd_fragments" not in columns: + op.add_column("accounts", sa.Column("xpd_fragments", sa.Integer(), nullable=True)) + + +def downgrade() -> None: + bind = op.get_bind() + if not sa.inspect(bind).has_table("accounts"): + return + columns = {column["name"] for column in sa.inspect(bind).get_columns("accounts")} + if "xpd_fragments" in columns: + op.drop_column("accounts", "xpd_fragments") diff --git a/web/backend/models.py b/web/backend/models.py index 13140e1..72aca2e 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -76,6 +76,7 @@ class Account(Base): xpd_plat_id = Column(Integer, nullable=True) xpd_area_id = Column(Integer, nullable=True) xpd_balance = Column(Integer, nullable=True) + xpd_fragments = Column(Integer, nullable=True) xpd_bind_status = Column(String(32), default="") created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index 9bae603..a2209e4 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -32,6 +32,7 @@ from ..services.douyu_service import ( cookie_account_ids_query, create_douyu_planned_tasks, douyu_config_value, + douyu_task_payload, ensure_douyu_config, ) @@ -120,104 +121,15 @@ def _account_out(account: Account) -> DouyuTaskAccountOut: xpd_plat_id=account.xpd_plat_id, xpd_area_id=account.xpd_area_id, xpd_balance=account.xpd_balance, + xpd_fragments=account.xpd_fragments, xpd_bind_status=account.xpd_bind_status or "", assigned_to=account.assigned_to, assigned_username=account.assigned_user.username if account.assigned_user else None, ) -def _slim_goods(goods: object) -> object: - """保留兑换图片和列表展示需要的商品小字段。""" - if not isinstance(goods, dict): - return None - return { - key: value - for key, value in goods.items() - if key in {"commodityId", "commodity_id", "commodityName", "name", "webPic", "pic", "score", "status"} - } - - -def _slim_limited_goods(goods: object) -> list[dict[str, object]]: - """限兑列表只返回前几个商品名,避免任务列表携带完整原始数组。""" - if not isinstance(goods, list): - return [] - result: list[dict[str, object]] = [] - for item in goods[:5]: - if not isinstance(item, dict): - continue - result.append({ - key: value - for key, value in item.items() - if key in {"commodityId", "commodity_id", "commodityName", "name"} - }) - return result - - -def _sanitize_task_result(result: dict | None, task_type: str, *, include_detail: bool = False) -> dict | None: - """列表接口剥离原始快照/大数组;详情接口保留完整 result。""" - if not isinstance(result, dict): - return result - if include_detail: - return result - - data = dict(result) - for key in ( - "raw", - "bind_info", - "before_bind_info", - "bind_candidates", - "cooldown_bind_info", - "after_bind_info", - "activity_bind_snapshot", - "points_query", - "exchange_balance_query", - "query_act_aliases", - "records", - ): - data.pop(key, None) - - # 和平小店任务 result 内嵌 role(含 gameOpenId 等),列表接口剥离其原始快照 - role = data.get("role") - if isinstance(role, dict): - data["role"] = {key: value for key, value in role.items() if key != "raw"} - - goods = data.get("goods") - if isinstance(goods, list): - data.pop("goods", None) - elif isinstance(goods, dict): - data["goods"] = _slim_goods(goods) - - if "limited_goods" in data: - data["limited_goods"] = _slim_limited_goods(data.get("limited_goods")) - - if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr", "get_xpd_bind_qr"}: - data.pop("url", None) - if task_type not in {"create_elite_qr", "create_esports_qr", "create_gold_qr"}: - data.pop("pay_url", None) - return data - - def _task_out(task: DouyuTask, *, include_detail: bool = False) -> DouyuTaskOut: - account = task.account - return DouyuTaskOut( - id=task.id, - batch_id=task.batch_id, - account_id=task.account_id, - account_username=account.username if account else "", - account_uid=account.uid if account else "", - account_nickname=account.nickname if account else "", - task_type=task.task_type, - status=task.status or "", - message=task.message or "", - result=_sanitize_task_result( - task.result if isinstance(task.result, dict) else None, - task.task_type or "", - include_detail=include_detail, - ), - created_by=task.created_by, - created_at=task.created_at, - finished_at=task.finished_at, - ) + return DouyuTaskOut(**douyu_task_payload(task, include_detail=include_detail)) def _config_out(config: DouyuConfig) -> DouyuConfigOut: diff --git a/web/backend/schemas.py b/web/backend/schemas.py index c8f741e..ff479a3 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -704,6 +704,7 @@ class DouyuTaskAccountOut(BaseModel): xpd_plat_id: Optional[int] = None xpd_area_id: Optional[int] = None xpd_balance: Optional[int] = None + xpd_fragments: Optional[int] = None xpd_bind_status: str = "" assigned_to: Optional[int] = None assigned_username: Optional[str] = None diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index 0782468..4d7dcb3 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -22,6 +22,7 @@ from .douyu_service import ( douyu_config_value, ensure_douyu_config, latest_success_cookie, + douyu_task_payload, update_account_profile_from_cookie, ) @@ -345,6 +346,22 @@ class DouyuBatchRunner: return "" return f"{role_name}({channel})" if channel else role_name + def _push_task_event(self, task: DouyuTask) -> None: + """向批次 WS 推送任务状态事件(level=task),前端即时更新不依赖轮询。""" + if not self.log_queue or not self.loop: + return + try: + payload = douyu_task_payload(task) + except Exception: + logger.exception("[douyu] 推送任务状态失败: task_id={}", task.id) + return + event = { + "level": "task", + "message": "", + "task": payload, + } + asyncio.run_coroutine_threadsafe(self.log_queue.put(event), self.loop) + def _mark_task( self, db: Session, @@ -359,6 +376,7 @@ class DouyuBatchRunner: task.result = result task.finished_at = datetime.now(timezone.utc) db.commit() + self._push_task_event(task) def _update_task_progress( self, @@ -373,6 +391,7 @@ class DouyuBatchRunner: if result is not None: task.result = result db.commit() + self._push_task_event(task) def _upsert_goods(self, db: Session, goods: list[dict]) -> None: now = datetime.now(timezone.utc) @@ -1334,6 +1353,67 @@ class DouyuBatchRunner: {"balance": balance, "role": role, "area_id": area_id}, ) + def _execute_query_xpd_fragments( + self, + db: Session, + task: DouyuTask, + account: Account, + cookie: str, + config: dict, + ): + """查询和平小店扭蛋碎片数量。 + + 优先现查角色;getrole 受限(Livelink 风控/失效)时回退账号已存角色, + 保证已绑定账号仍可查询。 + """ + client = self._client(cookie) + act_id = str(config["xpd_act_id"]) + embed_query: dict = {} + openid = str(account.xpd_openid or "") + roleid = str(account.xpd_role_id or "") + plat = str(account.xpd_plat_id or "1") + areaid = str(account.xpd_area_id or 1) + role: dict = {} + try: + ctx = self._xpd_role_context(client, config) + embed_query = ctx["embed"]["query"] + role = ctx["role"] if isinstance(ctx.get("role"), dict) else {} + if role.get("role_id"): + role_area = self._xpd_area_id(role, account) + openid = str(role.get("game_open_id") or "") or openid + roleid = str(role.get("role_id") or "") or roleid + plat = str(role.get("plat_id") or "1") or plat + areaid = str(role_area) or areaid + self._apply_xpd_role_to_account(account, role, role_area) + except Exception: + pass + if not openid or not roleid: + self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定") + return + result = client.xpd_fragments( + embed_query=embed_query, + act_id=act_id, + openid=openid, + roleid=roleid, + plat=plat, + areaid=areaid, + ) + fragments = result.get("fragments") + account.xpd_fragments = fragments + account.xpd_bind_status = "xpd_fragments_queried" + account.updated_at = datetime.now(timezone.utc) + db.commit() + if fragments is None: + self._mark_task(db, task, "failed", "未获取到小店扭蛋碎片数量") + return + self._mark_task( + db, + task, + "success", + f"小店扭蛋碎片: {fragments}", + {"fragments": fragments, "role": role, "area_id": int(areaid)}, + ) + def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): client = self._client(cookie) qr_act_alias = self._bind_qr_act_alias(config) @@ -2584,9 +2664,7 @@ class DouyuBatchRunner: if not task or self._stop.is_set(): return account = task.account - task.status = "running" - task.message = "执行中" - worker_db.commit() + self._update_task_progress(worker_db, task, "running", "执行中") with self._counter_lock: self._started += 1 @@ -2631,6 +2709,7 @@ class DouyuBatchRunner: "query_xpd_role": self._execute_query_xpd_role, "refresh_xpd_goods": self._execute_refresh_xpd_goods, "query_xpd_balance": self._execute_query_xpd_balance, + "query_xpd_fragments": self._execute_query_xpd_fragments, }.get(task.task_type) if handler is None: self._mark_task(worker_db, task, "failed", "不支持的任务类型") @@ -2669,6 +2748,8 @@ class DouyuBatchRunner: task.status = "pending" task.message = "等待执行" self.db.commit() + for task in tasks: + self._push_task_event(task) total = len(tasks) with ThreadPoolExecutor(max_workers=self.concurrency) as executor: diff --git a/web/backend/services/douyu_service.py b/web/backend/services/douyu_service.py index 8848b43..80c6f53 100644 --- a/web/backend/services/douyu_service.py +++ b/web/backend/services/douyu_service.py @@ -13,8 +13,7 @@ from core.douyu.cookie_utils import cookie_value from ..models import Account, DouyuConfig, DouyuTask, LoginTask -SUPPORTED_DOUYU_TASK_TYPES = { - "get_bind_qr": "获取绑定二维码", +SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码", "confirm_bind": "确认绑定", "create_elite_qr": "开通精英宝典30", "prepare_esports_bind": "绑定电竞手册角色", @@ -44,11 +43,11 @@ SUPPORTED_DOUYU_TASK_TYPES = { "query_xpd_role": "查询小店绑定角色", "refresh_xpd_goods": "刷新小店商品列表", "query_xpd_balance": "查询小店点券余额", + "query_xpd_fragments": "查询小店扭蛋碎片", } -DOUYU_CONFIG_DEFAULTS = { - "manual_id": "G4KA4Qnz4LDp7", +DOUYU_CONFIG_DEFAULTS = { "manual_id": "G4KA4Qnz4LDp7", "rid": "9263298", "bind_act_alias": "20260120QYOOB", "confirm_act_alias": "20260120QYOOB", @@ -223,3 +222,121 @@ def cleanup_orphan_douyu_tasks( task.finished_at = now db.commit() return len(tasks) + + +def slim_douyu_goods(goods: object) -> object: + """保留兑换图片和列表展示需要的商品小字段。""" + if not isinstance(goods, dict): + return None + return { + key: value + for key, value in goods.items() + if key in {"commodityId", "commodity_id", "commodityName", "name", "webPic", "pic", "score", "status"} + } + + +def slim_douyu_limited_goods(goods: object) -> list[dict[str, object]]: + """限兑列表只返回前几个商品名,避免任务列表携带完整原始数组。""" + if not isinstance(goods, list): + return [] + result: list[dict[str, object]] = [] + for item in goods[:5]: + if not isinstance(item, dict): + continue + result.append({ + key: value + for key, value in item.items() + if key in {"commodityId", "commodity_id", "commodityName", "name"} + }) + return result + + +def strip_douyu_raw_snapshots(value: object) -> object: + """递归移除任务结果中的原始接口快照。""" + if isinstance(value, dict): + return { + key: strip_douyu_raw_snapshots(item) + for key, item in value.items() + if key != "raw" + } + if isinstance(value, list): + return [strip_douyu_raw_snapshots(item) for item in value] + return value + + +def sanitize_douyu_task_result(result: dict | None, task_type: str, *, include_detail: bool = False) -> dict | None: + """列表/实时推送接口剥离原始快照与大数组;详情接口保留完整 result。""" + if not isinstance(result, dict): + return result + if include_detail: + return result + + data = strip_douyu_raw_snapshots(result) + if not isinstance(data, dict): + return None + for key in ( + "bind_info", + "before_bind_info", + "bind_candidates", + "cooldown_bind_info", + "after_bind_info", + "activity_bind_snapshot", + "activity_bind_info", + "points_query", + "exchange_balance_query", + "query_act_aliases", + "records", + ): + data.pop(key, None) + + # 和平小店任务 result 内嵌 role(含 gameOpenId 等),列表接口剥离其原始快照 + role = data.get("role") + if isinstance(role, dict): + data["role"] = strip_douyu_raw_snapshots(role) + + goods = data.get("goods") + if isinstance(goods, list): + data.pop("goods", None) + elif isinstance(goods, dict): + data["goods"] = slim_douyu_goods(goods) + + if "limited_goods" in data: + data["limited_goods"] = slim_douyu_limited_goods(data.get("limited_goods")) + + if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr", "get_xpd_bind_qr"}: + data.pop("url", None) + if task_type not in {"create_elite_qr", "create_esports_qr", "create_gold_qr"}: + data.pop("pay_url", None) + return data + + +def douyu_task_payload(task: DouyuTask, *, include_detail: bool = False) -> dict: + """生成 REST 与 WebSocket 共用的斗鱼任务载荷。""" + account = task.account + + def isoformat(value: datetime | None) -> str | None: + if value is None: + return None + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + return value.isoformat() + + return { + "id": task.id, + "batch_id": task.batch_id, + "account_id": task.account_id, + "account_username": (account.username if account else "") or "", + "account_uid": (account.uid if account else "") or "", + "account_nickname": (account.nickname if account else "") or "", + "task_type": task.task_type or "", + "status": task.status or "", + "message": task.message or "", + "result": sanitize_douyu_task_result( + task.result if isinstance(task.result, dict) else None, + task.task_type or "", + include_detail=include_detail, + ), + "created_by": task.created_by, + "created_at": isoformat(task.created_at), + "finished_at": isoformat(task.finished_at), + } diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index d54103c..2480cdb 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -233,6 +233,7 @@ export interface DouyuTaskAccountItem { xpd_plat_id: number | null; xpd_area_id: number | null; xpd_balance: number | null; + xpd_fragments: number | null; xpd_bind_status: string; assigned_to: number | null; assigned_username: string | null; diff --git a/web/frontend/src/hooks/useWebSocketLogs.ts b/web/frontend/src/hooks/useWebSocketLogs.ts index 40d2668..e657692 100644 --- a/web/frontend/src/hooks/useWebSocketLogs.ts +++ b/web/frontend/src/hooks/useWebSocketLogs.ts @@ -10,6 +10,7 @@ interface ConnectOptions { onClose?: () => void; onError?: () => void; onResult?: () => void; + onTask?: (task: Record) => void; } const MAX_LOGS = 1000; @@ -76,8 +77,13 @@ export function useWebSocketLogs() { ws.onmessage = (event) => { try { - const msg = JSON.parse(event.data) as RealtimeLog; + const msg = JSON.parse(event.data) as RealtimeLog & { task?: Record }; if (msg.level === 'heartbeat') return; + if (msg.level === 'task') { + // 任务状态即时推送(不进日志),前端直接局部更新 + if (msg.task) callbacksMapRef.current.get(key)?.onTask?.(msg.task); + return; + } if (msg.level === 'result') { callbacksMapRef.current.get(key)?.onResult?.(); return; diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index 674a007..754965c 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -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, + Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, theme, } from 'antd'; import type { TableProps } from 'antd'; import { @@ -88,8 +88,9 @@ const PEACE_QUICK_ACTIONS = [ { key: 'query_xpd_bind_info', icon: }, { key: 'confirm_xpd_bind', icon: }, { key: 'query_xpd_role', icon: }, - { key: 'refresh_xpd_goods', icon: }, { key: 'query_xpd_balance', icon: }, + { key: 'query_xpd_fragments', icon: }, + { key: 'refresh_xpd_goods', icon: }, ]; const ELITE_TASK_TYPES = new Set([ @@ -131,6 +132,7 @@ const PEACE_TASK_TYPES = new Set([ 'query_xpd_role', 'refresh_xpd_goods', 'query_xpd_balance', + 'query_xpd_fragments', ]); const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; @@ -314,6 +316,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) ? PEACE_QUICK_ACTIONS : (isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS); const { can } = usePermissions(); + const { token } = theme.useToken(); const [accounts, setAccounts] = useState([]); // 工作台已导入账号 ID(localStorage 持久化,默认空白,用户手动导入/移除) const [workbenchIds, setWorkbenchIds] = useState(() => { @@ -656,9 +659,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) const selectedHasRunning = selectedIds.some((id) => runningAccountIds.has(id)); - // 有活跃任务或 WS 连接时 3 秒刷 tasks;空闲 15 秒轻量刷新 + // 任务状态由 WS(level=task) 即时推送;轮询仅作兜底(多标签页/断线重连), + // 有活跃任务或 WS 连接时 5 秒一次,空闲 30 秒轻量刷新 useEffect(() => { - const intervalMs = hasActiveTasks || logs.connected || runningBatchIds.size > 0 ? 3000 : 15000; + const intervalMs = hasActiveTasks || logs.connected || runningBatchIds.size > 0 ? 5000 : 30000; const timer = setInterval(loadTasks, intervalMs); return () => clearInterval(timer); }, [hasActiveTasks, loadTasks, logs.connected, runningBatchIds]); @@ -870,6 +874,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) }); void loadData(); }, + onTask: (rawTask) => { + const task = rawTask as unknown as DouyuTaskItem; + // 已有任务原位更新保持排序;新任务插入头部(与后端 id desc 一致) + setTasks((prev) => { + if (prev.some((t) => t.id === task.id)) { + return prev.map((t) => (t.id === task.id ? task : t)); + } + return [task, ...prev].slice(0, 100); + }); + }, }); message.success(`已创建 ${result.count} 个任务`); setTimeout(() => { void loadTasks(); }, 400); @@ -1272,78 +1286,84 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) width: 80, render: (v) => v ?? -, }, - { - title: '鱼翅', dataIndex: 'gold_balance', width: 70, - render: (v: number | null) => v ?? -, - }, - { - title: '限制兑换', dataIndex: 'exchange_balance', width: 150, ellipsis: true, - render: (_, record) => { - const latestLimitedTask = latestLimitedGoodsTaskByAccount.get(record.id); - const text = limitedGoodsTaskText(latestLimitedTask); - if (!text) return 未查; - if (latestLimitedTask?.status !== 'success') { - return {text}; - } - const count = resultNumber(latestLimitedTask.result, 'limited_count'); - if (count === 0 || text === '无限制商品') return 无限制商品; - return {text}; - }, - }, - { - title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130, - render: (_, record) => { - if (isPeaceHandbook) return -; - const fromTask = latestChangeWaitByAccount.get(record.id); - const canChangeTime = fromTask?.canChangeTime ?? ( - isEsportsHandbook ? record.esports_can_change_time : null - ); - const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null; - if (esportsStatus) { - return esportsStatus.available - ? 可换绑 - : ( - - 暂不可换绑 - {esportsStatus.text} - + // 和平小店无鱼翅/限制兑换/换绑时间概念(可无限制换绑),展示扭蛋碎片 + ...(isPeaceHandbook + ? [{ + title: '扭蛋碎片', dataIndex: 'xpd_fragments', width: 90, + render: (v: number | null) => v ?? -, + }] as NonNullable['columns']> + : [ + { + title: '鱼翅', dataIndex: 'gold_balance', width: 70, + render: (v: number | null) => v ?? -, + }, + { + title: '限制兑换', dataIndex: 'exchange_balance', width: 150, ellipsis: true, + render: (_, record) => { + const latestLimitedTask = latestLimitedGoodsTaskByAccount.get(record.id); + const text = limitedGoodsTaskText(latestLimitedTask); + if (!text) return 未查; + if (latestLimitedTask?.status !== 'success') { + return {text}; + } + const count = resultNumber(latestLimitedTask.result, 'limited_count'); + if (count === 0 || text === '无限制商品') return 无限制商品; + return {text}; + }, + }, + { + title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130, + render: (_, record) => { + const fromTask = latestChangeWaitByAccount.get(record.id); + const canChangeTime = fromTask?.canChangeTime ?? ( + isEsportsHandbook ? record.esports_can_change_time : null ); - } - const wait = fromTask?.wait ?? ( - isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time - ); - const text = fromTask?.text || formatWaitSeconds(wait); - if (wait == null && !text) { - return 未查; - } - if (wait != null && wait <= 0) { - return 可换绑; - } - if (wait != null && wait > 0) { - return ( - - 冷却中 - {text || formatWaitSeconds(wait)} - - ); - } - // 接口没返回数字但有文案 - if (text === '可换绑') return 可换绑; - return {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 - ?? (isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time) ?? -1; - const bw = isEsportsHandbook - ? latestChangeWaitByAccount.get(b.id)?.canChangeTime ?? b.esports_can_change_time ?? -1 - : latestChangeWaitByAccount.get(b.id)?.wait - ?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1; - return aw - bw; - }, - }, + const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null; + if (esportsStatus) { + return esportsStatus.available + ? 可换绑 + : ( + + 暂不可换绑 + {esportsStatus.text} + + ); + } + const wait = fromTask?.wait ?? ( + isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time + ); + const text = fromTask?.text || formatWaitSeconds(wait); + if (wait == null && !text) { + return 未查; + } + if (wait != null && wait <= 0) { + return 可换绑; + } + if (wait != null && wait > 0) { + return ( + + 冷却中 + {text || formatWaitSeconds(wait)} + + ); + } + // 接口没返回数字但有文案 + if (text === '可换绑') return 可换绑; + return {text || '-'}; + }, + sorter: (a, b) => { + const aw = isEsportsHandbook + ? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1 + : latestChangeWaitByAccount.get(a.id)?.wait + ?? (isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time) ?? -1; + const bw = isEsportsHandbook + ? latestChangeWaitByAccount.get(b.id)?.canChangeTime ?? b.esports_can_change_time ?? -1 + : latestChangeWaitByAccount.get(b.id)?.wait + ?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1; + return aw - bw; + }, + }, + ] as NonNullable['columns']>), { title: '最近操作', width: 220, render: (_, record) => { @@ -1453,10 +1473,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) ); }; const sectionStyle = { - border: '1px solid #f0f0f0', + border: `1px solid ${token.colorBorderSecondary}`, borderRadius: 6, padding: 8, - background: '#fff', + background: token.colorBgContainer, }; const sectionTitleStyle = { display: 'flex', @@ -1506,6 +1526,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {renderActionButton('confirm_xpd_bind', 'primary')} {renderActionButton('query_xpd_role', 'primary')} {renderActionButton('query_xpd_balance', 'primary')} + {renderActionButton('query_xpd_fragments', 'primary')} {renderActionButton('refresh_xpd_goods')} @@ -1781,7 +1802,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) flex-shrink: 0; margin: 0; padding: 6px 4px 2px; - border-top: 1px solid #f0f0f0; + border-top: 1px solid ${token.colorBorderSecondary}; } `; @@ -2247,7 +2268,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) 兑换结果 {exchangePreview.task.account_nickname