feat(frontend): U8 适配前端类型支持流水线阶段事件

- types.ts: WsServerMessage 新增 phase_started/phase_completed/phase_failed 三个事件类型

- types.ts: ITeamPlanPhase 新增 task_description/depends_on/result 字段,parallel_type 和 milestone 改为可选

- chat.ts: handleWsMessage 新增 3 个 phase 事件 case 分支,调用 teamStore.updatePhaseStatus 更新阶段状态

- team.ts: 新增 updatePhaseStatus(phaseId, status, result?) 方法并导出

- ExpertTeamView.vue: 增强 phase 渲染展示 task_description 和 result,补充 --pending/--failed CSS 样式

- PlanVisualization.vue: 修复 parallel_type 可选后的类型检查错误
This commit is contained in:
chiguyong 2026-06-18 02:19:40 +08:00
parent 1e818b507d
commit a72bc012d5
5 changed files with 119 additions and 11 deletions

View File

@ -104,6 +104,9 @@ export type WsServerMessage =
| { type: 'expert_step'; data: { expert_id: string; expert_name: string; expert_color: string; content: string; step: number } } | { type: 'expert_step'; data: { expert_id: string; expert_name: string; expert_color: string; content: string; step: number } }
| { type: 'expert_result'; data: { expert_id: string; expert_name: string; expert_color: string; content: string } } | { type: 'expert_result'; data: { expert_id: string; expert_name: string; expert_color: string; content: string } }
| { type: 'plan_update'; data: { plan_phases: ITeamPlanPhase[] } } | { type: 'plan_update'; data: { plan_phases: ITeamPlanPhase[] } }
| { type: 'phase_started'; data: { phase_id: string; phase_name: string; assigned_expert: string; depends_on: string[] } }
| { type: 'phase_completed'; data: { phase_id: string; phase_name: string; result_summary: string } }
| { type: 'phase_failed'; data: { phase_id: string; phase_name: string; error: string } }
| { type: 'team_synthesis'; data: { content: string } } | { type: 'team_synthesis'; data: { content: string } }
| { type: 'team_dissolved'; data: { team_id: string } } | { type: 'team_dissolved'; data: { team_id: string } }
// Board Meeting 模式事件 // Board Meeting 模式事件
@ -130,9 +133,12 @@ export interface ITeamPlanPhase {
id: string id: string
name: string name: string
assigned_expert: string assigned_expert: string
task_description?: string
depends_on: string[]
status: 'pending' | 'in_progress' | 'completed' | 'failed' status: 'pending' | 'in_progress' | 'completed' | 'failed'
parallel_type: 'serial' | 'subtask_parallel' | 'competitive_parallel' result?: string
milestone: string parallel_type?: 'serial' | 'subtask_parallel' | 'competitive_parallel'
milestone?: string
} }
/** Expert team state */ /** Expert team state */

View File

