优化双平台后台界面
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "douyu-login-py"
|
||||
version = "0.2.0"
|
||||
description = "斗鱼批量登录 Web 后台"
|
||||
description = "直播账号运营 Web 后台"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="斗鱼批量登录后台",
|
||||
title="直播运营后台",
|
||||
version=get_app_version(),
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<title>直播运营后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Layout, Menu, Avatar, Space, Typography, Button, Modal, Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import {
|
||||
DashboardOutlined, UserOutlined, LogoutOutlined,
|
||||
CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined,
|
||||
MenuFoldOutlined, MenuUnfoldOutlined, SwapOutlined,
|
||||
SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ShoppingCartOutlined,
|
||||
SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ShoppingCartOutlined, ApartmentOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { getUser, clearAuth, type AuthUser } from '../store/auth';
|
||||
@@ -44,49 +45,52 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const menuItems: { key: string; label: string; icon: React.ReactNode }[] = [];
|
||||
const menuItems: MenuProps['items'] = [];
|
||||
const douyuItems: NonNullable<MenuProps['items']> = [];
|
||||
const huyaItems: NonNullable<MenuProps['items']> = [];
|
||||
const systemItems: NonNullable<MenuProps['items']> = [];
|
||||
|
||||
// Dashboard - 所有人可见
|
||||
menuItems.push({ key: '/', label: '概览', icon: <DashboardOutlined /> });
|
||||
|
||||
// 账号管理
|
||||
// 斗鱼
|
||||
if (canAny(['account:view_all', 'account:view_assigned'])) {
|
||||
menuItems.push({ key: '/accounts', label: '账号管理', icon: <UserOutlined /> });
|
||||
douyuItems.push({ key: '/accounts', label: '账号管理', icon: <UserOutlined /> });
|
||||
}
|
||||
|
||||
// 分配管理
|
||||
if (can('account:assign')) {
|
||||
menuItems.push({ key: '/assignments', label: '分配管理', icon: <SwapOutlined /> });
|
||||
douyuItems.push({ key: '/assignments', label: '分配管理', icon: <SwapOutlined /> });
|
||||
}
|
||||
|
||||
// 登录任务
|
||||
if (canAny(['login:batch', 'login:view_all'])) {
|
||||
menuItems.push({ key: '/login-tasks', label: '登录任务', icon: <ApiOutlined /> });
|
||||
douyuItems.push({ key: '/login-tasks', label: '登录任务', icon: <ApiOutlined /> });
|
||||
}
|
||||
|
||||
// Cookie 管理
|
||||
if (can('cookie:view')) {
|
||||
menuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
||||
douyuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
||||
}
|
||||
|
||||
// 虎牙 CK 管理
|
||||
// 虎牙
|
||||
if (can('huya:account')) {
|
||||
menuItems.push({ key: '/huya/accounts', label: '虎牙 CK', icon: <GiftOutlined /> });
|
||||
huyaItems.push({ key: '/huya/accounts', label: '账号管理', icon: <GiftOutlined /> });
|
||||
}
|
||||
|
||||
// 虎牙兑换与充值
|
||||
if (can('huya:task')) {
|
||||
menuItems.push({ key: '/huya/tasks', label: '虎牙任务', icon: <ShoppingCartOutlined /> });
|
||||
huyaItems.push({ key: '/huya/tasks', label: '任务操作台', icon: <ShoppingCartOutlined /> });
|
||||
}
|
||||
|
||||
// 代理配置
|
||||
if (douyuItems.length > 0) {
|
||||
menuItems.push({ key: 'group-douyu', type: 'group', label: '斗鱼', children: douyuItems });
|
||||
}
|
||||
if (huyaItems.length > 0) {
|
||||
menuItems.push({ key: 'group-huya', type: 'group', label: '虎牙', children: huyaItems });
|
||||
}
|
||||
|
||||
// 系统
|
||||
if (can('proxy:manage')) {
|
||||
menuItems.push({ key: '/proxy', label: '代理配置', icon: <CloudServerOutlined /> });
|
||||
systemItems.push({ key: '/proxy', label: '代理配置', icon: <CloudServerOutlined /> });
|
||||
}
|
||||
|
||||
// 用户管理
|
||||
if (can('user:view')) {
|
||||
menuItems.push({ key: '/users', label: '用户管理', icon: <TeamOutlined /> });
|
||||
systemItems.push({ key: '/users', label: '用户管理', icon: <TeamOutlined /> });
|
||||
}
|
||||
if (systemItems.length > 0) {
|
||||
menuItems.push({ key: 'group-system', type: 'group', label: '系统', children: systemItems });
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -129,7 +133,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
color: siderTextColor,
|
||||
}}>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{collapsed ? '鱼' : '斗鱼登录后台'}
|
||||
{collapsed ? <ApartmentOutlined /> : '直播运营后台'}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
@@ -1,48 +1,191 @@
|
||||
import { Card, Col, Row, Statistic, theme } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { accountApi, loginApi, type LoginTaskItem } from '../api/modules';
|
||||
import { Card, Col, Row, Space, Statistic, Tag, Typography, theme } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
accountApi,
|
||||
cookieApi,
|
||||
huyaApi,
|
||||
loginApi,
|
||||
type HuyaTaskItem,
|
||||
type LoginTaskItem,
|
||||
} from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface PlatformStats {
|
||||
accounts: number;
|
||||
tasks: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
douyu: PlatformStats & {
|
||||
cookies: number;
|
||||
};
|
||||
huya: PlatformStats & {
|
||||
goods: number;
|
||||
rechargeGoods: number;
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_STATS: DashboardStats = {
|
||||
douyu: {
|
||||
accounts: 0,
|
||||
tasks: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
cookies: 0,
|
||||
},
|
||||
huya: {
|
||||
accounts: 0,
|
||||
tasks: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
goods: 0,
|
||||
rechargeGoods: 0,
|
||||
},
|
||||
};
|
||||
|
||||
function countFailed<T extends { status: string }>(items: T[]) {
|
||||
return items.filter((item) => ['failed', 'error'].includes(item.status)).length;
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
title,
|
||||
value,
|
||||
color,
|
||||
loading,
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card size="small" loading={loading}>
|
||||
<Statistic title={title} value={value} styles={{ content: color ? { color } : undefined }} />
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [stats, setStats] = useState({ accounts: 0, tasks: 0, success: 0, failed: 0 });
|
||||
const { can, canAny } = usePermissions();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState<DashboardStats>(EMPTY_STATS);
|
||||
|
||||
const canViewDouyuAccounts = canAny(['account:view_all', 'account:view_assigned']);
|
||||
const canViewDouyuTasks = canAny(['login:batch', 'login:view_all', 'login:view_assigned']);
|
||||
const canViewCookies = can('cookie:view');
|
||||
const canViewHuyaAccounts = can('huya:account');
|
||||
const canViewHuyaTasks = can('huya:task');
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([accountApi.list(), loginApi.listTasks()])
|
||||
.then(([accounts, tasks]) => {
|
||||
setStats({
|
||||
accounts: accounts.length,
|
||||
tasks: tasks.length,
|
||||
success: tasks.filter((t: LoginTaskItem) => t.status === 'success').length,
|
||||
failed: tasks.filter((t: LoginTaskItem) => ['failed', 'error'].includes(t.status)).length,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
let ignore = false;
|
||||
|
||||
async function loadDashboard() {
|
||||
setLoading(true);
|
||||
const [
|
||||
douyuAccountsResult,
|
||||
douyuTasksResult,
|
||||
cookiesResult,
|
||||
huyaAccountsResult,
|
||||
huyaTasksResult,
|
||||
huyaGoodsResult,
|
||||
huyaRechargeGoodsResult,
|
||||
] = await Promise.allSettled([
|
||||
canViewDouyuAccounts ? accountApi.list() : Promise.resolve([]),
|
||||
canViewDouyuTasks ? loginApi.listTasks() : Promise.resolve([]),
|
||||
canViewCookies ? cookieApi.list() : Promise.resolve([]),
|
||||
canViewHuyaAccounts ? huyaApi.listAccounts() : Promise.resolve([]),
|
||||
canViewHuyaTasks ? huyaApi.listTasks() : Promise.resolve([]),
|
||||
canViewHuyaTasks ? huyaApi.listGoods() : Promise.resolve([]),
|
||||
canViewHuyaTasks ? huyaApi.listRechargeGoods() : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
if (ignore) return;
|
||||
|
||||
const douyuAccounts = douyuAccountsResult.status === 'fulfilled' ? douyuAccountsResult.value : [];
|
||||
const douyuTasks = douyuTasksResult.status === 'fulfilled' ? douyuTasksResult.value : [];
|
||||
const cookies = cookiesResult.status === 'fulfilled' ? cookiesResult.value : [];
|
||||
const huyaAccounts = huyaAccountsResult.status === 'fulfilled' ? huyaAccountsResult.value : [];
|
||||
const huyaTasks = huyaTasksResult.status === 'fulfilled' ? huyaTasksResult.value : [];
|
||||
const huyaGoods = huyaGoodsResult.status === 'fulfilled' ? huyaGoodsResult.value : [];
|
||||
const huyaRechargeGoods = huyaRechargeGoodsResult.status === 'fulfilled' ? huyaRechargeGoodsResult.value : [];
|
||||
|
||||
setStats({
|
||||
douyu: {
|
||||
accounts: douyuAccounts.length,
|
||||
tasks: douyuTasks.length,
|
||||
success: douyuTasks.filter((task: LoginTaskItem) => task.status === 'success').length,
|
||||
failed: countFailed(douyuTasks),
|
||||
cookies: cookies.length,
|
||||
},
|
||||
huya: {
|
||||
accounts: huyaAccounts.length,
|
||||
tasks: huyaTasks.length,
|
||||
success: huyaTasks.filter((task: HuyaTaskItem) => task.status === 'success').length,
|
||||
failed: countFailed(huyaTasks),
|
||||
goods: huyaGoods.length,
|
||||
rechargeGoods: huyaRechargeGoods.length,
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
void loadDashboard();
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [canViewCookies, canViewDouyuAccounts, canViewDouyuTasks, canViewHuyaAccounts, canViewHuyaTasks]);
|
||||
|
||||
const totalStats = useMemo(() => ({
|
||||
accounts: stats.douyu.accounts + stats.huya.accounts,
|
||||
tasks: stats.douyu.tasks + stats.huya.tasks,
|
||||
success: stats.douyu.success + stats.huya.success,
|
||||
failed: stats.douyu.failed + stats.huya.failed,
|
||||
}), [stats]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>概览</h2>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="账号总数" value={stats.accounts} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="登录任务总数" value={stats.tasks} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="成功" value={stats.success} styles={{ content: { color: token.colorSuccess } }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="失败" value={stats.failed} styles={{ content: { color: token.colorError } }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space align="center" wrap>
|
||||
<Title level={2} style={{ margin: 0 }}>概览</Title>
|
||||
<Tag color="blue">斗鱼</Tag>
|
||||
<Tag color="orange">虎牙</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
|
||||
<StatCard title="账号总数" value={totalStats.accounts} loading={loading} />
|
||||
<StatCard title="任务总数" value={totalStats.tasks} loading={loading} />
|
||||
<StatCard title="成功任务" value={totalStats.success} color={token.colorSuccess} loading={loading} />
|
||||
<StatCard title="失败任务" value={totalStats.failed} color={token.colorError} loading={loading} />
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>斗鱼</Title>
|
||||
<Text type="secondary">账号登录、Cookie 产出和批量登录任务</Text>
|
||||
</div>
|
||||
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
|
||||
<StatCard title="斗鱼账号" value={stats.douyu.accounts} loading={loading} />
|
||||
<StatCard title="登录任务" value={stats.douyu.tasks} loading={loading} />
|
||||
<StatCard title="成功 Cookie" value={stats.douyu.cookies} color={token.colorSuccess} loading={loading} />
|
||||
<StatCard title="失败/异常" value={stats.douyu.failed} color={token.colorError} loading={loading} />
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>虎牙</Title>
|
||||
<Text type="secondary">账号 Cookie、活动任务、兑换商品和充值商品</Text>
|
||||
</div>
|
||||
<Row gutter={[12, 12]}>
|
||||
<StatCard title="虎牙账号" value={stats.huya.accounts} loading={loading} />
|
||||
<StatCard title="虎牙任务" value={stats.huya.tasks} loading={loading} />
|
||||
<StatCard title="兑换商品" value={stats.huya.goods} loading={loading} />
|
||||
<StatCard title="充值商品" value={stats.huya.rechargeGoods} loading={loading} />
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -735,10 +735,31 @@ export default function HuyaTasksPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const taskSummary = (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8, color: token.colorTextSecondary, flexWrap: 'wrap' }}>
|
||||
<span>共 <b>{tasks.length}</b> 个任务</span>
|
||||
<span>已计划 <b>{plannedCount}</b></span>
|
||||
<span>成功 <b style={{ color: token.colorSuccess }}>{successCount}</b></span>
|
||||
<span>失败 <b style={{ color: token.colorError }}>{failedCount}</b></span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTaskTable = (pageSize = 12) => (
|
||||
<Table
|
||||
columns={taskColumns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ pageSize, showTotal: (total) => `共 ${total} 条` }}
|
||||
scroll={{ x: 920 }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<h2 style={{ margin: 0 }}>虎牙兑换与充值</h2>
|
||||
<h2 style={{ margin: 0 }}>虎牙任务操作台</h2>
|
||||
<Space wrap>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadAll} loading={loading}>
|
||||
刷新
|
||||
@@ -748,313 +769,327 @@ export default function HuyaTasksPage() {
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', paddingRight: 2 }}>
|
||||
<Row gutter={12}>
|
||||
<Col xs={24} xl={10}>
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><SettingOutlined />虎牙配置</Space>}
|
||||
extra={canConfig && (
|
||||
<Button size="small" type="primary" onClick={saveConfig} loading={savingConfig}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" disabled={!canConfig}>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="直播间 ID" name="room_pid">
|
||||
<Input placeholder="默认 1199650619883" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="SID" name="sid">
|
||||
<Input placeholder="默认 2203" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="兑换活动 ID" name="outer_act_id">
|
||||
<Input placeholder="默认 9504" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="绑定 bActId" name="bind_act_id">
|
||||
<Input placeholder="默认 9271" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="支付渠道" name="pay_channel">
|
||||
<Tabs
|
||||
defaultActiveKey="workbench"
|
||||
items={[
|
||||
{
|
||||
key: 'workbench',
|
||||
label: <Space><PlayCircleOutlined />操作台</Space>,
|
||||
children: (
|
||||
<>
|
||||
<Card size="small" title="批量动作" style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
]}
|
||||
mode="multiple"
|
||||
showSearch
|
||||
placeholder="选择虎牙账号"
|
||||
value={selectedIds}
|
||||
onChange={setSelectedIds}
|
||||
options={accountOptions}
|
||||
maxTagCount="responsive"
|
||||
style={{ width: '100%' }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
<div style={{ padding: '4px 8px', borderBottom: `1px solid ${token.colorBorderSecondary}`, display: 'flex', gap: 8 }}>
|
||||
<Button size="small" type="link" onClick={() => setSelectedIds(accounts.map((item) => item.id))}>
|
||||
全选 ({accounts.length})
|
||||
</Button>
|
||||
<Button size="small" type="link" onClick={() => setSelectedIds([])}>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
{menu}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><ShoppingOutlined />兑换商品列表</Space>}
|
||||
extra={(
|
||||
<Space size={6}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => startTask('refresh_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={() => startTask('exchange_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
兑换
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择兑换商品"
|
||||
value={selectedExchangeGoodsId || undefined}
|
||||
onChange={(value) => setSelectedExchangeGoodsId(value || '')}
|
||||
options={goodsOptions}
|
||||
style={{ minWidth: 220 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<DatePicker
|
||||
showTime
|
||||
allowClear
|
||||
value={exchangeAt}
|
||||
onChange={setExchangeAt}
|
||||
placeholder="立即兑换"
|
||||
style={{ width: 190 }}
|
||||
/>
|
||||
</Space>
|
||||
{goodsCategories.length > 0 && (
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={selectedGoodsCategory}
|
||||
onChange={setSelectedGoodsCategory}
|
||||
items={
|
||||
goodsCategories.map((item) => ({
|
||||
key: item.key,
|
||||
label: `${item.label} (${item.count})`,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
columns={goodsColumns}
|
||||
dataSource={filteredGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 220 }}
|
||||
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedExchangeGoodsId(record.product_id),
|
||||
})}
|
||||
rowClassName={(record) => record.product_id === selectedExchangeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><CreditCardOutlined />充值商品列表</Space>}
|
||||
extra={(
|
||||
<Space size={6}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => startTask('refresh_recharge_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<QrcodeOutlined />}
|
||||
onClick={() => startTask('create_recharge_order')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
支付码
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择充值商品"
|
||||
value={selectedRechargeGoodsId || undefined}
|
||||
onChange={(value) => setSelectedRechargeGoodsId(value || '')}
|
||||
options={rechargeGoodsOptions}
|
||||
style={{ minWidth: 220 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={999}
|
||||
value={rechargeCount}
|
||||
onChange={(value) => setRechargeCount(value || 1)}
|
||||
addonAfter="份"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Select
|
||||
value={rechargePayChannel}
|
||||
onChange={setRechargePayChannel}
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
]}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
columns={rechargeGoodsColumns}
|
||||
dataSource={sortedRechargeGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 220 }}
|
||||
locale={{ emptyText: '暂无充值商品,请先刷新充值商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedRechargeGoodsId(record.spu_id),
|
||||
})}
|
||||
rowClassName={(record) => record.spu_id === selectedRechargeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={14}>
|
||||
<Card size="small" title="批量动作" style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
showSearch
|
||||
placeholder="选择虎牙 CK"
|
||||
value={selectedIds}
|
||||
onChange={setSelectedIds}
|
||||
options={accountOptions}
|
||||
maxTagCount="responsive"
|
||||
style={{ width: '100%' }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
<div style={{ padding: '4px 8px', borderBottom: `1px solid ${token.colorBorderSecondary}`, display: 'flex', gap: 8 }}>
|
||||
<Button size="small" type="link" onClick={() => setSelectedIds(accounts.map((item) => item.id))}>
|
||||
全选 ({accounts.length})
|
||||
</Button>
|
||||
<Button size="small" type="link" onClick={() => setSelectedIds([])}>
|
||||
清空
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
value={selectedTaskType}
|
||||
onChange={setSelectedTaskType}
|
||||
options={Object.entries(taskTypes).map(([value, label]) => ({ value, label }))}
|
||||
style={{ flex: '1 1 220px', minWidth: 180 }}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={concurrency}
|
||||
onChange={(value) => setConcurrency(value || 1)}
|
||||
addonBefore="并发"
|
||||
style={{ width: 130, flexShrink: 0 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={starting}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
onClick={() => startTask()}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
{menu}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
value={selectedTaskType}
|
||||
onChange={setSelectedTaskType}
|
||||
options={Object.entries(taskTypes).map(([value, label]) => ({ value, label }))}
|
||||
style={{ flex: '1 1 220px', minWidth: 180 }}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{QUICK_ACTIONS.map((item) => (
|
||||
<Tooltip
|
||||
key={item.key}
|
||||
title={
|
||||
item.key === 'refresh_goods'
|
||||
? '使用一个选中的账号刷新当前 SID 兑换商品'
|
||||
: item.key === 'refresh_recharge_goods'
|
||||
? '使用一个选中的账号刷新充值商品列表'
|
||||
: item.key === 'exchange_goods'
|
||||
? '按商品页选择商品,支持立即或定时兑换'
|
||||
: item.key === 'create_recharge_order'
|
||||
? '按商品页选择生成扫码支付二维码'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={item.icon}
|
||||
size="small"
|
||||
onClick={() => startTask(item.key)}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
{taskTypes[item.key] || item.key}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{taskSummary}
|
||||
{renderTaskTable(12)}
|
||||
<RealtimeLogPanel
|
||||
logs={logs}
|
||||
connected={wsConnected}
|
||||
title="虎牙实时日志"
|
||||
emptyText="暂无虎牙任务日志"
|
||||
collapsible
|
||||
spinWhenEmpty
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={concurrency}
|
||||
onChange={(value) => setConcurrency(value || 1)}
|
||||
addonBefore="并发"
|
||||
style={{ width: 130, flexShrink: 0 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={starting}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
onClick={() => startTask()}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{QUICK_ACTIONS.map((item) => (
|
||||
<Tooltip
|
||||
key={item.key}
|
||||
title={
|
||||
item.key === 'refresh_goods'
|
||||
? '使用一个选中的 CK 刷新当前 SID 兑换商品'
|
||||
: item.key === 'refresh_recharge_goods'
|
||||
? '使用一个选中的 CK 刷新充值商品列表'
|
||||
: item.key === 'exchange_goods'
|
||||
? '按左侧选择商品,支持立即或定时兑换'
|
||||
: item.key === 'create_recharge_order'
|
||||
? '按左侧选择生成扫码支付二维码'
|
||||
: undefined
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'goods',
|
||||
label: <Space><ShoppingOutlined />商品</Space>,
|
||||
children: (
|
||||
<Row gutter={12}>
|
||||
<Col xs={24} xl={12}>
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><ShoppingOutlined />兑换商品</Space>}
|
||||
extra={(
|
||||
<Space size={6}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => startTask('refresh_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={() => startTask('exchange_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
兑换
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Button
|
||||
icon={item.icon}
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择兑换商品"
|
||||
value={selectedExchangeGoodsId || undefined}
|
||||
onChange={(value) => setSelectedExchangeGoodsId(value || '')}
|
||||
options={goodsOptions}
|
||||
style={{ minWidth: 220 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<DatePicker
|
||||
showTime
|
||||
allowClear
|
||||
value={exchangeAt}
|
||||
onChange={setExchangeAt}
|
||||
placeholder="立即兑换"
|
||||
style={{ width: 190 }}
|
||||
/>
|
||||
</Space>
|
||||
{goodsCategories.length > 0 && (
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={selectedGoodsCategory}
|
||||
onChange={setSelectedGoodsCategory}
|
||||
items={goodsCategories.map((item) => ({
|
||||
key: item.key,
|
||||
label: `${item.label} (${item.count})`,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
columns={goodsColumns}
|
||||
dataSource={filteredGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
onClick={() => startTask(item.key)}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
{taskTypes[item.key] || item.key}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
当前已接入查询积分、绑定、兑换记录、兑换商品列表、商品兑换、充值商品列表和扫码支付。
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
pagination={false}
|
||||
scroll={{ y: 360 }}
|
||||
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedExchangeGoodsId(record.product_id),
|
||||
})}
|
||||
rowClassName={(record) => record.product_id === selectedExchangeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8, color: token.colorTextSecondary }}>
|
||||
<span>共 <b>{tasks.length}</b> 个任务</span>
|
||||
<span>已计划 <b>{plannedCount}</b></span>
|
||||
<span>成功 <b style={{ color: token.colorSuccess }}>{successCount}</b></span>
|
||||
<span>失败 <b style={{ color: token.colorError }}>{failedCount}</b></span>
|
||||
</div>
|
||||
<Table
|
||||
columns={taskColumns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ pageSize: 12, showTotal: (total) => `共 ${total} 条` }}
|
||||
scroll={{ x: 920 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Col xs={24} xl={12}>
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><CreditCardOutlined />充值商品</Space>}
|
||||
extra={(
|
||||
<Space size={6}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => startTask('refresh_recharge_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<QrcodeOutlined />}
|
||||
onClick={() => startTask('create_recharge_order')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
支付码
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择充值商品"
|
||||
value={selectedRechargeGoodsId || undefined}
|
||||
onChange={(value) => setSelectedRechargeGoodsId(value || '')}
|
||||
options={rechargeGoodsOptions}
|
||||
style={{ minWidth: 220 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={999}
|
||||
value={rechargeCount}
|
||||
onChange={(value) => setRechargeCount(value || 1)}
|
||||
addonAfter="份"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Select
|
||||
value={rechargePayChannel}
|
||||
onChange={setRechargePayChannel}
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
]}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
columns={rechargeGoodsColumns}
|
||||
dataSource={sortedRechargeGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 410 }}
|
||||
locale={{ emptyText: '暂无充值商品,请先刷新充值商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedRechargeGoodsId(record.spu_id),
|
||||
})}
|
||||
rowClassName={(record) => record.spu_id === selectedRechargeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: <Space><SettingOutlined />配置</Space>,
|
||||
children: (
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><SettingOutlined />虎牙配置</Space>}
|
||||
extra={canConfig && (
|
||||
<Button size="small" type="primary" onClick={saveConfig} loading={savingConfig}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
style={{ maxWidth: 760 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" disabled={!canConfig}>
|
||||
<Row gutter={8}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item label="直播间 ID" name="room_pid">
|
||||
<Input placeholder="默认 1199650619883" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item label="SID" name="sid">
|
||||
<Input placeholder="默认 2203" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item label="兑换活动 ID" name="outer_act_id">
|
||||
<Input placeholder="默认 9504" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item label="绑定 bActId" name="bind_act_id">
|
||||
<Input placeholder="默认 9271" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item label="支付渠道" name="pay_channel">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'records',
|
||||
label: <Space><FieldTimeOutlined />任务记录</Space>,
|
||||
children: (
|
||||
<>
|
||||
{taskSummary}
|
||||
{renderTaskTable(20)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RealtimeLogPanel
|
||||
logs={logs}
|
||||
connected={wsConnected}
|
||||
title="虎牙实时日志"
|
||||
emptyText="暂无虎牙任务日志"
|
||||
collapsible
|
||||
spinWhenEmpty
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="兑换记录"
|
||||
open={!!exchangeRecordsTask}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) {
|
||||
}}>
|
||||
<Card style={{ width: 380, boxShadow: isDark ? '0 8px 24px rgba(0,0,0,0.4)' : '0 8px 24px rgba(0,0,0,0.15)' }}>
|
||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
斗鱼批量登录后台
|
||||
直播运营后台
|
||||
</Title>
|
||||
<Text type="secondary" style={{
|
||||
display: 'block',
|
||||
|
||||
Reference in New Issue
Block a user