feat: expose Douyin login QR in admin

This commit is contained in:
2026-09-16 10:50:05 +08:00
parent 9095920a5b
commit 7712b033b0
10 changed files with 379 additions and 1 deletions
+75
View File
@@ -68,6 +68,7 @@ export function CreatorAccountsPage() {
const [pending, setPending] = useState(true);
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const [loginQR, setLoginQR] = useState(null);
const [busy, setBusy] = useState(false);
const [strategies, setStrategies] = useState([]);
const [strategyError, setStrategyError] = useState(null);
@@ -202,6 +203,7 @@ export function CreatorAccountsPage() {
setEditingStrategyID("");
setBusy(false);
setNotice(null);
setLoginQR(null);
};
const change = (field) => (event) =>
setForm((value) => ({ ...value, [field]: event.target.value }));
@@ -240,6 +242,38 @@ export function CreatorAccountsPage() {
}
}
};
const requestLoginQR = async () => {
if (!selected || selected.platform !== "douyin") return;
const accountID = selected.id;
const version = selectionVersion.current;
setBusy(true);
setNotice(null);
try {
const result = await dataProvider.creatorAction(
`/creator/accounts/${encodeURIComponent(accountID)}/login-qr`,
);
if (version !== selectionVersion.current || accountID !== selectedID)
return;
setLoginQR(result);
setNotice({
variant: "success",
text: result.qr_detected
? "登录二维码已生成,请使用抖音 App 扫码。"
: "登录画面已生成;当前未检测到二维码,请按页面提示人工完成验证。",
});
} catch (qrError) {
if (version === selectionVersion.current && accountID === selectedID) {
setNotice({
variant: "destructive",
text: conflictMessage(qrError, "登录二维码获取失败;请确认浏览器环境正在运行"),
});
}
} finally {
if (version === selectionVersion.current && accountID === selectedID) {
setBusy(false);
}
}
};
const verifyLogin = async () => {
if (!selected) return;
setBusy(true);
@@ -260,6 +294,7 @@ export function CreatorAccountsPage() {
: item,
),
);
setLoginQR(null);
setNotice({ variant: "success", text: "浏览器身份核验成功。" });
await load();
} catch (verifyError) {
@@ -539,6 +574,14 @@ export function CreatorAccountsPage() {
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
onClick={requestLoginQR}
busy={busy}
disabled={selected.platform !== "douyin"}
>
显示登录二维码
</Button>
<Button
variant="outline"
onClick={verifyLogin}
@@ -561,6 +604,38 @@ export function CreatorAccountsPage() {
{notice.text}
</Alert>
) : null}
{loginQR ? (
<section
className="mb-5 rounded-lg border border-[#dfe5ee] bg-[#fafcff] p-4"
aria-label="抖音人工登录"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold">人工登录</h3>
<p className="mt-1 text-xs text-muted">
{loginQR.qr_detected
? "请使用抖音 App 扫描下方二维码。"
: "当前画面未检测到二维码,请按画面提示人工完成验证。"}
</p>
</div>
<Button
variant="outline"
onClick={requestLoginQR}
busy={busy}
>
刷新登录画面
</Button>
</div>
<img
className="mt-4 max-h-[520px] w-full rounded-md border border-[#e5eaf1] object-contain"
src={`data:${loginQR.content_type};base64,${loginQR.image_base64}`}
alt="抖音登录二维码或人工验证画面"
/>
<p className="mt-2 text-xs text-muted">
当前画面有效至 {dateTime(loginQR.expires_at)};完成扫码后请点击“核验浏览器身份”。
</p>
</section>
) : null}
<form onSubmit={save} className="grid gap-4 md:grid-cols-2">
<Field
id="creator-login-username"
+31
View File
@@ -151,6 +151,37 @@ describe("creator pages", () => {
expect(await screen.findByText("已开启大号模式。")).toBeTruthy();
});
it("displays the CreatorHub login screen returned by the gateway", async () => {
const dataProvider = provider({
creatorAction: vi.fn((path) =>
path.endsWith("/login-qr")
? Promise.resolve({
status: "manual_login",
content_type: "image/png",
image_base64: "cG5n",
qr_detected: true,
expires_at: "2026-01-01T00:02:00Z",
})
: Promise.resolve({ ...profile, login_status: "logged_in" }),
),
});
renderPage(<CreatorAccountsPage />, dataProvider);
fireEvent.click(
await screen.findByRole("button", { name: "显示登录二维码" }),
);
await waitFor(() =>
expect(dataProvider.creatorAction).toHaveBeenCalledWith(
"/creator/accounts/account-a/login-qr",
),
);
expect(
screen
.getByRole("img", { name: "抖音登录二维码或人工验证画面" })
.getAttribute("src"),
).toBe("data:image/png;base64,cG5n");
expect(await screen.findByText("请使用抖音 App 扫描下方二维码。")).toBeTruthy();
});
it("shows retryable account and strategy loading failures", async () => {
const dataProvider = provider({
getList: vi.fn().mockRejectedValue(new Error("accounts offline")),