支持账号按筛选全量批量操作

This commit is contained in:
yml2213
2026-07-25 10:50:56 +08:00
parent 09efb23f58
commit 10f298b4e1
8 changed files with 490 additions and 112 deletions
+137 -29
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+6
View File
@@ -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),
};
+6
View File
@@ -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 }),
+19 -1
View File
@@ -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 {
+87 -41
View File
@@ -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%' }}
+62 -17
View File
@@ -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%' }}