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:
co-authored by
Claude Fable 5
parent
e7edb56107
commit
1b20317b42
@@ -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}"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user