ui: add workflow status badge + last activity indicator in log header

- Shows planning/executing/done/failed/waiting badge with pulse animation
- Shows relative time since last activity (e.g. "30s 前")
This commit is contained in:
Fam Zheng 2026-03-09 10:22:55 +00:00
parent 3c5180d5eb
commit cc75f8deac

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import type { ExecutionLogEntry, Comment, LlmCallLogEntry } from '../types' import type { ExecutionLogEntry, Comment, LlmCallLogEntry } from '../types'
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '') const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
@ -35,6 +35,40 @@ function quoteLlmCall(e: Event, lc: LlmCallLogEntry) {
emit('quote', `[LLM] ${preview}`) emit('quote', `[LLM] ${preview}`)
} }
// Last activity relative time
const now = ref(Date.now())
let ticker: ReturnType<typeof setInterval> | null = null
onMounted(() => { ticker = setInterval(() => { now.value = Date.now() }, 5000) })
onUnmounted(() => { if (ticker) clearInterval(ticker) })
const lastActivityAgo = computed(() => {
if (props.workflowStatus === 'done' || props.workflowStatus === 'failed') return ''
const times = [
...props.entries.map(e => e.created_at),
...props.llmCalls.map(l => l.created_at),
].filter(Boolean)
if (!times.length) return ''
const latest = times.reduce((a, b) => a > b ? a : b)
const d = new Date(latest.includes('T') ? latest : latest + 'Z')
const secs = Math.floor((now.value - d.getTime()) / 1000)
if (secs < 5) return '刚刚'
if (secs < 60) return `${secs}s 前`
const mins = Math.floor(secs / 60)
if (mins < 60) return `${mins}m 前`
return `${Math.floor(mins / 60)}h 前`
})
const workflowStatusLabel = computed(() => {
switch (props.workflowStatus) {
case 'planning': return '规划中'
case 'executing': return '执行中'
case 'done': return '已完成'
case 'failed': return '失败'
case 'waiting_approval': return '等待确认'
default: return props.workflowStatus
}
})
const scrollContainer = ref<HTMLElement | null>(null) const scrollContainer = ref<HTMLElement | null>(null)
const userScrolledUp = ref(false) const userScrolledUp = ref(false)
const detailedMode = ref(false) const detailedMode = ref(false)
@ -177,6 +211,9 @@ watch(logItems, () => {
<div class="execution-section" ref="scrollContainer" @scroll="onScroll"> <div class="execution-section" ref="scrollContainer" @scroll="onScroll">
<div class="section-header"> <div class="section-header">
<h2>日志</h2> <h2>日志</h2>
<span v-if="workflowStatus && workflowStatus !== 'pending'" class="workflow-badge" :class="workflowStatus">{{ workflowStatusLabel }}</span>
<span v-if="lastActivityAgo" class="last-activity">{{ lastActivityAgo }}</span>
<span class="header-spacer" />
<label class="detail-toggle"> <label class="detail-toggle">
<input type="checkbox" v-model="detailedMode" /> <input type="checkbox" v-model="detailedMode" />
<span>详细</span> <span>详细</span>
@ -274,6 +311,51 @@ watch(logItems, () => {
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.header-spacer {
flex: 1;
}
.workflow-badge {
font-size: 11px;
font-weight: 600;
padding: 2px 10px;
border-radius: 10px;
letter-spacing: 0.3px;
}
.workflow-badge.planning,
.workflow-badge.executing {
background: var(--accent);
color: var(--bg-primary);
animation: pulse-badge 2s ease-in-out infinite;
}
.workflow-badge.done {
background: var(--success);
color: #fff;
}
.workflow-badge.failed {
background: var(--error);
color: #fff;
}
.workflow-badge.waiting_approval {
background: #ff9800;
color: #fff;
}
.last-activity {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
}
@keyframes pulse-badge {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.detail-toggle { .detail-toggle {
display: flex; display: flex;
align-items: center; align-items: center;