Files
live-hub-py/web/frontend/src/pages/HuyaCookiePage.tsx
T
yml2213andCursor e81119117f feat(huya): 多选登录改单线程队列并拆分路由与工作台
默认同时只登一个号,失败两次跳下一个,号间留间隔,结束后可一键重试失败账号。
顺带拆分虎牙路由与斗鱼工作台页面,并补齐 raw_cookie 重建入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 11:31:50 +08:00

340 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import {
Button, Card, Col, Dropdown, Input, Popconfirm, Row, Space, Statistic, Table, Tag, theme, Typography,
} from 'antd';
import { message } from '../utils/antdMessage';
import { CopyOutlined, DeleteOutlined, DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { huyaApi, type BasicSummary, type HuyaCookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
const { Text } = Typography;
function copyToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
return navigator.clipboard.writeText(text);
}
return new Promise((resolve, reject) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const ok = document.execCommand('copy');
document.body.removeChild(textarea);
if (ok) resolve();
else reject(new Error('execCommand copy failed'));
} catch (e) {
document.body.removeChild(textarea);
reject(e);
}
});
}
export default function HuyaCookiePage() {
const { token } = theme.useToken();
const [cookies, setCookies] = useState<HuyaCookieItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const value = localStorage.getItem('huya_cookie_page_size');
return value ? Number(value) || 20 : 20;
});
const [currentPage, setCurrentPage] = useState(1);
const { can } = usePermissions();
const canManage = can('huya:account');
const canView = can('huya:cookie:view') || can('huya:cookie:export') || canManage;
const canExport = can('huya:cookie:export') || canManage;
const canRebuild = can('huya:import') || can('huya:task') || canExport;
const loadCookies = useCallback(async () => {
setLoading(true);
try {
const data = await huyaApi.listCookiesPaged({
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setCookies(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
const loadSummary = useCallback(async () => {
try {
const data = await huyaApi.cookiesSummary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
useEffect(() => {
loadCookies();
}, [loadCookies]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await huyaApi.exportCookies(format);
const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]));
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = format === 'custom' ? 'huya_cookies_custom.txt' : 'huya_cookies.csv';
anchor.click();
URL.revokeObjectURL(url);
message.success('已导出');
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const handleCopyCookie = async (record: HuyaCookieItem) => {
try {
const detail = await huyaApi.getCookie(record.id);
if (!detail.cookie) {
message.warning('Cookie 为空');
return;
}
const text = `${detail.account_username}----${detail.cookie}`;
await copyToClipboard(text);
message.success(`已复制 ${detail.account_username} 的 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleCopySelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
try {
const selected = await Promise.all(selectedRowKeys.map((key) => huyaApi.getCookie(Number(key))));
const text = selected.map((item) => `${item.account_username}----${item.cookie || ''}`).join('\r\n');
await copyToClipboard(text);
message.success(`已复制 ${selected.length} 条 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleDelete = async (id: number) => {
try {
await huyaApi.deleteCookie(id);
message.success('已清除');
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const handleRebuild = async (record: HuyaCookieItem) => {
try {
await huyaApi.rebuildWebCookie(record.account_id);
message.success(`已从原始凭据重建 ${record.account_username} 的网页 Cookie`);
loadCookies();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const handleDeleteSelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择要清除的 Cookie');
return;
}
try {
const result = await huyaApi.deleteCookies(selectedRowKeys.map((key) => Number(key)));
message.success(result.message);
setSelectedRowKeys([]);
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const columns = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' as const },
{
title: '虎牙账号',
dataIndex: 'account_username',
width: 160,
ellipsis: true,
},
{
title: 'UID',
dataIndex: 'uid',
width: 130,
ellipsis: true,
},
{
title: '分配',
dataIndex: 'assigned_username',
width: 110,
align: 'center' as const,
render: (name: string | null) =>
name ? <Tag color="green">{name}</Tag> : <Text type="secondary" style={{ fontSize: 12 }}>未分配</Text>,
},
{
title: 'Cookie',
dataIndex: 'cookie_preview',
ellipsis: true,
render: (value: string) => {
if (!canView) return <Tag>***</Tag>;
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{value}</span>;
},
},
{
title: '更新时间',
dataIndex: 'created_at',
width: 180,
align: 'center' as const,
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
},
{
title: '操作',
width: 210,
align: 'center' as const,
fixed: 'right' as const,
render: (_: unknown, record: HuyaCookieItem) => (
<Space size={4}>
<Button size="small" icon={<CopyOutlined />} onClick={() => handleCopyCookie(record)}>
复制
</Button>
{canRebuild && record.has_raw_cookie && (
<Popconfirm
title="用 App 原始凭据重建网页 Cookie?"
description="不会重新登录或下单,只补齐网页设备字段。"
onConfirm={() => handleRebuild(record)}
>
<Button size="small" icon={<ReloadOutlined />}>重建</Button>
</Popconfirm>
)}
{canExport && (
<Popconfirm title="确认清除这条 Cookie?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />} />
</Popconfirm>
)}
</Space>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}>虎牙 Cookie 管理</h2>
<Space>
<Button icon={<CopyOutlined />} onClick={handleCopySelected} disabled={selectedRowKeys.length === 0}>
复制选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
{canExport && (
<Popconfirm
title={`确认清除选中的 ${selectedRowKeys.length} 条虎牙 Cookie?`}
onConfirm={handleDeleteSelected}
disabled={selectedRowKeys.length === 0}
>
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
清除选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
</Popconfirm>
)}
{canExport && (
<Dropdown
menu={{
items: [
{ key: 'csv', label: 'CSV(账号, UID, Cookie, 时间)', onClick: () => handleExport('csv') },
{ key: 'custom', label: '自定义(账号----ck)', onClick: () => handleExport('custom') },
],
}}
>
<Button type="primary" icon={<DownloadOutlined />}>
导出
</Button>
</Dropdown>
)}
</Space>
</div>
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="Cookie 总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="已分配" value={summary.assigned_count} styles={{ content: { color: token.colorSuccess } }} />
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="未分配" value={summary.unassigned_count} styles={{ content: { color: token.colorError } }} />
</Card>
</Col>
</Row>
<div style={{ marginBottom: 12 }}>
<Input.Search
placeholder="搜索账号、UID 或分配客服"
allowClear
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 300 }}
size="small"
prefix={<SearchOutlined />}
/>
</div>
<Table
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
columns={columns}
dataSource={cookies}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
onChange: (page, size) => {
setCurrentPage(page);
if (size !== pageSize) {
setPageSize(size);
localStorage.setItem('huya_cookie_page_size', String(size));
setCurrentPage(1);
}
},
}}
scroll={{ x: 960 }}
/>
</div>
);
}