import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { AlertTriangle, ArrowLeft, ArrowRight, RefreshCw, ScanLine } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { AuthShell } from "../features/auth/AuthShell"; import { syncWebDeviceName } from "../features/auth/auth"; import { QrCanvas } from "../features/qr/QrCanvas"; import { persistDesktopDeviceName } from "../net/desktop"; import { pollQrStatus, qrStart, type QrStartResp, type QrStatus } from "../net/qr"; import { readInjectedDeviceType } from "../store/helpers"; import { useAppStore } from "../store"; import { t } from "../i18n"; import { isAsciiDeviceName } from "../utils/format"; import { StateDot } from "../ui/glyphs"; import { Button, Callout, TextField } from "../ui/primitives"; import s from "./link_.new.module.css"; // 显码页:有人借了一台陌生设备,扫码把它接入自己的 cdrop。它本身公开(start / // status 无需登录),故不设路由守卫——已登录用户也可经此把另一设备接入。 export const Route = createFileRoute("/link_/new")({ component: LinkNewPage, }); type Step = "name" | "show"; function LinkNewPage() { const navigate = useNavigate(); const setAuth = useAppStore((s) => s.setAuth); const setSelfDeviceName = useAppStore((s) => s.setSelfDeviceName); const [ step, setStep ] = useState("name"); const [ name, setName ] = useState(suggestDeviceName); const [ session, setSession ] = useState(null); const [ phase, setPhase ] = useState("pending"); const [ error, setError ] = useState(null); const [ starting, setStarting ] = useState(false); // 轮询的取消句柄。换二维码 / 卸载时调用即停。 const cancelPollRef = useRef<(() => void) | null>(null); const onApproved = useCallback((st: QrStatus) => { if (st.status !== "approved" || !st.accessToken || !st.user) { return; } setAuth({ accessToken: st.accessToken, user: { id: st.user.id, name: st.user.name, avatar: st.user.avatar }, }); // 设备名以批准方下发的为权威(服务端登记的名字),回退到本地输入。 const finalName = st.deviceName?.trim() || name.trim(); setSelfDeviceName(finalName); persistDesktopDeviceName(finalName); syncWebDeviceName(finalName); navigate({ to: "/" }); }, [ name, navigate, setAuth, setSelfDeviceName ]); const begin = useCallback(async (deviceName: string) => { setError(null); setStarting(true); try { const resp = await qrStart({ deviceName, deviceType: readInjectedDeviceType(), }); setSession(resp); setPhase("pending"); setStep("show"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setStarting(false); } }, []); // 拿到 session 即起长轮询;session 变化(重新生成)时换新轮询。 useEffect(() => { if (!session) { return; } cancelPollRef.current?.(); const cancel = pollQrStatus(session.requestId, session.pollSecret, { onResolved: (st) => { setPhase(st.status); if (st.status === "approved") { onApproved(st); } }, onError: () => { /* 单轮抖动:轮询循环自重试,UI 保持等待态 */ }, }); cancelPollRef.current = cancel; return () => { cancel(); cancelPollRef.current = null; }; }, [ session, onApproved ]); const handleContinue = () => { const trimmed = name.trim(); if (!trimmed) { return; } if (!isAsciiDeviceName(trimmed)) { setError(t("settings.deviceName.asciiOnly")); return; } void begin(trimmed); }; const handleRegenerate = () => { setError(null); void begin(name.trim()); }; // 回上一步:弃掉当前二维码会话,退回命名步骤(轮询随 session 清空由 effect 清理)。 const handleBackToName = () => { cancelPollRef.current?.(); cancelPollRef.current = null; setSession(null); setError(null); setStep("name"); }; if (step === "name") { return ( {error && ( }>{error} )} setName(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter" && name.trim()) { e.preventDefault(); handleContinue(); } }} autoFocus /> ); } return ( {error && ( }>{error} )}
{session && }
  1. {t("qr.show.howto1")}
  2. {t("qr.show.howto2")}
{(phase === "expired" || phase === "denied") && ( } > {phase === "denied" ? t("qr.show.denied") : t("qr.show.expired")} )} {(phase === "expired" || phase === "denied") && ( )} {/* 始终给一个明确退出口:回到命名步骤,避免卡在显码 / 终态页。 */} {phase !== "approved" && ( )}
); } // 显码页的等待态:一行平静的状态,pending 时 accent 脉冲点 + 「等待批准」。 function StatusLine(props: { phase: QrStatus["status"] }) { const { phase } = props; if (phase === "approved") { return (
{t("qr.show.approved")}
); } if (phase === "expired" || phase === "denied") { return null; } return (
); } // suggestDeviceName:UA 预填一个可读的默认设备名(与 /setup 同口径),可编辑。 function suggestDeviceName(): string { const ua = navigator.userAgent; let browser = "Browser"; if (ua.includes("Firefox/")) { browser = "Firefox"; } else if (ua.includes("Edg/")) { browser = "Edge"; } else if (ua.includes("Chrome/")) { browser = "Chrome"; } else if (ua.includes("Safari/")) { browser = "Safari"; } let os = "Desktop"; if (ua.includes("Mac")) { os = "macOS"; } else if (ua.includes("Windows")) { os = "Windows"; } else if (ua.includes("Linux")) { os = "Linux"; } return `${browser}-${os}-${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0")}`; }