From 054af84144bc190b6d2df0f8bb790c2378675af5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 30 Aug 2026 09:55:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=99=8E=E7=89=99=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=BB=91=E5=AE=9A=E9=A1=B5=E5=A4=9A=E9=80=89=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E8=A7=A3=E7=BB=91=20=E2=80=94=20rowSelection=20?= =?UTF-8?q?=E8=B7=A8=E9=A1=B5=E4=BF=9D=E7=95=99=20+=20=E5=85=A8=E9=80=89?= =?UTF-8?q?=E5=85=A8=E9=83=A8=20+=20=E6=89=B9=E9=87=8F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/backend/routers/huya.py | 43 ++++++++++++ web/backend/schemas.py | 5 ++ web/frontend/src/api/huya.ts | 4 ++ web/frontend/src/api/types.ts | 7 ++ .../src/pages/HuyaDeviceBindingsPage.tsx | 67 ++++++++++++++++++- 5 files changed, 123 insertions(+), 3 deletions(-) diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 918ec4c..75b2857 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -57,6 +57,7 @@ from ..schemas import ( HuyaConfigOut, HuyaConfigUpdate, HuyaCookieImport, + HuyaDeviceBindingsBatchDeleteRequest, HuyaGoodsOut, HuyaPasswordAccountImport, HuyaPasswordLoginRequest, @@ -1824,3 +1825,45 @@ def delete_device_binding(account: str, db: Session = Depends(get_db), current: removed_state = True return {"ok": True, "account": account, "profile_removed": True, "fp_state_removed": removed_state, "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} diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 88496d4..e582f62 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -397,6 +397,11 @@ class HuyaPasswordLoginSelectedRequest(BaseModel): force_new_device: bool = False +class HuyaDeviceBindingsBatchDeleteRequest(BaseModel): + """批量解绑设备:删除账号画像与 hydevice 指纹状态。""" + accounts: list[str] = Field(..., min_length=1) + + class HuyaAccountOut(BaseModel): id: int uid: str = "" diff --git a/web/frontend/src/api/huya.ts b/web/frontend/src/api/huya.ts index 084943c..ea9d410 100644 --- a/web/frontend/src/api/huya.ts +++ b/web/frontend/src/api/huya.ts @@ -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('/huya/device-bindings/batch-delete', { accounts }), importCookies: (text: string, tag: string = '') => api.post('/huya/accounts/import-cookies', { text, tag }), importPasswordAccounts: (text: string, tag: string = '') => diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 6e08e91..bdd99ed 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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; diff --git a/web/frontend/src/pages/HuyaDeviceBindingsPage.tsx b/web/frontend/src/pages/HuyaDeviceBindingsPage.tsx index a1eb114..2647169 100644 --- a/web/frontend/src/pages/HuyaDeviceBindingsPage.tsx +++ b/web/frontend/src/pages/HuyaDeviceBindingsPage.tsx @@ -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([]); const [loading, setLoading] = useState(false); const [unbinding, setUnbinding] = useState(''); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + 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['columns'] = [ { title: '账号', dataIndex: 'account', width: 150, ellipsis: true, render: (v: string) => {v} }, @@ -108,7 +136,35 @@ export default function HuyaDeviceBindingsPage() {

虎牙设备绑定

- + + {selectedRowKeys.length > 0 && ( + + 已选 {selectedRowKeys.length} 个账号 + + )} + + + + + +
@@ -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} 条` }} />