import { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useAuth } from "../App"; import { Book, Check, Logo, TicketIcon } from "../components/icons"; import { Button, Field, InlineAlert } from "../components/ui"; import { ApiError, api, session, type Tokens } from "../lib/client"; import type { User } from "../lib/types"; /** * 登录后回哪儿:`?next=` 给邀请链接用。 * 只接受站内路径(以单个 `/` 开头),避免被拼成 `//evil.com` 的外跳。 */ function safeNext(raw: string | null): string | null { if (!raw) return null; if (!raw.startsWith("/") || raw.startsWith("//")) return null; return raw; } const HIGHLIGHTS = [ { icon: , title: "一件事,从头到尾", desc: "收下、开工、完成,状态一路可见" }, { icon: , title: "卡住就变成工单", desc: "原任务自动挂着等,办完再回来,不丢线", }, { icon: , title: "办完自动留档", desc: "验收通过生成知识草稿,核对后进团队库", }, ]; export function LoginPage() { const { reload } = useAuth(); const navigate = useNavigate(); const [params] = useSearchParams(); const next = safeNext(params.get("next")); const [phone, setPhone] = useState(""); const [code, setCode] = useState(""); const [password, setPassword] = useState(""); const [mode, setMode] = useState<"code" | "password">("code"); const [devCode, setDevCode] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [countdown, setCountdown] = useState(0); useEffect(() => { if (countdown <= 0) return; const timer = setTimeout(() => setCountdown((v) => v - 1), 1000); return () => clearTimeout(timer); }, [countdown]); const phoneValid = /^1[3-9]\d{9}$/.test(phone); async function sendCode() { setError(null); setBusy(true); try { const data = await api.post<{ dev_code?: string }>("/auth/sms/send", { phone }); setDevCode(data.dev_code ?? null); setCountdown(60); } catch (err) { setError(err instanceof ApiError ? err.message : "发送失败"); } finally { setBusy(false); } } async function submit(event: React.FormEvent) { event.preventDefault(); setError(null); setBusy(true); try { const data = mode === "code" ? await api.post<{ user: User; tokens: Tokens }>("/auth/login", { phone, code }) : await api.post<{ user: User; tokens: Tokens }>("/auth/login/password", { phone, password, }); session.save(data.tokens); await reload(); navigate(next ?? "/", { replace: true }); } catch (err) { setError(err instanceof ApiError ? err.message : "登录失败"); } finally { setBusy(false); } } return (
{/* 左:品牌页 */}
达道Task

从任务到知识,
一条道走到底。

你自己的待办、需要别人搭把手的时刻、以及干完活留下的经验——本来就是一回事。

    {HIGHLIGHTS.map((h) => (
  • {h.icon} {h.title} {h.desc}
  • ))}

个人先用着,团队按席位。数据在你自己的工作区里。

{/* 右:表单 */}
达道Task
任务 · 工单 · 知识

登录

{mode === "code" ? "手机号验证码登录,第一次登录会自动创建账号。" : "用手机号和登录密码进入。没设过密码的,先用验证码登录一次再设。"}

{( [ ["code", "验证码"], ["password", "密码"], ] as const ).map(([key, label]) => ( ))}
setPhone(e.target.value.trim())} maxLength={11} /> {mode === "code" ? (
setCode(e.target.value.trim())} maxLength={6} />
) : ( setPassword(e.target.value)} maxLength={64} /> )} {devCode && (
开发环境验证码:{devCode} (生产环境走短信通道,不回显)
)} {error && {error}}

登录即表示同意《服务条款》与《隐私政策》。我们不会把你的内容用于训练模型。

); }