✨ feat: 完成五子棋棋盘
This commit is contained in:
parent
601fa7cbb9
commit
a347115ca3
@ -78,6 +78,9 @@
|
||||
.app-header {
|
||||
--el-header-height: var(--header-height);
|
||||
--el-loading-spinner-size: 30px;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
// 侧边栏需要居中
|
||||
@ -92,7 +95,7 @@
|
||||
|
||||
.app-router-view-enter-active,
|
||||
.app-router-view-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.app-router-view-enter-from,
|
||||
|
@ -31,6 +31,9 @@ body {
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
.app-header-menu--horizontal {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.app-header-menu :is(.el-menu-item, .el-sub-menu__title) {
|
||||
user-select: none;
|
||||
|
22
src/components/gobang/GobangHeader.vue
Normal file
22
src/components/gobang/GobangHeader.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<el-header class="gobang-header flex align-center">
|
||||
<div class="flex align-center">
|
||||
<el-icon :size="30"><icon-cs-gobang /></el-icon>
|
||||
<h2 v-show="!smLess" class="gobang-header__title">五子棋</h2>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</el-header>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.gobang-header {
|
||||
--el-header-height: var(--header-height);
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
</script>
|
@ -66,33 +66,29 @@ router.beforeEach(async (to) => {
|
||||
const pageStore = usePageStore();
|
||||
const { permissionId } = to.meta;
|
||||
pageStore.setNewRouteId(to);
|
||||
try {
|
||||
if (!userStore.userInfo) {
|
||||
const succeed = await userStore.updateSelfUserInfo(true);
|
||||
if (!succeed) {
|
||||
if (permissionId === undefined) {
|
||||
return true;
|
||||
}
|
||||
return pageStore.createTempErrorRoute(
|
||||
{
|
||||
type: PageErrorType.NETWORK_ERROR,
|
||||
},
|
||||
to,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (to.meta.shouldLogin && !userStore.logined) {
|
||||
return pageStore.createTempErrorRoute({ type: PageErrorType.NOT_LOGIN }, to);
|
||||
}
|
||||
if (permissionId) {
|
||||
if (userStore.hasPermission(permissionId)) {
|
||||
if (!userStore.userInfo) {
|
||||
const succeed = await userStore.updateSelfUserInfo(true);
|
||||
if (!succeed) {
|
||||
if (permissionId === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
return pageStore.createTempErrorRoute({ type: PageErrorType.NO_PERMISSION }, to);
|
||||
}
|
||||
return pageStore.createTempErrorRoute(
|
||||
{
|
||||
type: PageErrorType.NETWORK_ERROR,
|
||||
},
|
||||
to,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (to.meta.shouldLogin && !userStore.logined) {
|
||||
return pageStore.createTempErrorRoute({ type: PageErrorType.NOT_LOGIN }, to);
|
||||
}
|
||||
if (permissionId) {
|
||||
if (userStore.hasPermission(permissionId)) {
|
||||
return true;
|
||||
} else {
|
||||
return pageStore.createTempErrorRoute({ type: PageErrorType.NO_PERMISSION }, to);
|
||||
}
|
||||
} finally {
|
||||
pageStore.removeRouteId(to);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -1,6 +1,10 @@
|
||||
export function create2DArray<T>(height: number, width: number, cell: T | (() => T)): T[][] {
|
||||
return Array.from({ length: height }, () =>
|
||||
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell)),
|
||||
export function create2DArray<T>(
|
||||
height: number,
|
||||
width: number,
|
||||
cell: T | ((x: number, y: number) => T),
|
||||
): T[][] {
|
||||
return Array.from({ length: height }, (_, y) =>
|
||||
Array.from({ length: width }, (_, x) => (cell instanceof Function ? cell(x, y) : cell)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
<template>
|
||||
<el-main class="gobang-list-page__wrapper flex">
|
||||
<el-container class="flex-stretch">
|
||||
<el-header class="gobang-list-page-header flex align-center">
|
||||
<el-icon :size="30"><icon-cs-gobang /></el-icon>
|
||||
<div v-show="!smLess" class="gobang-list-page-header__title">五子棋</div>
|
||||
<el-container direction="vertical">
|
||||
<gobang-header class="justify-between">
|
||||
<div class="gobang-list-page-header__button-wrapper flex align-center">
|
||||
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
|
||||
<el-button @click="showDialog = true" type="success">创建房间</el-button>
|
||||
<el-button type="primary">单人游戏</el-button>
|
||||
<el-button type="primary" disabled>单人游戏</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
</gobang-header>
|
||||
<el-main
|
||||
v-loading="loading"
|
||||
:element-loading-text="loadingText"
|
||||
@ -46,15 +44,6 @@
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.gobang-list-page-header {
|
||||
--el-header-height: var(--header-height);
|
||||
background-color: white;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
&__button-wrapper {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.gobang-list-page-main {
|
||||
gap: 10px;
|
||||
&__no-rooms-title {
|
||||
@ -130,31 +119,29 @@ export function useGobangSocket(respHandlers: RespHandlerMap) {
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import CreateGobangRoomDialog from '@/components/gobang/CreateGobangRoomDialog.vue';
|
||||
import GobangHeader from '@/components/gobang/GobangHeader.vue';
|
||||
import router from '@/router';
|
||||
import type { SucceedUserInfoResponse } from '@/schemas';
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref, toRaw, watch } from 'vue';
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
import { onMounted, ref, toRaw } from 'vue';
|
||||
const showDialog = ref(false);
|
||||
const rooms = ref<RoomRender[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadingText = ref<string>();
|
||||
const { send } = useGobangSocket({
|
||||
Error(payload) {
|
||||
ElMessage.error(payload.reason);
|
||||
Error(p) {
|
||||
ElMessage.error(p.reason);
|
||||
loading.value = false;
|
||||
},
|
||||
RoomList(payload) {
|
||||
rooms.value = payload.rooms.map(toRoomRender);
|
||||
RoomList(p) {
|
||||
rooms.value = p.rooms.map(toRoomRender);
|
||||
console.log(toRaw(rooms.value));
|
||||
loading.value = false;
|
||||
},
|
||||
RoomCreated(payload) {
|
||||
play(payload.roomId);
|
||||
RoomCreated(p) {
|
||||
// play(p.roomId);
|
||||
},
|
||||
});
|
||||
function refresh() {
|
||||
@ -187,7 +174,4 @@ function play(roomId: RoomId) {
|
||||
function createRoom() {
|
||||
send(JSON.stringify({ name: 'CreateRoom' } satisfies RequestOf<'CreateRoom'>));
|
||||
}
|
||||
function resetRooms() {
|
||||
send(JSON.stringify({ name: 'ResetRoom' } satisfies RequestOf<'ResetRoom'>));
|
||||
}
|
||||
</script>
|
||||
|
@ -1,15 +1,188 @@
|
||||
<template>
|
||||
<el-main class="gobang-play-page">
|
||||
<canvas ref="chessBoard"></canvas>
|
||||
<el-main v-loading="state !== 'idle'" class="gobang-play-page__wrapper flex">
|
||||
<el-container direction="vertical">
|
||||
<gobang-header class="justify-between">
|
||||
<div class="flex align-center">
|
||||
<template v-if="roomId">
|
||||
<h2 v-show="!smLess">蓝色基因对战平台</h2>
|
||||
<h3 class="gobang-play-page__brief-id flex center">#{{ roomId.slice(0, 8) }}</h3>
|
||||
</template>
|
||||
</div>
|
||||
</gobang-header>
|
||||
<el-main class="gobang-play-page-main center flex">
|
||||
<template v-if="state !== 'firstLoading'">
|
||||
<div class="gobang-chessboard" :style="{}">
|
||||
<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
|
||||
v-if="state !== 'waiting'"
|
||||
class="gobang-chessboard__cell"
|
||||
:class="[
|
||||
cell
|
||||
? cell.isWhite
|
||||
? 'gobang-chessboard__cell--white'
|
||||
: 'gobang-chessboard__cell--black'
|
||||
: undefined,
|
||||
]"
|
||||
:style="getCellStyle(cell, x, y)"
|
||||
></div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-main>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.gobang-play-page {
|
||||
&__wrapper {
|
||||
background-color: white;
|
||||
}
|
||||
&__brief-id {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
.gobang-header {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
.gobang-play-page-main {
|
||||
position: relative;
|
||||
padding-top: var(--header-height);
|
||||
}
|
||||
.gobang-chessboard {
|
||||
position: relative;
|
||||
zoom: v-bind(boardZoomCSS);
|
||||
&__cell {
|
||||
--size: 24px;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
@mixin border {
|
||||
border: 1px solid black;
|
||||
}
|
||||
&--white {
|
||||
@include border;
|
||||
background-color: white;
|
||||
}
|
||||
&--black {
|
||||
@include border;
|
||||
background-color: black;
|
||||
}
|
||||
user-select: none;
|
||||
&:not(&--white, &--black):hover {
|
||||
background-color: red;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import GobangHeader from '@/components/gobang/GobangHeader.vue';
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { create2DArray } from '@/utils';
|
||||
import { type RequestOf, type RoomId, useGobangSocket } from '@/views/GobangListPage.vue';
|
||||
import { ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onMounted, ref, watchEffect, type CSSProperties } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const { send } = useGobangSocket({});
|
||||
const roomId = useRoute().params.id as RoomId;
|
||||
const chessBoard = ref<HTMLCanvasElement>();
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
const roomId = useRoute().params.id as RoomId | undefined;
|
||||
const canvas = ref<HTMLCanvasElement>();
|
||||
const boardBackground = '#E2CFA9';
|
||||
const boardLength = 16;
|
||||
const gridSize = 30;
|
||||
const lineSize = 2;
|
||||
/** 棋盘外边框大小 */
|
||||
const boardOuterBorderSize = 2;
|
||||
/** 棋盘外边框+内边框总大小 */
|
||||
const boardBorderSize = 5;
|
||||
const boardPadding = 15;
|
||||
/** 棋盘大小 */
|
||||
const boardSize = computed(
|
||||
() => gridSize * (boardLength - 1) + boardBorderSize * 2 + boardPadding * 2,
|
||||
);
|
||||
const boardZoomCSS = computed(() => (smLess.value ? 0.75 : 1));
|
||||
watchEffect(() => {
|
||||
if (!canvas.value) return;
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const _boardBorderSize = boardBorderSize;
|
||||
const _boardSize = boardSize.value;
|
||||
canvas.value.width = _boardSize;
|
||||
canvas.value.height = _boardSize;
|
||||
ctx.clearRect(0, 0, _boardSize, _boardSize);
|
||||
ctx.fillStyle = boardBackground;
|
||||
ctx.fillRect(0, 0, _boardSize, _boardSize);
|
||||
ctx.strokeStyle = 'black';
|
||||
const drawBorder = (borderSize: number, offset: number) => {
|
||||
ctx.lineWidth = borderSize;
|
||||
ctx.strokeRect(
|
||||
borderSize / 2 + offset,
|
||||
borderSize / 2 + offset,
|
||||
_boardSize - borderSize - offset * 2,
|
||||
_boardSize - borderSize - offset * 2,
|
||||
);
|
||||
};
|
||||
drawBorder(2, 0);
|
||||
drawBorder(boardOuterBorderSize, boardPadding);
|
||||
drawBorder(lineSize, _boardBorderSize - lineSize + boardPadding);
|
||||
ctx.lineWidth = lineSize;
|
||||
const _gridSize = gridSize;
|
||||
for (let i = 1; i < boardLength - 1; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(boardPadding + _boardBorderSize + _gridSize * i, _boardBorderSize + boardPadding);
|
||||
ctx.lineTo(
|
||||
boardPadding + _boardBorderSize + _gridSize * i,
|
||||
_boardSize - _boardBorderSize - boardPadding,
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 1; i < boardLength - 1; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(_boardBorderSize + boardPadding, boardPadding + _boardBorderSize + _gridSize * i);
|
||||
ctx.lineTo(
|
||||
_boardSize - _boardBorderSize - boardPadding,
|
||||
boardPadding + _boardBorderSize + _gridSize * i,
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
console.log('rerender');
|
||||
});
|
||||
interface Chess {
|
||||
isWhite: boolean;
|
||||
}
|
||||
const grid = ref(
|
||||
create2DArray<Chess | undefined>(boardLength, boardLength, (x, y) =>
|
||||
x >= 8
|
||||
? undefined
|
||||
: {
|
||||
isWhite: !!((x + y) % 2),
|
||||
},
|
||||
),
|
||||
);
|
||||
function getCellStyle(chess: Chess | undefined, x: number, y: number) {
|
||||
const res: CSSProperties = {
|
||||
left: `${boardPadding + boardBorderSize + gridSize * x}px`,
|
||||
top: `${boardPadding + boardBorderSize + gridSize * y}px`,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
const state = ref<'firstLoading' | 'loading' | 'idle' | 'waiting'>('firstLoading');
|
||||
const isWhite = ref(false);
|
||||
const { send } = useGobangSocket({
|
||||
PlayerSideAllocation(p) {
|
||||
isWhite.value = p.isWhite;
|
||||
state.value = 'idle';
|
||||
},
|
||||
});
|
||||
function playerJoin(roomId: RoomId) {
|
||||
send(
|
||||
JSON.stringify({
|
||||
@ -20,4 +193,9 @@ function playerJoin(roomId: RoomId) {
|
||||
} satisfies RequestOf<'PlayerJoin'>),
|
||||
);
|
||||
}
|
||||
onMounted(() => {
|
||||
if (roomId) {
|
||||
playerJoin(roomId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -30,7 +30,12 @@ export default defineConfig({
|
||||
customCollections: ['cs'],
|
||||
}),
|
||||
],
|
||||
globs: ['!src/components/**/*.vue', '!src/views/**/*.vue'],
|
||||
globs: [
|
||||
'!src/components/*.vue',
|
||||
'!src/components/**/*.vue',
|
||||
'!src/views/*.vue',
|
||||
'!src/views/**/*.vue',
|
||||
],
|
||||
}),
|
||||
Icons({
|
||||
autoInstall: true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user