"use client"; import { showErrorMsg, showSuccessMsg } from "@/utils/message"; import { Alert, BackgroundImage, Box, Button, Flex, FloatingIndicator, Input, LoadingOverlay, Paper, PasswordInput, Stack, Tabs, Text, TextInput, useComputedColorScheme, } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { IconAlertCircle } from "@tabler/icons-react"; import { DigitalIcon } from "./components/icons/digital"; import DarkBgImage from "./components/images/dark-login-bg.png"; import LightBgImage from "./components/images/light-login-bg.png"; import loginBg2Dark from "./components/images/login-bg2-dark.png"; import loginBg2Light from "./components/images/login-bg2-light.png"; import LogoDigImage from "./components/images/logo-dig.png"; import QrLogin from "./components/login/qr-login"; import { useLoginStore } from "./store"; import tabClasses from "./Tab.module.css"; import { encrypt, getQueryVariable, validatePhoneNumber } from "./util"; const HeaderIcon = DigitalIcon; const headerTitle = "酷哇数字化平台"; const fixedTenant = "coowa"; const ssoStateStorageKey = (appId: string) => `sso_state:${appId}`; export default function LoginPage() { const colorScheme = useComputedColorScheme(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [addrMap, setAddrMap] = useState>({}); const [activeTab, setActiveTab] = useState(() => { // 飞书 OAuth 回调(URL 带 ?code=xxx)时自动切到飞书 tab if (typeof window !== "undefined" && getQueryVariable("code")) { return "feishu"; } return "account"; }); const [rootRef, setRootRef] = useState(null); const controlsRefs = useRef>({}); const setControlRef = (val: string) => (node: HTMLButtonElement) => { controlsRefs.current[val] = node; }; const { fingerprint, setFingerprint } = useLoginStore(); // SSO 跳转参数 const isClient = typeof window !== "undefined"; const appId = isClient ? (getQueryVariable("app_id") as string) || "" : ""; const appUrl = isClient ? (getQueryVariable("app_url") as string) || "" : ""; const state = isClient ? (getQueryVariable("state") as string) || "" : ""; // 缺少 app_id 或 app_url 时不允许登录(sso-portal 自身不需要登录) const missingSsoParams = !appId || !appUrl; // 持久化 SSO 参数:飞书 OAuth 跳转后会丢失 URL 参数。 useEffect(() => { if (appId && appUrl) { localStorage.setItem(appId, appUrl); // app_id -> app_url localStorage.setItem("sso_app_id", appId); // 记住当前 app_id if (state) { sessionStorage.setItem(ssoStateStorageKey(appId), state); } else { sessionStorage.removeItem(ssoStateStorageKey(appId)); } } }, [appId, appUrl, state]); // 获取租户简称列表(与 uirefbase 一致) useEffect(() => { fetch(`${process.env.NEXT_PUBLIC_BASE_PATH || ""}/api/front/getAbbrList`, { method: "GET", headers: { "Content-type": "application/json" }, }) .then((response) => response.json()) .then((res) => { setAddrMap(res?.data?.keys || {}); }) .catch(() => {}); }, []); // 生成浏览器指纹 useEffect(() => { const ua = navigator.userAgent; const screenInfo = `${screen.width}x${screen.height}`; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const raw = `${ua}|${screenInfo}|${timezone}`; let hash = 0; for (let i = 0; i < raw.length; i++) { hash = (hash << 5) - hash + raw.charCodeAt(i); hash |= 0; } setFingerprint(Math.abs(hash).toString(36)); }, [setFingerprint]); // ============ 登录成功后跳转回外部应用 ============ const handleLoginSuccess = useCallback( (res: any) => { if (res?.error) { setError(res.error || "登录失败,请重试"); return; } // if (!res?.user_info?.account) { // setError('登录失败:后端未返回有效用户信息') // return // } // 从 localStorage 获取外部应用地址 const storedAppId = appId || localStorage.getItem("sso_app_id") || ""; const targetUrl = appUrl || (storedAppId ? localStorage.getItem(storedAppId) || "" : ""); const callbackState = (appId && appUrl ? state : "") || (storedAppId ? sessionStorage.getItem(ssoStateStorageKey(storedAppId)) || "" : ""); if (res?.code && targetUrl) { try { const callbackUrl = new URL(targetUrl); callbackUrl.searchParams.set("sso_code", res.code); if (callbackState) { callbackUrl.searchParams.set("state", callbackState); } if (storedAppId) { sessionStorage.removeItem(ssoStateStorageKey(storedAppId)); } window.location.href = callbackUrl.toString(); } catch { setError("外部应用回调地址(app_url)格式无效,无法完成 SSO 跳转"); } return; } // 无外部应用地址 — sso-portal 不提供本地登录 setError("缺少外部应用回调地址(app_url),无法完成 SSO 跳转"); }, [appId, appUrl, state], ); // ============ 统一调用 /api/login ============ // SSO 参数优先取 URL,其次取 localStorage(飞书 OAuth 回调后 URL 参数丢失) const postLogin = async (payload: object) => { const effectiveAppId = appId || localStorage.getItem("sso_app_id") || ""; const effectiveAppUrl = appUrl || (effectiveAppId ? localStorage.getItem(effectiveAppId) || "" : ""); const res = await fetch( `${process.env.NEXT_PUBLIC_BASE_PATH || ""}/api/login`, { method: "POST", body: JSON.stringify( encrypt( JSON.stringify({ ...payload, fingerprint, ...(effectiveAppId ? { client_id: effectiveAppId } : {}), ...(effectiveAppUrl ? { client_redirect_uri: effectiveAppUrl } : {}), }), ), ), }, ) .then((r) => r.text()) .then((t) => JSON.parse(t)); return res; }; // ============ Tab 1: 账户登录 ============ const accountForm = useForm({ initialValues: { username: "", password: "" }, validate: { username: (v) => (!v ? "账户名不能为空" : null), password: (v) => (!v ? "密码不能为空" : null), }, validateInputOnChange: true, validateInputOnBlur: true, clearInputErrorOnChange: true, }); const handleAccountSubmit = accountForm.onSubmit(async (values) => { setLoading(true); setError(null); try { const res = await postLogin({ account: values.username, password: values.password, abbr: fixedTenant, type: "account", }); handleLoginSuccess(res); } catch (err) { setError(err instanceof Error ? err.message : "登录失败,请重试"); } finally { setLoading(false); } }); // ============ Tab 2: 手机号登录 ============ const [codeTime, setCodeTime] = useState(0); const handleCountDown = () => { setCodeTime(60); const timer = setInterval(() => { setCodeTime((t) => { if (t <= 1) { clearInterval(timer); return 0; } return t - 1; }); }, 1000); }; const handleGetCode = async () => { if (!validatePhoneNumber(phoneForm.values.phone)) { setError("请输入正确的手机号"); return; } try { const tntkey = addrMap?.[fixedTenant] || ""; if (!tntkey) { showErrorMsg("请输入正确的租户"); return; } const res: any = await fetch( `${process.env.NEXT_PUBLIC_BASE_PATH || ""}/api/getVerifyCode`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: phoneForm.values.phone.startsWith("+86") ? phoneForm.values.phone : `+86${phoneForm.values.phone}`, tntkey, }), }, ).then((r) => r.json()); if (res?.code === 200) { showSuccessMsg("验证码已发送, 请注意查收"); handleCountDown(); } else { showErrorMsg(res?.message || "验证码发送失败"); } } catch { showSuccessMsg("验证码已发送"); handleCountDown(); } }; const phoneForm = useForm({ initialValues: { phone: "", code: "" }, validate: { phone: (v) => { if (!v) return "手机号不能为空"; if (v.length !== 11) return "手机号必须为11位"; return null; }, code: (v) => { if (!v) return "验证码不能为空"; if (v.length !== 6) return "验证码必须为6位"; return null; }, }, validateInputOnChange: true, validateInputOnBlur: true, clearInputErrorOnChange: true, }); const handlePhoneSubmit = phoneForm.onSubmit(async (values) => { setLoading(true); setError(null); try { const tntkey = addrMap?.[fixedTenant] || ""; if (!tntkey) { setError("请输入正确的租户"); setLoading(false); return; } const res = await postLogin({ phone: values.phone.startsWith("+86") ? values.phone : `+86${values.phone}`, verify_code: values.code, tntkey, type: "phone", }); handleLoginSuccess(res); } catch (err) { setError(err instanceof Error ? err.message : "登录失败,请重试"); } finally { setLoading(false); } }); // ============ 错误提示 ============ useEffect(() => { if (error) showErrorMsg(error); }, [error]); const bgImage = useMemo( () => (colorScheme === "dark" ? DarkBgImage : LightBgImage), [colorScheme], ); const stackBg2Image = useMemo( () => (colorScheme === "dark" ? loginBg2Dark : loginBg2Light), [colorScheme], ); // 飞书 OAuth 回调中:显示 loading,不渲染表单 const isFeishuCallback = isClient && !!getQueryVariable("code"); if (isFeishuCallback) { return ( 正在验证飞书登录... {/* QrLogin 仍在后台执行 feishuLogin */} ); } return ( {/* 左侧装饰区域 */} {/* 右侧登录表单 */} {headerTitle} {/* 缺少 SSO 参数时顶部提示;有参数时显示来源 */} {missingSsoParams ? ( } > 缺少 SSO 参数(app_id / app_url),无法登录。
请从外部应用跳转到此页面。
) : ( appId && ( 正在为应用 {appId} 进行登录认证 ) )} 账户登录 手机号登录 {/* TODO: 暂时隐藏飞书扫码 tab,恢复时取消下方注释 + 同步恢复 Tabs.Panel value="feishu" */} {/* 飞书扫码 */} {/* 账户登录 */}
{/* 手机号登录 */}
{/* 飞书扫码(真实二维码) - 暂时隐藏 */} {false && ( {isClient && } )}
); }