新增斗鱼活动任务模块:绑定、宝典、鱼翅、积分、兑换等功能
- 新增 activity_client.py:封装斗鱼活动/兑换/充值/送礼接口 - 新增 cookie_utils.py:Cookie 解析与规范化工具 - 新增 douyu_service/douyu_runner:斗鱼任务服务层与批量执行器 - 新增 douyu 路由:任务类型查询、账号列表、配置管理、商品管理、批量任务、WebSocket 日志 - 新增 models/schemas:DouyuTask/DouyuConfig/DouyuGoodsSnapshot 模型,Account 扩展点数/鱼翅/绑定状态等字段 - 新增数据库迁移:斗鱼活动相关表与 accounts 字段补充 - 新增前端 DouyuTasksPage 任务操作台页面 - 兑换商品请求添加 sec-ch-ua 反检测头 - 兑换商品支持最多 8 次重试 + csrf_token 自动刷新 - 注册 douyu:task / douyu:config 权限点 - 侧边栏新增斗鱼分组与任务操作台菜单入口
This commit is contained in:
@@ -17,6 +17,7 @@ const LoginTasksPage = lazy(() => import('./pages/LoginTasksPage'));
|
||||
const ProxyPage = lazy(() => import('./pages/ProxyPage'));
|
||||
const UsersPage = lazy(() => import('./pages/UsersPage'));
|
||||
const CookiePage = lazy(() => import('./pages/CookiePage'));
|
||||
const DouyuTasksPage = lazy(() => import('./pages/DouyuTasksPage'));
|
||||
const HuyaAccountsPage = lazy(() => import('./pages/HuyaAccountsPage'));
|
||||
const HuyaAssignmentsPage = lazy(() => import('./pages/HuyaAssignmentsPage'));
|
||||
const HuyaCookiePage = lazy(() => import('./pages/HuyaCookiePage'));
|
||||
@@ -71,6 +72,7 @@ function AppContent() {
|
||||
<Route path="assignments" element={lazyRoute(<AssignmentsPage />)} />
|
||||
<Route path="login-tasks" element={lazyRoute(<LoginTasksPage />)} />
|
||||
<Route path="cookies" element={lazyRoute(<CookiePage />)} />
|
||||
<Route path="douyu/tasks" element={lazyRoute(<DouyuTasksPage />)} />
|
||||
<Route path="huya/accounts" element={lazyRoute(<HuyaAccountsPage />)} />
|
||||
<Route path="huya/register" element={lazyRoute(<HuyaRegisterPage />)} />
|
||||
<Route path="huya/assignments" element={lazyRoute(<HuyaAssignmentsPage />)} />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import api from './client';
|
||||
import type {
|
||||
DouyuConfig,
|
||||
DouyuGoodsItem,
|
||||
DouyuTaskAccountItem,
|
||||
DouyuTaskBatchRequest,
|
||||
DouyuTaskBatchResult,
|
||||
DouyuTaskItem,
|
||||
MessageResponse,
|
||||
} from './types';
|
||||
|
||||
export const douyuApi = {
|
||||
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/douyu/task-types'),
|
||||
listAccounts: (params?: { search?: string }) =>
|
||||
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
|
||||
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
|
||||
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
|
||||
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
|
||||
createTasks: (data: DouyuTaskBatchRequest) =>
|
||||
api.post<DouyuTaskBatchResult, DouyuTaskBatchResult>('/douyu/tasks/batch', data),
|
||||
listTasks: (batchId?: string) =>
|
||||
api.get<DouyuTaskItem[], DouyuTaskItem[]>('/douyu/tasks', { params: batchId ? { batch_id: batchId } : {} }),
|
||||
getTask: (taskId: number) => api.get<DouyuTaskItem, DouyuTaskItem>(`/douyu/tasks/${taskId}`),
|
||||
stopBatch: (batchId: string) => api.post<MessageResponse, MessageResponse>(`/douyu/stop/${batchId}`),
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export { accountApi } from './accounts';
|
||||
export { appApi } from './app';
|
||||
export { authApi } from './auth';
|
||||
export { cookieApi } from './cookies';
|
||||
export { douyuApi } from './douyu';
|
||||
export { huyaApi } from './huya';
|
||||
export { loginApi } from './login';
|
||||
export { proxyApi } from './proxy';
|
||||
|
||||
@@ -168,6 +168,78 @@ export interface CookieItem {
|
||||
account_password: string;
|
||||
}
|
||||
|
||||
// ==================== Douyu Activity ====================
|
||||
|
||||
export interface DouyuTaskAccountItem {
|
||||
id: number;
|
||||
username: string;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
tag: string;
|
||||
points: number | null;
|
||||
game_name: string;
|
||||
game_channel: string;
|
||||
gold_balance: number | null;
|
||||
exchange_balance: number | null;
|
||||
bind_status: string;
|
||||
change_role_wait_time: number | null;
|
||||
assigned_to: number | null;
|
||||
assigned_username: string | null;
|
||||
}
|
||||
|
||||
export interface DouyuConfig {
|
||||
manual_id: string;
|
||||
rid: string;
|
||||
bind_act_alias: string;
|
||||
confirm_act_alias: string;
|
||||
legacy_act_alias: string;
|
||||
room_id: string;
|
||||
elite_amount: number;
|
||||
gold_pay_type: number;
|
||||
gift_id: string;
|
||||
skin_id: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface DouyuTaskBatchRequest {
|
||||
account_ids: number[];
|
||||
task_type: string;
|
||||
concurrency?: number;
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DouyuTaskBatchResult {
|
||||
batch_id: string;
|
||||
count: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DouyuTaskItem {
|
||||
id: number;
|
||||
batch_id: string;
|
||||
account_id: number;
|
||||
account_username: string;
|
||||
account_uid: string;
|
||||
account_nickname: string;
|
||||
task_type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
result: Record<string, unknown> | null;
|
||||
created_by: number;
|
||||
created_at: string | null;
|
||||
finished_at: string | null;
|
||||
}
|
||||
|
||||
export interface DouyuGoodsItem {
|
||||
id: number;
|
||||
commodity_id: string;
|
||||
name: string;
|
||||
score: number | null;
|
||||
status: string;
|
||||
raw: Record<string, unknown> | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
// ==================== Huya ====================
|
||||
|
||||
export interface HuyaAccountItem {
|
||||
|
||||
@@ -70,6 +70,9 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
if (can('cookie:view')) {
|
||||
douyuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
||||
}
|
||||
if (can('douyu:task')) {
|
||||
douyuItems.push({ key: '/douyu/tasks', label: '任务操作台', icon: <ShoppingCartOutlined /> });
|
||||
}
|
||||
|
||||
// 虎牙
|
||||
if (canAny(['huya:account', 'huya:view_all', 'huya:view_assigned'])) {
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Form, Input, InputNumber, Modal, QRCode, Row, Select, Space, Table, Tag, Typography, theme,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined, GiftOutlined, LinkOutlined,
|
||||
QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
douyuApi,
|
||||
type DouyuConfig,
|
||||
type DouyuGoodsItem,
|
||||
type DouyuTaskAccountItem,
|
||||
type DouyuTaskItem,
|
||||
} from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { message } from '../utils/antdMessage';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const FALLBACK_TASK_TYPES: Record<string, string> = {
|
||||
get_bind_qr: '获取绑定二维码',
|
||||
confirm_bind: '确认绑定',
|
||||
create_elite_qr: '开通精英宝典30',
|
||||
create_gold_qr: '充值鱼翅',
|
||||
donate_elite_gift: '赠送精英令',
|
||||
query_points: '一键查询积分',
|
||||
exchange_goods: '兑换商品',
|
||||
query_game_name: '一键获取游戏名',
|
||||
query_change_bind_time: '一键查询换绑时间',
|
||||
query_limited_goods: '一键查询限兑商品',
|
||||
query_gold_balance: '一键查询鱼刺余额',
|
||||
refresh_goods: '刷新商品列表',
|
||||
query_exchange_records: '一键查询兑换记录',
|
||||
prefetch_csrf_token: '一键获取兑换 CSRF Token',
|
||||
};
|
||||
|
||||
const TASK_ICONS: Record<string, React.ReactNode> = {
|
||||
get_bind_qr: <QrcodeOutlined />,
|
||||
confirm_bind: <CheckCircleOutlined />,
|
||||
create_elite_qr: <CreditCardOutlined />,
|
||||
create_gold_qr: <CreditCardOutlined />,
|
||||
donate_elite_gift: <GiftOutlined />,
|
||||
query_points: <SearchOutlined />,
|
||||
exchange_goods: <ShoppingOutlined />,
|
||||
query_game_name: <SearchOutlined />,
|
||||
query_change_bind_time: <FieldTimeOutlined />,
|
||||
query_limited_goods: <SearchOutlined />,
|
||||
query_gold_balance: <SearchOutlined />,
|
||||
refresh_goods: <ReloadOutlined />,
|
||||
query_exchange_records: <FieldTimeOutlined />,
|
||||
prefetch_csrf_token: <LinkOutlined />,
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
planned: 'default',
|
||||
pending: 'default',
|
||||
running: 'processing',
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
error: 'error',
|
||||
stopped: 'warning',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
planned: '已计划',
|
||||
pending: '等待中',
|
||||
running: '执行中',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
error: '异常',
|
||||
stopped: '已停止',
|
||||
};
|
||||
|
||||
function accountLabel(account: DouyuTaskAccountItem): string {
|
||||
const name = account.nickname || account.username || account.uid || `#${account.id}`;
|
||||
const tag = account.tag ? ` [${account.tag}]` : '';
|
||||
const game = account.game_name ? ` / ${account.game_name}` : '';
|
||||
return `${name}${tag}${game}`;
|
||||
}
|
||||
|
||||
function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
|
||||
const value = result?.[key];
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
function resultNumber(result: Record<string, unknown> | null | undefined, key: string): number | null {
|
||||
const value = result?.[key];
|
||||
if (typeof value === 'number') return value;
|
||||
if (typeof value === 'string' && value.trim()) return Number(value) || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function taskPayUrl(task: DouyuTaskItem | null): string {
|
||||
return resultText(task?.result, 'pay_url') || resultText(task?.result, 'url');
|
||||
}
|
||||
|
||||
function goodsLabel(item: DouyuGoodsItem): string {
|
||||
return `${item.name || item.commodity_id}${item.score ? ` / ${item.score}积分` : ''}`;
|
||||
}
|
||||
|
||||
const defaultConfig: DouyuConfig = {
|
||||
manual_id: 'G4KA4Qnz4LDp7',
|
||||
rid: '9263298',
|
||||
bind_act_alias: '20250213NQCYX',
|
||||
confirm_act_alias: '20260120QYOOB',
|
||||
legacy_act_alias: 'cjm',
|
||||
room_id: '9263298',
|
||||
elite_amount: 3000,
|
||||
gold_pay_type: 1,
|
||||
gift_id: '23643',
|
||||
skin_id: '2942',
|
||||
updated_at: null,
|
||||
};
|
||||
|
||||
export default function DouyuTasksPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
|
||||
const [goods, setGoods] = useState<DouyuGoodsItem[]>([]);
|
||||
const [tasks, setTasks] = useState<DouyuTaskItem[]>([]);
|
||||
const [taskTypes, setTaskTypes] = useState<Record<string, string>>(FALLBACK_TASK_TYPES);
|
||||
const [config, setConfig] = useState<DouyuConfig>(defaultConfig);
|
||||
const [selectedAccountIds, setSelectedAccountIds] = useState<number[]>([]);
|
||||
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
||||
const [concurrency, setConcurrency] = useState(3);
|
||||
const [goldAmount, setGoldAmount] = useState(1);
|
||||
const [giftCount, setGiftCount] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [runningBatchId, setRunningBatchId] = useState('');
|
||||
const [configOpen, setConfigOpen] = useState(false);
|
||||
const [payTask, setPayTask] = useState<DouyuTaskItem | null>(null);
|
||||
const [configForm] = Form.useForm<DouyuConfig>();
|
||||
const logs = useWebSocketLogs();
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [accountResult, goodsResult, taskResult, configResult, taskTypeResult] = await Promise.allSettled([
|
||||
douyuApi.listAccounts(),
|
||||
douyuApi.listGoods(),
|
||||
douyuApi.listTasks(),
|
||||
douyuApi.getConfig(),
|
||||
douyuApi.taskTypes(),
|
||||
]);
|
||||
if (accountResult.status === 'fulfilled') setAccounts(accountResult.value);
|
||||
if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
|
||||
if (taskResult.status === 'fulfilled') setTasks(taskResult.value);
|
||||
if (configResult.status === 'fulfilled') setConfig(configResult.value);
|
||||
if (taskTypeResult.status === 'fulfilled') setTaskTypes(taskTypeResult.value);
|
||||
const errors = [
|
||||
accountResult.status === 'rejected' ? `账号: ${getErrorMessage(accountResult.reason)}` : '',
|
||||
goodsResult.status === 'rejected' ? `商品: ${getErrorMessage(goodsResult.reason)}` : '',
|
||||
taskResult.status === 'rejected' ? `任务: ${getErrorMessage(taskResult.reason)}` : '',
|
||||
configResult.status === 'rejected' ? `配置: ${getErrorMessage(configResult.reason)}` : '',
|
||||
].filter(Boolean);
|
||||
if (errors.length) message.warning(errors.join(';'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (configOpen) configForm.setFieldsValue(config);
|
||||
}, [configOpen, config, configForm]);
|
||||
|
||||
const activeTask = tasks.find((item) => ['planned', 'pending', 'running'].includes(item.status));
|
||||
|
||||
const goodsOptions = useMemo(
|
||||
() => goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) })),
|
||||
[goods],
|
||||
);
|
||||
|
||||
const startTask = async (taskType: string) => {
|
||||
if (selectedAccountIds.length === 0) {
|
||||
message.warning('请先选择斗鱼账号');
|
||||
return;
|
||||
}
|
||||
if (taskType === 'exchange_goods' && !selectedGoodsId) {
|
||||
message.warning('请先选择兑换商品');
|
||||
return;
|
||||
}
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (taskType === 'exchange_goods') {
|
||||
payload.commodity_id = selectedGoodsId;
|
||||
}
|
||||
if (taskType === 'create_gold_qr') {
|
||||
payload.amount = goldAmount;
|
||||
}
|
||||
if (taskType === 'donate_elite_gift') {
|
||||
payload.gift_count = giftCount;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await douyuApi.createTasks({
|
||||
account_ids: selectedAccountIds,
|
||||
task_type: taskType,
|
||||
concurrency,
|
||||
payload,
|
||||
});
|
||||
setRunningBatchId(result.batch_id);
|
||||
logs.connect(`/api/douyu/ws/${result.batch_id}`, {
|
||||
clear: true,
|
||||
onResult: () => {
|
||||
setRunningBatchId('');
|
||||
loadData();
|
||||
},
|
||||
});
|
||||
message.success(`已创建 ${result.count} 个任务`);
|
||||
setTimeout(loadData, 500);
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const stopTask = async () => {
|
||||
if (!runningBatchId) return;
|
||||
try {
|
||||
await douyuApi.stopBatch(runningBatchId);
|
||||
message.success('已发送停止信号');
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
const values = await configForm.validateFields();
|
||||
const saved = await douyuApi.updateConfig(values);
|
||||
setConfig(saved);
|
||||
setConfigOpen(false);
|
||||
message.success('配置已保存');
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const openPayTask = (task: DouyuTaskItem) => {
|
||||
const url = taskPayUrl(task);
|
||||
if (!url) {
|
||||
message.warning('任务结果中没有二维码链接');
|
||||
return;
|
||||
}
|
||||
setPayTask(task);
|
||||
};
|
||||
|
||||
const accountColumns: TableProps<DouyuTaskAccountItem>['columns'] = [
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'username',
|
||||
width: 190,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{record.nickname || record.username}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>UID {record.uid || '-'}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '游戏名',
|
||||
dataIndex: 'game_name',
|
||||
width: 180,
|
||||
render: (_, record) => record.game_name ? (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text>{record.game_name}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{record.game_channel || '-'}</Text>
|
||||
</Space>
|
||||
) : <Text type="secondary">未查</Text>,
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
dataIndex: 'points',
|
||||
width: 90,
|
||||
render: (value) => value ?? <Text type="secondary">-</Text>,
|
||||
sorter: (a, b) => (a.points ?? -1) - (b.points ?? -1),
|
||||
},
|
||||
{
|
||||
title: '鱼翅',
|
||||
dataIndex: 'gold_balance',
|
||||
width: 90,
|
||||
render: (value) => value ?? <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'bind_status',
|
||||
width: 120,
|
||||
render: (value) => value ? <Tag>{value}</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
const taskColumns: TableProps<DouyuTaskItem>['columns'] = [
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'account_username',
|
||||
width: 150,
|
||||
render: (_, record) => record.account_nickname || record.account_username || record.account_uid,
|
||||
},
|
||||
{
|
||||
title: '任务',
|
||||
dataIndex: 'task_type',
|
||||
width: 170,
|
||||
render: (value) => taskTypes[value] || value,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (value) => <Tag color={STATUS_COLORS[value] || 'default'}>{STATUS_LABELS[value] || value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'message',
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
const payUrl = taskPayUrl(record);
|
||||
const points = resultNumber(record.result, 'points');
|
||||
return (
|
||||
<Space wrap>
|
||||
<Text>{record.message || '-'}</Text>
|
||||
{typeof points === 'number' && <Tag color="blue">积分 {points}</Tag>}
|
||||
{payUrl && (
|
||||
<Button size="small" icon={<QrcodeOutlined />} onClick={() => openPayTask(record)}>
|
||||
二维码
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'finished_at',
|
||||
width: 170,
|
||||
render: (_, record) => formatTime(record.finished_at || record.created_at),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0 }}>斗鱼任务操作台</h2>
|
||||
<Text type="secondary">绑定、宝典、鱼翅、积分、兑换和余额任务</Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
|
||||
<Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>
|
||||
{runningBatchId && (
|
||||
<Button danger icon={<StopOutlined />} onClick={stopTask}>停止</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={24} xl={15}>
|
||||
<Card
|
||||
size="small"
|
||||
title="账号"
|
||||
extra={<Tag color="blue">已选 {selectedAccountIds.length}</Tag>}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedAccountIds,
|
||||
onChange: (keys) => setSelectedAccountIds(keys.map(Number)),
|
||||
}}
|
||||
columns={accountColumns}
|
||||
dataSource={accounts}
|
||||
pagination={{ pageSize: 8, showSizeChanger: false }}
|
||||
scroll={{ x: 700 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={9}>
|
||||
<Card size="small" title="操作">
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={10}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedAccountIds}
|
||||
onChange={setSelectedAccountIds}
|
||||
options={accounts.map((item) => ({ value: item.id, label: accountLabel(item) }))}
|
||||
placeholder="选择账号"
|
||||
maxTagCount="responsive"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 110 }} />
|
||||
<Input value="并发数" disabled />
|
||||
</Space.Compact>
|
||||
<Row gutter={[8, 8]}>
|
||||
{[
|
||||
'get_bind_qr',
|
||||
'confirm_bind',
|
||||
'create_elite_qr',
|
||||
'query_points',
|
||||
'query_game_name',
|
||||
'query_change_bind_time',
|
||||
'query_limited_goods',
|
||||
'query_gold_balance',
|
||||
].map((key) => (
|
||||
<Col span={12} key={key}>
|
||||
<Button block icon={TASK_ICONS[key]} onClick={() => startTask(key)} disabled={!!activeTask}>
|
||||
{taskTypes[key] || key}
|
||||
</Button>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Card size="small" title="兑换">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={selectedGoodsId || undefined}
|
||||
onChange={setSelectedGoodsId}
|
||||
options={goodsOptions}
|
||||
placeholder="选择兑换商品"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Space wrap>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => startTask('refresh_goods')} disabled={!!activeTask}>
|
||||
刷新商品
|
||||
</Button>
|
||||
<Button type="primary" icon={<ShoppingOutlined />} onClick={() => startTask('exchange_goods')} disabled={!!activeTask}>
|
||||
兑换商品
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card size="small" title="充值与送礼">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber min={1} value={goldAmount} onChange={(value) => setGoldAmount(value || 1)} style={{ width: 120 }} />
|
||||
<Button icon={<CreditCardOutlined />} onClick={() => startTask('create_gold_qr')} disabled={!!activeTask}>
|
||||
充值鱼翅
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber min={1} value={giftCount} onChange={(value) => setGiftCount(value || 1)} style={{ width: 120 }} />
|
||||
<Button icon={<GiftOutlined />} onClick={() => startTask('donate_elite_gift')} disabled={!!activeTask}>
|
||||
赠送精英令
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</Card>
|
||||
<RealtimeLogPanel logs={logs.logs} connected={logs.connected} height={180} />
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card size="small" title="任务记录">
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={taskColumns}
|
||||
dataSource={tasks}
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, showSizeChanger: false }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="斗鱼活动配置"
|
||||
open={configOpen}
|
||||
onCancel={() => setConfigOpen(false)}
|
||||
onOk={saveConfig}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
>
|
||||
<Form form={configForm} layout="vertical" initialValues={config}>
|
||||
<Row gutter={12}>
|
||||
<Col span={12}><Form.Item label="manualID" name="manual_id"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="RID" name="rid"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="绑定二维码活动" name="bind_act_alias"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="确认绑定活动" name="confirm_act_alias"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="旧版查询活动" name="legacy_act_alias"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="房间 ID" name="room_id"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="宝典金额(分)" name="elite_amount"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="鱼翅支付方式" name="gold_pay_type"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="精英令礼物 ID" name="gift_id"><Input /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label="皮肤 ID" name="skin_id"><Input /></Form.Item></Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="二维码"
|
||||
open={!!payTask}
|
||||
onCancel={() => setPayTask(null)}
|
||||
footer={null}
|
||||
centered
|
||||
>
|
||||
{payTask && (
|
||||
<Space direction="vertical" align="center" style={{ width: '100%' }}>
|
||||
<Text strong>{taskTypes[payTask.task_type] || payTask.task_type}</Text>
|
||||
<QRCode value={taskPayUrl(payTask)} size={260} />
|
||||
<Text copyable style={{ maxWidth: '100%', color: token.colorTextSecondary }}>
|
||||
{taskPayUrl(payTask)}
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user