支持账号按筛选全量批量操作
This commit is contained in:
+137
-29
@@ -5,8 +5,8 @@ from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, Account, AuditLog, LoginTask
|
||||
from ..schemas import AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
|
||||
from ..models import User, Account, AuditLog, LoginTask, DouyuTask
|
||||
from ..schemas import AccountBulkSelection, AccountBulkTag, AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
|
||||
from ..deps import get_current_user, require_permission
|
||||
from ..permissions import user_has_permission
|
||||
from ..services.account_service import (
|
||||
@@ -16,6 +16,82 @@ from ..services.account_service import (
|
||||
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
|
||||
|
||||
|
||||
def _visible_accounts_query(db: Session, current: User):
|
||||
"""返回当前用户可见的斗鱼账号查询。"""
|
||||
query = db.query(Account)
|
||||
if user_has_permission(current, "account:view_all"):
|
||||
return query
|
||||
if user_has_permission(current, "account:view_assigned"):
|
||||
return query.filter(Account.assigned_to == current.id)
|
||||
raise HTTPException(status_code=403, detail="无权查看账号")
|
||||
|
||||
|
||||
def _filter_accounts_query(
|
||||
db: Session,
|
||||
query,
|
||||
current: User,
|
||||
*,
|
||||
assigned_only: bool = False,
|
||||
tag: str | None = None,
|
||||
has_cookie: bool = False,
|
||||
search: str = "",
|
||||
):
|
||||
"""复用列表筛选条件,供分页列表和全量批量操作保持一致。"""
|
||||
if has_cookie:
|
||||
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
|
||||
if assigned_only and user_has_permission(current, "account:view_all"):
|
||||
query = query.filter(Account.assigned_to.isnot(None))
|
||||
if tag:
|
||||
query = query.filter(Account.tag == tag)
|
||||
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),
|
||||
))
|
||||
return query
|
||||
|
||||
|
||||
def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection) -> list[int]:
|
||||
"""解析批量操作目标:当前筛选全部或显式选择的 ID。"""
|
||||
if req.all_matching:
|
||||
rows = (
|
||||
_filter_accounts_query(
|
||||
db,
|
||||
_visible_accounts_query(db, current),
|
||||
current,
|
||||
assigned_only=req.assigned_only,
|
||||
tag=req.tag,
|
||||
has_cookie=req.has_cookie,
|
||||
search=req.search,
|
||||
)
|
||||
.with_entities(Account.id)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
return [account_id for account_id, in rows]
|
||||
|
||||
seen = set()
|
||||
ids = []
|
||||
for account_id in req.account_ids:
|
||||
if account_id not in seen:
|
||||
seen.add(account_id)
|
||||
ids.append(account_id)
|
||||
if not ids:
|
||||
return []
|
||||
rows = (
|
||||
_visible_accounts_query(db, current)
|
||||
.filter(Account.id.in_(ids))
|
||||
.with_entities(Account.id)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
allowed = {account_id for account_id, in rows}
|
||||
return [account_id for account_id in ids if account_id in allowed]
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_accounts(
|
||||
assigned_only: bool = Query(False),
|
||||
@@ -29,33 +105,15 @@ def list_accounts(
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""列表:按角色返回不同字段和范围。"""
|
||||
query = db.query(Account)
|
||||
|
||||
# 只展示已成功登录过的账号
|
||||
if has_cookie:
|
||||
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
|
||||
|
||||
# 权限控制:客服只能看分配给自己的
|
||||
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="无权查看账号")
|
||||
|
||||
if assigned_only and user_has_permission(current, "account:view_all"):
|
||||
query = query.filter(Account.assigned_to.isnot(None))
|
||||
|
||||
if tag:
|
||||
query = query.filter(Account.tag == tag)
|
||||
|
||||
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),
|
||||
))
|
||||
query = _filter_accounts_query(
|
||||
db,
|
||||
_visible_accounts_query(db, current),
|
||||
current,
|
||||
assigned_only=assigned_only,
|
||||
tag=tag,
|
||||
has_cookie=has_cookie,
|
||||
search=search,
|
||||
)
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
@@ -284,6 +342,27 @@ def batch_tag(
|
||||
return {"message": f"已为 {count} 个账号设置标签", "success": True}
|
||||
|
||||
|
||||
@router.put("/batch-tag-selection")
|
||||
def batch_tag_selection(
|
||||
req: AccountBulkTag,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("account:import")),
|
||||
):
|
||||
"""按显式选择或当前筛选结果批量设置账号标签。"""
|
||||
ids = _selected_account_ids(db, current, req)
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
tag = (req.tag_value or "").strip()
|
||||
count = (
|
||||
_visible_accounts_query(db, current)
|
||||
.filter(Account.id.in_(ids))
|
||||
.update({Account.tag: tag}, synchronize_session=False)
|
||||
)
|
||||
db.commit()
|
||||
scope = "当前筛选下" if req.all_matching else "选中的"
|
||||
return {"message": f"已为{scope} {count} 个账号设置标签", "success": True, "count": count}
|
||||
|
||||
|
||||
@router.get("/tags/list")
|
||||
def list_tags(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -310,6 +389,7 @@ def batch_delete_accounts(
|
||||
|
||||
# 先删除关联的登录任务
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
|
||||
# 删除账号
|
||||
deleted = db.query(Account).filter(Account.id.in_(ids)).delete(synchronize_session=False)
|
||||
@@ -322,6 +402,33 @@ def batch_delete_accounts(
|
||||
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
|
||||
|
||||
|
||||
@router.post("/batch-delete")
|
||||
def batch_delete_accounts_selection(
|
||||
req: AccountBulkSelection,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("account:delete")),
|
||||
):
|
||||
"""按显式选择或当前筛选结果批量删除账号及其关联任务。"""
|
||||
ids = _selected_account_ids(db, current, req)
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
deleted = (
|
||||
_visible_accounts_query(db, current)
|
||||
.filter(Account.id.in_(ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
db.add(AuditLog(
|
||||
user_id=current.id, username=current.username,
|
||||
action="account:delete",
|
||||
target=f"{'按筛选' if req.all_matching else '批量'}删除{deleted}个账号",
|
||||
))
|
||||
db.commit()
|
||||
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
|
||||
|
||||
|
||||
@router.delete("/{account_id}")
|
||||
def delete_account(
|
||||
account_id: int,
|
||||
@@ -334,6 +441,7 @@ def delete_account(
|
||||
|
||||
# 先删除关联的登录任务,避免外键约束失败
|
||||
db.query(LoginTask).filter(LoginTask.account_id == account_id).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id == account_id).delete(synchronize_session=False)
|
||||
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="account:delete", target=acc.username))
|
||||
|
||||
+153
-23
@@ -23,10 +23,22 @@ from core.sms_provider import parse_sms_lines
|
||||
|
||||
from ..database import SessionLocal, get_db
|
||||
from ..deps import authenticate_websocket, get_current_user, require_permission
|
||||
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, ProxyConfig, User
|
||||
from ..models import (
|
||||
HuyaAccount,
|
||||
HuyaConfig,
|
||||
HuyaGoodsSnapshot,
|
||||
HuyaRechargeGoodsSnapshot,
|
||||
HuyaRegisterItem,
|
||||
HuyaRegisterSuccessLog,
|
||||
HuyaTask,
|
||||
ProxyConfig,
|
||||
User,
|
||||
)
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
AccountAssign,
|
||||
AccountBulkSelection,
|
||||
AccountBulkTag,
|
||||
AccountTag,
|
||||
BatchAssign,
|
||||
HuyaAccountOut,
|
||||
@@ -103,6 +115,89 @@ def _visible_huya_accounts_query(db: Session, current: User):
|
||||
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
|
||||
|
||||
|
||||
def _filter_huya_accounts_query(
|
||||
query,
|
||||
current: User,
|
||||
*,
|
||||
assigned_only: bool = False,
|
||||
tag: str | None = None,
|
||||
has_cookie: bool = False,
|
||||
search: str = "",
|
||||
):
|
||||
"""复用虎牙账号列表筛选条件,供分页和批量操作保持一致。"""
|
||||
if assigned_only and _can_view_huya_all(current):
|
||||
query = query.filter(HuyaAccount.assigned_to.isnot(None))
|
||||
if tag:
|
||||
query = query.filter(HuyaAccount.tag == tag)
|
||||
if has_cookie:
|
||||
query = query.filter(HuyaAccount.cookie != "")
|
||||
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),
|
||||
))
|
||||
return query
|
||||
|
||||
|
||||
def _selected_huya_account_ids(db: Session, current: User, req: AccountBulkSelection) -> list[int]:
|
||||
"""解析虎牙批量操作目标:当前筛选全部或显式选择的 ID。"""
|
||||
base_query = _visible_huya_accounts_query(db, current)
|
||||
if req.all_matching:
|
||||
rows = (
|
||||
_filter_huya_accounts_query(
|
||||
base_query,
|
||||
current,
|
||||
assigned_only=req.assigned_only,
|
||||
tag=req.tag,
|
||||
has_cookie=req.has_cookie,
|
||||
search=req.search,
|
||||
)
|
||||
.with_entities(HuyaAccount.id)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
return [account_id for account_id, in rows]
|
||||
|
||||
seen = set()
|
||||
requested_ids = []
|
||||
for account_id in req.account_ids:
|
||||
if account_id not in seen:
|
||||
seen.add(account_id)
|
||||
requested_ids.append(account_id)
|
||||
if not requested_ids:
|
||||
return []
|
||||
rows = (
|
||||
base_query
|
||||
.filter(HuyaAccount.id.in_(requested_ids))
|
||||
.with_entities(HuyaAccount.id)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
allowed = {account_id for account_id, in rows}
|
||||
return [account_id for account_id in requested_ids if account_id in allowed]
|
||||
|
||||
|
||||
def _clear_huya_account_references(db: Session, account_ids: list[int]) -> None:
|
||||
"""删除账号前保留注册历史,把历史流水中的账号引用置空。"""
|
||||
if not account_ids:
|
||||
return
|
||||
db.query(HuyaRegisterItem).filter(HuyaRegisterItem.account_id.in_(account_ids)).update(
|
||||
{HuyaRegisterItem.account_id: None},
|
||||
synchronize_session=False,
|
||||
)
|
||||
db.query(HuyaRegisterSuccessLog).filter(HuyaRegisterSuccessLog.account_id.in_(account_ids)).update(
|
||||
{HuyaRegisterSuccessLog.account_id: None},
|
||||
synchronize_session=False,
|
||||
)
|
||||
|
||||
|
||||
def _visible_huya_tasks_query(db: Session, current: User):
|
||||
"""返回当前用户可查看的虎牙任务查询。"""
|
||||
query = db.query(HuyaTask).options(joinedload(HuyaTask.account))
|
||||
@@ -305,25 +400,14 @@ def list_accounts(
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""查看虎牙 CK 账号。"""
|
||||
query = _visible_huya_accounts_query(db, current)
|
||||
if assigned_only and _can_view_huya_all(current):
|
||||
query = query.filter(HuyaAccount.assigned_to.isnot(None))
|
||||
if tag:
|
||||
query = query.filter(HuyaAccount.tag == tag)
|
||||
if has_cookie:
|
||||
query = query.filter(HuyaAccount.cookie != "")
|
||||
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),
|
||||
))
|
||||
query = _filter_huya_accounts_query(
|
||||
_visible_huya_accounts_query(db, current),
|
||||
current,
|
||||
assigned_only=assigned_only,
|
||||
tag=tag,
|
||||
has_cookie=has_cookie,
|
||||
search=search,
|
||||
)
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
@@ -694,12 +778,13 @@ def password_login_selected_accounts(
|
||||
):
|
||||
"""对已导入的虎牙账号执行密码登录。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
if not req.account_ids:
|
||||
selected_ids = _selected_huya_account_ids(db, current, req)
|
||||
if not selected_ids:
|
||||
raise HTTPException(status_code=400, detail="请选择虎牙账号")
|
||||
|
||||
accounts = (
|
||||
_visible_huya_accounts_query(db, current)
|
||||
.filter(HuyaAccount.id.in_(req.account_ids))
|
||||
.filter(HuyaAccount.id.in_(selected_ids))
|
||||
.all()
|
||||
)
|
||||
account_map = {account.id: account for account in accounts}
|
||||
@@ -708,7 +793,7 @@ def password_login_selected_accounts(
|
||||
failed_count = 0
|
||||
include_cookie = _can_view_huya_cookie(current)
|
||||
|
||||
for account_id in req.account_ids:
|
||||
for account_id in selected_ids:
|
||||
account = account_map.get(account_id)
|
||||
if account is None:
|
||||
failed_count += 1
|
||||
@@ -827,12 +912,35 @@ def delete_accounts_batch(
|
||||
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="无效的账号ID")
|
||||
ids = _selected_huya_account_ids(db, current, AccountBulkSelection(account_ids=ids))
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择虎牙账号")
|
||||
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
_clear_huya_account_references(db, ids)
|
||||
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return {"message": f"已删除 {deleted} 个虎牙账号", "deleted": deleted, "success": True}
|
||||
|
||||
|
||||
@router.post("/accounts/batch-delete")
|
||||
def delete_accounts_batch_selection(
|
||||
req: AccountBulkSelection,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""按显式选择或当前筛选结果批量删除虎牙账号及任务记录。"""
|
||||
_require_huya_perm(current, "huya:delete")
|
||||
ids = _selected_huya_account_ids(db, current, req)
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择虎牙账号")
|
||||
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
_clear_huya_account_references(db, ids)
|
||||
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
scope = "当前筛选下" if req.all_matching else "选中的"
|
||||
return {"message": f"已删除{scope} {deleted} 个虎牙账号", "deleted": deleted, "success": True}
|
||||
|
||||
|
||||
@router.delete("/accounts/{account_id}")
|
||||
def delete_account(
|
||||
account_id: int,
|
||||
@@ -845,6 +953,7 @@ def delete_account(
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(synchronize_session=False)
|
||||
_clear_huya_account_references(db, [account_id])
|
||||
db.delete(account)
|
||||
db.commit()
|
||||
return {"message": "已删除", "success": True}
|
||||
@@ -964,6 +1073,27 @@ def batch_tag(
|
||||
return {"message": f"已为 {count} 个虎牙账号设置标签", "success": True, "count": count}
|
||||
|
||||
|
||||
@router.put("/accounts/batch-tag-selection")
|
||||
def batch_tag_selection(
|
||||
req: AccountBulkTag,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""按显式选择或当前筛选结果批量设置虎牙账号标签。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
ids = _selected_huya_account_ids(db, current, req)
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择虎牙账号")
|
||||
tag = (req.tag_value or "").strip()
|
||||
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).update(
|
||||
{HuyaAccount.tag: tag},
|
||||
synchronize_session=False,
|
||||
)
|
||||
db.commit()
|
||||
scope = "当前筛选下" if req.all_matching else "选中的"
|
||||
return {"message": f"已为{scope} {count} 个虎牙账号设置标签", "success": True, "count": count}
|
||||
|
||||
|
||||
@router.get("/accounts/tags/list")
|
||||
def list_tags(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
+20
-1
@@ -93,6 +93,20 @@ class BatchAssign(BaseModel):
|
||||
assigned_to: Optional[int] = None # None=取消分配
|
||||
|
||||
|
||||
class AccountBulkSelection(BaseModel):
|
||||
"""批量操作选择范围:显式 ID 或当前筛选条件下全部账号。"""
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
all_matching: bool = False
|
||||
search: str = ""
|
||||
tag: str = ""
|
||||
assigned_only: bool = False
|
||||
has_cookie: bool = False
|
||||
|
||||
|
||||
class AccountBulkTag(AccountBulkSelection):
|
||||
tag_value: str = ""
|
||||
|
||||
|
||||
class AccountTag(BaseModel):
|
||||
tag: Optional[str] = None
|
||||
account_ids: Optional[list[int]] = None
|
||||
@@ -334,7 +348,12 @@ class HuyaPasswordAccountImport(BaseModel):
|
||||
|
||||
class HuyaPasswordLoginSelectedRequest(BaseModel):
|
||||
"""选择已导入的虎牙账号执行密码登录。"""
|
||||
account_ids: list[int]
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
all_matching: bool = False
|
||||
search: str = ""
|
||||
tag: str = ""
|
||||
assigned_only: bool = False
|
||||
has_cookie: bool = False
|
||||
|
||||
|
||||
class HuyaAccountOut(BaseModel):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import api from './client';
|
||||
import type {
|
||||
AccountItem,
|
||||
AccountBulkSelection,
|
||||
AccountBulkTagRequest,
|
||||
AssignmentsSummary,
|
||||
BasicSummary,
|
||||
MessageCountResponse,
|
||||
@@ -27,8 +29,12 @@ export const accountApi = {
|
||||
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/tag`, { tag }),
|
||||
batchTag: (account_ids: number[], tag: string) =>
|
||||
api.put<MessageCountResponse, MessageCountResponse>('/accounts/batch-tag', { account_ids, tag }),
|
||||
batchTagSelection: (data: AccountBulkTagRequest) =>
|
||||
api.put<MessageCountResponse, MessageCountResponse>('/accounts/batch-tag-selection', data),
|
||||
listTags: () => api.get<string[], string[]>('/accounts/tags/list'),
|
||||
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/accounts/${id}`),
|
||||
batchDelete: (account_ids: number[]) =>
|
||||
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/accounts/batch/delete', { params: { account_ids: account_ids.join(',') } }),
|
||||
batchDeleteSelection: (data: AccountBulkSelection) =>
|
||||
api.post<MessageDeletedResponse, MessageDeletedResponse>('/accounts/batch-delete', data),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import api from './client';
|
||||
import type {
|
||||
BasicSummary,
|
||||
AccountBulkSelection,
|
||||
AccountBulkTagRequest,
|
||||
HuyaAccountItem,
|
||||
HuyaAccountSummary,
|
||||
HuyaAutoRegisterBatch,
|
||||
@@ -78,10 +80,14 @@ export const huyaApi = {
|
||||
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/tag`, { tag }),
|
||||
batchTag: (account_ids: number[], tag: string) =>
|
||||
api.put<MessageCountResponse, MessageCountResponse>('/huya/accounts/batch-tag', { account_ids, tag }),
|
||||
batchTagSelection: (data: AccountBulkTagRequest) =>
|
||||
api.put<MessageCountResponse, MessageCountResponse>('/huya/accounts/batch-tag-selection', data),
|
||||
listTags: () => api.get<string[], string[]>('/huya/accounts/tags/list'),
|
||||
deleteAccount: (id: number) => api.delete<MessageResponse, MessageResponse>(`/huya/accounts/${id}`),
|
||||
deleteAccounts: (accountIds: number[]) =>
|
||||
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }),
|
||||
deleteAccountsSelection: (data: AccountBulkSelection) =>
|
||||
api.post<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch-delete', data),
|
||||
listCookies: () => api.get<HuyaCookieItem[], HuyaCookieItem[]>('/huya/cookies'),
|
||||
listCookiesPaged: (params: PageParams & { include_cookie?: boolean }) =>
|
||||
api.get<PaginatedResponse<HuyaCookieItem>, PaginatedResponse<HuyaCookieItem>>('/huya/cookies', { params }),
|
||||
|
||||
@@ -23,6 +23,19 @@ export interface PageParams {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface AccountBulkSelection {
|
||||
account_ids?: number[];
|
||||
all_matching?: boolean;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
assigned_only?: boolean;
|
||||
has_cookie?: boolean;
|
||||
}
|
||||
|
||||
export interface AccountBulkTagRequest extends AccountBulkSelection {
|
||||
tag_value: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
@@ -412,7 +425,12 @@ export interface HuyaPasswordAccountImportResult extends MessageCountResponse {
|
||||
}
|
||||
|
||||
export interface HuyaPasswordLoginSelectedRequest {
|
||||
account_ids: number[];
|
||||
account_ids?: number[];
|
||||
all_matching?: boolean;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
assigned_only?: boolean;
|
||||
has_cookie?: boolean;
|
||||
}
|
||||
|
||||
export interface HuyaCookieItem {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Table, Button, Modal, Input, Select, Popconfirm, Typography, Tag, Space,
|
||||
Alert, Table, Button, Modal, Input, Select, Popconfirm, Typography, Tag, Space,
|
||||
Row, Col, Card, Statistic,
|
||||
} from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
@@ -29,6 +29,7 @@ export default function AccountsPage() {
|
||||
const [tagFilter, setTagFilter] = useState<string>('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedAllMatching, setSelectedAllMatching] = useState(false);
|
||||
const [batchTagInput, setBatchTagInput] = useState('');
|
||||
const [batchTagVisible, setBatchTagVisible] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -44,6 +45,7 @@ export default function AccountsPage() {
|
||||
const canImport = can('account:import');
|
||||
const canAssign = can('account:assign');
|
||||
const canDelete = can('account:delete');
|
||||
const canSelectRows = canImport || canDelete || canAssign;
|
||||
|
||||
const loadAccounts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -111,6 +113,23 @@ export default function AccountsPage() {
|
||||
return map;
|
||||
}, [tags]);
|
||||
|
||||
const selectedCount = selectedAllMatching ? total : selectedRowKeys.length;
|
||||
const selectionPayload = useMemo(() => ({
|
||||
account_ids: selectedAllMatching ? [] : selectedRowKeys.map((key) => Number(key)),
|
||||
all_matching: selectedAllMatching,
|
||||
search: searchText.trim(),
|
||||
tag: tagFilter,
|
||||
}), [searchText, selectedAllMatching, selectedRowKeys, tagFilter]);
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedAllMatching(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
clearSelection();
|
||||
}, [searchText, tagFilter]);
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importText.trim()) {
|
||||
message.warning('请输入账号数据');
|
||||
@@ -154,16 +173,16 @@ export default function AccountsPage() {
|
||||
};
|
||||
|
||||
const handleBatchTag = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await accountApi.batchTag(selectedRowKeys as number[], batchTagInput);
|
||||
message.success(`已为 ${selectedRowKeys.length} 个账号设置标签`);
|
||||
const result = await accountApi.batchTagSelection({ ...selectionPayload, tag_value: batchTagInput });
|
||||
message.success(result.message);
|
||||
setBatchTagVisible(false);
|
||||
setBatchTagInput('');
|
||||
setSelectedRowKeys([]);
|
||||
clearSelection();
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
@@ -177,6 +196,7 @@ export default function AccountsPage() {
|
||||
await accountApi.delete(id);
|
||||
message.success('已删除');
|
||||
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
|
||||
setSelectedAllMatching(false);
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
@@ -186,14 +206,14 @@ export default function AccountsPage() {
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await accountApi.batchDelete(selectedRowKeys as number[]);
|
||||
const result = await accountApi.batchDeleteSelection(selectionPayload);
|
||||
message.success(result.message);
|
||||
setSelectedRowKeys([]);
|
||||
clearSelection();
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
@@ -325,39 +345,39 @@ export default function AccountsPage() {
|
||||
}}
|
||||
/>
|
||||
{canImport && (
|
||||
<>
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
icon={<TagOutlined />}
|
||||
onClick={() => {
|
||||
setBatchTagInput('');
|
||||
setBatchTagVisible(true);
|
||||
}}
|
||||
>
|
||||
批量打标签
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Popconfirm
|
||||
title={`确认删除${selectedAllMatching ? '当前筛选下' : '选中的'} ${selectedCount} 个账号?`}
|
||||
description="将同时删除关联的登录任务"
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
icon={<TagOutlined />}
|
||||
onClick={() => {
|
||||
setBatchTagInput('');
|
||||
setBatchTagVisible(true);
|
||||
}}
|
||||
danger
|
||||
disabled={selectedCount === 0}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
批量打标签
|
||||
批量删除
|
||||
</Button>
|
||||
{canDelete && (
|
||||
<Popconfirm
|
||||
title={`确认删除选中的 ${selectedRowKeys.length} 个账号?`}
|
||||
description="将同时删除关联的登录任务"
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
|
||||
批量导入
|
||||
</Button>
|
||||
</>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{canImport && (
|
||||
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
|
||||
批量导入
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
@@ -388,9 +408,35 @@ export default function AccountsPage() {
|
||||
</Row>
|
||||
|
||||
<Table
|
||||
rowSelection={canImport ? {
|
||||
title={() => selectedRowKeys.length > 0 ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={(
|
||||
<Space wrap>
|
||||
<span>
|
||||
{selectedAllMatching
|
||||
? `已选择当前筛选下全部 ${total} 个账号`
|
||||
: `已选择 ${selectedRowKeys.length} 个账号`}
|
||||
</span>
|
||||
{!selectedAllMatching && total > selectedRowKeys.length ? (
|
||||
<Button size="small" type="link" onClick={() => setSelectedAllMatching(true)}>
|
||||
选择当前筛选下全部 {total} 个
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="small" type="link" onClick={clearSelection}>
|
||||
取消选择
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
) : undefined}
|
||||
rowSelection={canSelectRows ? {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
onChange: (keys) => {
|
||||
setSelectedRowKeys(keys);
|
||||
setSelectedAllMatching(false);
|
||||
},
|
||||
preserveSelectedRowKeys: true,
|
||||
} : undefined}
|
||||
columns={columns}
|
||||
@@ -444,7 +490,7 @@ export default function AccountsPage() {
|
||||
okText="确定"
|
||||
width={400}
|
||||
>
|
||||
<p>为选中的 {selectedRowKeys.length} 个账号设置标签:</p>
|
||||
<p>为{selectedAllMatching ? '当前筛选下' : '选中的'} {selectedCount} 个账号设置标签:</p>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
|
||||
Alert, Button, Card, Col, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import type { TableProps } from 'antd';
|
||||
@@ -67,6 +67,7 @@ export default function HuyaAccountsPage() {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [tagFilter, setTagFilter] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedAllMatching, setSelectedAllMatching] = useState(false);
|
||||
const [batchTagInput, setBatchTagInput] = useState('');
|
||||
const [batchTagVisible, setBatchTagVisible] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -158,6 +159,23 @@ export default function HuyaAccountsPage() {
|
||||
return map;
|
||||
}, [tags]);
|
||||
|
||||
const selectedCount = selectedAllMatching ? total : selectedRowKeys.length;
|
||||
const selectionPayload = useMemo(() => ({
|
||||
account_ids: selectedAllMatching ? [] : selectedRowKeys.map((key) => Number(key)),
|
||||
all_matching: selectedAllMatching,
|
||||
search: searchText.trim(),
|
||||
tag: tagFilter,
|
||||
}), [searchText, selectedAllMatching, selectedRowKeys, tagFilter]);
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedAllMatching(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
clearSelection();
|
||||
}, [searchText, tagFilter]);
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importText.trim()) {
|
||||
message.warning('请先粘贴虎牙 CK');
|
||||
@@ -273,14 +291,14 @@ export default function HuyaAccountsPage() {
|
||||
};
|
||||
|
||||
const handleLoginSelected = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
setPasswordLogging(true);
|
||||
try {
|
||||
const result = await huyaApi.passwordLoginSelected({
|
||||
account_ids: selectedRowKeys.map((key) => Number(key)),
|
||||
...selectionPayload,
|
||||
});
|
||||
setPasswordLoginResults(result.results || []);
|
||||
setPasswordLoginResultOpen(true);
|
||||
@@ -304,6 +322,7 @@ export default function HuyaAccountsPage() {
|
||||
await huyaApi.deleteAccount(id);
|
||||
message.success('已删除');
|
||||
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
|
||||
setSelectedAllMatching(false);
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
@@ -313,14 +332,14 @@ export default function HuyaAccountsPage() {
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await huyaApi.deleteAccounts(selectedRowKeys.map((key) => Number(key)));
|
||||
const result = await huyaApi.deleteAccountsSelection(selectionPayload);
|
||||
message.success(result.message);
|
||||
setSelectedRowKeys([]);
|
||||
clearSelection();
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
@@ -351,16 +370,16 @@ export default function HuyaAccountsPage() {
|
||||
};
|
||||
|
||||
const handleBatchTag = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await huyaApi.batchTag(selectedRowKeys.map((key) => Number(key)), batchTagInput);
|
||||
message.success(`已为 ${selectedRowKeys.length} 个虎牙账号设置标签`);
|
||||
const result = await huyaApi.batchTagSelection({ ...selectionPayload, tag_value: batchTagInput });
|
||||
message.success(result.message);
|
||||
setBatchTagVisible(false);
|
||||
setBatchTagInput('');
|
||||
setSelectedRowKeys([]);
|
||||
clearSelection();
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
@@ -556,7 +575,7 @@ export default function HuyaAccountsPage() {
|
||||
</Button>
|
||||
{canImport && (
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
disabled={selectedCount === 0}
|
||||
icon={<TagOutlined />}
|
||||
onClick={() => {
|
||||
setBatchTagInput('');
|
||||
@@ -568,7 +587,7 @@ export default function HuyaAccountsPage() {
|
||||
)}
|
||||
{canImport && (
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
disabled={selectedCount === 0}
|
||||
icon={<LoginOutlined />}
|
||||
loading={passwordLogging}
|
||||
onClick={handleLoginSelected}
|
||||
@@ -576,10 +595,10 @@ export default function HuyaAccountsPage() {
|
||||
登录选中
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && selectedRowKeys.length > 0 && (
|
||||
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙账号?`} onConfirm={handleDeleteSelected}>
|
||||
{canDelete && selectedCount > 0 && (
|
||||
<Popconfirm title={`确认删除${selectedAllMatching ? '当前筛选下' : '选中的'} ${selectedCount} 条虎牙账号?`} onConfirm={handleDeleteSelected}>
|
||||
<Button danger icon={<DeleteOutlined />}>
|
||||
删除选中 ({selectedRowKeys.length})
|
||||
删除选中 ({selectedCount})
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
@@ -657,9 +676,35 @@ export default function HuyaAccountsPage() {
|
||||
</div>
|
||||
|
||||
<Table
|
||||
title={() => selectedRowKeys.length > 0 ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={(
|
||||
<Space wrap>
|
||||
<span>
|
||||
{selectedAllMatching
|
||||
? `已选择当前筛选下全部 ${total} 个虎牙账号`
|
||||
: `已选择 ${selectedRowKeys.length} 个虎牙账号`}
|
||||
</span>
|
||||
{!selectedAllMatching && total > selectedRowKeys.length ? (
|
||||
<Button size="small" type="link" onClick={() => setSelectedAllMatching(true)}>
|
||||
选择当前筛选下全部 {total} 个
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="small" type="link" onClick={clearSelection}>
|
||||
取消选择
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
) : undefined}
|
||||
rowSelection={(canImport || canDelete || canAssign) ? {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
onChange: (keys) => {
|
||||
setSelectedRowKeys(keys);
|
||||
setSelectedAllMatching(false);
|
||||
},
|
||||
preserveSelectedRowKeys: true,
|
||||
} : undefined}
|
||||
columns={columns}
|
||||
@@ -720,7 +765,7 @@ export default function HuyaAccountsPage() {
|
||||
okText="确定"
|
||||
width={400}
|
||||
>
|
||||
<p>为选中的 {selectedRowKeys.length} 个虎牙账号设置标签:</p>
|
||||
<p>为{selectedAllMatching ? '当前筛选下' : '选中的'} {selectedCount} 个虎牙账号设置标签:</p>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
|
||||
Reference in New Issue
Block a user