import { useEffect, useState, useCallback } 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, type CookieItem } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; const { Text } = Typography; /** 复制文本到剪贴板,兼容非安全上下文(http://非localhost) */ function copyToClipboard(text: string): Promise { if (navigator.clipboard?.writeText) { return navigator.clipboard.writeText(text); } // fallback:利用 textarea + execCommand,兼容 http 远程访问 return new Promise((resolve, reject) => { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); try { const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) { resolve(); } else { reject(new Error('execCommand copy failed')); } } catch (e) { document.body.removeChild(ta); reject(e); } }); } export default function CookiePage() { const { token } = theme.useToken(); const [cookies, setCookies] = useState([]); const [loading, setLoading] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [searchText, setSearchText] = useState(''); const [pageSize, setPageSize] = useState(() => { const v = localStorage.getItem('cookie_page_size'); return v ? Number(v) || 20 : 20; }); const [currentPage, setCurrentPage] = useState(1); const { can } = usePermissions(); const canView = can('cookie:view'); const canExport = can('cookie:export'); const loadCookies = useCallback(async () => { setLoading(true); try { const data = await cookieApi.list(); setCookies(data); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setLoading(false); } }, []); useEffect(() => { loadCookies(); }, [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])); 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: unknown) { message.error(getErrorMessage(e)); } }; const handleCopyCookie = (cookie: string, username: string) => { if (!cookie) { message.warning('Cookie 为空'); return; } copyToClipboard(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'); copyToClipboard(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: unknown) { message.error(getErrorMessage(e)); } }; const handleDeleteSelected = async () => { if (selectedRowKeys.length === 0) { message.warning('请先选择要删除的 Cookie'); return; } try { const ids = selectedRowKeys.map((k) => Number(k)); const res = await cookieApi.deleteBatch(ids); message.success(res.message); setSelectedRowKeys([]); loadCookies(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; // 筛选 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 = [ { title: 'ID', dataIndex: 'id', width: 60, align: 'center' as const }, { title: '账号', dataIndex: 'account_username', width: 140, ellipsis: true, }, { title: '分配', dataIndex: 'assigned_username', width: 100, align: 'center' as const, render: (name: string | null) => name ? {name} : 未分配, }, { title: 'Cookie', dataIndex: 'cookie_preview', ellipsis: true, render: (val: string) => { if (!canView) return ***; return {val}; }, }, { title: '时间', dataIndex: 'created_at', width: 180, align: 'center' as const, render: (val: string) => formatTime(val), }, { title: '操作', width: 130, align: 'center' as const, fixed: 'right' as const, render: (_: unknown, record: CookieItem) => ( {canExport && ( handleDelete(record.id)}> {canExport && ( )} {canExport && ( handleExport('csv') }, { key: 'custom', label: '自定义(账号----密码----ck)', onClick: () => handleExport('custom') }, ], }} > )}
setSearchText(e.target.value)} style={{ width: 260 }} size="small" prefix={} />
setSelectedRowKeys(keys), }} columns={columns} dataSource={filteredCookies} rowKey="id" loading={loading} size="small" pagination={{ current: currentPage, pageSize, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); localStorage.setItem('cookie_page_size', String(size)); setCurrentPage(1); // 切换 pageSize 时重置到第1页 } }, }} scroll={{ x: 900 }} /> ); }