问题描述
在记录详情页面(如 Opportunity #107 ),头部所有 Action 按钮(Change Stage 、Mark as Won 等)点击后完全无反应 — 无弹窗、无确认、无 Toast、无 API 调用。
根因分析
Action 执行链路有 5 个环节断裂 ,导致按钮虽然渲染出来,但点击后执行静默失败:
#
断裂环节
涉及文件
影响
1
缺少 ActionProvider
apps/console/src/components/RecordDetailView.tsx
useAction() 回退到空的本地 ActionRunner,没有注册任何 handler
2
api action 的 target 是逻辑名称,不是 URL
examples/crm/src/actions/opportunity.actions.ts
ActionRunner.executeAPI() 对 'opportunity_change_stage' 执行 fetch(),直接报错但被静默 catch
3
无 onParamCollection handler
packages/core/src/actions/ActionRunner.ts L289-295
"Change Stage" 需要用户选择 new_stage 参数,但参数收集步骤被跳过
4
无 onConfirm handler
packages/core/src/actions/ActionRunner.ts L194
"Mark as Won" 的 confirmText 回退到 window.confirm,可能被浏览器抑制
5
无 onToast handler
packages/core/src/actions/ActionRunner.ts L188
成功/失败消息(successMessage/errorMessage)被静默丢弃,用户零反馈
调用链路详解
RecordDetailView
→ 从 objectDef.actions 筛选 record_header actions
→ 构建 action:bar schema → 传入 DetailView
→ DetailView 通过 SchemaRenderer 渲染 action:bar
→ action:bar 遍历 actions → 解析 action:button 组件
→ action:button 调用 useAction().execute()
→ ❌ 无 ActionProvider → 创建空 ActionRunner
→ ❌ 无自定义 handler → 走内置 executeAPI()
→ ❌ fetch('opportunity_change_stage') 失败
→ ❌ 无 toastHandler → 错误被静默吞掉
涉及的 Action 定义
// examples/crm/src/actions/opportunity.actions.ts
export const OpportunityActions = [
{
name : 'opportunity_change_stage' ,
label : 'Change Stage' ,
type : 'api' ,
target : 'opportunity_change_stage' , // ← 逻辑名称,不是 HTTP URL
params : [ { name : 'new_stage' , label : 'New Stage' , type : 'select' , required : true , options : [ ...] } ] ,
refreshAfter : true ,
successMessage : 'Stage updated successfully' ,
} ,
{
name : 'opportunity_mark_won' ,
label : 'Mark as Won' ,
type : 'api' ,
target : 'opportunity_mark_won' , // ← 逻辑名称,不是 HTTP URL
confirmText : 'Mark this opportunity as Closed Won?' ,
refreshAfter : true ,
successMessage : 'Opportunity marked as won!' ,
} ,
// ... opportunity_mark_lost 等
] ;
现有代码上下文
App.tsx 已有 useActionRunner 的使用模式(仅用于 CRUD dialog),可作为参考:
// apps/console/src/App.tsx L105-137
const { execute : executeAction , runner } = useActionRunner ( ) ;
runner . registerHandler ( 'crud_success' , async ( action ) => { ... } ) ;
runner . registerHandler ( 'dialog_cancel' , async ( ) => { ... } ) ;
但 RecordDetailView 没有类似的 handler 注册逻辑,也没有被 ActionProvider 包裹。
开发任务清单
任务 1:在 RecordDetailView 中引入 ActionProvider
文件 : apps/console/src/components/RecordDetailView.tsx
// 伪代码
< ActionProvider
context = { { record : currentRecordData , objectName, user } }
onConfirm = { confirmHandler }
onToast = { toastHandler }
onNavigate = { navigateHandler }
onParamCollection = { paramCollectionHandler }
handlers = { { api : apiActionHandler } }
>
< DetailView schema = { detailSchema } dataSource = { dataSource } . . . / >
</ ActionProvider >
任务 2:实现自定义 API Action Handler
新增文件 : apps/console/src/hooks/useRecordActions.ts(或扩展现有 useObjectActions.ts)
注册 api 类型的 handler,拦截所有 type: 'api' 的 action
根据 action.target(逻辑名称)映射到 dataSource 操作:
opportunity_change_stage → dataSource.update(objectName, recordId, { stage: params.new_stage })
opportunity_mark_won → dataSource.update(objectName, recordId, { stage: 'closed_won' })
opportunity_mark_lost → dataSource.update(objectName, recordId, { stage: 'closed_lost', loss_reason: params.loss_reason })
通用化:对于未知 target,尝试 dataSource.update(objectName, recordId, params) 作为默认行为
返回 { success: true, reload: true } 以触发页面刷新
任务 3:实现参数收集弹窗(onParamCollection)
新增文件 : apps/console/src/components/ActionParamDialog.tsx
任务 4:实现确认弹窗(onConfirm)
新增文件 : apps/console/src/components/ActionConfirmDialog.tsx(或复用已有 AlertDialog)
任务 5:接入 Toast Handler
文件 : apps/console/src/components/RecordDetailView.tsx
任务 6:接入 Navigate Handler
任务 7:处理执行后结果(refreshAfter / reload / redirect)
文件 : apps/console/src/components/RecordDetailView.tsx
任务 8:处理 visible 条件表达式的上下文
文件 : packages/components/src/renderers/action/action-button.tsx L52
任务 9:测试与文档
验收标准
✅ 点击 Change Stage → 弹出 Stage 选择弹窗 → 选择后确认 → 更新成功 → Toast 提示 "Stage updated successfully" → 页面数据刷新
✅ 点击 Mark as Won → 弹出确认弹窗 "Mark this opportunity as Closed Won?" → 确认后 → 更新成功 → Toast 提示 → 页面刷新
✅ 当 stage 已经是 closed_won 或 closed_lost 时,"Change Stage" 和 "Mark as Won" 按钮不显示 (visible 条件生效)
✅ API 调用失败时显示错误 Toast
✅ 所有测试通过
问题描述
在记录详情页面(如 Opportunity #107),头部所有 Action 按钮(Change Stage、Mark as Won 等)点击后完全无反应 — 无弹窗、无确认、无 Toast、无 API 调用。
根因分析
Action 执行链路有 5 个环节断裂,导致按钮虽然渲染出来,但点击后执行静默失败:
ActionProviderapps/console/src/components/RecordDetailView.tsxuseAction()回退到空的本地ActionRunner,没有注册任何 handlerapiaction 的target是逻辑名称,不是 URLexamples/crm/src/actions/opportunity.actions.tsActionRunner.executeAPI()对'opportunity_change_stage'执行fetch(),直接报错但被静默 catchonParamCollectionhandlerpackages/core/src/actions/ActionRunner.tsL289-295new_stage参数,但参数收集步骤被跳过onConfirmhandlerpackages/core/src/actions/ActionRunner.tsL194confirmText回退到window.confirm,可能被浏览器抑制onToasthandlerpackages/core/src/actions/ActionRunner.tsL188successMessage/errorMessage)被静默丢弃,用户零反馈调用链路详解
涉及的 Action 定义
现有代码上下文
App.tsx 已有
useActionRunner的使用模式(仅用于 CRUD dialog),可作为参考:但 RecordDetailView 没有类似的 handler 注册逻辑,也没有被
ActionProvider包裹。开发任务清单
任务 1:在 RecordDetailView 中引入 ActionProvider
文件:
apps/console/src/components/RecordDetailView.tsx<ActionProvider>包裹<DetailView>及其子组件context:包含当前 record 数据、objectName、user 信息updateContext)任务 2:实现自定义 API Action Handler
新增文件:
apps/console/src/hooks/useRecordActions.ts(或扩展现有useObjectActions.ts)api类型的 handler,拦截所有type: 'api'的 actionaction.target(逻辑名称)映射到dataSource操作:opportunity_change_stage→dataSource.update(objectName, recordId, { stage: params.new_stage })opportunity_mark_won→dataSource.update(objectName, recordId, { stage: 'closed_won' })opportunity_mark_lost→dataSource.update(objectName, recordId, { stage: 'closed_lost', loss_reason: params.loss_reason })dataSource.update(objectName, recordId, params)作为默认行为{ success: true, reload: true }以触发页面刷新任务 3:实现参数收集弹窗(onParamCollection)
新增文件:
apps/console/src/components/ActionParamDialog.tsxActionParamDef[]动态渲染 Shadcn 表单字段:type: 'select'→<Select>组件,options 来自 param.optionstype: 'text'→<Input>组件type: 'textarea'→<Textarea>组件required验证Record<string, any>,取消返回nullDialog或AlertDialog任务 4:实现确认弹窗(onConfirm)
新增文件:
apps/console/src/components/ActionConfirmDialog.tsx(或复用已有 AlertDialog)window.confirmconfirmText内容confirm对象(title、message、confirmText、cancelText)Promise<boolean>任务 5:接入 Toast Handler
文件:
apps/console/src/components/RecordDetailView.tsxonToasthandler,映射到 Sonner:successMessage和errorMessage正确展示任务 6:接入 Navigate Handler
onNavigate,映射到 React Router 的useNavigate()任务 7:处理执行后结果(refreshAfter / reload / redirect)
文件:
apps/console/src/components/RecordDetailView.tsxresult.reload/action.refreshAfterresult.redirect,通过 navigate 跳转任务 8:处理
visible条件表达式的上下文文件:
packages/components/src/renderers/action/action-button.tsxL52useCondition()当前使用空 context{},导致visible: "stage !== 'closed_won'"永远返回 trueuseCondition,使 visible 表达式能正确评估当前记录状态任务 9:测试与文档
useRecordActionshook 编写测试(handler 注册、API 映射、参数合并)RecordDetailView.test.tsx中测试完整 action 流程:验收标准
closed_won或closed_lost时,"Change Stage" 和 "Mark as Won" 按钮不显示(visible 条件生效)