import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router";
import { useDataProvider, useList, useOne } from "@refinedev/core";
import {
Alert,
Button,
Card,
CardContent,
ConfirmDialog,
DetailList,
Field,
Input,
PageHeader,
PageState,
Modal,
Select,
StatusPill,
Table,
TagInput,
Textarea,
conflictMessage,
} from "./lib/ui.jsx";
import { useTitle } from "./lib/hooks.js";
const createInitial = {
name: "",
platform: "",
platform_account_key: "",
tags: [],
cookies: "",
};
const platforms = [
{ value: "douyin", label: "抖音" },
{ value: "xiaohongshu", label: "小红书" },
{ value: "wechat-official", label: "公众号" },
{ value: "kuaishou", label: "快手" },
];
const reasonText = {
account_revoked: "授权已撤销",
account_paused: "账号已暂停",
binding_missing: "未绑定运行环境",
network_exit_missing: "未绑定固定出口",
network_exit_unhealthy: "固定出口不健康",
runtime_stop_pending: "运行环境停止结果待确认",
runtime_missing: "运行环境未启动",
runtime_active: "仍有活动运行实例",
account_conflict: "账号状态已变化,请刷新后重试",
environment_unavailable: "环境状态未知",
};
// 账号可恢复性判定:授权、绑定、出口健康、账号暂停四层都要看。
export function accountReadiness(account, binding, bindingsError = false) {
if (account.authorization_status !== "authorized")
return {
label: "授权已撤销",
reason: "account_revoked",
canResume: false,
ready: false,
};
if (bindingsError)
return {
label: "环境状态未知",
reason: "environment_unavailable",
canResume: false,
ready: false,
};
if (!binding)
return {
label: "未绑定运行环境",
reason: "binding_missing",
canResume: false,
ready: false,
};
const blocked = binding.cleanup_pending
? "runtime_stop_pending"
: binding.network_exit_id && binding.network_exit_health !== "healthy"
? "network_exit_unhealthy"
: "";
if (blocked)
return {
label: reasonText[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
? reasonText[schedule] || `不可调度:${schedule}`
: "可调度",
reason: schedule,
canResume: false,
ready: !schedule,
};
}
function ReadinessPill({ readiness, paused }) {
const tone = readiness.ready
? "success"
: paused && readiness.canResume
? "warning"
: "danger";
return ;
}
function AccountCreateForm({ onSubmit, busy, error }) {
const [form, setForm] = useState(createInitial);
const update = (key, value) =>
setForm((current) => ({ ...current, [key]: value }));
const valid =
form.name.trim() && form.platform && form.platform_account_key.trim();
async function submit(event) {
event.preventDefault();
if (!valid) return;
const data = {
name: form.name.trim(),
platform: form.platform.trim(),
platform_account_key: form.platform_account_key.trim(),
tags: form.tags,
};
// cookies 非必填:留空代表创建后走扫码登录,凭据由后续同步链路补齐
if (form.cookies.trim()) data.cookies = form.cookies.trim();
await onSubmit(data);
}
return (
);
}
function platformLabel(value) {
return platforms.find((platform) => platform.value === value)?.label || value;
}
export function AccountCreatePage() {
const navigate = useNavigate();
const dataProvider = useDataProvider()("default");
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
useTitle("CreatorHub · 创建社媒账号");
async function createAccount(data) {
setBusy(true);
setError(null);
try {
const result = await dataProvider.create({
resource: "accounts",
variables: data,
});
const id = result.data?.id;
if (!id) throw new Error("创建账号未返回账号 ID");
navigate(`/accounts/${encodeURIComponent(id)}/edit`);
} catch (reason) {
setError(reason);
} finally {
setBusy(false);
}
}
return (
);
}
function isCollectionAccount(account) {
return (
account.authorization_status === "authorized" &&
["normal", "muted"].includes(account.business_status) &&
account.login_status === "logged_in"
);
}
function AccountTags({ tags }) {
const values = Array.isArray(tags) ? tags : [];
return values.length ? (
{values.map((tag) => (
{tag}
))}
) : (
未设置
);
}
export function AccountList() {
const dataProvider = useDataProvider()("default");
const [accounts, setAccounts] = useState([]);
const [competitors, setCompetitors] = useState([]);
const [sourceFilter, setSourceFilter] = useState("");
const [pending, setPending] = useState(true);
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const [actionBusy, setActionBusy] = useState("");
const [tagTarget, setTagTarget] = useState(null);
const [tagDraft, setTagDraft] = useState([]);
const [tagBusy, setTagBusy] = useState(false);
const [deleteBusy, setDeleteBusy] = useState("");
useTitle("CreatorHub · 账号管理");
const load = async () => {
setPending(true);
setError(null);
try {
const [accountResult, competitorResult] = await Promise.all([
dataProvider.getList({ resource: "accounts" }),
dataProvider.getList({ resource: "creator-competitors" }),
]);
setAccounts(accountResult.data ?? []);
setCompetitors(competitorResult.data ?? []);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
};
useEffect(() => {
load();
}, [dataProvider]);
const rows = useMemo(
() => [
...accounts.map((account) => ({
...account,
source_type: "owned",
tags: account.tags ?? [],
})),
...competitors.map((account) => ({
...account,
name: account.nickname || account.platform_account_key,
source_type: "monitoring",
tags: account.tags ?? [],
})),
],
[accounts, competitors],
);
const visibleRows = sourceFilter
? rows.filter((account) => account.source_type === sourceFilter)
: rows;
async function syncCompetitor(competitor) {
setActionBusy(competitor.id);
setNotice(null);
try {
const result = await dataProvider.getList({ resource: "creator-accounts" });
const account = (result.data ?? []).find(
(candidate) =>
candidate.platform === competitor.platform &&
isCollectionAccount(candidate),
);
if (!account) {
throw new Error(
`没有可用于同步${platformLabel(competitor.platform)}监测账号的已登录自有账号。`,
);
}
await dataProvider.creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`,
{ account_id: account.id },
);
setNotice({ variant: "success", text: "监测账号同步任务已提交。" });
await load();
} catch (actionError) {
setNotice({
variant: "destructive",
text: conflictMessage(actionError, "监测账号同步失败"),
});
} finally {
setActionBusy("");
}
}
async function toggleCompetitor(competitor) {
setActionBusy(competitor.id);
setNotice(null);
try {
await dataProvider.creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? "pause" : "resume"}`,
);
setNotice({
variant: "success",
text: competitor.enabled ? "监测账号已暂停。" : "监测账号已恢复。",
});
await load();
} catch (actionError) {
setNotice({
variant: "destructive",
text: conflictMessage(actionError, "监测账号状态更新失败"),
});
} finally {
setActionBusy("");
}
}
async function deleteAccount(row) {
const label = row.source_type === "owned" ? "自有账号" : "监测账号";
if (!window.confirm(`确定删除${label}“${row.name}”吗?相关专属资源也会被清理。`)) {
return;
}
const busyID = `${row.source_type}:${row.id}`;
setDeleteBusy(busyID);
setNotice(null);
try {
await dataProvider.deleteOne({
resource: row.source_type === "owned" ? "accounts" : "creator-competitors",
id: row.id,
});
setNotice({ variant: "success", text: `${label}已删除。` });
await load();
} catch (deleteError) {
setNotice({
variant: "destructive",
text: conflictMessage(deleteError, `${label}删除失败`),
});
} finally {
setDeleteBusy("");
}
}
function openTags(row) {
setNotice(null);
setTagTarget(row);
setTagDraft(Array.isArray(row.tags) ? row.tags : []);
}
async function saveTags() {
if (!tagTarget) return;
setTagBusy(true);
setNotice(null);
try {
const result =
tagTarget.source_type === "owned"
? await dataProvider.creatorUpdate(
`/creator/accounts/${encodeURIComponent(tagTarget.id)}/tags`,
{ tags: tagDraft },
)
: await dataProvider.creatorUpdate(
`/creator/competitors/${encodeURIComponent(tagTarget.id)}`,
{ tags: tagDraft },
);
const tags = result.tags;
if (tagTarget.source_type === "owned") {
setAccounts((current) =>
current.map((account) =>
account.id === tagTarget.id ? { ...account, tags } : account,
),
);
} else {
setCompetitors((current) =>
current.map((account) =>
account.id === tagTarget.id ? { ...account, tags } : account,
),
);
}
setTagTarget(null);
setNotice({ variant: "success", text: "标签已保存。" });
} catch (saveError) {
setNotice({
variant: "destructive",
text: conflictMessage(saveError, "标签保存失败"),
});
} finally {
setTagBusy(false);
}
}
return (
{notice ? (
{notice.text}
) : null}
自有 {accounts.length} 个 · 监测 {competitors.length} 个 · 当前显示{" "}
{visibleRows.length} 个
(
{account.source_type === "owned" ? (
{account.name}
) : (
{account.name}
)}
{account.platform_account_key}
),
},
{
header: "类型",
width: "12%",
render: (account) => (
),
},
{
header: "平台",
width: "12%",
render: (account) => platformLabel(account.platform),
},
{
header: "标签",
width: "18%",
render: (account) => ,
},
{
header: "状态",
width: "16%",
render: (account) =>
account.source_type === "owned" ? (
{account.authorization_status === "authorized"
? "已授权"
: "已撤销"}
) : (
),
},
{
header: "操作",
width: "18%",
render: (account) => (
{account.source_type === "owned" ? (
<>
>
) : (
<>
>
)}
),
},
]}
rows={visibleRows}
rowKey={(account) => `${account.source_type}:${account.id}`}
minWidth="960px"
/>
{
if (!tagBusy) setTagTarget(null);
}}
title={`编辑标签 · ${tagTarget?.name || "账号"}`}
labelledBy="account-tags-title"
footer={
<>
>
}
>
);
}
export function AccountDetail() {
const { id } = useParams();
const navigate = useNavigate();
const dataProvider = useDataProvider()("default");
const [pauseOpen, setPauseOpen] = useState(false);
const [actionBusy, setActionBusy] = useState(false);
const [notice, setNotice] = useState(null);
const [draftText, setDraftText] = useState("");
const [draftBusy, setDraftBusy] = useState(false);
const {
result: account,
error,
isPending,
query,
} = useOne({ resource: "accounts", id, queryOptions: { retry: false } });
const { result: bindingResult, query: browsersQuery } = useList({
resource: "browsers",
queryOptions: { retry: false },
});
const browsers = bindingResult.data ?? [];
const browsersError = browsersQuery.error;
const refetchBrowsers = browsersQuery.refetch;
const { result: draftResult, query: draftQuery } = useList({
resource: "drafts",
filters: [{ field: "account_id", value: id }],
queryOptions: { retry: false },
});
const drafts = draftResult.data ?? [];
const refetchDrafts = draftQuery.refetch;
const binding = browsersError
? undefined
: browsers.find((item) => item.account_id === id);
const readiness = account
? accountReadiness(account, binding, !!browsersError)
: null;
useTitle("CreatorHub · 账号详情");
async function runAction(action) {
setActionBusy(true);
setNotice(null);
try {
await dataProvider.accountAction(account.id, action);
await Promise.all([query.refetch(), refetchBrowsers()]);
setNotice({
variant: "success",
text:
action === "pause"
? "账号已暂停;既有 hold 任务需后续逐条核验。"
: "账号已恢复;既有 hold 任务没有自动恢复。",
});
} catch (reason) {
setNotice({ variant: "destructive", text: conflictMessage(reason) });
} finally {
setActionBusy(false);
}
}
async function createDraft(event) {
event.preventDefault();
if (!draftText.trim() || !readiness?.ready) return;
setDraftBusy(true);
setNotice(null);
try {
const draft = await dataProvider.createDraft(account.id, draftText);
await refetchDrafts();
setDraftText("");
setNotice({
variant: "success",
text: `草稿版本 ${draft.version} 已创建,请进入只读快照核对。`,
});
} catch (reason) {
setNotice({ variant: "destructive", text: conflictMessage(reason) });
} finally {
setDraftBusy(false);
}
}
if (isPending) return ;
if (error || !account)
return (
{error?.message || "账号不存在"}
);
return (
{account.name}}
description={
{account.platform_account_key} · {account.platform}
}
>
{notice ? (
{notice.text}
) : null}
{browsersError ? (
环境不可用:运行环境与网络出口状态未知,依赖资源状态的操作暂不可用。
) : null}
暂停会把待领取任务置为 hold;恢复账号只恢复账号可用性,不会自动恢复既有
hold。
账号状态
固定资源
{browsersError ? (
运行环境与网络出口状态未知;重试成功后再执行依赖资源状态的操作。
) : binding ? (
<>
前往运行环境
>
) : (
<>
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
前往运行环境
>
)}
文本草稿
仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。
尚无草稿时无需处理;创建后请进入草稿核对。
{drafts.length > 0 ? (
{drafts.map((draft) => (
草稿版本 {draft.version}
{draft.content}
核对草稿
))}
) : null}
setPauseOpen(false)}
onConfirm={() => {
setPauseOpen(false);
runAction("pause");
}}
title="暂停账号"
confirmLabel="确认暂停"
body={`暂停账号 ${account.name}?待领取任务将进入 hold,恢复账号不会自动恢复既有 hold。`}
/>
);
}