diff --git a/src/components/cards/WorkflowTaskCard.vue b/src/components/cards/WorkflowTaskCard.vue index d8496c8f..086f8f6c 100644 --- a/src/components/cards/WorkflowTaskCard.vue +++ b/src/components/cards/WorkflowTaskCard.vue @@ -158,7 +158,6 @@ async function handleReset(item: Workflow) { } type WorkflowStatusColor = 'error' | 'info' | 'primary' | 'secondary' | 'success' | 'warning' -type WorkflowActionSegmentState = 'active' | 'complete' | 'failed' | 'pending' interface WorkflowActionDisplay { id?: string @@ -243,20 +242,22 @@ const finishedActionCount = computed(() => { 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 +// 全局执行进度(%):优先使用后端下发的整体进度,缺失时按已完成动作数兜底计算 +const globalProgressPercent = computed(() => { + const runtimeProgress = Number(props.workflow.execution_state?.runtime?.progress) + if (Number.isFinite(runtimeProgress)) { + return Math.min(Math.max(Math.round(runtimeProgress), 0), 100) + } - 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' + if (totalActionCount.value <= 0) return 0 - return 'pending' - }) + return Math.min(100, Math.round((finishedActionCount.value / totalActionCount.value) * 100)) +}) + +// 已执行次数:大于 0 时追加在执行状态行后展示 +const runCountText = computed(() => { + const count = props.workflow.run_count || 0 + return count > 0 ? ` · ${t('workflow.task.info.runCount', { count })}` : '' }) const runningActionName = computed(() => { @@ -279,21 +280,7 @@ const executionStatus = computed(() => { } } - 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'), - } + return null } if (props.workflow.state === 'F') { @@ -308,7 +295,7 @@ const executionStatus = computed(() => { return { color: undefined, icon: 'mdi-history', - text: t('workflow.task.info.lastExecuted', { time: formatDateDifference(props.workflow.last_time) }), + text: `${t('workflow.task.info.lastExecuted', { time: formatDateDifference(props.workflow.last_time) })}${runCountText.value}`, } } @@ -460,44 +447,8 @@ const executionStatus = computed(() => { -
-
- {{ t('workflow.task.info.actionCount') }} - {{ totalActionCount }} -
-
- {{ t('workflow.task.info.runCount') }} - {{ workflow.run_count || 0 }} -
-
- -
-
- {{ t('workflow.task.info.actionProgress') }} - {{ finishedActionCount }} / {{ totalActionCount }} -
-
- - -
-
-
{ {{ executionStatus.text }}
+ + @@ -530,7 +492,7 @@ const executionStatus = computed(() => { --workflow-card-header-shadow: none; display: flex; - min-block-size: 226px; + min-block-size: 176px; flex-direction: column; overflow: hidden; } @@ -613,8 +575,8 @@ const executionStatus = computed(() => { min-inline-size: 0; flex: 1 1 auto; flex-direction: column; - gap: 10px; - padding: 11px 12px !important; + gap: 8px; + padding: 12px 12px 12px !important; } .workflow-task-card__status-row { @@ -625,77 +587,20 @@ const executionStatus = computed(() => { 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; + flex: 0 0 auto; + block-size: 5px; + inline-size: 100%; + overflow: hidden; + background: rgba(var(--v-theme-on-surface), 0.1); } -.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; +.workflow-task-card__progress-fill { + display: block; + block-size: 100%; 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); + transition: inline-size 0.3s ease; } .workflow-task-card__execution-status { @@ -704,8 +609,6 @@ const executionStatus = computed(() => { 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 { @@ -721,7 +624,7 @@ const executionStatus = computed(() => { @media (width <= 599.98px) { .workflow-task-card { - min-block-size: 222px; + min-block-size: 170px; } .workflow-task-card__header, @@ -730,7 +633,7 @@ const executionStatus = computed(() => { } .workflow-task-card__body { - gap: 9px; + gap: 7px; } } diff --git a/src/components/cards/__tests__/WorkflowTaskCard.spec.ts b/src/components/cards/__tests__/WorkflowTaskCard.spec.ts index be8b4dc3..1d14e5fe 100644 --- a/src/components/cards/__tests__/WorkflowTaskCard.spec.ts +++ b/src/components/cards/__tests__/WorkflowTaskCard.spec.ts @@ -108,7 +108,7 @@ describe('WorkflowTaskCard redesign', () => { expect(container.querySelector('.v-chip')).toHaveTextContent(label) }) - it('renders structured node states as segmented action progress', async () => { + it('renders the global runtime progress on the bottom progress bar', async () => { const { container } = await renderCard({ execution_state: { nodes: { @@ -116,27 +116,52 @@ describe('WorkflowTaskCard redesign', () => { scrape: { state: 'skipped' }, transfer: { state: 'running' }, }, - runtime: { finished_actions: 2 }, + runtime: { finished_actions: 2, progress: 66 }, }, 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') + const fill = container.querySelector('.workflow-task-card__progress-fill') as HTMLElement + expect(fill.style.inlineSize).toBe('66%') + expect(screen.getByRole('progressbar', { name: '扫描和刮削' })).toHaveAttribute('aria-valuenow', '66') }) - it('falls back to unique legacy current-action ids and clamps the count', async () => { + it('falls back to legacy current-action counts and clamps the progress', 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) + const fill = container.querySelector('.workflow-task-card__progress-fill') as HTMLElement + expect(fill.style.inlineSize).toBe('67%') expect(screen.getByText('上次执行尚未完成')).toBeInTheDocument() }) + it('shows the run count after the last execution time', async () => { + const lastTime = '2026-08-03 08:00:00' + const { rerender } = await renderCard({ last_time: lastTime, run_count: 3, state: 'S' }) + + expect(screen.getByText(`上次执行 ${formatDateDifference(lastTime)} · 已执行 3 次`)).toBeInTheDocument() + + await rerender({ + eventTypes: [], + workflow: createWorkflow({ last_time: lastTime, run_count: 0, state: 'S' }), + }) + expect(screen.getByText(`上次执行 ${formatDateDifference(lastTime)}`)).toBeInTheDocument() + }) + + it('always renders the progress bar at the bottom of the card', async () => { + const { container, rerender } = await renderCard({ state: 'W' }) + + const idleBar = container.querySelector('.workflow-task-card__progress') as HTMLElement + expect(idleBar).toBeInTheDocument() + expect(idleBar).toHaveAttribute('aria-valuenow', '0') + + await rerender({ + eventTypes: [], + workflow: createWorkflow({ execution_state: { runtime: { progress: 100 } }, state: 'S' }), + }) + expect(container.querySelector('.workflow-task-card__progress')).toHaveAttribute('aria-valuenow', '100') + }) + 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' }) diff --git a/src/components/dialog/WorkflowActionsDialog.vue b/src/components/dialog/WorkflowActionsDialog.vue index 03a9b2ad..72d6b10e 100644 --- a/src/components/dialog/WorkflowActionsDialog.vue +++ b/src/components/dialog/WorkflowActionsDialog.vue @@ -589,12 +589,17 @@ const isMacOS = computed(() => { } .workflow-edge-panel { + --workflow-edge-panel-background: rgb(var(--v-theme-surface)); + --workflow-edge-panel-backdrop-filter: none; + position: absolute; z-index: 120; display: flex; flex-direction: column; padding: 16px; - background-color: rgb(var(--v-theme-surface)); + -webkit-backdrop-filter: var(--workflow-edge-panel-backdrop-filter); + backdrop-filter: var(--workflow-edge-panel-backdrop-filter); + background-color: var(--workflow-edge-panel-background); gap: 14px; inline-size: min(360px, calc(100vw - 32px)); inset-block-start: 20px; diff --git a/src/components/workflow/WorkflowSidebar.vue b/src/components/workflow/WorkflowSidebar.vue index 56bb9f9a..c2b9142f 100644 --- a/src/components/workflow/WorkflowSidebar.vue +++ b/src/components/workflow/WorkflowSidebar.vue @@ -187,10 +187,21 @@ onMounted(() => { @use 'sass:color'; .workflow-sidebar { + --workflow-sidebar-background: rgb(var(--v-theme-background)); + --workflow-sidebar-backdrop-filter: none; + --workflow-sidebar-header-background: rgb(var(--v-theme-background)); + --workflow-sidebar-footer-background: rgb(var(--v-theme-background)); + --workflow-sidebar-border-color: rgba(var(--v-theme-on-background), 0.06); + --workflow-sidebar-card-background: rgb(var(--v-theme-surface-variant)); + --workflow-sidebar-card-background-hover: rgb(var(--v-theme-surface-variant)); + --workflow-sidebar-desc-color: rgba(var(--v-theme-on-background), 0.55); + position: absolute; z-index: 100; overflow: hidden; - background-color: rgb(var(--v-theme-background)); + -webkit-backdrop-filter: var(--workflow-sidebar-backdrop-filter); + backdrop-filter: var(--workflow-sidebar-backdrop-filter); + background-color: var(--workflow-sidebar-background); box-shadow: 0 0 15px rgba(0, 0, 0, 8%); inline-size: 280px; inset-block: 0; @@ -220,8 +231,8 @@ onMounted(() => { .sidebar-header { flex-shrink: 0; padding: 16px; - background-color: rgb(var(--v-theme-background)); - border-block-end: 1px solid rgba(var(--v-theme-on-background), 0.06); + background-color: var(--workflow-sidebar-header-background); + border-block-end: 1px solid var(--workflow-sidebar-border-color); .header-content { position: relative; @@ -281,11 +292,11 @@ onMounted(() => { display: flex; align-items: center; padding: 10px; - background-color: rgb(var(--v-theme-surface-variant)); + background-color: var(--workflow-sidebar-card-background); transition: all 0.2s ease; &:hover { - background-color: rgb(var(--v-theme-surface-variant)); + background-color: var(--workflow-sidebar-card-background-hover); transform: translateY(-2px); } } @@ -318,7 +329,7 @@ onMounted(() => { .component-desc { overflow: hidden; - color: #71717a; + color: var(--workflow-sidebar-desc-color); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; @@ -327,8 +338,8 @@ onMounted(() => { .sidebar-footer { flex-shrink: 0; padding: 12px; - background-color: rgb(var(--v-theme-background)); - border-block-start: 1px solid rgba(0, 0, 0, 6%); + background-color: var(--workflow-sidebar-footer-background); + border-block-start: 1px solid var(--workflow-sidebar-border-color); .drag-btn { background-color: rgb(var(--v-theme-primary)); diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 7837dfb3..36bcf2c4 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -948,16 +948,14 @@ export default { timer: 'Timer', status: 'Status', actionCount: 'Action Count', - runCount: 'Run Count', + runCount: 'Run {count} times', progress: 'Progress', - actionProgress: 'Action Progress', error: 'Error Message', manualTrigger: 'Manual', lastExecuted: 'Last run {time}', neverExecuted: 'Never run', executionIncomplete: 'Previous run is incomplete', executingAction: 'Running {name}', - preparingAction: 'Preparing action {current}', noActions: 'No actions', }, }, diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 593c4dea..c1cd0d9d 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -937,16 +937,14 @@ export default { timer: '定时', status: '状态', actionCount: '动作数', - runCount: '已执行次数', + runCount: '已执行 {count} 次', progress: '进度', - actionProgress: '动作进度', error: '错误信息', manualTrigger: '手动', lastExecuted: '上次执行 {time}', neverExecuted: '从未执行', executionIncomplete: '上次执行尚未完成', executingAction: '正在执行 {name}', - preparingAction: '正在准备第 {current} 个动作', noActions: '暂无动作', }, }, diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 7c80f254..fb5dae41 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -937,16 +937,14 @@ export default { timer: '定時器', status: '狀態', actionCount: '動作數量', - runCount: '執行次數', + runCount: '已執行 {count} 次', progress: '進度', - actionProgress: '動作進度', error: '錯誤訊息', manualTrigger: '手動', lastExecuted: '上次執行 {time}', neverExecuted: '從未執行', executionIncomplete: '上次執行尚未完成', executingAction: '正在執行 {name}', - preparingAction: '正在準備第 {current} 個動作', noActions: '暫無動作', }, }, diff --git a/src/styles/themes/glass.scss b/src/styles/themes/glass.scss index e6952229..e5539e15 100644 --- a/src/styles/themes/glass.scss +++ b/src/styles/themes/glass.scss @@ -1008,6 +1008,24 @@ html[data-theme='glass'] { --workflow-card-header-shadow: inset 0 1px 0 var(--glass-highlight), inset 0 -1px 0 rgba(2, 6, 16, 14%); } + // 工作流编辑器组件选择栏 + .workflow-sidebar { + --workflow-sidebar-background: var(--glass-surface); + --workflow-sidebar-backdrop-filter: var(--glass-surface-backdrop-filter); + --workflow-sidebar-header-background: transparent; + --workflow-sidebar-footer-background: transparent; + --workflow-sidebar-border-color: var(--glass-border); + --workflow-sidebar-card-background: var(--glass-control); + --workflow-sidebar-card-background-hover: var(--glass-button-surface-hover); + --workflow-sidebar-desc-color: var(--glass-control-placeholder-color); + } + + // 工作流编辑器流程边设置面板 + .workflow-edge-panel { + --workflow-edge-panel-background: var(--glass-surface); + --workflow-edge-panel-backdrop-filter: var(--glass-surface-backdrop-filter); + } + // 分享卡保留原随机渐变的色相,以半透明染色、吸光和镜面层组成有色玻璃。 .workflow-share-card { --workflow-share-glass-start-opacity: calc(0.18 + var(--glass-tint-density, 0.65) * 0.38); diff --git a/src/styles/themes/transparent.scss b/src/styles/themes/transparent.scss index 58bc13f1..77fb921f 100644 --- a/src/styles/themes/transparent.scss +++ b/src/styles/themes/transparent.scss @@ -3,7 +3,7 @@ /* stylelint-disable scss/at-rule-no-unknown */ // 透明主题专用样式 -html[data-theme="transparent"] { +html[data-theme='transparent'] { // 定义透明度变量 --transparent-opacity: 0.3; --transparent-opacity-light: 0.2; @@ -21,10 +21,13 @@ html[data-theme="transparent"] { --app-grouped-list-icon-opacity: 0.88; // 应用、布局、主内容区域 - .v-application, .v-layout, .v-main, .layout-page-content { + .v-application, + .v-layout, + .v-main, + .layout-page-content { background: transparent; } - + // 侧边导航栏 .layout-vertical-nav { backdrop-filter: blur(var(--transparent-blur-heavy)); @@ -37,7 +40,7 @@ html[data-theme="transparent"] { backdrop-filter: blur(var(--transparent-blur)); background-color: rgba(var(--v-theme-surface), var(--transparent-opacity)); } - + // 卡片 .v-card:not(.no-blur) { backdrop-filter: blur(var(--transparent-blur)); @@ -67,6 +70,24 @@ html[data-theme="transparent"] { --workflow-card-header-shadow: inset 0 1px 0 rgba(var(--v-theme-on-surface), 0.14); } + // 工作流编辑器组件选择栏 + .workflow-sidebar { + --workflow-sidebar-background: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)); + --workflow-sidebar-backdrop-filter: blur(var(--transparent-blur-heavy)); + --workflow-sidebar-header-background: transparent; + --workflow-sidebar-footer-background: transparent; + --workflow-sidebar-border-color: rgba(var(--v-theme-on-surface), 0.08); + --workflow-sidebar-card-background: rgba(var(--v-theme-surface), var(--transparent-opacity)); + --workflow-sidebar-card-background-hover: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)); + --workflow-sidebar-desc-color: rgba(var(--v-theme-on-surface), 0.55); + } + + // 工作流编辑器流程边设置面板 + .workflow-edge-panel { + --workflow-edge-panel-background: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)); + --workflow-edge-panel-backdrop-filter: blur(var(--transparent-blur-heavy)); + } + // 订阅文件弹窗使用独立的玻璃表面参数,避免组件内通过全局选择器穿透主题。 .subscribe-files-dialog { --sfd-accent-opacity: 0.12; @@ -114,7 +135,7 @@ html[data-theme="transparent"] { backdrop-filter: blur(var(--transparent-blur)); background-color: rgba(var(--v-theme-surface), var(--transparent-opacity)); } - + // 表格 .v-table { border-radius: 0; @@ -124,7 +145,7 @@ html[data-theme="transparent"] { background-color: rgba(var(--v-theme-surface), var(--transparent-opacity)); } } - + // 页脚 .v-footer { backdrop-filter: blur(var(--transparent-blur)); @@ -136,17 +157,17 @@ html[data-theme="transparent"] { backdrop-filter: blur(var(--transparent-blur)); background-color: rgba(var(--v-theme-surface), var(--transparent-opacity)); } - + // 页面容器 .layout-content-wrapper { background: transparent; } - + // 无内容区域的背景设为透明 .page-content-container { background: transparent; } - + // 对话框和菜单蒙层样式 .v-overlay__scrim { background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)); @@ -167,7 +188,7 @@ html[data-theme="transparent"] { .v-field { background-color: rgba(var(--v-theme-surface), 0); } - + // 弹出层内容 .v-overlay__content { border-radius: var(--app-surface-radius); @@ -206,11 +227,7 @@ html[data-theme="transparent"] { .theme-customizer-shadow-slider, .theme-customizer-shadow-slider__sample { background: - linear-gradient( - 180deg, - rgba(var(--v-theme-on-surface), 0.02), - rgba(var(--v-theme-on-surface), 0.05) - ), + linear-gradient(180deg, rgba(var(--v-theme-on-surface), 0.02), rgba(var(--v-theme-on-surface), 0.05)), rgba(var(--v-theme-surface), var(--transparent-opacity-light)); } @@ -259,9 +276,9 @@ html[data-theme="transparent"] { ), rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important; } -} +} -html[data-theme="transparent"].transparent-background-blur-disabled { +html[data-theme='transparent'].transparent-background-blur-disabled { .global-blur-layer, .background-image.active { backdrop-filter: none; @@ -270,7 +287,7 @@ html[data-theme="transparent"].transparent-background-blur-disabled { } } -html[data-theme="transparent"].transparent-glass-lightweight { +html[data-theme='transparent'].transparent-glass-lightweight { --app-grouped-list-backdrop-filter: none; .global-blur-layer { @@ -334,7 +351,7 @@ html[data-theme="transparent"].transparent-glass-lightweight { } } -html[data-theme="transparent"].transparent-glass-realtime { +html[data-theme='transparent'].transparent-glass-realtime { --app-grouped-list-backdrop-filter: none; .background-image.active { @@ -379,7 +396,7 @@ html[data-theme="transparent"].transparent-glass-realtime { } } -html[data-theme="transparent"].transparent-blur-disabled { +html[data-theme='transparent'].transparent-blur-disabled { --app-grouped-list-backdrop-filter: none; .layout-vertical-nav,