diff --git a/ui/src/locales/lang/en-US/workflow.ts b/ui/src/locales/lang/en-US/workflow.ts
index f322667c8a3..5e0ca57049b 100644
--- a/ui/src/locales/lang/en-US/workflow.ts
+++ b/ui/src/locales/lang/en-US/workflow.ts
@@ -48,6 +48,8 @@ export default {
deleteMessage: 'This node cannot be deleted',
},
control: {
+ dragMode: 'Box Select',
+ clickMode: 'Point Select',
zoomOut: 'Zoom Out',
zoomIn: 'Zoom In',
fitView: 'Fit to Screen',
@@ -55,6 +57,21 @@ export default {
extend: 'Expand All',
beautify: 'Auto-Arrange',
},
+ shortcut: {
+ undo: 'Undo',
+ redo: 'Redo',
+ copy: 'Copy',
+ paste: 'Paste',
+ delete: 'Delete',
+ title: 'Keyboard Shortcuts',
+ action: 'Action',
+ keys: 'Shortcut',
+ operation: 'Operation',
+ pressKeys: 'Press new shortcut...',
+ reset: 'Reset',
+ resetAll: 'Reset All',
+ reloadTip: 'Changes take effect after page reload.',
+ },
variable: {
global: 'Global Variable',
chat: 'Chat Variable',
diff --git a/ui/src/locales/lang/zh-CN/workflow.ts b/ui/src/locales/lang/zh-CN/workflow.ts
index 42cdf5a89f7..c8d56d1fc94 100644
--- a/ui/src/locales/lang/zh-CN/workflow.ts
+++ b/ui/src/locales/lang/zh-CN/workflow.ts
@@ -47,6 +47,8 @@ export default {
deleteMessage: '节点不允许删除',
},
control: {
+ dragMode: '框选模式',
+ clickMode: '点选模式',
zoomOut: '缩小',
zoomIn: '放大',
fitView: '适应',
@@ -54,6 +56,21 @@ export default {
extend: '展开全部节点',
beautify: '一键美化',
},
+ shortcut: {
+ undo: '撤销',
+ redo: '重做',
+ copy: '复制',
+ paste: '粘贴',
+ delete: '删除',
+ title: '快捷键设置',
+ action: '动作',
+ keys: '快捷键',
+ operation: '操作',
+ pressKeys: '按下新快捷键...',
+ reset: '重置',
+ resetAll: '恢复默认',
+ reloadTip: '修改后需要刷新页面才能生效。',
+ },
variable: {
global: '全局变量',
chat: '会话变量',
diff --git a/ui/src/locales/lang/zh-Hant/workflow.ts b/ui/src/locales/lang/zh-Hant/workflow.ts
index 6678d4536a7..9f9e9eb3496 100644
--- a/ui/src/locales/lang/zh-Hant/workflow.ts
+++ b/ui/src/locales/lang/zh-Hant/workflow.ts
@@ -47,6 +47,8 @@ export default {
deleteMessage: '節點不允許刪除',
},
control: {
+ dragMode: '框選模式',
+ clickMode: '點選模式',
zoomOut: '縮小',
zoomIn: '放大',
fitView: '適應',
@@ -54,6 +56,21 @@ export default {
extend: '展開全部節點',
beautify: '一鍵美化',
},
+ shortcut: {
+ undo: '撤銷',
+ redo: '重做',
+ copy: '複製',
+ paste: '粘貼',
+ delete: '刪除',
+ title: '快捷鍵設置',
+ action: '動作',
+ keys: '快捷鍵',
+ operation: '操作',
+ pressKeys: '按下新快捷鍵...',
+ reset: '重置',
+ resetAll: '恢復默認',
+ reloadTip: '修改後需要刷新頁面才能生效。',
+ },
variable: {
global: '全局變量',
chat: '會話變量',
diff --git a/ui/src/views/application-workflow/index.vue b/ui/src/views/application-workflow/index.vue
index 7705e1a53cf..55d546c379e 100644
--- a/ui/src/views/application-workflow/index.vue
+++ b/ui/src/views/application-workflow/index.vue
@@ -38,17 +38,35 @@
{{ $t('workflow.setting.addComponent') }}
-
-
- {{ $t('common.debug') }}
-
-
-
- {{ $t('common.save') }}
-
-
- {{ $t('common.publish') }}
-
+
+
+
+ {{ $t('common.debug') }}
+
+
+
+
+
+ {{ $t('common.save') }}
+
+
+
+
+ {{ $t('common.publish') }}
+
+
@@ -74,6 +92,10 @@
+
+
+ {{ $t('workflow.shortcut.title') }}
+
@@ -92,7 +114,7 @@
-
+
@@ -151,6 +173,7 @@
source="work_flow"
@refresh="getDetail"
/>
+
+
+
diff --git a/ui/src/workflow/common/shortcut-config.ts b/ui/src/workflow/common/shortcut-config.ts
new file mode 100644
index 00000000000..702f0ef9863
--- /dev/null
+++ b/ui/src/workflow/common/shortcut-config.ts
@@ -0,0 +1,120 @@
+/**
+ * 快捷键配置模块
+ * 定义所有可配置动作的默认键位,支持用户通过 localStorage 自定义覆盖
+ */
+
+export interface ShortcutBinding {
+ /** Mousetrap 格式的键位列表,如 ['ctrl + s', 'cmd + s'] */
+ keys: string[]
+ /** 显示名称 */
+ label: string
+ /** i18n key — 用于动态读取翻译 */
+ labelKey: string
+ /** 分组 */
+ group: 'general' | 'edit' | 'view'
+}
+
+const STORAGE_KEY = 'workflowShortcuts'
+
+const ALL_ACTIONS: Record = {
+ save: { keys: ['cmd + s', 'ctrl + s'], label: '保存', labelKey: 'common.save', group: 'general' },
+ debug: { keys: ['cmd + shift + d', 'ctrl + shift + d'], label: '调试', labelKey: 'common.debug', group: 'general' },
+ publish: { keys: ['cmd + shift + p', 'ctrl + shift + p'], label: '发布', labelKey: 'common.publish', group: 'general' },
+ search: { keys: ['cmd + f', 'ctrl + f'], label: '搜索节点', labelKey: 'workflow.tip.searchPlaceholder', group: 'general' },
+ undo: { keys: ['cmd + z', 'ctrl + z'], label: '撤销', labelKey: 'workflow.shortcut.undo', group: 'edit' },
+ redo: { keys: ['cmd + y', 'ctrl + y'], label: '重做', labelKey: 'workflow.shortcut.redo', group: 'edit' },
+ copy: { keys: ['cmd + c', 'ctrl + c'], label: '复制', labelKey: 'workflow.shortcut.copy', group: 'edit' },
+ paste: { keys: ['cmd + v', 'ctrl + v'], label: '粘贴', labelKey: 'workflow.shortcut.paste', group: 'edit' },
+ delete: { keys: ['backspace', 'del', 'delete'], label: '删除', labelKey: 'workflow.shortcut.delete', group: 'edit' },
+ selectMode: { keys: ['s'], label: '框选模式', labelKey: 'workflow.control.dragMode', group: 'view' },
+ handMode: { keys: ['h'], label: '点选模式', labelKey: 'workflow.control.clickMode', group: 'view' },
+ zoomIn: { keys: ['cmd + =', 'ctrl + ='], label: '放大', labelKey: 'workflow.control.zoomIn', group: 'view' },
+ zoomOut: { keys: ['cmd + -', 'ctrl + -'], label: '缩小', labelKey: 'workflow.control.zoomOut', group: 'view' },
+ fitView: { keys: ['cmd + 0', 'ctrl + 0'], label: '适应画布', labelKey: 'workflow.control.fitView', group: 'view' },
+ collapseAll: { keys: ['cmd + [', 'ctrl + ['], label: '收起全部', labelKey: 'workflow.control.retract', group: 'view' },
+ expandAll: { keys: ['cmd + ]', 'ctrl + ]'], label: '展开全部', labelKey: 'workflow.control.extend', group: 'view' },
+ beautify: { keys: ['cmd + shift + l', 'ctrl + shift + l'], label: '一键美化', labelKey: 'workflow.control.beautify', group: 'view' },
+}
+
+function loadOverrides(): Record {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ return raw ? JSON.parse(raw) : {}
+ } catch {
+ return {}
+ }
+}
+
+/** 获取某个动作的键位列表(含用户自定义覆盖) */
+export function getShortcutKeys(action: string): string[] {
+ const overrides = loadOverrides()
+ const binding = ALL_ACTIONS[action]
+ if (!binding) return []
+ if (action in overrides) return overrides[action]
+ return binding.keys
+}
+
+/** 设置用户自定义键位 */
+export function setShortcutKeys(action: string, keys: string[]): void {
+ const overrides = loadOverrides()
+ overrides[action] = keys
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides))
+}
+
+/** 重置某个动作为默认键位 */
+export function resetShortcutKeys(action: string): void {
+ const overrides = loadOverrides()
+ delete overrides[action]
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides))
+}
+
+/** 重置所有动作为默认键位 */
+export function resetAllShortcutKeys(): void {
+ localStorage.removeItem(STORAGE_KEY)
+}
+
+/** 获取所有动作及其当前键位(含用户自定义) */
+export function getAllShortcuts(): Record {
+ const overrides = loadOverrides()
+ const result: Record = {}
+ for (const [action, binding] of Object.entries(ALL_ACTIONS)) {
+ result[action] = {
+ ...binding,
+ keys: action in overrides ? overrides[action] : binding.keys,
+ }
+ }
+ return result
+}
+
+/** 将 Mousetrap 键位格式转为人类可读的显示文本 */
+export function formatKeysForDisplay(keys: string[]): string {
+ if (keys.length === 0) return ''
+ // 取第一个非 alt 的键位作为主要显示(mac / win 取当前平台匹配的)
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
+ let key = keys[0]
+ // 如果有 cmd 版本的键,Mac 优先取 cmd;Windows 优先取 ctrl
+ const cmdKey = keys.find(k => k.includes('cmd'))
+ const ctrlKey = keys.find(k => k.includes('ctrl') && !k.includes('cmd'))
+ if (isMac && cmdKey) key = cmdKey
+ else if (!isMac && ctrlKey) key = ctrlKey
+
+ return key
+ .split(' + ')
+ .map(part => {
+ const p = part.toLowerCase()
+ if (p === 'cmd') return isMac ? '⌘' : 'Cmd'
+ if (p === 'ctrl') return isMac ? '⌃' : 'Ctrl'
+ if (p === 'shift') return isMac ? '⇧' : 'Shift'
+ if (p === 'alt') return isMac ? '⌥' : 'Alt'
+ if (p === 'backspace') return '⌫'
+ if (p === 'del' || p === 'delete') return '⌦'
+ if (p === 'return' || p === 'enter') return '↵'
+ if (p === 'escape' || p === 'esc') return 'Esc'
+ if (p === '=') return '+'
+ if (p === '-') return '-'
+ if (p === '0') return '0'
+ // 格式化单个字母或数字键
+ return p.length === 1 ? p.toUpperCase() : p.charAt(0).toUpperCase() + p.slice(1)
+ })
+ .join(isMac ? '' : '+')
+}
diff --git a/ui/src/workflow/common/shortcut.ts b/ui/src/workflow/common/shortcut.ts
index 69b69e11fe3..04f2adb1ce0 100644
--- a/ui/src/workflow/common/shortcut.ts
+++ b/ui/src/workflow/common/shortcut.ts
@@ -7,6 +7,18 @@ import { t } from '@/locales'
import { copyClick } from '@/utils/clipboard'
import { randomId } from '@/utils/common'
import { getMenuNodes, workflowModelDict } from './data'
+
+export interface ShortcutOptions {
+ /** Ctrl/Cmd+S 保存回调 */
+ onSave?: () => void
+ /** Ctrl/Cmd+Shift+D 调试回调 */
+ onDebug?: () => void
+ /** Ctrl/Cmd+Shift+P 发布回调 */
+ onPublish?: () => void
+ /** 光标模式 ref,用于与 NodeControl 同步按钮高亮 */
+ isDragMode?: { value: boolean }
+}
+
let activeCanvasId: string | null = null
type Point = { x: number; y: number }
const lastMouse = {
@@ -84,7 +96,7 @@ function translationEdgeData(edgeData: any, distance: any) {
const TRANSLATION_DISTANCE = 40
let CHILDREN_TRANSLATION_DISTANCE = 40
-export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
+export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel, options?: ShortcutOptions) {
bindMousePosition(lf)
bindCanvasActive(lf)
const { keyboard } = lf
@@ -340,24 +352,113 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
return false
}
graph.eventCenter.on('copy_node', copy_node)
- // 复制
+ // 复制 — 由 LogicFlow 核心处理
keyboard.on(['cmd + c', 'ctrl + c'], copy_node)
- // 粘贴
+ // 粘贴 — 由 LogicFlow 核心处理
keyboard.on(['cmd + v', 'ctrl + v'], () => {})
- // undo
- keyboard.on(['cmd + z', 'ctrl + z'], () => {
- // if (!keyboardOptions?.enabled) return true
- // if (graph.textEditElement) return true
- // lf.undo()
- // return false
+ // undo / redo — LogicFlow 核心已内置 cmd+z / ctrl+z 和 cmd+y / ctrl+y
+ // delete
+ keyboard.on(['backspace', 'del', 'delete'], delete_node)
+
+ // ========== 新增快捷键 ==========
+
+ // Ctrl+S / Cmd+S 保存
+ keyboard.on(['cmd + s', 'ctrl + s'], () => {
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+ options?.onSave?.()
+ return false
+ })
+
+ // Ctrl+Shift+D / Cmd+Shift+D 调试
+ keyboard.on(['cmd + shift + d', 'ctrl + shift + d'], () => {
+ if (!keyboardOptions?.enabled) return true
+ options?.onDebug?.()
+ return false
+ })
+
+ // Ctrl+Shift+P / Cmd+Shift+P 发布
+ keyboard.on(['cmd + shift + p', 'ctrl + shift + p'], () => {
+ if (!keyboardOptions?.enabled) return true
+ options?.onPublish?.()
+ return false
+ })
+
+ // Ctrl+= / Cmd+= 放大
+ keyboard.on(['ctrl + =', 'cmd + ='], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.zoom(true, [0, 0])
+ return false
+ })
+
+ // Ctrl+- / Cmd+- 缩小
+ keyboard.on(['ctrl + -', 'cmd + -'], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.zoom(false, [0, 0])
+ return false
+ })
+
+ // Ctrl+0 / Cmd+0 适应画布
+ keyboard.on(['ctrl + 0', 'cmd + 0'], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.resetZoom()
+ lf.resetTranslate()
+ lf.fitView()
+ return false
})
- // redo
- keyboard.on(['cmd + y', 'ctrl + y'], () => {
+
+ // S 切换到缩放/选择模式(框选)
+ keyboard.on(['s', 'S'], () => {
if (!keyboardOptions?.enabled) return true
if (graph.textEditElement) return true
- lf.redo()
+ const element = document.querySelector('.lf-drag-able') as HTMLElement | null
+ if (!element) return false
+ element.style.cursor = 'default'
+ lf.openSelectionSelect()
+ lf.extension.selectionSelect?.setSelectionSense(true, false)
+ if (options?.isDragMode) options.isDragMode.value = true
+ return false
+ })
+
+ // H 切换到点击模式(拖拽画布)
+ keyboard.on(['h', 'H'], () => {
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+ const element = document.querySelector('.lf-drag-able') as HTMLElement | null
+ if (!element) return false
+ element.style.cursor = 'pointer'
+ lf.closeSelectionSelect()
+ if (options?.isDragMode) options.isDragMode.value = false
+ return false
+ })
+
+ // Ctrl+[ / Cmd+[ 收起全部节点
+ keyboard.on(['ctrl + [', 'cmd + ['], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.graphModel.nodes.forEach((node: any) => {
+ node.properties.showNode = false
+ })
+ return false
+ })
+
+ // Ctrl+] / Cmd+] 展开全部节点
+ keyboard.on(['ctrl + ]', 'cmd + ]'], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.graphModel.nodes.forEach((node: any) => {
+ node.properties.showNode = true
+ })
+ return false
+ })
+
+ // Ctrl+Shift+L / Cmd+Shift+L 一键美化
+ keyboard.on(['ctrl + shift + l', 'ctrl + shift + L', 'cmd + shift + l', 'cmd + shift + L'], () => {
+ if (!keyboardOptions?.enabled) return true
+ lf.extension.dagre?.layout()
+ lf.graphModel.nodes.forEach((node: any) => {
+ if (node.type === 'loop-body-node') {
+ node?.loopLayout?.()
+ }
+ })
return false
})
- // delete
- keyboard.on(['backspace'], delete_node)
}
diff --git a/ui/src/workflow/index.vue b/ui/src/workflow/index.vue
index 2f469756435..5ecfebf43af 100644
--- a/ui/src/workflow/index.vue
+++ b/ui/src/workflow/index.vue
@@ -7,7 +7,8 @@