From d954063928bc6f41623ca64966e615df56af786c Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Tue, 10 Mar 2026 20:03:32 +1100 Subject: [PATCH 1/4] fixed the verdict name display bug --- CLAUDE.md | 15 +++++++++------ miniprogram/packageB/pages/chat-room/index.ts | 1 - miniprogram/packageB/pages/verdict/index.ts | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 257af17..d622b03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,11 +99,11 @@ All services are classes exported as singleton instances: | `room-service` | `roomService` | Room creation via HTTP API | | `room-websocket-service` | `roomWebSocketService` | Room join/leave via WebSocket | | `drum-service` | `drumService` | Drum game message sending/receiving | -| `chat-service` | `chatService` | Chat message handling | | `asr-service` | `asrService` | ASR text sync via WebSocket (throttled) | | `sts-service` | `stsService` | Tencent Cloud STS token fetching | | `verdict-service` | `verdictService` | Verdict result (WS listen + HTTP fallback + format mapping + caching) | -| `post-game-service` | `postGameService` | Post-game interactions (effects, leave together) | +| `post-game-service` | `postGameService` | Post-game interactions (execute punishment / beg for mercy) | +| `nickname-service` | `nicknameService` | User identity: nickname + userId storage, retrieval, validation | ### Dual Type System for WebSocket Messages @@ -112,6 +112,7 @@ The frontend uses **two separate type hierarchies** for WebSocket messages: 1. **General messages** (`types/websocket-common.ts`): `EWSMessageType` + `IWSMessage` — for `JOIN_ROOM`, `CHAT_SEND`, `ASR_TEXT_PUSH`, `SPEECH_TURN_END`, `VERDICT_RESULT`, etc. 2. **Drum messages** (`types/drum-websocket.ts`): `EDrumMessageType` + `IDrumMessage` — for `DRUM_READY`, `DRUM_START`, `DRUM_TAP`, `DRUM_FINISH`, `DRUM_RESULT` 3. **Verdict messages** (`types/verdict-ws.ts`): Verdict-specific payload types — `IBackendVerdictResult`, `IVerdictResultPayload`, `IVerdictFailedPayload`, etc. +4. **Emoji messages** (`types/emoji-websocket.ts`): `IEmojiReceiveData` — for `EMOJI_RECEIVE` server→client payload The backend uses a single unified `EWSMessageType` enum for all message types. @@ -236,10 +237,11 @@ Husky + lint-staged runs ESLint + Prettier on `.ts`, `.js`, `.json`, `.md`. Comm | `DRUM_TAP` | Record drum tap during game | | `CHAT_SEND` | Send a chat message | | `ASR_TEXT_PUSH` | Push ASR transcription text (throttled partials + immediate finals) | +| `EMOJI_SEND` | Send emoji reaction to opponent during chat | | `SPEECH_TURN_END` | Notify server that player's speech turn is done | | `VERDICT_RETRY` | Request retry after verdict generation failure | -| `POST_GAME_EFFECT` | Send post-game effect (execute punishment / beg for mercy) | -| `LEAVE_TOGETHER` | Request mutual leave from verdict page | +| `POST_GAME_ACTION` | Send post-game action (execute_punishment / beg_for_mercy) | +| `LEAVE_ROOM` | Request to leave the room from verdict page | **Server → Client**: @@ -253,12 +255,13 @@ Husky + lint-staged runs ESLint + Prettier on `.ts`, `.js`, `.json`, `.md`. Comm | `DRUM_RESULT` | Final game results (scores + winner) | | `CHAT_RECEIVE` | Receive chat message (broadcast) | | `ASR_TEXT` | ASR transcription result (broadcast to other participant) | +| `EMOJI_RECEIVE` | Emoji reaction from opponent during chat | | `SPEECH_TURN_SWITCH` | First speaker done, notify turn switch | | `CHAT_COMPLETE` | Both speakers done, triggers verdict generation | | `VERDICT_RESULT` | AI verdict result push (success) | | `VERDICT_FAILED` | AI verdict generation failed (with canRetry flag) | -| `POST_GAME_EFFECT` | Post-game effect broadcast (stamp/emoji) | -| `LEAVE_TOGETHER_ACK` | Acknowledge mutual leave (with `allReady` flag) | +| `POST_GAME_EFFECT` | Post-game effect broadcast from opponent (stamp/emoji) | +| `LEAVE_ROOM_ACK` | Acknowledge leave room request | | `ERROR` | Error notification | ### Room Flow diff --git a/miniprogram/packageB/pages/chat-room/index.ts b/miniprogram/packageB/pages/chat-room/index.ts index b8bf9cb..20c1c28 100644 --- a/miniprogram/packageB/pages/chat-room/index.ts +++ b/miniprogram/packageB/pages/chat-room/index.ts @@ -215,7 +215,6 @@ Page({ const opponentName: string = options.opponentName ? decodeURIComponent(options.opponentName) : DEFAULT_OPPONENT_NAME; - // 校验 roomCode if (!roomCode) { void wx.showToast({ title: '房间号无效', icon: 'error' }); diff --git a/miniprogram/packageB/pages/verdict/index.ts b/miniprogram/packageB/pages/verdict/index.ts index 97949d8..81d2e1f 100644 --- a/miniprogram/packageB/pages/verdict/index.ts +++ b/miniprogram/packageB/pages/verdict/index.ts @@ -225,7 +225,7 @@ Page({ } } - // Read participant nicknames from globalData (populated in waiting-room) + // Read participant nicknames from globalData const app = getApp(); const hostNickName: string = app.globalData.participants?.hostNickName || '玩家1'; From 50e479c0a8d19ac705524fbeb5b468f8f89061ca Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Wed, 11 Mar 2026 16:22:17 +1100 Subject: [PATCH 2/4] add zhuan fa button --- CLAUDE.md | 10 +++++++- miniprogram/components/styled-button/index.ts | 4 +++ .../components/styled-button/index.wxml | 1 + .../packageA/pages/waiting-room/index.ts | 25 +++++++++++++++++++ .../packageA/pages/waiting-room/index.wxml | 9 ++++++- .../packageA/pages/waiting-room/index.wxss | 23 +++++++++-------- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d622b03..158dfc5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Two-player real-time interactive WeChat Mini Program ("申冤" app) with a Node.js backend. Users create/join rooms, compete in a drum-tapping game to decide speaking order, then take turns voicing grievances with real-time ASR transcription. -**User Flow**: Welcome → Waiting Room → Drum Room (10s tap competition) → Chat Room (turn-based voice chat with ASR) → Verdict Waiting (AI analysis loading) → Verdict +**User Flow**: Welcome (nickname modal on first visit if no name set) → Waiting Room → Drum Room (10s tap competition) → Chat Room (turn-based voice chat with ASR) → Verdict Waiting (AI analysis loading) → Verdict + +**Welcome Page Nickname Flow**: + +- Returning users (nickname stored) → navigate directly to waiting room +- First-time users → bottom-sheet modal ("堂下何人,报上名来!") with: + - `type="nickname"` input (WeChat nickname picker, max 12 chars) + - Confirm ("击鼓申冤!") — saves via `nicknameService.saveNickName()` + - Skip ("稍后再说") — uses default name "申冤人" without saving **Tech Stack**: diff --git a/miniprogram/components/styled-button/index.ts b/miniprogram/components/styled-button/index.ts index 8c46e03..ca9fbd9 100644 --- a/miniprogram/components/styled-button/index.ts +++ b/miniprogram/components/styled-button/index.ts @@ -31,6 +31,10 @@ Component({ type: String, value: '', }, + openType: { + type: String, + value: '', + }, }, methods: { diff --git a/miniprogram/components/styled-button/index.wxml b/miniprogram/components/styled-button/index.wxml index 9fec9a6..4c75413 100644 --- a/miniprogram/components/styled-button/index.wxml +++ b/miniprogram/components/styled-button/index.wxml @@ -5,6 +5,7 @@ class="styled-button styled-button--{{ color }} {{ disabled ? 'styled-button--disabled' : '' }}" hover-class="{{ disabled ? '' : 'styled-button--pressed' }}" bindtap="handleTap" + open-type="{{ openType }}" style="{{ buttonStyle }}" > diff --git a/miniprogram/packageA/pages/waiting-room/index.ts b/miniprogram/packageA/pages/waiting-room/index.ts index c7f78ee..2cabf19 100644 --- a/miniprogram/packageA/pages/waiting-room/index.ts +++ b/miniprogram/packageA/pages/waiting-room/index.ts @@ -133,6 +133,10 @@ Page({ this.initAnimations(); this.initWebSocket(); this.initUser(); + + wx.enableAlertBeforeUnload({ + message: '退出后房间将失效,确定要离开吗?', + }); }, onShow(): void { @@ -631,6 +635,27 @@ Page({ } }, + /** + * 触发转发好友菜单 + */ + async handleForwardRoom(): Promise { + await wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage'], + }); + }, + + /** + * 页面分享配置 + */ + onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent { + console.log('roomCode', this.data.roomCode); + return { + title: '快来公堂对簿!清汤大老爷等你很久了!', + path: `/pages/waiting-room/index?room_id=${this.data.roomCode}`, + }; + }, + /** * 阻止遮罩层事件穿透 */ diff --git a/miniprogram/packageA/pages/waiting-room/index.wxml b/miniprogram/packageA/pages/waiting-room/index.wxml index 3799081..f680f13 100644 --- a/miniprogram/packageA/pages/waiting-room/index.wxml +++ b/miniprogram/packageA/pages/waiting-room/index.wxml @@ -91,8 +91,15 @@ {{ currentWaitingText }} - + + Date: Wed, 11 Mar 2026 16:37:16 +1100 Subject: [PATCH 3/4] updates in chat room --- miniprogram/packageB/pages/chat-room/index.ts | 11 +++++++ .../packageB/pages/chat-room/index.wxml | 23 +++++++++---- .../packageB/pages/chat-room/index.wxss | 32 ++++++++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/miniprogram/packageB/pages/chat-room/index.ts b/miniprogram/packageB/pages/chat-room/index.ts index 20c1c28..c1c275e 100644 --- a/miniprogram/packageB/pages/chat-room/index.ts +++ b/miniprogram/packageB/pages/chat-room/index.ts @@ -973,6 +973,17 @@ Page({ this.stopRecording(); }, + /** + * 提前结束发言:跳过剩余倒计时,立即切换阶段 + */ + onEndEarlyTap(): void { + if (!this.data.canSpeak) { + return; + } + void wx.vibrateShort({ type: 'medium' }); + this.switchPhase(); + }, + /** * 开始录音和语音识别 * 使用 QCloudAIVoice 插件的 start 方法,同时启动录音和识别 diff --git a/miniprogram/packageB/pages/chat-room/index.wxml b/miniprogram/packageB/pages/chat-room/index.wxml index 40f3a12..d1e0920 100644 --- a/miniprogram/packageB/pages/chat-room/index.wxml +++ b/miniprogram/packageB/pages/chat-room/index.wxml @@ -100,13 +100,22 @@ - - 🎤 + + + 🎤 + + + + {{isRecording ? '松开结束' : (canSpeak ? '按住说话' : '等待发言')}} diff --git a/miniprogram/packageB/pages/chat-room/index.wxss b/miniprogram/packageB/pages/chat-room/index.wxss index e2a8f23..351717c 100644 --- a/miniprogram/packageB/pages/chat-room/index.wxss +++ b/miniprogram/packageB/pages/chat-room/index.wxss @@ -8,7 +8,7 @@ .chat-room { display: flex; flex-direction: column; - min-height: 100vh; + height: 100vh; background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%); position: relative; overflow: hidden; @@ -241,6 +241,7 @@ .chat-room__dialog { flex: 1; + min-height: 0; width: 100%; padding: 0 32rpx; box-sizing: border-box; @@ -332,6 +333,14 @@ padding-bottom: 40rpx; } +.chat-room__mic-buttons { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 40rpx; +} + .chat-room__mic { width: 160rpx; height: 160rpx; @@ -375,6 +384,27 @@ color: rgba(255, 255, 255, 0.6); } +.chat-room__end-btn { + width: 160rpx; + height: 160rpx; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(180deg, #ef4444 0%, #dc2626 100%); + box-shadow: 0 8rpx 30rpx rgba(239, 68, 68, 0.4); +} + +.chat-room__end-btn:active { + transform: scale(0.95); +} + +.chat-room__end-btn-icon { + font-size: 56rpx; + color: #ffffff; + font-weight: bold; +} + /* ==================== Emoji Panel ==================== */ .chat-room__emoji-panel { From b4b622de96bf001c375b65d7207805015bef5424 Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Wed, 11 Mar 2026 21:42:33 +1100 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8DBug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/constants/config.ts | 2 ++ backend/src/controllers/ws-controller.ts | 20 +++++++++++++++++ miniprogram/packageA/pages/drum-room/index.ts | 11 ++++++++-- .../packageA/pages/waiting-room/index.ts | 22 +++++++++++++++++-- .../packageB/pages/chat-room/index.wxss | 15 +++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/backend/src/constants/config.ts b/backend/src/constants/config.ts index 3f6113c..33e58bb 100644 --- a/backend/src/constants/config.ts +++ b/backend/src/constants/config.ts @@ -28,6 +28,8 @@ export const DRUM_CONFIG = { COUNTDOWN_MS: 3000, /** Game duration (ms) */ GAME_DURATION_MS: 10000, + /** Max taps to win instantly */ + MAX_TAPS: 30, } as const; export const VERDICT_CONFIG = { diff --git a/backend/src/controllers/ws-controller.ts b/backend/src/controllers/ws-controller.ts index c5bde70..20e454a 100644 --- a/backend/src/controllers/ws-controller.ts +++ b/backend/src/controllers/ws-controller.ts @@ -261,6 +261,17 @@ export class WebSocketController { }, connectionId ); + + // Early finish: if a player reached MAX_TAPS, end the game immediately + const game = drumGameManager.getGame(result.roomId); + if ( + game && + game.phase === EGamePhase.Running && + (game.organizerScore >= DRUM_CONFIG.MAX_TAPS || + game.joinerScore >= DRUM_CONFIG.MAX_TAPS) + ) { + WebSocketController.finishDrumGame(result.roomId, game.endAtMs); + } } /** @@ -438,6 +449,15 @@ export class WebSocketController { * Finish drum game and broadcast result */ private static finishDrumGame(roomId: string, endAtMs: number): void { + // Guard: game may have already been finished early + if (!drumGameManager.getGame(roomId)) { + logger.log( + 'WSController', + `Game ${roomId} already finished, skipping` + ); + return; + } + // Broadcast DRUM_FINISH connectionManager.broadcastToRoom(roomId, { type: EWSMessageType.DrumFinish, diff --git a/miniprogram/packageA/pages/drum-room/index.ts b/miniprogram/packageA/pages/drum-room/index.ts index 1de45b4..3be625d 100644 --- a/miniprogram/packageA/pages/drum-room/index.ts +++ b/miniprogram/packageA/pages/drum-room/index.ts @@ -116,7 +116,8 @@ function getScoreKey(role: EPlayerRole): 'organizerScore' | 'joinerScore' { const RUNNING_DURATION_MS: number = 10000; const RESULT_DISPLAY_MS: number = 2000; const FLY_TEXT_DURATION_MS: number = 800; -const MAX_SCORE_FOR_PROGRESS: number = 100; +const MAX_TAPS: number = 30; +const MAX_SCORE_FOR_PROGRESS: number = MAX_TAPS; Page({ data: { @@ -403,8 +404,14 @@ Page({ const scoreKey: 'organizerScore' | 'joinerScore' = getScoreKey( this.data.selfRole ); - const newScore: number = this.data[scoreKey] + 1; + const currentScore: number = this.data[scoreKey]; + // Ignore taps beyond the cap + if (currentScore >= MAX_TAPS) { + return; + } + + const newScore: number = currentScore + 1; this._updateScore(this.data.selfRole, newScore); // Trigger feedback diff --git a/miniprogram/packageA/pages/waiting-room/index.ts b/miniprogram/packageA/pages/waiting-room/index.ts index 2cabf19..fbb2a82 100644 --- a/miniprogram/packageA/pages/waiting-room/index.ts +++ b/miniprogram/packageA/pages/waiting-room/index.ts @@ -129,7 +129,7 @@ Page({ isCreatingRoom: false, isJoiningRoom: false, - onLoad(): void { + onLoad(options: Record): void { this.initAnimations(); this.initWebSocket(); this.initUser(); @@ -137,6 +137,24 @@ Page({ wx.enableAlertBeforeUnload({ message: '退出后房间将失效,确定要离开吗?', }); + + const roomId = options['room_id']; + if (roomId) { + const value = roomId.replace(/\D/g, '').slice(0, 6); + const display: string[] = []; + for (let i = 0; i < 6; i++) { + display.push(value[i] || ''); + } + this.setData({ + showJoinModal: true, + roomCodeInput: value, + roomCodeDisplay: display, + inputFocus: false, + errorType: null, + errorMessage: '', + isJoinButtonDisabled: value.length !== 6, + }); + } }, onShow(): void { @@ -652,7 +670,7 @@ Page({ console.log('roomCode', this.data.roomCode); return { title: '快来公堂对簿!清汤大老爷等你很久了!', - path: `/pages/waiting-room/index?room_id=${this.data.roomCode}`, + path: `/packageA/pages/waiting-room/index?room_id=${this.data.roomCode}`, }; }, diff --git a/miniprogram/packageB/pages/chat-room/index.wxss b/miniprogram/packageB/pages/chat-room/index.wxss index 351717c..7c17c82 100644 --- a/miniprogram/packageB/pages/chat-room/index.wxss +++ b/miniprogram/packageB/pages/chat-room/index.wxss @@ -245,6 +245,21 @@ width: 100%; padding: 0 32rpx; box-sizing: border-box; + overflow-y: scroll; +} + +.chat-room__dialog::-webkit-scrollbar { + width: 6rpx; +} + +.chat-room__dialog::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3rpx; +} + +.chat-room__dialog::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.25); + border-radius: 3rpx; } .chat-room__dialog-inner {