✨ feat: 主框架添加响应式支持
This commit is contained in:
parent
f49c25ba9c
commit
24ff08605b
10
auto-imports.d.ts
vendored
10
auto-imports.d.ts
vendored
@ -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
13
components.d.ts
vendored
@ -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']
|
||||
|
142
src/App.vue
142
src/App.vue
@ -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);
|
||||
|
@ -17,6 +17,9 @@ body {
|
||||
.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
6
src/assets/icons/Menu.svg
Normal file
6
src/assets/icons/Menu.svg
Normal 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 |
@ -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),
|
||||
|
53
src/components/app/AppHeaderMenu.vue
Normal file
53
src/components/app/AppHeaderMenu.vue
Normal 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>
|
35
src/components/app/AppHeaderUser.vue
Normal file
35
src/components/app/AppHeaderUser.vue
Normal 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>
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user