feat(复盘): 实现关键动作时间线并增强失误分析

新增关键动作时间线记录和展示功能,完善失误分析模块:
1. 添加 ReviewActionTimelineItem 记录结构,用于记录关键动作事件
2. 在 ReviewSummaryResponse 中增加 actionTimeline 字段
3. 扩展 ReviewMistakeItem 结构,新增关联动作、推荐方向、压力来源等字段
4. 实现动作时间线构建和失误关联动作查找逻辑
5. 前端增加动作时间线和详细失误信息的展示组件
6. 更新文档说明当前复盘协议已支持真实对局数据

这些改动使复盘系统能够展示更详细的对局过程分析和更精准的失误定位,帮助玩家更好地理解对局关键节点和改进方向。
This commit is contained in:
hujun
2026-03-20 17:24:41 +08:00
parent faf87fe3d6
commit 27142804d6
9 changed files with 889 additions and 24 deletions

View File

@@ -51,6 +51,77 @@ function formatSeverity(severity: string) {
}
}
function formatReviewEventType(eventType: string) {
switch (eventType) {
case 'GAME_STARTED':
return '开局'
case 'GAME_PHASE_CHANGED':
return '阶段'
case 'LACK_SELECTED':
return '定缺'
case 'TILE_DISCARDED':
return '出牌'
case 'TILE_DRAWN':
return '摸牌'
case 'TURN_SWITCHED':
return '轮转'
case 'RESPONSE_WINDOW_OPENED':
return '响应开始'
case 'RESPONSE_WINDOW_CLOSED':
return '响应结束'
case 'PENG_DECLARED':
return '碰'
case 'GANG_DECLARED':
return '杠'
case 'HU_DECLARED':
return '胡'
case 'PASS_DECLARED':
return '过'
case 'SETTLEMENT_APPLIED':
return '结算'
default:
return eventType
}
}
function formatDirectionType(directionType: string) {
switch (directionType) {
case 'RISK_CONTROL':
return '风险控制'
case 'READY_HAND':
return '优先保叫'
case 'ABANDON_GANG':
return '放弃补杠'
case 'SPEED_UP':
return '提速成型'
case 'TAX_PROTECTION':
return '守住杠税'
case 'DANGER_TRACKING':
return '追踪危险张'
default:
return directionType
}
}
function formatPressureSourceType(sourceType: string) {
switch (sourceType) {
case 'ENDGAME':
return '残局压力'
case 'ROBBING_GANG':
return '抢杠风险'
case 'MULTI_MELD':
return '连续副露'
case 'SINGLE_MELD':
return '已有副露'
case 'SPEED_LOSS':
return '速度落后'
case 'TABLE_TENSION':
return '场面压强'
default:
return sourceType
}
}
// 当前组件已经接入真实复盘协议,
// 但仍允许“暂无个人结算”的空态,避免首版服务在对局前期没有样本时出现空白页。
</script>
@@ -84,6 +155,22 @@ function formatSeverity(severity: string) {
<p class="message-copy">{{ props.review.conclusion }}</p>
</article>
<article class="placeholder-card">
<div class="section-title">
<strong>关键动作回放</strong>
<span class="mini-pill">{{ props.review.actionTimeline.length }} </span>
</div>
<div class="seat-list">
<article v-for="item in props.review.actionTimeline" :key="`${item.stepNo}-${item.title}`" class="seat-card seat-card-wide">
<div class="seat-top">
<strong>#{{ item.stepNo }} · {{ item.title }}</strong>
<span class="mini-pill">{{ formatReviewEventType(item.eventType) }}</span>
</div>
<span class="message-copy">{{ item.summary }}</span>
</article>
</div>
</article>
<article class="placeholder-card">
<div class="section-title">
<strong>关键结算</strong>
@@ -114,7 +201,26 @@ function formatSeverity(severity: string) {
<strong>{{ item.title }}</strong>
<span class="mini-pill">{{ formatSeverity(item.severity) }}</span>
</div>
<div v-if="item.relatedStepNo !== null" class="mini-tags">
<span class="mini-tag">
关联动作 #{{ item.relatedStepNo }} · {{ item.relatedActionTitle ?? formatReviewEventType(item.relatedEventType ?? '') }}
</span>
<span class="mini-tag">
建议方向 · {{ item.recommendedDirectionLabel || formatDirectionType(item.recommendedDirectionType) }}
</span>
<span class="mini-tag">
压力来源 · {{ item.pressureSourceLabel || formatPressureSourceType(item.pressureSourceType) }}
</span>
</div>
<div v-if="item.contextSignals.length > 0" class="mini-tags">
<span v-for="signal in item.contextSignals" :key="`${item.title}-${signal}`" class="mini-tag">
{{ signal }}
</span>
</div>
<span class="message-copy">来源细节{{ item.pressureSourceDetail }}</span>
<span class="message-copy">压力摘要{{ item.pressureSummary }}</span>
<span class="message-copy">{{ item.issue }}</span>
<span class="message-copy">建议动作{{ item.recommendedAction }}</span>
<span class="message-copy">{{ item.suggestion }}</span>
</article>
</div>

View File

@@ -7,11 +7,29 @@ export type ReviewSettlementItem = {
fanLabels: string[]
}
export type ReviewActionTimelineItem = {
stepNo: number
eventType: string
title: string
summary: string
}
export type ReviewMistakeItem = {
severity: string
title: string
issue: string
suggestion: string
recommendedDirectionType: string
recommendedDirectionLabel: string
recommendedAction: string
pressureSourceType: string
pressureSourceLabel: string
pressureSourceDetail: string
pressureSummary: string
contextSignals: string[]
relatedStepNo: number | null
relatedEventType: string | null
relatedActionTitle: string | null
}
export type ReviewTrainingFocusItem = {
@@ -28,6 +46,7 @@ export type ReviewSummaryResponse = {
finalScore: number
resultLabel: string
conclusion: string
actionTimeline: ReviewActionTimelineItem[]
settlementTimeline: ReviewSettlementItem[]
mistakeInsights: ReviewMistakeItem[]
trainingFocuses: ReviewTrainingFocusItem[]