From 1b20317b424c9606275442a0fcf058e061105605 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 22 Jun 2026 20:20:59 +0800 Subject: [PATCH] =?UTF-8?q?Cookie=E5=AF=BC=E5=87=BA=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: export接口增加 format 参数,支持 csv 和 custom 格式 - custom 格式: 账号----密码----ck,导出为 txt - 后端: 导出用批量查 Account 替代逐条查询,消除 N+1 - 前端: 导出按钮改为下拉菜单可选格式 - 修复: blob 导出时不再取 .data(拦截器已提取) Co-Authored-By: Claude Fable 5 --- web/backend/routers/cookies.py | 50 ++++++++++++++++++++------- web/frontend/src/api/modules.ts | 2 +- web/frontend/src/pages/CookiePage.tsx | 25 +++++++++----- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index d2ae2d4..aa275de 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -65,28 +65,54 @@ def list_cookies( @router.get("/export") def export_cookies( + format: str = "csv", db: Session = Depends(get_db), current: User = Depends(require_permission("cookie:export")), ): - """导出 Cookie 为 CSV。""" + """导出 Cookie,支持 csv 和 custom 格式。 + csv: 账号, Cookie, 时间 + custom: 账号----密码----ck + """ tasks = db.query(LoginTask).filter( LoginTask.status == "success" ).order_by(LoginTask.finished_at.desc()).all() - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(["账号", "Cookie", "时间"]) + # 批量查账号,避免 N+1 + account_ids = [t.account_id for t in tasks] + 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: - acc = db.query(Account).filter(Account.id == t.account_id).first() - username = acc.username if acc else "" - writer.writerow([username, t.cookie or "", _fmt_dt(t.finished_at) or ""]) + if format == "custom": + # 账号----密码----ck + lines = [] + 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( - iter([output.getvalue()]), - media_type="text/csv", - headers={"Content-Disposition": "attachment; filename=cookies.csv"}, + iter([content]), + media_type=media, + headers={"Content-Disposition": f"attachment; filename={filename}"}, ) diff --git a/web/frontend/src/api/modules.ts b/web/frontend/src/api/modules.ts index 98e8722..2dd57c4 100644 --- a/web/frontend/src/api/modules.ts +++ b/web/frontend/src/api/modules.ts @@ -67,7 +67,7 @@ export const loginApi = { export const cookieApi = { list: () => api.get('/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(`/cookies/${id}`), }; diff --git a/web/frontend/src/pages/CookiePage.tsx b/web/frontend/src/pages/CookiePage.tsx index e8a71c4..2911cd0 100644 --- a/web/frontend/src/pages/CookiePage.tsx +++ b/web/frontend/src/pages/CookiePage.tsx @@ -1,5 +1,5 @@ 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 { cookieApi } from '../api/modules'; import { getUser, hasPerm } from '../store/auth'; @@ -34,13 +34,13 @@ export default function CookiePage() { loadCookies(); }, []); - const handleExport = async () => { + const handleExport = async (format: string = 'csv') => { try { - const res = await cookieApi.exportCsv(); - const url = URL.createObjectURL(new Blob([res.data])); + 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 = 'cookies.csv'; + a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv'; a.click(); URL.revokeObjectURL(url); message.success('已导出'); @@ -170,9 +170,18 @@ export default function CookiePage() { 复制选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`} {canExport && ( - + handleExport('csv') }, + { key: 'custom', label: '自定义(账号----密码----ck)', onClick: () => handleExport('custom') }, + ], + }} + > + + )}