feat: 主框架添加响应式支持

This commit is contained in:
Litrix2 2024-12-13 21:54:17 +08:00
parent f49c25ba9c
commit 24ff08605b
11 changed files with 204 additions and 110 deletions

10
auto-imports.d.ts vendored
View File

@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const IconEpAvatar: typeof import('~icons/ep/avatar')['default']
const IconEpCloseBold: typeof import('~icons/ep/close-bold')['default']
}

13
components.d.ts vendored
View File

@ -7,11 +7,14 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BackgroundComp: typeof import('./src/components/BackgroundComp.vue')['default']
'菜单': typeof import('./src/assets/icons/菜单.svg')['default']
AppHeaderMenu: typeof import('./src/components/app/AppHeaderMenu.vue')['default']
AppHeaderUser: typeof import('./src/components/app/AppHeaderUser.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']
@ -27,20 +30,16 @@ declare module 'vue' {
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
FestivalMenuItem: typeof import('./src/components/FestivalMenuItem.vue')['default']
Game2048: typeof import('./src/components/game2048/Game2048.vue')['default']
Game2048Button: typeof import('./src/components/game2048/Game2048Button.vue')['default']
Game2048Score: typeof import('./src/components/game2048/Game2048Score.vue')['default']
IconCsClub: typeof import('~icons/cs/club')['default']
IconCsLock: typeof import('~icons/cs/lock')['default']
IconCsMenu: typeof import('~icons/cs/menu')['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']
LoginRegisterDialog: typeof import('./src/components/LoginRegisterDialog.vue')['default']
Menu: typeof import('./src/assets/icons/Menu.svg')['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

@ -1,49 +1,32 @@
<template>
<el-container class="app__container">
<el-header class="app-header flex align-center">
<el-icon v-show="mobileScreen" size="30" @click="showVerticalHeaderMenu = true">
<icon-cs-menu />
</el-icon>
<router-link :custom="true" to="/" v-slot="{ navigate }">
<el-icon size="50" @click="navigate"><icon-cs-club /></el-icon>
<h3 class="app__title" @click="navigate">社团展示系统</h3>
<h3 v-show="!smallMobileScreen" class="app__title" @click="navigate">社团展示系统</h3>
</router-link>
<el-menu
class="app-header-menu justify-end"
mode="horizontal"
:default-active="$route.fullPath"
:router="true"
>
<el-menu-item index="/">首页</el-menu-item>
<el-sub-menu class="festival-menu" index="festival">
<template #title>社团文化节</template>
<festival-menu-item src="/2048.png" title="2048" to="/2048" />
<festival-menu-item
v-if="userStore.logined"
src="/gobang.svg"
title="五子棋"
to="/gobang"
/>
</el-sub-menu>
</el-menu>
<div class="app-header-user flex center" v-loading="userStore.initializing">
<template v-if="!userStore.initializing">
<!-- 仅收紧类型 -->
<template v-if="userInfo && userStore.logined">
<el-dropdown>
<div class="app-header-user__inner flex align-center">
<el-avatar :icon="userInfo.avatar ?? undefined"></el-avatar>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="app-header-user__username">
{{ userInfo.name }}
</div>
</template>
<el-button type="primary" v-else @click="showLoginRegisterDialog = true">登录</el-button>
<app-header-menu v-show="!mobileScreen" mode="horizontal" />
<el-dropdown v-if="!mobileScreen">
<app-header-user
@login="showLoginRegisterDialog = true"
@logout="logout"
@click="mobileScreen && (showVerticalHeaderMenu = true)"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</div>
</el-dropdown>
<app-header-user
v-else
@login="showLoginRegisterDialog = true"
@logout="logout"
@click="mobileScreen && (showVerticalHeaderMenu = true)"
/>
</el-header>
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
<el-container class="app-router-component__wrapper">
@ -56,7 +39,46 @@
</el-main>
</el-container>
<login-register-dialog v-model="showLoginRegisterDialog" />
<el-drawer
class="app-vertical-drawer"
v-model="showVerticalHeaderMenu"
direction="ltr"
size="250"
:with-header="false"
>
<app-header-user
v-if="userStore.logined"
@login="showLoginRegisterDialog = true"
@click="mobileScreen && (showVerticalHeaderMenu = true)"
/>
<el-button
v-if="userStore.logined"
class="app-vertical-drawer__logout"
type="primary"
:icon="CloseBold"
@click="logout"
>
退出登录
</el-button>
<app-header-menu mode="vertical" @select="showVerticalHeaderMenu = false" />
</el-drawer>
</template>
<style lang="scss">
.app-vertical-drawer {
--el-drawer-padding-primary: 0;
.el-drawer__body {
gap: 10px;
display: flex;
flex-direction: column;
}
.app-header-user {
margin-top: 10px;
}
&__logout {
margin: 0 10px;
}
}
</style>
<style lang="scss" scoped>
.app__container {
min-height: 100vh;
@ -66,29 +88,15 @@
--el-loading-spinner-size: 30px;
background-color: white;
border-bottom: 1px solid var(--el-border-color);
}
.app-header-menu__wrapper {
flex: 1;
}
.app-header-menu {
flex: 1;
--el-menu-horizontal-height: var(--header-height);
margin-right: 10px;
}
.app-header-menu :is(.el-menu-item, .el-sub-menu__title) {
user-select: none;
.app-header-user {
margin-left: auto;
}
}
.app-main {
padding: 0;
display: flex;
}
.app-header-user {
min-width: 50px;
gap: 10px;
}
.app-header-user__inner {
gap: 10px;
}
.app-router-view-enter-active,
.app-router-view-leave-active {
transition: opacity 0.4s ease;
@ -101,23 +109,22 @@
</style>
<script setup lang="ts">
import axiosInstance, { type RawResp } from '@/api/index';
import FestivalMenuItem from '@/components/FestivalMenuItem.vue';
import AppHeaderMenu from '@/components/app/AppHeaderMenu.vue';
import LoginRegisterDialog, {
loginImplKey,
registerImplKey,
} from '@/components/LoginRegisterDialog.vue';
import { usePageStore } from '@/stores/page.js';
import { useUserStore } from '@/stores/user';
import { CloseBold } from '@element-plus/icons-vue';
import type { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { provide, ref } from 'vue';
import { usePageStore } from '@/stores/page.js';
import { useUserStore } from '@/stores/user';
import { provide, ref, watch } from 'vue';
import { loginResponseSchema, registerResponseSchema } from './schemas/response';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const pageStore = usePageStore();
const { mobileScreen, smallMobileScreen } = storeToRefs(pageStore);
const showLoginRegisterDialog = ref(false);
provide(loginImplKey, async (params) => {
const loginRespRaw = await axiosInstance
@ -150,9 +157,18 @@ provide(registerImplKey, async (params) => {
return userStore.updateSelfUserInfo(true);
});
async function logout() {
showVerticalHeaderMenu.value = false;
userStore.$reset();
await userStore.updateSelfUserInfo(true);
}
const showVerticalHeaderMenu = ref(false);
watch(
() => mobileScreen,
(v) => {
if (v) return;
showVerticalHeaderMenu.value = false;
},
);
// onMounted(() => {
// userStore.token = null;
// userStore.updateSelfUserInfo(true);

View File

@ -17,6 +17,9 @@ body {
.flex {
display: flex !important;
}
.flex-column {
flex-direction: column;
}
.align-center {
align-items: center;
}

View File

@ -0,0 +1,6 @@
<svg t="1734095305704" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4438"
width="200" height="200">
<path
d="M170.666667 213.333333h682.666666v85.333334H170.666667V213.333333z m0 512h682.666666v85.333334H170.666667v-85.333334z m0-256h682.666666v85.333334H170.666667v-85.333334z"
fill="#444444" p-id="4439"></path>
</svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@ -129,7 +129,7 @@ export const registerImplKey = Symbol() as InjectionKey<
>;
</script>
<script lang="ts" setup>
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import VerifyInput, { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { inject, reactive, ref, watch, type InjectionKey } from 'vue';
const loginImpl = inject(loginImplKey, async () => false),

View File

@ -0,0 +1,53 @@
<template>
<el-menu
class="app-header-menu justify-end"
:class="[`app-header-menu--${mode}`]"
:mode="mode"
:default-active="$route.fullPath"
:router="true"
@select="$emit('select')"
>
<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>
</el-sub-menu>
</el-menu>
</template>
<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;
}
</style>
<script setup lang="ts">
import FestivalMenuItem from '@/components/FestivalMenuItem.vue';
import { useUserStore } from '@/stores/user';
import { reactive } from 'vue';
const userStore = useUserStore();
const festivalMenuItems = reactive([
{
src: '/2048.png',
title: '2048',
to: '/2048',
},
{
src: '/gobang.svg',
title: '五子棋',
to: '/gobang',
vIf: () => userStore.logined,
},
]);
defineProps<{
mode: 'horizontal' | 'vertical';
}>();
defineEmits(['select']);
</script>

View File

@ -0,0 +1,35 @@
<template>
<div
class="app-header-user flex center"
v-loading="userStore.initializing"
@click="userStore.logined && $emit('click')"
>
<template v-if="!userStore.initializing">
<!-- 仅收紧类型 -->
<template v-if="userInfo && userStore.logined">
<div class="flex align-center">
<el-avatar :icon="userInfo.avatar ?? undefined"></el-avatar>
</div>
<div class="app-header-user__username">
{{ userInfo.name }}
</div>
</template>
<el-button class="app-header-user__login" v-else type="primary" @click="$emit('login')">
登录
</el-button>
</template>
</div>
</template>
<style lang="scss" scoped>
.app-header-user {
min-width: 50px;
gap: 10px;
}
</style>
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
defineEmits(['logout', 'login', 'click']);
</script>

View File

@ -1,6 +1,7 @@
import router from '@/router';
import { IdPool } from '@/utils';
import type { PageErrorReason } from '@/views/ErrorPage.vue';
import { useMediaQuery } from '@vueuse/core';
import { defineStore } from 'pinia';
import { computed } from 'vue';
import {
@ -14,7 +15,8 @@ export const usePageStore = defineStore('page-store', () => {
const routeIdPool = new IdPool();
const routeIds = routeIdPool.usedIdSet;
const pageLoadingCount = computed(() => routeIds.size);
const mobileScreen = useMediaQuery('(max-width: 576px)');
const smallMobileScreen = useMediaQuery('(max-width: 400px)');
function setNewRouteId(route: RouteLocationNormalized) {
return (route.meta.routeId = routeIdPool.newId());
}
@ -51,6 +53,8 @@ export const usePageStore = defineStore('page-store', () => {
return {
pageLoadingCount,
mobileScreen,
smallMobileScreen,
setNewRouteId,
removeRouteId,
createTempErrorRoute,

View File

@ -20,7 +20,7 @@ export const useUserStore = defineStore('user', () => {
});
const permissions = computed<Permission[]>(() => userInfo.value?.auth.permissions ?? []);
const initializing = ref(false);
const logined = computed(() => userInfo.value && userInfo.value.id !== -1);
const logined = computed(() => (userInfo.value && userInfo.value.id !== -1) ?? false);
watch(
userInfo,
() => {
@ -30,8 +30,6 @@ export const useUserStore = defineStore('user', () => {
);
async function updateSelfUserInfo(showErrorMessage: boolean): Promise<boolean> {
initializing.value = true;
console.log('called');
try {
const raw = await axiosInstance
.get<RawResp>('/api/user/info')

View File

@ -1,6 +1,6 @@
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from "@vitejs/plugin-legacy";
import legacy from '@vitejs/plugin-legacy';
import { resolve } from 'node:path';
import AutoImport from 'unplugin-auto-import/vite';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
@ -12,51 +12,41 @@ import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
build: {
target: "es2015",
target: 'es2015',
},
plugins: [
legacy({
targets: ['defaults', 'Chrome >= 100', 'Edge >= 100', 'FireFox >= 110'],
polyfills: ["es.promise.with-resolvers"],
modernPolyfills: ["es.promise.with-resolvers"],
polyfills: ['es.promise.with-resolvers'],
modernPolyfills: ['es.promise.with-resolvers'],
}),
vue(),
vueJsx(),
AutoImport({
resolvers: [
ElementPlusResolver(),
IconsResolver({
prefix: 'icon',
enabledCollections: ['ep'],
customCollections: ['cs']
})
]
}),
Components({
resolvers: [
ElementPlusResolver(),
IconsResolver({
prefix: 'icon',
enabledCollections: ['ep'],
customCollections: ['cs']
})
]
customCollections: ['cs'],
}),
],
globs: ['!src/components/*.vue', '!src/views/*.vue'],
}),
Icons({
autoInstall: true,
customCollections: {
cs: FileSystemIconLoader('./src/assets/icons')
}
})
cs: FileSystemIconLoader('./src/assets/icons'),
},
}),
],
resolve: {
alias: {
'@': resolve('./src')
'@': resolve('./src'),
// vue: 'vue/dist/vue.esm-bundler.js'
}
},
},
server: {
host: '0.0.0.0',
port: 18081
}
port: 18081,
},
});