feat(web): task-3 accounts 模块全量迁移(列表/监控/导入/新建/详情/编辑,antd 默认组件 + 约定式路由 + terser 压缩绕开 esbuild helper 冲突)

This commit is contained in:
2026-09-22 12:40:09 +08:00
parent 542eac9e0a
commit 04ac91874a
22 changed files with 1551 additions and 133 deletions
+109
View File
@@ -0,0 +1,109 @@
// 公共展示工具:字段语义逐一对照 web.archived/src/shared/ui/ui.jsx
export function dateTime(value?: string | number | null): string {
if (!value) return '—';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
}
// 对齐归档版 conflictMessage:优先后端 error 字段,409 冲突时附加提示。
export function conflictMessage(error: any, conflictHint?: string): string {
const status = error?.status ?? error?.body?.status;
const message = error?.message || error?.body?.error || '请求失败';
if (status === 409 && conflictHint) return `${message}:${conflictHint}`;
return message;
}
export const platforms = [
{ value: 'douyin', label: '抖音' },
{ value: 'xiaohongshu', label: '小红书' },
{ value: 'wechat-official', label: '公众号' },
{ value: 'kuaishou', label: '快手' },
];
export function platformLabel(value?: string): string {
return platforms.find((item) => item.value === value)?.label || value || '—';
}
export const shareJobStatus: Record<string, { label: string; tone: 'success' | 'warning' | 'error' | 'default' }> = {
queued: { label: '待解析', tone: 'warning' },
processing: { label: '解析中', tone: 'warning' },
succeeded: { label: '已加入监控', tone: 'success' },
failed: { label: '解析失败', tone: 'error' },
};
// 对齐归档版 extractShareURL / platformForShareURL
const shareURLPattern = /https?:\/\/[^\s<>"'`]+/giu;
const trailingShareURLPunctuation = /[,。;!?、)》)】\]}>,.?!:;]+$/u;
export function platformForShareURL(value: string): string {
try {
const hostname = new URL(value).hostname.toLowerCase();
if (['www.douyin.com', 'v.douyin.com'].includes(hostname)) return 'douyin';
if (['www.xiaohongshu.com', 'xhslink.com', 'www.xhslink.com'].includes(hostname)) return 'xiaohongshu';
} catch {
return '';
}
return '';
}
export function extractShareURL(value?: string): string {
const candidates = String(value || '')
.match(shareURLPattern)
?.map((candidate) => candidate.replace(trailingShareURLPunctuation, ''))
.filter(Boolean);
return candidates?.find((candidate) => platformForShareURL(candidate)) || candidates?.[0] || '';
}
// 账号可恢复性判定(对齐归档版 accountReadiness):授权、绑定、出口健康、账号暂停四层。
export const readinessReasonText: Record<string, string> = {
account_revoked: '授权已撤销',
account_paused: '账号已暂停',
binding_missing: '未绑定运行环境',
network_exit_missing: '未绑定固定出口',
network_exit_unhealthy: '固定出口不健康',
runtime_stop_pending: '运行环境停止结果待确认',
runtime_missing: '运行环境未启动',
runtime_active: '仍有活动运行实例',
account_conflict: '账号状态已变化,请刷新后重试',
environment_unavailable: '环境状态未知',
};
export interface AccountReadiness {
label: string;
reason: string;
canResume: boolean;
ready: boolean;
}
export function accountReadiness(account: any, binding: any, bindingError: any = null): AccountReadiness {
if (account.authorization_status !== 'authorized')
return { label: '授权已撤销', reason: 'account_revoked', canResume: false, ready: false };
if (bindingError) return { label: '环境状态未知', reason: 'environment_unavailable', canResume: false, ready: false };
if (!binding) return { label: '未绑定运行环境', reason: 'binding_missing', canResume: false, ready: false };
let blocked = '';
if (binding.cleanup_pending) {
blocked = 'runtime_stop_pending';
} else if (binding.network_exit_id && binding.network_exit_health !== 'healthy') {
blocked = 'network_exit_unhealthy';
}
if (blocked) return { label: readinessReasonText[blocked], reason: blocked, canResume: false, ready: false };
if (account.runtime_status === 'paused') {
const stopped = !binding.runtime_instance_id;
return { label: stopped ? '资源就绪,可恢复' : '等待运行环境停止', reason: stopped ? '' : 'runtime_stop_pending', canResume: stopped, ready: false };
}
const schedule = binding.schedule_block_reason || '';
return {
label: schedule ? readinessReasonText[schedule] || `不可调度:${schedule}` : '可调度',
reason: schedule,
canResume: false,
ready: !schedule,
};
}
export function isCollectionAccount(account: any): boolean {
return (
account.authorization_status === 'authorized' &&
['normal', 'muted'].includes(account.business_status) &&
account.login_status === 'logged_in'
);
}
+25
View File
@@ -0,0 +1,25 @@
// 列表页共享工具:资源 → 查询字段白名单,对齐 web.archived/src/shared/api/dataProvider.js 的 filterKeys。
// creator-* 资源走 page/page_size 分页;其余资源后端返回全量或自带 total。
export const filterKeys: Record<string, string[]> = {
drafts: ['account_id'],
confirmations: ['draft_id'],
tasks: ['account_id', 'draft_id', 'state'],
'creator-competitors': ['platform'],
'creator-competitor-share-jobs': ['platform', 'status'],
'creator-works': ['platform', 'source_id', 'source_type', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares'],
'creator-comments': ['platform', 'work_id'],
'creator-leads': ['platform'],
'creator-operations': ['account_id'],
'creator-conversations': ['account_id'],
'creator-events': ['account_id'],
};
export function isCreatorResource(resource: string): boolean {
return resource.startsWith('creator-');
}
export interface ListQuery {
data: any[];
total: number;
has_next?: boolean;
}
+60
View File
@@ -0,0 +1,60 @@
import type { MenuDataItem } from '@ant-design/pro-components';
// 页头标题/副标题规则:语义对齐 web.archived/src/app/Layout.jsx 的 pageMetadataRules。
const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: string }> = [
{ test: (p) => p === '/accounts/import', title: '账号导入', subtitle: '管理分享链接解析任务,解析成功后自动加入监控账号。' },
{ test: (p) => p === '/accounts', title: '我的账号', subtitle: '创建和管理自己维护的账号。' },
{ test: (p) => p === '/accounts/monitoring', title: '监控账号', subtitle: '管理需要持续跟踪的竞品账号及采集状态。' },
{ test: (p) => p === '/accounts/new', title: '创建社媒账号', subtitle: '创建账号后,再在编辑页配置登录身份与账号策略。' },
{ test: (p) => /\/edit$/.test(p) && p.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
{ test: (p) => p.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
{ test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
{ test: (p) => p === '/creator/workbench', title: '运营工作台', subtitle: '评论、线索、私信与写操作均保留来源和明确结果。' },
{ test: (p) => p === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' },
{ test: (p) => p === '/competitors' || p.startsWith('/competitors/'), title: '竞品分析', subtitle: '粘贴作品分享内容,自动识别作者并加入监听队列。' },
{ test: (p) => p.startsWith('/drafts/'), title: '草稿核对', subtitle: '核对版本化草稿,确认后再将操作加入队列。' },
{ test: (p) => p.startsWith('/attempts/'), title: 'Attempt 详情', subtitle: '查看执行结果、运行环境与网络出口。' },
{ test: (p) => p === '/tasks', title: '任务中心', subtitle: '逐条处理暂停、版本变化与未知执行结果。' },
{ test: (p) => p.startsWith('/tasks/'), title: '任务详情', subtitle: '查看任务状态、执行结果与允许的下一步操作。' },
{ test: (p) => p === '/browsers', title: '运行环境', subtitle: '启动、停止并回收隔离的指纹浏览器运行环境。' },
{ test: (p) => p === '/browsers/new', title: '创建运行环境', subtitle: '为已授权且暂停的账号绑定网关、指纹与网络出口。' },
{ test: (p) => p.startsWith('/browsers/'), title: '运行环境详情', subtitle: '查看环境状态、账号绑定和运行实例信息。' },
{ test: (p) => p === '/network-exits', title: '网络出口', subtitle: '维护固定出口、健康观测与账号绑定。' },
{ test: (p) => p.startsWith('/network-exits/'), title: '网络出口详情', subtitle: '查看出口配置、健康状态和绑定关系。' },
{ test: (p) => p === '/gateways', title: '网关管理', subtitle: '注册并检查宿主机 native browser gateway。' },
{ test: (p) => p.startsWith('/gateways/'), title: '网关详情', subtitle: '查看网关连接信息和运行状态。' },
];
// 菜单结构:与 web.archived/src/app/Layout.jsx 完全一致(运营 / 资源 两组)。
export const menu: MenuDataItem[] = [
{
name: '运营',
children: [
{ path: '/accounts/import', name: '账号导入' },
{ path: '/accounts', name: '我的账号' },
{ path: '/accounts/monitoring', name: '监控账号' },
{ path: '/creator/competitors', name: '竞品分析' },
{ path: '/creator/workbench', name: '运营工作台' },
{ path: '/creator/settings', name: '采集设置' },
{ path: '/tasks', name: '任务中心' },
],
},
{
name: '资源',
children: [
{ path: '/browsers', name: '运行环境' },
{ path: '/network-exits', name: '网络出口' },
{ path: '/gateways', name: '网关管理' },
],
},
];
export function pageMetadata(pathname: string): { title: string; subtitle: string } {
const matched = rules.find((item) => item.test(pathname));
if (matched) return { title: matched.title, subtitle: matched.subtitle };
for (const group of menu) {
const hit = (group.children ?? []).find((item) => item.path && (pathname === item.path || pathname.startsWith(`${item.path}/`)));
if (hit) return { title: hit.name ?? '', subtitle: 'CreatorHub 运营控制台' };
}
return { title: 'CreatorHub', subtitle: '运营控制台' };
}