| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031 |
- import React, { useState, useEffect } from 'react';
- import { useNavigate, useSearchParams } from 'react-router-dom';
- import { useTranslation } from 'react-i18next';
- import { Shield, Zap, Lock, User, Eye, EyeOff, ArrowRight, Activity, Radio, Layers, Building2, Globe } from 'lucide-react';
- import { loginApi } from '../api';
- import type { LoginFormParams } from '../api/Login';
- import { toast } from 'sonner';
- import { env } from '../config/env';
- import * as authUtil from '../utils/auth';
- import { setPermissionInfo } from '../utils/permission';
- export default function Login() {
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const { t, i18n } = useTranslation();
-
- // 从环境变量获取验证码和租户配置
- const captchaEnable = ((import.meta as any).env?.VITE_APP_CAPTCHA_ENABLE as string) === 'true';
- const tenantEnable = env.tenantEnable;
-
- // 获取 redirect 参数
- const redirect = searchParams.get('redirect') || '';
-
- const [tenant, setTenant] = useState(env.defaultLogin.tenant);
- const [username, setUsername] = useState(env.defaultLogin.username);
- const [password, setPassword] = useState(env.defaultLogin.password);
- const [showPassword, setShowPassword] = useState(false);
- const [rememberMe, setRememberMe] = useState(true); // 默认记住我
- const [isLoading, setIsLoading] = useState(false);
- const [showResetPassword, setShowResetPassword] = useState(false);
- const [captchaVerification, setCaptchaVerification] = useState('');
- // 重置密码相关状态
- const [resetTenantName, setResetTenantName] = useState('');
- const [resetMobile, setResetMobile] = useState('');
- const [verificationCode, setVerificationCode] = useState('');
- const [newPassword, setNewPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [showNewPassword, setShowNewPassword] = useState(false);
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
- const [mobileCodeTimer, setMobileCodeTimer] = useState(0); // 验证码倒计时
- const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
- // 切换语言
- const toggleLanguage = () => {
- const newLang = i18n.language === 'zh' ? 'en' : 'zh';
- i18n.changeLanguage(newLang);
- };
- // 密码强度计算
- const calculatePasswordStrength = (pwd: string) => {
- let strength = 0;
- if (pwd.length >= 6) strength++;
- if (pwd.length >= 10) strength++;
- if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++;
- if (/\d/.test(pwd)) strength++;
- if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) strength++;
- return strength;
- };
- const getPasswordStrengthText = (strength: number) => {
- if (strength === 0) return { text: '', color: '' };
- if (strength <= 2) return { text: t('reset.weak'), color: 'text-red-600' };
- if (strength <= 3) return { text: t('reset.medium'), color: 'text-orange-600' };
- return { text: t('reset.strong'), color: 'text-green-600' };
- };
- const getPasswordStrengthBars = (strength: number) => {
- return (
- <div className="flex gap-1 mt-2">
- {[1, 2, 3, 4, 5].map((i) => (
- <div
- key={i}
- className={`h-1 flex-1 rounded-full transition-all ${
- i <= strength
- ? strength <= 2
- ? 'bg-red-500'
- : strength <= 3
- ? 'bg-orange-500'
- : 'bg-green-500'
- : 'bg-gray-200'
- }`}
- />
- ))}
- </div>
- );
- };
- const newPasswordStrength = calculatePasswordStrength(newPassword);
- const confirmPasswordStrength = calculatePasswordStrength(confirmPassword);
- const newPasswordStrengthInfo = getPasswordStrengthText(newPasswordStrength);
- const confirmPasswordStrengthInfo = getPasswordStrengthText(confirmPasswordStrength);
- // 获取租户ID
- const getTenantId = async () => {
- if (tenantEnable) {
- if (!tenant || !tenant.trim()) {
- throw new Error(t('login.tenant') || '请输入租户名称');
- }
- try {
- const tenantId = await loginApi.getTenantIdByName(tenant.trim());
- // axios 拦截器已经处理了响应,直接使用数据
- if (!tenantId) {
- throw new Error(t('login.invalidTenantName') || '无效的租户名称');
- }
- authUtil.setTenantId(String(tenantId));
- return tenantId;
- } catch (error: any) {
- console.error('获取租户ID失败:', error);
- const errorMessage = error?.message || t('login.getTenantIdFailed') || '获取租户ID失败';
- toast.error(errorMessage);
- throw error;
- }
- }
- return null;
- };
- // 根据域名获取租户信息
- const getTenantByWebsite = async () => {
- try {
- const website = window.location.host;
- const tenantInfo = await loginApi.getTenantByWebsite(website) as any;
- if (tenantInfo && tenantInfo.name) {
- setTenant(tenantInfo.name);
- authUtil.setTenantId(tenantInfo.id);
- }
- } catch (error) {
- // 静默失败,不影响登录流程
- console.log('根据域名获取租户信息失败:', error);
- }
- };
- // 获取登录表单缓存
- const getLoginFormCache = () => {
- const loginForm = authUtil.getLoginForm();
- if (loginForm) {
- if (loginForm.username) setUsername(loginForm.username);
- if (loginForm.password) setPassword(loginForm.password);
- if (loginForm.tenantName) setTenant(loginForm.tenantName);
- if (loginForm.rememberMe !== undefined) setRememberMe(loginForm.rememberMe);
- }
- };
- // 表单验证
- const validateForm = (): boolean => {
- if (!username.trim()) {
- toast.error(t('login.usernamePlaceholder') || '请输入用户名');
- return false;
- }
- if (!password.trim()) {
- toast.error(t('login.passwordPlaceholder') || '请输入密码');
- return false;
- }
- if (tenantEnable && !tenant.trim()) {
- toast.error(t('login.tenant') || '请输入租户名称');
- return false;
- }
- return true;
- };
- // 获取验证码(如果需要)
- const getCode = async () => {
- if (!validateForm()) {
- return;
- }
- // 如果未启用验证码,直接登录
- if (!captchaEnable) {
- await handleLogin({});
- } else {
- // 如果启用验证码,这里需要集成验证码组件
- // 暂时直接调用登录,实际项目中需要添加验证码组件
- toast.info('验证码功能需要集成验证码组件');
- await handleLogin({ captchaVerification: captchaVerification });
- }
- };
- // 登录处理
- const handleLogin = async (params: { captchaVerification?: string } = {}) => {
- setIsLoading(true);
-
- try {
- // 表单验证
- if (!validateForm()) {
- setIsLoading(false);
- return;
- }
- // 先获取租户ID(如果启用租户功能,必须在登录前获取)
- await getTenantId();
- // 构建登录参数
- const loginParams: LoginFormParams = {
- username: username.trim(),
- password: password.trim(),
- rememberMe: rememberMe,
- };
- // 如果启用租户功能,添加租户名称
- if (tenantEnable && tenant) {
- loginParams.tenantName = tenant.trim();
- }
- // 如果启用验证码,添加验证码
- if (captchaEnable && params.captchaVerification) {
- loginParams.captchaVerification = params.captchaVerification;
- }
- // 调用登录API
- const res = await loginApi.login(loginParams) as any;
- // 存储用户ID和Token
- if (res && res.userId) {
- authUtil.setUserId(String(res.userId));
- }
-
- // 存储Token(兼容多种格式)
- const token = res?.accessToken || res?.token;
- if (token) {
- authUtil.setToken(res);
- // 存储ACCESS_TOKEN
- authUtil.setAccessToken(token);
- }
- // 存储REFRESH_TOKEN
- if (res?.refreshToken) {
- authUtil.setRefreshToken(res.refreshToken);
- }
- // 存储用户信息
- if (res?.user) {
- authUtil.setUser(res.user);
- }
- if (!res) {
- return;
- }
- // 处理记住我功能
- if (rememberMe) {
- authUtil.setLoginForm({
- username: loginParams.username,
- password: loginParams.password,
- tenantName: loginParams.tenantName,
- rememberMe: true,
- });
- } else {
- authUtil.removeLoginForm();
- }
- // 显示加载提示
- toast.loading('正在加载系统中...', { id: 'loading' });
- // 登录成功后调用其他接口
- try {
- // 并行调用所有接口
- const [
- homePageData,
- systemAttribute,
- unreadCount,
- myAgent,
- permissionInfo,
- dictData
- ] = await Promise.allSettled([
- loginApi.getHomePage(),
- loginApi.getSystemAttributeByKey('sys.websocket.address'),
- loginApi.getUnreadCount(),
- loginApi.getMyAgent({ pageNum: 1, pageSize: -1, queryType: 0, startTime: '', endTime: '' }),
- loginApi.getInfo(),
- loginApi.getDictDataSimpleList()
- ]);
- // 存储权限信息(包含roleRouters和完整权限数据)
- if (permissionInfo.status === 'fulfilled' && permissionInfo.value) {
- const permissionData = permissionInfo.value as any;
- if (permissionData.roleRouters) {
- authUtil.setRoleRouters(permissionData.roleRouters);
- }
- // 存储完整的权限信息(user, roles, permissions, menus)
- // 注意:axios 拦截器已经提取了 data 字段,所以 permissionData 就是 data 的内容
- if (permissionData.user || permissionData.roles || permissionData.permissions || permissionData.menus) {
- setPermissionInfo(permissionData);
- }
- }
- // 存储其他数据(如果需要)
- // homePageData, systemAttribute, unreadCount, myAgent, dictData 可以根据需要存储
- } catch (error) {
- console.error('加载系统数据失败:', error);
- // 即使这些接口失败,也不影响登录流程
- }
- // 存储语言设置(如果还没有)
- if (!authUtil.getLang()) {
- authUtil.setLang(i18n.language || 'zh');
- }
- // 存储暗色模式设置(如果还没有)
- if (localStorage.getItem('isDark') === null) {
- authUtil.setIsDark(false);
- }
- // 处理重定向
- let redirectPath = redirect || '/dashboard';
-
- // 判断是否为SSO登录
- if (redirectPath.indexOf('sso') !== -1) {
- window.location.href = window.location.href.replace('/login?redirect=', '');
- return;
- }
- // 延迟一下再跳转,让用户看到加载提示
- setTimeout(() => {
- toast.dismiss('loading');
- toast.success(t('common.success') || '登录成功');
- navigate(redirectPath);
- }, 500);
- } catch (error: any) {
- toast.dismiss('loading');
- toast.error(error.message || t('common.error') || '登录失败');
- } finally {
- setIsLoading(false);
- }
- };
- // 处理表单提交
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- await getCode();
- };
- // 组件挂载时执行
- useEffect(() => {
- getLoginFormCache();
- getTenantByWebsite();
- }, []);
- // 清理定时器
- useEffect(() => {
- return () => {
- // 组件卸载时清理定时器(通过检查 timer 状态)
- if (mobileCodeTimer > 0) {
- setMobileCodeTimer(0);
- }
- };
- }, [mobileCodeTimer]);
- // 获取租户ID(用于重置密码)
- const getTenantIdForReset = async () => {
- if (tenantEnable && resetTenantName) {
- try {
- const tenantId = await loginApi.getTenantIdByName(resetTenantName);
- if (!tenantId) {
- toast.error(t('login.invalidTenantName') || '无效的租户名称');
- throw new Error('无效的租户名称');
- }
- authUtil.setTenantId(String(tenantId));
- } catch (error: any) {
- console.error('获取租户ID失败:', error);
- throw error;
- }
- }
- };
- // 获取验证码(重置密码用)
- const getCodeForReset = async () => {
- // 验证手机号
- if (!resetMobile || resetMobile.length !== 11) {
- toast.error(t('login.mobileNumberPlaceholder') || '请输入正确的手机号');
- return;
- }
- // 如果启用租户,验证租户名
- if (tenantEnable && !resetTenantName) {
- toast.error(t('login.tenantNamePlaceholder') || '请输入租户名称');
- return;
- }
- // 如果未启用验证码,直接发送短信验证码
- if (!captchaEnable) {
- await getSmsCode({});
- } else {
- // 如果启用验证码,这里需要集成验证码组件
- // 暂时直接调用发送验证码,实际项目中需要添加验证码组件
- toast.info('验证码功能需要集成验证码组件');
- await getSmsCode({ captchaVerification: captchaVerification });
- }
- };
- // 发送短信验证码
- const getSmsCode = async (params: { captchaVerification?: string } = {}) => {
- try {
- // 如果启用租户,先获取租户ID
- if (tenantEnable && resetTenantName) {
- await getTenantIdForReset();
- }
- const smsParams: any = {
- mobile: resetMobile,
- scene: 23, // 重置密码场景
- };
- if (tenantEnable && resetTenantName) {
- smsParams.tenantName = resetTenantName;
- }
- if (params.captchaVerification) {
- smsParams.captchaVerification = params.captchaVerification;
- }
- await loginApi.sendSmsCode(smsParams);
-
- toast.success(t('login.SmsSendMsg') || '验证码已发送');
-
- // 设置倒计时
- setMobileCodeTimer(60);
- const timer = setInterval(() => {
- setMobileCodeTimer((prev) => {
- if (prev <= 1) {
- clearInterval(timer);
- return 0;
- }
- return prev - 1;
- });
- }, 1000);
- } catch (error: any) {
- toast.error(error.message || t('common.error') || '发送验证码失败');
- }
- };
- // 重置密码
- const handleResetPassword = async (e: React.FormEvent) => {
- e.preventDefault();
-
- // 表单验证
- if (!resetMobile || resetMobile.length !== 11) {
- toast.error(t('login.mobileNumberPlaceholder') || '请输入正确的手机号');
- return;
- }
- if (!verificationCode) {
- toast.error(t('login.codePlaceholder') || '请输入验证码');
- return;
- }
- if (!newPassword || newPassword.length < 4 || newPassword.length > 16) {
- toast.error('密码长度为4到16位');
- return;
- }
- if (newPassword !== confirmPassword) {
- toast.error(t('reset.passwordMismatch') || '两次输入密码不一致');
- return;
- }
- if (tenantEnable && !resetTenantName) {
- toast.error(t('login.tenantNamePlaceholder') || '请输入租户名称');
- return;
- }
- setResetPasswordLoading(true);
- try {
- // 获取租户ID
- await getTenantIdForReset();
- // 调用重置密码接口
- const resetParams: any = {
- mobile: resetMobile,
- code: verificationCode,
- password: newPassword,
- };
- if (tenantEnable && resetTenantName) {
- resetParams.tenantName = resetTenantName;
- }
- await loginApi.smsResetPassword(resetParams);
-
- toast.success(t('login.resetPasswordSuccess') || '密码重置成功');
- setShowResetPassword(false);
-
- // 重置表单
- setResetTenantName('');
- setResetMobile('');
- setVerificationCode('');
- setNewPassword('');
- setConfirmPassword('');
- setMobileCodeTimer(0);
- } catch (error: any) {
- toast.error(error.message || t('common.error') || '重置密码失败');
- } finally {
- setResetPasswordLoading(false);
- }
- };
- return (
- <div className="min-h-screen h-screen relative overflow-hidden bg-white">
- {/* 语言切换按钮 */}
- <div className="absolute top-6 right-6 z-50">
- <button
- onClick={toggleLanguage}
- className="flex items-center gap-2 px-4 py-2.5 bg-white/90 backdrop-blur-sm border border-gray-200/50 rounded-xl shadow-lg hover:shadow-xl hover:bg-white transition-all group"
- >
- <Globe className="w-4 h-4 text-blue-600 group-hover:rotate-12 transition-transform" />
- <span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
- </button>
- </div>
- {/* 太极式背景分割 */}
- <div className="absolute inset-0">
- <svg className="absolute inset-0 w-full h-full" preserveAspectRatio="none">
- <defs>
- <linearGradient id="leftGradient" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" stopColor="#e8edff" />
- <stop offset="50%" stopColor="#c4d1fe" />
- <stop offset="100%" stopColor="#b8c5f9" />
- </linearGradient>
- <linearGradient id="rightGradient" x1="0%" y1="0%" x2="100%" y2="0%">
- <stop offset="0%" stopColor="#f8fafc" />
- <stop offset="100%" stopColor="#ffffff" />
- </linearGradient>
- <filter id="glow">
- <feGaussianBlur stdDeviation="6" result="coloredBlur"/>
- <feMerge>
- <feMergeNode in="coloredBlur"/>
- <feMergeNode in="SourceGraphic"/>
- </feMerge>
- </filter>
- </defs>
-
- <path
- d="M 0 0
- C 300 0, 600 150, 700 300
- S 850 500, 900 650
- S 750 850, 700 1000
- L 0 1000 Z"
- fill="url(#leftGradient)"
- />
-
- <path
- d="M 0 0
- C 300 0, 600 150, 700 300
- S 850 500, 900 650
- S 750 850, 700 1000
- L 1920 1000 L 1920 0 Z"
- fill="url(#rightGradient)"
- />
-
- <path
- d="M 700 0
- C 700 100, 750 200, 850 350
- S 900 550, 900 700
- S 800 900, 700 1000"
- fill="none"
- stroke="#c4d1fe"
- strokeWidth="2"
- opacity="0.5"
- filter="url(#glow)"
- />
- </svg>
-
- <div className="absolute top-[35%] left-[30%] w-32 h-32 bg-white/60 rounded-full blur-3xl"></div>
- <div className="absolute top-[65%] left-[32%] w-24 h-24 bg-blue-300/40 rounded-full blur-2xl"></div>
-
- <div className="absolute top-1/4 left-1/5 w-80 h-80 opacity-40 rounded-full blur-3xl animate-pulse" style={{ background: '#c4d1fe' }}></div>
- <div className="absolute bottom-1/3 left-1/6 w-64 h-64 opacity-30 rounded-full blur-3xl animate-pulse" style={{ background: '#b8c5f9', animationDelay: '1.5s' }}></div>
- </div>
- {/* 主内容 */}
- <div className="relative h-screen flex">
- {/* 左侧内容区 */}
- <div className="w-full lg:w-[58%] p-8 lg:px-16 lg:py-12 flex flex-col justify-between">
- <div>
- <div className="flex items-center gap-4 mb-12">
- <div className="relative">
- <div className="w-14 h-14 rounded-full flex items-center justify-center shadow-xl shadow-blue-400/40" style={{ background: '#4d79f8' }}>
- <Shield className="w-8 h-8 text-white" strokeWidth={2.5} />
- </div>
- <div className="absolute -top-1 -right-1 w-4 h-4 bg-green-400 rounded-full border-2 border-white shadow-lg">
- <div className="w-full h-full bg-green-400 rounded-full animate-ping"></div>
- </div>
- </div>
- <div>
- <h1 className="text-2xl text-gray-900">{env.appTitle}</h1>
- <p className="text-xs text-blue-600 tracking-widest mt-0.5">{t('login.subtitle')}</p>
- </div>
- </div>
- </div>
- <div className="space-y-10 max-w-2xl">
- <div className="space-y-4">
- <div className="inline-block px-3 py-1.5 bg-blue-100/80 backdrop-blur-sm rounded-full text-xs text-blue-700">
- {t('login.newGeneration')}
- </div>
- <h2 className="text-5xl lg:text-6xl leading-tight">
- <span className="inline-block text-gray-900 hover:scale-105 transition-transform duration-300">
- {t('login.slogan1')}
- </span>
- <br />
- <span className="relative inline-block mt-2">
- <span className="bg-gradient-to-r from-blue-600 via-cyan-400 to-blue-600 bg-clip-text text-transparent bg-[length:200%_auto] animate-[gradient_3s_ease_infinite]" style={{ backgroundSize: '200% auto' }}>
- {t('login.slogan2')}
- </span>
- <div className="absolute -bottom-2 left-0 w-32 h-1 bg-gradient-to-r from-blue-400 via-cyan-400 to-transparent rounded-full"></div>
- </span>
- </h2>
- <p className="text-lg text-gray-600 leading-relaxed max-w-xl">
- {t('login.description')}
- </p>
- </div>
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
- <div className="group">
- <div className="bg-white/60 backdrop-blur-sm rounded-2xl p-5 border border-blue-100/50 hover:bg-white/80 hover:shadow-xl hover:shadow-blue-200/30 transition-all duration-300 hover:-translate-y-1">
- <div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-blue-500 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform shadow-lg shadow-blue-400/30">
- <Activity className="w-6 h-6 text-white" strokeWidth={2.5} />
- </div>
- <h3 className="text-base text-gray-900 mb-1">{t('login.feature1Title')}</h3>
- <p className="text-sm text-gray-600 leading-relaxed">
- {t('login.feature1Desc')}
- </p>
- </div>
- </div>
- <div className="group">
- <div className="bg-white/60 backdrop-blur-sm rounded-2xl p-5 border border-cyan-100/50 hover:bg-white/80 hover:shadow-xl hover:shadow-cyan-200/30 transition-all duration-300 hover:-translate-y-1">
- <div className="w-12 h-12 bg-gradient-to-br from-cyan-400 to-cyan-500 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform shadow-lg shadow-cyan-400/30">
- <Radio className="w-6 h-6 text-white" strokeWidth={2.5} />
- </div>
- <h3 className="text-base text-gray-900 mb-1">{t('login.feature2Title')}</h3>
- <p className="text-sm text-gray-600 leading-relaxed">
- {t('login.feature2Desc')}
- </p>
- </div>
- </div>
- <div className="group">
- <div className="bg-white/60 backdrop-blur-sm rounded-2xl p-5 border border-blue-100/50 hover:bg-white/80 hover:shadow-xl hover:shadow-blue-200/30 transition-all duration-300 hover:-translate-y-1">
- <div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform shadow-lg shadow-blue-500/30">
- <Layers className="w-6 h-6 text-white" strokeWidth={2.5} />
- </div>
- <h3 className="text-base text-gray-900 mb-1">{t('login.feature3Title')}</h3>
- <p className="text-sm text-gray-600 leading-relaxed">
- {t('login.feature3Desc')}
- </p>
- </div>
- </div>
- </div>
- <div className="bg-white/60 backdrop-blur-sm rounded-2xl p-6 border border-blue-100/50 shadow-xl shadow-blue-100/20">
- <div className="grid grid-cols-3 gap-6">
- <div>
- <div className="flex items-center gap-2 mb-2">
- <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
- <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.securityLevel')}</span>
- </div>
- <div className="text-xl text-gray-900">{t('login.enterpriseGrade')}</div>
- </div>
- <div>
- <div className="flex items-center gap-2 mb-2">
- <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div>
- <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.responseSpeed')}</span>
- </div>
- <div className="text-xl text-gray-900"><100 <span className="text-base text-gray-500">ms</span></div>
- </div>
- <div>
- <div className="flex items-center gap-2 mb-2">
- <div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
- <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.systemVersion')}</span>
- </div>
- <div className="text-xl text-gray-900">v1.0</div>
- </div>
- </div>
- </div>
- </div>
- <div className="text-xs text-gray-400 text-center">
- {t('login.copyright')}
- </div>
- </div>
- {/* 右侧登录区 */}
- <div className="hidden lg:flex lg:w-[42%] items-center justify-start pl-8 pr-16 py-12">
- <div className="w-full max-w-md">
- {!showResetPassword ? (
- <div className="space-y-6">
- <div className="flex justify-center mb-8">
- <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl shadow-2xl shadow-blue-400/50" style={{ background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)' }}>
- <Zap className="w-10 h-10 text-white" strokeWidth={2.5} />
- </div>
- </div>
- <form onSubmit={handleSubmit} className="space-y-5">
- {env.tenantEnable && (
- <div>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <Building2 className="w-5 h-5" />
- </div>
- <input
- type="text"
- placeholder={t('login.tenant')}
- value={tenant}
- onChange={(e) => setTenant(e.target.value)}
- className="w-full h-13 pl-12 pr-4 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- </div>
- </div>
- )}
- <div>
- <label className="block text-sm text-gray-700 mb-2 ml-1">
- {t('login.username')}
- </label>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <User className="w-5 h-5" />
- </div>
- <input
- type="text"
- placeholder={t('login.usernamePlaceholder')}
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- className="w-full h-13 pl-12 pr-4 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- </div>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2 ml-1">
- {t('login.password')}
- </label>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <Lock className="w-5 h-5" />
- </div>
- <input
- type={showPassword ? 'text' : 'password'}
- placeholder={t('login.passwordPlaceholder')}
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- className="w-full h-13 pl-12 pr-12 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- <button
- type="button"
- onClick={() => setShowPassword(!showPassword)}
- className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
- >
- {showPassword ? (
- <EyeOff className="w-5 h-5" />
- ) : (
- <Eye className="w-5 h-5" />
- )}
- </button>
- </div>
- </div>
- <div className="flex items-center justify-between pt-2">
- <label className="flex items-center gap-2 cursor-pointer group">
- <input
- type="checkbox"
- checked={rememberMe}
- onChange={(e) => setRememberMe(e.target.checked)}
- className="w-4 h-4 rounded border-2 border-gray-300 text-blue-500 focus:ring-2 focus:ring-blue-400/50"
- />
- <span className="text-sm text-gray-600 group-hover:text-gray-900 transition-colors">
- {t('login.rememberMe')}
- </span>
- </label>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowResetPassword(true);
- }}
- className="text-sm text-blue-600 hover:text-blue-700 hover:underline transition-colors"
- >
- {t('login.forgotPassword')}
- </a>
- </div>
- <button
- type="submit"
- disabled={isLoading}
- className="relative w-full h-13 text-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center gap-2 group disabled:opacity-70 disabled:cursor-not-allowed mt-8 overflow-hidden"
- style={{
- background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)',
- boxShadow: '0 10px 25px -5px rgba(77, 121, 248, 0.4)'
- }}
- >
- <span className="relative z-10">{isLoading ? t('login.loggingIn') : t('login.loginButton')}</span>
- {!isLoading && (
- <ArrowRight className="relative z-10 w-5 h-5 group-hover:translate-x-1 transition-transform" />
- )}
- <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
- </button>
- </form>
- <div className="mt-8 p-5 bg-gradient-to-br from-blue-50/80 to-cyan-50/80 backdrop-blur-sm border border-blue-100/60 rounded-2xl">
- <div className="flex items-start gap-3">
- <div className="w-10 h-10 bg-white/80 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
- <Shield className="w-5 h-5 text-blue-600" />
- </div>
- <div className="text-sm">
- <div className="text-gray-900 mb-1">{t('login.systemNotice')}</div>
- <div className="text-gray-600 text-xs leading-relaxed">
- {t('login.noticeContent')}
- </div>
- </div>
- </div>
- </div>
- <div className="text-center mt-6 text-sm text-gray-500">
- {t('login.help')}
- </div>
- </div>
- ) : (
- <div className="space-y-6">
- <div className="flex justify-center mb-8">
- <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl shadow-2xl shadow-blue-400/50" style={{ background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)' }}>
- <Lock className="w-10 h-10 text-white" strokeWidth={2.5} />
- </div>
- </div>
- <form onSubmit={handleResetPassword} className="space-y-5">
- {tenantEnable && (
- <div>
- <label className="block text-sm text-gray-700 mb-2 ml-1">
- {t('login.tenantNamePlaceholder') || '租户名称'}
- </label>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <Building2 className="w-5 h-5" />
- </div>
- <input
- type="text"
- placeholder={t('login.tenantNamePlaceholder') || '请输入租户名称'}
- value={resetTenantName}
- onChange={(e) => setResetTenantName(e.target.value)}
- className="w-full h-13 pl-12 pr-4 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required={tenantEnable}
- />
- </div>
- </div>
- )}
- <div>
- <label className="block text-sm text-gray-700 mb-2 ml-1">
- {t('login.mobileNumberPlaceholder') || '手机号'}
- </label>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <User className="w-5 h-5" />
- </div>
- <input
- type="tel"
- placeholder={t('login.mobileNumberPlaceholder') || '请输入手机号'}
- value={resetMobile}
- onChange={(e) => setResetMobile(e.target.value)}
- maxLength={11}
- className="w-full h-13 pl-12 pr-4 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- </div>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2 ml-1">
- {t('login.codePlaceholder') || '验证码'}
- </label>
- <div className="flex gap-2">
- <div className="relative flex-1">
- <input
- type="text"
- placeholder={t('login.codePlaceholder') || '请输入验证码'}
- value={verificationCode}
- onChange={(e) => setVerificationCode(e.target.value)}
- className="w-full h-13 px-4 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- </div>
- <button
- type="button"
- onClick={getCodeForReset}
- disabled={mobileCodeTimer > 0}
- className="px-5 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all text-sm whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {mobileCodeTimer > 0
- ? `${mobileCodeTimer}秒后可重新获取`
- : (t('login.getSmsCode') || '获取验证码')
- }
- </button>
- </div>
- </div>
- <div>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <Lock className="w-5 h-5" />
- </div>
- <input
- type={showNewPassword ? 'text' : 'password'}
- placeholder={t('reset.newPassword')}
- value={newPassword}
- onChange={(e) => setNewPassword(e.target.value)}
- className="w-full h-13 pl-12 pr-12 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- <button
- type="button"
- onClick={() => setShowNewPassword(!showNewPassword)}
- className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
- >
- {showNewPassword ? (
- <EyeOff className="w-5 h-5" />
- ) : (
- <Eye className="w-5 h-5" />
- )}
- </button>
- </div>
- <div className="mt-2">
- {getPasswordStrengthBars(newPasswordStrength)}
- <div className="flex justify-between items-center mt-1">
- <span className="text-xs text-gray-500">{t('reset.passwordStrength')}</span>
- <span className={`text-xs ${newPasswordStrengthInfo.color || 'text-gray-400'}`}>
- {newPasswordStrengthInfo.text || t('reset.notEntered')}
- </span>
- </div>
- </div>
- </div>
- <div>
- <div className="relative">
- <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
- <Lock className="w-5 h-5" />
- </div>
- <input
- type={showConfirmPassword ? 'text' : 'password'}
- placeholder={t('reset.confirmPassword')}
- value={confirmPassword}
- onChange={(e) => setConfirmPassword(e.target.value)}
- className="w-full h-13 pl-12 pr-12 bg-white border-2 border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all"
- required
- />
- <button
- type="button"
- onClick={() => setShowConfirmPassword(!showConfirmPassword)}
- className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
- >
- {showConfirmPassword ? (
- <EyeOff className="w-5 h-5" />
- ) : (
- <Eye className="w-5 h-5" />
- )}
- </button>
- </div>
- <div className="mt-2">
- {getPasswordStrengthBars(confirmPasswordStrength)}
- <div className="flex justify-between items-center mt-1">
- <span className="text-xs text-gray-500">{t('reset.passwordStrength')}</span>
- <span className={`text-xs ${confirmPasswordStrengthInfo.color || 'text-gray-400'}`}>
- {confirmPasswordStrengthInfo.text || t('reset.notEntered')}
- </span>
- </div>
- <div className="mt-1 h-4">
- {confirmPassword && newPassword && (
- confirmPassword === newPassword ? (
- <span className="text-xs text-green-600">{t('reset.passwordMatch')}</span>
- ) : (
- <span className="text-xs text-red-600">{t('reset.passwordMismatch')}</span>
- )
- )}
- </div>
- </div>
- </div>
- <div className="flex gap-3 pt-4">
- <button
- type="button"
- onClick={() => {
- setShowResetPassword(false);
- setResetTenantName('');
- setResetMobile('');
- setVerificationCode('');
- setNewPassword('');
- setConfirmPassword('');
- setMobileCodeTimer(0);
- }}
- className="flex-1 h-13 bg-white border-2 border-gray-200 text-gray-700 rounded-xl hover:bg-gray-50 transition-all"
- >
- {t('login.backLogin') || '返回登录'}
- </button>
- <button
- type="submit"
- disabled={resetPasswordLoading || !newPassword || !confirmPassword || newPassword !== confirmPassword || !resetMobile || !verificationCode}
- className="relative flex-1 h-13 text-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center gap-2 group disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden"
- style={{
- background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)',
- boxShadow: '0 10px 25px -5px rgba(77, 121, 248, 0.4)'
- }}
- >
- <span className="relative z-10">
- {resetPasswordLoading
- ? (t('common.loading') || '处理中...')
- : (t('login.resetPassword') || '重置密码')
- }
- </span>
- <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
- </button>
- </div>
- </form>
- <div className="mt-6 p-4 bg-blue-50/80 border border-blue-100/60 rounded-xl">
- <p className="text-xs text-gray-600 leading-relaxed">
- <span className="text-blue-600">{t('reset.securityTip')}</span>
- {t('reset.securityHint')}
- </p>
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- }
|