增加斗鱼自定义CK筛选功能
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -3,11 +3,17 @@ import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PagePa
|
||||
|
||||
export const cookieApi = {
|
||||
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
||||
listPaged: (params: PageParams & { include_cookie?: boolean }) =>
|
||||
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
||||
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
||||
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
||||
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
||||
exportCsv: (format?: string) => api.get<Blob, Blob>('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
|
||||
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/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[]) =>
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface PageParams {
|
||||
page: number;
|
||||
page_size: number;
|
||||
search?: string;
|
||||
account_names?: string;
|
||||
}
|
||||
|
||||
export interface AccountBulkSelection {
|
||||
|
||||
@@ -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<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function parseAccountNames(text: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
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<CookieItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [customNamesText, setCustomNamesText] = useState('');
|
||||
const [customAccountNames, setCustomAccountNames] = useState<string[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [summary, setSummary] = useState<BasicSummary>({ 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() {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card size="small" title="获取自定义 CK" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
<Input.TextArea
|
||||
value={customNamesText}
|
||||
onChange={(e) => setCustomNamesText(e.target.value)}
|
||||
placeholder="从 Excel 复制斗鱼账户名后粘贴到这里,每行一个"
|
||||
autoSize={{ minRows: 3, maxRows: 7 }}
|
||||
/>
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<FilterOutlined />} onClick={handleApplyCustomFilter}>
|
||||
筛选自定义 CK
|
||||
</Button>
|
||||
<Button icon={<CopyOutlined />} onClick={handleCopyCustomCookies} disabled={customAccountNames.length === 0}>
|
||||
复制匹配 CK
|
||||
</Button>
|
||||
<Button onClick={handleClearCustomFilter} disabled={!customNamesText && customAccountNames.length === 0}>
|
||||
清空
|
||||
</Button>
|
||||
{customAccountNames.length > 0 && (
|
||||
<Text type="secondary">已载入 {customAccountNames.length} 个账户名,匹配结果共 {total} 条</Text>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Input.Search
|
||||
placeholder="搜索账号或分配客服"
|
||||
|
||||
Reference in New Issue
Block a user