默认同时只登一个号,失败两次跳下一个,号间留间隔,结束后可一键重试失败账号。 顺带拆分虎牙路由与斗鱼工作台页面,并补齐 raw_cookie 重建入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
340 lines
11 KiB
TypeScript
340 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|