feat: 改为静态路由
更改路由为静态路由
3
components.d.ts
vendored
@ -19,8 +19,8 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
@ -30,6 +30,7 @@ declare module 'vue' {
|
||||
IconCsValidate: typeof import('~icons/cs/validate')['default']
|
||||
IconEpLoading: typeof import('~icons/ep/loading')['default']
|
||||
IconEpUserFilled: typeof import('~icons/ep/user-filled')['default']
|
||||
LoadingIcon: typeof import('./src/components/LoadingIcon.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
VerifyInput: typeof import('./src/components/VerifyInput.vue')['default']
|
||||
|
4
env.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="element-plus/global" />
|
||||
/// <reference path="./components.d.ts" />
|
||||
/// <reference path="./auto-imports.d.ts" />
|
||||
/// <reference types="./components.d.ts" />
|
||||
/// <reference types="./auto-imports.d.ts" />
|
||||
|
123
src/App.vue
@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header height="50px">
|
||||
<div class="title-container">社团展示系统</div>
|
||||
<div class="content">
|
||||
<template v-if="userStore.initialized">
|
||||
<div class="header-title">社团展示系统</div>
|
||||
<div class="header-content">
|
||||
<template v-if="userStore.isInitialized">
|
||||
<div v-if="userStore.userInfo !== null" class="username">
|
||||
{{ userStore.userInfo.name }}
|
||||
</div>
|
||||
<el-dropdown v-if="userStore.userInfo !== null">
|
||||
<el-avatar :icon="userStore.userInfo.avatar ?? Avatar" :size="40"></el-avatar>
|
||||
<el-dropdown ref="dropdownRef" v-if="userStore.userInfo !== null">
|
||||
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/user" v-slot="{ navigate }" custom>
|
||||
<el-dropdown-item :icon="UserFilled" @click="navigate()">
|
||||
个人主页
|
||||
</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@ -24,9 +29,10 @@
|
||||
登录
|
||||
</el-avatar>
|
||||
</template>
|
||||
<el-icon v-else class="is-loading"><icon-ep-loading /></el-icon>
|
||||
<loading-icon v-else />
|
||||
</div>
|
||||
</el-header>
|
||||
<router-view />
|
||||
</el-container>
|
||||
<el-dialog
|
||||
class="login-dialog"
|
||||
@ -36,7 +42,7 @@
|
||||
>
|
||||
<el-tabs v-model="loginRegisterDialogActiveName">
|
||||
<el-tab-pane label="登录" name="login">
|
||||
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules">
|
||||
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @submit.prevent>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginFormData.username"
|
||||
@ -44,7 +50,9 @@
|
||||
:disabled="logining"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-user /></el-icon>
|
||||
<el-icon>
|
||||
<icon-ep-user-filled />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -56,7 +64,9 @@
|
||||
:disabled="logining"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-lock /></el-icon>
|
||||
<el-icon>
|
||||
<icon-cs-lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -66,6 +76,7 @@
|
||||
<el-form-item style="margin-bottom: 0">
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
style="width: 100%"
|
||||
@click="submitLoginForm"
|
||||
:loading="logining"
|
||||
@ -76,7 +87,12 @@
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="注册" name="register">
|
||||
<el-form ref="registerFormRef" :model="registerFormData" :rules="registerFormRules">
|
||||
<el-form
|
||||
ref="registerFormRef"
|
||||
:model="registerFormData"
|
||||
:rules="registerFormRules"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="registerFormData.username"
|
||||
@ -84,7 +100,9 @@
|
||||
:disabled="registering"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-user /></el-icon>
|
||||
<el-icon>
|
||||
<icon-cs-user />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -96,7 +114,9 @@
|
||||
:disabled="registering"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-lock /></el-icon>
|
||||
<el-icon>
|
||||
<icon-cs-lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -108,7 +128,9 @@
|
||||
:disabled="registering"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-lock /></el-icon>
|
||||
<el-icon>
|
||||
<icon-cs-lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -118,6 +140,7 @@
|
||||
<el-form-item style="margin-bottom: 0">
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
style="width: 100%"
|
||||
@click="submitRegisterForm"
|
||||
:loading="registering"
|
||||
@ -129,7 +152,6 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
<router-view />
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.el-header {
|
||||
@ -137,15 +159,29 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
--el-header-padding: var(--page-content-padding);
|
||||
box-shadow: 0 1px 5px var(--el-border-color);
|
||||
background-color: white;
|
||||
}
|
||||
.content {
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
--el-main-padding: var(--page-content-padding);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
.header-title {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@ -153,18 +189,26 @@
|
||||
<script setup lang="ts">
|
||||
import axiosInstance from '@/api';
|
||||
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
|
||||
import { getAvailableRoutes } from '@/router';
|
||||
import { loginResponseSchema, registerResponseSchema, userInfoResponseSchema } from '@/schemas';
|
||||
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { errorMessage } from '@/utils';
|
||||
import { Avatar, CloseBold, UserFilled } from '@element-plus/icons-vue';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { partial } from 'lodash-es';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { RouterView, useRouter } from 'vue-router';
|
||||
import { Avatar, CloseBold } from '@element-plus/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
watch(
|
||||
() => userStore.isInitialized,
|
||||
(value) => {
|
||||
if (value && userStore.token === null) {
|
||||
showLoginRegisterDialog.value = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
const showLoginRegisterDialog = ref(false);
|
||||
watch(showLoginRegisterDialog, (value) => {
|
||||
if (value) {
|
||||
@ -176,7 +220,6 @@ watch(showLoginRegisterDialog, (value) => {
|
||||
registerFormRef.value?.resetFields();
|
||||
}
|
||||
});
|
||||
|
||||
const loginRegisterDialogActiveName = ref('login');
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loginFormData = reactive({
|
||||
@ -212,7 +255,7 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
|
||||
...loginFormRules,
|
||||
confirmPassword: [
|
||||
{
|
||||
validator: (_, value, callback) => {
|
||||
validator(_, value, callback) {
|
||||
if (value === '') {
|
||||
callback('请输入密码');
|
||||
} else if (value !== registerFormData.password) {
|
||||
@ -228,6 +271,7 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
|
||||
});
|
||||
const registering = ref(false);
|
||||
const loginErrorMessage = partial(errorMessage, '登录');
|
||||
|
||||
interface LoginParams {
|
||||
username: string;
|
||||
password: string;
|
||||
@ -255,6 +299,7 @@ async function login(params: LoginParams) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitLoginForm() {
|
||||
let succeed = false;
|
||||
logining.value = true;
|
||||
@ -284,14 +329,19 @@ async function submitLoginForm() {
|
||||
if (succeed) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
loginFormData.verifyImage = 'none';
|
||||
loginFormRef.value?.resetFields('verifyCode');
|
||||
logining.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerErrorMessage = partial(errorMessage, '注册');
|
||||
|
||||
interface RegisterParams extends LoginParams {
|
||||
auth: number;
|
||||
}
|
||||
|
||||
async function register(params: RegisterParams) {
|
||||
try {
|
||||
const registerResp = registerResponseSchema.parse(
|
||||
@ -314,6 +364,7 @@ async function register(params: RegisterParams) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRegisterForm() {
|
||||
let succeed = false;
|
||||
registering.value = true;
|
||||
@ -338,37 +389,15 @@ async function submitRegisterForm() {
|
||||
if (succeed) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
registerFormData.verifyImage = 'none';
|
||||
registerFormRef.value?.resetFields('verifyCode');
|
||||
registering.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
userStore.$reset();
|
||||
await router.push('/');
|
||||
window.location.reload();
|
||||
}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const userInfoResponse = userInfoResponseSchema.parse(
|
||||
(await axiosInstance.get('/api/user/info')).data
|
||||
);
|
||||
if (userInfoResponse.type === 'error') {
|
||||
ElMessage.error(
|
||||
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// 判断是否已登录.
|
||||
if (userStore.token !== null) {
|
||||
userStore.updateUserInfo(userInfoResponse);
|
||||
}
|
||||
getAvailableRoutes(userInfoResponse.data.auth.permissions[0].routers);
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
|
||||
}
|
||||
} finally {
|
||||
userStore.initialized = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const baseURL = 'http://server.wzpmc.cn:18080/';
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: 'http://wzpmc.cn:18080/'
|
||||
baseURL
|
||||
});
|
||||
// 自动添加token到请求中.
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
|
@ -3,6 +3,16 @@
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--page-content-width: 980px;
|
||||
--page-content-padding: 0 calc((100vw - var(--page-content-width)) / 2);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: rgb(244, 246, 249);
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M6 22q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h1V6q0-2.075 1.463-3.537T12 1q2.075 0 3.538 1.463T17 6v2h1q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22zm6-5q.825 0 1.413-.587T14 15q0-.825-.587-1.412T12 13q-.825 0-1.412.588T10 15q0 .825.588 1.413T12 17M9 8h6V6q0-1.25-.875-2.125T12 3q-1.25 0-2.125.875T9 6z" />
|
||||
d="M6 22q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h1V6q0-2.075 1.463-3.537T12 1q2.075 0 3.538 1.463T17 6v2h1q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22zm6-5q.825 0 1.413-.587T14 15q0-.825-.587-1.412T12 13q-.825 0-1.412.588T10 15q0 .825.588 1.413T12 17M9 8h6V6q0-1.25-.875-2.125T12 3q-1.25 0-2.125.875T9 6z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 451 B |
@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="0.88em" height="1em" viewBox="0 0 448 512">
|
||||
<path fill="currentColor"
|
||||
d="M224 256a128 128 0 1 0 0-256a128 128 0 1 0 0 256m-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512h388.6c16.4 0 29.7-13.3 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3z" />
|
||||
d="M224 256a128 128 0 1 0 0-256a128 128 0 1 0 0 256m-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512h388.6c16.4 0 29.7-13.3 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 313 B |
@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 14 14">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M8 3a3 3 0 1 1-6 0a3 3 0 0 1 6 0m-1.95 9.5H.5A.5.5 0 0 1 0 12a5 5 0 0 1 9.725-1.64l-.076.11l-.601-.404A2 2 0 0 0 6.05 12.5m7.8-3.3a.75.75 0 0 0-1.2-.9l-2.67 3.9l-1.65-1.11a.75.75 0 1 0-.9 1.2l2.25 1.56a.75.75 0 0 0 1.05-.15z"
|
||||
clip-rule="evenodd" />
|
||||
d="M8 3a3 3 0 1 1-6 0a3 3 0 0 1 6 0m-1.95 9.5H.5A.5.5 0 0 1 0 12a5 5 0 0 1 9.725-1.64l-.076.11l-.601-.404A2 2 0 0 0 6.05 12.5m7.8-3.3a.75.75 0 0 0-1.2-.9l-2.67 3.9l-1.65-1.11a.75.75 0 1 0-.9 1.2l2.25 1.56a.75.75 0 0 0 1.05-.15z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 408 B |
BIN
src/assets/ws-icon.jfif
Normal file
After Width: | Height: | Size: 54 KiB |
@ -20,12 +20,14 @@
|
||||
transform 1.5s ease,
|
||||
filter 1.5s ease;
|
||||
}
|
||||
|
||||
.bg-enter-from,
|
||||
.bg-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
.bg-enter-to,
|
||||
.bg-leave-from {
|
||||
opacity: 1;
|
||||
@ -36,13 +38,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CancelledError,
|
||||
useBackgroundStore,
|
||||
type BackgroundTask,
|
||||
type BackgroundURL
|
||||
type BackgroundURL,
|
||||
CancelledError,
|
||||
useBackgroundStore
|
||||
} from '@/stores';
|
||||
import 'element-plus/theme-chalk/index.css';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
compId: number;
|
||||
}>();
|
||||
@ -53,6 +56,7 @@ let unMounted = false;
|
||||
let currentTask: BackgroundTask | undefined;
|
||||
let currentURL: BackgroundURL;
|
||||
let updateFuture: PromiseWithResolvers<void> | undefined;
|
||||
|
||||
async function main() {
|
||||
while (!unMounted) {
|
||||
currentURL = undefined;
|
||||
@ -97,6 +101,7 @@ async function main() {
|
||||
}
|
||||
console.log('shutdown');
|
||||
}
|
||||
|
||||
onMounted(main);
|
||||
onUnmounted(() => {
|
||||
unMounted = true;
|
||||
@ -104,6 +109,7 @@ onUnmounted(() => {
|
||||
currentTask?.addFuture.reject(new CancelledError());
|
||||
updateFuture?.reject(new CancelledError());
|
||||
});
|
||||
|
||||
async function onAfterEnter() {
|
||||
const { options, addFuture } = currentTask!;
|
||||
if (options.resolveTiming === 'transitionEnd') {
|
||||
@ -111,6 +117,7 @@ async function onAfterEnter() {
|
||||
}
|
||||
updateFuture?.resolve();
|
||||
}
|
||||
|
||||
function update() {
|
||||
const { options, addFuture } = currentTask!;
|
||||
if (currentURL !== undefined) {
|
||||
@ -125,6 +132,7 @@ function update() {
|
||||
updateFuture?.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function onAfterLeave() {
|
||||
update();
|
||||
}
|
||||
|
@ -60,6 +60,7 @@
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tile-common-appear {
|
||||
from {
|
||||
transform: scale(0);
|
||||
@ -68,6 +69,7 @@
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tile-bounce-appear {
|
||||
from {
|
||||
transform: scale(0.5);
|
||||
@ -79,14 +81,17 @@
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
background-color: rgb(187, 174, 158);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.game-container-enter-active {
|
||||
animation: container-appear 0.5s ease;
|
||||
}
|
||||
|
||||
.failed-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@ -101,15 +106,19 @@
|
||||
z-index: 20;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.failed-mask-enter-active {
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
|
||||
.failed-mask-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.failed-mask-enter-to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tile,
|
||||
.background-tile {
|
||||
position: absolute;
|
||||
@ -118,6 +127,7 @@
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tile {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
@ -127,24 +137,29 @@
|
||||
all 0.1s ease-in,
|
||||
z-index 0s linear;
|
||||
}
|
||||
|
||||
.tile-enter-active.tile-appear-from-others {
|
||||
animation: tile-bounce-appear 0.15s ease;
|
||||
}
|
||||
|
||||
.tile-enter-active:not(.tile-appear-from-others) {
|
||||
animation: tile-common-appear 0.15s ease;
|
||||
}
|
||||
|
||||
.background-tile {
|
||||
background-color: rgb(205, 193, 180);
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { useGame2048Store, type Tile } from '@/stores';
|
||||
import { IdDispenser, get2DArrayItem } from '@/utils';
|
||||
import { sample, shuffle, pick } from 'lodash-es';
|
||||
import { type Tile, useGame2048Store } from '@/stores';
|
||||
import { get2DArrayItem, IdDispenser } from '@/utils';
|
||||
import { pick, sample, shuffle } from 'lodash-es';
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { ElNotification } from 'element-plus';
|
||||
|
||||
const game2048Store = useGame2048Store();
|
||||
|
||||
/**
|
||||
* 方向键代码.
|
||||
*/
|
||||
@ -154,12 +169,14 @@ enum Directions {
|
||||
LEFT = 'ArrowLeft',
|
||||
RIGHT = 'ArrowRight'
|
||||
}
|
||||
|
||||
type SingleTileLine = (Tile | undefined)[];
|
||||
type OverlappedTileLine = Tile[][];
|
||||
/**
|
||||
* 存储合并操作所有被合并的块.
|
||||
*/
|
||||
type OriginalTilesMap = Map<Tile, Tile[]>;
|
||||
|
||||
/**
|
||||
* 行合并结果.
|
||||
*/
|
||||
@ -169,6 +186,7 @@ interface LineMergeResult {
|
||||
transitionLine: OverlappedTileLine;
|
||||
resultLine: OverlappedTileLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 界面过渡是否完成?
|
||||
*/
|
||||
@ -248,6 +266,7 @@ const tileBackgroundColorMapping: Record<number, string | undefined> = {
|
||||
1024: 'rgb(237, 197, 63)',
|
||||
2048: 'rgb(237, 194, 46)'
|
||||
};
|
||||
|
||||
/**
|
||||
* 持久化存储数块网格.
|
||||
*/
|
||||
@ -256,6 +275,7 @@ function storeTileGrid() {
|
||||
row.map((tiles) => tiles.map((tile) => pick(tile, 'number', 'removed')))
|
||||
);
|
||||
}
|
||||
|
||||
function addFuture<T = void>(
|
||||
tileId: number,
|
||||
futureMap: Map<number, PromiseWithResolvers<T>>
|
||||
@ -264,12 +284,14 @@ function addFuture<T = void>(
|
||||
futureMap.set(tileId, future);
|
||||
return future.promise;
|
||||
}
|
||||
|
||||
function getTilePosition(y: number, x: number) {
|
||||
return {
|
||||
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
|
||||
top: `${borderWidth + y * (tileWidth + borderWidth)}px`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数块的样式.
|
||||
*/
|
||||
@ -281,6 +303,7 @@ function getTileStyle(tile: Tile) {
|
||||
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加随机数块, 空间不够则跳过.
|
||||
* @param randomCount 随机数量.
|
||||
@ -313,6 +336,7 @@ async function addRandomTiles(randomCount: number, randomNumbers: number[] = [2,
|
||||
}
|
||||
await Promise.all(tileAddPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数块Div的对应Id.
|
||||
* @param element 数块Div.
|
||||
@ -367,6 +391,7 @@ function mergeLineImpl(originalTilesMap: OriginalTilesMap, line: SingleTileLine,
|
||||
} while (!once && !finished);
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并一行.
|
||||
* @return 若该行未更改返回合并结果, 否则返回undefined.
|
||||
@ -447,6 +472,7 @@ async function mergeTiles(direction: Directions) {
|
||||
}
|
||||
return [mergedGrid, changed, maxNumber];
|
||||
}
|
||||
|
||||
function forEachMergedGrid(
|
||||
callback: (data: { tiles: Tile[]; lineMergeResult: LineMergeResult; lineIndex: number }) => void
|
||||
) {
|
||||
@ -461,6 +487,7 @@ async function mergeTiles(direction: Directions) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function firstIndices(d: Directions = direction) {
|
||||
return Array.from(
|
||||
(function* () {
|
||||
@ -481,6 +508,7 @@ async function mergeTiles(direction: Directions) {
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
function secondIndices(d: Directions = direction) {
|
||||
return Array.from(
|
||||
(function* () {
|
||||
@ -510,9 +538,11 @@ async function mergeTiles(direction: Directions) {
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
function isColFirst(d: Directions = direction) {
|
||||
return d == Directions.LEFT || d == Directions.RIGHT;
|
||||
}
|
||||
|
||||
const [mergedGrid, changed, maxNumber] = getMergedGrid(false);
|
||||
const transitionTilesAndPromises: TileTransitionFuture['promise'][] = [];
|
||||
const tilesToRemoveId: Tile[] = [];
|
||||
@ -549,12 +579,14 @@ async function mergeTiles(direction: Directions) {
|
||||
}
|
||||
return Object.values(Directions).some((d) => getMergedGrid(true, d)[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 界面过渡完成.
|
||||
*/
|
||||
function onContainerTransitionEnd() {
|
||||
containerTransitionFuture?.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数块添加完成.
|
||||
* @param element 过渡完成的数块.
|
||||
@ -564,6 +596,7 @@ function onTileAddCompleted(element: Element) {
|
||||
tileAddFutureMap.get(tileId)?.resolve();
|
||||
tileAddFutureMap.delete(tileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数块过渡完成.
|
||||
*/
|
||||
@ -572,6 +605,7 @@ function onTileTransitionCompleted(event: TransitionEvent) {
|
||||
tileTransitionFutureMap.get(tileId)?.resolve();
|
||||
tileTransitionFutureMap.delete(tileId);
|
||||
}
|
||||
|
||||
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
|
||||
if (locked.value) {
|
||||
return;
|
||||
|
10
src/components/LoadingIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon :size="props.size" color="dimgrey" class="loading-icon is-loading">
|
||||
<icon-ep-loading />
|
||||
</el-icon>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
size?: number;
|
||||
}>();
|
||||
</script>
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<el-input v-model="model.verifyCode" placeholder="请输入验证码" :disabled="props.disabled">
|
||||
<template #prepend>
|
||||
<el-icon><icon-cs-validate /></el-icon>
|
||||
<el-icon>
|
||||
<icon-cs-validate />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button v-if="model.verifyImage === 'none'" @click="getVerifyImage">获取验证码</el-button>
|
||||
@ -16,6 +18,7 @@
|
||||
draggable="false"
|
||||
:src="model.verifyImage.img"
|
||||
@click="getVerifyImage"
|
||||
alt="验证码"
|
||||
/>
|
||||
</template>
|
||||
</el-popover>
|
||||
@ -36,7 +39,8 @@ import { errorMessage, timeout } from '@/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { partial } from 'lodash-es';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
export type VerifyImagePath =
|
||||
| 'none'
|
||||
| 'fetching'
|
||||
@ -54,22 +58,32 @@ const canRefresh = computed(() => refreshCoolDown.value === 0);
|
||||
const popOverMessage = ref('');
|
||||
const refreshCoolDown = ref(0);
|
||||
const verifyErrorMessage = partial(errorMessage, '获取验证码');
|
||||
watchEffect(() => {
|
||||
if (refreshCoolDown.value > 0) {
|
||||
popOverMessage.value = `过${refreshCoolDown.value.toFixed(0)}秒再换吧`;
|
||||
} else {
|
||||
popOverMessage.value = '看不清? 点击换一张';
|
||||
watch(
|
||||
() => refreshCoolDown.value,
|
||||
(value) => {
|
||||
if (value > 0) {
|
||||
popOverMessage.value = `过${refreshCoolDown.value.toFixed(0)}秒再换吧`;
|
||||
} else {
|
||||
popOverMessage.value = '看不清? 点击换一张';
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
watch(
|
||||
() => model.value.verifyImage,
|
||||
(value) => {
|
||||
if (value === 'none') {
|
||||
refreshCoolDown.value = 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function cooldown() {
|
||||
for (
|
||||
refreshCoolDown.value = coolDownDuration;
|
||||
refreshCoolDown.value > 0;
|
||||
refreshCoolDown.value--
|
||||
) {
|
||||
for (refreshCoolDown.value = coolDownDuration; refreshCoolDown.value > 0; ) {
|
||||
refreshCoolDown.value--;
|
||||
await timeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerifyImage() {
|
||||
if (model.value.verifyImage === 'fetching' || !canRefresh.value) {
|
||||
return;
|
||||
|
@ -1,2 +1 @@
|
||||
import { type InjectionKey } from 'vue';
|
||||
export {};
|
||||
|
@ -5,5 +5,6 @@ import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from '@/App.vue';
|
||||
import router from '@/router';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(createPinia()).use(router).mount('#app');
|
||||
|
@ -1,61 +1,91 @@
|
||||
import { type Route } from '@/schemas';
|
||||
import { userInfoResponseSchema } from '@/schemas';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { watch, type Component } from 'vue';
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
|
||||
import axiosInstance from '@/api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { errorMessage } from '@/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { RoutePermission } from './permissions';
|
||||
|
||||
export * from './permissions';
|
||||
|
||||
declare module 'vue-router' {
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
interface RouteMeta {
|
||||
permission: RoutePermission;
|
||||
}
|
||||
}
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home'
|
||||
component: () => import('@/views/MainPage.vue'),
|
||||
meta: {
|
||||
permission: RoutePermission.MAIN_PAGE
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/NotFoundPage.vue')
|
||||
path: '/user',
|
||||
component: () => import('@/views/UserPage.vue'),
|
||||
meta: {
|
||||
permission: RoutePermission.USER_PAGE
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/user/:id',
|
||||
component: () => import('@/views/UserPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/club',
|
||||
component: () => import('@/views/ClubPage.vue'),
|
||||
meta: {
|
||||
permission: RoutePermission.CLUB_PAGE
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:brokenPath',
|
||||
redirect: '/'
|
||||
}
|
||||
];
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes
|
||||
});
|
||||
const componentMap: Record<string, () => Promise<Component>> = {
|
||||
'MainPage.vue': () => import('@/views/MainPage.vue')
|
||||
};
|
||||
function _getAvailableRoutes(routeResponses: Route[], root: boolean, pathList: string[]) {
|
||||
const res: RouteRecordRaw[] = [];
|
||||
const { routeMap } = useUserStore();
|
||||
for (const routeResponse of routeResponses) {
|
||||
const path = routeResponse.name.toLowerCase();
|
||||
pathList.push(path);
|
||||
const route = {
|
||||
path: root ? `/${path}` : path,
|
||||
component: componentMap[routeResponse.component],
|
||||
children: _getAvailableRoutes(routeResponse.children, false, pathList.concat(path))
|
||||
};
|
||||
routeMap.set(`/${pathList.join('/')}`, root ? route : routeMap.get(`/${pathList[0]}`)!);
|
||||
res.push(route);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
export function getAvailableRoutes(routeResponse: Route[]) {
|
||||
_getAvailableRoutes(routeResponse, true, []);
|
||||
}
|
||||
router.beforeEach(async (to) => {
|
||||
console.log(to);
|
||||
const userStore = useUserStore();
|
||||
console.log(2, userStore.userInfo);
|
||||
// await timeout(1000);
|
||||
console.log(userStore.initialized);
|
||||
if (!userStore.initialized) {
|
||||
await new Promise((resolve) => watch(() => userStore.initialized, resolve));
|
||||
console.log(userStore.isInitialized);
|
||||
if (!userStore.isInitialized) {
|
||||
try {
|
||||
const userInfoResponse = userInfoResponseSchema.parse(
|
||||
(await axiosInstance.get('/api/user/info')).data
|
||||
);
|
||||
if (userInfoResponse.type === 'error') {
|
||||
ElMessage.error(
|
||||
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// 判断是否已登录.
|
||||
if (userStore.token !== null) {
|
||||
userStore.updateUserInfo(userInfoResponse);
|
||||
}
|
||||
userStore.permissions.push(...userInfoResponse.data.auth.permissions);
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
userStore.isInitialized = true;
|
||||
}
|
||||
}
|
||||
if (userStore.addedRouteSet.has(to.fullPath) || to.fullPath === '/404') {
|
||||
if (
|
||||
to.meta.permission === undefined ||
|
||||
userStore.permissions.find((fullPermission) => fullPermission.id === to.meta.permission)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const route = userStore.routeMap.get(to.fullPath);
|
||||
if (route !== undefined) {
|
||||
router.addRoute(route);
|
||||
userStore.addedRouteSet.add(to.fullPath);
|
||||
return to;
|
||||
} else {
|
||||
return '/404';
|
||||
return '/';
|
||||
}
|
||||
});
|
||||
export default router;
|
||||
|
5
src/router/permissions.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum RoutePermission {
|
||||
MAIN_PAGE = 1,
|
||||
USER_PAGE = 2,
|
||||
CLUB_PAGE = 7
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
|
||||
return z.union([
|
||||
z
|
||||
@ -10,8 +11,8 @@ function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
|
||||
.extend({ data: data })
|
||||
.transform((raw) =>
|
||||
Object.assign(raw, {
|
||||
type: 'success' as const
|
||||
})
|
||||
type: 'success'
|
||||
} as const)
|
||||
),
|
||||
z
|
||||
.object({
|
||||
@ -21,35 +22,39 @@ function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
|
||||
})
|
||||
.transform((raw) =>
|
||||
Object.assign(raw, {
|
||||
type: 'error' as const
|
||||
})
|
||||
type: 'error'
|
||||
} as const)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
export type SucceedResponse<T> = Extract<T, { type: 'success' }>;
|
||||
export type ErrorResponse<T> = Exclude<SucceedResponse<T>, T>;
|
||||
const ordinaryResponse = createResponseSchema(z.literal(true));
|
||||
export const loginResponseSchema = ordinaryResponse;
|
||||
export const registerResponseSchema = ordinaryResponse;
|
||||
const idAndNameSchema = z.object({
|
||||
export const ordinarySchema = createResponseSchema(z.literal(true));
|
||||
export const loginResponseSchema = ordinarySchema;
|
||||
export const registerResponseSchema = ordinarySchema;
|
||||
export const idAndNameSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string()
|
||||
});
|
||||
export interface Route extends z.infer<typeof idAndNameSchema> {
|
||||
component: string;
|
||||
children: Route[];
|
||||
}
|
||||
const routeResponsesSchema: z.ZodSchema<Route[]> = z.lazy(() =>
|
||||
idAndNameSchema.extend({ component: z.string(), children: routeResponsesSchema }).array()
|
||||
);
|
||||
|
||||
const _authSchema = idAndNameSchema.extend({
|
||||
permissions: idAndNameSchema.array()
|
||||
});
|
||||
export const userInfoResponseSchema = createResponseSchema(
|
||||
idAndNameSchema.extend({
|
||||
avatar: z.nullable(z.string()),
|
||||
auth: idAndNameSchema.extend({
|
||||
permissions: idAndNameSchema.extend({ routers: routeResponsesSchema }).array()
|
||||
})
|
||||
auth: _authSchema,
|
||||
club: z.nullable(
|
||||
idAndNameSchema.extend({
|
||||
commit: z.string(),
|
||||
auth: _authSchema
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
export type SucceedUserInfoResponse = SucceedResponse<z.infer<typeof userInfoResponseSchema>>;
|
||||
export type UserInfo = SucceedUserInfoResponse['data'];
|
||||
export const verifyResponseSchema = createResponseSchema(
|
||||
z
|
||||
.object({
|
||||
|
@ -2,22 +2,27 @@ import { create2DArray, useRefresh } from '@/utils';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { watch } from 'vue';
|
||||
|
||||
export interface Tile {
|
||||
id: number;
|
||||
number: number;
|
||||
removed: boolean;
|
||||
fromOthers: boolean;
|
||||
}
|
||||
|
||||
export const useGame2048Store = defineStore('2048', () => {
|
||||
const { key: gameKey, refresh: refreshGame } = useRefresh();
|
||||
|
||||
function create() {
|
||||
return create2DArray(height.value, width.value, () => []);
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
rawTileGrid.value = create();
|
||||
isInitial.value = true;
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
const width = useLocalStorage('2048-width', 4);
|
||||
const height = useLocalStorage('2048-height', 4);
|
||||
watch([width, height], $reset, { flush: 'sync' });
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { IdDispenser } from '@/utils';
|
||||
import { defineStore } from 'pinia';
|
||||
import { reactive, readonly } from 'vue';
|
||||
|
||||
export type BackgroundURL = string | undefined;
|
||||
export type BackgroundOptions = {
|
||||
resolveTiming: 'transitionStart' | 'transitionEnd';
|
||||
@ -10,11 +11,14 @@ export type BackgroundTask = {
|
||||
options: BackgroundOptions;
|
||||
addFuture: PromiseWithResolvers<void>;
|
||||
};
|
||||
|
||||
export class CancelledError extends Error {}
|
||||
|
||||
export const useBackgroundStore = defineStore('background', () => {
|
||||
const taskQueue = reactive<BackgroundTask[]>([]);
|
||||
const getFuturesMap = new Map<number, PromiseWithResolvers<BackgroundTask>>();
|
||||
const compIdDispenser = new IdDispenser();
|
||||
|
||||
function getURL(id: number) {
|
||||
const future = Promise.withResolvers<BackgroundTask>();
|
||||
if (taskQueue.length) {
|
||||
@ -24,6 +28,7 @@ export const useBackgroundStore = defineStore('background', () => {
|
||||
}
|
||||
return future.promise;
|
||||
}
|
||||
|
||||
function addURL(
|
||||
url: BackgroundURL,
|
||||
options: BackgroundOptions = { resolveTiming: 'transitionEnd' }
|
||||
@ -45,13 +50,16 @@ export const useBackgroundStore = defineStore('background', () => {
|
||||
}
|
||||
return addFuture.promise;
|
||||
}
|
||||
|
||||
function newCompId() {
|
||||
return compIdDispenser.getId();
|
||||
}
|
||||
|
||||
function unregisterComp(id: number) {
|
||||
getFuturesMap.get(id)?.reject(new CancelledError());
|
||||
getFuturesMap.delete(id);
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
for (const task of taskQueue) {
|
||||
task.addFuture.reject(new CancelledError());
|
||||
@ -62,6 +70,7 @@ export const useBackgroundStore = defineStore('background', () => {
|
||||
unregisterComp(id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
taskQueue: readonly(taskQueue),
|
||||
getURL,
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { userInfoResponseSchema, type SucceedResponse } from '@/schemas';
|
||||
import type { SucceedUserInfoResponse, UserInfo } from '@/schemas';
|
||||
import { type idAndNameSchema } from '@/schemas';
|
||||
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
|
||||
import { pick } from 'lodash-es';
|
||||
import { defineStore } from 'pinia';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { type RouteRecordRaw } from 'vue-router';
|
||||
import { z } from 'zod';
|
||||
type SucceedUserInfoResponse = SucceedResponse<z.infer<typeof userInfoResponseSchema>>;
|
||||
type UserInfo = Pick<SucceedUserInfoResponse['data'], 'id' | 'name' | 'avatar'>;
|
||||
|
||||
type FullPermission = z.infer<typeof idAndNameSchema>;
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = useLocalStorage<string | null>('token', null, {
|
||||
serializer: StorageSerializers.string
|
||||
@ -14,25 +13,25 @@ export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = useLocalStorage<UserInfo | null>('user-info', null, {
|
||||
serializer: StorageSerializers.object
|
||||
});
|
||||
const routeMap = reactive(new Map<string, RouteRecordRaw>());
|
||||
const addedRouteSet = reactive(new Set<string>());
|
||||
const permissions = reactive<FullPermission[]>([]);
|
||||
const isInitialized = ref(false);
|
||||
|
||||
function updateUserInfo(userInfoResponse: SucceedUserInfoResponse) {
|
||||
userInfo.value = pick(userInfoResponse.data, 'id', 'name', 'avatar');
|
||||
userInfo.value = userInfoResponse.data;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
token.value = null;
|
||||
userInfo.value = null;
|
||||
routeMap.clear();
|
||||
addedRouteSet.clear();
|
||||
permissions.splice(0);
|
||||
isInitialized.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
routeMap,
|
||||
addedRouteSet,
|
||||
initialized: isInitialized,
|
||||
permissions,
|
||||
isInitialized,
|
||||
updateUserInfo,
|
||||
$reset
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ export function create2DArray<T>(height: number, width: number, cell: T | (() =>
|
||||
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell))
|
||||
);
|
||||
}
|
||||
|
||||
export function get2DArrayItem<T>(
|
||||
grid: T[][],
|
||||
first: number,
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export function timeout(delay: number = 0) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
export class IdDispenser {
|
||||
#maxId: number;
|
||||
#usedIdSet: Set<number>;
|
||||
#unUsedIdSet: Set<number>;
|
||||
|
||||
constructor() {
|
||||
this.#maxId = 0;
|
||||
this.#usedIdSet = new Set<number>();
|
||||
this.#unUsedIdSet = new Set<number>();
|
||||
}
|
||||
|
||||
getId(...identifiersToRemove: number[]) {
|
||||
let id: number;
|
||||
if (this.#unUsedIdSet.size) {
|
||||
@ -23,6 +27,7 @@ export class IdDispenser {
|
||||
this.removeId(...identifiersToRemove);
|
||||
return id;
|
||||
}
|
||||
|
||||
removeId(...identifiers: number[]) {
|
||||
for (const id of identifiers) {
|
||||
if (!this.#usedIdSet.delete(id)) {
|
||||
@ -31,6 +36,7 @@ export class IdDispenser {
|
||||
this.#unUsedIdSet.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#maxId = 0;
|
||||
this.#unUsedIdSet.clear();
|
||||
|
5
src/views/ClubPage.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<el-main></el-main>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
<script setup lang="ts"></script>
|
@ -23,9 +23,11 @@
|
||||
align-items: center;
|
||||
background-color: rgb(250, 248, 239);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@ -34,8 +36,10 @@
|
||||
import Game2048 from '@/components/Game2048.vue';
|
||||
import { useGame2048Store } from '@/stores';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showSettings = ref(false);
|
||||
const game2048Store = useGame2048Store();
|
||||
|
||||
function click() {
|
||||
// game2048Store.width = 4;
|
||||
// game2048Store.height = 4;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>test{{ Math.random() }}</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Avatar } from '@element-plus/icons-vue';
|
||||
</script>
|
||||
<template>
|
||||
<el-main></el-main>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
<script setup lang="ts"></script>
|
||||
|
@ -1 +0,0 @@
|
||||
<template>404</template>
|
94
src/views/UserPage.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-main>
|
||||
<loading-icon v-if="!initialized" :size="50" />
|
||||
<template v-else-if="userInfo !== undefined">
|
||||
<div class="banner">
|
||||
<div class="banner-inner">
|
||||
<el-avatar :size="60"></el-avatar>
|
||||
<div>
|
||||
<div class="username">{{ userInfo.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else></template>
|
||||
</el-main>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.loading-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
height: 190px;
|
||||
background-image: url('@/assets/banner.jpg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.banner-inner {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 30px;
|
||||
line-height: 60px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { type UserInfo, userInfoResponseSchema } from '@/schemas';
|
||||
import { useUserStore } from '@/stores';
|
||||
import axiosInstance from '@/api';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const initialized = ref(false);
|
||||
const userInfo = ref<UserInfo>();
|
||||
const isSelf = computed(() =>
|
||||
userStore.userInfo === null ? false : userStore.userInfo.id === userInfo.value?.id
|
||||
);
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string | undefined;
|
||||
const avatar = ref<string>();
|
||||
|
||||
async function getAvatar(sha1: string) {
|
||||
const avatarResp = await axiosInstance.get(`/api/user/avatar${sha1}`);
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
if (id == undefined) {
|
||||
userInfo.value = userStore.userInfo!;
|
||||
initialized.value = true;
|
||||
} else {
|
||||
try {
|
||||
const userInfoResponse = userInfoResponseSchema.parse(
|
||||
(await axiosInstance.get(`/api/user/info/${id}`)).data
|
||||
);
|
||||
if (userInfoResponse.type === 'error') {
|
||||
return;
|
||||
}
|
||||
userInfo.value = userInfoResponse.data;
|
||||
if (!userInfo.value.avatar) {
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
initialized.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -8,7 +8,7 @@
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["unplugin-icons/types/vue"],
|
||||
"jsx": "preserve",
|
||||
|