فهرست منبع

重新设计了家长课堂登录页面

liyanbo 1 ماه پیش
والد
کامیت
02f0f9f12a

BIN
src/assets/images/homeMobileBG.png


BIN
src/assets/images/login/login-mobile.jpg


BIN
src/assets/images/login/login-mobile.png


+ 4 - 4
src/router/index.js

@@ -7,10 +7,10 @@ import {refreshAllDictData} from "@/utils/dictUtils.js";
 const routes = [
   { path: '/', component: () => import('../views/Login.vue') },
   { path: '/login', component: () => import('../views/Login.vue') },
-  { 
-    path: '/login-mobile', 
+  {
+    path: '/login-mobile',
     meta: { defaultQuery: { loginPath: '/parent-mobile-course' } },
-    component: () => import('../views/Login.vue') 
+    component: () => import('../views/Login-Mobile.vue')
   },
 
   // 免登录
@@ -333,4 +333,4 @@ router.beforeEach(async (to, from, next) => {
 })
 export default router;
 
-export { homeRoutes, blocklyRoutes, aiCourseRoutes, managementRoutes };
+export { homeRoutes, blocklyRoutes, aiCourseRoutes, managementRoutes };

+ 521 - 0
src/views/Login-Mobile.vue

@@ -0,0 +1,521 @@
+<template>
+  <div class="login-content">
+    <div class="bg-image-container" :style="{ backgroundImage: `url(${BGImages})` }"></div>
+    <div class="login-wrapper">
+      <div class="login-input">
+        <div class="avatar-container">
+          <img :src="LogoImage" alt="Logo" class="avatar-img" />
+        </div>
+        <h1 class="app-title">清华圆爸家长课堂</h1>
+        <el-form
+            ref="loginFormRef"
+            :model="loginData.loginForm"
+            :rules="rules"
+            label-width="0px"
+            class="input-item"
+        >
+          <el-form-item prop="tenantName" v-if="tenantOpen">
+            <el-input
+                v-model="loginData.loginForm.tenantName"
+                :prefix-icon="HomeFilled"
+                placeholder="学校"
+            />
+          </el-form-item>
+
+          <template v-if="isAuthorized">
+            <el-form-item prop="username">
+              <el-input
+                  v-model="loginData.loginForm.username"
+                  :prefix-icon="Avatar"
+                  placeholder="请输入手机号"
+              />
+            </el-form-item>
+            <el-form-item prop="password">
+              <el-input
+                  v-model="loginData.loginForm.password"
+                  type="password"
+                  :prefix-icon="Lock"
+                  placeholder="请输入登录密码"
+                  show-password
+              />
+            </el-form-item>
+          </template>
+
+          <template v-else>
+            <el-form-item prop="phoneNumber">
+              <el-input
+                  v-model="loginData.loginForm.phoneNumber"
+                  :prefix-icon="Iphone"
+                  placeholder="手机号"
+                  disabled
+              />
+            </el-form-item>
+            <el-form-item prop="smsCode">
+              <div class="sms-code-container">
+                <el-input
+                    v-model="loginData.loginForm.smsCode"
+                    placeholder="短信验证码"
+                    class="sms-input"
+                />
+                <el-button
+                    type="primary"
+                    @click="handleGetSmsCode"
+                    :disabled="countingDown"
+                    class="get-code-btn"
+                    :loading="sendingCode"
+                >
+                  {{ countingDown ? `${countDown}秒后重新获取` : '获取验证码' }}
+                </el-button>
+              </div>
+            </el-form-item>
+          </template>
+
+          <el-form-item>
+            <el-button class="login-btn" type="primary" @click="handleLogin">登 录</el-button>
+          </el-form-item>
+        </el-form>
+        <div class="link-section">
+          <router-link :to="{path: '/forget-password'}" class="forgot-link">忘记密码?</router-link>
+        </div>
+        <div class="register-section">
+          <router-link :to="{path: '/register-login', query: {bgImage: 'homeBG', loginType: 'login'}}" class="register-link">没有账号?立即注册</router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { HomeFilled, Avatar, Lock, Iphone } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+import LogoImage from '@/assets/images/login/login-mobile.png'
+import BGImages from '@/assets/images/homeMobileBG.png'
+import {
+  createLoginData,
+  createVerificationCodeLogic,
+  getTenantId,
+  loginLogic,
+  loadLoginData,
+  checkLoginStatus,
+  generateRules
+} from '@/utils/loginUtils.js'
+import {homeRoutes, managementRoutes} from "@/router/index.js";
+
+const router = useRouter()
+
+// 获取环境变量
+const appTitle = import.meta.env.VITE_APP_TITLE
+const tenantOpen = import.meta.env.VITE_APP_COMPULSORY_TENANT === 'true'
+
+const loginFormRef = ref(null)
+
+// 初始化登录数据
+const loginData = createLoginData()
+
+// 初始化验证码逻辑
+const { countingDown, countDown, sendingCode, getSmsCode, clearCountDownTimer } = createVerificationCodeLogic()
+
+// 授权状态 默认授权
+const isAuthorized = ref(true)
+
+// 生成表单验证规则
+const rules = generateRules(isAuthorized)
+
+// 获取短信验证码
+const handleGetSmsCode = async () => {
+  // 先验证租户和手机号是否填写
+  if (tenantOpen && !loginData.value.loginForm.tenantName) {
+    ElMessage.warning('请先输入学校名称')
+    return
+  }
+  if (!loginData.value.loginForm.phoneNumber) {
+    ElMessage.warning('请先输入手机号')
+    return
+  }
+  // 验证租户是否存在
+  let tenantId = null;
+  if(tenantOpen) {
+    tenantId = await getTenantId(loginData.value.loginForm.tenantName)
+    if (!tenantId) {
+      return
+    }
+  }
+
+  // 调用验证码逻辑
+  getSmsCode(tenantId, loginData.value.loginForm.tenantName, loginData.value.loginForm.phoneNumber)
+}
+
+// 登录
+const handleLogin = async () => {
+  if (!loginFormRef.value) return
+  await loginFormRef.value.validate(async valid => {
+    if (valid) {
+
+      // 先验证租户是否存在
+      let tenantId = null
+      if (tenantOpen) {
+        tenantId = await getTenantId(loginData.value.loginForm.tenantName)
+        if (!tenantId) {
+          // 租户验证失败,不执行登录
+          return
+        }
+      }
+
+      // 获取路由参数中的重定向路径
+      // 优先使用查询参数,如果没有则使用路由元数据中的默认值
+      const currentRoute = router.currentRoute.value;
+      const loginPath = currentRoute.query.loginPath || currentRoute.meta.defaultQuery?.loginPath || null
+
+      // 调用登录逻辑
+      await loginLogic(loginData.value.loginForm, tenantId, isAuthorized, router, loginPath)
+    }
+  })
+}
+
+// 在组件挂载时检查登录状态和恢复登录信息
+onMounted(() => {
+  // 加载本地存储的登录数据
+  loadLoginData(loginData)
+
+  if (Object.keys(router.currentRoute.value.query).length > 0) {
+    // 其他参数,重定向到登录页
+    router.replace(homeRoutes.login)
+  }
+
+
+  // 获取路由参数中的重定向路径
+  // 优先使用查询参数,如果没有则使用路由元数据中的默认值
+  const currentRoute = router.currentRoute.value;
+  const loginPath = currentRoute.query.loginPath || currentRoute.meta.defaultQuery?.loginPath || null
+
+  // 检查登录状态
+  checkLoginStatus(router, loginPath)
+
+  const handleKeyPress = (event) => {
+    // 检查是否按下回车键(keyCode 13)
+    if (event.key === 'Enter' || event.keyCode === 13) {
+      handleLogin()
+    }
+  }
+
+  document.addEventListener('keydown', handleKeyPress)
+
+  // 在组件卸载时移除事件监听
+  onUnmounted(() => {
+    document.removeEventListener('keydown', handleKeyPress)
+    clearCountDownTimer()
+  })
+})
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.login-content {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.bg-image-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-image: url(@/assets/images/homeBG.png);
+  background-size: cover;
+  background-position: center;
+  overflow: hidden;
+  z-index: -1;
+}
+
+.bg-image-container::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-image:
+    radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,0.8), transparent),
+    radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.6), transparent),
+    radial-gradient(1px 1px at 90px 40px, rgba(255,255,255,0.7), transparent),
+    radial-gradient(2px 2px at 130px 80px, rgba(255,255,255,0.5), transparent),
+    radial-gradient(1px 1px at 160px 120px, rgba(255,255,255,0.6), transparent),
+    radial-gradient(2px 2px at 200px 50px, rgba(255,255,255,0.7), transparent),
+    radial-gradient(1px 1px at 250px 90px, rgba(255,255,255,0.5), transparent),
+    radial-gradient(2px 2px at 300px 140px, rgba(255,255,255,0.6), transparent);
+  background-size: 350px 150px;
+  animation: twinkle 4s ease-in-out infinite;
+}
+
+@keyframes twinkle {
+  0%, 100% { opacity: 0.8; }
+  50% { opacity: 1; }
+}
+
+.login-wrapper {
+  width: 85%;
+  max-width: 400px;
+  padding: 40px 30px;
+  background: rgb(42 44 104 / 72%);
+  border-radius: 20px;
+}
+
+.login-input {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.avatar-container {
+  width: 120px;
+  height: 120px;
+  border-radius: 50%;
+  background: rgba(0, 26, 102, 0.8);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: none;
+  overflow: hidden;
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.app-title {
+  font-size: 30px;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 40px;
+  text-align: center;
+}
+
+.input-item {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  margin-top: 40px;
+}
+
+.input-item .el-input {
+  width: 100%;
+  height: 48px;
+  margin-bottom: 20px;
+}
+
+.el-input ::v-deep(.el-input__wrapper) {
+  height: 48px;
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.95);
+  border: none;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.el-input ::v-deep(.el-input__inner) {
+  height: 48px;
+  font-size: 30px;
+  color: #666;
+  line-height: 48px;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.el-input ::v-deep(.el-input__prefix) {
+  font-size: 30px;
+  color: #999;
+  line-height: 48px;
+  align-self: center;
+  margin-top: 0;
+}
+
+.el-input ::v-deep(.el-input__inner)::placeholder {
+  font-size: 25px;
+  color: #999;
+  line-height: 48px;
+  vertical-align: middle;
+}
+
+.login-btn {
+  width: 100%;
+  height: 50px;
+  font-size: 25px;
+  font-weight: bold;
+  color: #333;
+  border-radius: 12px;
+  margin: 15px 0;
+  border: none;
+  background: linear-gradient(180deg, #ffeb3b 0%, #ffc107 100%);
+  box-shadow: 0 4px 15px rgba(255, 183, 0, 0.4);
+}
+
+.login-btn:hover {
+  background: linear-gradient(180deg, #ffda33 0%, #ffc107 100%);
+}
+
+.link-section {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 10px;
+}
+
+.register-section {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+  margin-top: 5px;
+}
+
+.forgot-link {
+  font-size: 18px;
+  color: rgba(255, 255, 255, 0.8);
+  text-decoration: none;
+}
+
+.register-link {
+  font-size: 18px;
+  color: rgba(255, 255, 255, 0.8);
+  text-decoration: none;
+}
+
+.forgot-link:hover,
+.register-link:hover {
+  text-decoration: underline;
+}
+
+.sms-code-container {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 48px;
+}
+
+.sms-input {
+  flex: 1;
+  height: 48px;
+  margin-bottom: 0 !important;
+}
+
+.sms-input ::v-deep(.el-input__wrapper) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.get-code-btn {
+  width: 100px;
+  height: 48px;
+  margin: 0 !important;
+  padding: 0;
+  font-size: 14px;
+  border-radius: 0 12px 12px 0;
+  border: none;
+  background: linear-gradient(180deg, #ffd700 0%, #ffb700 100%);
+  color: #333;
+}
+
+@media screen and (min-width: 769px) {
+  .login-wrapper {
+    width: 380px;
+    padding: 50px 40px;
+  }
+
+  .avatar-container {
+    width: 130px;
+    height: 130px;
+  }
+
+  .avatar-img {
+    width: 100px;
+    height: 100px;
+  }
+
+  .app-title {
+    font-size: 30px;
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .login-wrapper {
+    width: 90%;
+    padding: 25% 25px;
+    height: 100%;
+  }
+
+  .avatar-container {
+    width: 120px;
+    height: 120px;
+    overflow: hidden;
+  }
+
+  .avatar-img {
+    width: 100%;
+    height: 100%;
+  }
+
+  .app-title {
+    font-size: 30px;
+    margin-bottom: 25px;
+  }
+
+  .input-item .el-input {
+    height: 55px;
+    margin-bottom: 18px;
+  }
+
+  .el-input ::v-deep(.el-input__wrapper) {
+    height: 55px;
+  }
+
+  .el-input ::v-deep(.el-input__inner) {
+    height: 55px;
+    font-size: 30px;
+  }
+
+  .el-input ::v-deep(.el-input__prefix) {
+    font-size: 30px;
+  }
+
+  .login-btn {
+    height: 46px;
+    font-size: 25px;
+  }
+
+  .sms-code-container {
+    height: 55px;
+  }
+
+  .sms-input {
+    height: 55px;
+  }
+
+  .get-code-btn {
+    width: 90px;
+    height: 55px;
+    font-size: 13px;
+  }
+}
+</style>

+ 3 - 13
src/views/Login.vue

@@ -176,13 +176,8 @@ const handleLogin = async () => {
         }
       }
 
-      // 获取路由参数中的重定向路径
-      // 优先使用查询参数,如果没有则使用路由元数据中的默认值
-      const currentRoute = router.currentRoute.value;
-      const loginPath = currentRoute.query.loginPath || currentRoute.meta.defaultQuery?.loginPath || null
-
       // 调用登录逻辑
-      await loginLogic(loginData.value.loginForm, tenantId, isAuthorized, router, loginPath)
+      await loginLogic(loginData.value.loginForm, tenantId, isAuthorized, router)
     }
   })
 }
@@ -197,13 +192,8 @@ onMounted(() => {
     router.replace(homeRoutes.login)
   }
 
-  // 获取路由参数中登陆成功后跳转页面
-  // 优先使用查询参数,如果没有则使用路由元数据中的默认值
-  const currentRoute = router.currentRoute.value;
-  const loginPath = currentRoute.query.loginPath || currentRoute.meta.defaultQuery?.loginPath || null;
-
-  // 检查登录状态,如果已登录则根据loginPath参数决定跳转位置
-  checkLoginStatus(router, loginPath)
+  // 检查登录状态
+  checkLoginStatus(router)
 
   const handleKeyPress = (event) => {
     // 检查是否按下回车键(keyCode 13)