- 资料表单三行分组:登录用户名/更新密码/业务状态;实名状态/实名姓名/实名证件号;账号备注独占一行 - 新增 FingerprintFields 共享组件(字段+载荷构建+回显转换),创建页同步复用 - 编辑页指纹独立卡片单独保存(PUT fingerprint),运行中环境保存后自动重启生效 - 修复暗病:保存资料不再发送 Number(undefined)=NaN 的 cooldown_seconds,改透传当前值
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// 指纹浏览器环境表单字段 + 提交载荷构建:创建页(new.tsx)与编辑页(edit.tsx)共用。
|
||||
// 留空项不提交,后端使用浏览器默认值;seed 由系统按账号派生并保证唯一,代理由网络出口统一管理,均不在表单内。
|
||||
// 字段契约来源:internal/environment/fingerprint.go(fingerprint-chromium 启动参数)。
|
||||
import { Col, Form, Input, InputNumber, Row, Select } from 'antd';
|
||||
|
||||
export interface FingerprintValues {
|
||||
platform?: string;
|
||||
platform_version?: string;
|
||||
brand?: string;
|
||||
brand_version?: string;
|
||||
hardware_concurrency?: number;
|
||||
lang?: string;
|
||||
accept_lang?: string;
|
||||
timezone?: string;
|
||||
disable_spoofing?: string[];
|
||||
}
|
||||
|
||||
export const spoofingOptions = [
|
||||
{ value: 'font', label: '字体' },
|
||||
{ value: 'audio', label: '音频' },
|
||||
{ value: 'canvas', label: 'Canvas' },
|
||||
{ value: 'clientrects', label: 'ClientRects' },
|
||||
{ value: 'gpu', label: 'GPU' },
|
||||
];
|
||||
|
||||
// 提交方向:只提交非空项;disable_spoofing 多选合并为逗号分隔字符串(后端契约)。
|
||||
export function fingerprintPayload(fp?: FingerprintValues): Record<string, unknown> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (!fp) return payload;
|
||||
if (fp.platform) payload.platform = fp.platform;
|
||||
if (fp.platform_version?.trim()) payload.platform_version = fp.platform_version.trim();
|
||||
if (fp.brand) payload.brand = fp.brand;
|
||||
if (fp.brand_version?.trim()) payload.brand_version = fp.brand_version.trim();
|
||||
if (fp.hardware_concurrency) payload.hardware_concurrency = fp.hardware_concurrency;
|
||||
if (fp.lang?.trim()) payload.lang = fp.lang.trim();
|
||||
if (fp.accept_lang?.trim()) payload.accept_lang = fp.accept_lang.trim();
|
||||
if (fp.timezone?.trim()) payload.timezone = fp.timezone.trim();
|
||||
if (fp.disable_spoofing?.length) payload.disable_spoofing = fp.disable_spoofing.join(',');
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 回显方向:后端存储的指纹(disable_spoofing 为逗号分隔字符串)转表单值。
|
||||
export function fingerprintFormValues(fp?: FingerprintValues | null) {
|
||||
return {
|
||||
fingerprint: {
|
||||
platform: fp?.platform || undefined,
|
||||
platform_version: fp?.platform_version || '',
|
||||
brand: fp?.brand || undefined,
|
||||
brand_version: fp?.brand_version || '',
|
||||
hardware_concurrency: fp?.hardware_concurrency || undefined,
|
||||
lang: fp?.lang || '',
|
||||
accept_lang: fp?.accept_lang || '',
|
||||
timezone: fp?.timezone || '',
|
||||
disable_spoofing: fp?.disable_spoofing ? String(fp.disable_spoofing).split(',') : [],
|
||||
} satisfies FingerprintValues,
|
||||
};
|
||||
}
|
||||
|
||||
export default function FingerprintFields() {
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'platform']} label="平台伪装" extra="浏览器上报的操作系统">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="默认"
|
||||
options={[
|
||||
{ value: 'windows', label: 'Windows' },
|
||||
{ value: 'linux', label: 'Linux' },
|
||||
{ value: 'macos', label: 'macOS' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'brand']} label="浏览器品牌" extra="UA 中的浏览器品牌">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="默认"
|
||||
options={[
|
||||
{ value: 'Chrome', label: 'Chrome' },
|
||||
{ value: 'Edge', label: 'Edge' },
|
||||
{ value: 'Opera', label: 'Opera' },
|
||||
{ value: 'Vivaldi', label: 'Vivaldi' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'platform_version']} label="平台版本" extra="如 10.0.0">
|
||||
<Input maxLength={32} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'brand_version']} label="品牌版本" extra="如 132.0.6834.159">
|
||||
<Input maxLength={32} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'lang']} label="语言" extra="如 zh-CN">
|
||||
<Input maxLength={16} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'accept_lang']} label="接受语言" extra="逗号分隔,如 zh-CN,en-US">
|
||||
<Input maxLength={256} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'timezone']} label="时区" extra="IANA 时区,如 Asia/Shanghai">
|
||||
<Input maxLength={64} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'hardware_concurrency']} label="硬件并发数" extra="CPU 逻辑核数,1-128">
|
||||
<InputNumber min={1} max={128} precision={0} style={{ width: '100%' }} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name={['fingerprint', 'disable_spoofing']}
|
||||
label="关闭伪装维度"
|
||||
extra="关闭指定维度的指纹伪装,保持浏览器真实特征"
|
||||
>
|
||||
<Select mode="multiple" allowClear placeholder="默认全部伪装" options={spoofingOptions} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
// 编辑社媒账号:账号资料维护 + 登录二维码/身份核验。
|
||||
// 布局对齐全站模式:实体标识经 usePageInfo 进页头 content 左侧,返回/登录操作按钮经 usePageActions 进页头右侧;
|
||||
// 主体两张 Card 纵向堆叠(登录状态 / 账号资料),不再自绘标题行、不再裸铺背景。
|
||||
// 编辑社媒账号:账号资料维护 + 指纹浏览器环境(单独保存) + 登录二维码/身份核验。
|
||||
// 资料表单三行分组:登录用户名/更新密码/业务状态;实名状态/实名姓名/实名证件号;账号备注。
|
||||
// 指纹字段复用 FingerprintFields 共享组件;保存后运行中的环境由后端停后以新指纹重启,未运行仅落库下次启动生效。
|
||||
// 布局对齐全站模式:实体标识经 usePageInfo 进页头 content 左侧,返回/登录操作按钮经 usePageActions 进页头右侧。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Alert, App, Button, Card, Descriptions, Divider, Flex, Form, Input, Select, Space, Typography } from 'antd';
|
||||
import { Alert, App, Button, Card, Col, Descriptions, Divider, Flex, Form, Input, Row, Select, Space, Typography } from 'antd';
|
||||
import { usePageActions, usePageInfo } from '@/components/PageActions';
|
||||
import { creatorAction, creatorGet, creatorUpdate, getList } from '@/services/api';
|
||||
import FingerprintFields, { fingerprintFormValues, fingerprintPayload } from '@/components/accounts/FingerprintFields';
|
||||
import { creatorAction, creatorGet, creatorUpdate, getOne, getList } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
|
||||
const platformLabelMap: Record<string, string> = { douyin: '抖音' };
|
||||
|
||||
const realNameStatusOptions = [
|
||||
{ value: 'unknown', label: '未知' },
|
||||
{ value: 'not_real_name', label: '未实名' },
|
||||
{ value: 'recorded', label: '已记录' },
|
||||
];
|
||||
|
||||
const businessStatusOptions = [
|
||||
{ value: 'normal', label: '正常' },
|
||||
{ value: 'muted', label: '禁言' },
|
||||
{ value: 'banned', label: '封禁' },
|
||||
{ value: 'deleted', label: '注销' },
|
||||
];
|
||||
|
||||
function profileForm(profile: any) {
|
||||
return {
|
||||
login_username: profile.login_username || '',
|
||||
@@ -27,8 +42,10 @@ export default function Page() {
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [fpBusy, setFpBusy] = useState(false);
|
||||
const [loginQR, setLoginQR] = useState<any>(null);
|
||||
const [form] = Form.useForm<any>();
|
||||
const [fpForm] = Form.useForm<any>();
|
||||
const { message: messageApi } = App.useApp();
|
||||
|
||||
const selected = profiles.find((item) => item.id === selectedID);
|
||||
@@ -66,20 +83,25 @@ export default function Page() {
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await getList({ resource: 'creator-accounts' });
|
||||
setProfiles(result.data);
|
||||
const hit = result.data.find((item: any) => item.id === selectedID);
|
||||
const [listResult, detail] = await Promise.all([
|
||||
getList({ resource: 'creator-accounts' }),
|
||||
getOne('accounts', selectedID),
|
||||
]);
|
||||
setProfiles(listResult.data);
|
||||
const hit = listResult.data.find((item: any) => item.id === selectedID);
|
||||
if (hit) {
|
||||
form.setFieldsValue(profileForm(hit));
|
||||
} else {
|
||||
setError(new Error('账号不存在'));
|
||||
}
|
||||
// 指纹浏览器环境:账号详情内联 environment.fingerprint 回显(未绑定环境时保持空表单)。
|
||||
fpForm.setFieldsValue(fingerprintFormValues(detail?.environment?.fingerprint));
|
||||
} catch (loadError) {
|
||||
setError(loadError);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [selectedID, form]);
|
||||
}, [selectedID, form, fpForm]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -91,7 +113,8 @@ export default function Page() {
|
||||
try {
|
||||
const result = await creatorUpdate(`/creator/accounts/${encodeURIComponent(selected.id)}/profile`, {
|
||||
...values,
|
||||
cooldown_seconds: Number(values.cooldown_seconds) });
|
||||
// 编辑页不提供冷却时间编辑入口:原样保留当前值,避免误改采集调度节奏。
|
||||
cooldown_seconds: selected.cooldown_seconds });
|
||||
setProfiles((items) => items.map((item) => (item.id === selected.id ? result : item)));
|
||||
form.setFieldsValue(profileForm(result));
|
||||
messageApi.success('账号资料已保存。');
|
||||
@@ -102,6 +125,31 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFingerprint() {
|
||||
if (!selected) return;
|
||||
try {
|
||||
await fpForm.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
setFpBusy(true);
|
||||
try {
|
||||
const payload = fingerprintPayload(fpForm.getFieldValue('fingerprint'));
|
||||
const result = await creatorUpdate(
|
||||
`/creator/accounts/${encodeURIComponent(selected.id)}/fingerprint`,
|
||||
payload,
|
||||
);
|
||||
fpForm.setFieldsValue(fingerprintFormValues(result?.fingerprint));
|
||||
messageApi.success(
|
||||
result?.runtime_id ? '指纹已保存,运行环境已用新配置重启。' : '指纹已保存,将在环境下次启动时生效。',
|
||||
);
|
||||
} catch (fpError) {
|
||||
messageApi.error(conflictMessage(fpError, '指纹保存失败;请检查指纹参数取值后重试。'));
|
||||
} finally {
|
||||
setFpBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestLoginQR() {
|
||||
if (!selected || selected.platform !== 'douyin') return;
|
||||
setBusy(true);
|
||||
@@ -182,41 +230,40 @@ export default function Page() {
|
||||
</Card>
|
||||
<Card title="账号资料">
|
||||
<Form form={form} layout="vertical" onFinish={save} initialValues={profileForm(selected)}>
|
||||
<Space size={16} wrap style={{ display: 'flex' }}>
|
||||
<Form.Item name="login_username" label="登录用户名" extra="密码只会进入凭据存储,不会回传到页面。">
|
||||
<Input autoComplete="username" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="更新密码" extra="留空表示保持现有凭据。">
|
||||
<Input.Password autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Form.Item name="real_name_status" label="实名状态">
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'unknown', label: '未知' },
|
||||
{ value: 'not_real_name', label: '未实名' },
|
||||
{ value: 'recorded', label: '已记录' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="business_status" label="业务状态">
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: 'normal', label: '正常' },
|
||||
{ value: 'muted', label: '禁言' },
|
||||
{ value: 'banned', label: '封禁' },
|
||||
{ value: 'deleted', label: '注销' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="real_name" label="实名姓名">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="identity_number" label="实名证件号">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="login_username" label="登录用户名" extra="密码只会进入凭据存储,不会回传到页面。">
|
||||
<Input autoComplete="username" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="password" label="更新密码" extra="留空表示保持现有凭据。">
|
||||
<Input.Password autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="business_status" label="业务状态">
|
||||
<Select options={businessStatusOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="real_name_status" label="实名状态">
|
||||
<Select options={realNameStatusOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="real_name" label="实名姓名">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="identity_number" label="实名证件号">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="note" label="账号备注">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
@@ -228,6 +275,21 @@ export default function Page() {
|
||||
</Flex>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card
|
||||
title="指纹浏览器环境"
|
||||
extra={
|
||||
<Button loading={fpBusy} disabled={!selected} onClick={saveFingerprint}>
|
||||
保存指纹
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
|
||||
留空项使用浏览器默认值;seed 由系统按账号自动派生并保证唯一,代理由网络出口统一管理,均不在此配置。运行中的环境保存后会以新指纹自动重启。
|
||||
</Typography.Text>
|
||||
<Form form={fpForm} layout="vertical" initialValues={fingerprintFormValues()}>
|
||||
<FingerprintFields />
|
||||
</Form>
|
||||
</Card>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
// 创建社媒账号:语义对齐 web.archived AccountCreatePage + AccountCreateForm。
|
||||
// cookies 非必填:留空代表创建后走扫码登录。创建成功跳编辑页。
|
||||
// 折叠区块:指纹浏览器环境(可选);seed 由后端从账号派生,代理由网络出口管理,均不在表单内。
|
||||
// 折叠区块:指纹浏览器环境(可选,字段复用 FingerprintFields);seed 由后端从账号派生,代理由网络出口管理,均不在表单内。
|
||||
import { useState } from 'react';
|
||||
import { history } from '@umijs/max';
|
||||
import { Alert, Button, Card, Col, Collapse, Flex, Form, Input, InputNumber, Row, Select, Typography } from 'antd';
|
||||
import { Alert, Button, Card, Collapse, Flex, Form, Input, Select, Typography } from 'antd';
|
||||
import { create } from '@/services/api';
|
||||
import { conflictMessage, platforms } from '@/utils/helpers';
|
||||
|
||||
interface FingerprintValues {
|
||||
platform?: string;
|
||||
platform_version?: string;
|
||||
brand?: string;
|
||||
brand_version?: string;
|
||||
hardware_concurrency?: number;
|
||||
lang?: string;
|
||||
accept_lang?: string;
|
||||
timezone?: string;
|
||||
disable_spoofing?: string[];
|
||||
}
|
||||
import FingerprintFields, { fingerprintPayload, type FingerprintValues } from '@/components/accounts/FingerprintFields';
|
||||
|
||||
interface FormValues {
|
||||
name: string;
|
||||
@@ -28,14 +17,6 @@ interface FormValues {
|
||||
fingerprint?: FingerprintValues;
|
||||
}
|
||||
|
||||
const spoofingOptions = [
|
||||
{ value: 'font', label: '字体' },
|
||||
{ value: 'audio', label: '音频' },
|
||||
{ value: 'canvas', label: 'Canvas' },
|
||||
{ value: 'clientrects', label: 'ClientRects' },
|
||||
{ value: 'gpu', label: 'GPU' },
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -54,20 +35,8 @@ export default function Page() {
|
||||
// cookies 非必填:留空代表创建后走扫码登录,凭据由后续同步链路补齐
|
||||
if (values.cookies?.trim()) data.cookies = values.cookies.trim();
|
||||
// 指纹表单:只提交非空项;留空项由后端使用浏览器默认值
|
||||
const fp = values.fingerprint;
|
||||
if (fp) {
|
||||
const fingerprint: Record<string, unknown> = {};
|
||||
if (fp.platform) fingerprint.platform = fp.platform;
|
||||
if (fp.platform_version?.trim()) fingerprint.platform_version = fp.platform_version.trim();
|
||||
if (fp.brand) fingerprint.brand = fp.brand;
|
||||
if (fp.brand_version?.trim()) fingerprint.brand_version = fp.brand_version.trim();
|
||||
if (fp.hardware_concurrency) fingerprint.hardware_concurrency = fp.hardware_concurrency;
|
||||
if (fp.lang?.trim()) fingerprint.lang = fp.lang.trim();
|
||||
if (fp.accept_lang?.trim()) fingerprint.accept_lang = fp.accept_lang.trim();
|
||||
if (fp.timezone?.trim()) fingerprint.timezone = fp.timezone.trim();
|
||||
if (fp.disable_spoofing?.length) fingerprint.disable_spoofing = fp.disable_spoofing.join(',');
|
||||
if (Object.keys(fingerprint).length) data.fingerprint = fingerprint;
|
||||
}
|
||||
const fingerprint = fingerprintPayload(values.fingerprint);
|
||||
if (Object.keys(fingerprint).length) data.fingerprint = fingerprint;
|
||||
const result = await create('accounts', data);
|
||||
const id = result?.data?.id ?? result?.id;
|
||||
if (!id) throw new Error('创建账号未返回账号 ID');
|
||||
@@ -115,72 +84,7 @@ export default function Page() {
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
|
||||
留空项使用浏览器默认值;seed 由系统按账号自动派生并保证唯一,代理由网络出口统一管理,均不在此配置。
|
||||
</Typography.Text>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'platform']} label="平台伪装" extra="浏览器上报的操作系统">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="默认"
|
||||
options={[
|
||||
{ value: 'windows', label: 'Windows' },
|
||||
{ value: 'linux', label: 'Linux' },
|
||||
{ value: 'macos', label: 'macOS' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'brand']} label="浏览器品牌" extra="UA 中的浏览器品牌">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="默认"
|
||||
options={[
|
||||
{ value: 'Chrome', label: 'Chrome' },
|
||||
{ value: 'Edge', label: 'Edge' },
|
||||
{ value: 'Opera', label: 'Opera' },
|
||||
{ value: 'Vivaldi', label: 'Vivaldi' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'platform_version']} label="平台版本" extra="如 10.0.0">
|
||||
<Input maxLength={32} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'brand_version']} label="品牌版本" extra="如 132.0.6834.159">
|
||||
<Input maxLength={32} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'lang']} label="语言" extra="如 zh-CN">
|
||||
<Input maxLength={16} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'accept_lang']} label="接受语言" extra="逗号分隔,如 zh-CN,en-US">
|
||||
<Input maxLength={256} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'timezone']} label="时区" extra="IANA 时区,如 Asia/Shanghai">
|
||||
<Input maxLength={64} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={['fingerprint', 'hardware_concurrency']} label="硬件并发数" extra="CPU 逻辑核数,1-128">
|
||||
<InputNumber min={1} max={128} precision={0} style={{ width: '100%' }} placeholder="默认" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name={['fingerprint', 'disable_spoofing']}
|
||||
label="关闭伪装维度"
|
||||
extra="关闭指定维度的指纹伪装,保持浏览器真实特征"
|
||||
>
|
||||
<Select mode="multiple" allowClear placeholder="默认全部伪装" options={spoofingOptions} />
|
||||
</Form.Item>
|
||||
<FingerprintFields />
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { readFileSync } = require('node:fs');
|
||||
const { test } = require('node:test');
|
||||
const ts = require('typescript');
|
||||
|
||||
function loadSource(relativePath) {
|
||||
return ts.createSourceFile(
|
||||
relativePath,
|
||||
readFileSync(`${__dirname}/../src/${relativePath}`, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
).getFullText();
|
||||
}
|
||||
|
||||
const editPage = loadSource('pages/accounts/$id/edit.tsx');
|
||||
const fingerprintFields = loadSource('components/accounts/FingerprintFields.tsx');
|
||||
const createPage = loadSource('pages/accounts/new.tsx');
|
||||
|
||||
test('账号资料表单按三行分组:登录用户名/更新密码/业务状态、实名三项、账号备注', () => {
|
||||
// 第一行:登录用户名、更新密码、业务状态在同一个 Row 中按序出现
|
||||
const firstRow = editPage.indexOf('<Row gutter={16}>');
|
||||
const businessStatus = editPage.indexOf('name="business_status"');
|
||||
const secondRow = editPage.indexOf('<Row gutter={16}>', firstRow + 1);
|
||||
assert.ok(firstRow > -1 && businessStatus > firstRow && businessStatus < secondRow, '登录用户名/更新密码/业务状态应同属第一行');
|
||||
assert.ok(editPage.includes('name="login_username"') && editPage.includes('name="password"'), '第一行应包含登录用户名与更新密码');
|
||||
|
||||
// 第二行:实名状态、实名姓名、实名证件号
|
||||
const noteItem = editPage.indexOf('name="note"');
|
||||
assert.ok(secondRow > firstRow && secondRow < noteItem, '实名三项应与第一行分行');
|
||||
assert.ok(
|
||||
editPage.includes('name="real_name_status"') && editPage.includes('name="real_name"') && editPage.includes('name="identity_number"'),
|
||||
'第二行应包含实名状态、实名姓名、实名证件号',
|
||||
);
|
||||
|
||||
// 账号备注独占一行(不包在 Row/Col 中)
|
||||
assert.ok(
|
||||
!editPage.slice(editPage.lastIndexOf('<Row gutter={16}>', noteItem), noteItem).includes('name="note"'),
|
||||
'账号备注不应在 Row 内',
|
||||
);
|
||||
});
|
||||
|
||||
test('指纹浏览器环境独立卡片单独保存,不混入账号资料保存', () => {
|
||||
assert.ok(editPage.includes('title="指纹浏览器环境"'), '指纹环境应为独立卡片');
|
||||
assert.ok(editPage.includes('>保存指纹<') || editPage.includes('保存指纹'), '指纹卡片应有独立保存按钮');
|
||||
assert.match(editPage, /\/creator\/accounts\/\$\{encodeURIComponent\(selected\.id\)\}\/fingerprint/);
|
||||
assert.ok(editPage.includes('<FingerprintFields />'), '指纹字段应复用共享组件');
|
||||
assert.ok(editPage.includes('fpForm.validateFields()'), '指纹保存前应先校验表单');
|
||||
});
|
||||
|
||||
test('指纹保存后按运行状态提示:运行中重启 / 未运行下次启动生效', () => {
|
||||
assert.ok(editPage.includes("result?.runtime_id ? '指纹已保存,运行环境已用新配置重启。' : '指纹已保存,将在环境下次启动时生效。'"));
|
||||
});
|
||||
|
||||
test('编辑页加载时从账号详情内联 environment.fingerprint 回显指纹', () => {
|
||||
assert.ok(editPage.includes("getOne('accounts', selectedID)"), '编辑页应请求账号详情');
|
||||
assert.match(editPage, /fingerprintFormValues\(detail\?\.environment\?\.fingerprint\)/);
|
||||
});
|
||||
|
||||
test('账号资料保存保留现有冷却时间,不再发送 NaN', () => {
|
||||
assert.ok(
|
||||
editPage.includes('cooldown_seconds: selected.cooldown_seconds'),
|
||||
'cooldown_seconds 应透传当前值,避免 Number(undefined)=NaN 被 400 拒绝',
|
||||
);
|
||||
assert.ok(!editPage.includes('Number(values.cooldown_seconds)'), '不得再从缺失的表单字段计算冷却时间');
|
||||
});
|
||||
|
||||
test('共享指纹字段:载荷只含非空项,disable_spoofing 以逗号合并', () => {
|
||||
assert.match(fingerprintFields, /fp\.disable_spoofing\?\.length\) payload\.disable_spoofing = fp\.disable_spoofing\.join\(','\)/);
|
||||
assert.match(fingerprintFields, /if \(fp\.timezone\?\.trim\(\)\) payload\.timezone = fp\.timezone\.trim\(\)/);
|
||||
assert.match(fingerprintFields, /payload\.hardware_concurrency = fp\.hardware_concurrency/);
|
||||
});
|
||||
|
||||
test('共享指纹字段:回显把逗号字符串拆回多选数组', () => {
|
||||
assert.match(fingerprintFields, /String\(fp\.disable_spoofing\)\.split\(','\)/);
|
||||
});
|
||||
|
||||
test('创建页与编辑页共用同一指纹字段组件', () => {
|
||||
assert.ok(createPage.includes("from '@/components/accounts/FingerprintFields'"));
|
||||
assert.ok(createPage.includes('<FingerprintFields />'));
|
||||
assert.match(createPage, /fingerprintPayload\(values\.fingerprint\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user