Files
live-hub-py/web/frontend/src/pages/CookiePage.tsx
T
yml2213 56de33a285 修复前端TypeScript编译错误
- CookiePage: blob类型转换为BlobPart,移除Tag的size属性
- UsersPage: 移除Divider的orientation属性(类型不兼容)
2026-06-23 00:59:02 +08:00

237 lines
7.1 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 { useEffect, useState } from 'react';
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth';
import { formatTime } from '../utils/time';
const { Text } = Typography;
export default function CookiePage() {
const { token } = theme.useToken();
const [cookies, setCookies] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const user = getUser();
const canView = hasPerm(user, 'cookie:view');
const canExport = hasPerm(user, 'cookie:export');
const loadCookies = async () => {
setLoading(true);
try {
const data = await cookieApi.list();
setCookies(data);
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadCookies();
}, []);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await cookieApi.exportCsv(format);
const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob as any]));
const a = document.createElement('a');
a.href = url;
a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv';
a.click();
URL.revokeObjectURL(url);
message.success('已导出');
} catch (e: any) {
message.error(e.message);
}
};
const handleCopyCookie = (cookie: string, username: string) => {
if (!cookie) {
message.warning('Cookie 为空');
return;
}
navigator.clipboard.writeText(cookie).then(() => {
message.success(`已复制 ${username} 的 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleCopySelected = () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
const selected = cookies.filter((c) => selectedRowKeys.includes(c.id));
const text = selected
.map((c) => `${c.account_username}: ${c.cookie || '(空)'}`)
.join('\n');
navigator.clipboard.writeText(text).then(() => {
message.success(`已复制 ${selected.length} 条 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleDelete = async (id: number) => {
try {
await cookieApi.delete(id);
message.success('已删除');
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadCookies();
} catch (e: any) {
message.error(e.message);
}
};
// 筛选
const filteredCookies = cookies.filter((c) => {
if (!searchText) return true;
const s = searchText.toLowerCase();
return (
c.account_username?.toLowerCase().includes(s) ||
c.assigned_username?.toLowerCase().includes(s)
);
});
const columns: any[] = [
{ title: 'ID', dataIndex: 'id', width: 60, align: 'center' },
{
title: '账号',
dataIndex: 'account_username',
width: 140,
ellipsis: true,
},
{
title: '分配',
dataIndex: 'assigned_username',
width: 100,
align: 'center',
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: (val: string) => {
if (!canView) return <Tag>***</Tag>;
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{val}</span>;
},
},
{
title: '时间',
dataIndex: 'created_at',
width: 180,
align: 'center',
render: (val: string) => formatTime(val),
},
{
title: '操作',
width: 130,
align: 'center',
fixed: 'right',
render: (_: any, record: any) => (
<Space size={4}>
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyCookie(record.cookie, record.account_username)}
>
复制
</Button>
{canExport && (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />} />
</Popconfirm>
)}
</Space>
),
},
];
const assignedCount = cookies.filter((c) => c.assigned_to).length;
const unassignedCount = cookies.length - assignedCount;
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 && (
<Dropdown
menu={{
items: [
{ key: 'csv', label: 'CSV(账号, 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={cookies.length} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={assignedCount}
valueStyle={{ color: token.colorSuccess }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="未分配"
value={unassignedCount}
valueStyle={{ color: token.colorError }}
/>
</Card>
</Col>
</Row>
<div style={{ marginBottom: 12 }}>
<Input.Search
placeholder="搜索账号或分配客服"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: 260 }}
size="small"
prefix={<SearchOutlined />}
/>
</div>
<Table
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
columns={columns}
dataSource={filteredCookies}
rowKey="id"
loading={loading}
size="small"
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `共 ${t} 条` }}
scroll={{ x: 900 }}
/>
</div>
);
}