feat: 实现响应候选模型与私有动作消息结构化
新增响应候选领域模型和结构化私有动作消息,支持响应窗口和候选动作下发。主要变更包括: - 新增 ResponseActionOption、ResponseActionSeatCandidate 和 ResponseActionWindow 模型 - 扩展 PrivateActionMessage 支持响应候选上下文 - 实现 ResponseActionWindowBuilder 构建弃牌响应候选 - 拆分 GameMessagePublisher 支持回合动作和响应动作消息 - 更新前端原型页展示结构化候选动作 - 新增响应优先级规则文档 RESPONSE_RESOLUTION_RULES.md
This commit is contained in:
@@ -63,11 +63,22 @@ type PublicGameMessage = {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type PrivateActionCandidate = {
|
||||
actionType: string
|
||||
tile: string | null
|
||||
}
|
||||
|
||||
type PrivateActionMessage = {
|
||||
gameId: string
|
||||
userId: string
|
||||
actionScope: string
|
||||
availableActions: string[]
|
||||
currentSeatNo: number
|
||||
windowId: string | null
|
||||
triggerEventType: string | null
|
||||
sourceSeatNo: number | null
|
||||
triggerTile: string | null
|
||||
candidates: PrivateActionCandidate[]
|
||||
}
|
||||
|
||||
type PrivateTeachingMessage = {
|
||||
@@ -107,11 +118,24 @@ const phaseLabelMap: Record<string, string> = {
|
||||
LACK_SELECTION: '定缺阶段'
|
||||
}
|
||||
|
||||
const actionScopeLabelMap: Record<string, string> = {
|
||||
TURN: '当前回合动作',
|
||||
RESPONSE: '响应候选动作'
|
||||
}
|
||||
|
||||
const canSelectLack = computed(() => game.value?.phase === 'LACK_SELECTION')
|
||||
const canDiscard = computed(
|
||||
() => game.value?.phase === 'PLAYING' && game.value.selfSeat.playerId === currentUserId.value && game.value.currentSeatNo === game.value.selfSeat.seatNo
|
||||
)
|
||||
const publicSeats = computed(() => game.value?.seats ?? [])
|
||||
const privateActionCandidates = computed(() => privateAction.value?.candidates ?? [])
|
||||
const privateActionSummary = computed(() => {
|
||||
if (!privateAction.value) {
|
||||
return ''
|
||||
}
|
||||
const scopeLabel = actionScopeLabelMap[privateAction.value.actionScope] ?? privateAction.value.actionScope
|
||||
return `${scopeLabel}:${privateAction.value.availableActions.join(' / ')}`
|
||||
})
|
||||
|
||||
async function requestJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
@@ -516,7 +540,27 @@ function discard(tile: string) {
|
||||
<div class="message-stack">
|
||||
<div class="message-card">
|
||||
<span class="meta-label">私有动作消息</span>
|
||||
<strong v-if="privateAction">{{ privateAction.availableActions.join(' / ') }}</strong>
|
||||
<template v-if="privateAction">
|
||||
<strong>{{ privateActionSummary }}</strong>
|
||||
<div class="mini-tags action-meta-row">
|
||||
<span class="mini-tag">座位 {{ privateAction.currentSeatNo }}</span>
|
||||
<span class="mini-tag">{{ actionScopeLabelMap[privateAction.actionScope] ?? privateAction.actionScope }}</span>
|
||||
<span v-if="privateAction.triggerTile" class="mini-tag">目标牌 {{ privateAction.triggerTile }}</span>
|
||||
<span v-if="privateAction.sourceSeatNo !== null" class="mini-tag">来源座位 {{ privateAction.sourceSeatNo }}</span>
|
||||
</div>
|
||||
<div class="candidate-list" v-if="privateActionCandidates.length > 0">
|
||||
<span
|
||||
v-for="(candidate, index) in privateActionCandidates"
|
||||
:key="`${candidate.actionType}-${candidate.tile ?? 'none'}-${index}`"
|
||||
class="candidate-chip"
|
||||
>
|
||||
{{ candidate.actionType }}<template v-if="candidate.tile"> · {{ candidate.tile }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="privateAction.actionScope === 'RESPONSE'" class="message-copy">
|
||||
当前原型页已能识别响应候选消息,后续会继续补正式动作面板和真实响应流程。
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="empty-copy">尚未收到私有动作消息</span>
|
||||
</div>
|
||||
<div class="message-card">
|
||||
|
||||
Reference in New Issue
Block a user