mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(ui/workflows): canvas node click + locale-aware node labels
This commit is contained in:
parent
844a64ad37
commit
12bd05382e
@ -1,12 +1,18 @@
|
|||||||
<template>
|
<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" />
|
<Handle type="target" :position="targetPosition" />
|
||||||
<div class="step-band" />
|
<div class="step-band" />
|
||||||
<div class="step-body">
|
<div class="step-body">
|
||||||
<div class="step-header">
|
<div class="step-header">
|
||||||
<span class="step-icon" v-html="modeIcon" />
|
<span class="step-icon" v-html="modeIcon" />
|
||||||
<span class="step-name" :title="data.name">{{ data.name }}</span>
|
<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>
|
||||||
<div v-if="agentSlug" class="step-row">
|
<div v-if="agentSlug" class="step-row">
|
||||||
<span class="step-row-label">{{ t('workflows.canvas.nodeAgent') }}</span>
|
<span class="step-row-label">{{ t('workflows.canvas.nodeAgent') }}</span>
|
||||||
@ -37,7 +43,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Handle, Position, type NodeProps } from '@vue-flow/core'
|
import { Handle, Position, type NodeProps } from '@vue-flow/core'
|
||||||
import type { StepNodeData } from '@/composables/useWorkflowGraph'
|
import type { StepNodeData } from '@/composables/useWorkflowGraph'
|
||||||
@ -45,6 +51,18 @@ import type { StepNodeData } from '@/composables/useWorkflowGraph'
|
|||||||
const props = defineProps<NodeProps<StepNodeData>>()
|
const props = defineProps<NodeProps<StepNodeData>>()
|
||||||
const { t } = useI18n()
|
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);
|
// 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.
|
// we read them off the node so the arrows attach in the right place.
|
||||||
const sourcePosition = computed(() => props.sourcePosition ?? Position.Right)
|
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)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -179,10 +206,15 @@ const modeIcon = computed(() => ICONS[data.value.modeType] ?? ICONS.sequential)
|
|||||||
}
|
}
|
||||||
.step-row-label {
|
.step-row-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
opacity: 0.55;
|
opacity: 0.6;
|
||||||
flex: 0 0 auto;
|
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 {
|
.step-row-value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -79,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useI18n } from 'vue-i18n'
|
||||||
import { VueFlow, type Node } from '@vue-flow/core'
|
import { VueFlow, type Node } from '@vue-flow/core'
|
||||||
import { Background } from '@vue-flow/background'
|
import { Background } from '@vue-flow/background'
|
||||||
@ -139,11 +139,24 @@ const miniNodeColor = (node: Node<StepNodeData>) => {
|
|||||||
}
|
}
|
||||||
const miniNodeStrokeColor = () => 'transparent'
|
const miniNodeStrokeColor = () => 'transparent'
|
||||||
|
|
||||||
function handleNodeClick(payload: { node: Node<StepNodeData> }) {
|
// Vue-flow's `@node-click` event signature has shifted across minor
|
||||||
emit('select-step', payload?.node?.data ?? null)
|
// 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() {
|
function handlePaneClick() {
|
||||||
emit('select-step', null)
|
selectStep(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset direction toggle highlight when external code resets the model.
|
// Reset direction toggle highlight when external code resets the model.
|
||||||
|
|||||||
@ -1960,6 +1960,15 @@ export default {
|
|||||||
nodeMergeStrategy: 'Merge',
|
nodeMergeStrategy: 'Merge',
|
||||||
approvalDefault: 'manual',
|
approvalDefault: 'manual',
|
||||||
channelsEmpty: 'unspecified',
|
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: {
|
inspector: {
|
||||||
title: 'Step inspector',
|
title: 'Step inspector',
|
||||||
empty: 'Click a node on the canvas to inspect it.',
|
empty: 'Click a node on the canvas to inspect it.',
|
||||||
|
|||||||
@ -1965,13 +1965,22 @@ export default {
|
|||||||
parseError: 'JSON 解析失败:{msg}',
|
parseError: 'JSON 解析失败:{msg}',
|
||||||
fullscreenEnter: '全屏',
|
fullscreenEnter: '全屏',
|
||||||
fullscreenExit: '退出全屏',
|
fullscreenExit: '退出全屏',
|
||||||
nodeAgent: 'Agent',
|
nodeAgent: '智能体',
|
||||||
nodeExpression: '条件',
|
nodeExpression: '条件',
|
||||||
nodeApprovalKind: '审批',
|
nodeApprovalKind: '审批类型',
|
||||||
nodeChannels: '渠道',
|
nodeChannels: '渠道',
|
||||||
nodeMergeStrategy: '合并策略',
|
nodeMergeStrategy: '合并策略',
|
||||||
approvalDefault: '人工审批',
|
approvalDefault: '人工审批',
|
||||||
channelsEmpty: '未指定',
|
channelsEmpty: '未指定',
|
||||||
|
modeLabels: {
|
||||||
|
sequential: '顺序',
|
||||||
|
fan_out: '扇出',
|
||||||
|
collect: '汇聚',
|
||||||
|
conditional: '条件',
|
||||||
|
await_approval: '等待审批',
|
||||||
|
dispatch_channel: '分发渠道',
|
||||||
|
write_memory: '写入记忆',
|
||||||
|
},
|
||||||
inspector: {
|
inspector: {
|
||||||
title: '步骤详情',
|
title: '步骤详情',
|
||||||
empty: '点击画布上的节点查看详情。',
|
empty: '点击画布上的节点查看详情。',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user