Files
xzmaster/frontend/src/components/GameWorkspace.vue
hujun faf87fe3d6 feat: 添加局后复盘服务与页面容器组件
新增复盘服务相关DTO、Controller和Service
实现复盘页面容器组件ReviewPageContainer
更新前端页面架构文档与开发计划
移除DemoGameController中的演示复盘接口
补充复盘服务单元测试
2026-03-20 16:50:49 +08:00

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>