Commands API 参考
Commands API 用于统一按钮、菜单、快捷键、权限、启用状态和执行入口。复杂业务系统中,同一个动作应注册为一个命令,再由工具栏、菜单、右键菜单或快捷键绑定同一个 command id。
命令不是按钮。按钮、菜单项和快捷键只是命令入口。
导出清单
核心导出:
CommandManagerCommandScopeAppCommandCommandStateCommandExecutionContextCommandTriggerCommandPredicateCommandReasonResolverCommandExecutorPermissionServiceCommandInterceptorCommandExecuteOptionsCommandEvaluateOptionsCommandInspectionCommandScopeDiagnosticRenderCommandScopeRenderCommandButtonRenderCommandToolbarCommandShortcutControllerCommandShortcutControllerOptionsnormalizeShortcutshortcutFromKeyboardEventcommandShortcutscreateCommandContextMenucreateCommandMenuBarItemsresolveCommandMenuEntriesexecuteCommandMenuSelectionCommandMenuItemCommandMenuBarItemCommandToolbarGroupCommandToolbarItemcommandManagerContextKey
AppCommand
AppCommand<TResult = unknown> 定义一个业务动作。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string |
是 | 命令唯一 id。应稳定,便于权限配置、快捷键和审计。 |
title |
string |
是 | 默认显示文本。 |
description |
string |
否 | 说明文本,常用于 tooltip 或诊断面板。 |
icon |
string |
否 | 图标名称。具体可用图标由图标组件决定。 |
shortcut |
string | readonly string[] |
否 | 快捷键,例如 Ctrl+S、Meta+S。 |
permission |
string | readonly string[] |
否 | 功能权限码。数组语义是全部权限都必须满足。 |
visible |
CommandPredicate |
否 | 是否显示入口。返回 false 时 enabled 也会是 false。 |
enabled |
CommandPredicate |
否 | 当前页面状态下是否可用。 |
canExecute |
CommandPredicate |
否 | 执行前业务条件。执行前会再次求值。 |
checked |
CommandPredicate |
否 | 切换类命令的选中状态。 |
disabledReason |
CommandReasonResolver |
否 | 禁用原因。未提供时默认是 当前状态不可用。 |
execute |
CommandExecutor<TResult> |
是 | 执行动作,可同步或异步。 |
visible、enabled、canExecute、checked 和 disabledReason 都接收 CommandExecutionContext。这些函数必须轻量、同步、无副作用;不要在里面请求接口、修改 UI 或扫描大数据。
CommandState
CommandManager.evaluate() 返回 CommandState。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
命令 id。 |
title |
string |
当前显示标题。 |
description |
string | undefined |
当前说明。 |
icon |
string | undefined |
当前图标。 |
visible |
boolean |
入口是否可见。 |
enabled |
boolean |
是否可执行。 |
checked |
boolean |
是否处于选中状态。 |
executing |
boolean |
是否正在执行中。 |
reason |
string | undefined |
禁用原因。 |
command |
AppCommand | undefined |
原始命令。命令不存在时为空。 |
命令不存在时,visible=false、enabled=false,reason 为 命令不存在。
CommandExecutionContext
命令执行和状态求值都会创建上下文。
| 字段 | 类型 | 说明 |
|---|---|---|
appContext |
AppContextRegistry |
从 source 所在 render tree 和应用根收集出的上下文快照。 |
commandManager |
CommandManager |
当前命令管理器。 |
source |
RenderObject | undefined |
触发命令的 render 对象。 |
trigger |
'api' | 'click' | 'keyboard' | 'menu' |
触发来源。 |
event |
unknown |
原始事件或调用方附加数据。 |
appContext 是一次命令求值或执行期间的快照,CommandManager 内部会在求值和执行结束后释放它。业务不要长期持有这个快照。
CommandManager
创建命令管理器:
const commandManager = new CommandManager({ permissionService: { has(permission, context) { return permission === 'record.save' }, },})
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rootScope |
CommandScope |
新建根 scope | 应用级命令范围。 |
permissionService |
PermissionService |
允许全部 | 权限服务。 |
属性:
| 属性 | 类型 | 说明 |
|---|---|---|
rootScope |
CommandScope |
根命令范围。 |
version |
number |
命令状态版本号。invalidate() 会递增。 |
permissionService |
PermissionService |
当前权限服务。 |
disposed |
boolean |
是否已释放。 |
方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
setPermissionService(service) |
void |
替换权限服务并触发 invalidate()。 |
register(command) |
DisposeFn |
注册应用级命令到 rootScope。返回释放函数。 |
addInterceptor(interceptor) |
DisposeFn |
注册执行拦截器。 |
evaluate(commandId, options?) |
CommandState |
求值命令可见、可用、选中和执行中状态。 |
inspect(commandId, options?) |
CommandInspection |
获取命令、scope 链、权限、快捷键和覆盖关系。 |
inspectAll(options?) |
CommandInspection[] |
检查当前 scope 可见的所有命令。 |
execute(commandId, options?) |
Promise<TResult | undefined> |
执行命令。不可见、不可用或不存在时返回 undefined。 |
createExecutionContext(options) |
CommandExecutionContext |
创建执行上下文。一般业务不需要直接调用。 |
resolveScope(source?) |
CommandScope |
从 source 向上查找最近命令 scope,找不到则返回 root scope。 |
createAppContextSnapshot(source?) |
AppContextRegistry |
从 source 和应用根创建上下文快照。 |
subscribe(listener) |
DisposeFn |
订阅命令状态版本变化。 |
invalidate() |
void |
通知绑定 UI 重新求值命令状态。 |
dispose() |
void |
释放监听、拦截器、执行状态和 root scope。 |
CommandExecuteOptions 和 CommandEvaluateOptions:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
source |
RenderObject |
undefined |
触发源。用于解析局部 command scope 和 app context。 |
trigger |
CommandTrigger |
api |
触发来源。 |
event |
unknown |
undefined |
原始事件或附加数据。 |
CommandScope
CommandScope 是命令的分层注册表。
构造参数:
const pageScope = new CommandScope(commandManager.rootScope, 'record-page')
| 参数 | 类型 | 说明 |
|---|---|---|
parent |
CommandScope | undefined |
父命令范围。 |
debugLabel |
string | undefined |
诊断标签。 |
属性和方法:
| 成员 | 返回值 | 说明 |
|---|---|---|
debugId |
number |
内部诊断 id。 |
debugLabel |
string | undefined |
诊断标签,可修改。 |
disposed |
boolean |
是否已释放。 |
register(command) |
DisposeFn |
注册命令。相同 id 会覆盖当前 scope 的旧命令。 |
unregister(commandId) |
boolean |
删除当前 scope 的命令。 |
get(commandId) |
AppCommand | undefined |
从当前 scope 向父级查找。 |
getOwn(commandId) |
AppCommand | undefined |
只读取当前 scope。 |
hasOwn(commandId) |
boolean |
当前 scope 是否有该命令。 |
ids() |
IterableIterator<string> |
当前 scope 自己注册的命令 id。 |
ownCommands() |
AppCommand[] |
当前 scope 自己注册的命令。 |
commands() |
AppCommand[] |
当前 scope 和父级合并后的可见命令,局部覆盖父级。 |
dispose() |
void |
释放当前 scope 的命令。不会释放父 scope。 |
RenderCommandScope
RenderCommandScope 把命令 scope 挂到 render tree。子组件触发命令时,会优先解析到最近的 RenderCommandScope。
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
commandManager |
CommandManager |
undefined |
用于注册后触发 invalidate(),也用于创建默认父 scope。 |
scope |
CommandScope |
新建 scope | 外部已有命令范围。 |
debugLabel |
string |
undefined |
诊断标签。 |
commands |
AppCommand[] |
[] |
初始化命令。 |
child |
RenderBox |
undefined |
被包裹的页面或组件根。 |
disposeScopeOnDispose |
boolean |
未传 scope 时为 true |
当前 render 释放时是否释放 command scope。 |
方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
register(command) |
DisposeFn |
注册局部命令并触发 manager 状态刷新。 |
setChild(child?) |
void |
替换子节点。 |
dispose() |
void |
按 disposeScopeOnDispose 决定是否释放 scope。 |
页面或组件命令应优先放在 RenderCommandScope。这样页面关闭、tab 销毁或组件释放时,局部命令会自动失效。
RenderCommandButton
RenderCommandButton 是绑定单个命令的按钮。
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
commandId |
string |
必填 | 绑定的命令 id。 |
commandManager |
CommandManager |
从 app context 读取 | 命令管理器。 |
label |
string |
命令 title |
覆盖按钮文本。 |
tooltip |
string |
命令禁用原因或说明 | 覆盖 tooltip。 |
variant |
ButtonVariant |
Button 默认值 | 按钮风格。 |
成员:
| 成员 | 返回值 | 说明 |
|---|---|---|
commandId |
string |
当前绑定的命令 id。 |
commandState |
CommandState | undefined |
最近一次命令状态。读取时会刷新。 |
executeCommand(trigger?, event?) |
Promise<unknown> |
手动执行命令。 |
refreshCommandState(force?) |
boolean |
刷新按钮状态,返回当前是否可见且可用。 |
按钮会订阅 CommandManager,manager invalidate() 后会重新求值并更新文本、禁用、loading 和 tooltip。
RenderCommandToolbar
RenderCommandToolbar 把一组命令转换为普通 RenderToolbar 项。
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
groups |
CommandToolbarGroup[] |
必填 | 工具栏分组。 |
commandManager |
CommandManager |
从 app context 读取 | 命令管理器。 |
CommandToolbarGroup:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
分组 id。 |
slot |
ToolbarSlot |
分组位置。 |
visible |
boolean | undefined |
分组是否显示。 |
items |
CommandToolbarItem[] |
分组项。 |
CommandToolbarItem:
| kind | 字段 | 说明 |
|---|---|---|
command |
commandId、id?、label?、tooltip?、showLabel? |
绑定命令按钮。 |
separator |
id? |
分隔线。 |
方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
setCommandGroups(groups) |
void |
替换命令分组。 |
Toolbar 会隐藏 state.visible=false 的命令项,禁用 state.enabled=false 的命令项。
菜单适配
命令菜单工具把命令配置转换为 ContextMenu 或 MenuBar 需要的数据。
CommandMenuItem:
| kind | 字段 | 说明 |
|---|---|---|
command |
commandId、key?、label?、icon?、shortcut?、danger?、disabled? |
命令菜单项。 |
submenu |
key、label、items、icon?、disabled?、visible? |
子菜单。 |
separator |
无 | 分隔线。 |
CommandMenuBarItem:
| 字段 | 类型 | 说明 |
|---|---|---|
key |
string |
一级菜单 key。 |
label |
string |
一级菜单文本。 |
items |
CommandMenuItem[] |
子项。 |
disabled |
boolean | undefined |
是否禁用。 |
visible |
boolean | undefined |
是否显示。 |
函数:
| 函数 | 返回值 | 说明 |
|---|---|---|
createCommandContextMenu(manager, source, items) |
{ entries; onSelect } |
生成 ContextMenu 条目和选择处理函数。 |
createCommandMenuBarItems(manager, source, menus) |
{ items; onSelect } |
生成 MenuBar 条目和选择处理函数。 |
resolveCommandMenuEntries(manager, source, items, selectionMap?, path?) |
ContextMenuEntry[] |
只生成菜单条目。 |
executeCommandMenuSelection(manager, source, selectionMap, key) |
void |
根据选择 key 执行命令。 |
菜单适配会自动过滤不可见命令,并把命令 shortcut 显示到菜单项。
快捷键
CommandShortcutController 把键盘事件映射到当前 scope 可见命令。
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
commandManager |
CommandManager |
必填 | 命令管理器。 |
source |
RenderObject | (() => RenderObject | undefined) |
undefined |
当前快捷键作用源。 |
priority |
number |
0 |
键盘绑定优先级。 |
方法和工具函数:
| 成员 | 返回值 | 说明 |
|---|---|---|
dispose() |
void |
注销键盘绑定。 |
commandShortcuts(command) |
string[] |
获取命令标准化后的快捷键数组。 |
normalizeShortcut(shortcut) |
string | undefined |
标准化快捷键字符串。 |
shortcutFromKeyboardEvent(event) |
string | undefined |
从键盘事件生成标准化快捷键。 |
快捷键会从 source 解析当前命令 scope,只执行可见且可用的第一个匹配命令,并调用 event.preventDefault()。
权限服务和拦截器
PermissionService:
| 方法 | 返回值 | 说明 |
|---|---|---|
has(permission, context) |
boolean |
判断当前上下文是否拥有某权限。 |
CommandInterceptor:
| 方法 | 返回值 | 说明 |
|---|---|---|
beforeExecute(command, context) |
boolean | void | Promise<boolean | void> |
执行前拦截。返回 false 会取消执行。 |
afterExecute(command, context, result) |
void | Promise<void> |
执行成功后调用。 |
onError(command, context, error) |
void | Promise<void> |
执行抛错时调用。 |
拦截器适合做统一确认、埋点、审计和错误处理。它不是业务状态存储系统。
命令检查
inspect() 和 inspectAll() 用于调试命令注册、覆盖、权限和快捷键。
CommandInspection:
| 字段 | 类型 | 说明 |
|---|---|---|
commandId |
string |
命令 id。 |
exists |
boolean |
是否存在命令。 |
state |
CommandState |
当前求值状态。 |
command |
AppCommand | undefined |
命令定义。 |
sourceScopeId |
number |
触发源解析到的 scope id。 |
sourceScopeLabel |
string |
触发源 scope 标签。 |
ownerScopeId |
number | undefined |
实际拥有该命令的 scope id。 |
ownerScopeLabel |
string | undefined |
实际拥有该命令的 scope 标签。 |
scopeChain |
CommandScopeDiagnostic[] |
从 source scope 到 root 的链路。 |
permissions |
string[] |
命令权限码。 |
shortcuts |
string[] |
命令快捷键。 |
overridesParent |
boolean |
当前命令是否覆盖了父级同 id 命令。 |
CommandScopeDiagnostic:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
number |
scope id。 |
label |
string |
scope 标签。 |
ownCommandIds |
string[] |
当前 scope 自己注册的命令。 |
activeCommandIds |
string[] |
当前 scope 和父级合并后的可见命令。 |
isSourceScope |
boolean |
是否是 source 解析到的 scope。 |
isOwnerScope |
boolean |
是否是命令实际拥有者。 |
使用示例
import { CommandManager, RenderCommandButton, RenderCommandScope, RenderStackPanel, createAppContextKey,} from 'ds-ui' const exampleCurrentRecordKey = createAppContextKey<{ id: string }>('record.current')let exampleDirty = true const exampleCommandManager = new CommandManager({ permissionService: { has: permission => permission === 'record.save', },}) const exampleContent = new RenderStackPanel({ spacing: 8 })const exampleScope = new RenderCommandScope({ commandManager: exampleCommandManager, debugLabel: 'record-page', commands: [ { id: 'record.save', title: '保存', icon: 'save', shortcut: ['Ctrl+S', 'Meta+S'], permission: 'record.save', canExecute: context => exampleDirty && Boolean(context.appContext.get(exampleCurrentRecordKey)), execute: () => { exampleDirty = false exampleCommandManager.invalidate() }, }, ], child: exampleContent,}) exampleContent.addChild(new RenderCommandButton({ commandId: 'record.save', commandManager: exampleCommandManager,})) exampleScope
使用边界
- 页面命令优先注册在页面级
RenderCommandScope,不要注册到 root scope。 source很重要。没有 source 时,只能解析 root scope 和应用级上下文。- 业务状态变化后调用
commandManager.invalidate(),不要在 hover 或 paint 中频繁调用。 execute()会在执行前重新求值权限和可用状态,不要只依赖按钮禁用状态。dispose()后继续注册、求值或执行会抛错。- 命令适合业务动作,不适合低层 pointer move、hover、输入字符等高频事件。