import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; import { fetchSsoProviders, fetchWecomConfig } from "../api/auth"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { isWecomWebView } from "../lib/runtimeContext"; import { buildWecomLoginUrl, resolveWecomBindNotice, resolveWecomEntryStatus, resolveWecomLoginNotice, type WecomNotice, } from "../lib/wecomEntry"; function resolveRedirect(searchParams: URLSearchParams) { const redirect = searchParams.get("redirect"); if (!redirect || !redirect.startsWith("/") || redirect.startsWith("//")) { return "/portal"; } 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 } = useAuth(); const [identifier, setIdentifier] = useState(""); const [password, setPassword] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const redirectTo = resolveRedirect(searchParams); const isWecomContext = isWecomWebView(); const ssoProvidersQuery = useQuery({ queryKey: ["auth", "sso-providers"], queryFn: () => fetchSsoProviders(), staleTime: 60_000, }); const oidcProvider = (ssoProvidersQuery.data ?? []).find((item) => item.id === "oidc" && item.enabled); const wecomConfigQuery = useQuery({ queryKey: ["auth", "wecom-config"], queryFn: () => fetchWecomConfig(), staleTime: 60_000, }); const wecomConfig = wecomConfigQuery.data; const wecomStatus = resolveWecomEntryStatus(wecomConfig); const wecomEntry = wecomConfig?.entryEnabled ? wecomConfig : null; const primaryMode = isWecomContext && wecomEntry?.loginUrl ? "wecom" : "password"; const wecomNotice = useMemo( () => resolveWecomBindNotice(searchParams.get("wecomBind")) ?? resolveWecomLoginNotice(searchParams.get("wecom")), [searchParams], ); 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(mapApiErrorMessage(err.code, err.details, err.message)); } else if (err instanceof TypeError) { setError("无法连接后端服务,请确认 server 已启动(http://127.0.0.1:3000)。"); } else { setError("登录失败,请稍后重试。"); } } finally { setSubmitting(false); } } function handleWecomLogin() { if (!wecomEntry?.loginUrl) { return; } window.location.assign(buildWecomLoginUrl(wecomEntry.loginUrl, redirectTo)); } function renderPasswordPanel(testId: string, primary: boolean) { return (
{primary ? "推荐入口" : "回退入口"}

使用账号密码登录

{primary ? "适合桌面浏览器直接登录;若企业微信入口暂未开放,也可正常使用。" : "企业微信免登失败、未绑定或超时时,可改用账号密码继续进入。"}

setIdentifier(event.target.value)} placeholder="邮箱或手机号" value={identifier} />
setPassword(event.target.value)} placeholder="请输入密码" type="password" value={password} />
{error ?
{error}
: null}
); } return (
统一入口

登录海音达工作台

企业微信负责进入与提醒,真正的审批、项目、周报和团队留痕都在海音达工作台内完成。

{isWecomContext ? "当前在企业微信中打开,推荐直接使用企业微信进入工作台;失败时可随时回退到账号密码登录。" : "当前为浏览器场景,推荐直接使用账号密码登录;企业微信入口会作为次级入口提供。"}
{wecomNotice ? (
{wecomNotice.title}
{wecomNotice.description}
) : null}
{primaryMode === "wecom" && wecomEntry?.loginUrl ? (
推荐入口

使用企业微信进入工作台

当前是企业微信场景,优先使用免登入口;若未绑定或回调失效,仍可切换为账号密码登录。

) : null} {renderPasswordPanel( primaryMode === "password" ? "login-password-primary-panel" : "login-password-secondary-panel", primaryMode === "password", )} {((primaryMode !== "wecom" && wecomEntry?.loginUrl) || oidcProvider) ? (
补充入口

其他进入方式

入口失败时请优先选择可理解、可回退的方式继续进入。

{primaryMode !== "wecom" && wecomEntry?.loginUrl ? ( ) : null} {oidcProvider ? ( ) : null}
) : null}
企业微信入口状态:{wecomStatus.label}
{wecomStatus.description}
{wecomStatus.reasonLabels.length ? (
    {wecomStatus.reasonLabels.map((label) => (
  • {label}
  • ))}
) : null}
); }