This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[Bug] 详情页所有 Action 按钮点击无响应 — ActionProvider / 参数弹窗 / API handler / Toast / Confirm 全链路缺失 #994
Copy link
Copy link
Closed
Description
问题描述
在记录详情页面(如 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>包裹<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_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
- 根据
ActionParamDef[]动态渲染 Shadcn 表单字段:type: 'select'→<Select>组件,options 来自 param.optionstype: 'text'→<Input>组件type: 'textarea'→<Textarea>组件
- 支持
required验证 - 确认返回
Record<string, any>,取消返回null - 弹窗使用 Shadcn
Dialog或AlertDialog
任务 4:实现确认弹窗(onConfirm)
新增文件: apps/console/src/components/ActionConfirmDialog.tsx(或复用已有 AlertDialog)
- 替换默认的
window.confirm - 渲染
confirmText内容 - 支持结构化
confirm对象(title、message、confirmText、cancelText) - 返回
Promise<boolean>
任务 5:接入 Toast Handler
文件: apps/console/src/components/RecordDetailView.tsx
- 传入
onToasthandler,映射到 Sonner:onToast={(msg, opts) => { if (opts?.type === 'error') toast.error(msg); else toast.success(msg); }}
- 确保
successMessage和errorMessage正确展示
任务 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:测试与文档
- 单元测试: 为
useRecordActionshook 编写测试(handler 注册、API 映射、参数合并) - 集成测试: 在
RecordDetailView.test.tsx中测试完整 action 流程:- 渲染 → 点击 "Change Stage" → 参数弹窗出现 → 选择 stage → 确认 → API 调用 → toast 成功 → 页面刷新
- 渲染 → 点击 "Mark as Won" → 确认弹窗出现 → 确认 → API 调用 → toast 成功
- 渲染 → 点击 "Mark as Won" → 确认弹窗出现 → 取消 → 无 API 调用
- 更新 ROADMAP.md: 记录 action 系统集成完成状态
验收标准
- ✅ 点击 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
- ✅ 所有测试通过
Reacted by Copilot
Activity