优化列表分页和数据加载

This commit is contained in:
yml2213
2026-07-24 13:25:27 +08:00
parent 6840728da6
commit f91c05c108
17 changed files with 647 additions and 161 deletions
+64 -5
View File
@@ -1,8 +1,8 @@
"""账号管理路由"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func, or_
from sqlalchemy.orm import Session, defer, joinedload
from ..database import get_db
from ..models import User, Account, AuditLog, LoginTask
@@ -16,11 +16,15 @@ from ..services.account_service import (
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
@router.get("", response_model=list[AccountOut])
@router.get("")
def list_accounts(
assigned_only: bool = Query(False),
tag: str = Query(None),
has_cookie: bool = Query(False),
search: str = Query(""),
page: int | None = Query(None, ge=1),
page_size: int = Query(20, ge=1, le=200),
include_sensitive: bool = Query(True),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
@@ -44,7 +48,31 @@ def list_accounts(
if tag:
query = query.filter(Account.tag == tag)
accounts = query.order_by(Account.id).options(joinedload(Account.assigned_user)).all()
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
query = query.filter(or_(
Account.username.ilike(pattern),
Account.tag.ilike(pattern),
Account.remark.ilike(pattern),
))
total = None
if page is not None:
total = query.order_by(None).count()
query = query.order_by(Account.id)
if page is not None:
query = query.offset((page - 1) * page_size).limit(page_size)
can_include_sensitive = include_sensitive and user_has_permission(current, "account:view_full")
options = [joinedload(Account.assigned_user)]
if not can_include_sensitive:
options.extend([
defer(Account.password),
defer(Account.email),
defer(Account.email_password),
])
accounts = query.options(*options).all()
result = []
for acc in accounts:
item = AccountOut(
@@ -55,14 +83,45 @@ def list_accounts(
created_at=acc.created_at,
)
# 只有管理员可看完整字段(密码、邮箱等)
if user_has_permission(current, "account:view_full"):
if can_include_sensitive:
item.password = acc.password
item.email = acc.email
item.email_password = acc.email_password
result.append(item)
if page is not None:
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
return result
@router.get("/summary")
def accounts_summary(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""账号管理统计,避免前端为了卡片统计拉全量账号。"""
query = db.query(Account)
if not user_has_permission(current, "account:view_all"):
if user_has_permission(current, "account:view_assigned"):
query = query.filter(Account.assigned_to == current.id)
else:
raise HTTPException(status_code=403, detail="无权查看账号")
total = query.count()
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
tag_count = (
query.filter(Account.tag != "", Account.tag.isnot(None))
.with_entities(Account.tag)
.distinct()
.count()
)
return {
"total": total,
"assigned_count": assigned_count,
"unassigned_count": max(0, total - assigned_count),
"tag_count": tag_count,
}
@router.post("/import")
def import_accounts(
req: AccountImport,
+93 -6
View File
@@ -1,9 +1,10 @@
"""Cookie 管理路由"""
from datetime import timezone
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import or_
from sqlalchemy.orm import Session, defer, joinedload
import io
import csv
@@ -38,17 +39,52 @@ def _visible_cookie_tasks_query(db: Session, current: User):
@router.get("")
def list_cookies(
search: str = Query(""),
page: int | None = Query(None, ge=1),
page_size: int = Query(20, ge=1, le=200),
include_cookie: bool = Query(True),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""查看登录成功的 Cookie 列表。"""
tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all()
if not include_cookie:
query = _visible_cookie_tasks_query(db, current).options(defer(LoginTask.cookie))
else:
query = _visible_cookie_tasks_query(db, current).options(
joinedload(LoginTask.account).joinedload(Account.assigned_user),
)
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
if user_has_permission(current, "login:view_all"):
query = query.join(Account, LoginTask.account_id == Account.id)
query = query.outerjoin(User, Account.assigned_to == User.id).filter(or_(
Account.username.ilike(pattern),
User.username.ilike(pattern),
))
total = None
if page is not None:
total = query.order_by(None).count()
query = query.order_by(LoginTask.finished_at.desc())
if page is not None:
query = query.offset((page - 1) * page_size).limit(page_size)
tasks = query.all()
# 批量查账号,避免 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()
account_query = db.query(Account).filter(Account.id.in_(account_ids))
if not include_cookie:
account_query = account_query.options(
joinedload(Account.assigned_user),
defer(Account.password),
defer(Account.email),
defer(Account.email_password),
)
accs = account_query.all()
accounts_map = {a.id: a for a in accs}
result = []
@@ -63,8 +99,8 @@ def list_cookies(
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
"created_at": _fmt_dt(t.finished_at),
}
# 只有有 cookie:view 权限才返回 cookie 和密码(用于自定义格式复制)
if user_has_permission(current, "cookie:view"):
# 分页列表默认只返回预览,复制/导出时再获取完整敏感字段。
if include_cookie and user_has_permission(current, "cookie:view"):
cookie = t.cookie or ""
item["cookie"] = cookie
item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie
@@ -74,9 +110,60 @@ def list_cookies(
item["cookie_preview"] = "***"
item["account_password"] = ""
result.append(item)
if page is not None:
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
return result
@router.get("/summary")
def cookies_summary(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""Cookie 管理统计,避免前端为了卡片统计拉全量 Cookie。"""
query = _visible_cookie_tasks_query(db, current)
if user_has_permission(current, "login:view_all"):
query = query.join(Account, LoginTask.account_id == Account.id)
total = query.count()
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
return {
"total": total,
"assigned_count": assigned_count,
"unassigned_count": max(0, total - assigned_count),
}
@router.get("/{task_id}")
def get_cookie(
task_id: int,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。"""
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="记录不存在")
acc = db.query(Account).filter(Account.id == task.account_id).first()
item = {
"id": task.id,
"batch_id": task.batch_id,
"account_id": task.account_id,
"account_username": acc.username if acc else "",
"assigned_to": acc.assigned_to if acc else None,
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
"created_at": _fmt_dt(task.finished_at),
"cookie": "",
"cookie_preview": "***",
"account_password": "",
}
if user_has_permission(current, "cookie:view"):
cookie = task.cookie or ""
item["cookie"] = cookie
item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie
item["account_password"] = acc.password if acc else ""
return item
@router.get("/export")
def export_cookies(
format: str = "csv",
+188 -9
View File
@@ -8,8 +8,8 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func, or_
from sqlalchemy.orm import Session, defer, joinedload
from core.huya import (
HuyaCredentialError,
@@ -134,6 +134,31 @@ def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccou
)
def _account_out_light(account: HuyaAccount, *, has_password: bool = False) -> HuyaAccountOut:
"""虎牙账号列表轻量输出,不读取加密字段。"""
return HuyaAccountOut(
id=account.id,
uid=account.uid or "",
yyuid=account.yyuid or "",
username=account.username or "",
has_password=has_password,
nickname=account.nickname or "",
cookie="",
cookie_preview="***",
tag=account.tag or "",
remark=account.remark or "",
status=account.status or "",
points=account.points,
game_name=account.game_name or "",
game_channel=account.game_channel or "",
game_phone=account.game_phone or "",
assigned_to=account.assigned_to,
assigned_username=account.assigned_user.username if account.assigned_user else None,
created_at=account.created_at,
updated_at=account.updated_at,
)
def _sanitize_task_result(result: dict | None, *, include_images: bool = False) -> dict | None:
"""列表接口默认剥离 base64 图片,避免轮询每次传 1MB+ 数据。"""
if not isinstance(result, dict):
@@ -196,17 +221,50 @@ def _cookie_out(account: HuyaAccount, include_cookie: bool) -> dict:
}
def _cookie_out_light(account: HuyaAccount) -> dict:
"""虎牙 Cookie 列表轻量输出,不读取完整加密 Cookie。"""
account_name = account.nickname or account.username or account.uid or str(account.id)
return {
"id": account.id,
"account_id": account.id,
"account_username": account_name,
"uid": account.uid or "",
"yyuid": account.yyuid or "",
"assigned_to": account.assigned_to,
"assigned_username": account.assigned_user.username if account.assigned_user else None,
"created_at": account.updated_at.isoformat() if account.updated_at else None,
"cookie": "",
"cookie_preview": "***",
}
def _huya_password_state_map(db: Session, account_ids: list[int]) -> dict[int, bool]:
"""不解密密码字段,仅判断密文字段是否非空。"""
if not account_ids:
return {}
rows = (
db.query(HuyaAccount.id, (HuyaAccount.account_password != "").label("has_password"))
.filter(HuyaAccount.id.in_(account_ids))
.all()
)
return {account_id: bool(has_password) for account_id, has_password in rows}
@router.get("/task-types")
def task_types(current: User = Depends(require_permission("huya:task"))):
"""返回当前规划的虎牙任务类型。"""
return SUPPORTED_TASK_TYPES
@router.get("/accounts", response_model=list[HuyaAccountOut])
@router.get("/accounts")
def list_accounts(
assigned_only: bool = Query(False),
tag: str | None = Query(None),
has_cookie: bool = Query(False),
search: str = Query(""),
page: int | None = Query(None, ge=1),
page_size: int = Query(20, ge=1, le=200),
include_cookie: bool | None = Query(None),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
@@ -218,9 +276,71 @@ def list_accounts(
query = query.filter(HuyaAccount.tag == tag)
if has_cookie:
query = query.filter(HuyaAccount.cookie != "")
accounts = query.order_by(HuyaAccount.id.desc()).all()
include_cookie = _can_view_huya_cookie(current)
return [_account_out(account, include_cookie=include_cookie) for account in accounts]
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
query = query.filter(or_(
HuyaAccount.uid.ilike(pattern),
HuyaAccount.yyuid.ilike(pattern),
HuyaAccount.username.ilike(pattern),
HuyaAccount.nickname.ilike(pattern),
HuyaAccount.tag.ilike(pattern),
HuyaAccount.game_name.ilike(pattern),
HuyaAccount.game_phone.ilike(pattern),
))
total = None
if page is not None:
total = query.order_by(None).count()
should_include_cookie = _can_view_huya_cookie(current) if include_cookie is None else (include_cookie and _can_view_huya_cookie(current))
query = query.order_by(HuyaAccount.id.desc())
if page is not None:
query = query.offset((page - 1) * page_size).limit(page_size)
if not should_include_cookie:
query = query.options(defer(HuyaAccount.cookie), defer(HuyaAccount.account_password))
accounts = query.all()
if should_include_cookie:
result = [_account_out(account, include_cookie=True) for account in accounts]
else:
password_map = _huya_password_state_map(db, [account.id for account in accounts])
result = [_account_out_light(account, has_password=password_map.get(account.id, False)) for account in accounts]
if page is not None:
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
return result
@router.get("/accounts/summary")
def accounts_summary(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""虎牙账号统计,避免前端为了卡片统计拉全量账号。"""
query = _visible_huya_accounts_query(db, current)
total = query.count()
assigned_count = query.filter(HuyaAccount.assigned_to.isnot(None)).count()
tag_count = (
query.filter(HuyaAccount.tag != "", HuyaAccount.tag.isnot(None))
.with_entities(HuyaAccount.tag)
.distinct()
.count()
)
password_ready_count = query.filter(HuyaAccount.account_password != "").count()
point_count = query.filter(HuyaAccount.points.isnot(None)).count()
bound_count = query.filter(or_(
HuyaAccount.game_name != "",
HuyaAccount.game_channel != "",
HuyaAccount.game_phone != "",
)).count()
return {
"total": total,
"assigned_count": assigned_count,
"unassigned_count": max(0, total - assigned_count),
"tag_count": tag_count,
"password_ready_count": password_ready_count,
"point_count": point_count,
"bound_count": bound_count,
}
@router.post("/accounts/import-cookies")
@@ -826,13 +946,59 @@ def list_tags(
@router.get("/cookies")
def list_cookies(
search: str = Query(""),
page: int | None = Query(None, ge=1),
page_size: int = Query(20, ge=1, le=200),
include_cookie: bool = Query(True),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""查看当前用户可见的虎牙 Cookie。"""
include_cookie = _can_view_huya_cookie(current)
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "").order_by(HuyaAccount.updated_at.desc()).all()
return [_cookie_out(account, include_cookie=include_cookie) for account in accounts]
should_include_cookie = include_cookie and _can_view_huya_cookie(current)
query = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "")
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
query = query.outerjoin(User, HuyaAccount.assigned_to == User.id).filter(or_(
HuyaAccount.uid.ilike(pattern),
HuyaAccount.yyuid.ilike(pattern),
HuyaAccount.username.ilike(pattern),
HuyaAccount.nickname.ilike(pattern),
User.username.ilike(pattern),
))
total = None
if page is not None:
total = query.order_by(None).count()
query = query.order_by(HuyaAccount.updated_at.desc())
if page is not None:
query = query.offset((page - 1) * page_size).limit(page_size)
if not should_include_cookie:
query = query.options(defer(HuyaAccount.cookie), defer(HuyaAccount.account_password))
accounts = query.all()
result = (
[_cookie_out(account, include_cookie=True) for account in accounts]
if should_include_cookie
else [_cookie_out_light(account) for account in accounts]
)
if page is not None:
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
return result
@router.get("/cookies/summary")
def cookies_summary(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""虎牙 Cookie 统计,避免前端为了卡片统计拉全量 Cookie。"""
query = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "")
total = query.count()
assigned_count = query.filter(HuyaAccount.assigned_to.isnot(None)).count()
return {
"total": total,
"assigned_count": assigned_count,
"unassigned_count": max(0, total - assigned_count),
}
@router.get("/cookies/export")
@@ -875,6 +1041,19 @@ def export_cookies(
)
@router.get("/cookies/{account_id}")
def get_cookie(
account_id: int,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""获取单条虎牙 Cookie 详情,供复制操作按需读取完整敏感字段。"""
account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id, HuyaAccount.cookie != "").first()
if not account:
raise HTTPException(status_code=404, detail="记录不存在")
return _cookie_out(account, include_cookie=_can_view_huya_cookie(current))
@router.delete("/cookies/batch")
def delete_cookies_batch(
account_ids: str = "",
+7 -1
View File
@@ -2,14 +2,20 @@ import api from './client';
import type {
AccountItem,
AssignmentsSummary,
BasicSummary,
MessageCountResponse,
MessageDeletedResponse,
MessageResponse,
PageParams,
PaginatedResponse,
} from './types';
export const accountApi = {
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
api.get<AccountItem[], AccountItem[]>('/accounts', { params }),
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
import: (text: string) => api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text }),
assign: (id: number, assigned_to: number | null) =>
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
+5 -1
View File
@@ -1,8 +1,12 @@
import api from './client';
import type { CookieItem, MessageDeletedResponse, MessageResponse } from './types';
import type { BasicSummary, CookieItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
export const cookieApi = {
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
listPaged: (params: PageParams & { include_cookie?: boolean }) =>
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 } : {} }),
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
deleteBatch: (ids: number[]) => api.delete<MessageDeletedResponse, MessageDeletedResponse>('/cookies/batch', { params: { task_ids: ids.join(',') } }),
+12 -1
View File
@@ -1,6 +1,8 @@
import api from './client';
import type {
BasicSummary,
HuyaAccountItem,
HuyaAccountSummary,
HuyaAutoRegisterBatch,
HuyaAutoRegisterRequest,
HuyaAutoRegisterRetryRequest,
@@ -25,12 +27,17 @@ import type {
MessageCountResponse,
MessageDeletedResponse,
MessageResponse,
PageParams,
PaginatedResponse,
} from './types';
export const huyaApi = {
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/huya/task-types'),
listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) =>
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
listAccountsPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) =>
api.get<PaginatedResponse<HuyaAccountItem>, PaginatedResponse<HuyaAccountItem>>('/huya/accounts', { params }),
accountsSummary: () => api.get<HuyaAccountSummary, HuyaAccountSummary>('/huya/accounts/summary'),
importCookies: (text: string, tag: string = '') =>
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
importPasswordAccounts: (text: string, tag: string = '') =>
@@ -76,6 +83,10 @@ export const huyaApi = {
deleteAccounts: (accountIds: number[]) =>
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }),
listCookies: () => api.get<HuyaCookieItem[], HuyaCookieItem[]>('/huya/cookies'),
listCookiesPaged: (params: PageParams & { include_cookie?: boolean }) =>
api.get<PaginatedResponse<HuyaCookieItem>, PaginatedResponse<HuyaCookieItem>>('/huya/cookies', { params }),
cookiesSummary: () => api.get<BasicSummary, BasicSummary>('/huya/cookies/summary'),
getCookie: (id: number) => api.get<HuyaCookieItem, HuyaCookieItem>(`/huya/cookies/${id}`),
exportCookies: (format?: string) => api.get<Blob, Blob>('/huya/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
deleteCookie: (id: number) => api.delete<MessageResponse, MessageResponse>(`/huya/cookies/${id}`),
deleteCookies: (accountIds: number[]) =>
+26
View File
@@ -17,6 +17,26 @@ export interface AppInfo {
version: string;
}
export interface PageParams {
page: number;
page_size: number;
search?: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
page_size: number;
}
export interface BasicSummary {
total: number;
assigned_count: number;
unassigned_count: number;
tag_count?: number;
}
// ==================== Auth ====================
export interface LoginResult {
@@ -172,6 +192,12 @@ export interface HuyaAccountItem {
updated_at: string | null;
}
export interface HuyaAccountSummary extends BasicSummary {
password_ready_count: number;
point_count: number;
bound_count: number;
}
export interface HuyaCookieImportResult extends MessageCountResponse {
skipped: number;
}
+4 -2
View File
@@ -12,6 +12,8 @@ interface ConnectOptions {
onResult?: () => void;
}
const MAX_LOGS = 1000;
function toWebSocketUrl(pathOrUrl: string): string {
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
return pathOrUrl;
@@ -63,9 +65,9 @@ export function useWebSocketLogs() {
callbacksRef.current.onResult?.();
return;
}
setLogs((prev) => [...prev, msg]);
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
} catch {
setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }]);
setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }].slice(-MAX_LOGS));
}
};
+53 -10
View File
@@ -6,7 +6,7 @@ import {
import { message } from '../utils/antdMessage';
import type { TableProps } from 'antd';
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
import { accountApi, userApi, type AccountItem, type UserInfo } from '../api/modules';
import { accountApi, userApi, type AccountItem, type BasicSummary, type UserInfo } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -27,9 +27,12 @@ export default function AccountsPage() {
const [importText, setImportText] = useState('');
const [importing, setImporting] = useState(false);
const [tagFilter, setTagFilter] = useState<string>('');
const [searchText, setSearchText] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchTagInput, setBatchTagInput] = useState('');
const [batchTagVisible, setBatchTagVisible] = useState(false);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0, tag_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('account_page_size');
return v ? Number(v) || 20 : 20;
@@ -47,14 +50,30 @@ export default function AccountsPage() {
try {
const params: { tag?: string } = {};
if (tagFilter) params.tag = tagFilter;
const data = await accountApi.list(params);
setAccounts(data);
const data = await accountApi.listPaged({
...params,
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_sensitive: canViewFull,
});
setAccounts(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [tagFilter]);
}, [canViewFull, currentPage, pageSize, searchText, tagFilter]);
const loadSummary = useCallback(async () => {
try {
const data = await accountApi.summary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
const loadUsers = useCallback(async () => {
try {
@@ -76,9 +95,13 @@ export default function AccountsPage() {
useEffect(() => {
loadAccounts();
}, [loadAccounts]);
useEffect(() => {
if (canAssign) loadUsers();
loadTags();
}, [loadAccounts, canAssign, loadUsers, loadTags]);
loadSummary();
}, [canAssign, loadUsers, loadTags, loadSummary]);
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
@@ -101,6 +124,7 @@ export default function AccountsPage() {
setImportText('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -142,6 +166,7 @@ export default function AccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -154,6 +179,7 @@ export default function AccountsPage() {
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -170,6 +196,7 @@ export default function AccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -279,10 +306,24 @@ export default function AccountsPage() {
placeholder="按标签筛选"
style={{ width: 150 }}
value={tagFilter || undefined}
onChange={(val) => setTagFilter(val || '')}
onChange={(val) => {
setTagFilter(val || '');
setCurrentPage(1);
}}
options={tags.map((t) => ({ value: t, label: t }))}
prefix={<FilterOutlined />}
/>
<Input.Search
allowClear
size="small"
placeholder="搜索账号/标签/备注"
style={{ width: 220 }}
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
/>
{canImport && (
<>
<Button
@@ -323,16 +364,16 @@ export default function AccountsPage() {
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
<Card size="small"><Statistic title="账号总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
<Card size="small"><Statistic title="标签数" value={summary.tag_count || tags.length} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={accounts.filter((a) => a.assigned_to).length}
value={summary.assigned_count}
/>
</Card>
</Col>
@@ -340,7 +381,7 @@ export default function AccountsPage() {
<Card size="small">
<Statistic
title="未分配"
value={accounts.filter((a) => !a.assigned_to).length}
value={summary.unassigned_count}
/>
</Card>
</Col>
@@ -350,6 +391,7 @@ export default function AccountsPage() {
rowSelection={canImport ? {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
} : undefined}
columns={columns}
dataSource={accounts}
@@ -359,6 +401,7 @@ export default function AccountsPage() {
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (page, size) => {
+1 -1
View File
@@ -46,7 +46,7 @@ export default function AssignmentsPage() {
// 加载账号(仅已成功登录过的)
const loadAccounts = async () => {
try {
const all = await accountApi.list({ has_cookie: true });
const all = await accountApi.list({ has_cookie: true, include_sensitive: false });
setAccounts(all);
} catch (e: unknown) {
message.error(getErrorMessage(e));
+58 -40
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd';
import { message } from '../utils/antdMessage';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi, type CookieItem } from '../api/modules';
import { cookieApi, type BasicSummary, type CookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -43,6 +43,8 @@ export default function CookiePage() {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('cookie_page_size');
return v ? Number(v) || 20 : 20;
@@ -56,19 +58,38 @@ export default function CookiePage() {
const loadCookies = useCallback(async () => {
setLoading(true);
try {
const data = await cookieApi.list();
setCookies(data);
const data = await cookieApi.listPaged({
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setCookies(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
const loadSummary = useCallback(async () => {
try {
const data = await cookieApi.summary();
setSummary(data);
} catch {
// 统计加载失败不影响列表操作。
}
}, []);
useEffect(() => {
loadCookies();
}, [loadCookies]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await cookieApi.exportCsv(format);
@@ -84,33 +105,36 @@ export default function CookiePage() {
}
};
const handleCopyCookie = (record: CookieItem) => {
if (!record.cookie) {
message.warning('Cookie 为空');
return;
const handleCopyCookie = async (record: CookieItem) => {
try {
const detail = await cookieApi.get(record.id);
if (!detail.cookie) {
message.warning('Cookie 为空');
return;
}
const text = `${detail.account_username}----${detail.account_password || ''}----${detail.cookie}`;
await copyToClipboard(text);
message.success(`已复制 ${detail.account_username} 的 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
const text = `${record.account_username}----${record.account_password || ''}----${record.cookie}`;
copyToClipboard(text).then(() => {
message.success(`已复制 ${record.account_username} 的 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleCopySelected = () => {
const handleCopySelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
const selected = cookies.filter((c) => selectedRowKeys.includes(c.id));
const text = selected
.map((c) => `${c.account_username}----${c.account_password || ''}----${c.cookie || ''}`)
.join('\r\n');
copyToClipboard(text).then(() => {
try {
const selected = await Promise.all(selectedRowKeys.map((key) => cookieApi.get(Number(key))));
const text = selected
.map((c) => `${c.account_username}----${c.account_password || ''}----${c.cookie || ''}`)
.join('\r\n');
await copyToClipboard(text);
message.success(`已复制 ${selected.length} 条 Cookie`);
}).catch(() => {
message.error('复制失败');
});
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleDelete = async (id: number) => {
@@ -119,6 +143,7 @@ export default function CookiePage() {
message.success('已删除');
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -135,21 +160,12 @@ export default function CookiePage() {
message.success(res.message);
setSelectedRowKeys([]);
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
// 筛选
const filteredCookies = cookies.filter((c) => {
if (!searchText) return true;
const s = searchText.toLowerCase();
return (
c.account_username?.toLowerCase().includes(s) ||
c.assigned_username?.toLowerCase().includes(s)
);
});
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60, align: 'center' as const },
{
@@ -206,9 +222,6 @@ export default function CookiePage() {
},
];
const assignedCount = cookies.filter((c) => c.assigned_to).length;
const unassignedCount = cookies.length - assignedCount;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -254,13 +267,13 @@ export default function CookiePage() {
</div>
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="Cookie 总数" value={cookies.length} /></Card>
<Card size="small"><Statistic title="Cookie 总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={assignedCount}
value={summary.assigned_count}
styles={{ content: { color: token.colorSuccess } }}
/>
</Card>
@@ -269,7 +282,7 @@ export default function CookiePage() {
<Card size="small">
<Statistic
title="未分配"
value={unassignedCount}
value={summary.unassigned_count}
styles={{ content: { color: token.colorError } }}
/>
</Card>
@@ -280,7 +293,10 @@ export default function CookiePage() {
placeholder="搜索账号或分配客服"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 260 }}
size="small"
prefix={<SearchOutlined />}
@@ -290,15 +306,17 @@ export default function CookiePage() {
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
columns={columns}
dataSource={filteredCookies}
dataSource={cookies}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (page, size) => {
+9 -9
View File
@@ -97,10 +97,10 @@ export default function DashboardPage() {
huyaGoodsResult,
huyaRechargeGoodsResult,
] = await Promise.allSettled([
canViewDouyuAccounts ? accountApi.list() : Promise.resolve([]),
canViewDouyuAccounts ? accountApi.summary() : Promise.resolve(null),
canViewDouyuTasks ? loginApi.listTasks() : Promise.resolve([]),
canViewCookies ? cookieApi.list() : Promise.resolve([]),
canViewHuyaAccounts ? huyaApi.listAccounts() : Promise.resolve([]),
canViewCookies ? cookieApi.summary() : Promise.resolve(null),
canViewHuyaAccounts ? huyaApi.accountsSummary() : Promise.resolve(null),
canViewHuyaTasks ? huyaApi.listTasks() : Promise.resolve([]),
canViewHuyaTasks ? huyaApi.listGoods() : Promise.resolve([]),
canViewHuyaTasks ? huyaApi.listRechargeGoods() : Promise.resolve([]),
@@ -108,24 +108,24 @@ export default function DashboardPage() {
if (ignore) return;
const douyuAccounts = douyuAccountsResult.status === 'fulfilled' ? douyuAccountsResult.value : [];
const douyuAccounts = douyuAccountsResult.status === 'fulfilled' ? douyuAccountsResult.value : null;
const douyuTasks = douyuTasksResult.status === 'fulfilled' ? douyuTasksResult.value : [];
const cookies = cookiesResult.status === 'fulfilled' ? cookiesResult.value : [];
const huyaAccounts = huyaAccountsResult.status === 'fulfilled' ? huyaAccountsResult.value : [];
const cookies = cookiesResult.status === 'fulfilled' ? cookiesResult.value : null;
const huyaAccounts = huyaAccountsResult.status === 'fulfilled' ? huyaAccountsResult.value : null;
const huyaTasks = huyaTasksResult.status === 'fulfilled' ? huyaTasksResult.value : [];
const huyaGoods = huyaGoodsResult.status === 'fulfilled' ? huyaGoodsResult.value : [];
const huyaRechargeGoods = huyaRechargeGoodsResult.status === 'fulfilled' ? huyaRechargeGoodsResult.value : [];
setStats({
douyu: {
accounts: douyuAccounts.length,
accounts: douyuAccounts?.total || 0,
tasks: douyuTasks.length,
success: douyuTasks.filter((task: LoginTaskItem) => task.status === 'success').length,
failed: countFailed(douyuTasks),
cookies: cookies.length,
cookies: cookies?.total || 0,
},
huya: {
accounts: huyaAccounts.length,
accounts: huyaAccounts?.total || 0,
tasks: huyaTasks.length,
success: huyaTasks.filter((task: HuyaTaskItem) => task.status === 'success').length,
failed: countFailed(huyaTasks),
+68 -34
View File
@@ -8,6 +8,7 @@ import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOu
import {
huyaApi,
type HuyaAccountItem,
type HuyaAccountSummary,
type HuyaPasswordLoginBatchItem,
type SupportUserItem,
} from '../api/modules';
@@ -68,6 +69,16 @@ export default function HuyaAccountsPage() {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchTagInput, setBatchTagInput] = useState('');
const [batchTagVisible, setBatchTagVisible] = useState(false);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<HuyaAccountSummary>({
total: 0,
assigned_count: 0,
unassigned_count: 0,
tag_count: 0,
password_ready_count: 0,
point_count: 0,
bound_count: 0,
});
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('huya_account_page_size');
return v ? Number(v) || 20 : 20;
@@ -86,14 +97,30 @@ export default function HuyaAccountsPage() {
try {
const params: { tag?: string } = {};
if (tagFilter) params.tag = tagFilter;
const data = await huyaApi.listAccounts(params);
setAccounts(data);
const data = await huyaApi.listAccountsPaged({
...params,
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setAccounts(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [tagFilter]);
}, [currentPage, pageSize, searchText, tagFilter]);
const loadSummary = useCallback(async () => {
try {
const data = await huyaApi.accountsSummary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
const loadUsers = useCallback(async () => {
try {
@@ -115,9 +142,13 @@ export default function HuyaAccountsPage() {
useEffect(() => {
loadAccounts();
}, [loadAccounts]);
useEffect(() => {
if (canAssign) loadUsers();
loadTags();
}, [loadAccounts, canAssign, loadUsers, loadTags]);
loadSummary();
}, [canAssign, loadUsers, loadTags, loadSummary]);
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
@@ -127,20 +158,6 @@ export default function HuyaAccountsPage() {
return map;
}, [tags]);
const filteredAccounts = useMemo(() => {
const s = searchText.trim().toLowerCase();
if (!s) return accounts;
return accounts.filter((item) => (
item.uid.toLowerCase().includes(s) ||
item.yyuid.toLowerCase().includes(s) ||
item.username.toLowerCase().includes(s) ||
item.nickname.toLowerCase().includes(s) ||
item.tag.toLowerCase().includes(s) ||
item.game_name.toLowerCase().includes(s) ||
item.game_phone.toLowerCase().includes(s)
));
}, [accounts, searchText]);
const handleImport = async () => {
if (!importText.trim()) {
message.warning('请先粘贴虎牙 CK');
@@ -155,6 +172,7 @@ export default function HuyaAccountsPage() {
setImportTag('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -223,6 +241,7 @@ export default function HuyaAccountsPage() {
setSmsState('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -245,6 +264,7 @@ export default function HuyaAccountsPage() {
setPasswordImportTag([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -271,6 +291,7 @@ export default function HuyaAccountsPage() {
}
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -285,6 +306,7 @@ export default function HuyaAccountsPage() {
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -301,6 +323,7 @@ export default function HuyaAccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -345,11 +368,6 @@ export default function HuyaAccountsPage() {
}
};
const boundCount = accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length;
const pointCount = accounts.filter((item) => item.points !== null && item.points !== undefined).length;
const assignedCount = accounts.filter((item) => item.assigned_to).length;
const passwordReadyCount = accounts.filter((item) => item.has_password).length;
const passwordLoginResultColumns: TableProps<HuyaPasswordLoginBatchItem>['columns'] = [
{ title: '账号ID', dataIndex: 'line', width: 80, align: 'center' },
{ title: '账号', dataIndex: 'username', width: 160, ellipsis: true },
@@ -526,7 +544,10 @@ export default function HuyaAccountsPage() {
placeholder="按标签筛选"
style={{ width: 150 }}
value={tagFilter || undefined}
onChange={(value) => setTagFilter(value || '')}
onChange={(value) => {
setTagFilter(value || '');
setCurrentPage(1);
}}
options={tags.map((tag) => ({ value: tag, label: tag }))}
prefix={<FilterOutlined />}
/>
@@ -586,25 +607,25 @@ export default function HuyaAccountsPage() {
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col xs={24} sm={8}>
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
<Card size="small"><Statistic title="账号总数" value={summary.total} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
<Card size="small"><Statistic title="标签数" value={summary.tag_count || tags.length} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已导入密码" value={passwordReadyCount} /></Card>
<Card size="small"><Statistic title="已导入密码" value={summary.password_ready_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
<Card size="small"><Statistic title="已查积分" value={summary.point_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已绑定信息" value={boundCount} /></Card>
<Card size="small"><Statistic title="已绑定信息" value={summary.bound_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已分配" value={assignedCount} /></Card>
<Card size="small"><Statistic title="已分配" value={summary.assigned_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="未分配" value={accounts.length - assignedCount} /></Card>
<Card size="small"><Statistic title="未分配" value={summary.unassigned_count} /></Card>
</Col>
</Row>
@@ -613,12 +634,23 @@ export default function HuyaAccountsPage() {
placeholder="搜索 UID、昵称、标签、游戏名、手机号"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
{tags.map((tag) => (
<Tag key={tag} color="blue" onClick={() => setSearchText(tag)} style={{ cursor: 'pointer' }}>
<Tag
key={tag}
color="blue"
onClick={() => {
setSearchText(tag);
setCurrentPage(1);
}}
style={{ cursor: 'pointer' }}
>
{tag}
</Tag>
))}
@@ -628,15 +660,17 @@ export default function HuyaAccountsPage() {
rowSelection={(canImport || canDelete || canAssign) ? {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
} : undefined}
columns={columns}
dataSource={filteredAccounts}
dataSource={accounts}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, size) => {
@@ -45,7 +45,7 @@ export default function HuyaAssignmentsPage() {
const loadAccounts = async () => {
try {
const data = await huyaApi.listAccounts({ has_cookie: true });
const data = await huyaApi.listAccounts({ has_cookie: true, include_cookie: false });
setAccounts(data);
} catch (e: unknown) {
message.error(getErrorMessage(e));
+56 -39
View File
@@ -4,7 +4,7 @@ import {
} from 'antd';
import { message } from '../utils/antdMessage';
import { CopyOutlined, DeleteOutlined, DownloadOutlined, SearchOutlined } from '@ant-design/icons';
import { huyaApi, type HuyaCookieItem } from '../api/modules';
import { huyaApi, type BasicSummary, type HuyaCookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -40,6 +40,8 @@ export default function HuyaCookiePage() {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const value = localStorage.getItem('huya_cookie_page_size');
return value ? Number(value) || 20 : 20;
@@ -54,19 +56,38 @@ export default function HuyaCookiePage() {
const loadCookies = useCallback(async () => {
setLoading(true);
try {
const data = await huyaApi.listCookies();
setCookies(data);
const data = await huyaApi.listCookiesPaged({
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setCookies(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
const loadSummary = useCallback(async () => {
try {
const data = await huyaApi.cookiesSummary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
useEffect(() => {
loadCookies();
}, [loadCookies]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await huyaApi.exportCookies(format);
@@ -82,31 +103,34 @@ export default function HuyaCookiePage() {
}
};
const handleCopyCookie = (record: HuyaCookieItem) => {
if (!record.cookie) {
message.warning('Cookie 为空');
return;
const handleCopyCookie = async (record: HuyaCookieItem) => {
try {
const detail = await huyaApi.getCookie(record.id);
if (!detail.cookie) {
message.warning('Cookie 为空');
return;
}
const text = `${detail.account_username}----${detail.cookie}`;
await copyToClipboard(text);
message.success(`已复制 ${detail.account_username} 的 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
const text = `${record.account_username}----${record.cookie}`;
copyToClipboard(text).then(() => {
message.success(`已复制 ${record.account_username} 的 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleCopySelected = () => {
const handleCopySelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
const selected = cookies.filter((item) => selectedRowKeys.includes(item.id));
const text = selected.map((item) => `${item.account_username}----${item.cookie || ''}`).join('\r\n');
copyToClipboard(text).then(() => {
try {
const selected = await Promise.all(selectedRowKeys.map((key) => huyaApi.getCookie(Number(key))));
const text = selected.map((item) => `${item.account_username}----${item.cookie || ''}`).join('\r\n');
await copyToClipboard(text);
message.success(`已复制 ${selected.length} 条 Cookie`);
}).catch(() => {
message.error('复制失败');
});
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleDelete = async (id: number) => {
@@ -115,6 +139,7 @@ export default function HuyaCookiePage() {
message.success('已清除');
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -130,22 +155,12 @@ export default function HuyaCookiePage() {
message.success(result.message);
setSelectedRowKeys([]);
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const filteredCookies = cookies.filter((item) => {
if (!searchText) return true;
const search = searchText.toLowerCase();
return (
item.account_username.toLowerCase().includes(search) ||
item.uid.toLowerCase().includes(search) ||
item.yyuid.toLowerCase().includes(search) ||
(item.assigned_username || '').toLowerCase().includes(search)
);
});
const columns = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' as const },
{
@@ -204,9 +219,6 @@ export default function HuyaCookiePage() {
},
];
const assignedCount = cookies.filter((item) => item.assigned_to).length;
const unassignedCount = cookies.length - assignedCount;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -245,16 +257,16 @@ export default function HuyaCookiePage() {
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="Cookie 总数" value={cookies.length} /></Card>
<Card size="small"><Statistic title="Cookie 总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="已分配" value={assignedCount} styles={{ content: { color: token.colorSuccess } }} />
<Statistic title="已分配" value={summary.assigned_count} styles={{ content: { color: token.colorSuccess } }} />
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="未分配" value={unassignedCount} styles={{ content: { color: token.colorError } }} />
<Statistic title="未分配" value={summary.unassigned_count} styles={{ content: { color: token.colorError } }} />
</Card>
</Col>
</Row>
@@ -264,7 +276,10 @@ export default function HuyaCookiePage() {
placeholder="搜索账号、UID 或分配客服"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 300 }}
size="small"
prefix={<SearchOutlined />}
@@ -275,15 +290,17 @@ export default function HuyaCookiePage() {
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
columns={columns}
dataSource={filteredCookies}
dataSource={cookies}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, size) => {
+1 -1
View File
@@ -392,7 +392,7 @@ export default function HuyaTasksPage() {
setLoading(true);
try {
const [accountResult, taskResult, goodsResult, rechargeGoodsResult, configResult, taskTypeResult, tagResult] = await Promise.allSettled([
huyaApi.listAccounts(),
huyaApi.listAccounts({ include_cookie: false }),
huyaApi.listTasks(),
huyaApi.listGoods(),
huyaApi.listRechargeGoods(),
+1 -1
View File
@@ -106,7 +106,7 @@ export default function LoginTasksPage() {
const loadAccounts = useCallback(async () => {
try {
const data = await accountApi.list();
const data = await accountApi.list({ include_sensitive: false });
setAccounts(data);
} catch (e: unknown) {
message.error(getErrorMessage(e));