@ -35,10 +35,18 @@
class="expert-team-view__phase" class="expert-team-view__phase"
:class="`expert-team-view__phase--${phase.status}`" :class="`expert-team-view__phase--${phase.status}`"
> >
<div class="expert-team-view__phase-header">
<span class="expert-team-view__phase-name">{{ phase.name }}</span> <span class="expert-team-view__phase-name">{{ phase.name }}</span>
<span class="expert-team-view__phase-expert">{{ phase.assigned_expert }}</span> <span class="expert-team-view__phase-expert">{{ phase.assigned_expert }}</span>
<a-tag :color="statusColor(phase.status)" size="small">{{ statusLabel(phase.status) }}</a-tag> <a-tag :color="statusColor(phase.status)" size="small">{{ statusLabel(phase.status) }}</a-tag>
</div> </div>
<div v-if="phase.task_description" class="expert-team-view__phase-desc">
{{ phase.task_description }}
</div>
<div v-if="phase.result" class="expert-team-view__phase-result">
{{ phase.result }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -152,11 +160,22 @@ function statusLabel(status: string): string {
} }
.expert-team-view__phase { .expert-team-view__phase {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-2) 0;
font-size: var(--font-xs);
border-bottom: 1px solid var(--border-primary);
}
.expert-team-view__phase:last-child {
border-bottom: none;
}
.expert-team-view__phase-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-1) 0;
font-size: var(--font-xs);
} }
.expert-team-view__phase-name { .expert-team-view__phase-name {
@ -167,11 +186,52 @@ function statusLabel(status: string): string {
color: var(--text-secondary); color: var(--text-secondary);
} }
.expert-team-view__phase--completed { .expert-team-view__phase-desc {
opacity: 0.7; color: var(--text-secondary);
font-size: var(--font-xs);
padding-left: var(--space-2);
border-left: 2px solid var(--border-primary);
white-space: pre-wrap;
word-break: break-word;
}
.expert-team-view__phase-result {
color: var(--text-tertiary);
font-size: var(--font-xs);
padding-left: var(--space-2);
border-left: 2px solid var(--color-success);
white-space: pre-wrap;
word-break: break-word;
max-height: 80px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.expert-team-view__phase--pending {
opacity: 0.6;
} }
.expert-team-view__phase--in_progress { .expert-team-view__phase--in_progress {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
background: var(--bg-tertiary);
margin: 0 calc(-1 * var(--space-3));
padding-left: var(--space-3);
padding-right: var(--space-3);
border-radius: var(--radius-sm);
}
.expert-team-view__phase--completed {
opacity: 0.7;
}
.expert-team-view__phase--failed {
color: var(--color-error);
}
.expert-team-view__phase--failed .expert-team-view__phase-result {
border-left-color: var(--color-error);
} }
</style> </style>

View File

@ -16,7 +16,7 @@
<span>执行者: {{ phase.assigned_expert }}</span> <span>执行者: {{ phase.assigned_expert }}</span>
<span v-if="phase.milestone"> | 里程碑: {{ phase.milestone }}</span> <span v-if="phase.milestone"> | 里程碑: {{ phase.milestone }}</span>
</div> </div>
<div v-if="phase.parallel_type !== 'serial'" class="plan-visualization__phase-type"> <div v-if="phase.parallel_type && phase.parallel_type !== 'serial'" class="plan-visualization__phase-type">
<a-tag size="small">{{ parallelLabel(phase.parallel_type) }}</a-tag> <a-tag size="small">{{ parallelLabel(phase.parallel_type) }}</a-tag>
</div> </div>
</div> </div>

View File

@ -661,6 +661,33 @@ export const useChatStore = defineStore('chat', () => {
break break
} }
case 'phase_started': {
const teamStore = _getTeamStore()
if (teamStore?.teamState) {
teamStore.updatePhaseStatus(payload.phase_id, 'in_progress')
streamingSteps.value.push(`阶段开始: ${payload.phase_name} (${payload.assigned_expert})`)
}
break
}
case 'phase_completed': {
const teamStore = _getTeamStore()
if (teamStore?.teamState) {
teamStore.updatePhaseStatus(payload.phase_id, 'completed', payload.result_summary)
streamingSteps.value.push(`阶段完成: ${payload.phase_name}`)
}
break
}
case 'phase_failed': {
const teamStore = _getTeamStore()
if (teamStore?.teamState) {
teamStore.updatePhaseStatus(payload.phase_id, 'failed', payload.error)
streamingSteps.value.push(`阶段失败: ${payload.phase_name} - ${payload.error}`)
}
break
}
// ── Board Meeting 模式事件 ──────────────────────────────────────── // ── Board Meeting 模式事件 ────────────────────────────────────────
case 'board_started': { case 'board_started': {

View File

@ -38,6 +38,21 @@ export const useTeamStore = defineStore('team', () => {
} }
} }
function updatePhaseStatus(
phaseId: string,
status: ITeamPlanPhase['status'],
result?: string,
) {
if (!teamState.value) return
const phases = teamState.value.plan_phases.map((p) => {
if (p.id !== phaseId) return p
return result !== undefined
? { ...p, status, result }
: { ...p, status }
})
teamState.value = { ...teamState.value, plan_phases: phases }
}
function selectExpert(expertId: string | null) { function selectExpert(expertId: string | null) {
selectedExpertId.value = expertId selectedExpertId.value = expertId
} }
@ -50,6 +65,6 @@ export const useTeamStore = defineStore('team', () => {
return { return {
teamState, selectedExpertId, activeExperts, leadExpert, teamState, selectedExpertId, activeExperts, leadExpert,
isTeamMode, currentPhase, completedPhases, isTeamMode, currentPhase, completedPhases,
setTeamState, updatePhases, selectExpert, clearTeam setTeamState, updatePhases, updatePhaseStatus, selectExpert, clearTeam
} }
}) })