feat: add workflow batch switcher

This commit is contained in:
Yoilun
2026-05-26 11:48:47 +08:00
parent 0fd7b17aba
commit cb46d5bc04
3 changed files with 101 additions and 12 deletions

View File

@@ -590,8 +590,10 @@ test('project and app views keep workflow inside the selected project view', asy
const thisFile = fileURLToPath(import.meta.url)
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
const appPath = new URL('../App.vue', `file://${thisFile}`)
const stylePath = new URL('../styles.css', `file://${thisFile}`)
const viewSource = await readFile(viewPath, 'utf8')
const appSource = await readFile(appPath, 'utf8')
const styleSource = await readFile(stylePath, 'utf8')
assert.doesNotMatch(viewSource, /sampleProjects/)
assert.doesNotMatch(viewSource, /sampleAgentMatrix/)
@@ -599,10 +601,18 @@ test('project and app views keep workflow inside the selected project view', asy
assert.doesNotMatch(appSource, /工作流视图/)
assert.match(viewSource, /selectedAgentId/)
assert.match(viewSource, /selectAgent/)
assert.match(viewSource, /selectedWorkflowBatchName/)
assert.match(viewSource, /selectedWorkflowBatch/)
assert.match(viewSource, /selectWorkflowBatch/)
assert.match(viewSource, /批次切换/)
assert.match(viewSource, /role="group"/)
assert.match(viewSource, /selectedWorkflowBatch\?\.handoffCount \?\? 0/)
assert.match(viewSource, /workflowBatches/)
assert.match(viewSource, /工作流批次/)
assert.match(viewSource, /supervision/)
assert.match(viewSource, /goalSource/)
assert.match(styleSource, /\.batch-tab:hover/)
assert.match(styleSource, /\.batch-tab:focus-visible/)
})
test('agent collaboration guide requires workflow batch metadata', async () => {

View File

@@ -267,6 +267,52 @@ button {
margin-top: 18px;
}
.batch-switcher {
grid-column: 1 / -1;
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
}
.batch-tab {
flex: 0 0 auto;
min-width: 190px;
max-width: 280px;
padding: 10px 12px;
color: var(--green);
text-align: left;
background: var(--panel-muted);
border: 1px solid var(--line);
border-radius: 8px;
}
.batch-tab:hover {
border-color: var(--green);
box-shadow: 0 8px 20px rgb(36 139 107 / 12%);
}
.batch-tab strong,
.batch-tab span {
display: block;
}
.batch-tab span {
margin-top: 4px;
color: var(--muted);
font-size: 0.82rem;
}
.batch-tab.active {
color: var(--panel);
background: var(--green);
border-color: var(--green);
}
.batch-tab.active span {
color: color-mix(in srgb, var(--panel) 78%, white);
}
.flow-section {
min-width: 0;
padding-top: 16px;
@@ -453,6 +499,7 @@ button {
.project-item[role="button"]:focus-visible,
.matrix-row[tabindex]:focus-visible,
.agent-list-item:focus-visible,
.batch-tab:focus-visible,
.tab-button:focus-visible {
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
outline-offset: 2px;

View File

@@ -11,12 +11,16 @@ const projectState = ref(normalizeProjects())
const runtimeState = ref(normalizeRuntime())
const selectedProjectId = ref('')
const selectedAgentId = ref('')
const selectedWorkflowBatchName = ref('')
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
const selectedAgent = computed(() =>
projectRuntime.value.agents.find((agent) => agent.id === selectedAgentId.value) ?? projectRuntime.value.agents[0],
)
const selectedWorkflowBatch = computed(() =>
projectRuntime.value.workflowBatches.find((batch) => batch.name === selectedWorkflowBatchName.value) ?? projectRuntime.value.workflowBatches[0],
)
onMounted(loadReadonlyData)
@@ -29,15 +33,27 @@ watch(
},
)
watch(
() => projectRuntime.value.workflowBatches.map((batch) => batch.name).join('|'),
() => {
selectedWorkflowBatchName.value = projectRuntime.value.workflowBatches[0]?.name ?? ''
},
)
function selectProject(projectId) {
selectedProjectId.value = projectId
selectedAgentId.value = ''
selectedWorkflowBatchName.value = ''
}
function selectAgent(agent) {
selectedAgentId.value = agent.id
}
function selectWorkflowBatch(batch) {
selectedWorkflowBatchName.value = batch.name
}
async function loadReadonlyData() {
loading.value = true
error.value = ''
@@ -50,6 +66,7 @@ async function loadReadonlyData() {
runtimeState.value = normalizeRuntime(runtimePayload)
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
selectedWorkflowBatchName.value = projectRuntime.value.workflowBatches[0]?.name ?? ''
} catch (err) {
error.value = err?.message || '连接后端接口失败'
} finally {
@@ -159,6 +176,21 @@ async function loadReadonlyData() {
</div>
<div v-if="!loading && !error && !projectRuntime.isEmpty" class="project-flow">
<div class="batch-switcher" role="group" aria-label="批次切换">
<button
v-for="batch in projectRuntime.workflowBatches"
:key="batch.name"
type="button"
class="batch-tab"
:class="{ active: batch.name === selectedWorkflowBatch?.name }"
:aria-pressed="batch.name === selectedWorkflowBatch?.name"
@click="selectWorkflowBatch(batch)"
>
<strong>{{ batch.name }}</strong>
<span>{{ batch.phaseCount }} 阶段 · {{ batch.handoffCount }} 交接</span>
</button>
</div>
<div class="flow-section">
<div class="panel-heading horizontal compact-heading">
<div>
@@ -167,17 +199,17 @@ async function loadReadonlyData() {
</div>
<span class="read-only-chip">{{ projectRuntime.workflowBatches.length }} 个批次</span>
</div>
<div v-if="projectRuntime.workflowBatches.length > 0" class="workflow-batches">
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="workflow-batch">
<div v-if="selectedWorkflowBatch" class="workflow-batches">
<section class="workflow-batch">
<div class="workflow-batch-heading">
<div>
<strong>{{ batch.name }}</strong>
<span>{{ batch.phaseCount }} 个阶段 · {{ batch.agentCount }} 个智能体 · {{ batch.handoffCount }} 条交接</span>
<strong>{{ selectedWorkflowBatch.name }}</strong>
<span>{{ selectedWorkflowBatch.phaseCount }} 个阶段 · {{ selectedWorkflowBatch.agentCount }} 个智能体 · {{ selectedWorkflowBatch.handoffCount }} 条交接</span>
</div>
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
<StatusBadge :label="selectedWorkflowBatch.statusZh" :status="selectedWorkflowBatch.status" source="SQLite 表" confidence="高" />
</div>
<ol class="phase-list compact">
<li v-for="phase in batch.phases" :key="`${batch.name}-${phase.name}`" :data-status="phase.status">
<li v-for="phase in selectedWorkflowBatch.phases" :key="`${selectedWorkflowBatch.name}-${phase.name}`" :data-status="phase.status">
<div class="phase-dot" aria-hidden="true"></div>
<div>
<strong>{{ phase.name }}</strong>
@@ -201,15 +233,15 @@ async function loadReadonlyData() {
<p class="eyebrow">交互方向</p>
<h3>项目内智能体流程记录</h3>
</div>
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
<span class="read-only-chip">{{ selectedWorkflowBatch?.handoffCount ?? 0 }} 条交接</span>
</div>
<div v-if="projectRuntime.workflowBatches.length > 0" class="phase-handoff-groups">
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="phase-handoff-group">
<div v-if="selectedWorkflowBatch" class="phase-handoff-groups">
<section class="phase-handoff-group">
<div class="phase-handoff-heading">
<strong>{{ batch.name }}</strong>
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
<strong>{{ selectedWorkflowBatch.name }}</strong>
<StatusBadge :label="selectedWorkflowBatch.statusZh" :status="selectedWorkflowBatch.status" source="SQLite 表" confidence="高" />
</div>
<section v-for="phase in batch.phases" :key="`${batch.name}-${phase.name}`" class="phase-handoff-nested">
<section v-for="phase in selectedWorkflowBatch.phases" :key="`${selectedWorkflowBatch.name}-${phase.name}`" class="phase-handoff-nested">
<strong>{{ phase.name }}</strong>
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
<p v-else class="muted-line">这个阶段暂时没有交接边</p>