fix(accounts): restore gateway selection and use creation modal

This commit is contained in:
2026-10-06 10:28:58 +08:00
parent 58e9a73cc4
commit 94df412579
11 changed files with 353 additions and 70 deletions
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react';
import { Alert, Button, Collapse, Form, Modal, Select, Typography } from 'antd';
import { creatorCreate, creatorGet } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
import FingerprintFields, { fingerprintPayload, type FingerprintValues } from '@/components/accounts/FingerprintFields';
interface Values {
gateway: string;
fingerprint?: FingerprintValues;
}
export default function AccountCreateModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const [form] = Form.useForm<Values>();
const [gateways, setGateways] = useState<{ name: string }[]>([]);
const [loading, setLoading] = useState(true);
const [gatewayError, setGatewayError] = useState<unknown>(null);
const [gatewayAttempt, setGatewayAttempt] = useState(0);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<unknown>(null);
useEffect(() => {
let active = true;
async function load() {
setLoading(true);
setGatewayError(null);
try {
const gateways = await creatorGet('/gateways');
if (active) setGateways(gateways);
} catch (error) {
if (active) setGatewayError(error);
} finally {
if (active) setLoading(false);
}
}
void load();
return () => { active = false; };
}, [gatewayAttempt]);
async function onFinish(values: Values) {
setBusy(true);
setError(null);
try {
const environment = await creatorCreate('/creator/environments', {
gateway: values.gateway,
fingerprint: fingerprintPayload(values.fingerprint),
});
if (!environment?.alias) throw new Error('创建环境未返回环境标识');
} catch (error) {
setError(error);
return;
} finally {
setBusy(false);
}
onSuccess();
}
return (
<Modal open title="新增账号" onCancel={() => { if (!busy) onClose(); }}
onOk={() => form.submit()} confirmLoading={busy} okText="创建" cancelText="取消"
okButtonProps={{ disabled: loading || Boolean(gatewayError) || gateways.length === 0 }}
cancelButtonProps={{ disabled: busy }}
closable={!busy} keyboard={!busy} mask={{ closable: !busy }}>
{gatewayError ? <Alert type="error" showIcon title={conflictMessage(gatewayError, '网关加载失败')}
action={<Button size="small" onClick={() => setGatewayAttempt(attempt => attempt + 1)} loading={loading}>重试</Button>} /> : null}
{!loading && !gatewayError && gateways.length === 0 ? <Alert type="info" showIcon title="暂无网关,请添加网关后再创建账号。" /> : null}
{error ? <Alert type="error" showIcon title={conflictMessage(error, '创建环境失败,配置已保留。')} /> : null}
<Form form={form} layout="vertical" onFinish={onFinish} disabled={busy}>
<Typography.Paragraph type="secondary">
创建抖音浏览器环境后扫码登录。昵称、UID 和头像自动同步;登录前不创建账号记录。
</Typography.Paragraph>
<Form.Item name="gateway" label="网关" rules={[{ required: true, message: '请选择网关' }]}>
<Select loading={loading} disabled={busy || loading || Boolean(gatewayError)} placeholder="请选择网关"
options={gateways.map(gateway => ({ value: gateway.name, label: gateway.name }))} />
</Form.Item>
<Collapse items={[{ key: 'fingerprint', label: '浏览器指纹(可选)', children: <>
<Typography.Paragraph type="secondary">留空使用默认配置。登录绑定不会改变浏览器指纹。</Typography.Paragraph>
<FingerprintFields />
</> }]} />
</Form>
</Modal>
);
}
@@ -12,6 +12,7 @@ import { remove, creatorUpdate, creatorGet, creatorAction } from '@/services/api
import { accountReadiness, conflictMessage, dateTime } from '@/utils/helpers';
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
import AccountLoginModal from '@/components/accounts/AccountLoginModal';
import AccountCreateModal from '@/components/accounts/AccountCreateModal';
// 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。
interface EnvironmentView {
@@ -52,7 +53,8 @@ function formatCount(value?: number | null): string {
export default function AccountManagementList() {
const [rows, setRows] = useState<Row[]>([]);
const [environments, setEnvironments] = useState<{ alias: string; runtime_id?: string }[]>([]);
const [createOpen, setCreateOpen] = useState(false);
const [environments, setEnvironments] = useState<{ alias: string; gateway: string; runtime_id?: string }[]>([]);
const [environmentTarget, setEnvironmentTarget] = useState<string | null>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
@@ -144,7 +146,7 @@ export default function AccountManagementList() {
...fixedLeft<Row>({}),
render: (_, account) => (
<div>
<Space><Avatar size="small" src={account.avatar_url} /><Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name}</Typography.Link></Space>
<Space><Avatar size="small" src={account.avatar_url} /><Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name || '待登录'}</Typography.Link></Space>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }} ellipsis={{ tooltip: true }}>
{account.platform_account_key}
</Typography.Paragraph>
@@ -252,7 +254,7 @@ export default function AccountManagementList() {
编辑账号
</Button>
<Popconfirm
title={`确定删除自有账号“${account.name}”吗?相关专属资源也会被清理。`}
title={`确定删除自有账号“${account.name || '待登录'}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(account)}
disabled={actionBusy === account.id}
>
@@ -270,11 +272,13 @@ export default function AccountManagementList() {
<Typography.Text type="secondary">共 {rows.length} 个自有账号</Typography.Text>
<Space>
<Button icon={<ReloadOutlined />} onClick={load} disabled={pending} aria-label="刷新" />
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/accounts/new')}>
创建浏览器环境
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
新增账号
</Button>
</Space>
</Flex>
{createOpen ? <AccountCreateModal onClose={() => setCreateOpen(false)}
onSuccess={() => { setCreateOpen(false); void load(); }} /> : null}
{environmentTarget ? (
<AccountLoginModal accountID={environmentTarget} accountName="待登录环境" resource="environments"
onClose={() => setEnvironmentTarget(null)} onSuccess={() => { setEnvironmentTarget(null); void load(); }} />
@@ -282,6 +286,8 @@ export default function AccountManagementList() {
{environments.length > 0 ? (
<Card title="待登录环境">
<Table rowKey="alias" dataSource={environments} pagination={tablePagination()} sticky columns={[
{ title: '账号', render: () => <Typography.Text>待登录</Typography.Text> },
{ title: '网关', dataIndex: 'gateway' },
{ title: '环境标识', dataIndex: 'alias', render: (alias: string) => <Typography.Text copyable>{alias}</Typography.Text> },
{ title: '状态', render: () => <Tag>待登录</Tag> },
{ title: '操作', render: (_, env) => <Button type="link" onClick={() => setEnvironmentTarget(env.alias)}>登录并同步账号</Button> },
@@ -303,7 +309,7 @@ export default function AccountManagementList() {
/>
<Modal
open={Boolean(tagTarget)}
title={`编辑标签 · ${tagTarget?.name || '账号'}`}
title={`编辑标签 · ${tagTarget?.name || '待登录'}`}
onCancel={() => {
if (!tagBusy) setTagTarget(null);
}}
@@ -318,7 +324,7 @@ export default function AccountManagementList() {
</Form>
</Modal>
{loginTarget ? (
<AccountLoginModal accountID={loginTarget.id} accountName={loginTarget.name}
<AccountLoginModal accountID={loginTarget.id} accountName={loginTarget.name || '待登录'}
onClose={() => setLoginTarget(null)} onSuccess={() => { void load(); }} />
) : null}
</Card>
@@ -1,5 +1,5 @@
// 指纹浏览器环境表单字段 + 提交载荷构建:创建页(new.tsx)与编辑页(edit.tsx)共用。
// 留空项不提交,后端使用浏览器默认值;seed 由系统按账号派生并保证唯一,代理由网络出口统一管理,均不在表单内。
// 指纹浏览器环境表单字段 + 提交载荷构建:创建弹窗与账号编辑页共用。
// 留空项不提交,后端使用浏览器默认值;seed 由环境独立序列分配,代理由网络出口统一管理,均不在表单内。
// 字段契约来源:internal/environment/fingerprint.go(fingerprint-chromium 启动参数)。
import { Col, Form, Input, InputNumber, Row, Select } from 'antd';