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';
-45
View File
@@ -1,45 +0,0 @@
import { useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Collapse, Flex, Form, Typography } from 'antd';
import { creatorCreate } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
import FingerprintFields, { fingerprintPayload, type FingerprintValues } from '@/components/accounts/FingerprintFields';
export default function Page() {
const [form] = Form.useForm<{ fingerprint?: FingerprintValues }>();
const [busy, setBusy] = useState(false);
const [error, setError] = useState<any>(null);
async function onFinish(values: { fingerprint?: FingerprintValues }) {
setBusy(true);
setError(null);
try {
const environment = await creatorCreate('/creator/environments', { fingerprint: fingerprintPayload(values.fingerprint) });
if (!environment?.alias) throw new Error('创建环境未返回环境标识');
history.replace('/accounts');
} catch (error) {
setError(error);
} finally {
setBusy(false);
}
}
return (
<Card variant="outlined">
{error ? <Alert type="error" showIcon title={conflictMessage(error, '创建环境失败,配置已保留。')} /> : null}
<Form form={form} layout="vertical" onFinish={onFinish} style={{ maxWidth: 720 }}>
<Typography.Paragraph type="secondary">
创建抖音浏览器环境后扫码登录。昵称、UID、头像和抖音号从登录结果同步,无需填写;登录成功前不会创建账号。
</Typography.Paragraph>
<Collapse style={{ marginBottom: 24 }} items={[{
key: 'fingerprint', label: '浏览器配置(可选)', children: <>
<Typography.Paragraph type="secondary">留空使用浏览器默认值。指纹标识由系统生成,登录绑定不会改变浏览器配置。</Typography.Paragraph>
<FingerprintFields />
</>,
}]} />
<Form.Item><Flex gap={8}>
<Button onClick={() => history.push('/accounts')}>取消</Button>
<Button type="primary" htmlType="submit" loading={busy}>创建浏览器环境</Button>
</Flex></Form.Item>
</Form>
</Card>
);
}
+1 -2
View File
@@ -19,7 +19,6 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
{ test: (p) => p === '/accounts', title: '我的账号', subtitle: '创建和管理自己维护的账号。' },
{ test: (p) => p === '/accounts/monitoring', title: '监控账号', subtitle: '管理需要持续跟踪的竞品账号及采集状态。' },
{ test: (p) => /^\/accounts\/monitoring\/.+/.test(p), title: '监控账号详情', subtitle: '查看监控账号画像与作品列表,点击作品可追溯指标统计。' },
{ test: (p) => p === '/accounts/new', title: '创建浏览器环境', subtitle: '登录成功后自动同步抖音昵称、UID、头像和抖音号。' },
{ test: (p) => /\/edit$/.test(p) && p.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
{ test: (p) => p.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
{ test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
@@ -75,4 +74,4 @@ export function menuSelectedKey(pathname: string): string {
// 侧栏选中态:ProLayout 默认按路径前缀高亮(/accounts 是 /accounts/import 的前缀,两项会同时高亮)。
// 这里改为受控 selectedKeys:从完整路径逐段向前回退,仅精确命中菜单叶路径;
// 详情页(如 /accounts/123、/accounts/new)回落到父级菜单项。
// 详情页(如 /accounts/123)回落到父级菜单项。