🎨 complete SMS login function

This commit is contained in:
landaiqing
2024-08-14 00:08:37 +08:00
parent fa1301689a
commit 48d2f61223
9 changed files with 119 additions and 55 deletions

View File

@@ -1,22 +1,61 @@
import {service} from "@/utils/alova/service.ts"; import {service} from "@/utils/alova/service.ts";
import {PhoneLogin} from "@/types/user";
/**
* 获取用户信息
*/
export const getUserInfo = () => { export const getUserInfo = () => {
return service.Get('/api/auth/user/List', { return service.Get('/api/auth/user/List', {
meta: { meta: {
ignoreToken: false ignoreToken: false
}, },
cacheFor: 1000 * 60
}); });
}; };
/**
* 刷新token
* @param refreshToken
*/
export const refreshToken = (refreshToken: string) => { export const refreshToken = (refreshToken: string) => {
return service.Get('/api/auth/token/refresh', { return service.Get('/api/auth/token/refresh', {
params: { params: {
refresh_token: refreshToken refresh_token: refreshToken
}, },
meta: { meta: {
authRole: 'refreshToken' authRole: 'refreshToken',
ignoreToken: false
} }
}); });
}; };
/**
* 发送短信验证码
* @param phone
*/
export const sendMessage = (phone: string) => {
return service.Get('/api/sms/test/send', {
params: {
phone: phone
},
meta: {
ignoreToken: true
}
});
};
/**
* 手机登录
* @param param
*/
export const phoneLoginApi = (param: PhoneLogin) => {
return service.Post('/api/user/phone_login', {
phone: param.phone,
captcha: param.captcha,
},
{
meta: {
ignoreToken: true,
authRole: 'login'
}
}
);
};

View File

