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:
@@ -1,5 +1,5 @@
|
||||
<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'
|
||||
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
|
||||
@@ -35,6 +35,40 @@ function quoteLlmCall(e: Event, lc: LlmCallLogEntry) {
|
||||
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 userScrolledUp = ref(false)
|
||||
const detailedMode = ref(false)
|
||||
@@ -177,6 +211,9 @@ watch(logItems, () => {
|
||||
<div class="execution-section" ref="scrollContainer" @scroll="onScroll">
|
||||
<div class="section-header">
|
||||
<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">
|
||||
<input type="checkbox" v-model="detailedMode" />
|
||||
<span>详细</span>
|
||||
@@ -274,6 +311,51 @@ watch(logItems, () => {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user