增加斗鱼自定义CK筛选功能
This commit is contained in:
@@ -29,6 +29,18 @@ def _fmt_dt(dt) -> str | None:
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
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 有效性检测接口(抓包参考:钱包中心鱼丸余额 + 用户等级详情)
|
# 斗鱼 Cookie 有效性检测接口(抓包参考:钱包中心鱼丸余额 + 用户等级详情)
|
||||||
CHECK_UA = (
|
CHECK_UA = (
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
"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("")
|
@router.get("")
|
||||||
def list_cookies(
|
def list_cookies(
|
||||||
search: str = Query(""),
|
search: str = Query(""),
|
||||||
|
account_names: str = Query(""),
|
||||||
page: int | None = Query(None, ge=1),
|
page: int | None = Query(None, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=200),
|
page_size: int = Query(20, ge=1, le=200),
|
||||||
include_cookie: bool = Query(True),
|
include_cookie: bool = Query(True),
|
||||||
@@ -143,6 +156,13 @@ def list_cookies(
|
|||||||
joinedload(LoginTask.account).joinedload(Account.assigned_user),
|
joinedload(LoginTask.account).joinedload(Account.assigned_user),
|
||||||
)
|
)
|
||||||
search_text = (search or "").strip()
|
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:
|
if search_text:
|
||||||
pattern = f"%{search_text}%"
|
pattern = f"%{search_text}%"
|
||||||
if user_has_permission(current, "login:view_all"):
|
if user_has_permission(current, "login:view_all"):
|
||||||
@@ -228,6 +248,7 @@ def cookies_summary(
|
|||||||
@router.get("/export")
|
@router.get("/export")
|
||||||
def export_cookies(
|
def export_cookies(
|
||||||
format: str = "csv",
|
format: str = "csv",
|
||||||
|
account_names: str = Query(""),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current: User = Depends(require_permission("cookie:export")),
|
current: User = Depends(require_permission("cookie:export")),
|
||||||
):
|
):
|
||||||
@@ -235,7 +256,15 @@ def export_cookies(
|
|||||||
csv: 账号, Cookie, 时间
|
csv: 账号, Cookie, 时间
|
||||||
custom: 账号----密码----ck
|
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
|
# 批量查账号,避免 N+1
|
||||||
account_ids = [t.account_id for t in tasks]
|
account_ids = [t.account_id for t in tasks]
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PagePa
|
|||||||
|
|
||||||
export const cookieApi = {
|
export const cookieApi = {
|
||||||
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
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 }),
|
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
||||||
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
||||||
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
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[]) =>
|
check: (ids: number[]) =>
|
||||||
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
|
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
|
||||||
relogin: (ids: number[]) =>
|
relogin: (ids: number[]) =>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface PageParams {
|
|||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
account_names?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountBulkSelection {
|
export interface AccountBulkSelection {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd';
|
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd';
|
||||||
import { message } from '../utils/antdMessage';
|
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 { cookieApi, type BasicSummary, type CookieCheckResult, type CookieItem } from '../api/modules';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { formatTime } from '../utils/time';
|
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() {
|
export default function CookiePage() {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const [cookies, setCookies] = useState<CookieItem[]>([]);
|
const [cookies, setCookies] = useState<CookieItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [customNamesText, setCustomNamesText] = useState('');
|
||||||
|
const [customAccountNames, setCustomAccountNames] = useState<string[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
|
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
|
||||||
const [pageSize, setPageSize] = useState(() => {
|
const [pageSize, setPageSize] = useState(() => {
|
||||||
@@ -148,6 +160,7 @@ export default function CookiePage() {
|
|||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
search: searchText.trim() || undefined,
|
search: searchText.trim() || undefined,
|
||||||
|
account_names: customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined,
|
||||||
include_cookie: false,
|
include_cookie: false,
|
||||||
});
|
});
|
||||||
setCookies(data.items);
|
setCookies(data.items);
|
||||||
@@ -174,7 +187,7 @@ export default function CookiePage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize, searchText]);
|
}, [currentPage, pageSize, searchText, customAccountNames]);
|
||||||
|
|
||||||
const loadSummary = useCallback(async () => {
|
const loadSummary = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -195,19 +208,60 @@ export default function CookiePage() {
|
|||||||
|
|
||||||
const handleExport = async (format: string = 'csv') => {
|
const handleExport = async (format: string = 'csv') => {
|
||||||
try {
|
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 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 = format === 'custom' ? 'cookies_custom.txt' : '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(customAccountNames.length > 0 ? '已导出匹配的 Cookie' : '已导出');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(getErrorMessage(e));
|
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) => {
|
const handleCopyCookie = async (record: CookieItem) => {
|
||||||
try {
|
try {
|
||||||
const detail = await cookieApi.get(record.id);
|
const detail = await cookieApi.get(record.id);
|
||||||
@@ -475,6 +529,30 @@ export default function CookiePage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="搜索账号或分配客服"
|
placeholder="搜索账号或分配客服"
|
||||||
|
|||||||
Reference in New Issue
Block a user