From 65271124c02e14b171b7131509a51c05692b702c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 8 Aug 2026 19:03:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=97=E9=B1=BC=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89CK=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/backend/routers/cookies.py | 31 +++++++++- web/frontend/src/api/cookies.ts | 10 +++- web/frontend/src/api/types.ts | 1 + web/frontend/src/pages/CookiePage.tsx | 86 +++++++++++++++++++++++++-- 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index 5f27db5..7b1ebd5 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -29,6 +29,18 @@ def _fmt_dt(dt) -> str | None: router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"]) + +def _parse_account_names(raw_names: str) -> list[str]: + """解析前端粘贴的 Excel 账号名,每行一个并去重。""" + names = [] + seen = set() + for value in (raw_names or "").replace("\r\n", "\n").replace("\r", "\n").split("\n"): + name = value.strip() + if name and name not in seen: + names.append(name) + seen.add(name) + return names + # 斗鱼 Cookie 有效性检测接口(抓包参考:钱包中心鱼丸余额 + 用户等级详情) CHECK_UA = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " @@ -129,6 +141,7 @@ def _visible_cookie_tasks_query(db: Session, current: User): @router.get("") def list_cookies( search: str = Query(""), + account_names: str = Query(""), page: int | None = Query(None, ge=1), page_size: int = Query(20, ge=1, le=200), include_cookie: bool = Query(True), @@ -143,6 +156,13 @@ def list_cookies( joinedload(LoginTask.account).joinedload(Account.assigned_user), ) search_text = (search or "").strip() + selected_names = _parse_account_names(account_names) + if selected_names: + query = query.filter( + LoginTask.account_id.in_( + db.query(Account.id).filter(Account.username.in_(selected_names)) + ) + ) if search_text: pattern = f"%{search_text}%" if user_has_permission(current, "login:view_all"): @@ -228,6 +248,7 @@ def cookies_summary( @router.get("/export") def export_cookies( format: str = "csv", + account_names: str = Query(""), db: Session = Depends(get_db), current: User = Depends(require_permission("cookie:export")), ): @@ -235,7 +256,15 @@ def export_cookies( csv: 账号, Cookie, 时间 custom: 账号----密码----ck """ - tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all() + query = _visible_cookie_tasks_query(db, current) + selected_names = _parse_account_names(account_names) + if selected_names: + query = query.filter( + LoginTask.account_id.in_( + db.query(Account.id).filter(Account.username.in_(selected_names)) + ) + ) + tasks = query.order_by(LoginTask.finished_at.desc()).all() # 批量查账号,避免 N+1 account_ids = [t.account_id for t in tasks] diff --git a/web/frontend/src/api/cookies.ts b/web/frontend/src/api/cookies.ts index f08cc87..4758c95 100644 --- a/web/frontend/src/api/cookies.ts +++ b/web/frontend/src/api/cookies.ts @@ -3,11 +3,17 @@ import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PagePa export const cookieApi = { list: () => api.get('/cookies'), - listPaged: (params: PageParams & { include_cookie?: boolean }) => + listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) => api.get, PaginatedResponse>('/cookies', { params }), summary: () => api.get('/cookies/summary'), get: (id: number) => api.get(`/cookies/${id}`), - exportCsv: (format?: string) => api.get('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }), + exportCsv: (format?: string, accountNames?: string) => api.get('/cookies/export', { + responseType: 'blob', + params: { + ...(format ? { format } : {}), + ...(accountNames ? { account_names: accountNames } : {}), + }, + }), check: (ids: number[]) => api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }), relogin: (ids: number[]) => diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 8b66e7d..67bb2a2 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -21,6 +21,7 @@ export interface PageParams { page: number; page_size: number; search?: string; + account_names?: string; } export interface AccountBulkSelection { diff --git a/web/frontend/src/pages/CookiePage.tsx b/web/frontend/src/pages/CookiePage.tsx index 1c0e231..e4ced1b 100644 --- a/web/frontend/src/pages/CookiePage.tsx +++ b/web/frontend/src/pages/CookiePage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback } from 'react'; import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd'; import { message } from '../utils/antdMessage'; -import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined } from '@ant-design/icons'; +import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined } from '@ant-design/icons'; import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieItem } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; @@ -37,12 +37,24 @@ function copyToClipboard(text: string): Promise { }); } +function parseAccountNames(text: string): string[] { + const seen = new Set(); + return text + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .split('\n') + .map((name) => name.trim()) + .filter((name) => name.length > 0 && !seen.has(name) && seen.add(name)); +} + 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 [customNamesText, setCustomNamesText] = useState(''); + const [customAccountNames, setCustomAccountNames] = useState([]); const [total, setTotal] = useState(0); const [summary, setSummary] = useState({ total: 0, assigned_count: 0, unassigned_count: 0 }); const [pageSize, setPageSize] = useState(() => { @@ -148,6 +160,7 @@ export default function CookiePage() { page: currentPage, page_size: pageSize, search: searchText.trim() || undefined, + account_names: customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined, include_cookie: false, }); setCookies(data.items); @@ -174,7 +187,7 @@ export default function CookiePage() { } finally { setLoading(false); } - }, [currentPage, pageSize, searchText]); + }, [currentPage, pageSize, searchText, customAccountNames]); const loadSummary = useCallback(async () => { try { @@ -195,19 +208,60 @@ export default function CookiePage() { const handleExport = async (format: string = 'csv') => { try { - const blob = await cookieApi.exportCsv(format); + const blob = await cookieApi.exportCsv( + format, + customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined, + ); 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('已导出'); + message.success(customAccountNames.length > 0 ? '已导出匹配的 Cookie' : '已导出'); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; + const handleApplyCustomFilter = () => { + const names = parseAccountNames(customNamesText); + if (names.length === 0) { + message.warning('请先粘贴斗鱼账户名,每行一个'); + return; + } + setCustomAccountNames(names); + setSelectedRowKeys([]); + setCurrentPage(1); + message.success(`已载入 ${names.length} 个账户名,正在筛选 Cookie`); + }; + + const handleClearCustomFilter = () => { + setCustomNamesText(''); + setCustomAccountNames([]); + setSelectedRowKeys([]); + setCurrentPage(1); + }; + + const handleCopyCustomCookies = async () => { + if (customAccountNames.length === 0) { + message.warning('请先筛选自定义账户名'); + return; + } + try { + const blob = await cookieApi.exportCsv('custom', customAccountNames.join('\n')); + const text = blob instanceof Blob ? await blob.text() : String(blob); + if (!text.trim()) { + message.warning('没有找到对应的 Cookie'); + return; + } + await copyToClipboard(text); + message.success(`已复制 ${total} 条匹配的自定义 CK`); + } catch (e: unknown) { + message.error(getErrorMessage(e) || '复制失败'); + } + }; + const handleCopyCookie = async (record: CookieItem) => { try { const detail = await cookieApi.get(record.id); @@ -475,6 +529,30 @@ export default function CookiePage() { + + + setCustomNamesText(e.target.value)} + placeholder="从 Excel 复制斗鱼账户名后粘贴到这里,每行一个" + autoSize={{ minRows: 3, maxRows: 7 }} + /> + + + + + {customAccountNames.length > 0 && ( + 已载入 {customAccountNames.length} 个账户名,匹配结果共 {total} 条 + )} + + +