|
@@ -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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|