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 thisFile = fileURLToPath(import.meta.url)
|
||||||
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
||||||
const appPath = new URL('../App.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 viewSource = await readFile(viewPath, 'utf8')
|
||||||
const appSource = await readFile(appPath, 'utf8')
|
const appSource = await readFile(appPath, 'utf8')
|
||||||
|
const styleSource = await readFile(stylePath, 'utf8')
|
||||||
|
|
||||||
assert.doesNotMatch(viewSource, /sampleProjects/)
|
assert.doesNotMatch(viewSource, /sampleProjects/)
|
||||||
assert.doesNotMatch(viewSource, /sampleAgentMatrix/)
|
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.doesNotMatch(appSource, /工作流视图/)
|
||||||
assert.match(viewSource, /selectedAgentId/)
|
assert.match(viewSource, /selectedAgentId/)
|
||||||
assert.match(viewSource, /selectAgent/)
|
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, /workflowBatches/)
|
||||||
assert.match(viewSource, /工作流批次/)
|
assert.match(viewSource, /工作流批次/)
|
||||||
assert.match(viewSource, /supervision/)
|
assert.match(viewSource, /supervision/)
|
||||||
assert.match(viewSource, /goalSource/)
|
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 () => {
|
test('agent collaboration guide requires workflow batch metadata', async () => {
|
||||||
|
|||||||
@@ -267,6 +267,52 @@ button {
|
|||||||
margin-top: 18px;
|
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 {
|
.flow-section {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@@ -453,6 +499,7 @@ button {
|
|||||||
.project-item[role="button"]:focus-visible,
|
.project-item[role="button"]:focus-visible,
|
||||||
.matrix-row[tabindex]:focus-visible,
|
.matrix-row[tabindex]:focus-visible,
|
||||||
.agent-list-item:focus-visible,
|
.agent-list-item:focus-visible,
|
||||||
|
.batch-tab:focus-visible,
|
||||||
.tab-button:focus-visible {
|
.tab-button:focus-visible {
|
||||||
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
|
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ const projectState = ref(normalizeProjects())
|
|||||||
const runtimeState = ref(normalizeRuntime())
|
const runtimeState = ref(normalizeRuntime())
|
||||||
const selectedProjectId = ref('')
|
const selectedProjectId = ref('')
|
||||||
const selectedAgentId = ref('')
|
const selectedAgentId = ref('')
|
||||||
|
const selectedWorkflowBatchName = ref('')
|
||||||
|
|
||||||
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
|
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
|
||||||
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
|
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
|
||||||
const selectedAgent = computed(() =>
|
const selectedAgent = computed(() =>
|
||||||
projectRuntime.value.agents.find((agent) => agent.id === selectedAgentId.value) ?? projectRuntime.value.agents[0],
|
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)
|
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) {
|
function selectProject(projectId) {
|
||||||
selectedProjectId.value = projectId
|
selectedProjectId.value = projectId
|
||||||
selectedAgentId.value = ''
|
selectedAgentId.value = ''
|
||||||
|
selectedWorkflowBatchName.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAgent(agent) {
|
function selectAgent(agent) {
|
||||||
selectedAgentId.value = agent.id
|
selectedAgentId.value = agent.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectWorkflowBatch(batch) {
|
||||||
|
selectedWorkflowBatchName.value = batch.name
|
||||||
|
}
|
||||||
|
|
||||||
async function loadReadonlyData() {
|
async function loadReadonlyData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -50,6 +66,7 @@ async function loadReadonlyData() {
|
|||||||
runtimeState.value = normalizeRuntime(runtimePayload)
|
runtimeState.value = normalizeRuntime(runtimePayload)
|
||||||
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
|
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
|
||||||
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
||||||
|
selectedWorkflowBatchName.value = projectRuntime.value.workflowBatches[0]?.name ?? ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err?.message || '连接后端接口失败'
|
error.value = err?.message || '连接后端接口失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -159,6 +176,21 @@ async function loadReadonlyData() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && !error && !projectRuntime.isEmpty" class="project-flow">
|
<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="flow-section">
|
||||||
<div class="panel-heading horizontal compact-heading">
|
<div class="panel-heading horizontal compact-heading">
|
||||||
<div>
|
<div>
|
||||||
@@ -167,17 +199,17 @@ async function loadReadonlyData() {
|
|||||||
</div>
|
</div>
|
||||||
<span class="read-only-chip">{{ projectRuntime.workflowBatches.length }} 个批次</span>
|
<span class="read-only-chip">{{ projectRuntime.workflowBatches.length }} 个批次</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="projectRuntime.workflowBatches.length > 0" class="workflow-batches">
|
<div v-if="selectedWorkflowBatch" class="workflow-batches">
|
||||||
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="workflow-batch">
|
<section class="workflow-batch">
|
||||||
<div class="workflow-batch-heading">
|
<div class="workflow-batch-heading">
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ batch.name }}</strong>
|
<strong>{{ selectedWorkflowBatch.name }}</strong>
|
||||||
<span>{{ batch.phaseCount }} 个阶段 · {{ batch.agentCount }} 个智能体 · {{ batch.handoffCount }} 条交接</span>
|
<span>{{ selectedWorkflowBatch.phaseCount }} 个阶段 · {{ selectedWorkflowBatch.agentCount }} 个智能体 · {{ selectedWorkflowBatch.handoffCount }} 条交接</span>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
|
<StatusBadge :label="selectedWorkflowBatch.statusZh" :status="selectedWorkflowBatch.status" source="SQLite 表" confidence="高" />
|
||||||
</div>
|
</div>
|
||||||
<ol class="phase-list compact">
|
<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 class="phase-dot" aria-hidden="true"></div>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ phase.name }}</strong>
|
<strong>{{ phase.name }}</strong>
|
||||||
@@ -201,15 +233,15 @@ async function loadReadonlyData() {
|
|||||||
<p class="eyebrow">交互方向</p>
|
<p class="eyebrow">交互方向</p>
|
||||||
<h3>项目内智能体流程记录</h3>
|
<h3>项目内智能体流程记录</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
<span class="read-only-chip">{{ selectedWorkflowBatch?.handoffCount ?? 0 }} 条交接</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="projectRuntime.workflowBatches.length > 0" class="phase-handoff-groups">
|
<div v-if="selectedWorkflowBatch" class="phase-handoff-groups">
|
||||||
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="phase-handoff-group">
|
<section class="phase-handoff-group">
|
||||||
<div class="phase-handoff-heading">
|
<div class="phase-handoff-heading">
|
||||||
<strong>{{ batch.name }}</strong>
|
<strong>{{ selectedWorkflowBatch.name }}</strong>
|
||||||
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
|
<StatusBadge :label="selectedWorkflowBatch.statusZh" :status="selectedWorkflowBatch.status" source="SQLite 表" confidence="高" />
|
||||||
</div>
|
</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>
|
<strong>{{ phase.name }}</strong>
|
||||||
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
|
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
|
||||||
<p v-else class="muted-line">这个阶段暂时没有交接边。</p>
|
<p v-else class="muted-line">这个阶段暂时没有交接边。</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user