修复: status→enum + type alias(SMELL-P1-6)

This commit is contained in:
2026-06-28 04:42:59 +08:00
parent 48c966f6f7
commit d1b9488853
21 changed files with 304 additions and 126 deletions

View File

@@ -2,7 +2,7 @@
<div v-if="dag" class="wf-dag">
<h4 class="wf-dag-title">{{ $t('taskDetail.workflowDagTitle') || '工作流结构' }}</h4>
<!-- 拓扑分层渲染同层并行层间串行 -->
<!-- 拓扑分层渲染 -->
<div class="wf-dag-layers">
<div v-for="(layer, li) in layers" :key="li" class="wf-dag-layer">
<div class="wf-dag-layer-label">{{ $t('taskDetail.workflowLayer') || '层' }} {{ li + 1 }}</div>
@@ -19,19 +19,41 @@
<span v-if="nodeStatuses[node.id]" class="wf-dag-node-status">{{ statusLabel(nodeStatuses[node.id]) }}</span>
</div>
</div>
<!-- 出边条件标签同层节点出站边展示条件 -->
<div v-if="layerOutEdges[li]" class="wf-dag-outedges">
<div v-for="edge in layerOutEdges[li]" :key="edge.source + '→' + edge.target" class="wf-dag-outedge">
<span class="wf-dag-outedge-arrow"> {{ edge.target }}</span>
<span class="wf-dag-cond-label" :class="{ 'wf-dag-cond-label--uncond': !edge.condition }">{{ edge.condition || ($t('taskDetail.workflowUnconditional') || '无条件') }}</span>
</div>
</div>
<!-- 层间箭头 -->
<div v-if="li < layers.length - 1" class="wf-dag-layer-arrow"></div>
</div>
</div>
<!-- 边条件列表折叠态hover 节点时参考 -->
<!-- 边条件编辑列表 -->
<details class="wf-dag-edges-detail">
<summary class="wf-dag-edges-summary">{{ $t('taskDetail.workflowEdgeDetail') || '边条件详情' }} ({{ dag.edges.length }})</summary>
<summary class="wf-dag-edges-summary">{{ $t('taskDetail.workflowEdgeDetail') || '边条件编辑' }} ({{ dag.edges.length }})</summary>
<div class="wf-dag-edges">
<div v-for="(edge, i) in dag.edges" :key="i" class="wf-dag-edge">
<span class="wf-dag-edge-arrow">{{ edge.source }} {{ edge.target }}</span>
<span v-if="edge.condition" class="wf-dag-edge-cond" :title="edge.condition">{{ edge.condition }}</span>
<span v-else class="wf-dag-edge-cond wf-dag-edge-cond--uncond">{{ $t('taskDetail.workflowUnconditional') || '无条件' }}</span>
<input
v-if="editingIndex === i"
ref="editInputRef"
class="wf-dag-edge-input"
:value="editValue"
@input="editValue = ($event.target as HTMLInputElement).value"
@keydown.enter="saveCondition(i)"
@keydown.escape="cancelEdit"
@blur="saveCondition(i)"
:placeholder="$t('taskDetail.workflowCondPlaceholder') || '输入条件表达式'"
/>
<span
v-else
class="wf-dag-cond-label"
:class="{ 'wf-dag-cond-label--uncond': !edge.condition, 'wf-dag-cond-label--editable': true }"
@click="startEdit(i, edge)"
>{{ edge.condition || ($t('taskDetail.workflowUnconditional') || '无条件') }}</span>
</div>
</div>
</details>
@@ -42,7 +64,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
interface DagNode {
id: string
@@ -63,6 +85,10 @@ const props = withDefaults(defineProps<{
nodeStatuses: () => ({}),
})
const emit = defineEmits<{
(e: 'update:dagJson', val: string): void
}>()
const dag = computed<{ nodes: DagNode[]; edges: EdgeDef[] } | null>(() => {
try { return JSON.parse(props.dagJson) }
catch { return null }
@@ -89,11 +115,10 @@ function nodeClass(id: string, statuses: Record<string, NodeStatus>): Record<str
}
}
/** 拓扑分层:从无入边的根节点开始,逐层推进 */
/** 拓扑分层 */
const layers = computed<(DagNode & { deps?: string[] })[][]>(() => {
if (!dag.value) return []
const d = dag.value
// 入边计数
const inDegree: Record<string, number> = {}
const edgeMap: Record<string, string[]> = {}
for (const n of d.nodes) {
@@ -105,8 +130,7 @@ const layers = computed<(DagNode & { deps?: string[] })[][]>(() => {
edgeMap[e.source].push(e.target)
inDegree[e.target] = (inDegree[e.target] || 0) + 1
}
// Kahn 拓扑排序
const layers: (DagNode & { deps?: string[] })[][] = []
const result: (DagNode & { deps?: string[] })[][] = []
let queue = Object.entries(inDegree).filter(([, d]) => d === 0).map(([id]) => id)
const visited = new Set<string>()
while (queue.length > 0) {
@@ -122,11 +146,59 @@ const layers = computed<(DagNode & { deps?: string[] })[][]>(() => {
if (inDegree[target] === 0) next.push(target)
}
}
if (layer.length > 0) layers.push(layer)
if (layer.length > 0) result.push(layer)
queue = next
}
return layers
return result
})
/**
* 每层出边索引缓存版layers 变化时重新计算,避免模板内重复调用函数。
* 返回 { [layerIndex]: EdgeDef[] },仅含出边非空的层。
*/
const layerOutEdges = computed<Record<number, EdgeDef[]>>(() => {
if (!dag.value) return {}
const bySource: Record<string, EdgeDef[]> = {}
for (const e of dag.value.edges) {
if (!bySource[e.source]) bySource[e.source] = []
bySource[e.source].push(e)
}
const result: Record<number, EdgeDef[]> = {}
for (let li = 0; li < layers.value.length; li++) {
const edges: EdgeDef[] = []
for (const node of layers.value[li]) {
const out = bySource[node.id]
if (out) edges.push(...out)
}
if (edges.length > 0) result[li] = edges
}
return result
})
// ── 条件编辑 ──
const editingIndex = ref<number | null>(null)
const editValue = ref('')
const editInputRef = ref<HTMLInputElement | null>(null)
function startEdit(i: number, edge: EdgeDef) {
editingIndex.value = i
editValue.value = edge.condition || ''
nextTick(() => editInputRef.value?.focus())
}
function saveCondition(i: number) {
if (!dag.value || editingIndex.value !== i) return
const edges = [...dag.value.edges]
const val = editValue.value.trim()
edges[i] = { ...edges[i], condition: val || null }
const updated = { ...dag.value, edges }
emit('update:dagJson', JSON.stringify(updated, null, 2))
editingIndex.value = null
}
function cancelEdit() {
editingIndex.value = null
}
</script>
<style scoped>
@@ -180,14 +252,40 @@ const layers = computed<(DagNode & { deps?: string[] })[][]>(() => {
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
/* 出边条件标签 */
.wf-dag-outedges {
display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 12px;
padding: 2px 0;
}
.wf-dag-outedge {
display: inline-flex; align-items: center; gap: 4px; font-size: 10px;
}
.wf-dag-outedge-arrow { color: var(--df-text-primary); white-space: nowrap; }
/* 条件标签(共享:出边展示 + 编辑列表) */
.wf-dag-cond-label {
font-family: var(--df-font-mono); font-size: 10px; padding: 0 4px;
background: rgba(74,144,226,0.08); border-radius: 2px; color: var(--df-info, #4a90e2);
max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wf-dag-cond-label--uncond { background: transparent; color: var(--df-text-dim, #aaa); }
.wf-dag-cond-label--editable {
cursor: pointer; font-size: 11px; padding: 1px 6px; border-radius: 3px;
transition: background 0.15s;
}
.wf-dag-cond-label--editable:hover { background: rgba(74,144,226,0.18); }
.wf-dag-cond-label--uncond.wf-dag-cond-label--editable:hover { background: var(--df-bg-card); }
/* 边条件编辑列表 */
.wf-dag-edges-detail { margin-top: 8px; }
.wf-dag-edges-summary { cursor: pointer; font-size: 11px; color: var(--df-info, #4a90e2); }
.wf-dag-edges { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.wf-dag-edge { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 11px; }
.wf-dag-edge-arrow { color: var(--df-text-primary); }
.wf-dag-edge-cond {
font-family: var(--df-font-mono); font-size: 11px; padding: 1px 6px;
background: rgba(74,144,226,0.08); border-radius: 3px; color: var(--df-info, #4a90e2);
.wf-dag-edge-arrow { color: var(--df-text-primary); white-space: nowrap; }
.wf-dag-edge-input {
flex: 1; min-width: 0; font-family: var(--df-font-mono); font-size: 11px;
padding: 2px 6px; border: 0.5px solid var(--df-border); border-radius: 3px;
background: var(--df-bg); color: var(--df-text); outline: none;
}
.wf-dag-edge-cond--uncond { background: transparent; color: var(--df-text-dim, #aaa); }
.wf-dag-edge-input:focus { border-color: var(--df-accent); }
</style>