新增复盘服务相关DTO、Controller和Service 实现复盘页面容器组件ReviewPageContainer 更新前端页面架构文档与开发计划 移除DemoGameController中的演示复盘接口 补充复盘服务单元测试
164 lines
5.6 KiB
Vue
164 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import GameActionDock from './GameActionDock.vue'
|
|
import GameMessageStack from './GameMessageStack.vue'
|
|
import PublicEventTimeline from './PublicEventTimeline.vue'
|
|
import PublicSeatBoard from './PublicSeatBoard.vue'
|
|
import SelfHandPanel from './SelfHandPanel.vue'
|
|
import ViewSwitchPanel from './ViewSwitchPanel.vue'
|
|
import type {
|
|
DiagnosticItem,
|
|
GameStateResponse,
|
|
PrivateActionCandidate,
|
|
PrivateActionMessage,
|
|
PrivateTeachingMessage,
|
|
PublicGameMessage,
|
|
ScoreChangeCardView,
|
|
SettlementCardView,
|
|
ViewUserOption
|
|
} from '../types/game'
|
|
import { phaseLabelMap } from '../utils/gameUi'
|
|
|
|
const props = defineProps<{
|
|
game: GameStateResponse | null
|
|
currentUserId: string
|
|
currentViewLabel: string
|
|
viewUserOptions: ViewUserOption[]
|
|
lackSuit: string
|
|
canSelectLack: boolean
|
|
canDiscard: boolean
|
|
recommendedDiscardTile: string | null
|
|
privateTeaching: PrivateTeachingMessage | null
|
|
privateTeachingHint: string
|
|
privateTeachingCandidates: { tile: string; score: number; reasonTags: string[] }[]
|
|
privateAction: PrivateActionMessage | null
|
|
privateActionSummary: string
|
|
actionPanelHint: string
|
|
turnActionCandidates: PrivateActionCandidate[]
|
|
responseActionCandidates: PrivateActionCandidate[]
|
|
responseContextSummary: string
|
|
actionDiagnosticItems: DiagnosticItem[]
|
|
publicSeats: GameStateResponse['seats']
|
|
publicEvents: PublicGameMessage[]
|
|
latestSettlementCard: SettlementCardView | null
|
|
latestSettlementScoreChanges: ScoreChangeCardView[]
|
|
wsStatus: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:lackSuit': [value: string]
|
|
refreshGameState: []
|
|
selectLack: []
|
|
switchUserView: [userId: string]
|
|
discard: [tile: string]
|
|
submitCandidateAction: [actionType: string, tile: string | null]
|
|
}>()
|
|
|
|
function updateLackSuit(event: Event) {
|
|
emit('update:lackSuit', (event.target as HTMLSelectElement).value)
|
|
}
|
|
|
|
function handleSwitchUserView(userId: string) {
|
|
emit('switchUserView', userId)
|
|
}
|
|
|
|
function handleSubmitCandidateAction(actionType: string, tile: string | null) {
|
|
emit('submitCandidateAction', actionType, tile)
|
|
}
|
|
|
|
// 这是“对局工作区”级别的容器组件:统一组合手牌、动作、私有消息、公共桌面与事件时间线,
|
|
// 让 App 容器只保留接口、WebSocket 和状态清理逻辑。
|
|
</script>
|
|
|
|
<template>
|
|
<section v-if="props.game" class="play-grid">
|
|
<article class="panel game-panel">
|
|
<div class="panel-head">
|
|
<div>
|
|
<p class="eyebrow">步骤 3</p>
|
|
<h2>对局控制台</h2>
|
|
</div>
|
|
<span class="status-pill">{{ phaseLabelMap[props.game.phase] ?? props.game.phase }}</span>
|
|
</div>
|
|
|
|
<div class="room-meta">
|
|
<div>
|
|
<span class="meta-label">对局 ID</span>
|
|
<strong>{{ props.game.gameId }}</strong>
|
|
</div>
|
|
<div>
|
|
<span class="meta-label">当前视角</span>
|
|
<strong>{{ props.currentViewLabel }}</strong>
|
|
</div>
|
|
<div>
|
|
<span class="meta-label">剩余牌墙</span>
|
|
<strong>{{ props.game.remainingWallCount }}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<ViewSwitchPanel :options="props.viewUserOptions" :current-user-id="props.currentUserId" @switch="handleSwitchUserView" />
|
|
|
|
<div class="btn-row vertical-on-mobile">
|
|
<button class="secondary-btn" @click="emit('refreshGameState')">刷新对局</button>
|
|
<label class="field compact-field">
|
|
<span>定缺</span>
|
|
<select :value="props.lackSuit" @change="updateLackSuit">
|
|
<option value="WAN">万</option>
|
|
<option value="TONG">筒</option>
|
|
<option value="TIAO">条</option>
|
|
</select>
|
|
</label>
|
|
<button class="primary-btn" :disabled="!props.canSelectLack" @click="emit('selectLack')">提交定缺</button>
|
|
</div>
|
|
|
|
<SelfHandPanel
|
|
:self-seat="props.game.selfSeat"
|
|
:current-seat-no="props.game.currentSeatNo"
|
|
:can-discard="props.canDiscard"
|
|
:recommended-discard-tile="props.recommendedDiscardTile"
|
|
:teaching-explanation="props.privateTeaching?.explanation ?? null"
|
|
@discard="emit('discard', $event)"
|
|
/>
|
|
|
|
<GameActionDock
|
|
:private-action="props.privateAction"
|
|
:private-action-summary="props.privateActionSummary"
|
|
:action-panel-hint="props.actionPanelHint"
|
|
:turn-action-candidates="props.turnActionCandidates"
|
|
:response-action-candidates="props.responseActionCandidates"
|
|
:recommended-discard-tile="props.recommendedDiscardTile"
|
|
:response-context-summary="props.responseContextSummary"
|
|
@submit="handleSubmitCandidateAction"
|
|
/>
|
|
</article>
|
|
|
|
<article class="panel board-panel">
|
|
<div class="panel-head">
|
|
<div>
|
|
<p class="eyebrow">步骤 4</p>
|
|
<h2>消息与公共桌面</h2>
|
|
</div>
|
|
<span class="status-pill">{{ props.wsStatus }}</span>
|
|
</div>
|
|
|
|
<GameMessageStack
|
|
:private-action="props.privateAction"
|
|
:private-action-summary="props.privateActionSummary"
|
|
:action-diagnostic-items="props.actionDiagnosticItems"
|
|
:action-panel-hint="props.actionPanelHint"
|
|
:private-teaching="props.privateTeaching"
|
|
:recommended-discard-tile="props.recommendedDiscardTile"
|
|
:teaching-hint="props.privateTeachingHint"
|
|
:teaching-candidates="props.privateTeachingCandidates"
|
|
/>
|
|
|
|
<PublicSeatBoard :seats="props.publicSeats" />
|
|
|
|
<PublicEventTimeline
|
|
:public-events="props.publicEvents"
|
|
:latest-settlement-card="props.latestSettlementCard"
|
|
:latest-settlement-score-changes="props.latestSettlementScoreChanges"
|
|
/>
|
|
</article>
|
|
</section>
|
|
</template>
|