From d6ab4d8699b72ebc4ebc7ad5712387247a9406b0 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 13:13:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=99=8E=E7=89=99=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20=E2=80=94=20=E7=99=BB=E5=BD=95=E6=B8=A0=E9=81=93=E5=8C=BA?= =?UTF-8?q?=E5=88=86=20(Web=20=E6=97=A7=E7=89=88=20/=20App=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=20/=20=E7=9F=AD=E4=BF=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - HuyaAccount 新增 login_channel 字段 + 迁移 20260829_0030 (启动自动升级) - 5 个登录端点成功后记录渠道: web/app 批量与单账号 + 短信 - HuyaAccountOut 输出 login_channel 前端: - 账号表新增『登录渠道』列 (App 协议/Web 旧版/短信/仅导入) - 工具栏登录按钮分组: App 协议登录(primary 推荐) 与 Web 登录(旧版, Tooltip 说明) - 行内操作新增单账号 App/Web 登录按钮 (复用批量端点传单 id) - tsc + vite build 通过 --- .../20260829_0030_huya_login_channel.py | 45 +++++++ web/backend/models.py | 2 + web/backend/routers/huya.py | 7 ++ web/backend/schemas.py | 2 + web/frontend/src/api/types.ts | 2 + web/frontend/src/pages/HuyaAccountsPage.tsx | 119 ++++++++++++++---- 6 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 web/backend/migrations/versions/20260829_0030_huya_login_channel.py diff --git a/web/backend/migrations/versions/20260829_0030_huya_login_channel.py b/web/backend/migrations/versions/20260829_0030_huya_login_channel.py new file mode 100644 index 0000000..f0ca0fd --- /dev/null +++ b/web/backend/migrations/versions/20260829_0030_huya_login_channel.py @@ -0,0 +1,45 @@ +"""虎牙账号增加登录渠道字段 (区分 Web 旧版 / App 协议登录)。 + +Revision ID: 20260829_0030 +Revises: 20260814_0029 +Create Date: 2026-08-29 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "20260829_0030" +down_revision: Union[str, None] = "20260814_0029" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """huya_accounts 新增 login_channel (上次成功登录渠道)。""" + bind = op.get_bind() + inspector = sa.inspect(bind) + if not inspector.has_table("huya_accounts"): + return + columns = {column["name"] for column in inspector.get_columns("huya_accounts")} + if "login_channel" not in columns: + op.add_column("huya_accounts", sa.Column("login_channel", sa.String(16), nullable=False, server_default="")) + indexes = {index["name"] for index in inspector.get_indexes("huya_accounts")} + if "ix_huya_accounts_login_channel" not in indexes: + op.create_index("ix_huya_accounts_login_channel", "huya_accounts", ["login_channel"]) + + +def downgrade() -> None: + """移除 login_channel 字段。""" + bind = op.get_bind() + inspector = sa.inspect(bind) + if not inspector.has_table("huya_accounts"): + return + indexes = {index["name"] for index in inspector.get_indexes("huya_accounts")} + if "ix_huya_accounts_login_channel" in indexes: + op.drop_index("ix_huya_accounts_login_channel", table_name="huya_accounts") + columns = {column["name"] for column in inspector.get_columns("huya_accounts")} + if "login_channel" in columns: + op.drop_column("huya_accounts", "login_channel") diff --git a/web/backend/models.py b/web/backend/models.py index c68c624..7ad5231 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -279,6 +279,8 @@ class HuyaAccount(Base): tag = Column(String(64), default="") remark = Column(String(256), default="") status = Column(String(32), default="imported") + # 上次成功登录渠道: web=旧版Web协议 / app=App协议(推荐) / sms=短信; 空=仅导入Cookie + login_channel = Column(String(16), default="", index=True) points = Column(Integer, nullable=True) game_name = Column(String(128), default="") game_channel = Column(String(64), default="") diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index b4ff190..2446e4e 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -283,6 +283,7 @@ def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccou tag=account.tag or "", remark=account.remark or "", status=account.status or "", + login_channel=getattr(account, "login_channel", "") or "", points=account.points, game_name=account.game_name or "", game_channel=account.game_channel or "", @@ -308,6 +309,7 @@ def _account_out_light(account: HuyaAccount, *, has_password: bool = False) -> H tag=account.tag or "", remark=account.remark or "", status=account.status or "", + login_channel=getattr(account, "login_channel", "") or "", points=account.points, game_name=account.game_name or "", game_channel=account.game_channel or "", @@ -555,6 +557,7 @@ def password_login_account( if hasattr(account, "account_password") and req.password: account.account_password = req.password account.status = "active" + account.login_channel = "web" db.commit() except ValueError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc @@ -597,6 +600,7 @@ def app_password_login_account( if hasattr(account, "account_password") and req.password: account.account_password = req.password account.status = "active" + account.login_channel = "app" db.commit() except ValueError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc @@ -662,6 +666,7 @@ def sms_login_account( try: account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint="") + account.login_channel = "sms" phone = (req.phone or "").strip() if phone: account.game_phone = phone @@ -921,6 +926,7 @@ def password_login_selected_accounts( tag=account.tag or "", username_hint=username, ) + saved.login_channel = "web" success_count += 1 results.append({ "line": account.id, @@ -1053,6 +1059,7 @@ def app_password_login_selected_accounts( username_hint=username, ) saved.status = "active" + saved.login_channel = "app" db.commit() success_count += 1 results.append({ diff --git a/web/backend/schemas.py b/web/backend/schemas.py index e1e94cf..88496d4 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -409,6 +409,8 @@ class HuyaAccountOut(BaseModel): tag: str = "" remark: str = "" status: str = "" + # 上次成功登录渠道: web / app / sms (空 = 仅导入 Cookie, 从未协议登录) + login_channel: str = "" points: Optional[int] = None game_name: str = "" game_channel: str = "" diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 958e75c..969cfb5 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -451,6 +451,8 @@ export interface HuyaAccountItem { tag: string; remark: string; status: string; + /** 上次成功登录渠道: web=旧版Web协议 / app=App协议(推荐) / sms=短信; 空=仅导入 */ + login_channel?: string; points: number | null; game_name: string; game_channel: string; diff --git a/web/frontend/src/pages/HuyaAccountsPage.tsx b/web/frontend/src/pages/HuyaAccountsPage.tsx index 54f7989..c2d76cd 100644 --- a/web/frontend/src/pages/HuyaAccountsPage.tsx +++ b/web/frontend/src/pages/HuyaAccountsPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { - Alert, Button, Card, Col, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography, + Alert, Button, Card, Col, Divider, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Tooltip, Typography, } from 'antd'; import { message } from '../utils/antdMessage'; import type { TableProps } from 'antd'; @@ -30,6 +30,13 @@ const STATUS_LABELS: Record = { invalid: '失效', }; +// 登录渠道展示 (对应后端 HuyaAccount.login_channel: web/app/sms) +const LOGIN_CHANNEL_LABELS: Record = { + app: { label: 'App 协议', color: 'processing' }, + web: { label: 'Web 旧版', color: 'warning' }, + sms: { label: '短信', color: 'cyan' }, +}; + const STATUS_COLORS: Record = { imported: 'blue', updated: 'cyan', @@ -321,6 +328,29 @@ export default function HuyaAccountsPage() { } }; + // 行内单账号登录: 复用批量端点, 只传该行 id (需已导入密码) + const [rowLoginKey, setRowLoginKey] = useState(''); + const handleRowLogin = async (record: HuyaAccountItem, channel: 'app' | 'web') => { + setRowLoginKey(`${channel}-${record.id}`); + try { + const payload = { account_ids: [record.id], all_matching: false, search: '', tag: '' }; + const result = channel === 'app' + ? await huyaApi.appPasswordLoginSelected(payload) + : await huyaApi.passwordLoginSelected(payload); + setPasswordLoginResults(result.results || []); + setLoginModalTitle(channel === 'app' ? 'App 协议登录结果' : 'Web 旧版登录结果'); + setPasswordLoginResultOpen(true); + const ok = (result.results || []).some((r) => r.success); + if (ok) { message.success('登录成功,Cookie 已保存'); } else { message.warning(result.message || '登录失败'); } + loadAccounts(); + loadSummary(); + } catch (e: unknown) { + message.error(getErrorMessage(e)); + } finally { + setRowLoginKey(''); + } + }; + const handleWebLoginSelected = async () => { if (selectedCount === 0) { message.warning('请先选择虎牙账号'); @@ -550,6 +580,18 @@ export default function HuyaAccountsPage() { {STATUS_LABELS[status] || status || '-'} ), }, + { + title: '登录渠道', + dataIndex: 'login_channel', + width: 100, + align: 'center', + render: (channel: string) => { + const info = LOGIN_CHANNEL_LABELS[channel]; + return info + ? {info.label} + : 仅导入; + }, + }, { title: '更新时间', dataIndex: 'updated_at', @@ -558,15 +600,38 @@ export default function HuyaAccountsPage() { }, { title: '操作', - width: 90, + width: 150, fixed: 'right', align: 'center', render: (_: unknown, record) => ( - canDelete ? ( - handleDelete(record.id)}> - )} + {canImport && } {canImport && ( - + + + )} {canImport && ( - + + + )} + {canImport && } {canDelete && selectedCount > 0 && (