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:
@@ -57,6 +57,7 @@ from ..schemas import (
|
|||||||
HuyaConfigOut,
|
HuyaConfigOut,
|
||||||
HuyaConfigUpdate,
|
HuyaConfigUpdate,
|
||||||
HuyaCookieImport,
|
HuyaCookieImport,
|
||||||
|
HuyaDeviceBindingsBatchDeleteRequest,
|
||||||
HuyaGoodsOut,
|
HuyaGoodsOut,
|
||||||
HuyaPasswordAccountImport,
|
HuyaPasswordAccountImport,
|
||||||
HuyaPasswordLoginRequest,
|
HuyaPasswordLoginRequest,
|
||||||
@@ -1824,3 +1825,45 @@ def delete_device_binding(account: str, db: Session = Depends(get_db), current:
|
|||||||
removed_state = True
|
removed_state = True
|
||||||
return {"ok": True, "account": account, "profile_removed": True, "fp_state_removed": removed_state,
|
return {"ok": True, "account": account, "profile_removed": True, "fp_state_removed": removed_state,
|
||||||
"message": "已解绑, 下次 App 协议登录将自动生成全新设备环境并重新注册"}
|
"message": "已解绑, 下次 App 协议登录将自动生成全新设备环境并重新注册"}
|
||||||
|
|
||||||
|
|
||||||
|
def _unbind_device_accounts(accounts: list[str]) -> tuple[list[str], list[str]]:
|
||||||
|
"""批量解绑实现: 返回 (成功解绑账号, 无绑定记录的账号)。"""
|
||||||
|
profiles = _load_profile_db()
|
||||||
|
removed: list[str] = []
|
||||||
|
missing: list[str] = []
|
||||||
|
seen = set()
|
||||||
|
for account in accounts:
|
||||||
|
account = (account or "").strip()
|
||||||
|
if not account or account in seen:
|
||||||
|
continue
|
||||||
|
seen.add(account)
|
||||||
|
if account not in profiles:
|
||||||
|
missing.append(account)
|
||||||
|
continue
|
||||||
|
profiles.pop(account)
|
||||||
|
state_dir = _fp_state_dir(account)
|
||||||
|
if state_dir.exists():
|
||||||
|
shutil.rmtree(state_dir, ignore_errors=True)
|
||||||
|
removed.append(account)
|
||||||
|
if removed:
|
||||||
|
_save_profile_db(profiles)
|
||||||
|
return removed, missing
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/device-bindings/batch-delete")
|
||||||
|
def delete_device_bindings(
|
||||||
|
req: HuyaDeviceBindingsBatchDeleteRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""批量解绑: 一次删除多个账号的设备画像与指纹状态。"""
|
||||||
|
_require_huya_perm(current, "huya:import")
|
||||||
|
removed, missing = _unbind_device_accounts(req.accounts)
|
||||||
|
if not removed:
|
||||||
|
raise HTTPException(status_code=404, detail="所选账号均没有设备绑定记录")
|
||||||
|
message = f"已批量解绑 {len(removed)} 个账号"
|
||||||
|
if missing:
|
||||||
|
message += f",{len(missing)} 个账号无绑定记录已跳过"
|
||||||
|
message += ",下次 App 协议登录将自动生成全新设备环境并重新注册"
|
||||||
|
return {"ok": True, "removed": removed, "missing": missing, "message": message}
|
||||||
|
|||||||
@@ -397,6 +397,11 @@ class HuyaPasswordLoginSelectedRequest(BaseModel):
|
|||||||
force_new_device: bool = False
|
force_new_device: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class HuyaDeviceBindingsBatchDeleteRequest(BaseModel):
|
||||||
|
"""批量解绑设备:删除账号画像与 hydevice 指纹状态。"""
|
||||||
|
accounts: list[str] = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class HuyaAccountOut(BaseModel):
|
class HuyaAccountOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
uid: str = ""
|
uid: str = ""
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
HuyaAccountItem,
|
HuyaAccountItem,
|
||||||
HuyaAccountSummary,
|
HuyaAccountSummary,
|
||||||
HuyaDeviceBindingListResult,
|
HuyaDeviceBindingListResult,
|
||||||
|
HuyaDeviceBindingsBatchDeleteResult,
|
||||||
HuyaAutoRegisterBatch,
|
HuyaAutoRegisterBatch,
|
||||||
HuyaAutoRegisterRequest,
|
HuyaAutoRegisterRequest,
|
||||||
HuyaAutoRegisterRetryRequest,
|
HuyaAutoRegisterRetryRequest,
|
||||||
@@ -49,6 +50,9 @@ export const huyaApi = {
|
|||||||
/** 解绑: 删除设备画像与 hydevice 指纹状态, 下次登录自动生成全新环境 */
|
/** 解绑: 删除设备画像与 hydevice 指纹状态, 下次登录自动生成全新环境 */
|
||||||
deleteDeviceBinding: (account: string) =>
|
deleteDeviceBinding: (account: string) =>
|
||||||
api.delete<{ ok: boolean; message: string }, { ok: boolean; message: string }>(`/huya/device-bindings/${encodeURIComponent(account)}`),
|
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 = '') =>
|
importCookies: (text: string, tag: string = '') =>
|
||||||
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
|
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
|
||||||
importPasswordAccounts: (text: string, tag: string = '') =>
|
importPasswordAccounts: (text: string, tag: string = '') =>
|
||||||
|
|||||||
@@ -486,6 +486,13 @@ export interface HuyaDeviceBindingListResult {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HuyaDeviceBindingsBatchDeleteResult {
|
||||||
|
ok: boolean;
|
||||||
|
removed: string[];
|
||||||
|
missing: string[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HuyaAccountSummary extends BasicSummary {
|
export interface HuyaAccountSummary extends BasicSummary {
|
||||||
password_ready_count: number;
|
password_ready_count: number;
|
||||||
point_count: number;
|
point_count: number;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* 解绑 = 删除 1+2 → 下次 App 协议登录自动生成全新环境并重新注册签发 t2/t5。
|
* 解绑 = 删除 1+2 → 下次 App 协议登录自动生成全新环境并重新注册签发 t2/t5。
|
||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
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 type { TableProps } from 'antd';
|
||||||
import { LinkOutlined, ReloadOutlined, RestOutlined } from '@ant-design/icons';
|
import { LinkOutlined, ReloadOutlined, RestOutlined } from '@ant-design/icons';
|
||||||
import { message } from '../utils/antdMessage';
|
import { message } from '../utils/antdMessage';
|
||||||
@@ -29,12 +29,19 @@ export default function HuyaDeviceBindingsPage() {
|
|||||||
const [items, setItems] = useState<HuyaDeviceBindingItem[]>([]);
|
const [items, setItems] = useState<HuyaDeviceBindingItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [unbinding, setUnbinding] = useState('');
|
const [unbinding, setUnbinding] = useState('');
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [unbindingSelected, setUnbindingSelected] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await huyaApi.listDeviceBindings();
|
const result = await huyaApi.listDeviceBindings();
|
||||||
setItems(result.items || []);
|
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) {
|
} catch (e: unknown) {
|
||||||
message.error(getErrorMessage(e));
|
message.error(getErrorMessage(e));
|
||||||
} finally {
|
} 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'] = [
|
const columns: TableProps<HuyaDeviceBindingItem>['columns'] = [
|
||||||
{ title: '账号', dataIndex: 'account', width: 150, ellipsis: true,
|
{ title: '账号', dataIndex: 'account', width: 150, ellipsis: true,
|
||||||
render: (v: string) => <Text copyable={{ text: v }}>{v}</Text> },
|
render: (v: string) => <Text copyable={{ text: v }}>{v}</Text> },
|
||||||
@@ -108,7 +136,35 @@ export default function HuyaDeviceBindingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||||
<h2 style={{ margin: 0 }}>虎牙设备绑定</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
<Row gutter={12} style={{ marginBottom: 16 }}>
|
<Row gutter={12} style={{ marginBottom: 16 }}>
|
||||||
@@ -130,8 +186,13 @@ export default function HuyaDeviceBindingsPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={items}
|
dataSource={items}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 20, showSizeChanger: false, showTotal: (t) => `共 ${t} 条` }}
|
|
||||||
size="middle"
|
size="middle"
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
|
preserveSelectedRowKeys: true,
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 20, showSizeChanger: false, showTotal: (t) => `共 ${t} 条` }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user