fix(ui/workflows): canvas node click + locale-aware node labels

This commit is contained in:
matevip 2026-05-08 15:06:02 +08:00
parent 844a64ad37
commit 12bd05382e
4 changed files with 74 additions and 11 deletions

View File

@ -1,12 +1,18 @@
<template>
<div class="step-node" :class="`mode-${data.modeType}`" :data-selected="props.selected">
<div
class="step-node"
:class="`mode-${data.modeType}`"
:data-selected="props.selected"
@click.stop="handleClick"
@pointerdown.stop
>
<Handle type="target" :position="targetPosition" />
<div class="step-band" />
<div class="step-body">
<div class="step-header">
<span class="step-icon" v-html="modeIcon" />
<span class="step-name" :title="data.name">{{ data.name }}</span>
<span class="step-mode-tag">{{ data.modeType }}</span>
<span class="step-mode-tag" :title="data.modeType">{{ modeLabel }}</span>
</div>
<div v-if="agentSlug" class="step-row">
<span class="step-row-label">{{ t('workflows.canvas.nodeAgent') }}</span>
@ -37,7 +43,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { Handle, Position, type NodeProps } from '@vue-flow/core'
import type { StepNodeData } from '@/composables/useWorkflowGraph'
@ -45,6 +51,18 @@ import type { StepNodeData } from '@/composables/useWorkflowGraph'
const props = defineProps<NodeProps<StepNodeData>>()
const { t } = useI18n()
// The canvas wrapper provides a selection callback so we can fire a
// click straight up to the parent without depending on vue-flow's
// node-click event, which doesn't fire reliably across versions when
// the click lands on a custom-template inner element.
const selectStep = inject<(data: StepNodeData | null) => void>(
'selectStepCallback',
() => {}
)
function handleClick() {
selectStep(props.data as StepNodeData)
}
// Position values come from the parent canvas (LR or TB orientation);
// we read them off the node so the arrows attach in the right place.
const sourcePosition = computed(() => props.sourcePosition ?? Position.Right)
@ -96,6 +114,15 @@ const ICONS: Record<string, string> = {
}
const modeIcon = computed(() => ICONS[data.value.modeType] ?? ICONS.sequential)
// Render the mode tag with the locale-mapped label and fall back to
// the raw schema string for unknown / future modes so the tag never
// shows an empty pill.
const modeLabel = computed(() => {
const key = `workflows.canvas.modeLabels.${data.value.modeType}`
const localized = t(key, '')
return localized && localized !== key ? localized : data.value.modeType
})
</script>
<style scoped>
@ -179,10 +206,15 @@ const modeIcon = computed(() => ICONS[data.value.modeType] ?? ICONS.sequential)
}
.step-row-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.55;
opacity: 0.6;
flex: 0 0 auto;
/* English row labels are short and visually fine in uppercase, but
CJK glyphs look noisy when the locale flips them through
text-transform. Leave casing to the locale string. */
}
:lang(en) .step-row-label {
text-transform: uppercase;
}
.step-row-value {
flex: 1;

View File

@ -79,7 +79,7 @@
</template>
<script setup lang="ts">
import { computed, markRaw, onBeforeUnmount, ref, watch } from 'vue'
import { computed, markRaw, onBeforeUnmount, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { VueFlow, type Node } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
@ -139,11 +139,24 @@ const miniNodeColor = (node: Node<StepNodeData>) => {
}
const miniNodeStrokeColor = () => 'transparent'
function handleNodeClick(payload: { node: Node<StepNodeData> }) {
emit('select-step', payload?.node?.data ?? null)
// Vue-flow's `@node-click` event signature has shifted across minor
// versions and sometimes doesn't fire reliably for custom-node templates
// because the click can be swallowed by the inner DOM. Bypass it: we
// provide a selection callback to the StepNode via inject, and the node
// invokes it directly on its own click handler.
function selectStep(data: StepNodeData | null) {
emit('select-step', data)
}
provide('selectStepCallback', selectStep)
function handleNodeClick(payload: { node: Node<StepNodeData> } | unknown) {
// Backup path: if vue-flow does fire its own node-click event with the
// expected shape, route it through the same emit.
const node = (payload as { node?: Node<StepNodeData> })?.node
if (node?.data) selectStep(node.data)
}
function handlePaneClick() {
emit('select-step', null)
selectStep(null)
}
// Reset direction toggle highlight when external code resets the model.

View File

@ -1960,6 +1960,15 @@ export default {
nodeMergeStrategy: 'Merge',
approvalDefault: 'manual',
channelsEmpty: 'unspecified',
modeLabels: {
sequential: 'sequential',
fan_out: 'fan out',
collect: 'collect',
conditional: 'conditional',
await_approval: 'await approval',
dispatch_channel: 'dispatch',
write_memory: 'write memory',
},
inspector: {
title: 'Step inspector',
empty: 'Click a node on the canvas to inspect it.',

View File

@ -1965,13 +1965,22 @@ export default {
parseError: 'JSON 解析失败:{msg}',
fullscreenEnter: '全屏',
fullscreenExit: '退出全屏',
nodeAgent: 'Agent',
nodeAgent: '智能体',
nodeExpression: '条件',
nodeApprovalKind: '审批',
nodeApprovalKind: '审批类型',
nodeChannels: '渠道',
nodeMergeStrategy: '合并策略',
approvalDefault: '人工审批',
channelsEmpty: '未指定',
modeLabels: {
sequential: '顺序',
fan_out: '扇出',
collect: '汇聚',
conditional: '条件',
await_approval: '等待审批',
dispatch_channel: '分发渠道',
write_memory: '写入记忆',
},
inspector: {
title: '步骤详情',
empty: '点击画布上的节点查看详情。',