feat: 初步增加2048动画打断功能

This commit is contained in:
Litrix 2024-06-13 18:00:41 +08:00
parent b4a8f0063d
commit 7b934108f5
10 changed files with 102 additions and 68 deletions

6
components.d.ts vendored
View File

@ -21,7 +21,9 @@ declare module 'vue' {
ElHeader: (typeof import('element-plus/es'))['ElHeader'];
ElIcon: (typeof import('element-plus/es'))['ElIcon'];
ElInput: (typeof import('element-plus/es'))['ElInput'];
ElInputNumber: (typeof ;'element-plus/es'['ElInputNumber'];)) ElPopover: (typeof import('element-plus/es'))['ElPopover'];
ElInputNumber: (typeof import('element-plus/es'))['ElInputNumber'];
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'];
Game2048: (typeof import('./src/components/Game2048.vue'))['default'];
@ -34,7 +36,5 @@ declare module 'vue' {
RouterLink: (typeof import('vue-router'))['RouterLink'];
RouterView: (typeof import('vue-router'))['RouterView'];
VerifyInput: (typeof import('./src/components/VerifyInput.vue'))['default'];
import(
}
}

View File

@ -3,28 +3,30 @@
<el-header height="50px">
<div class="header-title">社团展示系统</div>
<nav class="header-content">
<router-link v-slot="{ navigate }" custom to="/2048">
<div class="nav-img-items game-2048" @click="navigate">
<img alt="2048" draggable="false" src="@/assets/2048.png" />
</div>
</router-link>
<template v-if="userStore.isInitialized">
<div v-if="userStore.userInfo !== null" class="username">
{{ userStore.userInfo.name }}
</div>
<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>
</el-dropdown>
<router-link v-slot="{ navigate }" custom to="/2048">
<div class="nav-img-items game-2048" @click="navigate">
<img alt="2048" draggable="false" src="@/assets/2048.png" />
</div>
</router-link>
<template v-if="userStore.userInfo !== null">
<div class="username">
{{ userStore.userInfo.name }}
</div>
<el-dropdown ref="dropdownRef">
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
<template #dropdown>
<el-dropdown-menu>
<router-link v-slot="{ navigate }" custom to="/user">
<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>
</el-dropdown>
</template>
<el-avatar
v-else
style="color: black; user-select: none; cursor: pointer"
@ -160,13 +162,15 @@
</template>
<style scoped lang="scss">
.el-header {
--el-header-padding: var(--page-content-padding);
position: relative;
display: flex;
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);
box-shadow: 0 1px 5px rgba(black, 0.1);
background-color: white;
z-index: 100;
}
.el-container {
@ -347,6 +351,7 @@ async function submitLoginForm() {
verifyImage: { key },
verifyCode: code
} = loginFormData;
console.log(loginFormData);
succeed = await login({
username,
password,

View File

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

View File

@ -29,15 +29,21 @@
<template v-for="(tiles, x) in row">
<template v-for="tile in tiles" :key="tile.id">
<div
ref="tileDiv"
:class="[
'tile',
{
'tile-appear-from-others': tile.fromOthers
}
]"
:style="[getTilePosition(y, x), getTileStyle(tile)]"
:style="[
getTilePosition(y, x),
getTileStyle(tile),
{ transition: transitionCancelled ? 'none' : undefined }
]"
:data-tile-id="tile.id"
@transitionend="onTileTransitionCompleted"
@transitioncancel="onTileTransitionCompletedOrCancelled"
@transitionend="onTileTransitionCompletedOrCancelled"
>
{{ tile.number }}
</div>
@ -84,8 +90,11 @@
.game-container {
position: relative;
width: max-content;
height: max-content;
background-color: rgb(187, 174, 158);
border-radius: 6px;
margin-bottom: 10px;
}
.game-container-enter-active {
@ -133,9 +142,7 @@
font-size: 20px;
font-weight: bold;
line-height: 100px;
transition:
all 0.1s ease-in,
z-index 0s linear;
transition: all 0.1s ease-in;
}
.tile-enter-active.tile-appear-from-others {
@ -187,18 +194,19 @@ interface LineMergeResult {
resultLine: OverlappedTileLine;
}
type Future<T = void> = PromiseWithResolvers<T>;
/**
* 界面过渡是否完成?
*/
type ContainerTransitionFuture = PromiseWithResolvers<void>;
type ContainerTransitionFuture = Future;
/**
* 数块是否添加完成?
*/
type TileAddFuture = PromiseWithResolvers<void>;
type TileAddFuture = Future;
/**
* 数块过渡是否完成?
*/
type TileTransitionFuture = PromiseWithResolvers<void>;
type TileTransitionFuture = Future;
/**
* 合并结果网格.
*/
@ -223,6 +231,7 @@ const containerSizeStyle = computed(
*/
const showContainer = ref(false);
const showTiles = ref(false);
const tileDiv = ref<HTMLDivElement[]>([]);
/**
* 游戏状态.
*/
@ -245,6 +254,11 @@ watch(gameStatus, (value) => {
}
});
const locked = ref(true);
const isTileTransitioning = ref(false);
const transitionCancelled = ref(false);
const tileTransitionStyle = computed(() => ({
transition: !transitionCancelled.value ? 'all 1s ease-in' : 'none'
}));
let containerTransitionFuture: ContainerTransitionFuture | undefined;
/**
* 数块id分配器, 便于重用id.
@ -276,10 +290,7 @@ function storeTileGrid() {
);
}
function addFuture<T = void>(
tileId: number,
futureMap: Map<number, PromiseWithResolvers<T>>
): Promise<T> {
function addFuture<T = void>(tileId: number, futureMap: Map<number, Future<T>>): Promise<T> {
const future = Promise.withResolvers<T>();
futureMap.set(tileId, future);
return future.promise;
@ -548,12 +559,13 @@ async function mergeTiles(direction: Directions) {
const tilesToRemoveId: Tile[] = [];
forEachMergedGrid(({ tiles, lineMergeResult: { originalLine, transitionLine }, lineIndex }) => {
const transitionTiles = transitionLine[lineIndex];
transitionTilesAndPromises.push(
...transitionTiles
.filter((tile) => tile !== originalLine[lineIndex]) // .
.map((tile) => addFuture(tile.id, tileTransitionFutureMap))
);
if (!transitionCancelled.value) {
transitionTilesAndPromises.push(
...transitionTiles
.filter((tile) => tile !== originalLine[lineIndex]) // .
.map((tile) => addFuture(tile.id, tileTransitionFutureMap))
);
}
tiles.splice(0, tiles.length, ...transitionTiles);
});
await Promise.all(transitionTilesAndPromises);
@ -563,15 +575,18 @@ async function mergeTiles(direction: Directions) {
const transitionTiles = transitionLine[lineIndex];
const resultTiles = resultLine[lineIndex];
tiles.splice(0, tiles.length, ...resultTiles);
tileAddPromises.push(
...resultTiles
.filter((tile) => !originalLine.includes(tile)) // .
.map((tile) => addFuture(tile.id, tileAddFutureMap))
);
if (!transitionCancelled.value) {
tileAddPromises.push(
...resultTiles
.filter((tile) => !originalLine.includes(tile)) // .
.map((tile) => addFuture(tile.id, tileAddFutureMap))
);
}
tilesToRemoveId.push(...transitionTiles.filter((tile) => !resultTiles.includes(tile)));
}
);
await Promise.all(tileAddPromises.concat(addRandomTiles(changed ? 1 : 0)));
console.log(2);
tileIdDispenser.removeId(...tilesToRemoveId.map((tile) => tile.id)); // id.
if (maxNumber === successNumber) {
gameStatus.value = 'succeed';
@ -600,7 +615,7 @@ function onTileAddCompleted(element: Element) {
/**
* 数块过渡完成.
*/
function onTileTransitionCompleted(event: TransitionEvent) {
function onTileTransitionCompletedOrCancelled(event: TransitionEvent) {
const tileId = getElementTileId(event.currentTarget as HTMLDivElement);
tileTransitionFutureMap.get(tileId)?.resolve();
tileTransitionFutureMap.delete(tileId);
@ -615,13 +630,20 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
case Directions.DOWN:
case Directions.LEFT:
case Directions.RIGHT:
locked.value = true;
if ((await mergeTiles(e.key)) && gameStatus.value === 'playing') {
if (isTileTransitioning.value) {
locked.value = true;
transitionCancelled.value = true;
await new Promise((resolve) => watch(isTileTransitioning, resolve, { once: true }));
transitionCancelled.value = false;
locked.value = false;
}
isTileTransitioning.value = true;
if ((await mergeTiles(e.key)) && gameStatus.value === 'playing') {
isTileTransitioning.value = false;
storeTileGrid();
} else if (gameStatus.value === 'playing') {
gameStatus.value = 'failed';
game2048Store.$reset();
// game2048Store.$reset();
}
break;
}

View File

@ -103,7 +103,7 @@ async function getVerifyImage() {
}
const { img, key } = verifyResponse.data;
model.value.verifyImage = {
key,
key: key,
img
};
cooldown();

View File

@ -6,13 +6,14 @@ import { ElMessage } from 'element-plus';
import { errorMessage } from '@/utils';
import { AxiosError } from 'axios';
import { RoutePermission } from './permissions';
import { routeHasPermission } from '@/utils/permissions';
export * from './permissions';
declare module 'vue-router' {
// noinspection JSUnusedGlobalSymbols
interface RouteMeta {
permission: RoutePermission;
permission?: RoutePermission;
}
}
const routes: RouteRecordRaw[] = [
@ -83,10 +84,7 @@ router.beforeEach(async (to) => {
userStore.isInitialized = true;
}
}
if (
to.meta.permission === undefined ||
userStore.permissions.find((fullPermission) => fullPermission.id === to.meta.permission)
) {
if (routeHasPermission(to)) {
return true;
} else {
return '/';

View File

@ -11,7 +11,7 @@ export interface Tile {
}
export const useGame2048Store = defineStore('2048', () => {
const { key: gameKey, refresh: refreshGame } = useRefresh();
const { tilesKey: gameKey, refresh: refreshGame } = useRefresh();
function create() {
return create2DArray(height.value, width.value, () => []);

View File

@ -49,7 +49,7 @@ export function useRefresh() {
const key = ref(dispenser.getId());
// watch(key, (value) => console.log(value));
return {
key,
tilesKey: key,
refresh() {
key.value = dispenser.getId(key.value);
}

13
src/utils/permissions.ts Normal file
View File

@ -0,0 +1,13 @@
import type { RouteLocationNormalized } from 'vue-router';
import { useRoute } from 'vue-router';
import { useUserStore } from '@/stores';
import { RoutePermission } from '@/router';
export function hasPermission(permission: RoutePermission) {
const userStore = useUserStore();
return userStore.permissions.find((fullPermission) => fullPermission.id === permission);
}
export function routeHasPermission(route: RouteLocationNormalized = useRoute()) {
return route.meta.permission === undefined || hasPermission(route.meta.permission);
}

View File

@ -15,8 +15,8 @@
</template>
<style scoped lang="scss">
.page-root {
width: 100vw;
height: 100vh;
width: 100%;
height: calc(100vh - 50px);
display: flex;
flex-direction: column;
justify-content: center;
@ -27,10 +27,6 @@
.el-input {
width: auto;
}
.game-container {
margin-bottom: 10px;
}
</style>
<script setup lang="ts">
import Game2048 from '@/components/Game2048.vue';