feat: add workflow batch switcher
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user