Login.tsx 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. import React, { useState, useEffect } from 'react';
  2. import { useNavigate, useSearchParams } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import { Shield, Zap, Lock, User, Eye, EyeOff, ArrowRight, Activity, Radio, Layers, Building2, Globe } from 'lucide-react';
  5. import { loginApi } from '../api';
  6. import type { LoginFormParams } from '../api/Login';
  7. import { toast } from 'sonner';
  8. import { env } from '../config/env';
  9. import * as authUtil from '../utils/auth';
  10. import { setPermissionInfo } from '../utils/permission';
  11. export default function Login() {
  12. const navigate = useNavigate();
  13. const [searchParams] = useSearchParams();
  14. const { t, i18n } = useTranslation();
  15. // 从环境变量获取验证码和租户配置
  16. const captchaEnable = ((import.meta as any).env?.VITE_APP_CAPTCHA_ENABLE as string) === 'true';
  17. const tenantEnable = env.tenantEnable;
  18. // 获取 redirect 参数
  19. const redirect = searchParams.get('redirect') || '';
  20. const [tenant, setTenant] = useState(env.defaultLogin.tenant);
  21. const [username, setUsername] = useState(env.defaultLogin.username);
  22. const [password, setPassword] = useState(env.defaultLogin.password);
  23. const [showPassword, setShowPassword] = useState(false);
  24. const [rememberMe, setRememberMe] = useState(true); // 默认记住我
  25. const [isLoading, setIsLoading] = useState(false);
  26. const [showResetPassword, setShowResetPassword] = useState(false);
  27. const [captchaVerification, setCaptchaVerification] = useState('');
  28. // 重置密码相关状态
  29. const [resetTenantName, setResetTenantName] = useState('');
  30. const [resetMobile, setResetMobile] = useState('');
  31. const [verificationCode, setVerificationCode] = useState('');
  32. const [newPassword, setNewPassword] = useState('');
  33. const [confirmPassword, setConfirmPassword] = useState('');
  34. const [showNewPassword, setShowNewPassword] = useState(false);
  35. const [showConfirmPassword, setShowConfirmPassword] = useState(false);
  36. const [mobileCodeTimer, setMobileCodeTimer] = useState(0); // 验证码倒计时
  37. const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
  38. // 切换语言
  39. const toggleLanguage = () => {
  40. const newLang = i18n.language === 'zh' ? 'en' : 'zh';
  41. i18n.changeLanguage(newLang);
  42. };
  43. // 密码强度计算
  44. const calculatePasswordStrength = (pwd: string) => {
  45. let strength = 0;
  46. if (pwd.length >= 6) strength++;
  47. if (pwd.length >= 10) strength++;
  48. if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++;
  49. if (/\d/.test(pwd)) strength++;
  50. if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) strength++;
  51. return strength;
  52. };
  53. const getPasswordStrengthText = (strength: number) => {
  54. if (strength === 0) return { text: '', color: '' };
  55. if (strength <= 2) return { text: t('reset.weak'), color: 'text-red-600' };
  56. if (strength <= 3) return { text: t('reset.medium'), color: 'text-orange-600' };
  57. return { text: t('reset.strong'), color: 'text-green-600' };
  58. };
  59. const getPasswordStrengthBars = (strength: number) => {
  60. return (
  61. <div className="flex gap-1 mt-2">
  62. {[1, 2, 3, 4, 5].map((i) => (
  63. <div
  64. key={i}
  65. className={`h-1 flex-1 rounded-full transition-all ${
  66. i <= strength
  67. ? strength <= 2
  68. ? 'bg-red-500'
  69. : strength <= 3
  70. ? 'bg-orange-500'
  71. : 'bg-green-500'
  72. : 'bg-gray-200'
  73. }`}
  74. />
  75. ))}
  76. </div>
  77. );
  78. };
  79. const newPasswordStrength = calculatePasswordStrength(newPassword);
  80. const confirmPasswordStrength = calculatePasswordStrength(confirmPassword);
  81. const newPasswordStrengthInfo = getPasswordStrengthText(newPasswordStrength);
  82. const confirmPasswordStrengthInfo = getPasswordStrengthText(confirmPasswordStrength);
  83. // 获取租户ID
  84. const getTenantId = async () => {
  85. if (tenantEnable) {
  86. if (!tenant || !tenant.trim()) {
  87. throw new Error(t('login.tenant') || '请输入租户名称');
  88. }
  89. try {
  90. const tenantId = await loginApi.getTenantIdByName(tenant.trim());
  91. // axios 拦截器已经处理了响应,直接使用数据
  92. if (!tenantId) {
  93. throw new Error(t('login.invalidTenantName') || '无效的租户名称');
  94. }
  95. authUtil.setTenantId(String(tenantId));
  96. return tenantId;
  97. } catch (error: any) {
  98. console.error('获取租户ID失败:', error);
  99. const errorMessage = error?.message || t('login.getTenantIdFailed') || '获取租户ID失败';
  100. toast.error(errorMessage);
  101. throw error;
  102. }
  103. }
  104. return null;
  105. };
  106. // 根据域名获取租户信息
  107. const getTenantByWebsite = async () => {
  108. try {
  109. const website = window.location.host;
  110. const tenantInfo = await loginApi.getTenantByWebsite(website) as any;
  111. if (tenantInfo && tenantInfo.name) {
  112. setTenant(tenantInfo.name);
  113. authUtil.setTenantId(tenantInfo.id);
  114. }
  115. } catch (error) {
  116. // 静默失败,不影响登录流程
  117. console.log('根据域名获取租户信息失败:', error);
  118. }
  119. };
  120. // 获取登录表单缓存
  121. const getLoginFormCache = () => {
  122. const loginForm = authUtil.getLoginForm();
  123. if (loginForm) {
  124. if (loginForm.username) setUsername(loginForm.username);
  125. if (loginForm.password) setPassword(loginForm.password);
  126. if (loginForm.tenantName) setTenant(loginForm.tenantName);
  127. if (loginForm.rememberMe !== undefined) setRememberMe(loginForm.rememberMe);
  128. }
  129. };
  130. // 表单验证
  131. const validateForm = (): boolean => {
  132. if (!username.trim()) {
  133. toast.error(t('login.usernamePlaceholder') || '请输入用户名');
  134. return false;
  135. }
  136. if (!password.trim()) {
  137. toast.error(t('login.passwordPlaceholder') || '请输入密码');
  138. return false;
  139. }
  140. if (tenantEnable && !tenant.trim()) {
  141. toast.error(t('login.tenant') || '请输入租户名称');
  142. return false;
  143. }
  144. return true;
  145. };
  146. // 获取验证码(如果需要)
  147. const getCode = async () => {
  148. if (!validateForm()) {
  149. return;
  150. }
  151. // 如果未启用验证码,直接登录
  152. if (!captchaEnable) {
  153. await handleLogin({});
  154. } else {
  155. // 如果启用验证码,这里需要集成验证码组件
  156. // 暂时直接调用登录,实际项目中需要添加验证码组件
  157. toast.info('验证码功能需要集成验证码组件');
  158. await handleLogin({ captchaVerification: captchaVerification });
  159. }
  160. };
  161. // 登录处理
  162. const handleLogin = async (params: { captchaVerification?: string } = {}) => {
  163. setIsLoading(true);
  164. try {
  165. // 表单验证
  166. if (!validateForm()) {
  167. setIsLoading(false);
  168. return;
  169. }
  170. // 先获取租户ID(如果启用租户功能,必须在登录前获取)
  171. await getTenantId();
  172. // 构建登录参数
  173. const loginParams: LoginFormParams = {
  174. username: username.trim(),
  175. password: password.trim(),
  176. rememberMe: rememberMe,
  177. };
  178. // 如果启用租户功能,添加租户名称
  179. if (tenantEnable && tenant) {
  180. loginParams.tenantName = tenant.trim();
  181. }
  182. // 如果启用验证码,添加验证码
  183. if (captchaEnable && params.captchaVerification) {
  184. loginParams.captchaVerification = params.captchaVerification;
  185. }
  186. // 调用登录API
  187. const res = await loginApi.login(loginParams) as any;
  188. // 存储用户ID和Token
  189. if (res && res.userId) {
  190. authUtil.setUserId(String(res.userId));
  191. }
  192. // 存储Token(兼容多种格式)
  193. const token = res?.accessToken || res?.token;
  194. if (token) {
  195. authUtil.setToken(res);
  196. // 存储ACCESS_TOKEN
  197. authUtil.setAccessToken(token);
  198. }
  199. // 存储REFRESH_TOKEN
  200. if (res?.refreshToken) {
  201. authUtil.setRefreshToken(res.refreshToken);
  202. }
  203. // 存储用户信息
  204. if (res?.user) {
  205. authUtil.setUser(res.user);
  206. }
  207. if (!res) {
  208. return;
  209. }
  210. // 处理记住我功能
  211. if (rememberMe) {
  212. authUtil.setLoginForm({
  213. username: loginParams.username,
  214. password: loginParams.password,
  215. tenantName: loginParams.tenantName,
  216. rememberMe: true,
  217. });
  218. } else {
  219. authUtil.removeLoginForm();
  220. }
  221. // 显示加载提示
  222. toast.loading('正在加载系统中...', { id: 'loading' });
  223. // 登录成功后调用其他接口
  224. try {
  225. // 并行调用所有接口
  226. const [
  227. homePageData,
  228. systemAttribute,
  229. unreadCount,
  230. myAgent,
  231. permissionInfo,
  232. dictData
  233. ] = await Promise.allSettled([
  234. loginApi.getHomePage(),
  235. loginApi.getSystemAttributeByKey('sys.websocket.address'),
  236. loginApi.getUnreadCount(),
  237. loginApi.getMyAgent({ pageNum: 1, pageSize: -1, queryType: 0, startTime: '', endTime: '' }),
  238. loginApi.getInfo(),
  239. loginApi.getDictDataSimpleList()
  240. ]);
  241. // 存储权限信息(包含roleRouters和完整权限数据)
  242. if (permissionInfo.status === 'fulfilled' && permissionInfo.value) {
  243. const permissionData = permissionInfo.value as any;
  244. if (permissionData.roleRouters) {
  245. authUtil.setRoleRouters(permissionData.roleRouters);
  246. }
  247. // 存储完整的权限信息(user, roles, permissions, menus)
  248. // 注意:axios 拦截器已经提取了 data 字段,所以 permissionData 就是 data 的内容
  249. if (permissionData.user || permissionData.roles || permissionData.permissions || permissionData.menus) {
  250. setPermissionInfo(permissionData);
  251. }
  252. }
  253. // 存储其他数据(如果需要)
  254. // homePageData, systemAttribute, unreadCount, myAgent, dictData 可以根据需要存储
  255. } catch (error) {
  256. console.error('加载系统数据失败:', error);
  257. // 即使这些接口失败,也不影响登录流程
  258. }
  259. // 存储语言设置(如果还没有)
  260. if (!authUtil.getLang()) {
  261. authUtil.setLang(i18n.language || 'zh');
  262. }
  263. // 存储暗色模式设置(如果还没有)
  264. if (localStorage.getItem('isDark') === null) {
  265. authUtil.setIsDark(false);
  266. }
  267. // 处理重定向
  268. let redirectPath = redirect || '/dashboard';
  269. // 判断是否为SSO登录
  270. if (redirectPath.indexOf('sso') !== -1) {
  271. window.location.href = window.location.href.replace('/login?redirect=', '');
  272. return;
  273. }
  274. // 延迟一下再跳转,让用户看到加载提示
  275. setTimeout(() => {
  276. toast.dismiss('loading');
  277. toast.success(t('common.success') || '登录成功');
  278. navigate(redirectPath);
  279. }, 500);
  280. } catch (error: any) {
  281. toast.dismiss('loading');
  282. toast.error(error.message || t('common.error') || '登录失败');
  283. } finally {
  284. setIsLoading(false);
  285. }
  286. };
  287. // 处理表单提交
  288. const handleSubmit = async (e: React.FormEvent) => {
  289. e.preventDefault();
  290. await getCode();
  291. };
  292. // 组件挂载时执行
  293. useEffect(() => {
  294. getLoginFormCache();
  295. getTenantByWebsite();
  296. }, []);
  297. // 清理定时器
  298. useEffect(() => {
  299. return () => {
  300. // 组件卸载时清理定时器(通过检查 timer 状态)
  301. if (mobileCodeTimer > 0) {
  302. setMobileCodeTimer(0);
  303. }
  304. };
  305. }, [mobileCodeTimer]);
  306. // 获取租户ID(用于重置密码)
  307. const getTenantIdForReset = async () => {
  308. if (tenantEnable && resetTenantName) {
  309. try {
  310. const tenantId = await loginApi.getTenantIdByName(resetTenantName);
  311. if (!tenantId) {
  312. toast.error(t('login.invalidTenantName') || '无效的租户名称');
  313. throw new Error('无效的租户名称');
  314. }
  315. authUtil.setTenantId(String(tenantId));
  316. } catch (error: any) {
  317. console.error('获取租户ID失败:', error);
  318. throw error;
  319. }
  320. }
  321. };
  322. // 获取验证码(重置密码用)
  323. const getCodeForReset = async () => {
  324. // 验证手机号
  325. if (!resetMobile || resetMobile.length !== 11) {
  326. toast.error(t('login.mobileNumberPlaceholder') || '请输入正确的手机号');
  327. return;
  328. }
  329. // 如果启用租户,验证租户名
  330. if (tenantEnable && !resetTenantName) {
  331. toast.error(t('login.tenantNamePlaceholder') || '请输入租户名称');
  332. return;
  333. }
  334. // 如果未启用验证码,直接发送短信验证码
  335. if (!captchaEnable) {
  336. await getSmsCode({});
  337. } else {
  338. // 如果启用验证码,这里需要集成验证码组件
  339. // 暂时直接调用发送验证码,实际项目中需要添加验证码组件
  340. toast.info('验证码功能需要集成验证码组件');
  341. await getSmsCode({ captchaVerification: captchaVerification });
  342. }
  343. };
  344. // 发送短信验证码
  345. const getSmsCode = async (params: { captchaVerification?: string } = {}) => {
  346. try {
  347. // 如果启用租户,先获取租户ID
  348. if (tenantEnable && resetTenantName) {
  349. await getTenantIdForReset();
  350. }
  351. const smsParams: any = {
  352. mobile: resetMobile,
  353. scene: 23, // 重置密码场景
  354. };
  355. if (tenantEnable && resetTenantName) {
  356. smsParams.tenantName = resetTenantName;
  357. }
  358. if (params.captchaVerification) {
  359. smsParams.captchaVerification = params.captchaVerification;
  360. }
  361. await loginApi.sendSmsCode(smsParams);
  362. toast.success(t('login.SmsSendMsg') || '验证码已发送');
  363. // 设置倒计时
  364. setMobileCodeTimer(60);
  365. const timer = setInterval(() => {
  366. setMobileCodeTimer((prev) => {
  367. if (prev <= 1) {
  368. clearInterval(timer);
  369. return 0;
  370. }
  371. return prev - 1;
  372. });
  373. }, 1000);
  374. } catch (error: any) {
  375. toast.error(error.message || t('common.error') || '发送验证码失败');
  376. }
  377. };
  378. // 重置密码
  379. const handleResetPassword = async (e: React.FormEvent) => {
  380. e.preventDefault();
  381. // 表单验证
  382. if (!resetMobile || resetMobile.length !== 11) {
  383. toast.error(t('login.mobileNumberPlaceholder') || '请输入正确的手机号');
  384. return;
  385. }
  386. if (!verificationCode) {
  387. toast.error(t('login.codePlaceholder') || '请输入验证码');
  388. return;
  389. }
  390. if (!newPassword || newPassword.length < 4 || newPassword.length > 16) {
  391. toast.error('密码长度为4到16位');
  392. return;
  393. }
  394. if (newPassword !== confirmPassword) {
  395. toast.error(t('reset.passwordMismatch') || '两次输入密码不一致');
  396. return;
  397. }
  398. if (tenantEnable && !resetTenantName) {
  399. toast.error(t('login.tenantNamePlaceholder') || '请输入租户名称');
  400. return;
  401. }
  402. setResetPasswordLoading(true);
  403. try {
  404. // 获取租户ID
  405. await getTenantIdForReset();
  406. // 调用重置密码接口
  407. const resetParams: any = {
  408. mobile: resetMobile,
  409. code: verificationCode,
  410. password: newPassword,
  411. };
  412. if (tenantEnable && resetTenantName) {
  413. resetParams.tenantName = resetTenantName;
  414. }
  415. await loginApi.smsResetPassword(resetParams);
  416. toast.success(t('login.resetPasswordSuccess') || '密码重置成功');
  417. setShowResetPassword(false);
  418. // 重置表单
  419. setResetTenantName('');
  420. setResetMobile('');
  421. setVerificationCode('');
  422. setNewPassword('');
  423. setConfirmPassword('');
  424. setMobileCodeTimer(0);
  425. } catch (error: any) {
  426. toast.error(error.message || t('common.error') || '重置密码失败');
  427. } finally {
  428. setResetPasswordLoading(false);
  429. }
  430. };
  431. return (
  432. <div className="min-h-screen h-screen relative overflow-hidden bg-white">
  433. {/* 语言切换按钮 */}
  434. <div className="absolute top-6 right-6 z-50">
  435. <button
  436. onClick={toggleLanguage}
  437. 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"
  438. >
  439. <Globe className="w-4 h-4 text-blue-600 group-hover:rotate-12 transition-transform" />
  440. <span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
  441. </button>
  442. </div>
  443. {/* 太极式背景分割 */}
  444. <div className="absolute inset-0">
  445. <svg className="absolute inset-0 w-full h-full" preserveAspectRatio="none">
  446. <defs>
  447. <linearGradient id="leftGradient" x1="0%" y1="0%" x2="100%" y2="100%">
  448. <stop offset="0%" stopColor="#e8edff" />
  449. <stop offset="50%" stopColor="#c4d1fe" />
  450. <stop offset="100%" stopColor="#b8c5f9" />
  451. </linearGradient>
  452. <linearGradient id="rightGradient" x1="0%" y1="0%" x2="100%" y2="0%">
  453. <stop offset="0%" stopColor="#f8fafc" />
  454. <stop offset="100%" stopColor="#ffffff" />
  455. </linearGradient>
  456. <filter id="glow">
  457. <feGaussianBlur stdDeviation="6" result="coloredBlur"/>
  458. <feMerge>
  459. <feMergeNode in="coloredBlur"/>
  460. <feMergeNode in="SourceGraphic"/>
  461. </feMerge>
  462. </filter>
  463. </defs>
  464. <path
  465. d="M 0 0
  466. C 300 0, 600 150, 700 300
  467. S 850 500, 900 650
  468. S 750 850, 700 1000
  469. L 0 1000 Z"
  470. fill="url(#leftGradient)"
  471. />
  472. <path
  473. d="M 0 0
  474. C 300 0, 600 150, 700 300
  475. S 850 500, 900 650
  476. S 750 850, 700 1000
  477. L 1920 1000 L 1920 0 Z"
  478. fill="url(#rightGradient)"
  479. />
  480. <path
  481. d="M 700 0
  482. C 700 100, 750 200, 850 350
  483. S 900 550, 900 700
  484. S 800 900, 700 1000"
  485. fill="none"
  486. stroke="#c4d1fe"
  487. strokeWidth="2"
  488. opacity="0.5"
  489. filter="url(#glow)"
  490. />
  491. </svg>
  492. <div className="absolute top-[35%] left-[30%] w-32 h-32 bg-white/60 rounded-full blur-3xl"></div>
  493. <div className="absolute top-[65%] left-[32%] w-24 h-24 bg-blue-300/40 rounded-full blur-2xl"></div>
  494. <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>
  495. <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>
  496. </div>
  497. {/* 主内容 */}
  498. <div className="relative h-screen flex">
  499. {/* 左侧内容区 */}
  500. <div className="w-full lg:w-[58%] p-8 lg:px-16 lg:py-12 flex flex-col justify-between">
  501. <div>
  502. <div className="flex items-center gap-4 mb-12">
  503. <div className="relative">
  504. <div className="w-14 h-14 rounded-full flex items-center justify-center shadow-xl shadow-blue-400/40" style={{ background: '#4d79f8' }}>
  505. <Shield className="w-8 h-8 text-white" strokeWidth={2.5} />
  506. </div>
  507. <div className="absolute -top-1 -right-1 w-4 h-4 bg-green-400 rounded-full border-2 border-white shadow-lg">
  508. <div className="w-full h-full bg-green-400 rounded-full animate-ping"></div>
  509. </div>
  510. </div>
  511. <div>
  512. <h1 className="text-2xl text-gray-900">{env.appTitle}</h1>
  513. <p className="text-xs text-blue-600 tracking-widest mt-0.5">{t('login.subtitle')}</p>
  514. </div>
  515. </div>
  516. </div>
  517. <div className="space-y-10 max-w-2xl">
  518. <div className="space-y-4">
  519. <div className="inline-block px-3 py-1.5 bg-blue-100/80 backdrop-blur-sm rounded-full text-xs text-blue-700">
  520. {t('login.newGeneration')}
  521. </div>
  522. <h2 className="text-5xl lg:text-6xl leading-tight">
  523. <span className="inline-block text-gray-900 hover:scale-105 transition-transform duration-300">
  524. {t('login.slogan1')}
  525. </span>
  526. <br />
  527. <span className="relative inline-block mt-2">
  528. <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' }}>
  529. {t('login.slogan2')}
  530. </span>
  531. <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>
  532. </span>
  533. </h2>
  534. <p className="text-lg text-gray-600 leading-relaxed max-w-xl">
  535. {t('login.description')}
  536. </p>
  537. </div>
  538. <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
  539. <div className="group">
  540. <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">
  541. <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">
  542. <Activity className="w-6 h-6 text-white" strokeWidth={2.5} />
  543. </div>
  544. <h3 className="text-base text-gray-900 mb-1">{t('login.feature1Title')}</h3>
  545. <p className="text-sm text-gray-600 leading-relaxed">
  546. {t('login.feature1Desc')}
  547. </p>
  548. </div>
  549. </div>
  550. <div className="group">
  551. <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">
  552. <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">
  553. <Radio className="w-6 h-6 text-white" strokeWidth={2.5} />
  554. </div>
  555. <h3 className="text-base text-gray-900 mb-1">{t('login.feature2Title')}</h3>
  556. <p className="text-sm text-gray-600 leading-relaxed">
  557. {t('login.feature2Desc')}
  558. </p>
  559. </div>
  560. </div>
  561. <div className="group">
  562. <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">
  563. <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">
  564. <Layers className="w-6 h-6 text-white" strokeWidth={2.5} />
  565. </div>
  566. <h3 className="text-base text-gray-900 mb-1">{t('login.feature3Title')}</h3>
  567. <p className="text-sm text-gray-600 leading-relaxed">
  568. {t('login.feature3Desc')}
  569. </p>
  570. </div>
  571. </div>
  572. </div>
  573. <div className="bg-white/60 backdrop-blur-sm rounded-2xl p-6 border border-blue-100/50 shadow-xl shadow-blue-100/20">
  574. <div className="grid grid-cols-3 gap-6">
  575. <div>
  576. <div className="flex items-center gap-2 mb-2">
  577. <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
  578. <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.securityLevel')}</span>
  579. </div>
  580. <div className="text-xl text-gray-900">{t('login.enterpriseGrade')}</div>
  581. </div>
  582. <div>
  583. <div className="flex items-center gap-2 mb-2">
  584. <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div>
  585. <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.responseSpeed')}</span>
  586. </div>
  587. <div className="text-xl text-gray-900">&lt;100 <span className="text-base text-gray-500">ms</span></div>
  588. </div>
  589. <div>
  590. <div className="flex items-center gap-2 mb-2">
  591. <div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
  592. <span className="text-xs text-gray-500 uppercase tracking-wider">{t('login.systemVersion')}</span>
  593. </div>
  594. <div className="text-xl text-gray-900">v1.0</div>
  595. </div>
  596. </div>
  597. </div>
  598. </div>
  599. <div className="text-xs text-gray-400 text-center">
  600. {t('login.copyright')}
  601. </div>
  602. </div>
  603. {/* 右侧登录区 */}
  604. <div className="hidden lg:flex lg:w-[42%] items-center justify-start pl-8 pr-16 py-12">
  605. <div className="w-full max-w-md">
  606. {!showResetPassword ? (
  607. <div className="space-y-6">
  608. <div className="flex justify-center mb-8">
  609. <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%)' }}>
  610. <Zap className="w-10 h-10 text-white" strokeWidth={2.5} />
  611. </div>
  612. </div>
  613. <form onSubmit={handleSubmit} className="space-y-5">
  614. {env.tenantEnable && (
  615. <div>
  616. <div className="relative">
  617. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  618. <Building2 className="w-5 h-5" />
  619. </div>
  620. <input
  621. type="text"
  622. placeholder={t('login.tenant')}
  623. value={tenant}
  624. onChange={(e) => setTenant(e.target.value)}
  625. 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"
  626. required
  627. />
  628. </div>
  629. </div>
  630. )}
  631. <div>
  632. <label className="block text-sm text-gray-700 mb-2 ml-1">
  633. {t('login.username')}
  634. </label>
  635. <div className="relative">
  636. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  637. <User className="w-5 h-5" />
  638. </div>
  639. <input
  640. type="text"
  641. placeholder={t('login.usernamePlaceholder')}
  642. value={username}
  643. onChange={(e) => setUsername(e.target.value)}
  644. 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"
  645. required
  646. />
  647. </div>
  648. </div>
  649. <div>
  650. <label className="block text-sm text-gray-700 mb-2 ml-1">
  651. {t('login.password')}
  652. </label>
  653. <div className="relative">
  654. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  655. <Lock className="w-5 h-5" />
  656. </div>
  657. <input
  658. type={showPassword ? 'text' : 'password'}
  659. placeholder={t('login.passwordPlaceholder')}
  660. value={password}
  661. onChange={(e) => setPassword(e.target.value)}
  662. 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"
  663. required
  664. />
  665. <button
  666. type="button"
  667. onClick={() => setShowPassword(!showPassword)}
  668. className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
  669. >
  670. {showPassword ? (
  671. <EyeOff className="w-5 h-5" />
  672. ) : (
  673. <Eye className="w-5 h-5" />
  674. )}
  675. </button>
  676. </div>
  677. </div>
  678. <div className="flex items-center justify-between pt-2">
  679. <label className="flex items-center gap-2 cursor-pointer group">
  680. <input
  681. type="checkbox"
  682. checked={rememberMe}
  683. onChange={(e) => setRememberMe(e.target.checked)}
  684. className="w-4 h-4 rounded border-2 border-gray-300 text-blue-500 focus:ring-2 focus:ring-blue-400/50"
  685. />
  686. <span className="text-sm text-gray-600 group-hover:text-gray-900 transition-colors">
  687. {t('login.rememberMe')}
  688. </span>
  689. </label>
  690. <a
  691. href="#"
  692. onClick={(e) => {
  693. e.preventDefault();
  694. setShowResetPassword(true);
  695. }}
  696. className="text-sm text-blue-600 hover:text-blue-700 hover:underline transition-colors"
  697. >
  698. {t('login.forgotPassword')}
  699. </a>
  700. </div>
  701. <button
  702. type="submit"
  703. disabled={isLoading}
  704. 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"
  705. style={{
  706. background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)',
  707. boxShadow: '0 10px 25px -5px rgba(77, 121, 248, 0.4)'
  708. }}
  709. >
  710. <span className="relative z-10">{isLoading ? t('login.loggingIn') : t('login.loginButton')}</span>
  711. {!isLoading && (
  712. <ArrowRight className="relative z-10 w-5 h-5 group-hover:translate-x-1 transition-transform" />
  713. )}
  714. <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>
  715. </button>
  716. </form>
  717. <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">
  718. <div className="flex items-start gap-3">
  719. <div className="w-10 h-10 bg-white/80 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
  720. <Shield className="w-5 h-5 text-blue-600" />
  721. </div>
  722. <div className="text-sm">
  723. <div className="text-gray-900 mb-1">{t('login.systemNotice')}</div>
  724. <div className="text-gray-600 text-xs leading-relaxed">
  725. {t('login.noticeContent')}
  726. </div>
  727. </div>
  728. </div>
  729. </div>
  730. <div className="text-center mt-6 text-sm text-gray-500">
  731. {t('login.help')}
  732. </div>
  733. </div>
  734. ) : (
  735. <div className="space-y-6">
  736. <div className="flex justify-center mb-8">
  737. <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%)' }}>
  738. <Lock className="w-10 h-10 text-white" strokeWidth={2.5} />
  739. </div>
  740. </div>
  741. <form onSubmit={handleResetPassword} className="space-y-5">
  742. {tenantEnable && (
  743. <div>
  744. <label className="block text-sm text-gray-700 mb-2 ml-1">
  745. {t('login.tenantNamePlaceholder') || '租户名称'}
  746. </label>
  747. <div className="relative">
  748. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  749. <Building2 className="w-5 h-5" />
  750. </div>
  751. <input
  752. type="text"
  753. placeholder={t('login.tenantNamePlaceholder') || '请输入租户名称'}
  754. value={resetTenantName}
  755. onChange={(e) => setResetTenantName(e.target.value)}
  756. 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"
  757. required={tenantEnable}
  758. />
  759. </div>
  760. </div>
  761. )}
  762. <div>
  763. <label className="block text-sm text-gray-700 mb-2 ml-1">
  764. {t('login.mobileNumberPlaceholder') || '手机号'}
  765. </label>
  766. <div className="relative">
  767. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  768. <User className="w-5 h-5" />
  769. </div>
  770. <input
  771. type="tel"
  772. placeholder={t('login.mobileNumberPlaceholder') || '请输入手机号'}
  773. value={resetMobile}
  774. onChange={(e) => setResetMobile(e.target.value)}
  775. maxLength={11}
  776. 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"
  777. required
  778. />
  779. </div>
  780. </div>
  781. <div>
  782. <label className="block text-sm text-gray-700 mb-2 ml-1">
  783. {t('login.codePlaceholder') || '验证码'}
  784. </label>
  785. <div className="flex gap-2">
  786. <div className="relative flex-1">
  787. <input
  788. type="text"
  789. placeholder={t('login.codePlaceholder') || '请输入验证码'}
  790. value={verificationCode}
  791. onChange={(e) => setVerificationCode(e.target.value)}
  792. 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"
  793. required
  794. />
  795. </div>
  796. <button
  797. type="button"
  798. onClick={getCodeForReset}
  799. disabled={mobileCodeTimer > 0}
  800. 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"
  801. >
  802. {mobileCodeTimer > 0
  803. ? `${mobileCodeTimer}秒后可重新获取`
  804. : (t('login.getSmsCode') || '获取验证码')
  805. }
  806. </button>
  807. </div>
  808. </div>
  809. <div>
  810. <div className="relative">
  811. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  812. <Lock className="w-5 h-5" />
  813. </div>
  814. <input
  815. type={showNewPassword ? 'text' : 'password'}
  816. placeholder={t('reset.newPassword')}
  817. value={newPassword}
  818. onChange={(e) => setNewPassword(e.target.value)}
  819. 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"
  820. required
  821. />
  822. <button
  823. type="button"
  824. onClick={() => setShowNewPassword(!showNewPassword)}
  825. className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
  826. >
  827. {showNewPassword ? (
  828. <EyeOff className="w-5 h-5" />
  829. ) : (
  830. <Eye className="w-5 h-5" />
  831. )}
  832. </button>
  833. </div>
  834. <div className="mt-2">
  835. {getPasswordStrengthBars(newPasswordStrength)}
  836. <div className="flex justify-between items-center mt-1">
  837. <span className="text-xs text-gray-500">{t('reset.passwordStrength')}</span>
  838. <span className={`text-xs ${newPasswordStrengthInfo.color || 'text-gray-400'}`}>
  839. {newPasswordStrengthInfo.text || t('reset.notEntered')}
  840. </span>
  841. </div>
  842. </div>
  843. </div>
  844. <div>
  845. <div className="relative">
  846. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
  847. <Lock className="w-5 h-5" />
  848. </div>
  849. <input
  850. type={showConfirmPassword ? 'text' : 'password'}
  851. placeholder={t('reset.confirmPassword')}
  852. value={confirmPassword}
  853. onChange={(e) => setConfirmPassword(e.target.value)}
  854. 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"
  855. required
  856. />
  857. <button
  858. type="button"
  859. onClick={() => setShowConfirmPassword(!showConfirmPassword)}
  860. className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
  861. >
  862. {showConfirmPassword ? (
  863. <EyeOff className="w-5 h-5" />
  864. ) : (
  865. <Eye className="w-5 h-5" />
  866. )}
  867. </button>
  868. </div>
  869. <div className="mt-2">
  870. {getPasswordStrengthBars(confirmPasswordStrength)}
  871. <div className="flex justify-between items-center mt-1">
  872. <span className="text-xs text-gray-500">{t('reset.passwordStrength')}</span>
  873. <span className={`text-xs ${confirmPasswordStrengthInfo.color || 'text-gray-400'}`}>
  874. {confirmPasswordStrengthInfo.text || t('reset.notEntered')}
  875. </span>
  876. </div>
  877. <div className="mt-1 h-4">
  878. {confirmPassword && newPassword && (
  879. confirmPassword === newPassword ? (
  880. <span className="text-xs text-green-600">{t('reset.passwordMatch')}</span>
  881. ) : (
  882. <span className="text-xs text-red-600">{t('reset.passwordMismatch')}</span>
  883. )
  884. )}
  885. </div>
  886. </div>
  887. </div>
  888. <div className="flex gap-3 pt-4">
  889. <button
  890. type="button"
  891. onClick={() => {
  892. setShowResetPassword(false);
  893. setResetTenantName('');
  894. setResetMobile('');
  895. setVerificationCode('');
  896. setNewPassword('');
  897. setConfirmPassword('');
  898. setMobileCodeTimer(0);
  899. }}
  900. className="flex-1 h-13 bg-white border-2 border-gray-200 text-gray-700 rounded-xl hover:bg-gray-50 transition-all"
  901. >
  902. {t('login.backLogin') || '返回登录'}
  903. </button>
  904. <button
  905. type="submit"
  906. disabled={resetPasswordLoading || !newPassword || !confirmPassword || newPassword !== confirmPassword || !resetMobile || !verificationCode}
  907. 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"
  908. style={{
  909. background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 50%, #4d79f8 100%)',
  910. boxShadow: '0 10px 25px -5px rgba(77, 121, 248, 0.4)'
  911. }}
  912. >
  913. <span className="relative z-10">
  914. {resetPasswordLoading
  915. ? (t('common.loading') || '处理中...')
  916. : (t('login.resetPassword') || '重置密码')
  917. }
  918. </span>
  919. <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>
  920. </button>
  921. </div>
  922. </form>
  923. <div className="mt-6 p-4 bg-blue-50/80 border border-blue-100/60 rounded-xl">
  924. <p className="text-xs text-gray-600 leading-relaxed">
  925. <span className="text-blue-600">{t('reset.securityTip')}</span>
  926. {t('reset.securityHint')}
  927. </p>
  928. </div>
  929. </div>
  930. )}
  931. </div>
  932. </div>
  933. </div>
  934. </div>
  935. );
  936. }