Skip to content

[Bug] 详情页所有 Action 按钮点击无响应 — ActionProvider / 参数弹窗 / API handler / Toast / Confirm 全链路缺失 #994

Description

@hotlong

问题描述

在记录详情页面(如 Opportunity #107),头部所有 Action 按钮(Change StageMark as Won 等)点击后完全无反应 — 无弹窗、无确认、无 Toast、无 API 调用。

screenshot

根因分析

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> 包裹 <DetailView> 及其子组件
  • 传入 context:包含当前 record 数据、objectName、user 信息
  • context 需要在 DetailView 数据加载完成后动态更新(updateContext
// 伪代码
<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_stagedataSource.update(objectName, recordId, { stage: params.new_stage })
    • opportunity_mark_wondataSource.update(objectName, recordId, { stage: 'closed_won' })
    • opportunity_mark_lostdataSource.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

  • 根据 ActionParamDef[] 动态渲染 Shadcn 表单字段:
    • type: 'select'<Select> 组件,options 来自 param.options
    • type: 'text'<Input> 组件
    • type: 'textarea'<Textarea> 组件
  • 支持 required 验证
  • 确认返回 Record<string, any>,取消返回 null
  • 弹窗使用 Shadcn DialogAlertDialog

任务 4:实现确认弹窗(onConfirm)

新增文件: apps/console/src/components/ActionConfirmDialog.tsx(或复用已有 AlertDialog)

  • 替换默认的 window.confirm
  • 渲染 confirmText 内容
  • 支持结构化 confirm 对象(titlemessageconfirmTextcancelText
  • 返回 Promise<boolean>

任务 5:接入 Toast Handler

文件: apps/console/src/components/RecordDetailView.tsx

  • 传入 onToast handler,映射到 Sonner:
    onToast={(msg, opts) => {
      if (opts?.type === 'error') toast.error(msg);
      else toast.success(msg);
    }}
  • 确保 successMessageerrorMessage 正确展示

任务 6:接入 Navigate Handler

  • 传入 onNavigate,映射到 React Router 的 useNavigate()
  • 支持 SPA 内导航和外部链接

任务 7:处理执行后结果(refreshAfter / reload / redirect)

文件: apps/console/src/components/RecordDetailView.tsx

  • action 执行完毕后检查 result.reload / action.refreshAfter
  • 如为 true,触发 DetailView 重新加载数据(通过 refreshKey 或 dataSource 重新查询)
  • 如有 result.redirect,通过 navigate 跳转

任务 8:处理 visible 条件表达式的上下文

文件: packages/components/src/renderers/action/action-button.tsx L52

  • useCondition() 当前使用空 context {},导致 visible: "stage !== 'closed_won'" 永远返回 true
  • 需要确保 record 数据作为 context 传入 useCondition,使 visible 表达式能正确评估当前记录状态
  • 可通过 ActionProvider context 或 SchemaRenderer data prop 传递

任务 9:测试与文档

  • 单元测试: 为 useRecordActions hook 编写测试(handler 注册、API 映射、参数合并)
  • 集成测试: 在 RecordDetailView.test.tsx 中测试完整 action 流程:
    • 渲染 → 点击 "Change Stage" → 参数弹窗出现 → 选择 stage → 确认 → API 调用 → toast 成功 → 页面刷新
    • 渲染 → 点击 "Mark as Won" → 确认弹窗出现 → 确认 → API 调用 → toast 成功
    • 渲染 → 点击 "Mark as Won" → 确认弹窗出现 → 取消 → 无 API 调用
  • 更新 ROADMAP.md: 记录 action 系统集成完成状态

验收标准

  1. ✅ 点击 Change Stage → 弹出 Stage 选择弹窗 → 选择后确认 → 更新成功 → Toast 提示 "Stage updated successfully" → 页面数据刷新
  2. ✅ 点击 Mark as Won → 弹出确认弹窗 "Mark this opportunity as Closed Won?" → 确认后 → 更新成功 → Toast 提示 → 页面刷新
  3. ✅ 当 stage 已经是 closed_wonclosed_lost 时,"Change Stage" 和 "Mark as Won" 按钮不显示(visible 条件生效)
  4. ✅ API 调用失败时显示错误 Toast
  5. ✅ 所有测试通过

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions