From 178eba1eea2ca5d17f39dd3af21aad328e44aec5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 5 Sep 2026 18:30:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E5=85=91=E6=8D=A2?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=88=90=E9=A1=B5=E9=9D=A2=E5=B9=B6=E6=8C=89?= =?UTF-8?q?=E6=96=97=E9=B1=BC=E6=A8=A1=E5=BC=8F=E4=BC=98=E5=8C=96=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 /huya/browser-exchange 页面(精英宝典下方),支持导入账号与 执行兑换,客服仅见已分配账号;账号管理页移除该功能。导入弹窗对齐 斗鱼操作台:服务端分页筛选、可搜索标签、无 CK 不展示、导入本页/ 一键导入全部/导入选中;新增 /accounts/ids 接口与 workbench browser_exchange scope。 --- docs/HUYA_BROWSER_EXCHANGE.md | 13 +- tests/test_huya_workbench_scopes.py | 32 ++ web/backend/routers/huya.py | 32 +- web/backend/schemas.py | 2 +- web/frontend/src/App.tsx | 2 + web/frontend/src/api/huya.ts | 9 +- web/frontend/src/api/types.ts | 3 + web/frontend/src/layouts/MainLayout.tsx | 3 +- web/frontend/src/pages/HuyaAccountsPage.tsx | 118 +----- .../src/pages/HuyaBrowserExchangePage.tsx | 377 ++++++++++++++++++ web/frontend/src/utils/huyaExchange.ts | 32 ++ 11 files changed, 493 insertions(+), 130 deletions(-) create mode 100644 web/frontend/src/pages/HuyaBrowserExchangePage.tsx create mode 100644 web/frontend/src/utils/huyaExchange.ts diff --git a/docs/HUYA_BROWSER_EXCHANGE.md b/docs/HUYA_BROWSER_EXCHANGE.md index 32f4ebd..ac5a4b3 100644 --- a/docs/HUYA_BROWSER_EXCHANGE.md +++ b/docs/HUYA_BROWSER_EXCHANGE.md @@ -2,6 +2,14 @@ 人工兑换在操作者自己的 Chrome/Edge 中完成:后端下发目标页地址和登录材料,登录态自动(扩展)或手动(控制台脚本)写入本机浏览器,兑换本身始终由人工点击,服务端不托管任何浏览器会话。 +## 入口与账号范围 + +独立页面:虎牙 → 浏览器兑换(位于精英宝典下方;账号管理页不再提供该功能)。 + +- 页面维护一份「兑换账号列表」,通过「导入账号」从自己可见的账号池勾选加入;客服只能看到分配给自己的账号。 +- 列表按用户隔离保存(工作台 scope `browser_exchange`),与精英宝典的账号集合互不影响。 +- 账号需先在账号管理完成 App 协议登录(有 Cookie)才能执行兑换。 + ## 一键自动注入(推荐):安装内置扩展 项目自带 Chrome/Edge 扩展 `web/extension/huya-exchange-helper/`,安装一次后即可自动注入: @@ -12,7 +20,7 @@ 之后的流程: -1. 点击账号行的浏览器图标 → 后端返回兑换材料 → 扩展自动把该账号 Cookie 写入 `.huya.com` 域并在新标签页打开已登录的兑换页。 +1. 在兑换列表点击账号的「浏览器兑换」→ 后端返回兑换材料 → 扩展自动把该账号 Cookie 写入 `.huya.com` 域并在新标签页打开已登录的兑换页。 2. 确认页面右上角账号无误后,手动完成兑换。 3. 回到后台点击「完成」。 @@ -20,7 +28,7 @@ ## 手动方式(未装扩展时的兜底) -1. 点击账号行的浏览器图标,弹出「浏览器兑换」窗口。 +1. 在兑换列表点击「浏览器兑换」,弹出「浏览器兑换」窗口。 2. 点击「打开兑换页面」在新标签页打开兑换活动页(地址来自配置的 `room_pid`,未配置时回落到活动页 `https://zt.huya.com/b02faae1/pc/index.html`)。 3. 点击「复制登录脚本」,在兑换页面按 `F12` 打开控制台,粘贴脚本并回车执行。Chrome/Edge 首次粘贴需先手动输入 `allow pasting` 再回车。脚本把该账号 Cookie 写入 `.huya.com` 域并自动跳转。 4. 确认登录态正确后完成兑换,回到系统点击「完成」。 @@ -43,6 +51,7 @@ - 脚本由 `core.huya.cookie_utils.browser_login_script` 生成:Cookie 以 JSON 数组内联(自动转义),逐条 `document.cookie` 写入 `domain=.huya.com; path=/`,有效期 180 天,最后跳转目标页。 - 扩展注入走同一条接口:前端解析 `cookie_string` 后通过 DOM 事件交给扩展,由 `chrome.cookies` API 写入。 +- 兑换账号列表复用 `/api/huya/workbench-accounts`(`handbook_scope=browser_exchange`)。 - 服务端不保存任何会话状态。 ## 安全说明 diff --git a/tests/test_huya_workbench_scopes.py b/tests/test_huya_workbench_scopes.py index 12a0015..e1a82f9 100644 --- a/tests/test_huya_workbench_scopes.py +++ b/tests/test_huya_workbench_scopes.py @@ -6,6 +6,7 @@ from web.backend.database import Base from web.backend.models import HuyaAccount, HuyaWorkbench, HuyaWorkbenchAccount, User from web.backend.routers.huya import ( delete_account, + list_huya_account_ids, list_huya_workbench_accounts, list_tasks, update_huya_workbench_accounts, @@ -70,6 +71,37 @@ class TestHuyaWorkbenchScope: handbook_scope="invalid", ) + def test_browser_exchange_scope_and_account_ids(self): + no_cookie = HuyaAccount(uid="200", yyuid="200", cookie="") + self.session.add(no_cookie) + self.session.commit() + + update_huya_workbench_accounts( + HuyaWorkbenchAccountsUpdate(handbook_scope="browser_exchange", account_ids=[self.account.id]), + db=self.session, + current=self.user, + ) + assert list_huya_workbench_accounts("browser_exchange", db=self.session, current=self.user) == { + "account_ids": [self.account.id], + "configured": True, + } + # browser_exchange 与 elite 两个 scope 的账号集合互不影响 + assert list_huya_workbench_accounts("elite", db=self.session, current=self.user) == { + "account_ids": [], + "configured": False, + } + + # ids 接口:has_cookie=True 只返回有 Cookie 的账号 + result = list_huya_account_ids(has_cookie=True, db=self.session, current=self.user) + assert result == {"account_ids": [self.account.id], "total": 1} + all_ids = list_huya_account_ids(db=self.session, current=self.user) + assert sorted(all_ids["account_ids"]) == sorted([self.account.id, no_cookie.id]) + assert all_ids["total"] == 2 + + # ids 接口:search 匹配 uid + searched = list_huya_account_ids(search="200", db=self.session, current=self.user) + assert searched["account_ids"] == [no_cookie.id] + def test_account_delete_cleans_workbench_membership(self): self.session.add( HuyaWorkbenchAccount( diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index ba3297b..be240a6 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -315,10 +315,10 @@ def _require_huya_task_account_access( raise HTTPException(status_code=403, detail="包含无权操作的虎牙账号") -def _elite_workbench_account_ids(db: Session, current: User) -> list[int]: +def _workbench_account_ids(db: Session, current: User, scope: str) -> list[int]: query = db.query(HuyaWorkbenchAccount.account_id).filter( HuyaWorkbenchAccount.user_id == current.id, - HuyaWorkbenchAccount.handbook_scope == "elite", + HuyaWorkbenchAccount.handbook_scope == scope, ) if not _can_view_huya_all(current): query = query.join(HuyaAccount, HuyaAccount.id == HuyaWorkbenchAccount.account_id).filter( @@ -629,6 +629,26 @@ def accounts_summary( } +@router.get("/accounts/ids") +def list_huya_account_ids( + search: str = "", + tag: str = "", + has_cookie: bool = False, + db: Session = Depends(get_db), + current: User = Depends(require_permission("huya:task")), +): + """返回当前用户可见的全部账号 ID(不受分页限制),供工作台一键导入。""" + query = _filter_huya_accounts_query( + _visible_huya_accounts_query(db, current), + current, + tag=tag or None, + has_cookie=has_cookie, + search=search, + ) + rows = query.with_entities(HuyaAccount.id).all() + return {"account_ids": [row[0] for row in rows], "total": len(rows)} + + @router.post("/accounts/import-cookies") def import_cookies( req: HuyaCookieImport, @@ -1821,12 +1841,12 @@ def update_config( @router.get("/workbench-accounts") def list_huya_workbench_accounts( - handbook_scope: str = Query(..., pattern="^elite$"), + handbook_scope: str = Query(..., pattern="^elite$|^browser_exchange$"), db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): - """读取当前用户的虎牙精英宝典账号集合。""" - ids = _elite_workbench_account_ids(db, current) + """读取当前用户指定工作台(精英宝典/浏览器兑换)的账号集合。""" + ids = _workbench_account_ids(db, current, handbook_scope) configured = db.query(HuyaWorkbench.id).filter( HuyaWorkbench.user_id == current.id, HuyaWorkbench.handbook_scope == handbook_scope, @@ -1840,7 +1860,7 @@ def update_huya_workbench_accounts( db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): - """覆盖虎牙精英宝典账号集合,支持跨浏览器同步。""" + """覆盖当前用户指定工作台(精英宝典/浏览器兑换)的账号集合,支持跨浏览器同步。""" ids = sorted(set(req.account_ids)) if any(account_id < 1 for account_id in ids): raise HTTPException(status_code=400, detail="无效的账号 ID") diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 4beced9..3cfb3e4 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -511,7 +511,7 @@ class HuyaTaskBatchRequest(BaseModel): class HuyaWorkbenchAccountsUpdate(BaseModel): - handbook_scope: str = Field(..., pattern="^elite$") + handbook_scope: str = Field(..., pattern="^elite$|^browser_exchange$") account_ids: list[int] = Field(default_factory=list) diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx index a0b28d0..5a0166e 100644 --- a/web/frontend/src/App.tsx +++ b/web/frontend/src/App.tsx @@ -26,6 +26,7 @@ const HuyaRegisterPage = lazy(() => import('./pages/HuyaRegisterPage')); const HuyaDeviceBindingsPage = lazy(() => import('./pages/HuyaDeviceBindingsPage')); const HuyaTasksPage = lazy(() => import('./pages/HuyaTasksPage')); const HuyaElitePage = lazy(() => import('./pages/HuyaElitePage')); +const HuyaBrowserExchangePage = lazy(() => import('./pages/HuyaBrowserExchangePage')); const YybRechargePage = lazy(() => import('./pages/YybRechargePage')); const AuditLogsPage = lazy(() => import('./pages/AuditLogsPage')); @@ -96,6 +97,7 @@ function AppContent() { )} /> )} /> )} /> + )} /> )} /> )} /> )} /> diff --git a/web/frontend/src/api/huya.ts b/web/frontend/src/api/huya.ts index 48ccf33..4581fe2 100644 --- a/web/frontend/src/api/huya.ts +++ b/web/frontend/src/api/huya.ts @@ -30,6 +30,7 @@ import type { HuyaTaskBatchResult, HuyaTaskItem, HuyaBrowserExchangeKit, + HuyaWorkbenchScope, MessageCountResponse, MessageDeletedResponse, MessageResponse, @@ -42,8 +43,10 @@ export const huyaApi = { taskTypes: () => api.get, Record>('/huya/task-types'), listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) => api.get('/huya/accounts', { params }), - listAccountsPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) => + listAccountsPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; search?: string; include_cookie?: boolean }) => api.get, PaginatedResponse>('/huya/accounts', { params }), + listAccountIds: (params?: { search?: string; tag?: string; has_cookie?: boolean }) => + api.get<{ account_ids: number[]; total: number }, { account_ids: number[]; total: number }>('/huya/accounts/ids', { params }), accountsSummary: () => api.get('/huya/accounts/summary'), /** 设备绑定: 账号 ↔ 环境列表 (画像+指纹状态+账号表合并) */ listDeviceBindings: () => @@ -119,11 +122,11 @@ export const huyaApi = { updateConfig: (data: Partial) => api.put('/huya/config', data), listGoods: () => api.get('/huya/goods'), listRechargeGoods: () => api.get('/huya/recharge-goods'), - listWorkbenchAccounts: (scope: 'elite' = 'elite') => + listWorkbenchAccounts: (scope: HuyaWorkbenchScope = 'elite') => api.get<{ account_ids: number[]; configured: boolean }, { account_ids: number[]; configured: boolean }>( '/huya/workbench-accounts', { params: { handbook_scope: scope } }, ), - updateWorkbenchAccounts: (accountIds: number[], scope: 'elite' = 'elite') => + updateWorkbenchAccounts: (accountIds: number[], scope: HuyaWorkbenchScope = 'elite') => api.put<{ account_ids: number[]; success: boolean }, { account_ids: number[]; success: boolean }>( '/huya/workbench-accounts', { handbook_scope: scope, account_ids: accountIds }, ), diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index be9f5b5..8c814f7 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -464,6 +464,9 @@ export interface HuyaAccountItem { updated_at: string | null; } +/** 工作台账号集合范围: elite=精英宝典 / browser_exchange=浏览器兑换 */ +export type HuyaWorkbenchScope = 'elite' | 'browser_exchange'; + /** 本机浏览器人工兑换材料 (登录脚本在用户自己的 Chrome/Edge 控制台执行) */ export interface HuyaBrowserExchangeKit { account_id: number; diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index f4d03e1..f204d25 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -6,7 +6,7 @@ import { CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined, MenuFoldOutlined, MenuUnfoldOutlined, SwapOutlined, SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ShoppingCartOutlined, ApartmentOutlined, MobileOutlined, - BookOutlined, TrophyOutlined, ShopOutlined, + BookOutlined, TrophyOutlined, ShopOutlined, GlobalOutlined, SafetyCertificateOutlined, FileSearchOutlined, } from '@ant-design/icons'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; @@ -96,6 +96,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { } if (can('huya:task')) { huyaItems.push({ key: '/huya/elite', label: '精英宝典', icon: }); + huyaItems.push({ key: '/huya/browser-exchange', label: '浏览器兑换', icon: }); huyaItems.push({ key: '/huya/tasks', label: '旧版任务台', icon: }); } diff --git a/web/frontend/src/pages/HuyaAccountsPage.tsx b/web/frontend/src/pages/HuyaAccountsPage.tsx index 4dc0394..658b100 100644 --- a/web/frontend/src/pages/HuyaAccountsPage.tsx +++ b/web/frontend/src/pages/HuyaAccountsPage.tsx @@ -4,19 +4,17 @@ import { } from 'antd'; import { message } from '../utils/antdMessage'; import type { TableProps } from 'antd'; -import { CopyOutlined, DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOutlined, ReloadOutlined, SearchOutlined, TagOutlined, GlobalOutlined } from '@ant-design/icons'; +import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons'; import { huyaApi, type HuyaAccountItem, type HuyaAccountSummary, - type HuyaBrowserExchangeKit, type HuyaPasswordLoginBatchItem, type SupportUserItem, } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; -import { copyTextToClipboard } from '../utils/clipboard'; const { Text, Paragraph } = Typography; const { TextArea } = Input; @@ -50,30 +48,6 @@ const STATUS_COLORS: Record = { invalid: 'error', }; -/** 解析后端 Cookie 字符串为 [name, value] 数组,供兑换扩展注入 */ -function parseCookieString(cookieString: string): [string, string][] { - return cookieString - .split(';') - .map((part) => part.trim()) - .filter((part) => part.includes('=')) - .map((part) => { - const idx = part.indexOf('='); - return [part.slice(0, idx), part.slice(idx + 1)]; - }); -} - -/** 内置扩展的内容脚本会在所有页面打上此标记 */ -function huyaExchangeHelperReady(): boolean { - return document.documentElement.dataset.huyaExchangeHelper === '1'; -} - -/** 请求内置扩展自动注入登录态;同时留存到 window 供扩展图标点击兜底读取 */ -function requestExtensionInject(kit: HuyaBrowserExchangeKit): void { - const payload = JSON.stringify({ target_url: kit.target_url, cookies: parseCookieString(kit.cookie_string) }); - (window as unknown as { __huyaExchangeKit?: string }).__huyaExchangeKit = payload; - window.dispatchEvent(new CustomEvent('huya-exchange-kit', { detail: payload })); -} - export default function HuyaAccountsPage() { const [accounts, setAccounts] = useState([]); const [users, setUsers] = useState([]); @@ -99,9 +73,6 @@ export default function HuyaAccountsPage() { const [smsState, setSmsState] = useState(''); const [smsSending, setSmsSending] = useState(false); const [smsLogging, setSmsLogging] = useState(false); - const [exchangeKit, setExchangeKit] = useState(null); - const [exchangeTitle, setExchangeTitle] = useState(''); - const [exchangeLoading, setExchangeLoading] = useState(false); const [searchText, setSearchText] = useState(''); const [tagFilter, setTagFilter] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); @@ -380,34 +351,6 @@ export default function HuyaAccountsPage() { } }; - const handleBrowserExchange = async (record: HuyaAccountItem) => { - setExchangeLoading(true); - try { - const kit = await huyaApi.createBrowserExchange(record.id); - requestExtensionInject(kit); - setExchangeKit(kit); - setExchangeTitle(record.nickname || record.username || record.yyuid || `账号#${record.id}`); - } catch (e: unknown) { - message.error(getErrorMessage(e)); - } finally { - setExchangeLoading(false); - } - }; - - const openExchangePage = () => { - if (!exchangeKit) return; - window.open(exchangeKit.target_url, '_blank', 'noopener,noreferrer'); - }; - - const copyExchangeText = async (text: string, tip: string) => { - try { - await copyTextToClipboard(text); - message.success(tip); - } catch { - message.error('复制失败,请重试'); - } - }; - const handleWebLoginSelected = async () => { if (selectedCount === 0) { message.warning('请先选择虎牙账号'); @@ -683,11 +626,6 @@ export default function HuyaAccountsPage() { )} - {canImport && record.has_cookie && ( - - , - , - ] : null} - > - {exchangeKit && ( - - {exchangeHelperReady ? ( - - ) : ( - - )} - - 手动步骤 1: - 点击下方「打开兑换页面」,在新标签页打开兑换活动页。 - - - 手动步骤 2: - 点击「复制登录脚本」,在兑换页面按 F12 打开控制台,粘贴脚本并回车执行。 - Chrome/Edge 首次粘贴需先手动输入 allow pasting 再回车。脚本会写入该账号的登录态并自动跳转。 - - - - - - - 备选:安装 Cookie-editor 类扩展后,在 .huya.com 域下导入 Cookie 字符串。脚本和 Cookie 等同账号凭证,请勿泄露给他人。 - - - 手动步骤 3: - 确认页面右上角登录态为目标账号后完成兑换,回到此处点击「完成」。 - - - )} - - ([]); + const [accounts, setAccounts] = useState([]); + const [loading, setLoading] = useState(false); + // 导入弹窗:候选池走服务端分页筛选(搜索/标签/仅有 CK),与斗鱼操作台导入一致 + const [importOpen, setImportOpen] = useState(false); + const [importPool, setImportPool] = useState([]); + const [importTotal, setImportTotal] = useState(0); + const [importPage, setImportPage] = useState(1); + const [importLoading, setImportLoading] = useState(false); + const [importSearch, setImportSearch] = useState(''); + const [importTag, setImportTag] = useState(''); + const [importTags, setImportTags] = useState([]); + const [importSelectedIds, setImportSelectedIds] = useState([]); + const [importAllLoading, setImportAllLoading] = useState(false); + const [exchangeKit, setExchangeKit] = useState(null); + const [exchangeTitle, setExchangeTitle] = useState(''); + const [exchangeId, setExchangeId] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + try { + const [poolResult, workbenchResult] = await Promise.all([ + huyaApi.listAccounts({ include_cookie: false }), + huyaApi.listWorkbenchAccounts(SCOPE), + ]); + setPool(poolResult); + const byId = new Map(poolResult.map((item) => [item.id, item])); + setAccounts( + workbenchResult.account_ids + .map((id) => byId.get(Number(id))) + .filter((item): item is HuyaAccountItem => Boolean(item)), + ); + } catch (error) { + message.error(getErrorMessage(error)); + } finally { + setLoading(false); + } + }, []); + + const loadImportPool = useCallback(async () => { + setImportLoading(true); + try { + // 候选池 = 账号库按搜索/标签过滤且必须有 Cookie,剔除已加入兑换列表的账号 + const result = await huyaApi.listAccountsPaged({ + search: importSearch.trim() || undefined, + tag: importTag || undefined, + has_cookie: true, + page: importPage, + page_size: IMPORT_PAGE_SIZE, + }); + const exclude = new Set(accounts.map((item) => item.id)); + setImportPool(result.items.filter((item) => !exclude.has(item.id))); + setImportTotal(result.total); + } catch (error) { + message.error(getErrorMessage(error)); + } finally { + setImportLoading(false); + } + }, [accounts, importPage, importSearch, importTag]); + + useEffect(() => { void load(); }, [load]); + useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]); + useEffect(() => { + if (!importOpen) return; + void huyaApi.listTags().then(setImportTags).catch(() => {}); + }, [importOpen]); + // 切换搜索/标签/翻页时清空已选,避免计数与可见行不一致 + useEffect(() => { + setImportPage(1); + setImportSelectedIds([]); + }, [importSearch, importTag]); + useEffect(() => { setImportSelectedIds([]); }, [importPage]); + + const persistAccounts = async (ids: number[]): Promise => { + const saved = await huyaApi.updateWorkbenchAccounts(ids, SCOPE); + const byId = new Map(pool.map((item) => [item.id, item])); + setAccounts( + saved.account_ids + .map((id) => byId.get(Number(id))) + .filter((item): item is HuyaAccountItem => Boolean(item)), + ); + return saved.account_ids; + }; + + const removeAccount = async (record: HuyaAccountItem) => { + try { + await persistAccounts(accounts.filter((item) => item.id !== record.id).map((item) => item.id)); + } catch (error) { + message.error(getErrorMessage(error)); + } + }; + + const importAccounts = async (ids: number[]) => { + const toImport = importPool.filter((item) => ids.includes(item.id)); + if (!toImport.length) return; + try { + await persistAccounts([...new Set([...accounts.map((item) => item.id), ...toImport.map((item) => item.id)])]); + setImportSelectedIds([]); + setImportOpen(false); + message.success(`已导入 ${toImport.length} 个账号`); + } catch (error) { + message.error(getErrorMessage(error)); + } + }; + + const importAllAccounts = async () => { + setImportAllLoading(true); + try { + const result = await huyaApi.listAccountIds({ + search: importSearch.trim() || undefined, + tag: importTag || undefined, + has_cookie: true, + }); + const existing = new Set(accounts.map((item) => item.id)); + const imported = result.account_ids.filter((id) => !existing.has(id)); + if (!imported.length) { + message.info('当前筛选条件下没有可新增的账号'); + return; + } + await persistAccounts([...accounts.map((item) => item.id), ...imported]); + setImportSelectedIds([]); + setImportOpen(false); + message.success(`已一键导入 ${imported.length} 个账号`); + } catch (error) { + message.error(getErrorMessage(error)); + } finally { + setImportAllLoading(false); + } + }; + + const handleExchange = async (record: HuyaAccountItem) => { + setExchangeId(record.id); + try { + const kit = await huyaApi.createBrowserExchange(record.id); + requestExtensionInject(kit); + setExchangeKit(kit); + setExchangeTitle(record.nickname || record.username || record.yyuid || `账号#${record.id}`); + } catch (error) { + message.error(getErrorMessage(error)); + } finally { + setExchangeId(null); + } + }; + + const copyExchangeText = async (text: string, tip: string) => { + try { + await copyTextToClipboard(text); + message.success(tip); + } catch { + message.error('复制失败,请重试'); + } + }; + + const exchangeHelperReady = huyaExchangeHelperReady(); + + const columns: TableProps['columns'] = [ + { + title: '账号', + render: (_value, record) => ( + + {record.nickname || record.username || record.uid} + {record.username && record.username !== (record.nickname || record.username) && ( + {record.username} + )} + + ), + }, + { title: '标签', dataIndex: 'tag', render: (tag: string) => (tag ? {tag} : '-') }, + { + title: '登录态', + render: (_value, record) => (record.has_cookie + ? 有 Cookie + : 无 Cookie), + }, + { + title: '操作', + width: 200, + render: (_value, record) => ( + + + + + void removeAccount(record)}> + + + + + + {!exchangeHelperReady && ( + + )} + + + + rowKey="id" + size="small" + columns={columns} + dataSource={accounts} + loading={loading} + pagination={accounts.length > 20 ? { pageSize: 20 } : false} + locale={{ emptyText: '还没有账号,点击右上角「导入账号」开始' }} + /> + + + setImportOpen(false)} + footer={null} + width={700} + > +
+ + } + allowClear + value={importSearch} + onChange={(event) => setImportSearch(event.target.value)} + style={{ width: 220 }} + /> +