✨ feat: 完成五子棋WS事件分配器
This commit is contained in:
parent
4ae374a8b6
commit
bb77439200
@ -1,10 +0,0 @@
|
||||
.gobang-header {
|
||||
--el-header-height: var(--header-height);
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
}
|
@ -20,6 +20,7 @@
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
&__left {
|
||||
z-index: 1;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ import { reactive, readonly, type Ref, ref, watch } from 'vue';
|
||||
export * from './2d-array';
|
||||
export * from './api';
|
||||
|
||||
export function timeout(delay: number = 0) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
export const timeout = (delay: number = 0) =>
|
||||
new Promise<void>((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
export function useRefresh() {
|
||||
const key = ref(0);
|
||||
@ -121,9 +120,8 @@ export class AsyncQueue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export function arrayIncludes<T, R>(array: T[], value: R): value is T & R {
|
||||
return array.includes(value as any);
|
||||
}
|
||||
export const arrayIncludes = <T, R>(array: T[], value: R): value is T & R =>
|
||||
array.includes(value as any);
|
||||
|
||||
export function waitRef<T>(ref: Ref<T>): Promise<void>;
|
||||
export function waitRef<T, const R extends T>(ref: Ref<T>, ...expect: R[]): Promise<R>;
|
||||
@ -141,3 +139,9 @@ export function waitRef<T, const R extends T>(ref: Ref<T>, ...expect: R[]) {
|
||||
});
|
||||
}
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
export type ValueOf<T extends object> = T[keyof T];
|
||||
/**
|
||||
* 检测key是否为obj的键,并收紧key的类型.
|
||||
*/
|
||||
export const isKeyOf = <T extends object>(obj: T, key: keyof any): key is keyof typeof obj =>
|
||||
key in obj;
|
||||
|
@ -4,7 +4,9 @@
|
||||
<gobang-header class="justify-between">
|
||||
<div class="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="success" :disabled="loading" @click="showDialog = true">
|
||||
创建房间
|
||||
</el-button>
|
||||
<el-button type="primary" disabled>单人游戏</el-button>
|
||||
</div>
|
||||
</gobang-header>
|
||||
@ -94,81 +96,109 @@ export type Request =
|
||||
| SimplePart<'CreateRoom'>
|
||||
| PayloadPart<'PlayerJoin', { roomId: RoomId }>
|
||||
| SimplePart<'ResetRoom'>;
|
||||
export type RequestOf<T extends Request['name']> = Extract<Request, { name: T }>;
|
||||
export const relations = {
|
||||
export type Resp =
|
||||
| PayloadPart<'UserInfo', SucceedUserInfoResponse>
|
||||
| PayloadPart<'RoomList', { rooms: Room[] }>
|
||||
| PayloadPart<'RoomCreated', { roomId: RoomId }>
|
||||
| PayloadPart<'PlayerSideAllocation', { isWhite: boolean }>;
|
||||
const relations = {
|
||||
RoomList: 'RoomList',
|
||||
RoomCreated: 'CreateRoom',
|
||||
PlayerSideAllocation: 'PlayerJoin',
|
||||
} as const;
|
||||
const reversedRelations: Record<string, string[]> = {};
|
||||
} as const satisfies Partial<Record<Resp['name'], Request['name']>>;
|
||||
type RelatedRespNames = keyof typeof relations;
|
||||
type RelatedRequestNames = ValueOf<typeof relations>;
|
||||
const reversedRelations = {} as Record<RelatedRequestNames, string[]>;
|
||||
for (const [k, v] of Object.entries(relations)) {
|
||||
(reversedRelations[v] ??= []).push(k);
|
||||
}
|
||||
export interface RelatedPart<N extends keyof typeof relations, P extends Payload>
|
||||
extends PayloadPart<N, P> {
|
||||
originalPacketName?: (typeof relations)[N];
|
||||
}
|
||||
export interface ErrorResp extends PayloadPart<'Error', { reason: string }> {
|
||||
originalPacketName: Request['name'];
|
||||
}
|
||||
export type Resp =
|
||||
| PayloadPart<'UserInfo', SucceedUserInfoResponse>
|
||||
| RelatedPart<'RoomList', { rooms: Room[] }>
|
||||
| RelatedPart<'RoomCreated', { roomId: RoomId }>
|
||||
| RelatedPart<'PlayerSideAllocation', { isWhite: boolean }>;
|
||||
export type RequestOf<T extends Request['name']> = Extract<Request, { name: T }>;
|
||||
export type RespOf<T extends Resp['name']> = Extract<Resp, { name: T }>;
|
||||
interface UseGobangSocketOptions {
|
||||
export interface UseGobangSocketOptions {
|
||||
succeed: {
|
||||
[P in Resp['name']]?: (payload: RespOf<P>['payload']) => void;
|
||||
};
|
||||
error?: {
|
||||
[P in Request['name']]?: (reason: string) => boolean | void;
|
||||
};
|
||||
finally?: {
|
||||
[P in Request['name']]?: () => void;
|
||||
// 只有拥有依赖关系的请求才可以有finally
|
||||
[P in RelatedRequestNames]?: (error: boolean) => void;
|
||||
};
|
||||
}
|
||||
export function useGobangSocket(options: UseGobangSocketOptions) {
|
||||
const userStore = useUserStore();
|
||||
const map = new Map<string, Set<string>>();
|
||||
const { send: _send } = useWebSocket<string>(
|
||||
`ws://172.16.114.84:58080/chess/${userStore.token}`,
|
||||
{
|
||||
onMessage(_, { data }) {
|
||||
const resp: Resp | ErrorResp = JSON.parse(data);
|
||||
const { name } = resp;
|
||||
let reqName: Request['name'] | undefined;
|
||||
try {
|
||||
if (name === 'Error') {
|
||||
const { originalPacketName } = resp;
|
||||
ElMessage.error(resp.payload.reason);
|
||||
reqName = originalPacketName;
|
||||
map.delete(originalPacketName);
|
||||
return;
|
||||
const relationMap = new Map<RelatedRequestNames, [Request, Set<string>]>();
|
||||
let firstConnected = true;
|
||||
const ws = useWebSocket<string>(`wss://wzpmc.cn:18080/chess/${userStore.token}`, {
|
||||
onConnected() {
|
||||
// 新连接无故断开的临时解决方案
|
||||
if (firstConnected) {
|
||||
firstConnected = false;
|
||||
return;
|
||||
}
|
||||
for (const [req] of relationMap.values()) {
|
||||
send(req);
|
||||
}
|
||||
},
|
||||
onMessage(_, { data }) {
|
||||
const resp: Resp | ErrorResp = JSON.parse(data);
|
||||
console.log(resp);
|
||||
const { name } = resp;
|
||||
let reqName: Request['name'] | undefined;
|
||||
let error = false;
|
||||
try {
|
||||
if (name === 'Error') {
|
||||
const { originalPacketName } = resp;
|
||||
reqName = originalPacketName;
|
||||
error = true;
|
||||
if (isKeyOf(relations, originalPacketName)) {
|
||||
relationMap.delete(originalPacketName);
|
||||
}
|
||||
if ('originalPacketName' in resp && resp.originalPacketName) {
|
||||
const { originalPacketName } = resp;
|
||||
const set = map.get(originalPacketName);
|
||||
if (set) {
|
||||
set.delete(name);
|
||||
if (!set.size) {
|
||||
map.delete(name);
|
||||
reqName = originalPacketName;
|
||||
}
|
||||
const errorHandler = options.error?.[originalPacketName];
|
||||
if (errorHandler) {
|
||||
const res = errorHandler(resp.payload.reason);
|
||||
if (res) return;
|
||||
}
|
||||
ElMessage.error(resp.payload.reason);
|
||||
return;
|
||||
}
|
||||
if (isKeyOf(relations, name)) {
|
||||
const v = relationMap.get(relations[name]);
|
||||
if (v) {
|
||||
const [, set] = v;
|
||||
set.delete(name);
|
||||
if (!set.size) {
|
||||
relationMap.delete(relations[name]);
|
||||
reqName = relations[name];
|
||||
}
|
||||
}
|
||||
options.succeed[name]?.(resp.payload as any);
|
||||
} finally {
|
||||
if (reqName) {
|
||||
options.finally?.[reqName]?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
options.succeed[name]?.(resp.payload as any);
|
||||
} finally {
|
||||
if (reqName && isKeyOf(reversedRelations, reqName)) {
|
||||
options.finally?.[reqName]?.(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
autoReconnect: true,
|
||||
onDisconnected() {
|
||||
console.log('disconnected');
|
||||
},
|
||||
});
|
||||
const send = (data: Request) => {
|
||||
const { name } = data;
|
||||
if (isKeyOf(reversedRelations, name)) {
|
||||
relationMap.set(name, [data, new Set(reversedRelations[name])]);
|
||||
}
|
||||
console.log('send', data);
|
||||
ws.send(JSON.stringify(data));
|
||||
};
|
||||
return {
|
||||
send(data: Request) {
|
||||
const respNames = reversedRelations[data.name];
|
||||
map.set(data.name, respNames && new Set(respNames));
|
||||
_send(JSON.stringify(data));
|
||||
},
|
||||
send,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@ -178,6 +208,7 @@ import GobangHeader from '@/components/gobang/GobangHeader.vue';
|
||||
import router from '@/router';
|
||||
import type { SucceedUserInfoResponse } from '@/schemas';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { isKeyOf, type ValueOf } from '@/utils';
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { onMounted, ref, toRaw } from 'vue';
|
||||
|
@ -89,9 +89,10 @@
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import GobangHeader from '@/components/gobang/GobangHeader.vue';
|
||||
import router from '@/router';
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { create2DArray } from '@/utils';
|
||||
import { type RequestOf, type RoomId, useGobangSocket } from '@/views/GobangListPage.vue';
|
||||
import { useGobangSocket, type RoomId } from '@/views/GobangListPage.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onMounted, ref, watchEffect, type CSSProperties } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
@ -179,20 +180,31 @@ function getCellStyle(chess: Chess | undefined, x: number, y: number) {
|
||||
const state = ref<'firstLoading' | 'loading' | 'idle' | 'waiting'>('firstLoading');
|
||||
const isWhite = ref(false);
|
||||
const { send } = useGobangSocket({
|
||||
PlayerSideAllocation(p) {
|
||||
isWhite.value = p.isWhite;
|
||||
state.value = 'idle';
|
||||
succeed: {
|
||||
PlayerSideAllocation(p) {
|
||||
isWhite.value = p.isWhite;
|
||||
},
|
||||
},
|
||||
error: {
|
||||
PlayerJoin() {
|
||||
// router.back();
|
||||
},
|
||||
},
|
||||
finally: {
|
||||
PlayerJoin(error) {
|
||||
if (!error) {
|
||||
state.value = 'idle';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
function playerJoin(roomId: RoomId) {
|
||||
send(
|
||||
JSON.stringify({
|
||||
name: 'PlayerJoin',
|
||||
payload: {
|
||||
roomId,
|
||||
},
|
||||
} satisfies RequestOf<'PlayerJoin'>),
|
||||
);
|
||||
send({
|
||||
name: 'PlayerJoin',
|
||||
payload: {
|
||||
roomId,
|
||||
},
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
if (roomId) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user