🎈 perf: 优化菜单显示逻辑

This commit is contained in:
Litrix2 2024-12-21 22:17:07 +08:00
parent 9f333ecc87
commit feac940ecc
13 changed files with 90 additions and 56 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
VITE_DEBUG=false

5
env.d.ts vendored
View File

@ -1,4 +1,7 @@
/// <reference types="vite/client" />
/// <reference types="element-plus/global" />
/// <reference types="./components.d.ts" />
/// <reference types="./auto-imports.d.ts" />
type BooleanString = 'true' | 'false';
interface ImportMetaEnv {
readonly VITE_DEBUG: BooleanString;
}

View File

@ -13,11 +13,17 @@
<header-user @login="onLoginButtonClick" @logout="logout" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="UserFilled" @click="jumpToUserPage">个人主页</el-dropdown-item>
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<header-user v-else @login="onLoginButtonClick" @logout="logout" @click="onUserClick" />
<header-user
v-else
@login="onLoginButtonClick"
@logout="logout"
@click="showVerticalHeaderMenu = true"
/>
</el-header>
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
<el-container>
@ -38,19 +44,14 @@
:with-header="false"
>
<h3 v-show="!showAppTitle" class="app__title text-center">社团展示系统</h3>
<header-user v-if="userStore.logined" @login="showLoginRegisterDialog = true" />
<template v-if="userStore.logined">
<header-user @login="showLoginRegisterDialog = true" />
<el-button :icon="UserFilled" @click="jumpToUserPage">个人主页</el-button>
<el-button type="danger" :icon="CloseBold" @click="logout">退出登录</el-button>
</template>
<div v-else class="app-vertical-drawer__not-login-title flex justify-center items-center">
尚未登录
</div>
<el-button
v-if="userStore.logined"
class="app-vertical-drawer__logout"
type="primary"
:icon="CloseBold"
@click="logout"
>
退出登录
</el-button>
<header-menu mode="vertical" @select="showVerticalHeaderMenu = false" />
</el-drawer>
</template>
@ -68,7 +69,7 @@
&__not-login-title {
font-weight: bold;
}
&__logout {
.el-button {
margin: 0 10px;
}
}
@ -97,7 +98,7 @@
}
.app-router-view-enter-active,
.app-router-view-leave-active {
transition: opacity 0.25s ease;
transition: opacity 0.25s;
}
.app-router-view-enter-from,
@ -116,13 +117,14 @@ import LoginRegisterDialog, {
import { useMediaStore } from '@/stores/media';
import { usePageStore } from '@/stores/page.js';
import { useUserStore } from '@/stores/user';
import { CloseBold } from '@element-plus/icons-vue';
import { CloseBold, UserFilled } from '@element-plus/icons-vue';
import { useMediaQuery } from '@vueuse/core';
import type { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { provide, ref, watch } from 'vue';
import { loginResponseSchema, registerResponseSchema } from './schemas/response';
import router from '@/router';
const userStore = useUserStore();
const pageStore = usePageStore();
const { mdLess } = storeToRefs(useMediaStore());
@ -170,8 +172,14 @@ watch(mdLess, (v) => {
function onLoginButtonClick() {
showLoginRegisterDialog.value = true;
}
function onUserClick() {
showVerticalHeaderMenu.value = true;
function jumpToUserPage() {
showVerticalHeaderMenu.value = false;
router.push({
name: 'User',
params: {
id: userStore.userInfo!.id,
},
});
}
// onMounted(() => {
// userStore.token = null;

View File

@ -1,6 +1,6 @@
<template>
<el-menu-item class="festival-menu-item flex" :index="to">
<img :src="src" class="festival-menu-item__img" />
<el-menu-item class="festival-menu-item flex" :index="to.fullPath">
<img :src="imgURL" class="festival-menu-item__img" />
<div class="festival-menu-item__title">{{ title }}</div>
</el-menu-item>
</template>
@ -16,9 +16,6 @@
}
</style>
<script setup lang="ts">
defineProps<{
src: string;
title: string;
to?: string;
}>();
import type { MenuItem } from '@/components/app/HeaderMenu.vue';
defineProps<MenuItem>();
</script>

View File

@ -1,7 +1,6 @@
<template>
<el-menu
class="app-header-menu justify-end"
:class="[`app-header-menu--${mode}`]"
:mode="mode"
:default-active="$route.fullPath"
router
@ -10,8 +9,14 @@
<el-menu-item index="/">首页</el-menu-item>
<el-sub-menu class="festival-menu" index="festival">
<template #title>社团文化节</template>
<template v-for="{ src, title, to, vIf } of festivalMenuItems">
<festival-menu-item v-if="!vIf || vIf()" :key="to" :src="src" :title="title" :to="to" />
<template v-for="{ imgURL, title, to } of festivalMenuItems">
<festival-menu-item
v-if="isValid(to)"
:key="to.fullPath"
:imgURL="imgURL"
:title="title"
:to="to"
/>
</template>
</el-sub-menu>
</el-menu>
@ -19,33 +24,46 @@
<style lang="scss" scoped>
.app-header-menu {
--el-menu-horizontal-height: var(--header-height);
}
.app-header-menu--horizontal {
flex: 1;
margin-right: 10px;
}
.app-header-menu :is(.el-menu-item, .el-sub-menu__title) {
user-select: none;
--el-menu-bg-color: transparent;
--el-menu-border-color: transparent;
&.el-menu--horizontal {
flex: 1;
margin-right: 10px;
}
:is(.el-menu-item, .el-sub-menu__title) {
user-select: none;
}
}
</style>
<script lang="ts">
export interface MenuItem {
imgURL: string;
title: string;
to: RouteLocationGeneric;
}
</script>
<script setup lang="ts">
import FestivalMenuItem from '@/components/app/FestivalMenuItem.vue';
import router from '@/router';
import { useUserStore } from '@/stores/user';
import { reactive } from 'vue';
import type { RouteLocationGeneric } from 'vue-router';
const userStore = useUserStore();
const festivalMenuItems = reactive([
const festivalMenuItems = reactive<MenuItem[]>([
{
src: '/2048.png',
imgURL: '/2048.png',
title: '2048',
to: '/2048',
to: router.resolve('/2048')!,
},
{
src: '/gobang.svg',
imgURL: '/gobang.svg',
title: '五子棋',
to: '/gobang',
vIf: () => userStore.logined,
to: router.resolve('/gobang')!,
},
]);
const isValid = ({ meta: { shouldLogin, permissionId } }: RouteLocationGeneric) =>
(!shouldLogin || userStore.logined) && (!permissionId || userStore.permissions.has(permissionId));
defineProps<{
mode: 'horizontal' | 'vertical';
}>();

View File

@ -2,7 +2,7 @@
<el-dialog
v-model="show"
:fullscreen="smLess"
items-center
align-center
class="login-register-dialog"
width="400"
>

View File

@ -293,7 +293,7 @@ watch(gameStatus, (status) => {
message: `单个块分数达到${game2048Store.successNumber}`,
type: 'success',
duration: 3500,
offset: 50,
position: 'bottom-left',
});
break;
case 'failed':
@ -302,7 +302,7 @@ watch(gameStatus, (status) => {
message: '你无路可走',
type: 'error',
duration: 3500,
offset: 50,
position: 'bottom-left',
});
break;
}

View File

@ -3,8 +3,8 @@
class="create-gobang-room-dialog"
v-model="show"
title="创建房间"
items-center
:fullscreen="smLess"
align-center
>
<el-button type="success" @click="onCreateRoomButtonClick">创建房间</el-button>
</el-dialog>

View File

@ -14,14 +14,17 @@ declare module 'vue-router' {
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/MainPage.vue'),
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/UserPage.vue'),
},
{
path: '/club',
name: 'Club',
component: () => import('@/views/ClubPage.vue'),
meta: {
permissionId: RoutePermissionId.CLUB_PAGE,
@ -29,6 +32,7 @@ const routes: RouteRecordRaw[] = [
},
{
path: '/2048',
name: '2048',
component: () => import('@/views/Game2048Page.vue'),
},
{
@ -57,7 +61,7 @@ const routes: RouteRecordRaw[] = [
},
{
path: '/:path(.*)*',
name: 'notFound',
name: 'NotFound',
component: () => import('@/views/ErrorPage.vue'),
props: {
type: PageErrorType.NOT_FOUND,
@ -92,7 +96,7 @@ router.beforeEach(async (to) => {
return pageStore.createTempErrorRoute({ type: PageErrorType.NOT_LOGIN }, to);
}
if (permissionId) {
if (userStore.hasPermission(permissionId)) {
if (userStore.permissions.has(permissionId)) {
return true;
} else {
return pageStore.createTempErrorRoute({ type: PageErrorType.NO_PERMISSION }, to);

View File

@ -19,7 +19,12 @@ export const useUserStore = defineStore('user', () => {
const userInfo = useLocalStorage<UserInfo | null>('user-info', null, {
serializer: StorageSerializers.object,
});
const permissions = computed<Permission[]>(() => userInfo.value?.auth.permissions ?? []);
const permissions = computed(
() =>
new Map<number, Permission>(
(userInfo.value?.auth.permissions ?? []).map((permission) => [permission.id, permission]),
),
);
const initializing = ref(false);
const logined = computed(() => (userInfo.value && userInfo.value.id !== -1) ?? false);
watch(
@ -55,9 +60,7 @@ export const useUserStore = defineStore('user', () => {
initializing.value = false;
}
}
function hasPermission(permissionId: number) {
return permissions.value.find((permission) => permission.id === permissionId);
}
const logoutInterceptors = new Set<LogoutInterceptor>();
function addLogoutInterceptor(fn: LogoutInterceptor) {
logoutInterceptors.add(fn);
@ -79,7 +82,6 @@ export const useUserStore = defineStore('user', () => {
permissions,
initializing,
updateSelfUserInfo,
hasPermission,
logoutInterceptors,
addLogoutInterceptor,
removeLogoutInterceptor,

View File

@ -1,4 +1,4 @@
export type Future<T = void> = PromiseWithResolvers<T>;
export type MaybePromise<T> = T | Promise<T>;
export type MaybeArray<T, R extends boolean = false> = T | (R extends false ? T[] : readonly T[]);
export type Future<T = void> = PromiseWithResolvers<T>;
export type ValueOf<T extends object> = T[keyof T];

View File

@ -148,15 +148,15 @@ const relations = {
PlaceChessPiece: ['RoomInfo'],
ResetRoom: ['RoomInfo'],
} as const satisfies Relations<Request['name'], Resp['name']>;
const DEBUG = false;
export function useGobangSocket(
options: Omit<UseGameSocketOptions<Request, Resp, typeof relations>, 'url' | 'relations'>,
) {
return useGameSocket<Request, Resp>()(
Object.assign(options, {
url: DEBUG
? `ws://172.16.114.84:58080/chess/${useUserStore().token}`
: `wss://wzpmc.cn:18080/chess/${useUserStore().token}`,
url:
import.meta.env.VITE_DEBUG === 'true'
? `ws://172.16.114.84:58080/chess/${useUserStore().token}`
: `wss://wzpmc.cn:18080/chess/${useUserStore().token}`,
relations,
}),
);

View File

@ -545,6 +545,7 @@ const { send } = useGobangSocket({
},
error: {
PlayerJoin() {
canExit = true;
router.back();
return false;
},