fix(accounts): restore gateway selection and use creation modal
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)回落到父级菜单项。
|
||||
|
||||
Reference in New Issue
Block a user