feat: 完成五子棋响应式布局

This commit is contained in:
Litrix2 2024-12-20 22:00:55 +08:00
parent 864b585c2e
commit fd72812e22
8 changed files with 218 additions and 152 deletions

View File

@ -1,6 +1,6 @@
<template>
<el-container class="app__container">
<el-header class="app-header flex items-center bg-white">
<el-header class="app-header flex items-center">
<el-icon v-show="mdLess" size="30" @click="showVerticalHeaderMenu = true">
<icon-cs-menu />
</el-icon>
@ -20,7 +20,7 @@
<header-user v-else @login="onLoginButtonClick" @logout="logout" @click="onUserClick" />
</el-header>
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
<el-container class="app-router-component__wrapper">
<el-container>
<router-view v-slot="{ Component: comp }">
<transition appear mode="out-in" name="app-router-view">
<component class="app-router-component" :is="comp" />
@ -74,18 +74,14 @@
}
</style>
<style lang="scss" scoped>
@use '@/assets/mixins' as m;
.app__container {
min-height: 100vh;
}
.app-header {
--el-header-height: var(--header-height);
@include m.header(0);
--el-loading-spinner-size: 30px;
position: fixed;
width: 100%;
z-index: 100;
user-select: none;
top: 0;
border-bottom: 1px solid var(--el-border-color);
//
.app-header-user {
margin-left: auto;

9
src/assets/mixins.scss Normal file
View File

@ -0,0 +1,9 @@
@mixin header($multiply) {
@apply bg-white;
--el-header-height: var(--header-height);
border-bottom: 1px solid var(--el-border-color);
position: fixed;
top: calc(var(--header-height) * $multiply);
width: 100%;
z-index: 100;
}

View File

@ -1,5 +1,5 @@
<template>
<el-header class="gobang-header flex items-center bg-white">
<el-header class="gobang-header flex items-center">
<router-link custom :to="{ name: 'GobangList' }" v-slot="{ navigate }">
<div class="gobang-header__left flex items-center" @click="navigate">
<el-icon :size="30"><icon-cs-gobang /></el-icon>
@ -10,13 +10,9 @@
</el-header>
</template>
<style lang="scss" scoped>
@use '@/assets/mixins' as m;
.gobang-header {
--el-header-height: var(--header-height);
border-bottom: 1px solid var(--el-border-color);
position: fixed;
top: var(--header-height);
width: 100%;
z-index: 100;
@include m.header(1);
user-select: none;
&__left {
z-index: 1;

View File

@ -0,0 +1,15 @@
<template>
<div class="flex items-center gap-5px">
<div
class="gobang-chessboard__cell"
:class="white ? 'gobang-chessboard__cell--white' : 'gobang-chessboard__cell--black'"
></div>
<div class="font-bold">
<template v-if="white">白方</template>
<template v-else>黑方</template>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{ white: boolean }>();
</script>

View File

@ -28,8 +28,8 @@ export interface UseGameSocketOptions<
error?: {
[P in TRequest['name']]?: (reason: string) => boolean | void;
};
pre?: {
// 只有拥有依赖关系的请求才可以有pre
before?: {
// 只有拥有依赖关系的请求才可以有before
[P in keyof TRelations]?: (error: boolean) => void;
};
finally?: {
@ -83,7 +83,7 @@ export function useGameSocket<TRequest extends SimplePart<string>, TResp extends
error = true;
if (relations[originalPacketName]) {
reqName = originalPacketName;
options.pre?.[reqName]?.(true);
options.before?.[reqName]?.(true);
relationMap.delete(originalPacketName);
}
const errorHandler = options.error?.[originalPacketName];
@ -104,7 +104,7 @@ export function useGameSocket<TRequest extends SimplePart<string>, TResp extends
if (set.size) continue;
relationMap.delete(k);
reqName = k;
options.pre?.[reqName]?.(false);
options.before?.[reqName]?.(false);
break;
}
}

View File

@ -1,5 +1,5 @@
<template>
<el-main class="game-2048-page__wrapper !flex justify-center">
<el-main class="game-2048-page-wrapper !flex justify-center">
<div class="game-2048-page">
<div class="game-header">
<div class="game-title">
@ -24,7 +24,7 @@
</el-main>
</template>
<style lang="scss" scoped>
.game-2048-page__wrapper {
.game-2048-page-wrapper {
--font-color: rgb(119, 110, 101);
font-family: 'Arial', '微软雅黑', '黑体', sans-serif;
background-color: rgb(250, 248, 239);

View File

@ -1,5 +1,5 @@
<template>
<el-main class="gobang-list-page__wrapper !flex bg-white">
<el-main class="!flex bg-white">
<el-container>
<gobang-header class="justify-between">
<div class="flex items-center">
@ -31,7 +31,9 @@
<template #default="{ row }">
<el-button
type="success"
:disabled="(row as RoomRender).state === RoomState.GAMING"
:disabled="
arrayIncludes([RoomState.GAMING, RoomState.FINISHED], (row as RoomRender).state)
"
@click="onJoinButtonClick(row)"
>
<template

View File

@ -1,5 +1,5 @@
<template>
<el-main v-loading="firstLoading || placing" class="gobang-play-page__wrapper !flex bg-white">
<el-main v-loading="firstLoading || placing" class="!flex bg-white">
<el-container direction="vertical">
<gobang-header class="justify-between">
<div class="flex items-center">
@ -14,47 +14,76 @@
<h2 v-else>单人游戏</h2>
</div>
</gobang-header>
<el-container direction="horizontal">
<el-main class="gobang-play-page-main !flex flex-col justify-center items-center">
<div
v-loading="matchState === MatchState.FINISHED"
class="gobang-chessboard"
element-loading-svg-view-box="0 0 0 0"
:class="{
'gobang-chessboard--enabled': enabled,
}"
<el-container class="gobang-play-page-main-wrapper" direction="horizontal">
<el-container direction="vertical">
<el-header
v-if="mdLess && otherUser"
class="other-header flex justify-between items-center"
>
<canvas class="gobang-chessboard__background" ref="canvas"></canvas>
<template v-for="(row, y) of grid">
<template v-for="(cell, x) of row">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
class="gobang-chessboard__cell absolute border border-black rounded-full"
:class="[
cell
? cell.isWhite
? 'gobang-chessboard__cell--white'
: 'gobang-chessboard__cell--black'
: undefined,
,
{
'gobang-chessboard__cell--win-blink': blink?.[y][x],
'gobang-chessboard__cell--last':
lastChess && lastChess[0] === x && lastChess[1] === y,
},
]"
:style="getCellStyle(cell, x, y)"
@click="onCellClick(cell, x, y)"
></div>
<gobang-user footer :user="otherUser" />
<div class="absolute-center font-bold text-5">
{{ otherPrompt }}
</div>
<gobang-side :white="!white" />
</el-header>
<el-main
class="gobang-play-page-main !flex flex-col justify-center items-center realtive"
>
<div
v-loading="matchState === MatchState.FINISHED"
class="gobang-chessboard"
element-loading-svg-view-box="0 0 0 0"
:class="{
'gobang-chessboard--enabled': enabled,
}"
>
<canvas class="gobang-chessboard__background" ref="canvas"></canvas>
<template v-for="(row, y) of grid">
<template v-for="(cell, x) of row">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
class="gobang-chessboard__cell absolute"
:class="[
cell
? cell.isWhite
? 'gobang-chessboard__cell--white'
: 'gobang-chessboard__cell--black'
: undefined,
,
{
'gobang-chessboard__cell--win-blink': blink?.[y][x],
'gobang-chessboard__cell--last':
lastChess && lastChess[0] === x && lastChess[1] === y,
},
]"
:style="getCellStyle(cell, x, y)"
@click="onCellClick(cell, x, y)"
@touchend="onCellClick(cell, x, y)"
></div>
</template>
</template>
</template>
</div>
</el-main>
</div>
<el-button
v-if="mdLess && matchState === MatchState.FINISHED"
class="absolute bottom-10px"
type="primary"
:loading="restartState === RestartState.RESTARTING"
:disabled="restartState === RestartState.WAITING"
@click="restart"
>
重新开始
</el-button>
</el-main>
</el-container>
<el-aside
v-if="!mdLess"
class="gobang-play-page-aside !flex flex-col items-center justify-between"
>
<div><gobang-user v-if="otherUser" :footer="false" :user="otherUser" /></div>
<div class="flex flex-col items-center gap-5px">
<gobang-user v-if="otherUser" :user="otherUser" />
<gobang-side :white="!white" />
<div class="font-bold">{{ otherPrompt }}</div>
</div>
<div>
<div
v-if="otherUser"
@ -69,46 +98,51 @@
等待玩家加入.
</div>
</div>
<div class="flex flex-col items-center">
<div class="flex flex-col items-center gap-1">
<gobang-user v-if="userStore.userInfo" :user="userStore.userInfo" />
<div v-if="matchState === MatchState.GAMING" class="font-bold text-5">
<template v-if="selfRound">请落子.</template>
<template v-else>等待对方落子.</template>
</div>
<template v-else-if="matchState === MatchState.FINISHED">
<div class="font-bold text-5">
<template v-if="restartState === RestartState.WAITING">等待对方重新开始.</template>
<template v-else-if="otherRequiresRestart">对方已重新开始.</template>
<template v-else>等待重新开始.</template>
</div>
<el-button
type="primary"
:loading="restartState === RestartState.RESTARTING"
:disabled="restartState === RestartState.WAITING"
@click="restart"
>
重新开始
</el-button>
</template>
<gobang-side :white="white" />
<div v-if="selfPrompt" class="font-bold">{{ selfPrompt }}</div>
<el-button
v-if="matchState === MatchState.FINISHED"
type="primary"
:loading="restartState === RestartState.RESTARTING"
:disabled="restartState === RestartState.WAITING"
@click="restart"
>
重新开始
</el-button>
</div>
</el-aside>
</el-container>
<el-footer v-if="mdLess" class="gobang-play-page-footer flex items-center">
<el-footer
v-if="mdLess"
class="gobang-play-page-footer flex justify-between items-center relative"
>
<gobang-user v-if="userStore.userInfo" footer :user="userStore.userInfo" />
<div class="absolute-self-center font-bold text-5">
<template v-if="otherUser">
{{ selfPrompt }}
</template>
<template v-else>等待玩家加入.</template>
</div>
<gobang-side :white="white" />
</el-footer>
</el-container>
</el-main>
</template>
<style lang="scss" scoped>
@use '@/assets/mixins' as m;
.gobang-play-page {
&-main-wrapper {
margin-top: var(--header-height);
}
&-main {
--el-main-padding: 0;
margin-top: var(--header-height);
margin-top: v-bind(mainMarginTopCSS);
position: relative;
}
&-aside {
position: relative;
margin-top: var(--header-height);
border-left: 1px solid var(--el-border-color);
width: 200px;
padding: 80px 0;
@ -118,46 +152,15 @@
--el-footer-height: var(--header-height);
}
}
.other-header {
@include m.header(2);
}
.gobang-chessboard {
position: relative;
zoom: v-bind(boardZoom);
&__cell {
--size: 24px;
--el-mask-color: rgb(255, 255, 255, 0.7);
.gobang-chessboard__cell {
transform: translate(-50%, -50%);
width: var(--size);
height: var(--size);
user-select: none;
&--white {
@apply bg-white border-solid border-inset;
}
&--black {
@apply bg-black border-solid border-inset;
}
&--last {
&::after {
@apply absolute inset--1;
content: '';
border: 2px solid red;
animation: blink 1s ease-in-out infinite;
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
}
&--win-blink {
// box-shadow: 0 0 10px red;
//
z-index: 2001;
}
}
&--enabled {
.gobang-chessboard__cell {
@ -168,6 +171,43 @@
}
}
}
::v-deep .gobang-chessboard__cell {
@apply border border-black rounded-full;
--size: 24px;
width: var(--size);
height: var(--size);
user-select: none;
&--white {
@apply bg-white border-inset;
}
&--black {
@apply bg-black;
}
&--last {
&::after {
@apply absolute inset--1;
content: '';
border: 2px solid red;
animation: blink 1s ease-in-out infinite;
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
}
&--win-blink {
// box-shadow: 0 0 10px red;
//
z-index: 2001;
}
}
</style>
<script lang="ts">
const boardBackground = '#E2CFA9';
@ -192,13 +232,14 @@ const deltaMap: Record<WinFace, [number, number]> = {
</script>
<script setup lang="ts">
import GobangHeader from '@/components/gobang/GobangHeader.vue';
import GobangSide from '@/components/gobang/GobangSide.vue';
import GobangUser from '@/components/gobang/GobangUser.vue';
import router from '@/router';
import { useMediaStore } from '@/stores/media';
import { useUserStore } from '@/stores/user';
import { arrayIncludes, create2DArray, iter2DArray, zip } from '@/utils';
import { create2DArray, iter2DArray, zip } from '@/utils';
import { useGobangSocket, WinFace, type RoomDetail, type RoomId } from '@/views/GobangListPage.vue';
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
import { ElMessage, ElNotification } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watch, watchEffect, type CSSProperties } from 'vue';
import { useRoute } from 'vue-router';
@ -206,11 +247,14 @@ const userStore = useUserStore();
const { smLess, mdLess } = storeToRefs(useMediaStore());
const roomId = useRoute().params.id as RoomId | undefined;
const canvas = ref<HTMLCanvasElement>();
const mainMarginTopCSS = computed(() =>
mdLess.value && otherUser.value ? 'var(--header-height)' : 0,
);
/** 棋盘大小 */
const boardSize = computed(
() => gridSize * (boardLength - 1) + boardBorderSize * 2 + boardPadding * 2,
);
const boardZoom = computed(() => (smLess.value ? 0.75 : 1));
const boardZoom = computed(() => (smLess.value ? 0.75 : mdLess.value ? 0.9 : 1));
watchEffect(() => {
if (!canvas.value) return;
const ctx = canvas.value.getContext('2d');
@ -267,6 +311,27 @@ function getCellStyle(chess: Chess | undefined, x: number, y: number) {
};
return res;
}
const selfPrompt = computed(() => {
switch (matchState.value) {
case MatchState.GAMING:
if (selfRound.value) return '请落子.';
else return '等待对方落子.';
case MatchState.FINISHED:
if (restartState.value === RestartState.WAITING) return '等待对方重新开始.';
return '等待重新开始.';
}
return undefined;
});
const otherPrompt = computed(() => {
switch (matchState.value) {
case MatchState.GAMING:
if (!selfRound.value) return '对方思考中.';
return undefined;
case MatchState.FINISHED:
if (otherRequiresRestart.value) return '对方已请求重新开始.';
}
return undefined;
});
const firstLoading = ref(true);
enum MatchState {
WAITING = 'WAITING',
@ -287,22 +352,16 @@ watch(matchState, (v) => {
case MatchState.GAMING:
ElMessage('对局开始');
break;
case MatchState.FINISHED:
resetLoadingStates();
break;
}
});
const placing = ref(false);
const selfRound = ref(false);
function resetSelfRound() {
selfRound.value = !isWhite.value;
selfRound.value = !white.value;
}
const enabled = computed(
() => matchState.value === MatchState.GAMING && selfRound.value && !placing.value,
);
function resetLoadingStates() {
placing.value = false;
}
const room = ref<RoomDetail>();
const otherUser = computed(() => {
if (!room.value) return;
@ -315,7 +374,7 @@ watch(
updateMatchState();
},
);
const isWhite = ref(false);
const white = ref(false);
const grid = computed<Grid | undefined>(() =>
room.value?.pieces.map((row) => row.map((v) => (v !== -1 ? { isWhite: !!v } : undefined))),
);
@ -339,9 +398,6 @@ watch(
);
const blink = ref<boolean[][]>();
const lastChess = ref<[number, number]>();
function resetBlink() {
blink.value = undefined;
}
const win = ref(false);
enum RestartState {
NO = 'NO',
@ -350,17 +406,11 @@ enum RestartState {
}
const restartState = ref<RestartState>(RestartState.NO);
function resetRestartState() {
restartState.value = RestartState.NO;
}
const otherRequiresRestart = ref(false);
function resetOtherRequiresRestart() {
otherRequiresRestart.value = false;
}
function resetNecessaryStates() {
resetSelfRound();
resetBlink();
resetRestartState();
blink.value = undefined;
restartState.value = RestartState.NO;
lastChess.value = undefined;
}
function onCellClick(cell: Chess | undefined, x: number, y: number) {
@ -383,24 +433,24 @@ const { send } = useGobangSocket({
});
},
PlayerSideAllocation(p) {
isWhite.value = p.isWhite;
white.value = p.isWhite;
resetSelfRound();
},
RoomInfo(p) {
room.value = p;
const { whiteRequestRestart, blackRequestRestart } = room.value;
const selfRequiresRestart = isWhite.value ? whiteRequestRestart : blackRequestRestart;
const selfRequiresRestart = white.value ? whiteRequestRestart : blackRequestRestart;
otherRequiresRestart.value =
(isWhite.value ? blackRequestRestart : whiteRequestRestart) ?? false;
(white.value ? blackRequestRestart : whiteRequestRestart) ?? false;
if (matchState.value === MatchState.GAMING) {
selfRound.value = isWhite.value === p.canWhiteDown;
selfRound.value = white.value === p.canWhiteDown;
}
if (
restartState.value === RestartState.WAITING &&
!selfRequiresRestart &&
!otherRequiresRestart.value
) {
resetOtherRequiresRestart();
otherRequiresRestart.value = false;
resetNecessaryStates();
updateMatchState();
}
@ -426,22 +476,20 @@ const { send } = useGobangSocket({
},
PlayerWin() {
win.value = true;
ElMessageBox({
ElNotification({
title: '你赢了!',
message: '🎉🥳🎊',
type: 'success',
center: true,
confirmButtonText: '点按空白处关闭',
position: 'bottom-left',
});
},
PlayerLose() {
win.value = false;
ElMessageBox({
ElNotification({
title: '你输了!',
message: '🤣👉🤡',
type: 'error',
center: true,
confirmButtonText: '点按空白处关闭',
position: 'bottom-left',
});
},
},
@ -451,7 +499,7 @@ const { send } = useGobangSocket({
return false;
},
},
pre: {
before: {
ResetRoom() {
restartState.value = RestartState.WAITING;
},