fix: fix router cannot change bug

This commit is contained in:
Litrix 2024-12-09 20:43:08 +08:00
parent 57e7825f74
commit dc2f094b6f
12 changed files with 197 additions and 190 deletions

BIN
bun.lockb Normal file

Binary file not shown.

71
components.d.ts vendored
View File

@ -7,39 +7,42 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BackgroundComp: (typeof import('./src/components/BackgroundComp.vue'))['default'];
ElAvatar: (typeof import('element-plus/es'))['ElAvatar'];
ElButton: (typeof import('element-plus/es'))['ElButton'];
ElContainer: (typeof import('element-plus/es'))['ElContainer'];
ElDialog: (typeof import('element-plus/es'))['ElDialog'];
ElDrawer: (typeof import('element-plus/es'))['ElDrawer'];
ElDropdown: (typeof import('element-plus/es'))['ElDropdown'];
ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem'];
ElDropdownMenu: (typeof import('element-plus/es'))['ElDropdownMenu'];
ElForm: (typeof import('element-plus/es'))['ElForm'];
ElFormItem: (typeof import('element-plus/es'))['ElFormItem'];
ElHeader: (typeof import('element-plus/es'))['ElHeader'];
ElIcon: (typeof import('element-plus/es'))['ElIcon'];
ElImage: (typeof import('element-plus/es'))['ElImage'];
ElInput: (typeof import('element-plus/es'))['ElInput'];
ElInputNumber: (typeof import('element-plus/es'))['ElInputNumber'];
ElMain: (typeof import('element-plus/es'))['ElMain'];
ElMenu: (typeof import('element-plus/es'))['ElMenu'];
ElMenuItem: (typeof import('element-plus/es'))['ElMenuItem'];
ElPopover: (typeof import('element-plus/es'))['ElPopover'];
ElTabPane: (typeof import('element-plus/es'))['ElTabPane'];
ElTabs: (typeof import('element-plus/es'))['ElTabs'];
Game2048: (typeof import('./src/components/Game2048.vue'))['default'];
Game2048Button: (typeof import('./src/components/Game2048Button.vue'))['default'];
Game2048Score: (typeof import('./src/components/Game2048Score.vue'))['default'];
IconCsLoading: (typeof import('~icons/cs/loading'))['default'];
IconCsLock: (typeof import('~icons/cs/lock'))['default'];
IconCsUser: (typeof import('~icons/cs/user'))['default'];
IconCsValidate: (typeof import('~icons/cs/validate'))['default'];
IconEpLoading: (typeof import('~icons/ep/loading'))['default'];
IconEpUserFilled: (typeof import('~icons/ep/user-filled'))['default'];
RouterLink: (typeof import('vue-router'))['RouterLink'];
RouterView: (typeof import('vue-router'))['RouterView'];
VerifyInput: (typeof import('./src/components/VerifyInput.vue'))['default'];
BackgroundComp: typeof import('./src/components/BackgroundComp.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: (typeof import('element-plus/es'))['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: (typeof import('element-plus/es'))['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: (typeof import('element-plus/es'))['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
Game2048: typeof import('./src/components/Game2048.vue')['default']
Game2048Button: typeof import('./src/components/Game2048Button.vue')['default']
Game2048Score: typeof import('./src/components/Game2048Score.vue')['default']
IconCsLoading: typeof import('~icons/cs/loading')['default']
IconCsLock: typeof import('~icons/cs/lock')['default']
IconCsUser: typeof import('~icons/cs/user')['default']
IconCsValidate: typeof import('~icons/cs/validate')['default']
IconEpLoading: typeof import('~icons/ep/loading')['default']
IconEpUserFilled: typeof import('~icons/ep/user-filled')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VerifyInput: typeof import('./src/components/VerifyInput.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -14,40 +14,42 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.9.0",
"axios": "^1.6.8",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"element-plus": "^2.7.0",
"element-plus": "^2.9.0",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"mitt": "^3.0.1",
"pinia": "^2.3.0",
"vfonts": "^0.0.3",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"zod": "^3.22.4"
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.15",
"@rushstack/eslint-patch": "^1.10.2",
"@iconify-json/ep": "^1.2.1",
"@rushstack/eslint-patch": "^1.10.4",
"@tsconfig/node20": "^20.1.4",
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.7",
"@vitejs/plugin-vue": "^5.0.4",
"@types/node": "^20.17.9",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.24.1",
"npm-run-all2": "^6.1.2",
"prettier": "^3.2.5",
"sass": "^1.75.0",
"typescript": "^5.5.3",
"unplugin-auto-import": "^0.17.5",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.32.0",
"npm-run-all2": "^6.2.6",
"prettier": "^3.4.2",
"sass": "^1.82.0",
"typescript": "^5.7.2",
"unplugin-auto-import": "^0.17.8",
"unplugin-icons": "^0.18.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"vite": "^5.4.11",
"vue-tsc": "^1.8.27"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -19,7 +19,7 @@
<img alt="2048" class="header-menu-image" src="@/assets/2048.png" />
</el-menu-item>
</el-menu>
<template v-if="userStore.userInfo !== null">
<template v-if="userStore.userInfo && userStore.userInfo.id !== -1">
<div class="username">
{{ userStore.userInfo.name }}
</div>
@ -27,11 +27,11 @@
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
<template #dropdown>
<el-dropdown-menu>
<router-link v-slot="{ navigate }" custom to="/user">
<!-- <router-link v-slot="{ navigate }" custom to="/user">
<el-dropdown-item :icon="UserFilled" @click="navigate()">
个人主页
</el-dropdown-item>
</router-link>
</router-link> -->
<el-dropdown-item :icon="CloseBold" @click="logout">
退出登录
</el-dropdown-item>
@ -57,14 +57,14 @@
</transition>
</div>
</el-header>
<el-main>
<el-main v-loading="!!pageStore.pageLoadingCount">
<router-view v-slot="{ Component: comp }">
<transition appear mode="out-in" name="page-root">
<component :is="comp" />
</transition>
</router-view>
<transition name="page-root"></transition>
<div
<!-- <div
:style="
pageStore.pageLoadingCount
? {
@ -85,7 +85,7 @@
<icon-cs-loading />
</el-icon>
</div>
</div>
</div> -->
</el-main>
</el-container>
<el-dialog
@ -358,7 +358,7 @@
}
</style>
<script lang="ts" setup>
import axiosInstance from '@/api';
import axiosInstance, { type RawResp } from '@/api';
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
@ -453,24 +453,21 @@ interface LoginParams {
code: string;
}
async function login(params: LoginParams) {
try {
const loginResp = loginResponseSchema.parse(
(await axiosInstance.post('/api/user/login', params)).data
);
if (loginResp.type === 'error') {
ElMessage.error(loginErrorMessage(loginResp.code, loginResp.msg));
return false;
}
return true;
} catch (e) {
if (e instanceof AxiosError) {
async function login(params: LoginParams): Promise<boolean> {
const loginRespRaw = await axiosInstance
.post<RawResp>('/api/user/login', params)
.then((r) => r.data)
.catch((e: AxiosError) => {
ElMessage.error(loginErrorMessage(e.code, e.message));
} else {
ElMessage.error('error');
}
});
if (!loginRespRaw) return false;
const loginResp = loginResponseSchema.parse(loginRespRaw);
if (loginResp.type === 'error') {
ElMessage.error(loginErrorMessage(loginResp.code, loginResp.msg));
return false;
}
await userStore.updateSelfUserInfo(true);
return true;
}
async function submitLoginForm() {
@ -492,7 +489,6 @@ async function submitLoginForm() {
verifyImage: { key },
verifyCode: code
} = loginFormData;
console.log(loginFormData);
succeed = await login({
username,
password,
@ -501,7 +497,7 @@ async function submitLoginForm() {
});
} finally {
if (succeed) {
router.push({ path: route.fullPath, force: true });
showLoginRegisterDialog.value = false;
} else {
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields('verifyCode');
@ -560,7 +556,7 @@ async function submitRegisterForm() {
succeed = await register({ username, password, key, code, auth: 1 });
} finally {
if (succeed) {
userStore.userInfoStatus = 'unInitialized';
userStore.userInfoStatus = 'uninitialized';
router.push({ path: route.fullPath, force: true });
} else {
registerFormData.verifyImage = 'none';
@ -572,6 +568,6 @@ async function submitRegisterForm() {
async function logout() {
userStore.$reset();
router.push({ path: route.fullPath, force: true });
await userStore.updateSelfUserInfo(true);
}
</script>

View File

@ -1,7 +1,7 @@
import { useUserStore } from '@/stores';
import axios from 'axios';
const baseURL = 'http://wzpmc.cn:18080/';
const baseURL = 'https://wzpmc.cn:18080/';
const axiosInstance = axios.create({
baseURL
});
@ -15,9 +15,10 @@ axiosInstance.interceptors.request.use((config) => {
axiosInstance.interceptors.response.use((response) => {
const userStore = useUserStore();
const authorization = response.headers['set-authorization'] as string | undefined;
if (authorization !== undefined) {
if (authorization) {
userStore.token = authorization;
}
return response;
});
export default axiosInstance;
export type RawResp = Record<string, unknown>;

7
src/bus.ts Normal file
View File

@ -0,0 +1,7 @@
import mitt from 'mitt';
type Events = {
userDataUpdateSucceed: void;
userDataUpdateFailed: void;
};
export const bus = mitt<Events>();

View File

@ -1,11 +1,7 @@
import axiosInstance from '@/api';
import { type SucceedUserInfoResponse, userInfoResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { type PageErrorReason, usePageStore } from '@/stores/page';
import { errorMessage, IdPool, waitRef } from '@/utils';
import { IdPool, waitRef } from '@/utils';
import { useThrottleFn } from '@vueuse/core';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { toRef } from 'vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { RoutePermissionId } from './permissions';
@ -13,7 +9,6 @@ import { RoutePermissionId } from './permissions';
export * from './permissions';
declare module 'vue-router' {
// noinspection JSUnusedGlobalSymbols
interface RouteMeta {
errorRouteName?: string;
routeId?: number;
@ -69,69 +64,41 @@ const router = createRouter({
routes: routes
});
async function getUserInfoResponse(
showErrorMessage: boolean
): Promise<SucceedUserInfoResponse | undefined> {
try {
const userInfoResponse = userInfoResponseSchema.parse(
(await axiosInstance.get('/api/user/info')).data
);
if (userInfoResponse.type === 'error') {
if (showErrorMessage) {
ElMessage.error(
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
);
}
return;
}
return userInfoResponse;
} catch (e) {
if (showErrorMessage && e instanceof AxiosError) {
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
}
}
}
router.beforeEach(async (...[to, , next]) => {
router.beforeEach(async (to, from) => {
const userStore = useUserStore();
const pageStore = usePageStore();
const { permissionId } = to.meta;
pageStore.setNewRouteId(to);
switch (userStore.userInfoStatus) {
case 'unInitialized':
case 'failed': {
if (permissionId === undefined) {
next();
}
userStore.userInfoStatus = 'initializing';
const userInfoResponse = await getUserInfoResponse(true);
if (userInfoResponse === undefined) {
userStore.userInfoStatus = 'failed';
if (permissionId === undefined) {
return;
try {
switch (userStore.userInfoStatus) {
case 'uninitialized': {
const succeed = await userStore.updateSelfUserInfo(true);
if (!succeed) {
if (permissionId === undefined) {
return;
}
return pageStore.createTempErrorRoute(
{
type: 'networkError',
originalPath: to.fullPath
},
to
);
}
pageStore.removeRouteId(to);
return pageStore.createTempErrorRoute(
{
type: 'networkError',
originalPath: to.fullPath
},
to
);
break;
}
userStore.updateUserInfo(userInfoResponse.data);
userStore.userInfoStatus = 'initialized';
break;
case 'initializing':
return false;
}
case 'initializing':
return false;
}
if (permissionId !== undefined) {
if (userStore.hasPermission(permissionId)) {
return true;
} else {
return pageStore.createTempErrorRoute({ type: 'noPermission' });
if (permissionId !== undefined) {
if (userStore.hasPermission(permissionId)) {
return true;
} else {
return pageStore.createTempErrorRoute({ type: 'noPermission' }, from);
}
}
} finally {
pageStore.removeRouteId(to);
}
});
router.beforeEach((to) => {
@ -144,28 +111,24 @@ router.beforeEach((to) => {
pageStore.pageErrorReason = undefined;
}
});
router.afterEach(
useThrottleFn(
async (to) => {
const userStore = useUserStore();
const pageStore = usePageStore();
if (['initialized', 'failed'].includes(userStore.userInfoStatus)) {
await waitRef(toRef(userStore, 'userInfoStatus'), 'initialized', 'failed');
}
const userInfoResponse = await getUserInfoResponse(false);
if (userInfoResponse === undefined) {
return;
}
userStore.updateUserInfo(userInfoResponse.data);
const { permissionId } = to.meta;
if (permissionId !== undefined && !userStore.hasPermission(permissionId)) {
router.push(pageStore.createTempErrorRoute({ type: 'noPermission' }));
}
},
10000,
true
)
);
// router.afterEach(
// useThrottleFn(
// async (to) => {
// const userStore = useUserStore();
// const pageStore = usePageStore();
// if (['initialized', 'failed'].includes(userStore.userInfoStatus)) {
// await waitRef(toRef(userStore, 'userInfoStatus'), 'initialized');
// }
// await userStore.updateSelfUserInfo(false, true);
// const { permissionId } = to.meta;
// if (permissionId !== undefined && !userStore.hasPermission(permissionId)) {
// router.push(pageStore.createTempErrorRoute({ type: 'noPermission' }));
// }
// },
// 10000,
// true
// )
// );
router.afterEach((to) => {
const pageStore = usePageStore();
pageStore.removeRouteId(to);

View File

@ -46,7 +46,7 @@ export const usePageStore = defineStore('page-store', () => {
}
| undefined
>,
currentRoute: RouteLocationNormalized = useRoute(),
currentRoute: RouteLocationNormalized | undefined = useRoute(),
root: boolean = false
): RouteLocationRaw {
pageErrorReason.value = reason;

View File

@ -1,8 +1,12 @@
import axiosInstance, { type RawResp } from '@/api';
import type { UserInfo } from '@/schemas';
import { type idAndNameSchema } from '@/schemas';
import { userInfoResponseSchema, type idAndNameSchema } from '@/schemas';
import { errorMessage } from '@/utils';
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { z } from 'zod';
type Permission = z.infer<typeof idAndNameSchema>;
@ -13,32 +17,45 @@ export const useUserStore = defineStore('user', () => {
const userInfo = useLocalStorage<UserInfo | null>('user-info', null, {
serializer: StorageSerializers.object
});
const permissions = reactive<Permission[]>([]);
const userInfoStatus = ref<'unInitialized' | 'initializing' | 'initialized' | 'failed'>(
'unInitialized'
const permissions = computed<Permission[]>(() => userInfo.value?.auth.permissions ?? []);
const userInfoStatus = ref<'uninitialized' | 'initializing' | 'initialized'>('uninitialized');
watch(
userInfo,
(info) => {
userInfoStatus.value = info ? 'initialized' : 'uninitialized';
},
{ flush: 'sync' }
);
/**
* .
* @param info
*/
function updateUserInfo(info: UserInfo) {
if (token.value !== null) {
userInfo.value = info;
async function updateSelfUserInfo(
showErrorMessage: boolean,
silent: boolean = false
): Promise<boolean> {
if (!silent) userInfoStatus.value = 'initializing';
const raw = await axiosInstance
.get<RawResp>('/api/user/info')
.then((r) => r.data)
.catch((e: AxiosError) => {
if (!showErrorMessage) return;
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
});
const resp = userInfoResponseSchema.parse(raw);
if (resp.type === 'error') {
if (showErrorMessage) {
ElMessage.error(errorMessage('获取用户信息失败', resp.code, resp.msg));
}
return false;
}
permissions.splice(0, permissions.length, ...info.auth.permissions);
console.log(permissions);
userInfo.value = resp.data;
return true;
}
function hasPermission(permissionId: number) {
return permissions.find((permission) => permission.id === permissionId);
return permissions.value.find((permission) => permission.id === permissionId);
}
function $reset() {
token.value = null;
userInfo.value = null;
permissions.splice(0);
userInfoStatus.value = 'unInitialized';
userInfoStatus.value = 'uninitialized';
}
return {
@ -46,7 +63,7 @@ export const useUserStore = defineStore('user', () => {
userInfo,
permissions,
userInfoStatus,
updateUserInfo,
updateSelfUserInfo,
hasPermission,
$reset
};

View File

@ -1,5 +1,14 @@
<template>
<div class="page-root">这是主页</div>
<div class="page-root">
<div class="page" v-if="userStore.userInfo">
<div>用户名{{ userStore.userInfo.name }}</div>
<div>id{{ userStore.userInfo.id }}</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { useUserStore } from '@/stores/user.js';
const userStore = useUserStore();
</script>

View File

@ -52,7 +52,7 @@
import axiosInstance from '@/api';
import { type UserInfo, userInfoResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { computed, onBeforeMount, ref } from 'vue';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useUserStore();
@ -68,7 +68,12 @@ const avatar = ref<string>();
async function getAvatar(sha1: string) {
const avatarResp = await axiosInstance.get(`/api/user/avatar${sha1}`);
}
watch(
() => userStore.userInfo,
(info) => {
userInfo.value = info ?? undefined;
}
);
onBeforeMount(async () => {
if (id == undefined) {
userInfo.value = userStore.userInfo!;

View File

@ -45,5 +45,9 @@ export default defineConfig({
'@': resolve('./src')
// vue: 'vue/dist/vue.esm-bundler.js'
}
},
server: {
host: '0.0.0.0',
port: 18081
}
});