Cookie导出增加格式选择

- 后端: export接口增加 format 参数,支持 csv 和 custom 格式
- custom 格式: 账号----密码----ck,导出为 txt
- 后端: 导出用批量查 Account 替代逐条查询,消除 N+1
- 前端: 导出按钮改为下拉菜单可选格式
- 修复: blob 导出时不再取 .data(拦截器已提取)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-22 20:20:59 +08:00
co-authored by Claude Fable 5
parent e7edb56107
commit 1b20317b42
3 changed files with 56 additions and 21 deletions
+38 -12
View File
@@ -65,28 +65,54 @@ def list_cookies(
@router.get("/export") @router.get("/export")
def export_cookies( def export_cookies(
format: str = "csv",
db: Session = Depends(get_db), db: Session = Depends(get_db),
current: User = Depends(require_permission("cookie:export")), current: User = Depends(require_permission("cookie:export")),
): ):
"""导出 Cookie 为 CSV。""" """导出 Cookie,支持 csv 和 custom 格式。
csv: 账号, Cookie, 时间
custom: 账号----密码----ck
"""
tasks = db.query(LoginTask).filter( tasks = db.query(LoginTask).filter(
LoginTask.status == "success" LoginTask.status == "success"
).order_by(LoginTask.finished_at.desc()).all() ).order_by(LoginTask.finished_at.desc()).all()
output = io.StringIO() # 批量查账号,避免 N+1
writer = csv.writer(output) account_ids = [t.account_id for t in tasks]
writer.writerow(["账号", "Cookie", "时间"]) accounts_map = {}
if account_ids:
accs = db.query(Account).filter(Account.id.in_(account_ids)).all()
accounts_map = {a.id: a for a in accs}
for t in tasks: if format == "custom":
acc = db.query(Account).filter(Account.id == t.account_id).first() # 账号----密码----ck
username = acc.username if acc else "" lines = []
writer.writerow([username, t.cookie or "", _fmt_dt(t.finished_at) or ""]) for t in tasks:
acc = accounts_map.get(t.account_id)
username = acc.username if acc else ""
password = acc.password if acc else ""
ck = t.cookie or ""
lines.append(f"{username}----{password}----{ck}")
content = "\n".join(lines)
filename = "cookies_custom.txt"
media = "text/plain"
else:
# CSV: 账号, Cookie, 时间
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["账号", "Cookie", "时间"])
for t in tasks:
acc = accounts_map.get(t.account_id)
username = acc.username if acc else ""
writer.writerow([username, t.cookie or "", _fmt_dt(t.finished_at) or ""])
content = output.getvalue()
filename = "cookies.csv"
media = "text/csv"
output.seek(0)
return StreamingResponse( return StreamingResponse(
iter([output.getvalue()]), iter([content]),
media_type="text/csv", media_type=media,
headers={"Content-Disposition": "attachment; filename=cookies.csv"}, headers={"Content-Disposition": f"attachment; filename={filename}"},
) )
+1 -1
View File
@@ -67,7 +67,7 @@ export const loginApi = {
export const cookieApi = { export const cookieApi = {
list: () => api.get<any, any[]>('/cookies'), list: () => api.get<any, any[]>('/cookies'),
exportCsv: () => api.get('/cookies/export', { responseType: 'blob' }), exportCsv: (format?: string) => api.get('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
delete: (id: number) => api.delete<any, any>(`/cookies/${id}`), delete: (id: number) => api.delete<any, any>(`/cookies/${id}`),
}; };
+17 -8
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm, Space, Typography, Input, theme } from 'antd'; 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 { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi } from '../api/modules'; import { cookieApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
@@ -34,13 +34,13 @@ export default function CookiePage() {
loadCookies(); loadCookies();
}, []); }, []);
const handleExport = async () => { const handleExport = async (format: string = 'csv') => {
try { try {
const res = await cookieApi.exportCsv(); const blob = await cookieApi.exportCsv(format);
const url = URL.createObjectURL(new Blob([res.data])); const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]));
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'cookies.csv'; a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv';
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
message.success('已导出'); message.success('已导出');
@@ -170,9 +170,18 @@ export default function CookiePage() {
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button> </Button>
{canExport && ( {canExport && (
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExport}> <Dropdown
CSV menu={{
</Button> items: [
{ key: 'csv', label: 'CSV(账号, Cookie, 时间)', onClick: () => handleExport('csv') },
{ key: 'custom', label: '自定义(账号----密码----ck', onClick: () => handleExport('custom') },
],
}}
>
<Button type="primary" icon={<DownloadOutlined />}>
</Button>
</Dropdown>
)} )}
</Space> </Space>
</div> </div>