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

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
+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%' }}