使用回调函数解决Promise异步问题
问题:使用pure-admin-vue项目进行登录,想要代码执行顺序是:1. 在登录接口调用成功后,进行设置本地存储信息setToken()方法操作, 2. 但是setToken方法中存在获取用户信息的接口getUserInfo(), 3. 等获取成功信息后再路由初始化配置 initRouter()
但是项目中确实执行了登录后,就进行了路由初始化配置,导致第一次登录后页面不进行跳转。第二次执行后才跳转。
总共三个页面:1. src\views\login\index.vue
<script setup lang="ts"> import { useI18n } from "vue-i18n"; import Motion from "./utils/motion"; import { useRouter } from "vue-router"; import { message } from "@/utils/message"; import { loginRules } from "./utils/rule"; import TypeIt from "@/components/ReTypeit"; import { debounce } from "@pureadmin/utils"; import { useNav } from "@/layout/hooks/useNav"; import { useEventListener } from "@vueuse/core"; import type { FormInstance } from "element-plus"; import { $t, transformI18n } from "@/plugins/i18n"; import { operates, thirdParty } from "./utils/enums"; import { useLayout } from "@/layout/hooks/useLayout"; import LoginPhone from "./components/LoginPhone.vue"; import LoginRegist from "./components/LoginRegist.vue"; import LoginUpdate from "./components/LoginUpdate.vue"; import LoginQrCode from "./components/LoginQrCode.vue"; import { useUserStoreHook } from "@/store/modules/user"; import { initRouter, getTopMenu } from "@/router/utils"; import { bg, avatar, illustration } from "./utils/static"; import { ReImageVerify } from "@/components/ReImageVerify"; import { ref, toRaw, reactive, watch, computed } from "vue"; import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useTranslationLang } from "@/layout/hooks/useTranslationLang"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import dayIcon from "@/assets/svg/day.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component"; import globalization from "@/assets/svg/globalization.svg?component"; import Lock from "~icons/ri/lock-fill"; import Check from "~icons/ep/check"; import User from "~icons/ri/user-3-fill"; import Info from "~icons/ri/information-line"; import Keyhole from "~icons/ri/shield-keyhole-line"; import { Encrypt, Decrypt } from "@/utils/aes"; import { getAesKey, getIvKey } from "@/utils/config"; defineOptions({ name: "Login" }); const imgCode = ref(""); const loginDay = ref(7); const router = useRouter(); const loading = ref(false); const checked = ref(false); const disabled = ref(false); const ruleFormRef = ref<FormInstance>(); const currentPage = computed(() => { return useUserStoreHook().currentPage; }); const { t } = useI18n(); const { initStorage } = useLayout(); initStorage(); const { dataTheme, themeMode, dataThemeChange, setLayoutThemeColor } = useDataThemeChange(); dataThemeChange(themeMode.value); const { title, getDropdownItemStyle, getDropdownItemClass } = useNav(); const { locale, translationCh, translationEn } = useTranslationLang(); const ruleForm = reactive({ username: "admin", password: "123456", verifyCode: "" }); const onLogin = async (formEl: FormInstance | undefined) => { if (!formEl) return; await formEl.validate(valid => { if (valid) { loading.value = true; useUserStoreHook() .loginByUsername({ username: ruleForm.username, // password: ruleForm.password, // password: Encrypt(ruleForm.password, getAesKey(), getIvKey()), encryptPassword: Encrypt( ruleForm.password, "w8ghb7kyuH15ykby", "GcxQYCWvHQW58XAD" ) }) .then(res => { // console.log("打印", res); if (res.msg == "success" || res.success) { console.log("路由初始化"); // 获取后端路由; return initRouter().then(() => { disabled.value = true; router .push(getTopMenu(true).path) .then(() => { message(t("login.pureLoginSuccess"), { type: "success" }); }) .finally(() => (disabled.value = false)); }); } else { console.log("路由跳转失败哦"); message(t("login.pureLoginFail"), { type: "error" }); } }) .finally(() => (loading.value = false)); } }); }; const immediateDebounce: any = debounce( formRef => onLogin(formRef), 1000, true ); useEventListener(document, "keydown", ({ code }) => { if ( ["Enter", "NumpadEnter"].includes(code) && !disabled.value && !loading.value ) immediateDebounce(ruleFormRef.value); }); watch(imgCode, value => { useUserStoreHook().SET_VERIFYCODE(value); }); watch(checked, bool => { useUserStoreHook().SET_ISREMEMBERED(bool); }); watch(loginDay, value => { useUserStoreHook().SET_LOGINDAY(value); }); </script> <template> <!-- 登录框页面 --> <div class="select-none"> <img :src="bg" class="wave" /> <div class="flex-c absolute right-5 top-3"> <!-- 主题 --> <!-- <el-switch v-model="dataTheme" inline-prompt :active-icon="dayIcon" :inactive-icon="darkIcon" @change="dataThemeChange" /> --> <!-- 国际化 --> <!-- <el-dropdown trigger="click"> <globalization class="hover:text-primary hover:bg-[transparent]! w-[20px] h-[20px] ml-1.5 cursor-pointer outline-hidden duration-300" /> <template #dropdown> <el-dropdown-menu class="translation"> <el-dropdown-item :style="getDropdownItemStyle(locale, 'zh')" :class="['dark:text-white!', getDropdownItemClass(locale, 'zh')]" @click="translationCh" > <IconifyIconOffline v-show="locale === 'zh'" class="check-zh" :icon="Check" /> 简体中文 </el-dropdown-item> <el-dropdown-item :style="getDropdownItemStyle(locale, 'en')" :class="['dark:text-white!', getDropdownItemClass(locale, 'en')]" @click="translationEn" > <span v-show="locale === 'en'" class="check-en"> <IconifyIconOffline :icon="Check" /> </span> English </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> --> </div> <div class="login-container"> <div class="img"> <component :is="toRaw(illustration)" /> </div> <div class="login-box"> <div class="login-form"> <!-- 图标矢量图 --> <avatar class="avatar" /> <!-- 标题Title --> <Motion> <h2 class="outline-hidden"> <TypeIt :options="{ strings: [title], cursor: false, speed: 100 }" /> </h2> </Motion> <el-form v-if="currentPage === 0" ref="ruleFormRef" :model="ruleForm" :rules="loginRules" size="large" > <Motion :delay="100"> <el-form-item :rules="[ { required: true, message: transformI18n($t('login.pureUsernameReg')), trigger: 'blur' } ]" prop="username" > <el-input v-model="ruleForm.username" clearable :placeholder="t('login.pureUsername')" :prefix-icon="useRenderIcon(User)" /> </el-form-item> </Motion> <Motion :delay="150"> <el-form-item prop="password"> <el-input v-model="ruleForm.password" clearable show-password :placeholder="t('login.purePassword')" :prefix-icon="useRenderIcon(Lock)" /> </el-form-item> </Motion> <Motion :delay="200"> <el-form-item prop="verifyCode"> <el-input v-model="ruleForm.verifyCode" clearable :placeholder="t('login.pureVerifyCode')" :prefix-icon="useRenderIcon(Keyhole)" > <template v-slot:append> <ReImageVerify v-model:code="imgCode" /> </template> </el-input> </el-form-item> </Motion> <Motion :delay="250"> <el-form-item> <div class="w-full h-[20px] flex justify-between items-center"> <el-checkbox v-model="checked"> <span class="flex"> <select v-model="loginDay" :style="{ width: loginDay < 10 ? '10px' : '16px', outline: 'none', background: 'none', appearance: 'none', border: 'none' }" > <option value="1">1</option> <option value="7">7</option> <option value="30">30</option> </select> {{ t("login.pureRemember") }} <IconifyIconOffline v-tippy="{ content: t('login.pureRememberInfo'), placement: 'top' }" :icon="Info" class="ml-1" /> </span> </el-checkbox> <el-button link type="primary" @click="useUserStoreHook().SET_CURRENTPAGE(4)" > {{ t("login.pureForget") }} </el-button> </div> <el-button class="w-full mt-4!" size="default" type="primary" :loading="loading" :disabled="disabled" @click="onLogin(ruleFormRef)" > {{ t("login.pureLogin") }} </el-button> </el-form-item> </Motion> <Motion :delay="300"> <el-form-item> <div class="w-full h-[20px] flex justify-between items-center"> <el-button v-for="(item, index) in operates" :key="index" class="w-full mt-4!" size="default" @click="useUserStoreHook().SET_CURRENTPAGE(index + 1)" > {{ t(item.title) }} </el-button> </div> </el-form-item> </Motion> </el-form> <!-- 手机号登录 --> <LoginPhone v-if="currentPage === 1" /> <!-- 二维码登录 --> <LoginQrCode v-if="currentPage === 2" /> <!-- 注册 --> <LoginRegist v-if="currentPage === 3" /> <!-- 忘记密码 --> <LoginUpdate v-if="currentPage === 4" /> </div> </div> </div> <div class="w-full flex-c absolute bottom-3 text-sm text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]" > Copyright © 2020-present <a class="hover:text-primary!" href="https://github.com/pure-admin" target="_blank" > {{ title }} </a> </div> </div> </template> <style scoped> @import url("@/style/login.css"); </style> <style lang="scss" scoped> :deep(.el-input-group__append, .el-input-group__prepend) { padding: 0; } .translation { ::v-deep(.el-dropdown-menu__item) { padding: 5px 40px; } .check-zh { position: absolute; left: 20px; } .check-en { position: absolute; left: 20px; } } .select-none { width: 100%; height: 100vh; /* 视窗高度 */ background-image: radial-gradient( circle, rgba(81, 126, 168, 0.7) 0%, rgba(177, 121, 121, 0.3) 50%, transparent 70% ); background-size: cover; animation: ripple 5s infinite; } @keyframes ripple { 0% { background-position: center; } 100% { background-position: 100% 100%; /* 使背景移动以创建动态效果 */ } } </style>
关键代码:
const onLogin = async (formEl: FormInstance | undefined) => { if (!formEl) return; await formEl.validate(valid => { if (valid) { loading.value = true; useUserStoreHook() .loginByUsername({ username: ruleForm.username, // password: ruleForm.password, encryptPassword: Encrypt( ruleForm.password, "w8ghb7kyuH15ykby", "GcxQYCWvHQW58XAD" ) }) .then(res => { if (res.msg == "success" || res.success) { console.log("路由初始化"); // 获取后端路由---这里未等待loginByUsername里面的异步接口(getUserInfo)执行完成,就直接调用了initRouter(),导致第一次登录不进行跳转
return initRouter().then(() => { disabled.value = true; router .push(getTopMenu(true).path) .then(() => { message(t("login.pureLoginSuccess"), { type: "success" }); }) .finally(() => (disabled.value = false)); }); } else { console.log("路由跳转失败哦"); message(t("login.pureLoginFail"), { type: "error" }); } }) .finally(() => (loading.value = false)); } }); };
第二个页面:src\store\modules\user.ts
import { defineStore } from "pinia";
import {
type userType,
store,
router,
resetRouter,
routerArrays,
storageLocal
} from "../utils";
import {
type UserResult,
type RefreshTokenResult,
getLogin,
logOutApi,
refreshTokenApi,
getUserInfo
} from "@/api/user";
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, setUserToken, removeToken, userKey } from "@/utils/auth";
export const useUserStore = defineStore("pure-user", {
state: (): userType => ({
// id
id: storageLocal().getItem<DataInfo<number>>(userKey)?.id ?? "",
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
// 用户名
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
// 昵称
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
// 手机号
mobile: storageLocal().getItem<DataInfo<number>>(userKey)?.mobile ?? "",
// 角色Id
roleId: storageLocal().getItem<DataInfo<number>>(userKey)?.roleId ?? "",
// 角色名称
roleName: storageLocal().getItem<DataInfo<number>>(userKey)?.roleName ?? "",
// 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 按钮级别权限
permissions:
storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [],
// 前端生成的验证码(按实际需求替换)
verifyCode: "",
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
currentPage: 0,
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天,默认7天
loginDay: 7
}),
actions: {
/** 存储ID */
SET_ID(id: any) {
this.id = id;
},
/** 存储头像 */
SET_AVATAR(avatar: string) {
this.avatar = avatar;
},
/** 存储用户名 */
SET_USERNAME(username: string) {
this.username = username;
},
/** 存储昵称 */
SET_NICKNAME(nickname: string) {
this.nickname = nickname;
},
/** 存储手机号 */
SET_MOBILE(mobile: any) {
this.mobile = mobile;
},
/** 存储角色id */
SET_ROLEID(roleId: any) {
this.roleId = roleId;
},
/** 存储角色名称 */
SET_ROLENAME(roleName: any) {
this.roleName = roleName;
},
/** 存储角色 */
SET_ROLES(roles: Array<string>) {
this.roles = roles;
},
/** 存储按钮级别权限 */
SET_PERMS(permissions: Array<string>) {
this.permissions = permissions;
},
/** 存储前端生成的验证码 */
SET_VERIFYCODE(verifyCode: string) {
this.verifyCode = verifyCode;
},
/** 存储登录页面显示哪个组件 */
SET_CURRENTPAGE(value: number) {
this.currentPage = value;
},
/** 存储是否勾选了登录页的免登录 */
SET_ISREMEMBERED(bool: boolean) {
this.isRemembered = bool;
},
/** 设置登录页的免登录存储几天 */
SET_LOGINDAY(value: number) {
this.loginDay = Number(value);
},
/** 登入 */
async loginByUsername(data) {
return new Promise<UserResult>((resolve, reject) => {
getLogin(data)
.then(data => {
// console.log('成功登录', data, data.body);
if (data?.msg) {
setToken(data.body, () => {
resolve(data); //通过回调解决 Promise
});
} else {
reject();
}
})
.catch(error => {
reject(error);
});
});
},
/** 登出(调用接口) */
logOut() {
return new Promise<UserResult>((resolve, reject) => {
logOutApi().then(data => {
// console.log('退出登录成功', data);
this.webLogOut()
})
.catch(error => {
// console.log('退出失败哦', error);
this.webLogOut()
});
});
},
/** 前端登出(不调用接口) */
webLogOut() {
this.username = "";
this.roles = [];
this.permissions = [];
removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
router.push("/login");
},
/** 刷新`token` */
async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data)
.then(data => {
if (data) {
setToken(data.data);
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
}
}
});
export function useUserStoreHook() {
return useUserStore(store);
}
第二个页面-关键代码:
/** 登入 */ async loginByUsername(data) { return new Promise<UserResult>((resolve, reject) => { getLogin(data) .then(data => { // console.log('成功登录', data, data.body); if (data?.msg) { // setToken(data.body) 这里使用promise的回调函数()=>{}进行操作,解决异步问题 setToken(data.body, () => { resolve(data); //这里之前没有写回调函数就会出现首次登录不跳转,通过回调解决 Promise }); } else { reject(); } }) .catch(error => { reject(error); }); }); },
第三个页面:src\utils\auth.ts
import Cookies from "js-cookie"; import { useUserStoreHook } from "@/store/modules/user"; import { storageLocal, isString, isIncludeAllChildren } from "@pureadmin/utils"; import { getUserInfo } from "@/api/user"; import { object } from "vue-types"; import { log } from "console"; export interface DataInfo<T> { /** token */ accessToken: string; /** `accessToken`的过期时间(时间戳) */ expires: T; /** 用于调用刷新accessToken的接口时所需的token */ refreshToken: string; /** id */ id: any; /** 头像 */ avatar?: string; /** 用户名 */ username?: string; /** 昵称 */ nickname?: string; /** 当前登录用户的角色 */ roles?: Array<string>; /** 当前登录用户的按钮级别权限 */ permissions?: Array<string>; } export const userKey = "user-info"; export const TokenKey = "authorized-token"; /** * 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统, * 从而支持多标签页打开已经登录的系统后无需再登录。 * 浏览器完全关闭后`multiple-tabs`将自动从`cookie`中销毁, * 再次打开浏览器需要重新登录系统 * */ export const multipleTabsKey = "multiple-tabs"; /** 获取`token` */ export function getToken(): DataInfo<number> { // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错 // console.log('获取token', TokenKey, Cookies.get(TokenKey), JSON.parse(Cookies.get(TokenKey)), storageLocal().getItem(userKey)); return Cookies.get(TokenKey) ? JSON.parse(Cookies.get(TokenKey)) : storageLocal().getItem(userKey); } /** * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案 * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间) * 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁) * 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) */ export async function setToken(data: DataInfo<Date>, logincb?) { // logincb ? 这个是形参 let expires = 0; // console.log('存储信息', data); const accessToken = data const refreshToken = data // const { accessToken, refreshToken } = data; // console.log('存储赋值', accessToken, refreshToken); const { isRemembered, loginDay } = useUserStoreHook(); expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可 const cookieString = JSON.stringify({ accessToken, expires, refreshToken }); expires > 0 ? Cookies.set(TokenKey, cookieString, { expires: (expires - Date.now()) / 86400000 }) : Cookies.set(TokenKey, cookieString); Cookies.set( multipleTabsKey, "true", isRemembered ? { expires: loginDay } : {} ); await getUserInfo().then(userData => { // console.log('用户列表获取成功', userData,); if (userData?.msg == 'success') { logincb && logincb(); setUserToken(userData.body, refreshToken, expires,); } }) .catch(error => { console.log('用户列表获取失败', error); reject(error); }); } // 存储用户信息 export function setUserToken(data: DataInfo<Date>, refreshToken: any, expires: any) { console.log('返回用户信息', data); function setUserKey({ id, avatar, username, nickname, mobile, roleId, roleName, roles, permissions }) { useUserStoreHook().SET_ID(id); useUserStoreHook().SET_AVATAR(avatar); useUserStoreHook().SET_USERNAME(username); useUserStoreHook().SET_NICKNAME(nickname); useUserStoreHook().SET_MOBILE(mobile); useUserStoreHook().SET_ROLEID(roleId); useUserStoreHook().SET_ROLENAME(roleName); useUserStoreHook().SET_ROLES(roles); useUserStoreHook().SET_PERMS(permissions); storageLocal().setItem(userKey, { refreshToken, expires, id, avatar, username, nickname, mobile, roleId, roleName, roles, permissions }); } // if (data.username && data.roles) { if (data.id && data.roleId) { const { username, roles } = data; setUserKey({ id: data?.id ?? "", avatar: data?.avatarUrl ?? "", username: data?.name ?? "", nickname: data?.nickname ?? "", mobile: data?.mobile ?? "", roleId: data?.roleId ?? "", roleName: data?.roleName ?? "", roles, permissions: data?.permissions ?? [] }); } else { const id = storageLocal().getItem<DataInfo<number>>(userKey)?.id ?? ""; const avatar = storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? ""; const username = storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? ""; const nickname = storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? ""; const mobile = storageLocal().getItem<DataInfo<number>>(userKey)?.mobile ?? ""; const roleId = storageLocal().getItem<DataInfo<number>>(userKey)?.roleId ?? []; const roleName = storageLocal().getItem<DataInfo<number>>(userKey)?.roleName ?? []; const roles = storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? []; const permissions = storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? []; setUserKey({ id, avatar, username, nickname, mobile, roleId, roleName, roles, permissions }); } } /** 删除`token`以及key值为`user-info`的localStorage信息 */ export function removeToken() { Cookies.remove(TokenKey); Cookies.remove(multipleTabsKey); storageLocal().removeItem(userKey); } /** 格式化token(jwt格式) */ export const formatToken = (token: string): string => { // return "Bearer " + token; return token; }; /** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/ export const hasPerms = (value: string | Array<string>): boolean => { if (!value) return false; const allPerms = "*:*:*"; const { permissions } = useUserStoreHook(); if (!permissions) return false; if (permissions.length === 1 && permissions[0] === allPerms) return true; const isAuths = isString(value) ? permissions.includes(value) : isIncludeAllChildren(value, permissions); return isAuths ? true : false; };
第三个页面关键代码:
export async function setToken(data: DataInfo<Date>, logincb?) { // logincb ? 这个是形参 let expires = 0; const accessToken = data const refreshToken = data // console.log('存储赋值', accessToken, refreshToken); const { isRemembered, loginDay } = useUserStoreHook(); expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可 const cookieString = JSON.stringify({ accessToken, expires, refreshToken }); expires > 0 ? Cookies.set(TokenKey, cookieString, { expires: (expires - Date.now()) / 86400000 }) : Cookies.set(TokenKey, cookieString); Cookies.set( multipleTabsKey, "true", isRemembered ? { expires: loginDay }: {} ); await getUserInfo().then(userData => { // console.log('用户列表获取成功', userData,); 这里形参logincb() if (userData?.msg == 'success') { logincb && logincb(); setUserToken(userData.body, refreshToken, expires,); } }) .catch(error => { console.log('用户列表获取失败', error); reject(error); }); }

浙公网安备 33010602011771号