From bb77439200a9b6c5eb85a0a13a83d0ac5fcb602c Mon Sep 17 00:00:00 2001 From: Litrix Date: Mon, 16 Dec 2024 20:19:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E5=AE=8C=E6=88=90=E4=BA=94?= =?UTF-8?q?=E5=AD=90=E6=A3=8BWS=E4=BA=8B=E4=BB=B6=E5=88=86=E9=85=8D?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/gobang-header.scss | 10 -- src/components/gobang/GobangHeader.vue | 1 + src/utils/index.ts | 16 +-- src/views/GobangListPage.vue | 135 +++++++++++++++---------- src/views/GobangPlayPage.vue | 36 ++++--- 5 files changed, 118 insertions(+), 80 deletions(-) delete mode 100644 src/assets/gobang-header.scss diff --git a/src/assets/gobang-header.scss b/src/assets/gobang-header.scss deleted file mode 100644 index e644e8b..0000000 --- a/src/assets/gobang-header.scss +++ /dev/null @@ -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; -} diff --git a/src/components/gobang/GobangHeader.vue b/src/components/gobang/GobangHeader.vue index 8f551e5..a9b9255 100644 --- a/src/components/gobang/GobangHeader.vue +++ b/src/components/gobang/GobangHeader.vue @@ -20,6 +20,7 @@ z-index: 100; user-select: none; &__left { + z-index: 1; gap: 10px; } } diff --git a/src/utils/index.ts b/src/utils/index.ts index b92ff07..524b334 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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((resolve) => setTimeout(resolve, delay)); -} +export const timeout = (delay: number = 0) => + new Promise((resolve) => setTimeout(resolve, delay)); export function useRefresh() { const key = ref(0); @@ -121,9 +120,8 @@ export class AsyncQueue { } } -export function arrayIncludes(array: T[], value: R): value is T & R { - return array.includes(value as any); -} +export const arrayIncludes = (array: T[], value: R): value is T & R => + array.includes(value as any); export function waitRef(ref: Ref): Promise; export function waitRef(ref: Ref, ...expect: R[]): Promise; @@ -141,3 +139,9 @@ export function waitRef(ref: Ref, ...expect: R[]) { }); } export type MaybePromise = T | Promise; +export type ValueOf = T[keyof T]; +/** + * 检测key是否为obj的键,并收紧key的类型. + */ +export const isKeyOf = (obj: T, key: keyof any): key is keyof typeof obj => + key in obj; diff --git a/src/views/GobangListPage.vue b/src/views/GobangListPage.vue index 98a2a40..7f1ef72 100644 --- a/src/views/GobangListPage.vue +++ b/src/views/GobangListPage.vue @@ -4,7 +4,9 @@
刷新 - 创建房间 + + 创建房间 + 单人游戏
@@ -94,81 +96,109 @@ export type Request = | SimplePart<'CreateRoom'> | PayloadPart<'PlayerJoin', { roomId: RoomId }> | SimplePart<'ResetRoom'>; -export type RequestOf = Extract; -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 = {}; +} as const satisfies Partial>; +type RelatedRespNames = keyof typeof relations; +type RelatedRequestNames = ValueOf; +const reversedRelations = {} as Record; for (const [k, v] of Object.entries(relations)) { (reversedRelations[v] ??= []).push(k); } -export interface RelatedPart - extends PayloadPart { - 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 = Extract; export type RespOf = Extract; -interface UseGobangSocketOptions { +export interface UseGobangSocketOptions { succeed: { [P in Resp['name']]?: (payload: RespOf

['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>(); - const { send: _send } = useWebSocket( - `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]>(); + let firstConnected = true; + const ws = useWebSocket(`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, }; } @@ -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'; diff --git a/src/views/GobangPlayPage.vue b/src/views/GobangPlayPage.vue index 3ccfb4d..1a6371b 100644 --- a/src/views/GobangPlayPage.vue +++ b/src/views/GobangPlayPage.vue @@ -89,9 +89,10 @@