|
|
@@ -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>
|