🎈 perf: 完善五子棋房间列表页
This commit is contained in:
parent
c7c13d019f
commit
601fa7cbb9
4
components.d.ts
vendored
4
components.d.ts
vendored
@ -25,8 +25,12 @@ declare module 'vue' {
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
GobangListPage: typeof import('./src/views/GobangListPage.vue')['default']
|
||||
GobangPlayPage: typeof import('./src/views/GobangPlayPage.vue')['default']
|
||||
IconCsClub: typeof import('~icons/cs/club')['default']
|
||||
IconCsGobang: typeof import('~icons/cs/gobang')['default']
|
||||
IconCsLock: typeof import('~icons/cs/lock')['default']
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
|
13
src/App.vue
13
src/App.vue
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<el-container class="app__container">
|
||||
<el-header class="app-header flex align-center">
|
||||
<el-icon v-show="lgLess" size="30" @click="showVerticalHeaderMenu = true">
|
||||
<el-icon v-show="mdLess" size="30" @click="showVerticalHeaderMenu = true">
|
||||
<icon-cs-menu />
|
||||
</el-icon>
|
||||
<router-link :custom="true" to="/" v-slot="{ navigate }">
|
||||
<router-link custom to="/" v-slot="{ navigate }">
|
||||
<el-icon size="50" @click="navigate"><icon-cs-club /></el-icon>
|
||||
<h3 v-show="!smLess" class="app__title" @click="navigate">社团展示系统</h3>
|
||||
</router-link>
|
||||
<header-menu v-show="!lgLess" mode="horizontal" />
|
||||
<el-dropdown v-if="!lgLess && userStore.logined">
|
||||
<header-menu v-show="!mdLess" mode="horizontal" />
|
||||
<el-dropdown v-if="!mdLess && userStore.logined">
|
||||
<header-user @login="onLoginButtonClick" @logout="logout" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@ -73,7 +73,6 @@
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.app__container {
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.app-header {
|
||||
@ -120,7 +119,7 @@ import { provide, ref, watch } from 'vue';
|
||||
import { loginResponseSchema, registerResponseSchema } from './schemas/response';
|
||||
const userStore = useUserStore();
|
||||
const pageStore = usePageStore();
|
||||
const { smLess, lgLess } = storeToRefs(useMediaStore());
|
||||
const { smLess, mdLess } = storeToRefs(useMediaStore());
|
||||
const showLoginRegisterDialog = ref(false);
|
||||
provide(loginImplKey, async (params) => {
|
||||
const loginRespRaw = await axiosInstance
|
||||
@ -158,7 +157,7 @@ async function logout() {
|
||||
await userStore.updateSelfUserInfo(true);
|
||||
}
|
||||
const showVerticalHeaderMenu = ref(false);
|
||||
watch(lgLess, (v) => {
|
||||
watch(mdLess, (v) => {
|
||||
if (v) return;
|
||||
showVerticalHeaderMenu.value = false;
|
||||
});
|
||||
|
@ -11,7 +11,6 @@ body {
|
||||
}
|
||||
|
||||
#app {
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.flex {
|
||||
|
@ -4,7 +4,7 @@
|
||||
:class="[`app-header-menu--${mode}`]"
|
||||
:mode="mode"
|
||||
:default-active="$route.fullPath"
|
||||
:router="true"
|
||||
router
|
||||
@select="$emit('select')"
|
||||
>
|
||||
<el-menu-item index="/">首页</el-menu-item>
|
||||
|
@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
:fullscreen="xs"
|
||||
:align-center="true"
|
||||
class="login-dialog"
|
||||
width="400"
|
||||
>
|
||||
<el-dialog v-model="show" :fullscreen="xs" align-center class="login-register-dialog" width="400">
|
||||
<el-tabs
|
||||
class="login-dialog__tabs"
|
||||
:class="{ 'login-dialog__tabs--center': xs }"
|
||||
:stretch="true"
|
||||
class="login-register-dialog__tabs"
|
||||
:class="{ 'login-register-dialog__tabs--center': xs }"
|
||||
stretch
|
||||
v-model="loginRegisterDialogActiveName"
|
||||
>
|
||||
<el-tab-pane label="登录" name="login">
|
||||
@ -108,7 +102,7 @@
|
||||
<el-form-item prop="verifyCode">
|
||||
<verify-input v-model="registerFormData" :disabled="registering" />
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-bottom: 0">
|
||||
<el-form-item>
|
||||
<el-button
|
||||
:loading="registering"
|
||||
native-type="submit"
|
||||
@ -125,10 +119,15 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.login-dialog__tabs {
|
||||
&--center .el-tabs__nav-scroll {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.login-register-dialog {
|
||||
&__tabs {
|
||||
&--center .el-tabs__nav-scroll {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.el-form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,31 @@
|
||||
<template>
|
||||
<el-dialog></el-dialog>
|
||||
<el-dialog
|
||||
class="create-gobang-room-dialog"
|
||||
v-model="show"
|
||||
title="创建房间"
|
||||
align-center
|
||||
:fullscreen="smLess"
|
||||
>
|
||||
<el-button type="success" @click="onCreateRoomButtonClick">创建房间</el-button>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
<script setup lang="ts"></script>
|
||||
<style lang="scss">
|
||||
.create-gobang-room-dialog {
|
||||
.el-dialog__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
const show = defineModel({ default: false });
|
||||
const emit = defineEmits(['click']);
|
||||
function onCreateRoomButtonClick() {
|
||||
show.value = false;
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
@ -33,7 +33,16 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
path: '/gobang',
|
||||
component: () => import('@/views/GobangPage.vue'),
|
||||
name: 'GobangList',
|
||||
component: () => import('@/views/GobangListPage.vue'),
|
||||
meta: {
|
||||
shouldLogin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/gobang/:id',
|
||||
name: 'GobangPlay',
|
||||
component: () => import('@/views/GobangPlayPage.vue'),
|
||||
meta: {
|
||||
shouldLogin: true,
|
||||
},
|
||||
|
@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<el-main class="error-page__wrapper flex center">
|
||||
<div class="error-container">
|
||||
<div class="error-reason">{{ descriptionMap[type] }}</div>
|
||||
</div>
|
||||
<el-main class="error-page flex flex-column center">
|
||||
<div class="error-page__reason">{{ descriptionMap[type] }}</div>
|
||||
<router-link custom v-slot="{ navigate }" to="/">
|
||||
<el-button type="primary" @click="navigate">返回首页</el-button>
|
||||
</router-link>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-reason {
|
||||
font-size: 50px;
|
||||
.error-page {
|
||||
gap: 10px;
|
||||
&__reason {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
@ -27,7 +32,7 @@ const descriptionMap: Record<PageErrorType, string> = {
|
||||
[PageErrorType.NOT_FOUND]: '404',
|
||||
[PageErrorType.NETWORK_ERROR]: '网络错误',
|
||||
[PageErrorType.NO_PERMISSION]: '没有权限',
|
||||
[PageErrorType.NOT_LOGIN]: '需要登录',
|
||||
[PageErrorType.NOT_LOGIN]: '你需要登录才能访问此页面',
|
||||
};
|
||||
const reason = defineProps<PageErrorReason>();
|
||||
defineProps<PageErrorReason>();
|
||||
</script>
|
||||
|
193
src/views/GobangListPage.vue
Normal file
193
src/views/GobangListPage.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main
|
||||
v-loading="loading"
|
||||
:element-loading-text="loadingText"
|
||||
class="gobang-list-page-main flex-column"
|
||||
:class="{ flex: !rooms.length, center: !rooms.length }"
|
||||
>
|
||||
<template v-if="rooms.length || !loading">
|
||||
<template v-if="!rooms.length">
|
||||
<div class="gobang-list-page-main__no-rooms-title">没有房间</div>
|
||||
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
|
||||
</template>
|
||||
<el-table v-else :data="rooms">
|
||||
<el-table-column prop="briefId" label="房间ID" />
|
||||
<el-table-column prop="briefId" :formatter="getPlayerCount" label="房间人数" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button type="success" :loading="row.joining" @click="onJoinButtonClick(row)">
|
||||
加入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<create-gobang-room-dialog v-model="showDialog" @click="createRoom" />
|
||||
</el-main>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.gobang-list-page__wrapper {
|
||||
background-color: white;
|
||||
}
|
||||
.gobang-list-page-header__title {
|
||||
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 {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
/** 房间状态 */
|
||||
export enum RoomState {
|
||||
/** 房间无人 */
|
||||
CREATED = 'CREATED',
|
||||
/** 房间等人 */
|
||||
WAITING = 'WAITING',
|
||||
/** 正在游戏 */
|
||||
GAMING = 'GAMING',
|
||||
}
|
||||
/** 房间UUID */
|
||||
export type RoomId = string;
|
||||
export interface Room {
|
||||
id: RoomId;
|
||||
full: boolean;
|
||||
state: RoomState;
|
||||
isWhiteAcceptRestart: boolean;
|
||||
isBlackAcceptRestart: boolean;
|
||||
canWhiteDown: boolean;
|
||||
}
|
||||
export interface RoomRender extends Room {
|
||||
briefId: string;
|
||||
}
|
||||
export function toRoomRender(room: Room): RoomRender {
|
||||
return Object.assign(room, {
|
||||
briefId: room.id.slice(0, 8),
|
||||
});
|
||||
}
|
||||
export type Request =
|
||||
| SimplePart<'RoomList'>
|
||||
| SimplePart<'CreateRoom'>
|
||||
| Part<'PlayerJoin', { roomId: RoomId }>
|
||||
| SimplePart<'ResetRoom'>;
|
||||
export type RequestOf<T extends Request['name']> = Extract<Request, { name: T }>;
|
||||
export interface SimplePart<N extends string> {
|
||||
name: N;
|
||||
}
|
||||
export interface Part<N extends string, P extends Record<string, unknown>> extends SimplePart<N> {
|
||||
payload: P;
|
||||
}
|
||||
export type RespErrorPart = Part<'Error', { reason: string }>;
|
||||
export type Resp =
|
||||
| RespErrorPart
|
||||
| Part<'UserInfo', SucceedUserInfoResponse>
|
||||
// RoomList
|
||||
| Part<'RoomList', { rooms: Room[] }>
|
||||
// CreateRoom
|
||||
| Part<'RoomCreated', { roomId: RoomId }>
|
||||
// PlayerJoin
|
||||
| Part<'PlayerSideAllocation', { isWhite: boolean }>;
|
||||
export type RespOf<T extends Resp['name']> = Extract<Resp, { name: T }>;
|
||||
export type RespHandlerMap = {
|
||||
[P in Resp['name']]?: (payload: RespOf<P>['payload']) => void;
|
||||
};
|
||||
export function useGobangSocket(respHandlers: RespHandlerMap) {
|
||||
const userStore = useUserStore();
|
||||
return useWebSocket<string>(`ws://wzpmc.cn:18080/chess/${userStore.token}`, {
|
||||
onMessage(_, { data }) {
|
||||
const resp: Resp = JSON.parse(data);
|
||||
console.log(resp);
|
||||
respHandlers[resp.name]?.(resp.payload as any);
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import CreateGobangRoomDialog from '@/components/gobang/CreateGobangRoomDialog.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());
|
||||
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);
|
||||
loading.value = false;
|
||||
},
|
||||
RoomList(payload) {
|
||||
rooms.value = payload.rooms.map(toRoomRender);
|
||||
console.log(toRaw(rooms.value));
|
||||
loading.value = false;
|
||||
},
|
||||
RoomCreated(payload) {
|
||||
play(payload.roomId);
|
||||
},
|
||||
});
|
||||
function refresh() {
|
||||
loading.value = true;
|
||||
loadingText.value = '加载中';
|
||||
send(JSON.stringify({ name: 'RoomList' } satisfies RequestOf<'RoomList'>));
|
||||
}
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
function getPlayerCount(row: RoomRender) {
|
||||
switch (row.state) {
|
||||
case RoomState.CREATED:
|
||||
return '0/2';
|
||||
case RoomState.WAITING:
|
||||
return '1/2';
|
||||
case RoomState.GAMING:
|
||||
return '2/2';
|
||||
}
|
||||
}
|
||||
function onJoinButtonClick(room: RoomRender) {
|
||||
play(room.id);
|
||||
}
|
||||
function play(roomId: RoomId) {
|
||||
router.push({
|
||||
name: 'GobangPlay',
|
||||
params: { id: roomId },
|
||||
});
|
||||
}
|
||||
function createRoom() {
|
||||
send(JSON.stringify({ name: 'CreateRoom' } satisfies RequestOf<'CreateRoom'>));
|
||||
}
|
||||
function resetRooms() {
|
||||
send(JSON.stringify({ name: 'ResetRoom' } satisfies RequestOf<'ResetRoom'>));
|
||||
}
|
||||
</script>
|
@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<el-main class="gobang-page__wrapper flex">
|
||||
<el-container class="flex-stretch">
|
||||
<el-header class="gobang-page-header flex align-center">
|
||||
<el-icon :size="30"><icon-cs-gobang /></el-icon>
|
||||
<div v-show="!smLess" class="gobang-page-header__title">五子棋</div>
|
||||
<div class="gobang-page-header__button-wrapper flex align-center">
|
||||
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
|
||||
<el-button type="success">创建房间</el-button>
|
||||
<el-button type="primary">单人游戏</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main
|
||||
v-loading="loading"
|
||||
class="gobang-page-main flex-column"
|
||||
:class="{ flex: !rooms.length, center: !rooms.length }"
|
||||
>
|
||||
<div class="gobang-page-main__no-rooms-title">没有房间</div>
|
||||
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<create-gobang-room-dialog />
|
||||
</el-main>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.gobang-page__wrapper {
|
||||
background-color: white;
|
||||
}
|
||||
.gobang-page-header__title {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.gobang-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-page-main {
|
||||
gap: 10px;
|
||||
&__no-rooms-title {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts" setup>
|
||||
import CreateGobangRoomDialog from '@/components/gobang/CreateGobangRoomDialog.vue';
|
||||
import type { SucceedUserInfoResponse } from '@/schemas';
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
const userStore = useUserStore();
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
/** 房间状态 */
|
||||
enum RoomState {
|
||||
CREATED,
|
||||
WAITING,
|
||||
GAMING,
|
||||
}
|
||||
interface Room {
|
||||
/** 房间UUID */
|
||||
id: string;
|
||||
state: RoomState;
|
||||
pieces: unknown;
|
||||
isWhiteAcceptRestart: boolean;
|
||||
isBlackAcceptRestart: boolean;
|
||||
canWhiteDown: boolean;
|
||||
}
|
||||
type GobangResp =
|
||||
| {
|
||||
name: 'UserInfo';
|
||||
payload: SucceedUserInfoResponse;
|
||||
}
|
||||
| {
|
||||
name: 'RoomList';
|
||||
payload: {
|
||||
rooms: Room[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
name: 'PlayerJoin';
|
||||
payload: {
|
||||
rooms: Room[];
|
||||
};
|
||||
};
|
||||
type RespOf<T extends GobangResp['name']> = Extract<GobangResp, { name: T }>;
|
||||
type RespHandlerMap = {
|
||||
[P in GobangResp['name']]?: (payload: RespOf<P>['payload']) => void;
|
||||
};
|
||||
const respHandlers: RespHandlerMap = {
|
||||
RoomList(payload) {
|
||||
rooms.value = payload.rooms;
|
||||
loading.value = false;
|
||||
},
|
||||
PlayerJoin: (payload) => {
|
||||
rooms.value = payload.rooms;
|
||||
},
|
||||
};
|
||||
const rooms = ref<Room[]>([]);
|
||||
const loading = ref(false);
|
||||
watch(loading, (loading) => {
|
||||
if (!loading) return;
|
||||
send(JSON.stringify({ name: 'RoomList' }));
|
||||
console.log(1);
|
||||
});
|
||||
const { send } = useWebSocket<string>(`ws://wzpmc.cn:18080/chess/${userStore.token}`, {
|
||||
onMessage(_, { data }) {
|
||||
const resp: GobangResp = JSON.parse(data);
|
||||
respHandlers[resp.name]?.(resp.payload as any);
|
||||
},
|
||||
});
|
||||
function refresh() {
|
||||
loading.value = true;
|
||||
}
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
23
src/views/GobangPlayPage.vue
Normal file
23
src/views/GobangPlayPage.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<el-main class="gobang-play-page">
|
||||
<canvas ref="chessBoard"></canvas>
|
||||
</el-main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { type RequestOf, type RoomId, useGobangSocket } from '@/views/GobangListPage.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const { send } = useGobangSocket({});
|
||||
const roomId = useRoute().params.id as RoomId;
|
||||
const chessBoard = ref<HTMLCanvasElement>();
|
||||
function playerJoin(roomId: RoomId) {
|
||||
send(
|
||||
JSON.stringify({
|
||||
name: 'PlayerJoin',
|
||||
payload: {
|
||||
roomId,
|
||||
},
|
||||
} satisfies RequestOf<'PlayerJoin'>),
|
||||
);
|
||||
}
|
||||
</script>
|
Loading…
x
Reference in New Issue
Block a user