- CookiePage: blob类型转换为BlobPart,移除Tag的size属性 - UsersPage: 移除Divider的orientation属性(类型不兼容)
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
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>
|
||
);
|
||
} |