@@ -34,7 +34,10 @@ export default {
rotateCaptchaTitle: "Please drag the slider to complete the puzzle", rotateCaptchaTitle: "Please drag the slider to complete the puzzle",
systemError: "System error, please try again later", systemError: "System error, please try again later",
captchaExpired: "captcha expired, please try again", captchaExpired: "captcha expired, please try again",
sendCaptchaSuccess: "captcha sent successfully, please check your phone!",
sendCaptchaError: "captcha sending failed, please try again later",
loginSuccess: "login success",
loginError: "login failed!",
}, },
error: { error: {
networkError: 'Network error, please try again later', networkError: 'Network error, please try again later',

View File

@@ -34,6 +34,10 @@ export default {
rotateCaptchaTitle: "请拖动滑块完成拼图", rotateCaptchaTitle: "请拖动滑块完成拼图",
systemError: "系统错误!请稍后再试!", systemError: "系统错误!请稍后再试!",
captchaExpired: "验证码已过期,请重新获取!", captchaExpired: "验证码已过期,请重新获取!",
sendCaptchaSuccess: "验证码已发送,请注意查收!",
sendCaptchaError: "验证码发送失败,请稍后再试!",
loginSuccess: "登录成功!",
loginError: "登录失败!",
}, },

View File

@@ -1,30 +1,19 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {ref} from 'vue'; import {reactive} from 'vue';
import {User} from "@/types/user";
export const useAuthStore = defineStore( export const useAuthStore = defineStore(
'user', 'user',
() => { () => {
const user = ref<User>(); const user: any = reactive({
accessToken: '',
function setUser(data: User) { userId: '',
user.value = data; refreshToken: '',
} expiresAt: 0,
});
function getUser() {
return user.value;
}
function clearUser() {
user.value = void 0;
}
return { return {
user, user,
setUser,
getUser,
clearUser
}; };
}, },
{ {
@@ -32,7 +21,7 @@ export const useAuthStore = defineStore(
persist: { persist: {
key: 'user', key: 'user',
storage: localStorage, storage: localStorage,
paths: ["user"], paths: ['user'],
} }
} }
); );

7
src/types/user.d.ts vendored
View File

@@ -1,10 +1,3 @@
export interface User {
accessToken?: string
userId?: string
refreshToken?: string
expiresAt?: number
}
export interface AccountLogin { export interface AccountLogin {
account?: string account?: string

View File

@@ -8,7 +8,7 @@ export const localforageStorageAdapter = {
get(key: string) { get(key: string) {
let value: any; let value: any;
localforage.getItem(key).then((res: any) => { localforage.getItem(key).then((res: any) => {
if (res === null || res === undefined) { if (res === null || res === undefined || res === "") {
value = ""; value = "";
} else { } else {
value = res; value = res;

View File

@@ -24,15 +24,18 @@ const {onAuthRequired, onResponseRefreshToken} = createServerTokenAuthentication
handler: async () => { handler: async () => {
try { try {
const user = useStore().user; const user = useStore().user;
const res: any = await refreshToken(user.getUser()?.refreshToken || ''); const res: any = await refreshToken(user.user?.refreshToken || '');
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
user.setUser({ const {uid, access_token, refresh_token, expires_at} = res.data;
userId: res.data.userId, user.user.userId = uid;
accessToken: res.data.access_token, user.user.accessToken = access_token;
refreshToken: res.data.refresh_token, user.user.refreshToken = refresh_token;
expiresAt: res.data.expires_at, user.user.expiresAt = expires_at;
});
} }
// else {
// message.error(res.message);
// await router.push('/login');
// }
} catch (error) { } catch (error) {
// token刷新失败跳转回登录页 // token刷新失败跳转回登录页
message.error(i18n.global.t('error.authTokenError')).then(); message.error(i18n.global.t('error.authTokenError')).then();
@@ -55,10 +58,10 @@ export const service = createAlova({
beforeRequest: onAuthRequired(async (method: any) => { beforeRequest: onAuthRequired(async (method: any) => {
if (!method.meta?.ignoreToken) { if (!method.meta?.ignoreToken) {
const user = useStore().user; const user = useStore().user;
method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.getUser()?.accessToken}`; method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.user.accessToken}`;
} }
const lang = useStore().lang; const lang = useStore().lang;
method.config.headers['Accept-Language'] = lang.lang|| 'zh'; method.config.headers['Accept-Language'] = lang.lang || 'zh';
}), }),
// 响应拦截器 // 响应拦截器
responded: onResponseRefreshToken({ responded: onResponseRefreshToken({

View File

@@ -142,7 +142,7 @@ const state = reactive({
* 验证码发送倒计时 * 验证码发送倒计时
*/ */
const countDown = () => { const countDown = () => {
const startTime = localStorage.getItem('startTimeSendCaptcha'); const startTime = localStorage.getItem('start_time_send_captcha');
const nowTime = new Date().getTime(); const nowTime = new Date().getTime();
let surplus: number = 60; let surplus: number = 60;
let timer: any; let timer: any;
@@ -150,7 +150,7 @@ const countDown = () => {
surplus = 60 - Math.floor((nowTime - Number(startTime)) / 1000); surplus = 60 - Math.floor((nowTime - Number(startTime)) / 1000);
surplus = surplus <= 0 ? 0 : surplus; surplus = surplus <= 0 ? 0 : surplus;
} else { } else {
localStorage.setItem('startTimeSendCaptcha', String(nowTime)); localStorage.setItem('start_time_send_captcha', String(nowTime));
} }
state.countDownTime = surplus; state.countDownTime = surplus;
@@ -160,7 +160,7 @@ const countDown = () => {
} }
timer = setInterval(() => { timer = setInterval(() => {
if (state.countDownTime <= 0) { if (state.countDownTime <= 0) {
localStorage.removeItem('startTimeSendCaptcha'); localStorage.removeItem('start_time_send_captcha');
clearInterval(timer); clearInterval(timer);
state.countDownTime = 60; state.countDownTime = 60;
state.showCountDown = false; state.showCountDown = false;
@@ -171,7 +171,7 @@ const countDown = () => {
}, 1000); }, 1000);
}; };
onMounted(() => { onMounted(() => {
const sendEndTime = localStorage.getItem('startTimeSendCaptcha'); const sendEndTime = localStorage.getItem('start_time_send_captcha');
if (sendEndTime) { if (sendEndTime) {
state.showCountDown = true; state.showCountDown = true;
countDown(); countDown();

View File

@@ -21,6 +21,7 @@
<span class="login-card-span">{{ t("login.phone") }}</span> <span class="login-card-span">{{ t("login.phone") }}</span>
<AInput v-model:value="phoneLoginForm.phone" class="login-form-input" size="large" <AInput v-model:value="phoneLoginForm.phone" class="login-form-input" size="large"
:placeholder=phoneValidate allow-clear :placeholder=phoneValidate allow-clear
autocomplete="off"
> >
<template #prefix> <template #prefix>
<TabletOutlined/> <TabletOutlined/>
@@ -72,13 +73,13 @@
{{ t("login.accountLogin") }} {{ t("login.accountLogin") }}
</span> </span>
</template> </template>
<AForm ref="accountLoginFormRef" :rules="rules" :model="accountLoginForm"> <AForm ref="accountLoginFormRef" :rules="rules" :model="accountLoginForm" autocomplete="off">
<AFormItem <AFormItem
class="login-form-item" class="login-form-item"
name="account"> name="account">
<span class="login-card-span">{{ t("login.account") }}</span> <span class="login-card-span">{{ t("login.account") }}</span>
<AInput v-model:value="accountLoginForm.account" class="login-form-input" size="large" <AInput v-model:value="accountLoginForm.account" class="login-form-input" size="large"
:placeholder=accountValidate allow-clear> :placeholder=accountValidate allow-clear autocomplete="off">
<template #prefix> <template #prefix>
<user-outlined/> <user-outlined/>
</template> </template>
@@ -90,7 +91,7 @@
<AFlex :vertical="true"> <AFlex :vertical="true">
<span class="login-card-span">{{ t("login.password") }}</span> <span class="login-card-span">{{ t("login.password") }}</span>
<AInputPassword v-model:value="accountLoginForm.password" class="login-form-input" size="large" <AInputPassword v-model:value="accountLoginForm.password" class="login-form-input" size="large"
:placeholder=passwordValidate allow-clear> :placeholder=passwordValidate allow-clear autocomplete="off">
<template #prefix> <template #prefix>
<SafetyOutlined/> <SafetyOutlined/>
</template> </template>
@@ -151,6 +152,8 @@ import LoginFooter from "@/views/Login/LoginFooter.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {checkRotatedCaptcha, getRotatedCaptchaData} from "@/api/captcha"; import {checkRotatedCaptcha, getRotatedCaptchaData} from "@/api/captcha";
import {message} from "ant-design-vue"; import {message} from "ant-design-vue";
import {phoneLoginApi, sendMessage} from "@/api/user";
import useStore from "@/store";
const router = useRouter(); const router = useRouter();
const {t} = useI18n(); const {t} = useI18n();
@@ -289,11 +292,22 @@ async function accountLoginSubmit() {
async function phoneLoginSubmit() { async function phoneLoginSubmit() {
phoneLoginFormRef.value phoneLoginFormRef.value
.validate() .validate()
.then(() => { .then(async () => {
console.log('values', phoneLoginForm); const res: any = await phoneLoginApi(phoneLoginForm);
if (res.code === 0 && res.success) {
const userStore = useStore().user;
const {uid, access_token, refresh_token, expires_at} = res.data;
userStore.user.userId = uid;
userStore.user.accessToken = access_token;
userStore.user.refreshToken = refresh_token;
userStore.user.expiresAt = expires_at;
message.success(t('login.loginSuccess'));
} else {
message.error(res.message);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log('error', error); console.error(error);
}); });
} }
@@ -303,9 +317,11 @@ async function phoneLoginSubmit() {
async function getRotateCaptcha() { async function getRotateCaptcha() {
const data: any = await getRotatedCaptchaData(); const data: any = await getRotatedCaptchaData();
if (data.code === 0 && data.data) { if (data.code === 0 && data.data) {
captchaData.image = data.data.image; const {angle, image, thumb, key} = data.data;
captchaData.thumb = data.data.thumb; captchaData.angle = angle;
captchaData.key = data.data.key; captchaData.image = image;
captchaData.thumb = thumb;
captchaData.key = key;
} else { } else {
message.error(t('login.systemError')); message.error(t('login.systemError'));
} }
@@ -324,9 +340,11 @@ async function checkCaptcha(angle: number) {
} else { } else {
const result: any = await checkRotatedCaptcha(angle, captchaData.key); const result: any = await checkRotatedCaptcha(angle, captchaData.key);
if (result.code === 0 && result.success) { if (result.code === 0 && result.success) {
message.success(t('login.captchaSuccess'));
showRotateCaptcha.value = false; showRotateCaptcha.value = false;
countDown(); const result: boolean = await sendMessageByPhone();
if (result) {
countDown();
}
} else if (result.code === 1011) { } else if (result.code === 1011) {
message.error(t('login.captchaExpired')); message.error(t('login.captchaExpired'));
getRotateCaptcha().then(() => { getRotateCaptcha().then(() => {
@@ -346,6 +364,21 @@ async function checkCaptcha(angle: number) {
async function closeRotateCaptcha() { async function closeRotateCaptcha() {
showRotateCaptcha.value = false; showRotateCaptcha.value = false;
} }
/**
* 发送手机验证码
*/
async function sendMessageByPhone(): Promise<boolean> {
const phone: string = phoneLoginForm.phone as string;
const res: any = await sendMessage(phone);
if (res.code === 0 && res.success) {
message.success(t('login.sendCaptchaSuccess'));
return true;
} else {
message.error(res.data);
return false;
}
}
</script> </script>
<style src="./index.scss" scoped> <style src="./index.scss" scoped>
@import "@/assets/styles/global.scss"; @import "@/assets/styles/global.scss";