import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage"; import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Building2, MailCheck } from "lucide-react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { fetchAuthCapabilities, fetchSsoProviders, fetchWecomConfig, requestEmailLoginCode } from "../api/auth"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { AuthPanel } from "../components/auth/AuthPanel"; import { getDeviceFingerprint } from "../auth/deviceFingerprint"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { PasswordInput } from "../components/ui/password-input"; import { isWecomWebView, resolveDefaultWorkspacePath } from "../lib/runtimeContext"; import { buildWecomLoginUrl, resolveWecomBindNotice, resolveWecomLoginNotice, type WecomNotice, } from "../lib/wecomEntry"; type LoginMode = "password" | "email_code"; function resolveRedirect(searchParams: URLSearchParams, fallback: string) { const redirect = searchParams.get("redirect"); if (!redirect || !redirect.startsWith("/") || redirect.startsWith("//")) { return fallback; } return redirect; } function noticeClasses(tone: WecomNotice["tone"]) { if (tone === "success") { return "border-emerald-200 bg-emerald-50 text-emerald-700"; } if (tone === "warning") { return "border-amber-200 bg-amber-50 text-amber-700"; } return "border-slate-200 bg-slate-50 text-slate-700"; } export function LoginPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { login, loginWithEmailCode } = useAuth(); const [loginMode, setLoginMode] = useState("password"); const [identifier, setIdentifier] = useState(""); const [password, setPassword] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const [emailLoginEmail, setEmailLoginEmail] = useState(""); const [emailLoginCode, setEmailLoginCode] = useState(""); const [emailCodeRequested, setEmailCodeRequested] = useState(false); const [emailCodeCooldown, setEmailCodeCooldown] = useState(0); const [emailCodeSubmitting, setEmailCodeSubmitting] = useState(false); const [emailCodeSending, setEmailCodeSending] = useState(false); const [emailCodeMessage, setEmailCodeMessage] = useState(""); const [emailCodeError, setEmailCodeError] = useState(""); const [passwordFallbackOpen, setPasswordFallbackOpen] = useState(false); const isWecomContext = isWecomWebView(); const defaultWorkspacePath = resolveDefaultWorkspacePath(); const redirectTo = resolveRedirect(searchParams, defaultWorkspacePath); const ssoProvidersQuery = useQuery({ queryKey: ["auth", "sso-providers"], queryFn: () => fetchSsoProviders(), staleTime: 60_000, }); const oidcProvider = (ssoProvidersQuery.data ?? []).find((item) => item.id === "oidc" && item.enabled); const authCapabilitiesQuery = useQuery({ queryKey: ["auth", "capabilities"], queryFn: () => fetchAuthCapabilities(), staleTime: 60_000, }); const emailDeliveryReady = authCapabilitiesQuery.data?.emailDelivery?.ready === true; const emailDeliveryChecking = authCapabilitiesQuery.isLoading; const emailDeliveryUnavailable = authCapabilitiesQuery.isSuccess && !emailDeliveryReady; const emailDeliveryUnknown = authCapabilitiesQuery.isError; const wecomConfigQuery = useQuery({ queryKey: ["auth", "wecom-config"], queryFn: () => fetchWecomConfig(), staleTime: 60_000, }); const wecomConfig = wecomConfigQuery.data; const wecomEntry = wecomConfig?.entryEnabled && wecomConfig.loginUrl ? wecomConfig : null; const pcWecomEntry = !isWecomContext && wecomConfig?.pcWebLoginEnabled && wecomConfig.pcLoginUrl ? wecomConfig : null; const wecomNotice = useMemo( () => resolveWecomBindNotice(searchParams.get("wecomBind")) ?? resolveWecomLoginNotice(searchParams.get("wecom")), [searchParams], ); const showWecomPrimary = isWecomContext; const showPasswordPanel = !showWecomPrimary || Boolean(wecomNotice) || passwordFallbackOpen || (showWecomPrimary && wecomConfig && !wecomEntry); const showBrowserWecomNotice = !showWecomPrimary && Boolean(wecomEntry) && !pcWecomEntry; useEffect(() => { if (emailCodeCooldown <= 0) { return undefined; } const timer = window.setTimeout(() => { setEmailCodeCooldown((current) => Math.max(0, current - 1)); }, 1000); return () => window.clearTimeout(timer); }, [emailCodeCooldown]); async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setSubmitting(true); setError(""); try { const result = await login(identifier.trim(), password); if (result.mustChangePassword) { navigate("/change-password", { replace: true }); } else { navigate(redirectTo, { replace: true }); } } catch (err) { if (isApiError(err)) { setError(resolveUserErrorMessage(err)); } else if (err instanceof TypeError) { setError("无法连接后端服务,请稍后再试或联系管理员。"); } else { setError("登录失败,请稍后重试。"); } } finally { setSubmitting(false); } } function handleWecomLogin() { if (!wecomEntry?.loginUrl) { return; } window.location.assign(buildWecomLoginUrl(wecomEntry.loginUrl, redirectTo, getDeviceFingerprint())); } function handlePcWecomLogin() { if (!pcWecomEntry?.pcLoginUrl) { return; } window.location.assign(buildWecomLoginUrl(pcWecomEntry.pcLoginUrl, redirectTo, getDeviceFingerprint())); } async function handleRequestEmailCode(event: React.FormEvent) { event.preventDefault(); setEmailCodeError(""); setEmailCodeMessage(""); const email = emailLoginEmail.trim(); if (!email) { setEmailCodeError("请输入企业邮箱。"); return; } if (!emailDeliveryReady) { setEmailCodeError( emailDeliveryChecking ? "正在检查邮件服务,请稍后再试。" : "邮件验证码暂不可用,请使用密码登录或联系管理员。", ); return; } setEmailCodeSending(true); try { await requestEmailLoginCode({ email }); setEmailCodeRequested(true); setEmailCodeCooldown(60); setEmailCodeMessage("如账号可用,验证码将发送到企业邮箱;10 分钟内有效,请同时查看免提醒邮件。"); } catch (err) { if (isApiError(err)) { setEmailCodeError(resolveUserErrorMessage(err)); } else if (err instanceof TypeError) { setEmailCodeError("无法连接后端服务,请稍后再试或联系管理员。"); } else { setEmailCodeError("发送验证码失败,请稍后重试。"); } } finally { setEmailCodeSending(false); } } async function handleEmailCodeLogin(event: React.FormEvent) { event.preventDefault(); setEmailCodeError(""); const email = emailLoginEmail.trim(); const code = emailLoginCode.trim(); if (!email) { setEmailCodeError("请输入企业邮箱。"); return; } if (code.length !== 6) { setEmailCodeError("请输入 6 位验证码。"); return; } setEmailCodeSubmitting(true); try { const result = await loginWithEmailCode(email, code); if (result.mustChangePassword) { navigate("/change-password", { replace: true }); } else { navigate(redirectTo, { replace: true }); } } catch (err) { if (isApiError(err)) { setEmailCodeError(resolveUserErrorMessage(err)); } else if (err instanceof TypeError) { setEmailCodeError("无法连接后端服务,请稍后再试或联系管理员。"); } else { setEmailCodeError("登录失败,请稍后重试。"); } } finally { setEmailCodeSubmitting(false); } } function selectLoginMode(mode: LoginMode) { setLoginMode(mode); setError(""); setEmailCodeError(""); setEmailCodeMessage(""); } function renderBrowserEnterprisePanel() { if (!pcWecomEntry && !showBrowserWecomNotice && !oidcProvider) { return null; } return (
企业入口
{pcWecomEntry ? (

企业微信登录

前往企业微信官方登录页,使用手机企业微信扫码后进入 OA 工作台。

) : null} {pcWecomEntry ? ( ) : showBrowserWecomNotice ? (
) : null} {oidcProvider ? ( ) : null}
); } function renderPasswordPanel(testId: string) { return (
setIdentifier(event.target.value)} placeholder="邮箱或手机号" value={identifier} />
setPassword(event.target.value)} placeholder="请输入密码" value={password} />
忘记密码?
{error ?
{error}
: null}
); } function renderEmailCodePanel(testId: string) { return (
setEmailLoginEmail(event.target.value)} placeholder="请输入企业邮箱" type="email" value={emailLoginEmail} />
{!emailDeliveryReady ? (
) : null}
setEmailLoginCode(event.target.value.replace(/\D/g, "").slice(0, 6))} placeholder="6 位验证码" value={emailLoginCode} />
{emailCodeMessage ? (
{emailCodeMessage}
) : null} {emailCodeError ? (
{emailCodeError}
) : null}
); } return (
{wecomNotice ? (
{wecomNotice.title}
{wecomNotice.description}
) : null} {showWecomPrimary ? (
{wecomConfig && !wecomEntry ? (
企业微信入口不可用,请用账号密码。
) : null} {!showPasswordPanel ? ( ) : null}
) : null} {showWecomPrimary ? ( showPasswordPanel ? renderPasswordPanel("login-password-secondary-panel") : null ) : (
{pcWecomEntry ? "可使用企业微信扫码登录;企业邮箱验证码和账号密码作为备用方式。" : "员工日常建议用企业邮箱验证码;固定办公电脑可使用账号密码。企业微信免登需要从企业微信工作台打开。"}
{pcWecomEntry ? (

备用登录

使用账号密码或企业邮箱验证码。

) : null}
{loginMode === "email_code" ? renderEmailCodePanel("login-email-code-panel") : renderPasswordPanel("login-password-primary-panel")}
{renderBrowserEnterprisePanel()}
)}
); }