feat: 初步增加2048动画打断功能
This commit is contained in:
parent
b4a8f0063d
commit
7b934108f5
6
components.d.ts
vendored
6
components.d.ts
vendored
@ -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(
|
||||
}
|
||||
}
|
||||
|
51
src/App.vue
51
src/App.vue
@ -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,
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ async function getVerifyImage() {
|
||||
}
|
||||
const { img, key } = verifyResponse.data;
|
||||
model.value.verifyImage = {
|
||||
key,
|
||||
key: key,
|
||||
img
|
||||
};
|
||||
cooldown();
|
||||
|
@ -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 '/';
|
||||
|
@ -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, () => []);
|
||||
|
@ -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
13
src/utils/permissions.ts
Normal 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);
|
||||
}
|
@ -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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user