mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
Redesign workflow task cards
This commit is contained in:
@@ -5,6 +5,7 @@ import { useConfirm } from '@/composables/useConfirm'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||||
|
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
const WorkflowActionsDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowActionsDialog.vue'))
|
const WorkflowActionsDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowActionsDialog.vue'))
|
||||||
const WorkflowAddEditDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowAddEditDialog.vue'))
|
const WorkflowAddEditDialog = defineAsyncComponent(() => import('@/components/dialog/WorkflowAddEditDialog.vue'))
|
||||||
@@ -177,45 +178,175 @@ async function handleReset(item: Workflow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算状态颜色
|
type WorkflowStatusColor = 'error' | 'info' | 'primary' | 'secondary' | 'success' | 'warning'
|
||||||
const resolveStatusVariant = (status: string | undefined) => {
|
type WorkflowActionSegmentState = 'active' | 'complete' | 'failed' | 'pending'
|
||||||
if (status === 'S')
|
|
||||||
return {
|
interface WorkflowActionDisplay {
|
||||||
color: 'success',
|
id?: string
|
||||||
bgColor: 'linear-gradient(to bottom right, rgba(76, 175, 80, 0.9), rgba(76, 175, 80, 0.7))',
|
name?: string
|
||||||
text: t('workflow.task.status.success'),
|
type?: string
|
||||||
}
|
|
||||||
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'),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算当前动作占比
|
interface WorkflowExecutionNode {
|
||||||
const resolveProgress = (item: Workflow) => {
|
state?: string
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<WorkflowActionDisplay[]>(() =>
|
||||||
|
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<Record<string, WorkflowExecutionNode>>(() => {
|
||||||
|
const nodes = props.workflow.execution_state?.nodes
|
||||||
|
return nodes && typeof nodes === 'object' && !Array.isArray(nodes)
|
||||||
|
? (nodes as Record<string, WorkflowExecutionNode>)
|
||||||
|
: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
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<WorkflowActionSegmentState[]>(() => {
|
||||||
|
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'),
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
@@ -223,151 +354,179 @@ const resolveProgress = (item: Workflow) => {
|
|||||||
<!-- Hover 命中区域保持静止,避免卡片上浮后底边反复触发 mouseleave。 -->
|
<!-- Hover 命中区域保持静止,避免卡片上浮后底边反复触发 mouseleave。 -->
|
||||||
<div v-bind="hover.props" class="workflow-task-card-hover-area h-full">
|
<div v-bind="hover.props" class="workflow-task-card-hover-area h-full">
|
||||||
<VCard
|
<VCard
|
||||||
class="app-hover-lift-card mx-auto h-full"
|
class="workflow-task-card app-hover-lift-card mx-auto h-full"
|
||||||
@click="handleFlow(workflow)"
|
@click="handleFlow(workflow)"
|
||||||
:ripple="false"
|
:ripple="false"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:class="{ 'app-hover-lift-card--hovering': hover.isHovering }"
|
:class="[
|
||||||
|
`workflow-task-card--status-${statusVariant.color}`,
|
||||||
|
{ 'app-hover-lift-card--hovering': hover.isHovering },
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<VCardItem
|
<VCardItem class="workflow-task-card__header">
|
||||||
class="px-2 py-2"
|
<template #prepend>
|
||||||
:style="{
|
<VAvatar
|
||||||
background: resolveStatusVariant(workflow?.state).bgColor,
|
:color="statusVariant.color"
|
||||||
}"
|
variant="tonal"
|
||||||
>
|
rounded="md"
|
||||||
<template #prepend>
|
size="32"
|
||||||
<VAvatar variant="text" size="small">
|
class="workflow-task-card__trigger-icon"
|
||||||
<VIcon
|
>
|
||||||
v-if="workflow?.state === 'P'"
|
<VIcon :icon="triggerDisplay.icon" :data-workflow-trigger-icon="triggerDisplay.icon" />
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VCardTitle class="workflow-task-card__title text-body-1" :title="workflow.description || workflow.name">
|
||||||
|
{{ workflow.name }}
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardSubtitle class="workflow-task-card__trigger-text">
|
||||||
|
{{ triggerDisplay.text }}
|
||||||
|
</VCardSubtitle>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<IconBtn
|
||||||
|
class="workflow-task-card__menu"
|
||||||
|
size="small"
|
||||||
|
density="compact"
|
||||||
|
:aria-label="t('workflow.task.moreActions')"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
|
<VTooltip activator="parent" location="top">{{ t('workflow.task.moreActions') }}</VTooltip>
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem base-color="primary" @click="handleEdit(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-note-edit" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.edit') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem base-color="success" @click="handleFlow(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-vector-polyline" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.editFlow') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, false)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-play-speed" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.continue') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, true)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-replay" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.restart') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem v-else base-color="info" @click="handleRun(workflow, true)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-run" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.run') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem base-color="warning" @click="handleReset(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-restore-alert" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.reset') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem base-color="info" @click="handleShare(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-share" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.share') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem base-color="error" @click="handleDelete(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-delete" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('workflow.task.delete') }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<VCardText class="workflow-task-card__body">
|
||||||
|
<div class="workflow-task-card__status-row">
|
||||||
|
<VChip :color="statusVariant.color" :prepend-icon="statusVariant.icon" size="small" variant="tonal">
|
||||||
|
{{ statusVariant.text }}
|
||||||
|
</VChip>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
v-if="workflow.state === 'P'"
|
||||||
color="success"
|
color="success"
|
||||||
icon="mdi-play"
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
density="compact"
|
||||||
|
prepend-icon="mdi-play"
|
||||||
|
:aria-label="t('common.enable')"
|
||||||
@click.stop="handleEnable(workflow)"
|
@click.stop="handleEnable(workflow)"
|
||||||
/>
|
>
|
||||||
<VIcon v-else color="warning" icon="mdi-pause" @click.stop="handlePause(workflow)" />
|
{{ t('common.enable') }}
|
||||||
</VAvatar>
|
</VBtn>
|
||||||
</template>
|
<VBtn
|
||||||
<VCardTitle class="text-white text-lg">
|
v-else
|
||||||
<span :title="workflow?.description">{{ workflow?.name }}</span>
|
:color="statusVariant.color"
|
||||||
</VCardTitle>
|
variant="text"
|
||||||
<template #append>
|
size="small"
|
||||||
<IconBtn>
|
density="compact"
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
prepend-icon="mdi-pause"
|
||||||
<VMenu activator="parent" close-on-content-click>
|
:aria-label="t('common.pause')"
|
||||||
<VList>
|
@click.stop="handlePause(workflow)"
|
||||||
<VListItem base-color="primary" @click="handleEdit(workflow)">
|
>
|
||||||
<template #prepend>
|
{{ t('common.pause') }}
|
||||||
<VIcon icon="mdi-note-edit" />
|
</VBtn>
|
||||||
</template>
|
</div>
|
||||||
<VListItemTitle>{{ t('workflow.task.edit') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
<div class="workflow-task-card__metrics">
|
||||||
<VListItem base-color="success" @click="handleFlow(workflow)">
|
<div class="workflow-task-card__metric">
|
||||||
<template #prepend>
|
<span>{{ t('workflow.task.info.actionCount') }}</span>
|
||||||
<VIcon icon="mdi-vector-polyline" />
|
<strong>{{ totalActionCount }}</strong>
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.editFlow') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, false)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-play-speed" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.continue') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, true)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-replay" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.restart') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem v-else base-color="info" @click="handleRun(workflow, true)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-run" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.run') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem base-color="warning" @click="handleReset(workflow)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-restore-alert" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.reset') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem base-color="info" @click="handleShare(workflow)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-share" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.share') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem base-color="error" @click="handleDelete(workflow)">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-delete" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>{{ t('workflow.task.delete') }}</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</template>
|
|
||||||
</VCardItem>
|
|
||||||
<VDivider />
|
|
||||||
<VCardText class="pa-3">
|
|
||||||
<div class="d-flex flex-column gap-y-3">
|
|
||||||
<div class="d-flex flex-wrap gap-x-3">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="mb-1">{{ t('workflow.task.info.trigger') }}</div>
|
|
||||||
<h5>
|
|
||||||
<span v-if="workflow?.trigger_type === 'timer' || !workflow?.trigger_type">
|
|
||||||
<VIcon icon="mdi-clock-outline" size="small" class="me-1" />
|
|
||||||
{{ workflow?.timer }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="workflow?.trigger_type === 'event'">
|
|
||||||
<VIcon icon="mdi-calendar-check" size="small" class="me-1" />
|
|
||||||
{{ getEventTypeText(workflow?.event_type || '') }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="workflow?.trigger_type === 'manual'">
|
|
||||||
<VIcon icon="mdi-hand-pointing-up" size="small" class="me-1" />
|
|
||||||
{{ t('workflow.task.info.manualTrigger') }}
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="workflow-task-card__metric">
|
||||||
<div class="mb-1">{{ t('workflow.task.info.status') }}</div>
|
<span>{{ t('workflow.task.info.runCount') }}</span>
|
||||||
<h5 :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
<strong>{{ workflow.run_count || 0 }}</strong>
|
||||||
{{ resolveStatusVariant(workflow?.state).text }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-x-3">
|
|
||||||
<div class="flex-1">
|
<div class="workflow-task-card__progress">
|
||||||
<div class="mb-1">{{ t('workflow.task.info.actionCount') }}</div>
|
<div class="workflow-task-card__progress-label">
|
||||||
<div>
|
<span>{{ t('workflow.task.info.actionProgress') }}</span>
|
||||||
<VAvatar size="24" color="primary" variant="tonal">
|
<strong>{{ finishedActionCount }} / {{ totalActionCount }}</strong>
|
||||||
<span class="text-xs">{{ workflow?.actions?.length }}</span>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div
|
||||||
<div class="mb-1">{{ t('workflow.task.info.runCount') }}</div>
|
class="workflow-task-card__action-track"
|
||||||
<h5>{{ workflow?.run_count }}</h5>
|
role="progressbar"
|
||||||
|
:aria-label="`${t('workflow.task.info.actionProgress')}: ${finishedActionCount} / ${totalActionCount}`"
|
||||||
|
aria-valuemin="0"
|
||||||
|
:aria-valuemax="Math.max(totalActionCount, 1)"
|
||||||
|
:aria-valuenow="finishedActionCount"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(segment, index) in actionSegments"
|
||||||
|
:key="workflowActions[index]?.id || index"
|
||||||
|
class="workflow-task-card__action-segment"
|
||||||
|
:class="`workflow-task-card__action-segment--${segment}`"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="totalActionCount === 0"
|
||||||
|
class="workflow-task-card__action-segment workflow-task-card__action-segment--empty"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-x-3">
|
|
||||||
<div class="flex-1">
|
<div
|
||||||
<div class="mb-1">{{ t('workflow.task.info.progress') }}</div>
|
class="workflow-task-card__execution-status"
|
||||||
<div class="d-flex align-center gap-5">
|
:class="executionStatus.color ? `text-${executionStatus.color}` : 'text-medium-emphasis'"
|
||||||
<div class="flex-grow-1">
|
:title="executionStatus.text"
|
||||||
<VProgressLinear color="info" rounded :model-value="resolveProgress(workflow)" />
|
>
|
||||||
</div>
|
<VIcon :icon="executionStatus.icon" size="small" />
|
||||||
<span> {{ resolveProgress(workflow) }}% </span>
|
<span>{{ executionStatus.text }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-x-3" v-if="workflow?.result">
|
</VCardText>
|
||||||
<div class="flex-1">
|
|
||||||
<div class="mb-1">{{ t('workflow.task.info.error') }}</div>
|
|
||||||
<div class="text-error">{{ workflow?.result }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
</VCard>
|
||||||
</div>
|
</div>
|
||||||
</VHover>
|
</VHover>
|
||||||
@@ -378,4 +537,218 @@ const resolveProgress = (item: Workflow) => {
|
|||||||
.workflow-task-card-hover-area {
|
.workflow-task-card-hover-area {
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-task-card {
|
||||||
|
--workflow-status-rgb: var(--v-theme-info);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-info);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
min-block-size: 226px;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-primary {
|
||||||
|
--workflow-status-rgb: var(--v-theme-primary);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-secondary {
|
||||||
|
--workflow-status-rgb: var(--v-theme-secondary);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-info {
|
||||||
|
--workflow-status-rgb: var(--v-theme-info);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-success {
|
||||||
|
--workflow-status-rgb: var(--v-theme-success);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-warning {
|
||||||
|
--workflow-status-rgb: var(--v-theme-warning);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card--status-error {
|
||||||
|
--workflow-status-rgb: var(--v-theme-error);
|
||||||
|
--workflow-status-on-rgb: var(--v-theme-on-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
border-block-end: 1px solid rgba(var(--workflow-status-on-rgb), 0.18);
|
||||||
|
background: linear-gradient(
|
||||||
|
118deg,
|
||||||
|
color-mix(in srgb, rgb(var(--workflow-status-rgb)) 88%, rgb(var(--v-theme-on-surface)) 12%) 0%,
|
||||||
|
rgb(var(--workflow-status-rgb)) 52%,
|
||||||
|
color-mix(in srgb, rgb(var(--workflow-status-rgb)) 78%, rgb(var(--v-theme-surface)) 22%) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__trigger-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgb(var(--workflow-status-on-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__title {
|
||||||
|
display: -webkit-box;
|
||||||
|
min-inline-size: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: rgb(var(--workflow-status-on-rgb));
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px !important;
|
||||||
|
letter-spacing: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__trigger-text {
|
||||||
|
min-inline-size: 0;
|
||||||
|
margin-block-start: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: rgba(var(--workflow-status-on-rgb), 0.78);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 16px !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__menu {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgb(var(--workflow-status-on-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__body {
|
||||||
|
display: flex;
|
||||||
|
min-inline-size: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 11px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__status-row {
|
||||||
|
display: flex;
|
||||||
|
min-inline-size: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__metric {
|
||||||
|
display: grid;
|
||||||
|
min-inline-size: 0;
|
||||||
|
gap: 1px;
|
||||||
|
padding-inline-end: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__metric + .workflow-task-card__metric {
|
||||||
|
padding-inline: 10px 0;
|
||||||
|
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__metric span,
|
||||||
|
.workflow-task-card__progress-label span {
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__metric strong,
|
||||||
|
.workflow-task-card__progress-label strong {
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__progress {
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__progress-label {
|
||||||
|
display: flex;
|
||||||
|
min-inline-size: 0;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-track {
|
||||||
|
display: flex;
|
||||||
|
min-inline-size: 0;
|
||||||
|
gap: 4px;
|
||||||
|
margin-block-start: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-segment {
|
||||||
|
block-size: 6px;
|
||||||
|
min-inline-size: 2px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
border-radius: var(--app-control-radius);
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-segment--complete {
|
||||||
|
background: rgb(var(--workflow-status-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-segment--active {
|
||||||
|
background: rgba(var(--workflow-status-rgb), 0.22);
|
||||||
|
box-shadow: inset 0 0 0 1px rgb(var(--workflow-status-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-segment--failed {
|
||||||
|
background: rgb(var(--v-theme-error));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__action-segment--empty {
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__execution-status {
|
||||||
|
display: flex;
|
||||||
|
min-inline-size: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin-block-start: auto;
|
||||||
|
padding-block-start: 9px;
|
||||||
|
border-block-start: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__execution-status .v-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__execution-status span {
|
||||||
|
min-inline-size: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 599.98px) {
|
||||||
|
.workflow-task-card {
|
||||||
|
min-block-size: 222px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__header,
|
||||||
|
.workflow-task-card__body {
|
||||||
|
padding-inline: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-task-card__body {
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||||
|
import type { Workflow } from '@/api/types'
|
||||||
|
import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
|
||||||
|
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||||
|
import { renderWithProviders } from '@tests/support/render'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
apiDelete: vi.fn(),
|
||||||
|
apiPost: vi.fn(),
|
||||||
|
confirm: vi.fn(),
|
||||||
|
openSharedDialog: vi.fn(),
|
||||||
|
toastError: vi.fn(),
|
||||||
|
toastSuccess: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/api', () => ({
|
||||||
|
default: {
|
||||||
|
delete: (...args: unknown[]) => mocks.apiDelete(...args),
|
||||||
|
post: (...args: unknown[]) => mocks.apiPost(...args),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-toastification', () => ({
|
||||||
|
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useConfirm', () => ({
|
||||||
|
useConfirm: () => mocks.confirm,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useSharedDialog', () => ({
|
||||||
|
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createWorkflow(overrides: Partial<Workflow> = {}): Workflow {
|
||||||
|
return {
|
||||||
|
actions: [
|
||||||
|
{ id: 'scan', name: '扫描目录', type: 'ScanFile' },
|
||||||
|
{ id: 'scrape', name: '刮削文件', type: 'ScrapeFile' },
|
||||||
|
{ id: 'transfer', name: '整理文件', type: 'TransferFile' },
|
||||||
|
],
|
||||||
|
current_action: undefined,
|
||||||
|
execution_state: {},
|
||||||
|
id: 'workflow-1',
|
||||||
|
name: '扫描和刮削',
|
||||||
|
run_count: 0,
|
||||||
|
state: 'W',
|
||||||
|
trigger_type: 'manual',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderCard(workflowOverrides: Partial<Workflow> = {}) {
|
||||||
|
return renderWithProviders(WorkflowTaskCard, {
|
||||||
|
props: {
|
||||||
|
eventTypes: [{ title: '下载完成', value: 'download.completed' }],
|
||||||
|
workflow: createWorkflow(workflowOverrides),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WorkflowTaskCard redesign', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.apiDelete.mockReset()
|
||||||
|
mocks.apiPost.mockReset()
|
||||||
|
mocks.confirm.mockReset()
|
||||||
|
mocks.openSharedDialog.mockReset()
|
||||||
|
mocks.toastError.mockReset()
|
||||||
|
mocks.toastSuccess.mockReset()
|
||||||
|
mocks.apiPost.mockResolvedValue({ success: true })
|
||||||
|
mocks.confirm.mockResolvedValue(true)
|
||||||
|
mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maps the generated card icon to the workflow trigger type', async () => {
|
||||||
|
const { container, rerender } = await renderCard()
|
||||||
|
|
||||||
|
expect(container.querySelector('[data-workflow-trigger-icon="mdi-hand-pointing-up"]')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('手动')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await rerender({
|
||||||
|
eventTypes: [{ title: '下载完成', value: 'download.completed' }],
|
||||||
|
workflow: createWorkflow({ timer: '10 * * * *', trigger_type: 'timer' }),
|
||||||
|
})
|
||||||
|
expect(container.querySelector('[data-workflow-trigger-icon="mdi-clock-outline"]')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('10 * * * *')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await rerender({
|
||||||
|
eventTypes: [{ title: '下载完成', value: 'download.completed' }],
|
||||||
|
workflow: createWorkflow({ event_type: 'download.completed', trigger_type: 'event' }),
|
||||||
|
})
|
||||||
|
expect(container.querySelector('[data-workflow-trigger-icon="mdi-calendar-check-outline"]')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('下载完成')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['W', 'warning', '待执行'],
|
||||||
|
['R', 'primary', '运行中'],
|
||||||
|
['S', 'success', '成功'],
|
||||||
|
['P', 'secondary', '暂停'],
|
||||||
|
['F', 'error', '失败'],
|
||||||
|
] as const)('maps %s to the semantic %s status color', async (state, color, label) => {
|
||||||
|
const { container } = await renderCard({ state })
|
||||||
|
|
||||||
|
expect(container.querySelector('.workflow-task-card')).toHaveClass(`workflow-task-card--status-${color}`)
|
||||||
|
expect(container.querySelector('.v-chip')).toHaveTextContent(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders structured node states as segmented action progress', async () => {
|
||||||
|
const { container } = await renderCard({
|
||||||
|
execution_state: {
|
||||||
|
nodes: {
|
||||||
|
scan: { state: 'success' },
|
||||||
|
scrape: { state: 'skipped' },
|
||||||
|
transfer: { state: 'running' },
|
||||||
|
},
|
||||||
|
runtime: { finished_actions: 2 },
|
||||||
|
},
|
||||||
|
state: 'R',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('2 / 3')).toBeInTheDocument()
|
||||||
|
expect(container.querySelectorAll('.workflow-task-card__action-segment--complete')).toHaveLength(2)
|
||||||
|
expect(container.querySelectorAll('.workflow-task-card__action-segment--active')).toHaveLength(1)
|
||||||
|
expect(screen.getByText('正在执行 整理文件')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('progressbar', { name: '动作进度: 2 / 3' })).toHaveAttribute('aria-valuenow', '2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to unique legacy current-action ids and clamps the count', async () => {
|
||||||
|
const { container } = await renderCard({ current_action: ',scan,,scrape,scan,unknown,', state: 'P' })
|
||||||
|
|
||||||
|
expect(screen.getByText('2 / 3')).toBeInTheDocument()
|
||||||
|
expect(container.querySelectorAll('.workflow-task-card__action-segment--complete')).toHaveLength(2)
|
||||||
|
expect(container.querySelectorAll('.workflow-task-card__action-segment--pending')).toHaveLength(1)
|
||||||
|
expect(screen.getByText('上次执行尚未完成')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows failure, last-run and never-run execution details at the bottom', async () => {
|
||||||
|
const lastTime = '2026-08-03 08:00:00'
|
||||||
|
const { rerender } = await renderCard({ result: '目录无访问权限', state: 'F' })
|
||||||
|
|
||||||
|
expect(screen.getByText('目录无访问权限')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await rerender({
|
||||||
|
eventTypes: [],
|
||||||
|
workflow: createWorkflow({ last_time: lastTime, state: 'S' }),
|
||||||
|
})
|
||||||
|
expect(screen.getByText(`上次执行 ${formatDateDifference(lastTime)}`)).toBeInTheDocument()
|
||||||
|
|
||||||
|
await rerender({ eventTypes: [], workflow: createWorkflow() })
|
||||||
|
expect(screen.getByText('从未执行')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps card navigation and enable controls as separate actions', async () => {
|
||||||
|
const { container } = await renderCard({ state: 'P' })
|
||||||
|
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: '启用' }))
|
||||||
|
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledWith('workflow/workflow-1/start'))
|
||||||
|
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
await fireEvent.click(container.querySelector('.workflow-task-card') as Element)
|
||||||
|
expect(mocks.openSharedDialog).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.openSharedDialog.mock.calls[0][1]).toEqual({ workflow: expect.objectContaining({ id: 'workflow-1' }) })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('derives all custom card colors and geometry from global theme tokens', () => {
|
||||||
|
const source = readFileSync('src/components/cards/WorkflowTaskCard.vue', 'utf8')
|
||||||
|
|
||||||
|
expect(source).toContain('var(--v-theme-primary)')
|
||||||
|
expect(source).toContain('var(--v-theme-info)')
|
||||||
|
expect(source).toContain('var(--v-theme-success)')
|
||||||
|
expect(source).toContain('var(--v-theme-warning)')
|
||||||
|
expect(source).toContain('var(--v-theme-error)')
|
||||||
|
expect(source).toContain('var(--v-theme-on-surface)')
|
||||||
|
expect(source).toContain('var(--app-control-radius)')
|
||||||
|
expect(source).toContain('linear-gradient(')
|
||||||
|
expect(source).not.toMatch(/#[\da-f]{3,8}\b/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -802,6 +802,7 @@ export default {
|
|||||||
edit: 'Edit Task',
|
edit: 'Edit Task',
|
||||||
editFlow: 'Edit Flow',
|
editFlow: 'Edit Flow',
|
||||||
share: 'Share',
|
share: 'Share',
|
||||||
|
moreActions: 'More actions',
|
||||||
continue: 'Continue',
|
continue: 'Continue',
|
||||||
restart: 'Restart',
|
restart: 'Restart',
|
||||||
run: 'Run Now',
|
run: 'Run Now',
|
||||||
@@ -824,7 +825,7 @@ export default {
|
|||||||
running: 'Running',
|
running: 'Running',
|
||||||
failed: 'Failed',
|
failed: 'Failed',
|
||||||
paused: 'Paused',
|
paused: 'Paused',
|
||||||
waiting: 'Waiting',
|
waiting: 'Pending',
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
trigger: 'Trigger',
|
trigger: 'Trigger',
|
||||||
@@ -833,8 +834,15 @@ export default {
|
|||||||
actionCount: 'Action Count',
|
actionCount: 'Action Count',
|
||||||
runCount: 'Run Count',
|
runCount: 'Run Count',
|
||||||
progress: 'Progress',
|
progress: 'Progress',
|
||||||
|
actionProgress: 'Action Progress',
|
||||||
error: 'Error Message',
|
error: 'Error Message',
|
||||||
manualTrigger: 'Manual',
|
manualTrigger: 'Manual',
|
||||||
|
lastExecuted: 'Last run {time}',
|
||||||
|
neverExecuted: 'Never run',
|
||||||
|
executionIncomplete: 'Previous run is incomplete',
|
||||||
|
executingAction: 'Running {name}',
|
||||||
|
preparingAction: 'Preparing action {current}',
|
||||||
|
noActions: 'No actions',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scanFile: {
|
scanFile: {
|
||||||
|
|||||||
@@ -792,6 +792,7 @@ export default {
|
|||||||
edit: '编辑任务',
|
edit: '编辑任务',
|
||||||
editFlow: '编辑流程',
|
editFlow: '编辑流程',
|
||||||
share: '分享',
|
share: '分享',
|
||||||
|
moreActions: '更多操作',
|
||||||
continue: '继续执行',
|
continue: '继续执行',
|
||||||
restart: '重新执行',
|
restart: '重新执行',
|
||||||
run: '立即执行',
|
run: '立即执行',
|
||||||
@@ -814,7 +815,7 @@ export default {
|
|||||||
running: '运行中',
|
running: '运行中',
|
||||||
failed: '失败',
|
failed: '失败',
|
||||||
paused: '暂停',
|
paused: '暂停',
|
||||||
waiting: '等待',
|
waiting: '待执行',
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
trigger: '触发方式',
|
trigger: '触发方式',
|
||||||
@@ -823,8 +824,15 @@ export default {
|
|||||||
actionCount: '动作数',
|
actionCount: '动作数',
|
||||||
runCount: '已执行次数',
|
runCount: '已执行次数',
|
||||||
progress: '进度',
|
progress: '进度',
|
||||||
|
actionProgress: '动作进度',
|
||||||
error: '错误信息',
|
error: '错误信息',
|
||||||
manualTrigger: '手动',
|
manualTrigger: '手动',
|
||||||
|
lastExecuted: '上次执行 {time}',
|
||||||
|
neverExecuted: '从未执行',
|
||||||
|
executionIncomplete: '上次执行尚未完成',
|
||||||
|
executingAction: '正在执行 {name}',
|
||||||
|
preparingAction: '正在准备第 {current} 个动作',
|
||||||
|
noActions: '暂无动作',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scanFile: {
|
scanFile: {
|
||||||
|
|||||||
@@ -792,6 +792,7 @@ export default {
|
|||||||
edit: '編輯任務',
|
edit: '編輯任務',
|
||||||
editFlow: '編輯流程',
|
editFlow: '編輯流程',
|
||||||
share: '分享',
|
share: '分享',
|
||||||
|
moreActions: '更多操作',
|
||||||
continue: '繼續',
|
continue: '繼續',
|
||||||
restart: '重新開始',
|
restart: '重新開始',
|
||||||
run: '立即執行',
|
run: '立即執行',
|
||||||
@@ -814,7 +815,7 @@ export default {
|
|||||||
running: '執行中',
|
running: '執行中',
|
||||||
failed: '失敗',
|
failed: '失敗',
|
||||||
paused: '已暫停',
|
paused: '已暫停',
|
||||||
waiting: '等待中',
|
waiting: '待執行',
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
trigger: '觸發方式',
|
trigger: '觸發方式',
|
||||||
@@ -823,8 +824,15 @@ export default {
|
|||||||
actionCount: '動作數量',
|
actionCount: '動作數量',
|
||||||
runCount: '執行次數',
|
runCount: '執行次數',
|
||||||
progress: '進度',
|
progress: '進度',
|
||||||
|
actionProgress: '動作進度',
|
||||||
error: '錯誤訊息',
|
error: '錯誤訊息',
|
||||||
manualTrigger: '手動',
|
manualTrigger: '手動',
|
||||||
|
lastExecuted: '上次執行 {time}',
|
||||||
|
neverExecuted: '從未執行',
|
||||||
|
executionIncomplete: '上次執行尚未完成',
|
||||||
|
executingAction: '正在執行 {name}',
|
||||||
|
preparingAction: '正在準備第 {current} 個動作',
|
||||||
|
noActions: '暫無動作',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scanFile: {
|
scanFile: {
|
||||||
|
|||||||
Reference in New Issue
Block a user