🦄 refactor: 重构app主页
@ -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
@ -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']
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 290 KiB |
Before Width: | Height: | Size: 7.1 MiB After Width: | Height: | Size: 7.1 MiB |
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 774 KiB |
Before Width: | Height: | Size: 735 KiB After Width: | Height: | Size: 735 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 440 KiB After Width: | Height: | Size: 440 KiB |
15
src/App.vue
@ -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
@ -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>
|
@ -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,
|
||||
});
|
||||
|
@ -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
@ -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 |
Before Width: | Height: | Size: 54 KiB |
@ -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';
|
||||
|
||||
|
24
src/components/FestivalMenuItem.vue
Normal 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>
|
@ -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';
|
||||
|
238
src/components/LoginRegisterDialog.vue
Normal 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>
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -1,3 +0,0 @@
|
||||
export * from './background';
|
||||
export * from './2048';
|
||||
export * from './user';
|
@ -50,7 +50,6 @@ export const usePageStore = defineStore('page-store', () => {
|
||||
}
|
||||
|
||||
return {
|
||||
routeIds,
|
||||
pageLoadingCount,
|
||||
setNewRouteId,
|
||||
removeRouteId,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|