优化列表分页和数据加载

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