🦄 refactor: 重构app主页

This commit is contained in:
Litrix 2024-12-12 12:41:24 +08:00
parent 85300fa2d2
commit 09be778916
29 changed files with 501 additions and 70 deletions

View File

@ -7,13 +7,13 @@ module.exports = {
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
'@vue/eslint-config-prettier/skip-formatting',
],
parserOptions: {
ecmaVersion: 'latest'
ecmaVersion: 'latest',
},
rules: {
'vue/no-unused-vars': 'warn',
'vue/multi-word-component-names': 'off'
}
'vue/multi-word-component-names': 'off',
},
};

7
components.d.ts vendored
View File

@ -23,18 +23,25 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElSpinner: typeof import('element-plus/es')['ElSpinner']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElText: typeof import('element-plus/es')['ElText']
FestivalMenuItem: typeof import('./src/components/FestivalMenuItem.vue')['default']
Game2048: typeof import('./src/components/Game2048.vue')['default']
Game2048Button: typeof import('./src/components/Game2048Button.vue')['default']
Game2048Score: typeof import('./src/components/Game2048Score.vue')['default']
IconCsClub: typeof import('~icons/cs/club')['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']
LoginRegisterDialog: typeof import('./src/components/LoginRegisterDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VerifyInput: typeof import('./src/components/VerifyInput.vue')['default']

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 290 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

View File

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 774 KiB

View File

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 735 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 440 KiB

View File

@ -6,7 +6,7 @@
<div class="header-title" @click="navigate()">社团展示系统</div>
</router-link>
<transition name="header-content-container">
<template v-if="!userStore.isInitializing">
<template v-if="!userStore.initializing">
<nav class="header-content-container">
<el-menu
:default-active="$route.fullPath"
@ -16,7 +16,7 @@
>
<div style="flex: 1"></div>
<el-menu-item index="/2048">
<img alt="2048" class="header-menu-image" src="@/assets/2048.png" />
<img alt="2048" class="header-menu-image" src="/2048.png" />
</el-menu-item>
</el-menu>
<template v-if="userStore.userInfo && userStore.userInfo.id !== -1">
@ -63,7 +63,6 @@
<component :is="comp" />
</transition>
</router-view>
<transition name="page-root"></transition>
</el-main>
</el-container>
<el-dialog
@ -339,7 +338,7 @@
import axiosInstance, { type RawResp } from '@/api';
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { useUserStore } from '@/stores/user';
import { usePageStore } from '@/stores/page';
import { errorMessage } from '@/utils';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
@ -349,12 +348,13 @@ import { partial } from 'lodash-es';
import { reactive, ref, watch } from 'vue';
import { RouterView, useRoute } from 'vue-router';
import router from './router';
import { onMounted } from 'vue';
const userStore = useUserStore();
const pageStore = usePageStore();
const showLoginRegisterDialog = ref(false);
watch(
() => userStore.isInitializing,
() => userStore.initializing,
(value) => {
console.log(1);
if (!value && userStore.token === null) {
@ -384,7 +384,7 @@ let loginFormData = reactive({
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: 6, message: '用户名长度不能小于6位' },
{ min: 3, message: '用户名长度不能小于3位' },
],
password: [
{ required: true, message: '请输入密码' },
@ -544,4 +544,7 @@ async function logout() {
userStore.token = null;
await userStore.updateSelfUserInfo(true);
}
onMounted(() => {
console.log(userStore.token);
});
</script>

150
src/App2.vue Normal file
View File

@ -0,0 +1,150 @@
<template>
<el-container class="app__container">
<el-header class="app-header flex align-center">
<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>
</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" />
</el-sub-menu>
</el-menu>
<div class="app-header-user flex center" v-loading="userStore.initializing">
<template v-if="!userStore.initializing">
<template v-if="userStore.userInfo && userStore.userInfo.id !== -1">
<el-dropdown>
<el-avatar :icon="userStore.userInfo.avatar ?? undefined"></el-avatar>
<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">
{{ userStore.userInfo.name }}
</div>
</template>
<el-button type="primary" v-else @click="showLoginRegisterDialog = true">登录</el-button>
</template>
</div>
</el-header>
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
<el-container class="app-router-component__wrapper">
<router-view v-slot="{ Component: comp }">
<transition appear mode="out-in" name="app-router-view">
<component class="app-router-component" :is="comp" />
</transition>
</router-view>
</el-container>
</el-main>
</el-container>
<login-register-dialog v-model="showLoginRegisterDialog" />
</template>
<style lang="scss" scoped>
.app__container {
min-height: 100vh;
}
.app-header {
--header-height: 50px;
--el-header-height: var(--header-height);
--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-main {
padding: 0;
flex: 1;
display: flex;
}
.app-header-user {
min-width: 50px;
gap: 10px;
}
.app-router-view-enter-active,
.app-router-view-leave-active {
transition: opacity 0.5s ease;
}
.app-router-view-enter-from,
.app-router-view-leave-to {
opacity: 0;
}
.app-router-component__wrapper {
flex: 1;
}
</style>
<script setup lang="ts">
import { CloseBold } from '@element-plus/icons-vue';
import type { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { provide, ref } from 'vue';
import axiosInstance, { type RawResp } from './api/index';
import FestivalMenuItem from './components/FestivalMenuItem.vue';
import LoginRegisterDialog, {
loginImplKey,
registerImplKey,
} from './components/LoginRegisterDialog.vue';
import { loginResponseSchema, registerResponseSchema } from './schemas/response';
import { useUserStore } from './stores/user';
import { usePageStore } from './stores/page.js';
const userStore = useUserStore();
const pageStore = usePageStore();
const showLoginRegisterDialog = ref(false);
provide(loginImplKey, async (params) => {
const loginRespRaw = await axiosInstance
.post<RawResp>('/api/user/login', params)
.then((r) => r.data)
.catch((err: AxiosError) => {
ElMessage.error([err.code, err.message].join(':'));
});
if (!loginRespRaw) return false;
const resp = loginResponseSchema.parse(loginRespRaw);
if (resp.type === 'error') {
ElMessage.error([resp.code, resp.msg].join(':'));
return false;
}
return userStore.updateSelfUserInfo(true);
});
provide(registerImplKey, async (params) => {
const raw = await axiosInstance
.put<RawResp>('/api/user/create', params)
.then((r) => r.data)
.catch((err: AxiosError) => {
ElMessage.error([err.code, err.message].join(':'));
});
if (!raw) return false;
const resp = registerResponseSchema.parse(raw);
if (resp.type === 'error') {
ElMessage.error([resp.code, resp.msg].join(':'));
return false;
}
return userStore.updateSelfUserInfo(true);
});
async function logout() {
userStore.token = null;
await userStore.updateSelfUserInfo(true);
}
// onMounted(() => {
// userStore.token = null;
// userStore.updateSelfUserInfo(true);
// });
</script>

View File

@ -1,7 +1,7 @@
import { useUserStore } from '@/stores';
import { useUserStore } from '@/stores/user';
import axios from 'axios';
const baseURL = 'https://wzpmc.cn:18080/';
const baseURL = 'http://172.16.114.84:58080/';
const axiosInstance = axios.create({
baseURL,
});

View File

@ -4,10 +4,6 @@
box-sizing: border-box;
}
:root {
--page-content-width: 980px;
}
body {
background-color: rgb(244, 246, 249);
}
@ -16,3 +12,22 @@ body {
width: 100vw;
height: 100vh;
}
.flex {
display: flex !important;
}
.align-center {
align-items: center;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.center {
@extend .justify-center;
@extend .align-center;
}

26
src/assets/icons/Club.svg Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733910218851"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1549"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path
d="M512.79616 806.37312c-145.024 0-263.04-118.016-263.04-263.04s118.016-263.04 263.04-263.04 263.04 118.016 263.04 263.04c0.128 145.024-117.888 263.04-263.04 263.04z m0-487.808c-123.904 0-224.64 100.864-224.64 224.64 0 123.904 100.864 224.64 224.64 224.64s224.64-100.864 224.64-224.64c0.128-123.776-100.736-224.64-224.64-224.64z"
fill="#409eff" p-id="1550"></path>
<path d="M497.43616 388.70912m-20.352 0a20.352 20.352 0 1 0 40.704 0 20.352 20.352 0 1 0-40.704 0Z" fill="#409eff"
p-id="1551"></path>
<path
d="M497.43616 428.26112c-21.888 0-39.552-17.792-39.552-39.552s17.664-39.68 39.552-39.68c21.888 0 39.552 17.792 39.552 39.552s-17.792 39.68-39.552 39.68z m0-40.832c-0.64 0-1.152 0.512-1.152 1.152s0.512 1.152 1.152 1.152 1.152-0.512 1.152-1.152-0.512-1.152-1.152-1.152z"
fill="#409eff" p-id="1552"></path>
<path d="M441.11616 511.46112m-42.88 0a42.88 42.88 0 1 0 85.76 0 42.88 42.88 0 1 0-85.76 0Z" fill="#409eff"
p-id="1553"></path>
<path
d="M441.11616 573.54112c-34.304 0-62.08-27.904-62.08-62.08s27.904-62.08 62.08-62.08 62.08 27.904 62.08 62.08-27.904 62.08-62.08 62.08z m0-85.76c-13.056 0-23.68 10.624-23.68 23.68s10.624 23.68 23.68 23.68 23.68-10.624 23.68-23.68-10.624-23.68-23.68-23.68z"
fill="#409eff" p-id="1554"></path>
<path d="M575.64416 485.47712m-19.072 0a19.072 19.072 0 1 0 38.144 0 19.072 19.072 0 1 0-38.144 0Z" fill="#409eff"
p-id="1555"></path>
<path
d="M575.64416 523.87712c-21.12 0-38.272-17.152-38.272-38.272s17.152-38.272 38.272-38.272 38.272 17.152 38.272 38.272-17.024 38.272-38.272 38.272z m0.128-38.4z"
fill="#409eff" p-id="1556"></path>
<path
d="M336.79616 763.10912c-56.192 0-98.176-18.176-120.192-52.48-29.952-46.72-16.512-115.456 36.992-188.416l30.976 22.784c-43.008 58.624-56.32 112.768-35.712 145.024 17.92 27.904 61.312 39.808 119.424 32.768 148.48-26.368 282.368-112.64 367.616-236.928 28.928-48.512 35.712-91.904 18.304-119.168-20.224-31.616-73.472-42.496-142.592-29.184l-7.296-37.76c86.4-16.768 152.96 0.128 182.272 46.208 25.984 40.576 19.584 97.408-18.048 160l-0.64 1.024c-45.696 66.688-103.296 122.752-171.136 166.528s-142.72 73.088-222.208 87.168l-1.024 0.128c-12.8 1.536-25.088 2.304-36.736 2.304z"
fill="#409eff" p-id="1557"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -42,7 +42,7 @@ import {
type BackgroundURL,
CancelledError,
useBackgroundStore,
} from '@/stores';
} from '@/stores/background';
import 'element-plus/theme-chalk/index.css';
import { onMounted, onUnmounted, ref } from 'vue';

View File

@ -0,0 +1,24 @@
<template>
<el-menu-item class="festival-menu-item flex" :index="to">
<img :src="src" class="festival-menu-item__img" />
<div class="festival-menu-item__title">{{ title }}</div>
</el-menu-item>
</template>
<style lang="scss" scoped>
.festival-menu-item__img {
width: 30px;
height: 30px;
margin-right: 10px;
}
.festival-menu-item__title {
flex: 1;
text-align: center;
}
</style>
<script setup lang="ts">
defineProps<{
src: string;
title: string;
to?: string;
}>();
</script>

View File

@ -187,7 +187,7 @@
}
</style>
<script lang="ts" setup>
import { type GameState, type Tile, useGame2048Store } from '@/stores';
import { type GameState, type Tile, useGame2048Store } from '@/stores/2048';
import { chainIterables, type Future, get2DArrayItem } from '@/utils';
import { useEventListener } from '@vueuse/core';
import { ElNotification } from 'element-plus';

View File

@ -0,0 +1,238 @@
<template>
<el-dialog v-model="show" :align-center="true" class="login-dialog" width="400">
<el-tabs v-model="loginRegisterDialogActiveName">
<el-tab-pane label="登录" name="login">
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @submit.prevent>
<el-form-item prop="username">
<el-input
v-model="loginFormData.username"
:disabled="logining"
placeholder="请输入用户名"
>
<template #prepend>
<el-icon>
<icon-ep-user-filled />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginFormData.password"
:disabled="logining"
placeholder="请输入密码"
type="password"
>
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<verify-input v-model="loginFormData" :disabled="logining" />
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
:loading="logining"
native-type="submit"
style="width: 100%"
type="primary"
@click="submitLoginForm"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
<el-form
ref="registerFormRef"
:model="registerFormData"
:rules="registerFormRules"
@submit.prevent
>
<el-form-item prop="username">
<el-input
v-model="registerFormData.username"
:disabled="registering"
placeholder="请输入注册用户名"
>
<template #prepend>
<el-icon>
<icon-cs-user />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerFormData.password"
:disabled="registering"
placeholder="请输入密码"
type="password"
>
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerFormData.confirmPassword"
:disabled="registering"
placeholder="请确认密码"
type="password"
>
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<verify-input v-model="registerFormData" :disabled="registering" />
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
:loading="registering"
native-type="submit"
style="width: 100%"
type="primary"
@click="submitRegisterForm"
>
注册
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script lang="ts">
export interface LoginParams {
username: string;
password: string;
key: string;
code: string;
}
export interface RegisterParams extends LoginParams {
auth: number;
}
export const loginImplKey = Symbol() as InjectionKey<(params: LoginParams) => Promise<boolean>>;
export const registerImplKey = Symbol() as InjectionKey<
(params: RegisterParams) => Promise<boolean>
>;
</script>
<script lang="ts" setup>
import { 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),
registerImpl = inject(registerImplKey, async () => false);
const show = defineModel<boolean>({ default: false });
const loginRegisterDialogActiveName = ref('login');
const loginFormRef = ref<FormInstance>();
const loginFormData = reactive({
username: 'wubaopu2',
password: '123456',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
});
watch(show, (show) => {
if (!show) return;
loginFormRef.value?.resetFields();
registerFormRef.value?.resetFields();
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: 6, message: '用户名长度不能小于6位' },
],
password: [
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能小于6位' },
],
verifyCode: [
{ required: true, message: '请输入验证码' },
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' },
],
});
const logining = ref(false);
async function submitLoginForm() {
logining.value = true;
try {
try {
await loginFormRef.value?.validate();
} catch (e) {
return;
}
if (typeof loginFormData.verifyImage === 'string') {
ElMessage.error('请获取验证码');
return;
}
const {
username,
password,
verifyImage: { key },
verifyCode: code,
} = loginFormData;
if (!(await loginImpl({ username, password, key, code }))) return;
show.value = false;
} finally {
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields('verifyCode');
logining.value = false;
}
}
const registerFormRef = ref<FormInstance>();
let registerFormData = reactive({
username: '',
password: '',
confirmPassword: '',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
...loginFormRules,
confirmPassword: [
{
validator(_, value, callback) {
if (value === '') {
callback('请输入密码');
} else if (value !== registerFormData.password) {
callback('密码不一致');
} else if (value.length < 6) {
callback('密码长度不能小于6位');
} else {
callback();
}
},
},
],
});
const registering = ref(false);
async function submitRegisterForm() {
registering.value = true;
try {
try {
await registerFormRef.value?.validate();
} catch {
return;
}
if (typeof registerFormData.verifyImage === 'string') {
ElMessage.error('请输入验证码');
return;
}
} finally {
registerFormData.verifyImage = 'none';
registerFormRef.value?.resetFields('verifyCode');
registering.value = false;
}
}
</script>

View File

@ -1,7 +1,7 @@
import '@/assets/global.scss';
import 'element-plus/theme-chalk/index.css';
import 'element-plus/theme-chalk/display.css';
import App from '@/App.vue';
import App from '@/App2.vue';
import router from '@/router';
import { createPinia } from 'pinia';
import { createApp } from 'vue';

View File

@ -1,4 +1,4 @@
import { useUserStore } from '@/stores';
import { useUserStore } from '@/stores/user';
import { usePageStore } from '@/stores/page';
import { PageErrorType, type PageErrorReason } from '@/views/ErrorPage.vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
@ -46,15 +46,6 @@ const routes: RouteRecordRaw[] = [
} satisfies PageErrorReason,
},
];
// (function processRoutes(routes: RouteRecordRaw[], idPool: IdPool) {
// for (const route of routes) {
// if (route.name === undefined) {
// route.name = `AnonymousPage${idPool.newId()}`;
// processRoutes(route.children ?? [], idPool);
// }
// }
// })(routes, new IdPool(1));
const router = createRouter({
history: createWebHistory(),
routes: routes,
@ -80,8 +71,6 @@ router.beforeEach(async (to) => {
);
}
}
console.log(userStore.permissions);
if (permissionId) {
if (userStore.hasPermission(permissionId)) {
return true;
@ -93,24 +82,6 @@ router.beforeEach(async (to) => {
pageStore.removeRouteId(to);
}
});
// 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

@ -1,6 +1,6 @@
import { DoubleQueue } from '@/utils/double-queue';
import { defineStore } from 'pinia';
import { reactive, readonly, ref } from 'vue';
import { markRaw, reactive, readonly, ref } from 'vue';
export type BackgroundURL = string | undefined;
export type BackgroundOptions = {

View File

@ -1,3 +0,0 @@
export * from './background';
export * from './2048';
export * from './user';

View File

@ -50,7 +50,6 @@ export const usePageStore = defineStore('page-store', () => {
}
return {
routeIds,
pageLoadingCount,
setNewRouteId,
removeRouteId,

View File

@ -1,7 +1,8 @@
import axiosInstance, { type RawResp } from '@/api';
import router from '@/router';
import type { UserInfo } from '@/schemas';
import { type idAndNameSchema, userInfoResponseSchema } from '@/schemas';
import { errorMessage } from '@/utils';
import { errorMessage, timeout } from '@/utils';
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
@ -18,16 +19,17 @@ export const useUserStore = defineStore('user', () => {
serializer: StorageSerializers.object,
});
const permissions = computed<Permission[]>(() => userInfo.value?.auth.permissions ?? []);
const isInitializing = ref(false);
const initializing = ref(false);
watch(
userInfo,
(info) => {
isInitializing.value = !!info;
initializing.value = !!info;
router.push({ path: router.currentRoute.value.fullPath, force: true });
},
{ flush: 'sync' },
);
async function updateSelfUserInfo(showErrorMessage: boolean): Promise<boolean> {
isInitializing.value = true;
initializing.value = true;
try {
const raw = await axiosInstance
.get<RawResp>('/api/user/info')
@ -46,7 +48,7 @@ export const useUserStore = defineStore('user', () => {
userInfo.value = resp.data;
return true;
} finally {
isInitializing.value = false;
initializing.value = false;
}
}
function hasPermission(permissionId: number) {
@ -57,7 +59,7 @@ export const useUserStore = defineStore('user', () => {
token,
userInfo,
permissions,
isInitializing,
initializing,
updateSelfUserInfo,
hasPermission,
};

View File

@ -31,6 +31,8 @@ export enum PageErrorType {
}
</script>
<script lang="ts" setup>
import { useBackgroundStore } from '@/stores/background.js';
export type PageErrorReason = {
type: PageErrorType.NOT_FOUND | PageErrorType.NETWORK_ERROR | PageErrorType.NO_PERMISSION;
};

View File

@ -1,6 +1,6 @@
<template>
<div class="page-root page-max-width">
<div class="outer">
<el-main class="game-2048-page__wrapper flex justify-center">
<div class="game-2048-page">
<div class="game-header">
<div class="game-title">
<b>{{ game2048Store.successNumber.toString().padStart(4, '0') }}</b>
@ -21,20 +21,17 @@
</div>
<game2048 :key="game2048Store.gameKey" class="game" />
</div>
</div>
</el-main>
</template>
<style lang="scss" scoped>
.page-root {
.game-2048-page__wrapper {
--font-color: rgb(119, 110, 101);
font-family: 'Arial', '微软雅黑', '黑体', sans-serif;
background-color: rgb(250, 248, 239);
}
.outer {
.game-2048-page {
width: max-content;
margin: 0 auto;
}
.game-header {
display: flex;
align-items: center;
@ -71,7 +68,7 @@
<script lang="ts" setup>
import Game2048 from '@/components/Game2048.vue';
import Game2048Score from '@/components/Game2048Score.vue';
import { useGame2048Store } from '@/stores';
import { useGame2048Store } from '@/stores/2048';
const game2048Store = useGame2048Store();

View File

@ -51,7 +51,7 @@
<script lang="ts" setup>
import axiosInstance from '@/api';
import { type UserInfo, userInfoResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { useUserStore } from '@/stores/user';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';