2026-02-12 05:43:59 +00:00
|
|
|
|
import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
|
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
2026-02-10 09:48:44 +00:00
|
|
|
|
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
2026-02-11 05:44:31 +00:00
|
|
|
|
import { getCurrentUser, getSystemParamValue } from "../api";
|
2026-02-12 07:51:03 +00:00
|
|
|
|
import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined, ShopOutlined } from "@ant-design/icons";
|
2026-02-10 09:48:44 +00:00
|
|
|
|
import "./Login.css";
|
|
|
|
|
|
|
|
|
|
|
|
const { Title, Text, Link } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
export default function Login() {
|
|
|
|
|
|
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const [captchaEnabled, setCaptchaEnabled] = useState(true);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
2026-02-12 05:43:59 +00:00
|
|
|
|
const loadCaptcha = useCallback(async () => {
|
2026-02-11 05:44:31 +00:00
|
|
|
|
if (!captchaEnabled) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchCaptcha();
|
|
|
|
|
|
setCaptcha(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
message.error("加载验证码失败");
|
|
|
|
|
|
}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
}, [captchaEnabled]);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const init = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const value = await getSystemParamValue("security.captcha.enabled", "true");
|
|
|
|
|
|
const enabled = value !== "false";
|
|
|
|
|
|
setCaptchaEnabled(enabled);
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
loadCaptcha();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setCaptchaEnabled(true);
|
|
|
|
|
|
loadCaptcha();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
init();
|
2026-02-12 05:43:59 +00:00
|
|
|
|
}, [loadCaptcha]);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
|
|
|
|
|
|
const onFinish = async (values: any) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await login({
|
|
|
|
|
|
username: values.username,
|
|
|
|
|
|
password: values.password,
|
2026-02-12 07:51:03 +00:00
|
|
|
|
tenantCode: values.tenantCode,
|
2026-02-11 05:44:31 +00:00
|
|
|
|
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
|
|
|
|
|
captchaCode: captchaEnabled ? values.captchaCode : undefined
|
2026-02-10 09:48:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
localStorage.setItem("accessToken", data.accessToken);
|
|
|
|
|
|
localStorage.setItem("refreshToken", data.refreshToken);
|
|
|
|
|
|
localStorage.setItem("username", values.username);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const profile = await getCurrentUser();
|
|
|
|
|
|
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
sessionStorage.removeItem("userProfile");
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success("登录成功");
|
|
|
|
|
|
window.location.href = "/";
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message || "登录失败");
|
2026-02-11 05:44:31 +00:00
|
|
|
|
if (captchaEnabled) {
|
|
|
|
|
|
loadCaptcha();
|
|
|
|
|
|
}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="login-page">
|
|
|
|
|
|
<div className="login-left">
|
|
|
|
|
|
<div className="login-brand">
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<img src="/logo.svg" alt="MeetingAI Logo" className="brand-logo-img" />
|
2026-02-10 09:48:44 +00:00
|
|
|
|
<span className="brand-name">MeetingAI</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="login-hero">
|
|
|
|
|
|
<h1 className="hero-title">
|
|
|
|
|
|
智能会议<br />
|
|
|
|
|
|
<span className="hero-accent">实时语音处理</span><br />
|
|
|
|
|
|
系统
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="hero-desc">
|
|
|
|
|
|
全流程自动化会议记录,声纹识别与智能摘要<br />
|
|
|
|
|
|
提升团队协作效率的新一代解决方案。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="login-left-footer">
|
|
|
|
|
|
<div className="footer-item">企业级安全</div>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<div className="footer-divider" aria-hidden="true" />
|
2026-02-10 09:48:44 +00:00
|
|
|
|
<div className="footer-item">多语言支持</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="login-right">
|
|
|
|
|
|
<div className="login-container">
|
|
|
|
|
|
<div className="login-header">
|
|
|
|
|
|
<Title level={2}>账号登录</Title>
|
|
|
|
|
|
<Text type="secondary">欢迎回来,请登录您的账号</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={onFinish}
|
|
|
|
|
|
className="login-form"
|
|
|
|
|
|
requiredMark={false}
|
2026-02-12 05:43:59 +00:00
|
|
|
|
autoComplete="off"
|
2026-02-10 09:48:44 +00:00
|
|
|
|
>
|
2026-02-12 07:51:03 +00:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="tenantCode"
|
|
|
|
|
|
rules={[{ required: false }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
prefix={<ShopOutlined className="text-gray-400" aria-hidden="true" />}
|
|
|
|
|
|
placeholder="租户编码 (平台管理可留空)"
|
|
|
|
|
|
aria-label="租户编码"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-02-10 09:48:44 +00:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="username"
|
|
|
|
|
|
rules={[{ required: true, message: "请输入用户名" }]}
|
|
|
|
|
|
>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Input
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
prefix={<UserOutlined className="text-gray-400" aria-hidden="true" />}
|
|
|
|
|
|
placeholder="用户名"
|
|
|
|
|
|
autoComplete="username"
|
|
|
|
|
|
spellCheck={false}
|
|
|
|
|
|
aria-label="用户名"
|
|
|
|
|
|
/>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="password"
|
|
|
|
|
|
rules={[{ required: true, message: "请输入密码" }]}
|
|
|
|
|
|
>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Input.Password
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
prefix={<LockOutlined className="text-gray-400" aria-hidden="true" />}
|
|
|
|
|
|
placeholder="密码"
|
|
|
|
|
|
autoComplete="current-password"
|
|
|
|
|
|
aria-label="密码"
|
|
|
|
|
|
/>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-02-11 05:44:31 +00:00
|
|
|
|
{captchaEnabled && (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="captchaCode"
|
|
|
|
|
|
rules={[{ required: true, message: "请输入验证码" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="captcha-wrapper">
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Input
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
prefix={<SafetyOutlined className="text-gray-400" aria-hidden="true" />}
|
|
|
|
|
|
placeholder="验证码"
|
|
|
|
|
|
maxLength={6}
|
|
|
|
|
|
aria-label="验证码"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="captcha-image-btn"
|
|
|
|
|
|
onClick={loadCaptcha}
|
|
|
|
|
|
icon={!captcha ? <ReloadOutlined spin /> : null}
|
|
|
|
|
|
aria-label="点击刷新验证码"
|
|
|
|
|
|
>
|
|
|
|
|
|
{captcha && <img src={captcha.imageBase64} alt="验证码图片" />}
|
|
|
|
|
|
</Button>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</div>
|
2026-02-11 05:44:31 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
|
|
|
|
|
|
<div className="login-extra">
|
|
|
|
|
|
<Form.Item name="remember" valuePropName="checked" noStyle>
|
|
|
|
|
|
<Checkbox>记住我</Checkbox>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Link className="forgot-password">忘记密码?</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
2026-02-12 05:43:59 +00:00
|
|
|
|
<Button type="primary" htmlType="submit" loading={loading} block size="large" className="login-submit-btn">
|
|
|
|
|
|
{loading ? "登录中…" : "立即登录"}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="login-footer">
|
|
|
|
|
|
<Text type="secondary">
|
2026-02-12 05:43:59 +00:00
|
|
|
|
演示账号:<Text strong className="tabular-nums">admin</Text> / 密码:<Text strong className="tabular-nums">123456</Text>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|