feat: expose Douyin login QR in admin
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")),
|
||||
|
||||
Reference in New Issue
Block a user