feat(web): 虎牙设备绑定页多选批量解绑 — rowSelection 跨页保留 + 全选全部 + 批量接口

- routers/huya.py: POST /api/huya/device-bindings/batch-delete (去重/跳过无记录/删画像+指纹目录)
- schemas.py: HuyaDeviceBindingsBatchDeleteRequest
- DeviceBindingsPage: 多选(rowSelection preserve) / 全选全部 / 解绑选中(N) + loading, 刷新后清理失效选中
- api/huya.ts + types.ts: deleteDeviceBindings 与响应类型

验证: 批量解绑逻辑单测 OK; 77 后端单测 OK; tsc + vite build OK
This commit is contained in:
yml2213
2026-08-30 09:55:53 +08:00
parent 22aa485536
commit 054af84144
5 changed files with 123 additions and 3 deletions
+4
View File
@@ -6,6 +6,7 @@ import type {
HuyaAccountItem,
HuyaAccountSummary,
HuyaDeviceBindingListResult,
HuyaDeviceBindingsBatchDeleteResult,
HuyaAutoRegisterBatch,
HuyaAutoRegisterRequest,
HuyaAutoRegisterRetryRequest,
@@ -49,6 +50,9 @@ export const huyaApi = {
/** 解绑: 删除设备画像与 hydevice 指纹状态, 下次登录自动生成全新环境 */
deleteDeviceBinding: (account: string) =>
api.delete<{ ok: boolean; message: string }, { ok: boolean; message: string }>(`/huya/device-bindings/${encodeURIComponent(account)}`),
/** 批量解绑: 一次删除多个账号的设备画像与指纹状态 */
deleteDeviceBindings: (accounts: string[]) =>
api.post<HuyaDeviceBindingsBatchDeleteResult, HuyaDeviceBindingsBatchDeleteResult>('/huya/device-bindings/batch-delete', { accounts }),
importCookies: (text: string, tag: string = '') =>
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
importPasswordAccounts: (text: string, tag: string = '') =>
+7
View File
@@ -486,6 +486,13 @@ export interface HuyaDeviceBindingListResult {
total: number;
}
export interface HuyaDeviceBindingsBatchDeleteResult {
ok: boolean;
removed: string[];
missing: string[];
message: string;
}
export interface HuyaAccountSummary extends BasicSummary {
password_ready_count: number;
point_count: number;
@@ -9,7 +9,7 @@
* 解绑 = 删除 1+2 → 下次 App 协议登录自动生成全新环境并重新注册签发 t2/t5。
*/
import { useCallback, useEffect, useState } from 'react';
import { Button, Card, Col, Popconfirm, Row, Statistic, Table, Tag, Tooltip, Typography } from 'antd';
import { Button, Card, Col, Popconfirm, Row, Space, Statistic, Table, Tag, Tooltip, Typography } from 'antd';
import type { TableProps } from 'antd';
import { LinkOutlined, ReloadOutlined, RestOutlined } from '@ant-design/icons';
import { message } from '../utils/antdMessage';
@@ -29,12 +29,19 @@ export default function HuyaDeviceBindingsPage() {
const [items, setItems] = useState<HuyaDeviceBindingItem[]>([]);
const [loading, setLoading] = useState(false);
const [unbinding, setUnbinding] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [unbindingSelected, setUnbindingSelected] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const result = await huyaApi.listDeviceBindings();
setItems(result.items || []);
// 已解绑/消失的账号从选中里剔除,避免残留脏选择。
setSelectedRowKeys((keys) => {
const alive = new Set((result.items || []).map((item) => item.account));
return keys.filter((key) => alive.has(String(key)));
});
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -58,6 +65,27 @@ export default function HuyaDeviceBindingsPage() {
}
};
/** 批量解绑选中的账号 */
const handleUnbindSelected = async () => {
const accounts = selectedRowKeys.map(String);
if (accounts.length === 0) return;
setUnbindingSelected(true);
try {
const result = await huyaApi.deleteDeviceBindings(accounts);
message.success(result.message || `已解绑 ${result.removed.length} 个账号`);
setSelectedRowKeys([]);
load();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setUnbindingSelected(false);
}
};
const handleSelectAll = () => {
setSelectedRowKeys(items.map((item) => item.account));
};
const columns: TableProps<HuyaDeviceBindingItem>['columns'] = [
{ title: '账号', dataIndex: 'account', width: 150, ellipsis: true,
render: (v: string) => <Text copyable={{ text: v }}>{v}</Text> },
@@ -108,7 +136,35 @@ export default function HuyaDeviceBindingsPage() {
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
<h2 style={{ margin: 0 }}></h2>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
<Space wrap>
{selectedRowKeys.length > 0 && (
<Text type="secondary">
<b>{selectedRowKeys.length}</b>
</Text>
)}
<Button size="small" disabled={items.length === 0} onClick={handleSelectAll}>
</Button>
<Popconfirm
title={`确认批量解绑选中的 ${selectedRowKeys.length} 个账号?`}
description="删除设备画像与指纹状态, 下次 App 协议登录将生成全新设备环境并重新注册"
okText="解绑"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
onConfirm={handleUnbindSelected}
>
<Button
danger
size="small"
icon={<RestOutlined />}
disabled={selectedRowKeys.length === 0}
loading={unbindingSelected}
>
({selectedRowKeys.length})
</Button>
</Popconfirm>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
</Space>
</div>
<Row gutter={12} style={{ marginBottom: 16 }}>
@@ -130,8 +186,13 @@ export default function HuyaDeviceBindingsPage() {
columns={columns}
dataSource={items}
loading={loading}
pagination={{ pageSize: 20, showSizeChanger: false, showTotal: (t) => `${t}` }}
size="middle"
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
pagination={{ pageSize: 20, showSizeChanger: false, showTotal: (t) => `${t}` }}
/>
</div>