虎牙支持先导入账号密码再登录

This commit is contained in:
yml2213
2026-07-05 20:47:16 +08:00
parent 552eb9fa82
commit 2203420146
9 changed files with 493 additions and 73 deletions
+7
View File
@@ -5,8 +5,11 @@ import type {
HuyaCookieItem,
HuyaCookieImportResult,
HuyaGoodsItem,
HuyaPasswordAccountImportResult,
HuyaPasswordLoginBatchResult,
HuyaPasswordLoginRequest,
HuyaPasswordLoginResult,
HuyaPasswordLoginSelectedRequest,
HuyaRechargeGoodsItem,
HuyaTaskBatchRequest,
HuyaTaskBatchResult,
@@ -22,8 +25,12 @@ export const huyaApi = {
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
importCookies: (text: string, tag: string = '') =>
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
importPasswordAccounts: (text: string, tag: string = '') =>
api.post<HuyaPasswordAccountImportResult, HuyaPasswordAccountImportResult>('/huya/accounts/import-passwords', { text, tag }),
passwordLogin: (data: HuyaPasswordLoginRequest) =>
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
passwordLoginSelected: (data: HuyaPasswordLoginSelectedRequest) =>
api.post<HuyaPasswordLoginBatchResult, HuyaPasswordLoginBatchResult>('/huya/accounts/password-login/selected', data),
assign: (id: number, assigned_to: number | null) =>
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
batchAssign: (account_ids: number[], assigned_to: number | null) =>
+23
View File
@@ -116,6 +116,7 @@ export interface HuyaAccountItem {
uid: string;
yyuid: string;
username: string;
has_password: boolean;
nickname: string;
cookie: string;
cookie_preview: string;
@@ -148,6 +149,28 @@ export interface HuyaPasswordLoginResult extends MessageResponse {
sdid: string;
}
export interface HuyaPasswordLoginBatchItem {
line: number;
username: string;
success: boolean;
message: string;
account?: HuyaAccountItem;
sdid?: string;
}
export interface HuyaPasswordLoginBatchResult extends MessageCountResponse {
failed: number;
results: HuyaPasswordLoginBatchItem[];
}
export interface HuyaPasswordAccountImportResult extends MessageCountResponse {
skipped: number;
}
export interface HuyaPasswordLoginSelectedRequest {
account_ids: number[];
}
export interface HuyaCookieItem {
id: number;
account_id: number;
+154 -72
View File
@@ -1,10 +1,15 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Button, Card, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
Button, Card, Col, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
} from 'antd';
import type { TableProps } from 'antd';
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
import { huyaApi, type HuyaAccountItem, type SupportUserItem } from '../api/modules';
import {
huyaApi,
type HuyaAccountItem,
type HuyaPasswordLoginBatchItem,
type SupportUserItem,
} from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -15,7 +20,9 @@ const { TextArea } = Input;
const STATUS_LABELS: Record<string, string> = {
imported: '已导入',
updated: '已更新',
password_imported: '待登录',
login_success: '登录成功',
login_failed: '登录失败',
active: '正常',
invalid: '失效',
};
@@ -23,18 +30,13 @@ const STATUS_LABELS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
imported: 'blue',
updated: 'cyan',
password_imported: 'warning',
login_success: 'success',
login_failed: 'error',
active: 'success',
invalid: 'error',
};
interface PasswordLoginFormValues {
username: string;
password: string;
tag?: string;
cookie?: string;
}
export default function HuyaAccountsPage() {
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
const [users, setUsers] = useState<SupportUserItem[]>([]);
@@ -44,8 +46,13 @@ export default function HuyaAccountsPage() {
const [importText, setImportText] = useState('');
const [importTag, setImportTag] = useState('');
const [importing, setImporting] = useState(false);
const [passwordLoginOpen, setPasswordLoginOpen] = useState(false);
const [passwordImportOpen, setPasswordImportOpen] = useState(false);
const [passwordImportText, setPasswordImportText] = useState('');
const [passwordImportTag, setPasswordImportTag] = useState<string[]>([]);
const [passwordImporting, setPasswordImporting] = useState(false);
const [passwordLoginResultOpen, setPasswordLoginResultOpen] = useState(false);
const [passwordLogging, setPasswordLogging] = useState(false);
const [passwordLoginResults, setPasswordLoginResults] = useState<HuyaPasswordLoginBatchItem[]>([]);
const [searchText, setSearchText] = useState('');
const [tagFilter, setTagFilter] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
@@ -56,7 +63,6 @@ export default function HuyaAccountsPage() {
return v ? Number(v) || 20 : 20;
});
const [currentPage, setCurrentPage] = useState(1);
const [passwordLoginForm] = Form.useForm<PasswordLoginFormValues>();
const { can } = usePermissions();
const canManage = can('huya:account');
@@ -146,29 +152,52 @@ export default function HuyaAccountsPage() {
}
};
const openPasswordLogin = () => {
passwordLoginForm.resetFields();
setPasswordLoginOpen(true);
const openPasswordImport = () => {
setPasswordImportText('');
setPasswordImportTag([]);
setPasswordLoginResults([]);
setPasswordImportOpen(true);
};
const closePasswordLogin = () => {
if (passwordLogging) return;
setPasswordLoginOpen(false);
passwordLoginForm.resetFields();
const handleImportPasswordAccounts = async () => {
if (!passwordImportText.trim()) {
message.warning('请先粘贴虎牙账号密码');
return;
}
setPasswordImporting(true);
try {
const tag = passwordImportTag.length > 0 ? passwordImportTag[passwordImportTag.length - 1].trim() : '';
const result = await huyaApi.importPasswordAccounts(passwordImportText, tag);
message.success(result.message);
setPasswordImportOpen(false);
setPasswordImportText('');
setPasswordImportTag([]);
loadAccounts();
loadTags();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setPasswordImporting(false);
}
};
const handlePasswordLogin = async (values: PasswordLoginFormValues) => {
const handleLoginSelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择虎牙账号');
return;
}
setPasswordLogging(true);
try {
const result = await huyaApi.passwordLogin({
username: values.username.trim(),
password: values.password,
tag: values.tag?.trim() || '',
cookie: values.cookie?.trim() || '',
const result = await huyaApi.passwordLoginSelected({
account_ids: selectedRowKeys.map((key) => Number(key)),
});
message.success(result.message || '登录成功,Cookie 已保存');
setPasswordLoginOpen(false);
passwordLoginForm.resetFields();
setPasswordLoginResults(result.results || []);
setPasswordLoginResultOpen(true);
if (result.failed > 0) {
message.warning(result.message);
} else {
message.success(result.message || '登录成功,Cookie 已保存');
}
loadAccounts();
loadTags();
} catch (e: unknown) {
@@ -192,7 +221,7 @@ export default function HuyaAccountsPage() {
const handleDeleteSelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择虎牙 CK');
message.warning('请先选择虎牙账号');
return;
}
try {
@@ -248,6 +277,28 @@ export default function HuyaAccountsPage() {
const boundCount = accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length;
const pointCount = accounts.filter((item) => item.points !== null && item.points !== undefined).length;
const assignedCount = accounts.filter((item) => item.assigned_to).length;
const passwordReadyCount = accounts.filter((item) => item.has_password).length;
const passwordLoginResultColumns: TableProps<HuyaPasswordLoginBatchItem>['columns'] = [
{ title: '账号ID', dataIndex: 'line', width: 80, align: 'center' },
{ title: '账号', dataIndex: 'username', width: 160, ellipsis: true },
{
title: '状态',
dataIndex: 'success',
width: 90,
align: 'center',
render: (success: boolean) => (
<Tag color={success ? 'success' : 'error'}>{success ? '成功' : '失败'}</Tag>
),
},
{
title: '保存账号',
width: 160,
ellipsis: true,
render: (_: unknown, record) => record.account?.nickname || record.account?.username || record.account?.uid || '-',
},
{ title: '提示', dataIndex: 'message', ellipsis: true },
];
const columns: TableProps<HuyaAccountItem>['columns'] = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
@@ -260,6 +311,7 @@ export default function HuyaAccountsPage() {
<Text type="secondary" style={{ fontSize: 12 }}>
UID {record.uid || record.yyuid || '-'}
</Text>
{record.has_password ? <Tag color="gold"></Tag> : null}
</Space>
),
},
@@ -422,16 +474,26 @@ export default function HuyaAccountsPage() {
</Button>
)}
{canImport && (
<Button
disabled={selectedRowKeys.length === 0}
icon={<LoginOutlined />}
loading={passwordLogging}
onClick={handleLoginSelected}
>
</Button>
)}
{canDelete && selectedRowKeys.length > 0 && (
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙 CK`} onConfirm={handleDeleteSelected}>
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙账号`} onConfirm={handleDeleteSelected}>
<Button danger icon={<DeleteOutlined />}>
({selectedRowKeys.length})
</Button>
</Popconfirm>
)}
{canImport && (
<Button icon={<LoginOutlined />} onClick={openPasswordLogin}>
<Button icon={<ImportOutlined />} onClick={openPasswordImport}>
</Button>
)}
{canImport && (
@@ -449,6 +511,9 @@ export default function HuyaAccountsPage() {
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已导入密码" value={passwordReadyCount} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
</Col>
@@ -553,48 +618,65 @@ export default function HuyaAccountsPage() {
</Modal>
<Modal
title="虎牙密码登录"
open={passwordLoginOpen}
onCancel={closePasswordLogin}
onOk={() => passwordLoginForm.submit()}
okText="登录并保存"
cancelButtonProps={{ disabled: passwordLogging }}
confirmLoading={passwordLogging}
maskClosable={!passwordLogging}
closable={!passwordLogging}
width={560}
title="导入虎牙账号密码"
open={passwordImportOpen}
onCancel={() => {
if (passwordImporting) return;
setPasswordImportOpen(false);
}}
onOk={handleImportPasswordAccounts}
okText="导入"
cancelButtonProps={{ disabled: passwordImporting }}
confirmLoading={passwordImporting}
maskClosable={!passwordImporting}
closable={!passwordImporting}
width={720}
>
<Form
form={passwordLoginForm}
layout="vertical"
requiredMark={false}
onFinish={handlePasswordLogin}
disabled={passwordLogging}
>
<Form.Item
name="username"
label="账号"
rules={[{ required: true, message: '请输入虎牙账号' }]}
>
<Input autoComplete="username" placeholder="手机号、邮箱或虎牙账号" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password autoComplete="current-password" placeholder="虎牙登录密码" />
</Form.Item>
<Form.Item name="tag" label="标签">
<Input placeholder="可选,保存到账号标签" />
</Form.Item>
<Form.Item name="cookie" label="预置 Cookie">
<TextArea
rows={3}
placeholder="可选;若已有 hdid/sdid 等风控 Cookie,可粘贴在这里"
/>
</Form.Item>
</Form>
<Space direction="vertical" style={{ width: '100%' }} size={12}>
<TextArea
rows={10}
value={passwordImportText}
onChange={(e) => setPasswordImportText(e.target.value)}
placeholder="每行一条:账号----密码;如需预置 Cookie:账号----密码----Cookie"
disabled={passwordImporting}
/>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder="可选,保存到账号标签"
maxCount={1}
value={passwordImportTag}
onChange={setPasswordImportTag}
options={tags.map((tag) => ({ value: tag, label: tag }))}
disabled={passwordImporting}
/>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
</Paragraph>
</Space>
</Modal>
<Modal
title="虎牙登录结果"
open={passwordLoginResultOpen}
onCancel={() => setPasswordLoginResultOpen(false)}
footer={[
<Button key="ok" type="primary" onClick={() => setPasswordLoginResultOpen(false)}>
</Button>,
]}
width={860}
>
{passwordLoginResults.length > 0 && (
<Table
columns={passwordLoginResultColumns}
dataSource={passwordLoginResults}
rowKey={(record) => `${record.line}-${record.username || 'empty'}`}
size="small"
pagination={false}
scroll={{ x: 720, y: 260 }}
/>
)}
</Modal>
</div>
);