支持账号按筛选全量批量操作
This commit is contained in:
@@ -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