فهرست منبع

feat:"增加验证码登录功能"

soobin 2 هفته پیش
والد
کامیت
91c2a07490

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "shenze-vue3-app",
-  "version": "1.5.919",
+  "version": "1.5.923",
   "private": true,
   "scripts": {
     "start": "vue-cli-service serve",

+ 2 - 2
public/version.json

@@ -1,5 +1,5 @@
 {
-  "version": "1.5.919",
-  "timestamp": "2025-09-19T09:19:54.966",
+  "version": "1.5.923",
+  "timestamp": "2025-09-23T09:16:51.015",
   "commitHash": "dev-build"
 }

+ 9 - 1
src/assets/language/en.json

@@ -1450,7 +1450,15 @@
     "regusterButton": "Register",
     "loginButton": "Login",
     "loginSucess": "Login Success",
-    "loginWithWechat": "Login with WeChat"
+    "loginWithWechat": "Login with WeChat",
+    "passwordLogin": "Password",
+    "smsLogin": "SMS Code",
+    "phoneOrEmailInput": "Phone/Email",
+    "phoneOrEmailFormatError": "Invalid phone or email",
+    "smsCodeInput": "Code",
+    "sendSmsCode": "Send Code",
+    "smsCodeSent": "Sent",
+    "smsCodeSendFailed": "Failed"
   },
   "register": {
     "header": "Registration",

+ 9 - 1
src/assets/language/es.json

@@ -1449,7 +1449,15 @@
         "regusterButton": "Registrarse",
         "loginButton": "Iniciar sesión",
         "loginSucess": "Acceso exitoso",
-        "loginWithWechat": "Iniciar sesión con WeChat"
+        "loginWithWechat": "Iniciar sesión con WeChat",
+        "passwordLogin": "Contraseña",
+        "smsLogin": "SMS",
+        "phoneOrEmailInput": "Teléfono/Email",
+        "phoneOrEmailFormatError": "Formato incorrecto",
+        "smsCodeInput": "Código",
+        "sendSmsCode": "Enviar",
+        "smsCodeSent": "Enviado",
+        "smsCodeSendFailed": "Error"
     },
     "register": {
         "header": "Registro usuario",

+ 9 - 1
src/assets/language/fr.json

@@ -1480,7 +1480,15 @@
         "regusterButton": "S'inscrire",
         "loginButton": "Connexion",
         "loginSucess": "Connexion réussie",
-        "loginWithWechat": "Connexion via WeChat"
+        "loginWithWechat": "Connexion via WeChat",
+        "passwordLogin": "Mot de passe",
+        "smsLogin": "Code SMS",
+        "phoneOrEmailInput": "Téléphone/Email",
+        "phoneOrEmailFormatError": "Format incorrect",
+        "smsCodeInput": "Code",
+        "sendSmsCode": "Envoyer",
+        "smsCodeSent": "Envoyé",
+        "smsCodeSendFailed": "Échec"
     },
     "register": {
         "header": "Inscription",

+ 9 - 1
src/assets/language/ja.json

@@ -1443,7 +1443,15 @@
         "regusterButton": "登録する",
         "loginButton": "ログイン",
         "loginSucess": "ログイン成功",
-        "loginWithWechat": "WeChatでログイン"
+        "loginWithWechat": "WeChatでログイン",
+        "passwordLogin": "パスワード",
+        "smsLogin": "SMS認証",
+        "phoneOrEmailInput": "電話番号/メール",
+        "phoneOrEmailFormatError": "形式が正しくありません",
+        "smsCodeInput": "認証コード",
+        "sendSmsCode": "コードを送信",
+        "smsCodeSent": "送信済み",
+        "smsCodeSendFailed": "送信失敗"
     },
     "register": {
         "header": "ユーザー登録",

+ 9 - 1
src/assets/language/pt.json

@@ -1449,7 +1449,15 @@
         "regusterButton": "Cadastre-se",
         "loginButton": "Entrar",
         "loginSucess": "Login realizado",
-        "loginWithWechat": "Entrar com WeChat"
+        "loginWithWechat": "Entrar com WeChat",
+        "passwordLogin": "Senha",
+        "smsLogin": "SMS",
+        "phoneOrEmailInput": "Telefone/Email",
+        "phoneOrEmailFormatError": "Formato inválido",
+        "smsCodeInput": "Código",
+        "sendSmsCode": "Enviar",
+        "smsCodeSent": "Enviado",
+        "smsCodeSendFailed": "Falha"
     },
     "register": {
         "header": "Cadastro de Usuário",

+ 9 - 1
src/assets/language/ru.json

@@ -1478,7 +1478,15 @@
         "regusterButton": "Регистрация",
         "loginButton": "Войти",
         "loginSucess": "Успешный вход",
-        "loginWithWechat": "Войти через WeChat"
+        "loginWithWechat": "Войти через WeChat",
+        "passwordLogin": "Пароль",
+        "smsLogin": "SMS-код",
+        "phoneOrEmailInput": "Телефон/Email",
+        "phoneOrEmailFormatError": "Неверный формат",
+        "smsCodeInput": "Код",
+        "sendSmsCode": "Отправить код",
+        "smsCodeSent": "Отправлено",
+        "smsCodeSendFailed": "Ошибка"
     },
     "register": {
         "header": "Регистрация",

+ 9 - 1
src/assets/language/uk.json

@@ -1450,7 +1450,15 @@
         "regusterButton": "Реєстрація",
         "loginButton": "Увійти",
         "loginSucess": "Успішний вхід",
-        "loginWithWechat": "Увійти через WeChat"
+        "loginWithWechat": "Увійти через WeChat",
+        "passwordLogin": "Пароль",
+        "smsLogin": "SMS",
+        "phoneOrEmailInput": "Телефон/Email",
+        "phoneOrEmailFormatError": "Невірний формат",
+        "smsCodeInput": "Код",
+        "sendSmsCode": "Надіслати",
+        "smsCodeSent": "Відправлено",
+        "smsCodeSendFailed": "Помилка"
     },
     "register": {
         "header": "Реєстрація",

+ 9 - 1
src/assets/language/zh.json

@@ -1455,7 +1455,15 @@
     "regusterButton": "点击注册",
     "loginButton": "登录",
     "loginSucess": "登陆成功",
-    "loginWithWechat": "快速登录"
+    "loginWithWechat": "快速登录",
+    "passwordLogin": "密码登录",
+    "smsLogin": "验证码登录",
+    "phoneOrEmailInput": "手机号/邮箱",
+    "phoneOrEmailFormatError": "手机号或邮箱格式不正确",
+    "smsCodeInput": "验证码",
+    "sendSmsCode": "发送验证码",
+    "smsCodeSent": "已发送",
+    "smsCodeSendFailed": "发送失败"
   },
   "register": {
     "header": "用户注册",

+ 10 - 0
src/service/login.js

@@ -29,4 +29,14 @@ export function getUserDetailByWxCode(params) {
 // 微信登录
 export function wxLogin(openid) {
   return axios.get('/SZWL-SERVER/wxLogin/wxLogin', { params: { openid } }).then(response => response.data);
+}
+
+// 发送验证码
+export function sendSmsCode(params) {
+  return axios.post('/SZWL-SERVER/tAdmin/sentLoginCode', params);
+}
+
+// 验证码登录
+export function loginWithSms(params) {
+  return axios.post('/SZWL-SERVER/tAdmin/loginByCode', params);
 }

+ 291 - 55
src/views/login.vue

@@ -23,36 +23,97 @@
 
       <!-- 登录表单 -->
       <div class="form-container">
-        <van-form @submit="onSubmit" class="login-form">
-          <!-- 用户名输入 -->
-          <van-field
-            v-model="userName"
-            name="userName"
-            :placeholder="$t('login.userNameInput')"
-            :rules="[{ required: true, message: $t('login.userNameInput') }]"
-            class="form-field"
+        <!-- 登录方式切换 -->
+        <div class="login-type-tabs">
+          <div 
+            class="tab-item" 
+            :class="{ active: loginType === 'password' }"
+            @click="switchLoginType('password')"
           >
-            <template #left-icon>
-              <van-icon name="user-o" size="22" class="field-icon" />
-            </template>
-          </van-field>
-
-          <!-- 密码输入 -->
-          <van-field
-            v-model="userPwd"
-            name="userPwd"
-            type="password"
-            :placeholder="$t('login.passWordInput')"
-            :rules="[{ required: true, message: $t('login.passWordInput') }]"
-            class="form-field"
+            {{ $t('login.passwordLogin') }}
+          </div>
+          <div 
+            class="tab-item" 
+            :class="{ active: loginType === 'sms' }"
+            @click="switchLoginType('sms')"
           >
-            <template #left-icon>
-              <van-icon name="lock" size="22" class="field-icon" />
-            </template>
-          </van-field>
+            {{ $t('login.smsLogin') }}
+          </div>
+        </div>
+
+        <van-form @submit="onSubmit" class="login-form">
+          <!-- 密码登录 -->
+          <div v-if="loginType === 'password'">
+            <!-- 用户名输入 -->
+            <van-field
+              v-model="userName"
+              name="userName"
+              :placeholder="$t('login.userNameInput')"
+              :rules="[{ required: true, message: $t('login.userNameInput') }]"
+              class="form-field"
+            >
+              <template #left-icon>
+                <van-icon name="user-o" size="22" class="field-icon" />
+              </template>
+            </van-field>
+
+            <!-- 密码输入 -->
+            <van-field
+              v-model="userPwd"
+              name="userPwd"
+              type="password"
+              :placeholder="$t('login.passWordInput')"
+              :rules="[{ required: true, message: $t('login.passWordInput') }]"
+              class="form-field"
+            >
+              <template #left-icon>
+                <van-icon name="lock" size="22" class="field-icon" />
+              </template>
+            </van-field>
+          </div>
+
+          <!-- 验证码登录 -->
+          <div v-if="loginType === 'sms'">
+            <!-- 手机号/邮箱输入 -->
+            <van-field
+              v-model="phoneOrEmail"
+              name="phoneOrEmail"
+              :placeholder="$t('login.phoneOrEmailInput')"
+              :rules="phoneOrEmailRules"
+              class="form-field"
+            >
+              <template #left-icon>
+                <van-icon name="phone-o" size="22" class="field-icon" />
+              </template>
+            </van-field>
+
+            <!-- 验证码输入 -->
+            <van-field
+              v-model="smsCode"
+              name="smsCode"
+              :placeholder="$t('login.smsCodeInput')"
+              :rules="[{ required: true, message: $t('login.smsCodeInput') }]"
+              class="form-field sms-field"
+            >
+              <template #left-icon>
+                <van-icon name="shield-o" size="22" class="field-icon" />
+              </template>
+              <template #button>
+                <van-button
+                  size="small"
+                  type="primary"
+                  :disabled="smsCountdown > 0 || !phoneOrEmail"
+                  @click="sendSmsCode"
+                  class="sms-button"
+                >
+                  {{ smsCountdown > 0 ? `${smsCountdown}s` : $t('login.sendSmsCode') }}
+                </van-button>
+              </template>
+            </van-field>
+          </div>
 
-          <!-- 记住密码和忘记密码 -->
-          <div class="password-options">
+          <!-- 记住密码和忘记密码 (仅密码登录时显示) -->
+          <div v-if="loginType === 'password'" class="password-options">
             <div class="remember-me">
               <van-checkbox v-model="checked" shape="square" icon-size="20">
                 {{ $t("login.checkedPassWord") }}
@@ -108,7 +169,7 @@ import {
   Dialog,
   Button,
 } from "vant";
-import { loginSys, getSys, getOpenid } from "../service/login";
+import { loginSys, getSys, getOpenid, sendSmsCode as sendSmsCodeApi, loginWithSms } from "../service/login";
 import {
   setLocal,
   getLocal,
@@ -134,6 +195,49 @@ export default {
     const currentLan = ref(""); // 当前语言
     const logoName = ref(defaultLogo); // Logo图片名称
     const sysTitle = ref(""); // 页头标题
+    
+    // 验证码登录相关
+    const loginType = ref("password"); // 登录方式:password | sms
+    const phoneOrEmail = ref(""); // 手机号或邮箱
+    const smsCode = ref(""); // 验证码
+    const smsCountdown = ref(0); // 验证码倒计时
+    const countdownTimer = ref(null); // 倒计时定时器
+
+    // 手机号/邮箱验证规则
+    const phoneOrEmailRules = [
+      { required: true, message: t('login.phoneOrEmailInput') },
+      {
+        validator: (value) => {
+          const phoneRegex = /^1[3-9]\d{9}$/;
+          const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+          return phoneRegex.test(value) || emailRegex.test(value);
+        },
+        message: t('login.phoneOrEmailFormatError')
+      }
+    ];
+
+    // 切换登录方式
+    const switchLoginType = (type) => {
+      loginType.value = type;
+      // 清空表单数据
+      if (type === 'password') {
+        phoneOrEmail.value = '';
+        smsCode.value = '';
+        clearCountdown();
+      } else {
+        userName.value = '';
+        userPwd.value = '';
+      }
+    };
+
+    // 清除倒计时
+    const clearCountdown = () => {
+      if (countdownTimer.value) {
+        clearInterval(countdownTimer.value);
+        countdownTimer.value = null;
+      }
+      smsCountdown.value = 0;
+    };
 
     // 页面初始化
     onMounted(() => {
@@ -185,38 +289,96 @@ export default {
       }
     };
 
+    // 发送验证码
+    const sendSmsCode = async () => {
+      if (!phoneOrEmail.value) {
+        showFailToast(t('login.phoneOrEmailInput'));
+        return;
+      }
+
+      try {
+        const { data } = await sendSmsCodeApi({
+          phoneOrEmail: phoneOrEmail.value,
+          hostName: "Sunzee",
+        });
+
+        if (data.code === "00000") {
+          showSuccessToast(t('login.smsCodeSent'));
+          startCountdown();
+        } else {
+          showFailToast(data.message || t('login.smsCodeSendFailed'));
+        }
+      } catch (error) {
+        showFailToast(t('login.smsCodeSendFailed'));
+      }
+    };
+
+    // 开始倒计时
+    const startCountdown = () => {
+      smsCountdown.value = 60;
+      countdownTimer.value = setInterval(() => {
+        smsCountdown.value--;
+        if (smsCountdown.value <= 0) {
+          clearCountdown();
+        }
+      }, 1000);
+    };
+
     // 登录
     const onSubmit = async (values) => {
-      const { data } = await loginSys({
-        username: values.userName,
-        password: md5(values.userPwd),
-        hostName: "Sunzee",
-      });
-      console.log(checked.value);
-      if (data.code === "00000") {
-        setLocal("loginUser", JSON.stringify(data.data));
-        if (checked.value) {
-          const savedCredentials = JSON.stringify({
-            savedUsername: values.userName,
-            savedPassword: values.userPwd,
-          });
-          localStorage.setItem("savedCredentials", savedCredentials);
-        } else {
-          const savedCredentials = localStorage.getItem("savedCredentials");
-          if (savedCredentials) {
-            localStorage.removeItem("savedCredentials", savedCredentials);
+      if (loginType.value === 'password') {
+        // 密码登录
+        const { data } = await loginSys({
+          username: values.userName,
+          password: md5(values.userPwd),
+          hostName: "Sunzee",
+        });
+        console.log(checked.value);
+        if (data.code === "00000") {
+          setLocal("loginUser", JSON.stringify(data.data));
+          if (checked.value) {
+            const savedCredentials = JSON.stringify({
+              savedUsername: values.userName,
+              savedPassword: values.userPwd,
+            });
+            localStorage.setItem("savedCredentials", savedCredentials);
+          } else {
+            const savedCredentials = localStorage.getItem("savedCredentials");
+            if (savedCredentials) {
+              localStorage.removeItem("savedCredentials", savedCredentials);
+            }
           }
+          showSuccessToast(t("login.loginSucess"));
+          localStorage.setItem("firstLogin", true);
+
+          // 需要刷新页面,否则 axios.js 文件里的 token 不会被重置
+          // window.location.href = '/shenze/';
+          setTimeout(() => {
+            router.push("/home");
+          }, 200);
+        } else {
+          showFailToast(data.message);
         }
-        showSuccessToast(t("login.loginSucess"));
-        localStorage.setItem("firstLogin", true);
-
-        // 需要刷新页面,否则 axios.js 文件里的 token 不会被重置
-        // window.location.href = '/shenze/';
-        setTimeout(() => {
-          router.push("/home");
-        }, 200);
       } else {
-        showFailToast(data.message);
+        // 验证码登录
+        const { data } = await loginWithSms({
+          phoneOrEmail: values.phoneOrEmail,
+          code: values.smsCode,
+          hostName: "Sunzee",
+        });
+
+        if (data.code === "00000") {
+          setLocal("loginUser", JSON.stringify(data.data));
+          showSuccessToast(t("login.loginSucess"));
+          localStorage.setItem("firstLogin", true);
+          clearCountdown();
+
+          setTimeout(() => {
+            router.push("/home");
+          }, 200);
+        } else {
+          showFailToast(data.message);
+        }
       }
     };
     // 跳转注册页面
@@ -277,6 +439,14 @@ export default {
       currentLan,
       logoName,
       sysTitle,
+      // 验证码登录相关
+      loginType,
+      phoneOrEmail,
+      smsCode,
+      smsCountdown,
+      phoneOrEmailRules,
+      switchLoginType,
+      sendSmsCode,
     };
   },
   components: {
@@ -335,6 +505,37 @@ export default {
   width: 100%;
   max-width: 400px;
 
+  .login-type-tabs {
+    display: flex;
+    margin: 15px 15px 0 15px;
+    background-color: #f5f9ff;
+    border-radius: 12px;
+    padding: 4px;
+    margin-bottom: 0;
+
+    .tab-item {
+      flex: 1;
+      text-align: center;
+      padding: 12px 0;
+      border-radius: 8px;
+      font-size: 16px;
+      font-weight: 500;
+      color: #666;
+      cursor: pointer;
+      transition: all 0.3s ease;
+
+      &.active {
+        background-color: white;
+        color: @theme-color;
+        box-shadow: 0 2px 8px rgba(2, 77, 163, 0.15);
+      }
+
+      &:hover:not(.active) {
+        color: @theme-color;
+      }
+    }
+  }
+
   .login-form {
     margin: 15px;
     background-color: white;
@@ -362,6 +563,41 @@ export default {
     :deep(.van-field__error-message) {
       margin-left: 10px;
     }
+
+    &.sms-field {
+      :deep(.van-field__button) {
+        padding-left: 10px;
+      }
+
+      :deep(.van-field__left-icon) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      .sms-button {
+        background: linear-gradient(135deg, @theme-color, #0f6fee);
+        border: none;
+        border-radius: 8px;
+        color: white;
+        font-size: 14px;
+        font-weight: 500;
+        min-width: 100px;
+        height: 36px;
+        box-shadow: 0 2px 8px rgba(2, 77, 163, 0.3);
+
+        &:disabled {
+          background: #ccc;
+          color: #999;
+          box-shadow: none;
+        }
+
+        &:active:not(:disabled) {
+          opacity: 0.9;
+          transform: translateY(1px);
+        }
+      }
+    }
   }
 }