重构虎牙精英宝典协议与支付状态链路
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
HUYA_DEFAULT_ROOM_PID = "1199650619883"
|
||||
HUYA_DEFAULT_SID = "2203"
|
||||
HUYA_DEFAULT_OUTER_ACT_ID = "9504"
|
||||
HUYA_DEFAULT_OUTER_ACT_ID = "17096"
|
||||
HUYA_DEFAULT_BIND_ACT_ID = "9271"
|
||||
HUYA_DEFAULT_PAY_CHANNEL = "Zfb"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import threading
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
@@ -130,6 +131,7 @@ class HuyaBatchRunnerCore:
|
||||
task.result = result
|
||||
task.finished_at = datetime.now(UTC)
|
||||
worker_db.commit()
|
||||
self._push_task_event(task)
|
||||
|
||||
def _update_task_progress(
|
||||
self,
|
||||
@@ -144,6 +146,34 @@ class HuyaBatchRunnerCore:
|
||||
if result is not None:
|
||||
task.result = result
|
||||
worker_db.commit()
|
||||
self._push_task_event(task)
|
||||
|
||||
def _push_task_event(self, task: HuyaTask) -> None:
|
||||
"""向批次 WS 推送任务状态;二维码图片留给详情接口按需读取。"""
|
||||
if not self.log_queue or not self.loop:
|
||||
return
|
||||
result = copy.deepcopy(task.result) if isinstance(task.result, dict) else None
|
||||
if result and isinstance(result.get("mini_qrcode_image"), str):
|
||||
result.pop("mini_qrcode_image", None)
|
||||
result["has_mini_qrcode"] = True
|
||||
event = {
|
||||
"level": "task",
|
||||
"message": "",
|
||||
"task": {
|
||||
"id": task.id,
|
||||
"batch_id": task.batch_id,
|
||||
"account_id": task.account_id,
|
||||
"task_type": task.task_type,
|
||||
"handbook_scope": getattr(task, "handbook_scope", "legacy") or "legacy",
|
||||
"status": task.status or "",
|
||||
"message": task.message or "",
|
||||
"result": result,
|
||||
"created_by": task.created_by,
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||
"finished_at": task.finished_at.isoformat() if task.finished_at else None,
|
||||
},
|
||||
}
|
||||
asyncio.run_coroutine_threadsafe(self.log_queue.put(event), self.loop)
|
||||
|
||||
|
||||
class HuyaBatchRegistry:
|
||||
|
||||
@@ -58,6 +58,12 @@ class GoodsMixin:
|
||||
return
|
||||
result = response.to_dict()
|
||||
result["act_id"] = act_id
|
||||
act_info = client.get_act_info(act_id=act_id)
|
||||
user_tasks = client.get_act_user_task_detail(uid=uid, cookie=cookie, act_id=act_id)
|
||||
if act_info is not None:
|
||||
result["act_info"] = act_info.to_dict()
|
||||
if user_tasks is not None:
|
||||
result["user_task_detail"] = user_tasks.to_dict()
|
||||
if response.status != 200:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
@@ -324,6 +330,44 @@ class GoodsMixin:
|
||||
client: Any = HuyaHttpClient(
|
||||
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
|
||||
)
|
||||
detail = client.get_act_prize_detail(
|
||||
uid=uid, cookie=cookie, sid=sid_int, pid=product_id
|
||||
)
|
||||
if detail is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙兑换详情接口无响应")
|
||||
return
|
||||
detail_result = detail.to_dict()
|
||||
prize = detail.prize
|
||||
if detail.status != 200 or prize is None:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
detail.msg or f"虎牙兑换详情获取失败: {detail.status}",
|
||||
detail_result,
|
||||
)
|
||||
return
|
||||
now_ts = int(time.time())
|
||||
if prize.isCanExchange == 0:
|
||||
self._mark_task(worker_db, task, "failed", "该商品当前不可兑换", detail_result)
|
||||
return
|
||||
if prize.isShowNum and prize.leftNum <= 0:
|
||||
self._mark_task(worker_db, task, "failed", "该商品库存不足", detail_result)
|
||||
return
|
||||
if prize.exchangeStartTime and now_ts < prize.exchangeStartTime:
|
||||
self._mark_task(worker_db, task, "failed", "该商品尚未开始兑换", detail_result)
|
||||
return
|
||||
if prize.exchangeEndTime and now_ts > prize.exchangeEndTime:
|
||||
self._mark_task(worker_db, task, "failed", "该商品兑换已结束", detail_result)
|
||||
return
|
||||
score = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int)
|
||||
if score is None:
|
||||
self._mark_task(worker_db, task, "error", "兑换前积分查询无响应", detail_result)
|
||||
return
|
||||
if score.status != 200 or score.available_score < prize.newScore:
|
||||
detail_result["score"] = score.to_dict()
|
||||
self._mark_task(worker_db, task, "failed", "可用积分不足", detail_result)
|
||||
return
|
||||
response = client.score_exchange_prize(
|
||||
uid=uid, cookie=cookie, sid=sid_int, pid=product_id
|
||||
)
|
||||
@@ -340,6 +384,7 @@ class GoodsMixin:
|
||||
"scheduled_at": scheduled_at.isoformat() if scheduled_at else "",
|
||||
"executed_at": datetime.now(UTC).isoformat(),
|
||||
"goods": snapshot.raw if snapshot else None,
|
||||
"prize_detail": detail_result,
|
||||
}
|
||||
)
|
||||
if response.status != 200:
|
||||
@@ -352,6 +397,13 @@ class GoodsMixin:
|
||||
)
|
||||
return
|
||||
|
||||
post_score = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int)
|
||||
post_records = client.get_user_prize_records(uid=uid, cookie=cookie, sid=sid_int)
|
||||
if post_score is not None:
|
||||
result["post_exchange_score"] = post_score.to_dict()
|
||||
account.points = post_score.available_score
|
||||
if post_records is not None:
|
||||
result["post_exchange_records"] = post_records.to_dict()
|
||||
account.status = "goods_exchanged"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
message = response.msg or f"兑换成功: {product_name}"
|
||||
|
||||
@@ -86,52 +86,66 @@ class RechargeMixin:
|
||||
guid: str,
|
||||
cookie: str,
|
||||
order_id: int,
|
||||
sid: int,
|
||||
result: dict,
|
||||
) -> tuple[str, dict | None]:
|
||||
deadline = time.time() + HUYA_PAYMENT_POLL_SECONDS
|
||||
order_id_text = str(order_id)
|
||||
last_order = None
|
||||
while not self._stop.is_set() and time.time() < deadline:
|
||||
resp = client.query_user_order_list(
|
||||
uid=uid,
|
||||
guid=guid,
|
||||
cookie=cookie,
|
||||
offset=0,
|
||||
page_size=10,
|
||||
order_type=1,
|
||||
status=0,
|
||||
timeout=10.0,
|
||||
detail = client.order_detail(
|
||||
uid=uid, guid=guid, cookie=cookie, order_id=order_id, timeout=10.0
|
||||
)
|
||||
checked_at = datetime.now(UTC).isoformat()
|
||||
if resp is not None and getattr(resp, "orders", None):
|
||||
for order in resp.orders:
|
||||
if str(getattr(order, "orderId", "")) != order_id_text:
|
||||
continue
|
||||
last_order = order.to_dict()
|
||||
status = int(getattr(order, "orderStatus", 0) or 0)
|
||||
order = getattr(detail, "order", None) if detail is not None else None
|
||||
if order is not None and (
|
||||
not getattr(order, "orderId", 0)
|
||||
or str(getattr(order, "orderId", "")) == order_id_text
|
||||
):
|
||||
last_order = order.to_dict()
|
||||
status = int(getattr(order, "orderStatus", 0) or 0)
|
||||
result.update(
|
||||
{
|
||||
"payment_checked_at": checked_at,
|
||||
"payment_order": last_order,
|
||||
"payment_order_status": status,
|
||||
"payment_order_status_label": self._huya_order_status_label(status),
|
||||
}
|
||||
)
|
||||
if self._is_huya_order_paid(order):
|
||||
result.update(
|
||||
{
|
||||
"payment_checked_at": checked_at,
|
||||
"payment_order": last_order,
|
||||
"payment_order_status": status,
|
||||
"payment_order_status_label": self._huya_order_status_label(
|
||||
status
|
||||
),
|
||||
"payment_status": "paid",
|
||||
"payment_status_label": "已支付",
|
||||
"payment_paid": True,
|
||||
"payment_paid_at": checked_at,
|
||||
}
|
||||
)
|
||||
if self._is_huya_order_paid(order):
|
||||
result.update(
|
||||
{
|
||||
"payment_status": "paid",
|
||||
"payment_status_label": "已支付",
|
||||
"payment_paid": True,
|
||||
"payment_paid_at": checked_at,
|
||||
}
|
||||
)
|
||||
return "paid", last_order
|
||||
break
|
||||
return "paid", last_order
|
||||
else:
|
||||
result["payment_checked_at"] = checked_at
|
||||
# 兼容旧 HTTP 网关不提供 orderDetailV5 的情况。
|
||||
resp = client.query_user_order_list(
|
||||
uid=uid, guid=guid, cookie=cookie, offset=0, page_size=10,
|
||||
order_type=1, status=0, timeout=10.0,
|
||||
)
|
||||
for legacy_order in getattr(resp, "orders", []) if resp else []:
|
||||
if str(getattr(legacy_order, "orderId", "")) != order_id_text:
|
||||
continue
|
||||
last_order = legacy_order.to_dict()
|
||||
status = int(getattr(legacy_order, "orderStatus", 0) or 0)
|
||||
result.update({
|
||||
"payment_checked_at": checked_at,
|
||||
"payment_order": last_order,
|
||||
"payment_order_status": status,
|
||||
"payment_order_status_label": self._huya_order_status_label(status),
|
||||
})
|
||||
if self._is_huya_order_paid(legacy_order):
|
||||
result.update({
|
||||
"payment_status": "paid", "payment_status_label": "已支付",
|
||||
"payment_paid": True, "payment_paid_at": checked_at,
|
||||
})
|
||||
return "paid", last_order
|
||||
result.setdefault("payment_checked_at", checked_at)
|
||||
if self._stop.wait(HUYA_PAYMENT_POLL_INTERVAL):
|
||||
break
|
||||
|
||||
@@ -260,7 +274,7 @@ class RechargeMixin:
|
||||
pid=pid,
|
||||
spu_id=spu_id,
|
||||
sku_id=0,
|
||||
game_id="0",
|
||||
game_id="",
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
)
|
||||
@@ -389,7 +403,7 @@ class RechargeMixin:
|
||||
pid=pid,
|
||||
spu_id=spu_id,
|
||||
sku_id=sku_id or 0,
|
||||
game_id="0",
|
||||
game_id="",
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
)
|
||||
@@ -515,11 +529,18 @@ class RechargeMixin:
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
order_id=order_resp.orderId,
|
||||
sid=self._to_int(config_info.get("sid")) or 2203,
|
||||
result=result,
|
||||
)
|
||||
account.updated_at = datetime.now(UTC)
|
||||
if payment_status == "paid":
|
||||
account.status = "recharge_paid"
|
||||
post_score = client.query_user_score(
|
||||
uid=uid, cookie=cookie, sid=self._to_int(config_info.get("sid")) or 2203
|
||||
)
|
||||
if post_score is not None:
|
||||
result["post_payment_score"] = post_score.to_dict()
|
||||
account.points = post_score.available_score
|
||||
paid_message = f"支付成功: {product_name} x{count} {result['amount_text']}"
|
||||
if payment_order and payment_order.get("pay_time"):
|
||||
paid_message += f",支付时间 {self._format_local_time(int(payment_order['pay_time']) // 1000)}"
|
||||
@@ -534,4 +555,4 @@ class RechargeMixin:
|
||||
|
||||
account.status = "recharge_order_created"
|
||||
timeout_message = f"{message},{result['payment_status_label']}"
|
||||
self._mark_task(worker_db, task, "success", timeout_message, result)
|
||||
self._mark_task(worker_db, task, "timeout", timeout_message, result)
|
||||
|
||||
@@ -57,6 +57,13 @@ def apply_huya_config_defaults(config: HuyaConfig) -> bool:
|
||||
setattr(config, field, HUYA_CONFIG_DEFAULTS[field])
|
||||
changed = True
|
||||
continue
|
||||
if (
|
||||
field == "outer_act_id"
|
||||
and str(getattr(config, field, "") or "").strip() == "9504"
|
||||
):
|
||||
setattr(config, field, HUYA_CONFIG_DEFAULTS[field])
|
||||
changed = True
|
||||
continue
|
||||
normalized = huya_config_value(field, getattr(config, field, None))
|
||||
if getattr(config, field, None) != normalized:
|
||||
setattr(config, field, normalized)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CheckCircleOutlined, ColumnWidthOutlined, GiftOutlined, ImportOutlined, QrcodeOutlined,
|
||||
ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAccountItem, type HuyaConfig, type HuyaGoodsItem, type HuyaTaskItem } from '../api/modules';
|
||||
import { huyaApi, type HuyaAccountItem, type HuyaConfig, type HuyaGoodsItem, type HuyaRechargeGoodsItem, type HuyaTaskItem } from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||
@@ -24,14 +24,15 @@ const TASK_LABELS: Record<string, string> = {
|
||||
query_act_tasks: '读取宝典任务', get_bind_qr: '获取绑定二维码', query_game_name: '查询游戏角色',
|
||||
confirm_bind: '确认绑定角色', query_points: '查询积分', refresh_goods: '刷新商品',
|
||||
exchange_goods: '兑换商品', query_exchange_records: '查询兑换记录', create_recharge_order: '生成开通支付码',
|
||||
refresh_recharge_goods: '刷新开通商品',
|
||||
};
|
||||
const TASK_COLORS: Record<string, string> = {
|
||||
planned: 'default', pending: 'default', running: 'processing', success: 'success',
|
||||
failed: 'error', error: 'error', stopped: 'warning',
|
||||
failed: 'error', error: 'error', stopped: 'warning', timeout: 'warning',
|
||||
};
|
||||
const TASK_STATUS_LABELS: Record<string, string> = {
|
||||
planned: '已计划', pending: '等待中', running: '执行中', success: '成功',
|
||||
failed: '失败', error: '异常', stopped: '已停止',
|
||||
failed: '失败', error: '异常', stopped: '已停止', timeout: '支付超时',
|
||||
};
|
||||
|
||||
function savedInterval(): number {
|
||||
@@ -57,6 +58,8 @@ export default function HuyaElitePage() {
|
||||
const [tasks, setTasks] = useState<HuyaTaskItem[]>([]);
|
||||
const [goods, setGoods] = useState<HuyaGoodsItem[]>([]);
|
||||
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
||||
const [rechargeGoods, setRechargeGoods] = useState<HuyaRechargeGoodsItem[]>([]);
|
||||
const [selectedRechargeSpu, setSelectedRechargeSpu] = useState('hy-5879340');
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
@@ -81,9 +84,9 @@ export default function HuyaElitePage() {
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [poolResult, workbenchResult, goodsResult, taskResult] = await Promise.all([
|
||||
const [poolResult, workbenchResult, goodsResult, rechargeGoodsResult, taskResult] = await Promise.all([
|
||||
huyaApi.listAccounts({ include_cookie: false }), huyaApi.listWorkbenchAccounts(SCOPE),
|
||||
huyaApi.listGoods(), huyaApi.listTasks(undefined, SCOPE),
|
||||
huyaApi.listGoods(), huyaApi.listRechargeGoods(), huyaApi.listTasks(undefined, SCOPE),
|
||||
]);
|
||||
setPool(poolResult);
|
||||
const byId = new Map(poolResult.map((item) => [item.id, item]));
|
||||
@@ -94,6 +97,7 @@ export default function HuyaElitePage() {
|
||||
setAccounts(storedIds.map((id) => byId.get(Number(id))).filter((item): item is HuyaAccountItem => Boolean(item)));
|
||||
setSelectedIds((prev) => prev.filter((id) => storedIds.includes(id)));
|
||||
setGoods(goodsResult);
|
||||
setRechargeGoods(rechargeGoodsResult);
|
||||
setTasks(taskResult);
|
||||
} catch (error) { message.error(getErrorMessage(error)); } finally { setLoading(false); }
|
||||
}, []);
|
||||
@@ -123,17 +127,28 @@ export default function HuyaElitePage() {
|
||||
const startTask = async (taskType: string) => {
|
||||
if (!selectedIds.length) { message.warning('请先勾选账号'); return; }
|
||||
if (taskType === 'exchange_goods' && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; }
|
||||
if (taskType === 'create_recharge_order' && !selectedRechargeSpu) { message.warning('请先选择宝典商品'); return; }
|
||||
setStarting(true);
|
||||
try {
|
||||
const payload = taskType === 'exchange_goods'
|
||||
? { sid: Number(config?.sid || DEFAULT_SID), product_id: Number(selectedGoodsId), act_id: ACT_ID }
|
||||
: taskType === 'query_act_tasks' ? { act_id: ACT_ID } : {};
|
||||
: taskType === 'query_act_tasks' ? { act_id: ACT_ID }
|
||||
: taskType === 'create_recharge_order' ? (() => {
|
||||
const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu);
|
||||
return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' };
|
||||
})() : {};
|
||||
const created = await huyaApi.createTasks({ account_ids: selectedIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload });
|
||||
setActiveBatches((prev) => [...new Set([...prev, created.batch_id])]);
|
||||
logs.connectBatch(created.batch_id, `/api/huya/ws/${created.batch_id}`, {
|
||||
clear: false,
|
||||
onTask: (raw) => setTasks((prev) => [raw as unknown as HuyaTaskItem, ...prev.filter((item) => item.id !== Number((raw as { id?: number }).id))].slice(0, 300)),
|
||||
onResult: () => { setActiveBatches((prev) => prev.filter((id) => id !== created.batch_id)); void loadTasks(); },
|
||||
onTask: (raw) => {
|
||||
const next = raw as unknown as HuyaTaskItem;
|
||||
setTasks((prev) => [next, ...prev.filter((item) => item.id !== next.id)].slice(0, 300));
|
||||
const result = next.result || {};
|
||||
if (next.task_type === 'create_recharge_order' && typeof result.pay_url === 'string' && result.pay_url) setQrTask(next);
|
||||
if (next.task_type === 'get_bind_qr' && (result.mini_qrcode_image || result.bind_redirect_url)) setQrTask(next);
|
||||
},
|
||||
onResult: () => { setActiveBatches((prev) => prev.filter((id) => id !== created.batch_id)); void load(); },
|
||||
});
|
||||
message.success(`已创建 ${created.count} 个任务`);
|
||||
window.setTimeout(() => { void loadTasks(); }, 400);
|
||||
@@ -172,6 +187,7 @@ export default function HuyaElitePage() {
|
||||
return accounts.filter((item) => !value || [item.uid, item.nickname, item.username, item.tag, item.game_name].some((v) => String(v || '').toLowerCase().includes(value)));
|
||||
}, [accounts, search]);
|
||||
const filteredGoods = useMemo(() => goods.map((item) => ({ ...item, label: `${item.name} / ${item.price ?? '-'}分 / ${item.remain_text || '动态库存'}` })), [goods]);
|
||||
const filteredRechargeGoods = useMemo(() => rechargeGoods.map((item) => ({ ...item, label: `${item.name || item.spu_id} / ${item.price ? `${(item.price / 100).toFixed(2)}元` : '价格动态'}` })), [rechargeGoods]);
|
||||
const exchangeCount = tasks.filter((task) => task.task_type === 'exchange_goods').length;
|
||||
|
||||
const accountColumns: TableProps<HuyaAccountItem>['columns'] = [
|
||||
@@ -190,11 +206,12 @@ export default function HuyaElitePage() {
|
||||
</div></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><ShoppingOutlined /><span>兑换</span></div><Select size="small" showSearch optionFilterProp="label" value={selectedGoodsId || undefined} onChange={setSelectedGoodsId} options={filteredGoods.map((item) => ({ value: item.product_id, label: item.label }))} placeholder="选择兑换商品" style={{ width: '100%' }} /><div className="huya-action-grid"><Button size="small" icon={<ReloadOutlined />} onClick={() => void startTask('refresh_goods')} disabled={starting || !selectedIds.length}>刷新商品</Button><Button size="small" type="primary" icon={<ShoppingOutlined />} onClick={() => void startTask('exchange_goods')} disabled={starting || !selectedIds.length || !selectedGoodsId}>兑换商品</Button></div></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><GiftOutlined /><span>开通宝典</span></div><Button block size="small" icon={<QrcodeOutlined />} onClick={() => void startTask('create_recharge_order')} disabled={starting || !selectedIds.length}>生成开通支付码</Button></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><ShoppingOutlined /><span>开通商品</span></div><Select size="small" showSearch optionFilterProp="label" value={selectedRechargeSpu || undefined} onChange={setSelectedRechargeSpu} options={filteredRechargeGoods.map((item) => ({ value: item.spu_id, label: item.label }))} placeholder="先刷新开通商品" style={{ width: '100%' }} /><Button block size="small" icon={<ReloadOutlined />} onClick={() => void startTask('refresh_recharge_goods')} disabled={starting || !selectedIds.length} style={{ marginTop: 6 }}>刷新开通商品</Button></div>
|
||||
<RealtimeLogPanel logs={logs.logs} connected={logs.connected} title="实时日志" emptyText="暂无任务日志" height={150} collapsible defaultVisible={false} />
|
||||
</Space>
|
||||
);
|
||||
|
||||
const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}>移出选中</Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 760, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task?.task_type === 'get_bind_qr') void openQrTask(task); } })} /></div></>;
|
||||
const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}>移出选中</Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 760, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task && ['get_bind_qr', 'create_recharge_order'].includes(task.task_type) && task.result) void openQrTask(task); } })} /></div></>;
|
||||
|
||||
const configField = (key: Exclude<keyof HuyaConfig, 'updated_at'>, label: string) => configDraft && <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}><Text>{label}</Text><Input value={configDraft[key] || ''} onChange={(event) => setConfigDraft({ ...configDraft, [key]: event.target.value })} style={{ width: 300 }} /></Space>;
|
||||
|
||||
@@ -202,7 +219,7 @@ export default function HuyaElitePage() {
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}><div><h2 style={{ margin: 0 }}>虎牙精英宝典工作台</h2><Text type="secondary">绑定、开通、积分与兑换任务</Text></div><Space><Tooltip title={layoutMode === 'split' ? '切换为上下布局' : '切换为左右布局'}><Button icon={<ColumnWidthOutlined />} onClick={() => { const next = layoutMode === 'split' ? 'stack' : 'split'; setLayoutMode(next); localStorage.setItem(LAYOUT_KEY, next); }} /></Tooltip><Button icon={<ReloadOutlined />} onClick={() => void load()} loading={loading}>刷新</Button>{canConfig && <Button icon={<SettingOutlined />} onClick={() => void openSettings()}>设置</Button>}{!!activeBatches.length && <Button danger icon={<StopOutlined />} onClick={() => void stop()}>停止</Button>}</Space></Space>
|
||||
{layoutMode === 'split' ? <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}><Card size="small" title="账号" extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary">并发</Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ width: 360, flexShrink: 0, minHeight: 0, overflow: 'hidden' }} styles={{ body: { padding: 8, overflowY: 'auto', height: 'calc(100% - 38px)' } }}>{operationBody}</Card></div> : <><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary">并发</Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ flexShrink: 0, marginBottom: 12 }} styles={{ body: { padding: 8 } }}>{operationBody}</Card><Card size="small" title="账号" extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card></>}
|
||||
<Text type="secondary" style={{ fontSize: 12, marginTop: 6 }}>兑换任务 {exchangeCount} 条;勾选账号后可批量执行。</Text>
|
||||
<Modal open={qrTask !== null} title="绑定二维码" footer={null} onCancel={() => setQrTask(null)} centered>{qrTask && <div style={{ textAlign: 'center' }}>{resultText(qrTask, 'mini_qrcode_image') ? <img src={`data:image/png;base64,${resultText(qrTask, 'mini_qrcode_image')}`} alt="绑定二维码" style={{ width: 240, height: 240 }} /> : <QRCode value={resultText(qrTask, 'bind_redirect_url') || 'https://zt.huya.com/b02faae1/pc/index.html'} size={240} />}<p><Tag color={TASK_COLORS[qrTask.status] || 'default'}>{qrTask.message || qrTask.status}</Tag></p></div>}</Modal>
|
||||
<Modal open={qrTask !== null} title={qrTask?.task_type === 'create_recharge_order' ? '开通支付二维码' : '绑定二维码'} footer={null} onCancel={() => setQrTask(null)} centered>{qrTask && <div style={{ textAlign: 'center' }}>{resultText(qrTask, 'mini_qrcode_image') ? <img src={`data:image/png;base64,${resultText(qrTask, 'mini_qrcode_image')}`} alt="绑定二维码" style={{ width: 240, height: 240 }} /> : <QRCode value={resultText(qrTask, 'pay_url') || resultText(qrTask, 'bind_redirect_url') || 'https://zt.huya.com/b02faae1/pc/index.html'} size={240} />}<p><Tag color={TASK_COLORS[qrTask.status] || 'default'}>{qrTask.message || qrTask.status}</Tag></p>{qrTask.task_type === 'create_recharge_order' && <Text type="secondary">扫码后订单状态会自动轮询更新</Text>}</div>}</Modal>
|
||||
<Modal open={importOpen} title="导入精英宝典账号" onCancel={() => setImportOpen(false)} onOk={() => { void saveAccounts([...new Set([...accounts.map((item) => item.id), ...importSelected])]).then(() => { setImportSelected([]); setImportOpen(false); }).catch((error) => message.error(getErrorMessage(error))); }} okText="导入" cancelText="取消"><Input.Search allowClear placeholder="搜索账号" value={importSearch} onChange={(event) => setImportSearch(event.target.value)} style={{ marginBottom: 8 }} /><Select allowClear placeholder="标签" value={importTag || undefined} onChange={(value) => setImportTag(value || '')} options={importTags.map((tag) => ({ value: tag, label: tag }))} style={{ width: '100%', marginBottom: 8 }} /><Table rowKey="id" size="small" dataSource={pool.filter((item) => !accounts.some((current) => current.id === item.id) && (!importSearch || [item.uid, item.nickname, item.username].some((value) => String(value || '').includes(importSearch))) && (!importTag || item.tag === importTag))} columns={[{ title: '账号', render: (_value, row) => row.nickname || row.username || row.uid }, { title: '标签', dataIndex: 'tag' }]} pagination={{ pageSize: 8 }} rowSelection={{ selectedRowKeys: importSelected, onChange: (keys) => setImportSelected(keys.map(Number)) }} /></Modal>
|
||||
<Modal open={configOpen} title="精英宝典设置" onCancel={() => setConfigOpen(false)} onOk={() => void saveConfig()} confirmLoading={configSaving} okText="保存" cancelText="取消" width={500}>{configLoading ? <div style={{ padding: 24, textAlign: 'center' }}>加载中...</div> : configDraft && <Space direction="vertical" size={10} style={{ width: '100%' }}>{configField('sid', '活动 SID')}{configField('bind_act_id', '绑定活动 ID')}{configField('outer_act_id', '外部活动 ID')}{configField('room_pid', '直播间 PID')}{configField('pay_channel', '支付渠道')}<Space style={{ width: '100%', justifyContent: 'space-between' }}><Text>空闲轮询间隔(秒)</Text><InputNumber min={5} max={60} value={refreshInterval} onChange={(value) => { const next = Math.min(60, Math.max(5, value || 15)); setRefreshInterval(next); localStorage.setItem('huya_elite_refresh_interval', String(next)); }} style={{ width: 300 }} /></Space></Space>}</Modal>
|
||||
</div>;
|
||||
|
||||
Reference in New Issue
Block a user