Login.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <!-- 登录页面 -->
  3. <div class="login-content">
  4. <!-- 背景图容器 -->
  5. <div
  6. class="bg-image-container"
  7. :style="{ backgroundImage: `url(${BGImages})`, backgroundSize: 'cover' }"
  8. ></div>
  9. <!-- 登录输入框 -->
  10. <div class="login-wrapper">
  11. <div class="login-input">
  12. <span>{{ appTitle }}</span>
  13. <el-form
  14. ref="loginFormRef"
  15. :model="loginData.loginForm"
  16. :rules="rules"
  17. label-width="0px"
  18. class="input-item"
  19. >
  20. <el-form-item prop="tenantName">
  21. <el-input v-show="!tenantNameQuery"
  22. v-model="loginData.loginForm.tenantName"
  23. :prefix-icon="HomeFilled"
  24. placeholder="学校"
  25. />
  26. </el-form-item>
  27. <!-- 条件显示手机号和短信验证码或账号和密码 -->
  28. <template v-if="isAuthorized">
  29. <el-form-item prop="username">
  30. <el-input
  31. v-model="loginData.loginForm.username"
  32. :prefix-icon="Avatar"
  33. placeholder="账号"
  34. />
  35. </el-form-item>
  36. <el-form-item prop="password">
  37. <el-input
  38. v-model="loginData.loginForm.password"
  39. class="password-input"
  40. type="password"
  41. :prefix-icon="Lock"
  42. placeholder="密码"
  43. show-password
  44. />
  45. </el-form-item>
  46. </template>
  47. <template v-else>
  48. <el-form-item prop="phoneNumber">
  49. <el-input
  50. v-model="loginData.loginForm.phoneNumber"
  51. :prefix-icon="Iphone"
  52. placeholder="手机号"
  53. />
  54. </el-form-item>
  55. <!-- 短信验证码输入框和获取验证码按钮 -->
  56. <el-form-item prop="smsCode">
  57. <div class="sms-code-container">
  58. <el-input
  59. v-model="loginData.loginForm.smsCode"
  60. placeholder="短信验证码"
  61. class="sms-input"
  62. />
  63. <el-button
  64. type="primary"
  65. @click="getSmsCode"
  66. :disabled="countingDown"
  67. class="get-code-btn"
  68. :loading="sendingCode"
  69. >
  70. {{ countingDown ? `${countDown}秒后重新获取` : '获取验证码' }}
  71. </el-button>
  72. </div>
  73. </el-form-item>
  74. </template>
  75. <!-- 登录按钮 -->
  76. <el-form-item>
  77. <el-button class="login-btn" type="primary" @click="handleLogin">登录</el-button>
  78. </el-form-item>
  79. </el-form>
  80. <!-- 多选框 -->
  81. <div class="check-box">
  82. <el-checkbox
  83. v-model="loginData.loginForm.rememberMe"
  84. label="记住我"
  85. size="large"
  86. />
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. </template>
  92. <script setup>
  93. import { ref, onMounted, computed, onUnmounted, nextTick } from 'vue'
  94. import { useRouter } from 'vue-router'
  95. import { HomeFilled, Avatar, Lock,Iphone } from '@element-plus/icons-vue'
  96. import { getTenantIdByName, login,smsLogin,smsCode } from '@/api/login/login.js'
  97. import { ElLoading, ElMessage } from 'element-plus'
  98. import BGImages from '@/assets/images/homeBG.png'
  99. const router = useRouter()
  100. // 获取环境变量
  101. const appTitle = import.meta.env.VITE_APP_TITLE
  102. const loginFormRef = ref(null)
  103. const loginData = ref({
  104. loginForm: {
  105. tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
  106. username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
  107. password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
  108. smsCode: '', // 短信验证码字段
  109. rememberMe: false, // 记住
  110. phoneNumber: '' // 手机号字段
  111. }
  112. })
  113. const loginLoading = ref(false)
  114. const loading = ref() // ElLoading.service 返回的实例
  115. const tenantId = ref('')
  116. // 登录状态标识
  117. const isLoggedIn = ref(false)
  118. //地址栏传参默认值
  119. const tenantNameQuery = ref()
  120. // 短信验证码相关状态
  121. const countingDown = ref(false)
  122. const countDown = ref(60)
  123. const sendingCode = ref(false)
  124. let countDownTimer = null
  125. // 授权状态 默认授权
  126. const isAuthorized = ref(true)
  127. // 输入框校验 - 根据授权状态动态调整
  128. const rules = computed(() => {
  129. if (isAuthorized.value) {
  130. // 授权状态:需要账号和密码
  131. return {
  132. tenantName: [{ required: true, message: '请输入学校名称', trigger: 'blur' }],
  133. username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
  134. password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
  135. }
  136. } else {
  137. // 未授权状态:需要手机号和短信验证码
  138. return {
  139. tenantName: [{ required: true, message: '请输入学校名称', trigger: 'blur' }],
  140. phoneNumber: [
  141. { required: true, message: '请输入手机号', trigger: 'blur' },
  142. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
  143. ],
  144. smsCode: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }]
  145. }
  146. }
  147. })
  148. // 获取租户 ID
  149. const getTenantId = async () => {
  150. try {
  151. const res = await getTenantIdByName(loginData.value.loginForm.tenantName)
  152. console.log(res);
  153. if (res && res.data) {
  154. //记录租户id
  155. tenantId.value = res.data
  156. return true; // 租户验证成功
  157. } else {
  158. ElMessage.error('租户填写错误!')
  159. return false; // 租户验证失败
  160. }
  161. } catch (error) {
  162. ElMessage.error('租户填写错误!')
  163. console.error('获取租户 ID 错误:', error)
  164. return false; // 租户验证失败
  165. }
  166. }
  167. // 获取短信验证码函数
  168. const getSmsCode = async () => {
  169. // 先验证租户和手机号是否填写
  170. if (!loginData.value.loginForm.tenantName) {
  171. ElMessage.warning('请先输入学校名称')
  172. return
  173. }
  174. if (!loginData.value.loginForm.phoneNumber) {
  175. ElMessage.warning('请先输入手机号')
  176. return
  177. }
  178. // 验证租户是否存在
  179. const tenantValid = await getTenantId()
  180. if (!tenantValid) {
  181. return
  182. }
  183. sendingCode.value = true
  184. try {
  185. // 获取验证码
  186. const res = await smsCode(
  187. { 'Tenant-Id': tenantId.value },{
  188. tenantName: loginData.value.loginForm.tenantName,
  189. mobile: loginData.value.loginForm.phoneNumber,
  190. scene: import.meta.env.VITE_APP_LOGIN_SMS_TEMPLATE_ID,
  191. })
  192. console.log('发送短信验证码:', res)
  193. if (res.code === 0) {
  194. ElMessage.success('验证码发送成功')
  195. // 开始倒计时
  196. startCountDown()
  197. } else {
  198. ElMessage.error(res.message || '验证码发送失败')
  199. }
  200. } catch (error) {
  201. ElMessage.error('验证码发送失败,请重试')
  202. console.error('发送验证码错误:', error)
  203. } finally {
  204. sendingCode.value = false
  205. }
  206. }
  207. // 验证码倒计时函数
  208. const startCountDown = () => {
  209. countingDown.value = true
  210. countDown.value = 60
  211. if (countDownTimer) {
  212. clearInterval(countDownTimer)
  213. }
  214. countDownTimer = setInterval(() => {
  215. countDown.value--
  216. if (countDown.value <= 0) {
  217. clearInterval(countDownTimer)
  218. countingDown.value = false
  219. }
  220. }, 1000)
  221. }
  222. // 登录
  223. const handleLogin = async params => {
  224. if (!loginFormRef.value) return
  225. await loginFormRef.value.validate(async valid => {
  226. if (valid) {
  227. loginLoading.value = true
  228. try {
  229. // 先验证租户是否存在
  230. const tenantValid = await getTenantId()
  231. if (!tenantValid) {
  232. // 租户验证失败,不执行登录
  233. loginLoading.value = false
  234. return
  235. }
  236. const loginDataLoginForm = { ...loginData.value.loginForm }
  237. // 根据授权状态选择不同的登录接口
  238. let res
  239. if (!isAuthorized.value) {
  240. // 未授权状态,使用短信验证码登录
  241. res = await smsLogin(
  242. { 'Tenant-Id': tenantId.value },{
  243. mobile: loginData.value.loginForm.phoneNumber,
  244. code: loginData.value.loginForm.smsCode,
  245. })
  246. } else {
  247. // 授权状态,使用账号密码登录
  248. res = await login(
  249. { 'Tenant-Id': tenantId.value },
  250. loginDataLoginForm
  251. )
  252. }
  253. if (!res) {
  254. return
  255. }
  256. // 校验登录状态
  257. if (res.code === 0) {
  258. ElMessage.success('登录成功')
  259. isLoggedIn.value = true
  260. // 存储登录状态
  261. localStorage.setItem('isLoggedIn', 'true')
  262. localStorage.setItem('token', res.data.accessToken)
  263. // 总是存储用户名和租户名称
  264. localStorage.setItem('userName', loginData.value.loginForm.username)
  265. localStorage.setItem('tenantName', loginData.value.loginForm.tenantName)
  266. // 存储记住我状态
  267. localStorage.setItem('rememberMe', loginData.value.loginForm.rememberMe)
  268. if (loginData.value.loginForm.rememberMe) {
  269. // 保存密码
  270. localStorage.setItem('password', loginData.value.loginForm.password)
  271. } else {
  272. // 如果没有勾选记住我,清除密码
  273. localStorage.removeItem('password')
  274. localStorage.removeItem('maxCourseSections')
  275. }
  276. loading.value = ElLoading.service({
  277. lock: true,
  278. text: '正在加载系统中...',
  279. background: 'rgba(0, 0, 0, 0.7)'
  280. })
  281. // 登录成功后,跳转到指定的页面
  282. router.push(!tenantNameQuery.value?'/home':'/programming')
  283. } else if (res.code === 1002000009) {
  284. // 未授权状态,切换到短信验证码登录
  285. ElMessage.warning(res.msg || '登录IP未被授权,请使用手机号短信验证码登录!')
  286. isAuthorized.value = false
  287. } else {
  288. ElMessage.error(res.msg || '登录失败,请检查账号密码!')
  289. }
  290. } catch (error) {
  291. ElMessage.error('登录出错,请重试!')
  292. console.error('登录错误:', error)
  293. } finally {
  294. loginLoading.value = false
  295. if (loading.value) {
  296. loading.value.close()
  297. }
  298. }
  299. }
  300. })
  301. }
  302. // 在组件挂载时检查登录状态和恢复登录信息
  303. onMounted(() => {
  304. const storedStatus = localStorage.getItem('isLoggedIn') // isLoggedIn
  305. const storedTenantName = localStorage.getItem('tenantName')
  306. const storedUserName = localStorage.getItem('userName')
  307. const storedPassword = localStorage.getItem('password')
  308. // 恢复登录信息到输入框
  309. if (storedTenantName) {
  310. loginData.value.loginForm.tenantName = storedTenantName
  311. }
  312. if (storedUserName) {
  313. loginData.value.loginForm.username = storedUserName
  314. }
  315. if (storedPassword) {
  316. loginData.value.loginForm.password = storedPassword
  317. loginData.value.loginForm.rememberMe = true
  318. }
  319. // 检查地址栏是否有tenantName参数
  320. let tenantName = router.currentRoute.value.meta?.TENANT;
  321. if (tenantName && tenantName === "内部测试租户") {
  322. loginData.value.loginForm.tenantName = tenantNameQuery.value = tenantName
  323. } else if (Object.keys(router.currentRoute.value.query).length > 0) {
  324. // 其他参数,重定向到登录页
  325. router.replace('/login')
  326. }
  327. // 检查登录状态,如果已登录则直接跳转到首页
  328. if (storedStatus === 'true') {
  329. isLoggedIn.value = true
  330. router.push(!tenantNameQuery.value?'/home':'/programming')
  331. }
  332. const handleKeyPress = (event) => {
  333. // 检查是否按下回车键(keyCode 13)
  334. if (event.key === 'Enter' || event.keyCode === 13) {
  335. handleLogin()
  336. }
  337. }
  338. document.addEventListener('keydown', handleKeyPress)
  339. // 在组件卸载时移除事件监听
  340. onUnmounted(() => {
  341. document.removeEventListener('keydown', handleKeyPress)
  342. if (countDownTimer) {
  343. clearInterval(countDownTimer)
  344. }
  345. })
  346. })
  347. </script>
  348. <style scoped lang="scss">
  349. @use 'sass:math';
  350. // 定义rpx转换函数
  351. @function rpx($px) {
  352. @return math.div($px, 750) * 100vw;
  353. }
  354. .login-content {
  355. position: fixed;
  356. top: 0;
  357. left: 0;
  358. right: 0;
  359. bottom: 0;
  360. display: flex;
  361. flex-direction: row; // 修改为水平布局
  362. }
  363. .bg-image-container {
  364. flex: 3; // 背景图占比为 3
  365. background-size: cover;
  366. background-position: center;
  367. }
  368. .login-wrapper {
  369. flex: 1; // 登录框占比为 1
  370. background: linear-gradient(to bottom, #001169, #8a78d0);
  371. padding: 20px;
  372. position: static;
  373. transform: none;
  374. display: flex; // 添加 Flexbox 布局
  375. justify-content: center; // 水平居中
  376. align-items: center; // 垂直居中
  377. }
  378. .login-input {
  379. width: rpx(190);
  380. height: rpx(240);
  381. display: flex;
  382. justify-content: center; // 水平居中
  383. align-items: center; // 垂直居中
  384. flex-direction: column; // 子元素垂直排列
  385. text-align: center; // 文本居中
  386. }
  387. .login-input span{
  388. color: white;
  389. font-size: rpx(11);
  390. padding-bottom: rpx(5);
  391. letter-spacing: rpx(1);
  392. }
  393. .input-item {
  394. display: flex;
  395. flex-direction: column; // 子元素垂直排列
  396. justify-content: center; // 内容垂直居中
  397. align-items: center; // 内容水平居中
  398. }
  399. .el-input ::v-deep(.el-input__wrapper){
  400. border-radius: rpx(5);
  401. }
  402. .input-item .el-form-item {
  403. margin-bottom: 0;
  404. }
  405. .input-item .el-input {
  406. width: rpx(150);
  407. height: rpx(22);
  408. margin-bottom: rpx(15);
  409. font-size: rpx(7);
  410. }
  411. .el-form-item ::v-deep(.el-form-item__error) {
  412. top: rpx(25);
  413. }
  414. .login-btn {
  415. width: rpx(150);
  416. height: rpx(22);
  417. color: black;
  418. font-size: rpx(8);
  419. letter-spacing: rpx(10);
  420. border-radius: rpx(5);
  421. margin: rpx(15) 0 auto;
  422. border: none;
  423. background: linear-gradient(to bottom, #fee78a, #ffce1b);
  424. box-shadow: 0 8px 8px rgb(0, 0, 0, 0.2);
  425. }
  426. .password-input {
  427. margin-bottom: rpx(0) !important;
  428. }
  429. .check-box {
  430. width: rpx(150);
  431. height: rpx(18);
  432. margin: rpx(5) auto;
  433. display: flex;
  434. justify-content: flex-end;
  435. align-items: center;
  436. }
  437. .check-box .el-checkbox {
  438. color: white;
  439. padding-right: rpx(10);
  440. font-size: rpx(6);
  441. }
  442. .el-checkbox ::v-deep(.el-checkbox__label){
  443. font-size: rpx(6);
  444. }
  445. .check-box .forgot-password {
  446. color: white;
  447. font-size: rpx(6);
  448. text-decoration: none;
  449. }
  450. // 短信验证码容器样式
  451. .sms-code-container {
  452. display: flex;
  453. align-items: center;
  454. width: rpx(150);
  455. height: rpx(22);
  456. }
  457. .sms-input {
  458. flex: 1;
  459. height: rpx(22);
  460. margin-bottom: 0 !important;
  461. }
  462. .sms-input ::v-deep(.el-input__wrapper) {
  463. border-top-right-radius: 0;
  464. border-bottom-right-radius: 0;
  465. }
  466. .get-code-btn {
  467. width: rpx(40);
  468. height: rpx(22);
  469. margin: 0 !important;
  470. padding: 0;
  471. font-size: rpx(5);
  472. border-radius: rpx(5);
  473. border-top-left-radius: 0;
  474. border-bottom-left-radius: 0;
  475. letter-spacing: normal;
  476. display: flex;
  477. align-items: center;
  478. justify-content: center;
  479. background-color: #fff;
  480. color: #919191;
  481. // background: linear-gradient(to bottom, #78c0ff, #0070f3);
  482. border: none;
  483. }
  484. </style>