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"; import { post, upload } from "./axios";
export const Login = (params) => post("/admin/check", params); // 登录 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 logout = (params) => post("/main/logout", params); // 退出登录
export const getAsyncRouter = (params) => post("/main/index", params); // 获取动态路由 export const getAsyncRouter = (params) => post("/main/index", params); // 获取动态路由
export const getHasMenu = (params) => export const getHasMenu = (params) =>

View File

@ -24,10 +24,11 @@
:rules="data.rules" :rules="data.rules"
@submit.prevent="fnLogin" @submit.prevent="fnLogin"
> >
<el-form-item prop="username"> <el-form-item prop="phone">
<el-input <el-input
v-model="data.form.username" v-model="data.form.phone"
placeholder="请输入用户名" placeholder="请输入手机号"
maxlength="11"
tabindex="1" tabindex="1"
> >
<template #prepend> <template #prepend>
@ -35,20 +36,26 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="code">
<el-input <div class="code-row">
v-model="data.form.password" <el-input
type="password" v-model="data.form.code"
placeholder="请输入密码" placeholder="请输入验证码"
tabindex="2" maxlength="6"
> tabindex="2"
<template #prepend> >
<icon-lock size="16" fill="#9ba2a8" :stroke-width="3" /> <template #prepend>
</template> <icon-lock size="16" fill="#9ba2a8" :stroke-width="3" />
</el-input> </template>
</el-form-item> </el-input>
<el-form-item> <el-button
<verification v-model:verification-pass="verificationPass" /> class="code-btn"
:disabled="data.countdown > 0"
@click="fnSendCode"
>
{{ data.countdown > 0 ? `${data.countdown}s` : "获取验证码" }}
</el-button>
</div>
</el-form-item> </el-form-item>
<el-form-item class="button"> <el-form-item class="button">
<el-button native-type="submit">登录</el-button> <el-button native-type="submit">登录</el-button>
@ -84,9 +91,8 @@
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import Verification from "@/components/verification/index";
import { useUserStore } from "@/pinia/user"; import { useUserStore } from "@/pinia/user";
import { getAppVersion, Login } from "@/request/api"; import { getAppVersion, sendSmsCode, loginByCode } from "@/request/api";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js"; import useFormValidate from "@/assets/js/useFormValidate.js";
import LayoutQrCode from "@/components/qr_code/index.vue"; 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 VITE_FILE_URL = import.meta.env.VITE_FILE_URL;
const router = useRouter(); const router = useRouter();
const formRef = ref(null); const formRef = ref(null);
const verificationPass = ref(false);
const userStore = useUserStore(); const userStore = useUserStore();
const data = reactive({ const data = reactive({
form: { form: {
username: "", phone: "",
password: "", code: "",
},
rules: {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
}, },
qyAppSrc: "", 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 () => { const fnAppVersion = async () => {
@ -113,48 +130,60 @@ const fnAppVersion = async () => {
data.qyAppSrc = VITE_FILE_URL + resData.pd.FILEURL; data.qyAppSrc = VITE_FILE_URL + resData.pd.FILEURL;
}; };
await fnAppVersion(); 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( const fnLogin = debounce(
1000, 1000,
() => { () => {
if (import.meta.env.DEV) { fnSubmitLogin();
fnSubmitLogin();
return;
}
if (verificationPass.value) {
fnSubmitLogin();
} else {
ElMessage.warning("请进行登录验证");
}
}, },
{ atBegin: true } { 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({ const fnSubmitLogin = async () => {
KEYDATA, await useFormValidate(formRef, "请填写手机号和验证码");
const resData = await loginByCode({
PHONE: data.form.phone,
CODE: data.form.code,
SOURCE: 1, SOURCE: 1,
}); });
console.log(resData,'123') if (resData.result === "success") {
await userStore.setUserInfo({ await userStore.setUserInfo({
...userStore.getUserInfo, ...userStore.getUserInfo,
...resData, ...resData,
}); });
await router.replace({ await router.replace({
path: "/index", path: "/index",
query: { query: {
passwordType: resData.passwordType, passwordType: resData.passwordType,
token: resData.token, // token: resData.token,
}, },
}); });
} else {
ElMessage.error(resData.msg || "登录失败");
}
}; };
</script> </script>
@ -248,6 +277,23 @@ const fnSubmitLogin = async () => {
--el-input-border-color: #dde0eb; --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 { .button {
.el-button { .el-button {
width: 100%; width: 100%;