AiCourseLogin.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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-model="loginData.loginForm.tenantName"
  22. :prefix-icon="HomeFilled"
  23. placeholder="学校"
  24. />
  25. </el-form-item>
  26. <!-- 条件显示手机号和短信验证码或账号和密码 -->
  27. <template v-if="isAuthorized">
  28. <el-form-item prop="username">
  29. <el-input
  30. v-model="loginData.loginForm.username"
  31. :prefix-icon="Avatar"
  32. placeholder="账号"
  33. />
  34. </el-form-item>
  35. <el-form-item prop="password">
  36. <el-input
  37. v-model="loginData.loginForm.password"
  38. class="password-input"
  39. type="password"
  40. :prefix-icon="Lock"
  41. placeholder="密码"
  42. show-password
  43. />
  44. </el-form-item>
  45. </template>
  46. <template v-else>
  47. <el-form-item prop="phoneNumber">
  48. <el-input
  49. v-model="loginData.loginForm.phoneNumber"
  50. :prefix-icon="Iphone"
  51. placeholder="手机号"
  52. />
  53. </el-form-item>
  54. <!-- 短信验证码输入框和获取验证码按钮 -->
  55. <el-form-item prop="smsCode">
  56. <div class="sms-code-container">
  57. <el-input
  58. v-model="loginData.loginForm.smsCode"
  59. placeholder="短信验证码"
  60. class="sms-input"
  61. />
  62. <el-button
  63. type="primary"
  64. @click="handleGetSmsCode"
  65. :disabled="countingDown"
  66. class="get-code-btn"
  67. :loading="sendingCode"
  68. >
  69. {{ countingDown ? `${countDown}秒后重新获取` : '获取验证码' }}
  70. </el-button>
  71. </div>
  72. </el-form-item>
  73. </template>
  74. <!-- 登录按钮 -->
  75. <el-form-item>
  76. <el-button class="login-btn" type="primary" @click="handleLogin">登录</el-button>
  77. </el-form-item>
  78. </el-form>
  79. <!-- 多选框 -->
  80. <div class="check-box">
  81. <router-link to="/register-login" class="register-link">没有账号?立即注册</router-link>
  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, onUnmounted } from 'vue'
  94. import { useRouter } from 'vue-router'
  95. import { HomeFilled, Avatar, Lock, Iphone } from '@element-plus/icons-vue'
  96. import { ElMessage } from 'element-plus'
  97. import BGImages from '@/assets/images/aiCourseBG.png'
  98. import {
  99. createLoginData,
  100. createVerificationCodeLogic,
  101. getTenantId,
  102. loginLogic,
  103. loadLoginData,
  104. checkLoginStatus,
  105. generateRules
  106. } from '@/utils/loginUtils.js'
  107. import {aiCourseRoutes} from "@/router/index.js";
  108. const router = useRouter()
  109. // 获取环境变量
  110. const appTitle = import.meta.env.VITE_APP_TITLE
  111. const loginFormRef = ref(null)
  112. // 初始化登录数据
  113. const loginData = createLoginData()
  114. // 初始化验证码逻辑
  115. const { countingDown, countDown, sendingCode, getSmsCode, clearCountDownTimer } = createVerificationCodeLogic()
  116. // 登录状态标识
  117. const isLoggedIn = ref(false)
  118. // 授权状态 默认授权
  119. const isAuthorized = ref(true)
  120. // 生成表单验证规则
  121. const rules = generateRules(isAuthorized)
  122. // 获取短信验证码
  123. const handleGetSmsCode = async () => {
  124. // 先验证租户和手机号是否填写
  125. if (!loginData.value.loginForm.tenantName) {
  126. ElMessage.warning('请先输入学校名称')
  127. return
  128. }
  129. if (!loginData.value.loginForm.phoneNumber) {
  130. ElMessage.warning('请先输入手机号')
  131. return
  132. }
  133. // 验证租户是否存在
  134. const tenantId = await getTenantId(loginData.value.loginForm.tenantName)
  135. if (!tenantId) {
  136. return
  137. }
  138. // 调用验证码逻辑
  139. getSmsCode(tenantId, loginData.value.loginForm.tenantName, loginData.value.loginForm.phoneNumber)
  140. }
  141. // 登录
  142. const handleLogin = async () => {
  143. if (!loginFormRef.value) return
  144. await loginFormRef.value.validate(async valid => {
  145. if (valid) {
  146. // 先验证租户是否存在
  147. const tenantId = await getTenantId(loginData.value.loginForm.tenantName)
  148. if (!tenantId) {
  149. // 租户验证失败,不执行登录
  150. return
  151. }
  152. // 调用登录逻辑
  153. await loginLogic(loginData.value.loginForm, tenantId, isAuthorized, router, aiCourseRoutes.home)
  154. }
  155. })
  156. }
  157. // 在组件挂载时检查登录状态和恢复登录信息
  158. onMounted(() => {
  159. // 加载本地存储的登录数据
  160. loadLoginData(loginData)
  161. // 检查地址栏是否有tenantName参数
  162. if (Object.keys(router.currentRoute.value.query).length > 0) {
  163. // 其他参数,重定向到登录页
  164. router.replace(aiCourseRoutes.login)
  165. }
  166. // 检查登录状态,如果已登录则直接跳转到首页
  167. checkLoginStatus(router, aiCourseRoutes.home)
  168. const handleKeyPress = (event) => {
  169. // 检查是否按下回车键(keyCode 13)
  170. if (event.key === 'Enter' || event.keyCode === 13) {
  171. handleLogin()
  172. }
  173. }
  174. document.addEventListener('keydown', handleKeyPress)
  175. // 在组件卸载时移除事件监听
  176. onUnmounted(() => {
  177. document.removeEventListener('keydown', handleKeyPress)
  178. clearCountDownTimer()
  179. })
  180. })
  181. </script>
  182. <style scoped lang="scss">
  183. @use 'sass:math';
  184. // 定义rpx转换函数
  185. @function rpx($px) {
  186. @return math.div($px, 750) * 100vw;
  187. }
  188. .login-content {
  189. position: fixed;
  190. top: 0;
  191. left: 0;
  192. right: 0;
  193. bottom: 0;
  194. display: flex;
  195. flex-direction: row; // 修改为水平布局
  196. }
  197. .bg-image-container {
  198. flex: 3; // 背景图占比为 3
  199. background-size: cover;
  200. background-position: center;
  201. }
  202. .login-wrapper {
  203. flex: 1; // 登录框占比为 1
  204. background: linear-gradient(to bottom, #001169, #8a78d0);
  205. padding: 20px;
  206. position: static;
  207. transform: none;
  208. display: flex; // 添加 Flexbox 布局
  209. justify-content: center; // 水平居中
  210. align-items: center; // 垂直居中
  211. }
  212. .login-input {
  213. width: rpx(190);
  214. height: rpx(240);
  215. display: flex;
  216. justify-content: center; // 水平居中
  217. align-items: center; // 垂直居中
  218. flex-direction: column; // 子元素垂直排列
  219. text-align: center; // 文本居中
  220. }
  221. .login-input span{
  222. color: white;
  223. font-size: rpx(11);
  224. padding-bottom: rpx(5);
  225. letter-spacing: rpx(1);
  226. }
  227. .input-item {
  228. display: flex;
  229. flex-direction: column; // 子元素垂直排列
  230. justify-content: center; // 内容垂直居中
  231. align-items: center; // 内容水平居中
  232. }
  233. .el-input ::v-deep(.el-input__wrapper){
  234. border-radius: rpx(5);
  235. }
  236. .input-item .el-form-item {
  237. margin-bottom: 0;
  238. }
  239. .input-item .el-input {
  240. width: rpx(150);
  241. height: rpx(22);
  242. margin-bottom: rpx(15);
  243. font-size: rpx(7);
  244. }
  245. .el-form-item ::v-deep(.el-form-item__error) {
  246. top: rpx(25);
  247. }
  248. .login-btn {
  249. width: rpx(150);
  250. height: rpx(22);
  251. color: black;
  252. font-size: rpx(8);
  253. letter-spacing: rpx(10);
  254. border-radius: rpx(5);
  255. margin: rpx(15) 0 auto;
  256. border: none;
  257. background: linear-gradient(to bottom, #fee78a, #ffce1b);
  258. box-shadow: 0 8px 8px rgb(0, 0, 0, 0.2);
  259. }
  260. .password-input {
  261. margin-bottom: rpx(0) !important;
  262. }
  263. .check-box {
  264. width: rpx(150);
  265. height: rpx(18);
  266. margin: rpx(5) auto;
  267. display: flex;
  268. justify-content: space-between;
  269. align-items: center;
  270. }
  271. .check-box .el-checkbox {
  272. color: white;
  273. // padding-right: rpx(10);
  274. font-size: rpx(6);
  275. }
  276. .el-checkbox ::v-deep(.el-checkbox__label){
  277. font-size: rpx(6);
  278. }
  279. .check-box .register-link {
  280. color: white;
  281. font-size: rpx(6);
  282. text-decoration: none;
  283. margin-right: rpx(5);
  284. }
  285. .check-box .register-link:hover {
  286. text-decoration: underline;
  287. }
  288. .check-box .forgot-password {
  289. color: white;
  290. font-size: rpx(6);
  291. text-decoration: none;
  292. }
  293. // 短信验证码容器样式
  294. .sms-code-container {
  295. display: flex;
  296. align-items: center;
  297. width: rpx(150);
  298. height: rpx(22);
  299. }
  300. .sms-input {
  301. flex: 1;
  302. height: rpx(22);
  303. margin-bottom: 0 !important;
  304. }
  305. .sms-input ::v-deep(.el-input__wrapper) {
  306. border-top-right-radius: 0;
  307. border-bottom-right-radius: 0;
  308. }
  309. .get-code-btn {
  310. width: rpx(40);
  311. height: rpx(22);
  312. margin: 0 !important;
  313. padding: 0;
  314. font-size: rpx(5);
  315. border-radius: rpx(5);
  316. border-top-left-radius: 0;
  317. border-bottom-left-radius: 0;
  318. letter-spacing: normal;
  319. display: flex;
  320. align-items: center;
  321. justify-content: center;
  322. background-color: #fff;
  323. color: #919191;
  324. // background: linear-gradient(to bottom, #78c0ff, #0070f3);
  325. border: none;
  326. }
  327. // 移动端响应式设计
  328. @media screen and (max-width: 768px) {
  329. .login-content {
  330. flex-direction: column;
  331. }
  332. .bg-image-container {
  333. position: fixed;
  334. top: 0;
  335. left: 0;
  336. width: 100%;
  337. height: 100%;
  338. z-index: -1;
  339. background-size: cover;
  340. background-position: center;
  341. }
  342. .login-wrapper {
  343. flex: none;
  344. width: 70%;
  345. margin: 0 auto;
  346. padding: 30px;
  347. background: rgba(0, 17, 105, 0.85);
  348. border-radius: 15px;
  349. position: absolute;
  350. top: 50%;
  351. left: 50%;
  352. transform: translate(-50%, -50%);
  353. min-height: 40%;
  354. overflow-y: auto;
  355. }
  356. .login-input {
  357. width: 100%;
  358. height: 100%;
  359. display: flex;
  360. flex-direction: column;
  361. justify-content: center;
  362. }
  363. .login-input span {
  364. font-size: rpx(18);
  365. padding-bottom: rpx(20);
  366. }
  367. .input-item {
  368. width: 100%;
  369. align-items: stretch;
  370. }
  371. .input-item .el-input {
  372. width: 100% !important;
  373. max-width: none;
  374. height: rpx(45) !important;
  375. font-size: rpx(14) !important;
  376. margin-bottom: rpx(25) !important;
  377. }
  378. .el-input ::v-deep(.el-input__wrapper) {
  379. height: rpx(45) !important;
  380. }
  381. .el-input ::v-deep(.el-input__inner) {
  382. height: rpx(45) !important;
  383. font-size: rpx(14) !important;
  384. }
  385. .login-btn {
  386. width: 100% !important;
  387. max-width: none;
  388. height: rpx(45) !important;
  389. font-size: rpx(16) !important;
  390. margin: rpx(25) 0 !important;
  391. letter-spacing: rpx(15);
  392. }
  393. .check-box {
  394. width: 100% !important;
  395. max-width: none;
  396. height: auto;
  397. margin: rpx(15) 0 !important;
  398. }
  399. .check-box .el-checkbox {
  400. font-size: rpx(12) !important;
  401. }
  402. .check-box .register-link {
  403. font-size: rpx(12) !important;
  404. margin-right: rpx(15) !important;
  405. }
  406. .el-checkbox ::v-deep(.el-checkbox__label) {
  407. font-size: rpx(12) !important;
  408. }
  409. .sms-code-container {
  410. width: 100% !important;
  411. max-width: none;
  412. height: rpx(45) !important;
  413. }
  414. .sms-input {
  415. width: calc(100% - rpx(75)) !important;
  416. height: rpx(45) !important;
  417. }
  418. .get-code-btn {
  419. width: rpx(75) !important;
  420. height: rpx(45) !important;
  421. font-size: rpx(10) !important;
  422. }
  423. .el-form-item ::v-deep(.el-form-item__error) {
  424. top: rpx(50) !important;
  425. font-size: rpx(10) !important;
  426. }
  427. }
  428. </style>