From 14a4d9dc9c3af294dd3e183d975e3b14b13d26d4 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 4 Aug 2026 09:20:56 +0800 Subject: [PATCH] Redesign workflow task cards --- src/components/cards/WorkflowTaskCard.vue | 709 +++++++++++++----- .../cards/__tests__/WorkflowTaskCard.spec.ts | 181 +++++ src/locales/en-US.ts | 10 +- src/locales/zh-CN.ts | 10 +- src/locales/zh-TW.ts | 10 +- 5 files changed, 749 insertions(+), 171 deletions(-) create mode 100644 src/components/cards/__tests__/WorkflowTaskCard.spec.ts diff --git a/src/components/cards/WorkflowTaskCard.vue b/src/components/cards/WorkflowTaskCard.vue index 7555fdcc..6643952e 100644 --- a/src/components/cards/WorkflowTaskCard.vue +++ b/src/components/cards/WorkflowTaskCard.vue @@ -5,6 +5,7 @@ import { useConfirm } from '@/composables/useConfirm' import api from '@/api' import { useI18n } from 'vue-i18n' import { openSharedDialog } from '@/composables/useSharedDialog' +import { formatDateDifference } from '@/@core/utils/formatters' const WorkflowActionsDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowActionsDialog.vue')) const WorkflowAddEditDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowAddEditDialog.vue')) @@ -177,45 +178,175 @@ async function handleReset(item: Workflow) { } } -// 计算状态颜色 -const resolveStatusVariant = (status: string | undefined) => { - if (status === 'S') - return { - color: 'success', - bgColor: 'linear-gradient(to bottom right, rgba(76, 175, 80, 0.9), rgba(76, 175, 80, 0.7))', - text: t('workflow.task.status.success'), - } - else if (status === 'R') - return { - color: 'primary', - bgColor: 'linear-gradient(to bottom right, rgba(33, 150, 243, 0.9), rgba(33, 150, 243, 0.7))', - text: t('workflow.task.status.running'), - } - else if (status === 'F') - return { - color: 'error', - bgColor: 'linear-gradient(to bottom right, rgba(244, 67, 54, 0.9), rgba(244, 67, 54, 0.7))', - text: t('workflow.task.status.failed'), - } - else if (status === 'P') - return { - color: 'warning', - bgColor: 'linear-gradient(to bottom right, rgba(255, 152, 0, 0.9), rgba(255, 152, 0, 0.7))', - text: t('workflow.task.status.paused'), - } - else - return { - color: 'info', - bgColor: 'linear-gradient(to bottom right, rgba(33, 150, 243, 0.9), rgba(33, 150, 243, 0.7))', - text: t('workflow.task.status.waiting'), - } +type WorkflowStatusColor = 'error' | 'info' | 'primary' | 'secondary' | 'success' | 'warning' +type WorkflowActionSegmentState = 'active' | 'complete' | 'failed' | 'pending' + +interface WorkflowActionDisplay { + id?: string + name?: string + type?: string } -// 计算当前动作占比 -const resolveProgress = (item: Workflow) => { - const current_action_length = item.current_action?.split(',').length || 0 - return item.actions?.length ? Math.round((current_action_length / (item.actions.length || 1)) * 100) : 0 +interface WorkflowExecutionNode { + state?: string } + +const resolveStatusVariant = (status: string | undefined) => { + const variants: Record< + string, + { + color: WorkflowStatusColor + icon: string + text: string + } + > = { + S: { color: 'success', icon: 'mdi-check-circle-outline', text: t('workflow.task.status.success') }, + R: { color: 'primary', icon: 'mdi-progress-clock', text: t('workflow.task.status.running') }, + F: { color: 'error', icon: 'mdi-alert-circle-outline', text: t('workflow.task.status.failed') }, + P: { color: 'secondary', icon: 'mdi-pause-circle-outline', text: t('workflow.task.status.paused') }, + W: { color: 'warning', icon: 'mdi-clock-outline', text: t('workflow.task.status.waiting') }, + } + + return variants[status || 'W'] || variants.W +} + +const statusVariant = computed(() => resolveStatusVariant(props.workflow.state)) + +const triggerDisplay = computed(() => { + if (props.workflow.trigger_type === 'event') { + return { + icon: 'mdi-calendar-check-outline', + text: getEventTypeText(props.workflow.event_type || ''), + } + } + + if (props.workflow.trigger_type === 'manual') { + return { + icon: 'mdi-hand-pointing-up', + text: t('workflow.task.info.manualTrigger'), + } + } + + return { + icon: 'mdi-clock-outline', + text: props.workflow.timer || t('workflow.task.info.timer'), + } +}) + +const workflowActions = computed(() => + Array.isArray(props.workflow.actions) ? props.workflow.actions : [], +) + +const totalActionCount = computed(() => workflowActions.value.length) + +const currentActionIds = computed(() => { + return new Set( + (props.workflow.current_action || '') + .split(',') + .map(actionId => actionId.trim()) + .filter(Boolean), + ) +}) + +const executionNodes = computed>(() => { + const nodes = props.workflow.execution_state?.nodes + return nodes && typeof nodes === 'object' && !Array.isArray(nodes) + ? (nodes as Record) + : {} +}) + +const finishedActionCount = computed(() => { + const runtimeCount = Number(props.workflow.execution_state?.runtime?.finished_actions) + const knownActionIds = new Set(workflowActions.value.map(action => String(action.id || '')).filter(Boolean)) + const fallbackCount = [...currentActionIds.value].filter(actionId => knownActionIds.has(actionId)).length + const count = Number.isFinite(runtimeCount) && runtimeCount >= 0 ? Math.trunc(runtimeCount) : fallbackCount + + return Math.min(Math.max(count, 0), totalActionCount.value) +}) + +const actionSegments = computed(() => { + return workflowActions.value.map((action, index) => { + const actionId = action.id ? String(action.id) : '' + const nodeState = actionId ? executionNodes.value[actionId]?.state : undefined + + if (nodeState === 'failed') return 'failed' + if (nodeState === 'running' || nodeState === 'queued') return 'active' + if (nodeState === 'success' || nodeState === 'completed' || nodeState === 'skipped') return 'complete' + if (actionId && currentActionIds.value.has(actionId)) return 'complete' + if (index < finishedActionCount.value) return 'complete' + if (props.workflow.state === 'R' && index === finishedActionCount.value) return 'active' + + return 'pending' + }) +}) + +const runningActionName = computed(() => { + const runningAction = workflowActions.value.find(action => { + if (!action.id) return false + const nodeState = executionNodes.value[String(action.id)]?.state + return nodeState === 'running' || nodeState === 'queued' + }) + + return runningAction?.name || runningAction?.type || '' +}) + +const executionStatus = computed(() => { + if (props.workflow.state === 'R') { + if (runningActionName.value) { + return { + color: 'primary' as WorkflowStatusColor, + icon: 'mdi-pulse', + text: t('workflow.task.info.executingAction', { name: runningActionName.value }), + } + } + + if (totalActionCount.value > 0) { + return { + color: 'primary' as WorkflowStatusColor, + icon: 'mdi-pulse', + text: t('workflow.task.info.preparingAction', { + current: Math.min(finishedActionCount.value + 1, totalActionCount.value), + }), + } + } + + return { + color: 'secondary' as WorkflowStatusColor, + icon: 'mdi-vector-polyline-remove', + text: t('workflow.task.info.noActions'), + } + } + + if (props.workflow.state === 'F') { + return { + color: 'error' as WorkflowStatusColor, + icon: 'mdi-alert-circle-outline', + text: props.workflow.result || t('workflow.task.status.failed'), + } + } + + if (props.workflow.last_time) { + return { + color: undefined, + icon: 'mdi-history', + text: t('workflow.task.info.lastExecuted', { time: formatDateDifference(props.workflow.last_time) }), + } + } + + if (finishedActionCount.value > 0) { + return { + color: undefined, + icon: 'mdi-history', + text: t('workflow.task.info.executionIncomplete'), + } + } + + return { + color: undefined, + icon: 'mdi-history', + text: t('workflow.task.info.neverExecuted'), + } +})