imeeting/frontend/src/pages/auth/login/index.tsx

240 lines
8.4 KiB
TypeScript

import { App, Button, Checkbox, Form, Input, Typography } from "antd";
import { LockOutlined, ReloadOutlined, SafetyOutlined, UserOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getCurrentUser, getOpenPlatformConfig, getSystemParamValue } from "@/api";
import { fetchCaptcha, login, type CaptchaResponse } from "@/api/auth";
import type { SysPlatformConfig } from "@/types";
import "./index.less";
const { Title, Text, Link } = Typography;
type LoginFormValues = {
username: string;
password: string;
captchaCode?: string;
remember?: boolean;
tenantCode?: string;
};
export default function Login() {
const { t } = useTranslation();
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
const [captchaEnabled, setCaptchaEnabled] = useState(true);
const [loading, setLoading] = useState(false);
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
const [form] = Form.useForm<LoginFormValues>();
const { message } = App.useApp();
const loadCaptcha = useCallback(async () => {
if (!captchaEnabled) {
return;
}
const data = await fetchCaptcha();
setCaptcha(data);
}, [captchaEnabled]);
useEffect(() => {
const init = async () => {
try {
const [captchaValue, config] = await Promise.all([
getSystemParamValue("security.captcha.enabled", "true"),
getOpenPlatformConfig()
]);
setPlatformConfig(config);
const enabled = captchaValue !== "false";
setCaptchaEnabled(enabled);
if (enabled) {
await loadCaptcha();
}
} catch {
setCaptchaEnabled(true);
await loadCaptcha();
}
};
init();
}, [loadCaptcha]);
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.get("timeout") === "1") {
message.warning(t("login.loginTimeout"));
window.history.replaceState({}, document.title, window.location.pathname);
}
}, [t]);
const onFinish = async (values: LoginFormValues) => {
setLoading(true);
try {
const data = await login({
username: values.username,
password: values.password,
tenantCode: values.tenantCode,
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
captchaCode: captchaEnabled ? values.captchaCode : undefined
});
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken);
localStorage.setItem("username", values.username);
if (data.availableTenants) {
localStorage.setItem("availableTenants", JSON.stringify(data.availableTenants));
const payload = JSON.parse(atob(data.accessToken.split(".")[1]));
localStorage.setItem("activeTenantId", String(payload.tenantId));
}
try {
const profile = await getCurrentUser();
sessionStorage.setItem("userProfile", JSON.stringify(profile));
localStorage.setItem("displayName", profile.displayName || profile.username || values.username);
localStorage.setItem("username", profile.username || values.username);
} catch {
sessionStorage.removeItem("userProfile");
localStorage.removeItem("displayName");
}
message.success(t("common.success"));
window.location.href = "/";
} catch {
if (captchaEnabled) {
await loadCaptcha();
}
} finally {
setLoading(false);
}
};
const loginStyle = platformConfig?.loginBgUrl
? {
backgroundImage: `url(${platformConfig.loginBgUrl})`,
backgroundSize: "cover",
backgroundPosition: "center",
position: "relative" as const
}
: {};
const leftStyle = platformConfig?.loginBgUrl
? {
...loginStyle,
background: "rgba(255, 255, 255, 0.2)",
backdropFilter: "blur(10px)"
}
: {};
const rightStyle = platformConfig?.loginBgUrl
? {
background: "rgba(255, 255, 255, 0.85)",
backdropFilter: "blur(20px)"
}
: {};
return (
<div className="login-page" style={loginStyle}>
<div className="login-left" style={leftStyle}>
<div className="login-brand">
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="Logo" className="brand-logo-img" />
<span className="brand-name">{platformConfig?.projectName || "UnisBase"}</span>
</div>
<div className="login-hero">
<h1 className="hero-title">
{t("login.heroTitle1")}
<br />
<span className="hero-accent">{t("login.heroTitle2")}</span>
<br />
{t("login.heroTitle3")}
</h1>
<p className="hero-desc">{platformConfig?.systemDescription || t("login.heroDesc")}</p>
</div>
<div className="login-left-footer">
<div className="footer-item">{t("login.enterpriseSecurity")}</div>
<div className="footer-divider" aria-hidden="true" />
<div className="footer-item">{t("login.multiLang")}</div>
{platformConfig?.icpInfo ? (
<>
<div className="footer-divider" aria-hidden="true" />
<div className="footer-item">{platformConfig.icpInfo}</div>
</>
) : null}
</div>
</div>
<div className="login-right" style={rightStyle}>
<div className="login-container">
<div className="login-header">
<Title level={2}>{t("login.welcome")}</Title>
<Text type="secondary">{t("login.subtitle")}</Text>
</div>
<Form form={form} layout="vertical" onFinish={onFinish} className="login-form" requiredMark={false} autoComplete="off">
<Form.Item name="username" rules={[{ required: true, message: t("login.username") }]}>
<Input
size="large"
prefix={<UserOutlined className="text-gray-400" aria-hidden="true" />}
placeholder={t("login.username")}
autoComplete="username"
spellCheck={false}
aria-label={t("login.username")}
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: t("login.password") }]}>
<Input.Password
size="large"
prefix={<LockOutlined className="text-gray-400" aria-hidden="true" />}
placeholder={t("login.password")}
autoComplete="current-password"
aria-label={t("login.password")}
/>
</Form.Item>
{captchaEnabled ? (
<Form.Item name="captchaCode" rules={[{ required: true, message: t("login.captcha") }]}>
<div className="captcha-wrapper">
<Input
size="large"
prefix={<SafetyOutlined className="text-gray-400" aria-hidden="true" />}
placeholder={t("login.captcha")}
maxLength={6}
aria-label={t("login.captcha")}
/>
<Button
className="captcha-image-btn"
onClick={() => void loadCaptcha()}
icon={!captcha ? <ReloadOutlined spin /> : null}
aria-label="刷新验证码"
>
{captcha ? <img src={captcha.imageBase64} alt="验证码" /> : null}
</Button>
</div>
</Form.Item>
) : null}
<div className="login-extra">
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>{t("login.rememberMe")}</Checkbox>
</Form.Item>
<Link className="forgot-password">{t("login.forgotPassword")}</Link>
</div>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block size="large" className="login-submit-btn">
{loading ? t("login.loggingIn") : t("login.submit")}
</Button>
</Form.Item>
</Form>
<div className="login-footer">
<Text type="secondary">
{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}
<Text strong className="tabular-nums">123456</Text>
</Text>
</div>
</div>
</div>
</div>
);
}