feat(login): 添加手机验证码登录功能

- 新增 sendSmsCode 和 loginByCode API 接口
- 将登录方式从用户名密码改为手机号验证码
- 添加手机号格式验证和验证码输入框
- 实现验证码倒计时功能和发送逻辑
- 更新登录表单验证规则和提交流程
- 移除原有的图形验证码组件依赖
演示用
fangjiakai 2026-04-22 14:56:36 +08:00
parent 0bfef35792
commit 2caaec186f
2 changed files with 107 additions and 59 deletions

View File

@ -1,6 +1,8 @@
import { post, upload } from "./axios";
export const Login = (params) => post("/admin/check", params); // 登录
export const sendSmsCode = (params) => post("/admin/sendSmsCode", params); // 发送验证码
export const loginByCode = (params) => post("/admin/checkByCode", params); // 验证码登录
export const logout = (params) => post("/main/logout", params); // 退出登录
export const getAsyncRouter = (params) => post("/main/index", params); // 获取动态路由
export const getHasMenu = (params) =>

View File

@ -24,10 +24,11 @@
:rules="data.rules"
@submit.prevent="fnLogin"
>
<el-form-item prop="username">
<el-form-item prop="phone">
<el-input
v-model="data.form.username"
placeholder="请输入用户名"
v-model="data.form.phone"
placeholder="请输入手机号"
maxlength="11"
tabindex="1"
>
<template #prepend>
@ -35,20 +36,26 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="data.form.password"
type="password"
placeholder="请输入密码"
tabindex="2"
>
<template #prepend>
<icon-lock size="16" fill="#9ba2a8" :stroke-width="3" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<verification v-model:verification-pass="verificationPass" />
<el-form-item prop="code">
<div class="code-row">
<el-input
v-model="data.form.code"
placeholder="请输入验证码"
maxlength="6"
tabindex="2"
>
<template #prepend>
<icon-lock size="16" fill="#9ba2a8" :stroke-width="3" />
</template>
</el-input>
<el-button
class="code-btn"
:disabled="data.countdown > 0"
@click="fnSendCode"
>
{{ data.countdown > 0 ? `${data.countdown}s` : "获取验证码" }}
</el-button>
</div>
</el-form-item>
<el-form-item class="button">
<el-button native-type="submit">登录</el-button>
@ -84,9 +91,8 @@
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import Verification from "@/components/verification/index";
import { useUserStore } from "@/pinia/user";
import { getAppVersion, Login } from "@/request/api";
import { getAppVersion, sendSmsCode, loginByCode } from "@/request/api";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import LayoutQrCode from "@/components/qr_code/index.vue";
@ -94,18 +100,29 @@ import LayoutQrCode from "@/components/qr_code/index.vue";
const VITE_FILE_URL = import.meta.env.VITE_FILE_URL;
const router = useRouter();
const formRef = ref(null);
const verificationPass = ref(false);
const userStore = useUserStore();
const data = reactive({
form: {
username: "",
password: "",
},
rules: {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
phone: "",
code: "",
},
qyAppSrc: "",
countdown: 0,
rules: {
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "手机号格式不正确",
trigger: "blur",
},
],
code: [
{ required: true, message: "请输入验证码", trigger: "blur" },
{ len: 6, message: "验证码为6位数字", trigger: "blur" },
],
},
});
const fnAppVersion = async () => {
@ -113,48 +130,60 @@ const fnAppVersion = async () => {
data.qyAppSrc = VITE_FILE_URL + resData.pd.FILEURL;
};
await fnAppVersion();
let timer = null;
const fnSendCode = async () => {
if (!data.form.phone || !/^1[3-9]\d{9}$/.test(data.form.phone)) {
ElMessage.warning("请输入正确的手机号");
return;
}
if (data.countdown > 0) return;
const resData = await sendSmsCode({ PHONE: data.form.phone });
if (resData.result === "success") {
ElMessage.success("验证码已发送");
data.countdown = 60;
timer = setInterval(() => {
data.countdown--;
if (data.countdown <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
ElMessage.error(resData.msg || "发送失败");
}
};
const fnLogin = debounce(
1000,
() => {
if (import.meta.env.DEV) {
fnSubmitLogin();
return;
}
if (verificationPass.value) {
fnSubmitLogin();
} else {
ElMessage.warning("请进行登录验证");
}
fnSubmitLogin();
},
{ atBegin: true }
);
const fnSubmitLogin = async () => {
await useFormValidate(formRef, "请输入用户名密码");
// eslint-disable-next-line no-undef
const jsencrypt = new JSEncrypt();
jsencrypt.setPublicKey(
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUoHAavCikaZxjlDM6Km8cX+ye78F4oF39AcEfnE1p2Yn9pJ9WFxYZ4Vkh6F8SKMi7k4nYsKceqB1RwG996SvHQ5C3pM3nbXCP4K15ad6QhN4a7lzlbLhiJcyIKszvvK8ncUDw8mVQ0j/2mwxv05yH6LN9OKU6Hzm1ninpWeE+awIDAQAB"
);
const KEYDATA = jsencrypt.encrypt(
"zcloudchina" + data.form.username + ",zy," + data.form.password
);
const resData = await Login({
KEYDATA,
const fnSubmitLogin = async () => {
await useFormValidate(formRef, "请填写手机号和验证码");
const resData = await loginByCode({
PHONE: data.form.phone,
CODE: data.form.code,
SOURCE: 1,
});
console.log(resData,'123')
await userStore.setUserInfo({
...userStore.getUserInfo,
...resData,
});
await router.replace({
path: "/index",
query: {
passwordType: resData.passwordType,
token: resData.token, //
},
});
if (resData.result === "success") {
await userStore.setUserInfo({
...userStore.getUserInfo,
...resData,
});
await router.replace({
path: "/index",
query: {
passwordType: resData.passwordType,
token: resData.token,
},
});
} else {
ElMessage.error(resData.msg || "登录失败");
}
};
</script>
@ -248,6 +277,23 @@ const fnSubmitLogin = async () => {
--el-input-border-color: #dde0eb;
}
.code-row {
display: flex;
width: 100%;
gap: 10px;
.el-input {
flex: 1;
}
}
.code-btn {
height: 40px;
width: 120px;
flex-shrink: 0;
font-size: 13px;
}
.button {
.el-button {
width: 100%;