From 10f298b4e1a19e53ed8a544260a1fe065b907cf5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 25 Jul 2026 10:50:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B4=A6=E5=8F=B7=E6=8C=89?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=85=A8=E9=87=8F=E6=89=B9=E9=87=8F=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/backend/routers/accounts.py | 166 ++++++++++++++---- web/backend/routers/huya.py | 176 +++++++++++++++++--- web/backend/schemas.py | 21 ++- web/frontend/src/api/accounts.ts | 6 + web/frontend/src/api/huya.ts | 6 + web/frontend/src/api/types.ts | 20 ++- web/frontend/src/pages/AccountsPage.tsx | 128 +++++++++----- web/frontend/src/pages/HuyaAccountsPage.tsx | 79 +++++++-- 8 files changed, 490 insertions(+), 112 deletions(-) diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py index 0343d49..534e0fb 100644 --- a/web/backend/routers/accounts.py +++ b/web/backend/routers/accounts.py @@ -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)) diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index b6bd272..66d0c31 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -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), diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 35d392a..2d649dd 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -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): diff --git a/web/frontend/src/api/accounts.ts b/web/frontend/src/api/accounts.ts index 270b680..93c52cb 100644 --- a/web/frontend/src/api/accounts.ts +++ b/web/frontend/src/api/accounts.ts @@ -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(`/accounts/${id}/tag`, { tag }), batchTag: (account_ids: number[], tag: string) => api.put('/accounts/batch-tag', { account_ids, tag }), + batchTagSelection: (data: AccountBulkTagRequest) => + api.put('/accounts/batch-tag-selection', data), listTags: () => api.get('/accounts/tags/list'), delete: (id: number) => api.delete(`/accounts/${id}`), batchDelete: (account_ids: number[]) => api.delete('/accounts/batch/delete', { params: { account_ids: account_ids.join(',') } }), + batchDeleteSelection: (data: AccountBulkSelection) => + api.post('/accounts/batch-delete', data), }; diff --git a/web/frontend/src/api/huya.ts b/web/frontend/src/api/huya.ts index 54e9021..beb057a 100644 --- a/web/frontend/src/api/huya.ts +++ b/web/frontend/src/api/huya.ts @@ -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(`/huya/accounts/${id}/tag`, { tag }), batchTag: (account_ids: number[], tag: string) => api.put('/huya/accounts/batch-tag', { account_ids, tag }), + batchTagSelection: (data: AccountBulkTagRequest) => + api.put('/huya/accounts/batch-tag-selection', data), listTags: () => api.get('/huya/accounts/tags/list'), deleteAccount: (id: number) => api.delete(`/huya/accounts/${id}`), deleteAccounts: (accountIds: number[]) => api.delete('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }), + deleteAccountsSelection: (data: AccountBulkSelection) => + api.post('/huya/accounts/batch-delete', data), listCookies: () => api.get('/huya/cookies'), listCookiesPaged: (params: PageParams & { include_cookie?: boolean }) => api.get, PaginatedResponse>('/huya/cookies', { params }), diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 4482ae4..695a02f 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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 { 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 { diff --git a/web/frontend/src/pages/AccountsPage.tsx b/web/frontend/src/pages/AccountsPage.tsx index 6dfc8ba..28f3c71 100644 --- a/web/frontend/src/pages/AccountsPage.tsx +++ b/web/frontend/src/pages/AccountsPage.tsx @@ -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(''); const [searchText, setSearchText] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); + 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 && ( - <> + + )} + {canDelete && ( + - {canDelete && ( - - - - )} - - + + )} + {canImport && ( + )} @@ -388,9 +408,35 @@ export default function AccountsPage() { selectedRowKeys.length > 0 ? ( + + + {selectedAllMatching + ? `已选择当前筛选下全部 ${total} 个账号` + : `已选择 ${selectedRowKeys.length} 个账号`} + + {!selectedAllMatching && total > selectedRowKeys.length ? ( + + ) : null} + + + )} + /> + ) : 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} > -

为选中的 {selectedRowKeys.length} 个账号设置标签:

+

为{selectedAllMatching ? '当前筛选下' : '选中的'} {selectedCount} 个账号设置标签:

selectedRowKeys.length > 0 ? ( + + + {selectedAllMatching + ? `已选择当前筛选下全部 ${total} 个虎牙账号` + : `已选择 ${selectedRowKeys.length} 个虎牙账号`} + + {!selectedAllMatching && total > selectedRowKeys.length ? ( + + ) : null} + + + )} + /> + ) : 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} > -

为选中的 {selectedRowKeys.length} 个虎牙账号设置标签:

+

为{selectedAllMatching ? '当前筛选下' : '选中的'} {selectedCount} 个虎牙账号设置标签: