diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000..e58a994 Binary files /dev/null and b/bun.lockb differ diff --git a/components.d.ts b/components.d.ts index 9207363..bcea3bc 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] } } diff --git a/package.json b/package.json index 5bb2430..0a42da1 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/App.vue b/src/App.vue index 5023c95..81806a5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,7 +19,7 @@ - + {{ userStore.userInfo.name }} @@ -27,11 +27,11 @@ - + 退出登录 @@ -57,14 +57,14 @@ - + - -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 { + const loginRespRaw = await axiosInstance + .post('/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); } diff --git a/src/api/index.ts b/src/api/index.ts index f44a468..0fdf786 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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; diff --git a/src/bus.ts b/src/bus.ts new file mode 100644 index 0000000..a891669 --- /dev/null +++ b/src/bus.ts @@ -0,0 +1,7 @@ +import mitt from 'mitt'; + +type Events = { + userDataUpdateSucceed: void; + userDataUpdateFailed: void; +}; +export const bus = mitt(); diff --git a/src/router/index.ts b/src/router/index.ts index 145b2df..b976005 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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 { - 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); diff --git a/src/stores/page.ts b/src/stores/page.ts index 29faecb..53c49c1 100644 --- a/src/stores/page.ts +++ b/src/stores/page.ts @@ -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; diff --git a/src/stores/user.ts b/src/stores/user.ts index e3842ad..fd929a5 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -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; @@ -13,32 +17,45 @@ export const useUserStore = defineStore('user', () => { const userInfo = useLocalStorage('user-info', null, { serializer: StorageSerializers.object }); - const permissions = reactive([]); - const userInfoStatus = ref<'unInitialized' | 'initializing' | 'initialized' | 'failed'>( - 'unInitialized' + const permissions = computed(() => 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 { + if (!silent) userInfoStatus.value = 'initializing'; + const raw = await axiosInstance + .get('/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 }; diff --git a/src/views/MainPage.vue b/src/views/MainPage.vue index aad636e..5f618ce 100644 --- a/src/views/MainPage.vue +++ b/src/views/MainPage.vue @@ -1,5 +1,14 @@ - 这是主页 + + + 用户名:{{ userStore.userInfo.name }} + id:{{ userStore.userInfo.id }} + + - + diff --git a/src/views/UserPage.vue b/src/views/UserPage.vue index 252f896..5534a06 100644 --- a/src/views/UserPage.vue +++ b/src/views/UserPage.vue @@ -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(); 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!; diff --git a/vite.config.ts b/vite.config.ts index 40972cb..3bb8d20 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -45,5 +45,9 @@ export default defineConfig({ '@': resolve('./src') // vue: 'vue/dist/vue.esm-bundler.js' } + }, + server: { + host: '0.0.0.0', + port: 18081 } });