@qfei-design/make-app-approval 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/PUBLIC_API.md +147 -0
  2. package/README.md +65 -2
  3. package/capabilities.json +76 -0
  4. package/dist/detail/approval-task-detail.d.ts +2 -0
  5. package/dist/detail/diagnostics.d.ts +2 -0
  6. package/dist/detail/field-value.d.ts +2 -0
  7. package/dist/detail/index.d.ts +3 -0
  8. package/dist/detail/task-policy.d.ts +18 -0
  9. package/dist/detail/types.d.ts +111 -0
  10. package/dist/detail.cjs +1 -0
  11. package/dist/detail.mjs +500 -0
  12. package/dist/index.cjs +1 -0
  13. package/dist/index.d.ts +21 -0
  14. package/dist/index.mjs +28 -0
  15. package/dist/list/approval-table.d.ts +14 -0
  16. package/dist/list/diagnostics.d.ts +3 -0
  17. package/dist/list/index.d.ts +3 -0
  18. package/dist/list/my-approvals-page.d.ts +2 -0
  19. package/dist/list/page-contract.d.ts +3 -0
  20. package/dist/list/status-label.d.ts +1 -0
  21. package/dist/list/types.d.ts +71 -0
  22. package/dist/list/ui-components.d.ts +51 -0
  23. package/dist/list/use-approval-page.d.ts +12 -0
  24. package/dist/list/virtual-page-loader.d.ts +26 -0
  25. package/dist/list.cjs +1 -0
  26. package/dist/list.mjs +387 -0
  27. package/dist/react/index.d.ts +7 -0
  28. package/dist/react.cjs +1 -0
  29. package/dist/react.mjs +17 -0
  30. package/dist/styles.css +102 -0
  31. package/dist/styles.css.d.ts +1 -0
  32. package/dist/withdrawal.d.ts +14 -0
  33. package/docs/host-integration.md +242 -0
  34. package/docs/implementation-plan.md +61 -0
  35. package/docs/publishing.md +108 -0
  36. package/examples/antd-components.tsx +40 -0
  37. package/examples/detail-integration.tsx +80 -0
  38. package/examples/host-integration.tsx +36 -0
  39. package/package.ai.json +85 -0
  40. package/package.json +117 -4
  41. package/recipes.json +114 -0
package/PUBLIC_API.md ADDED
@@ -0,0 +1,147 @@
1
+ # 公开 API
2
+
3
+ 以下均已实现;历史与移动页面尚未对外提供。完整职责和宿主模板见 [接入说明](docs/host-integration.md)。
4
+
5
+ ## 根入口
6
+
7
+ 从 `@qfei-design/make-app-approval` 导入:
8
+
9
+ | 导出 | 输入与结果 |
10
+ | --- | --- |
11
+ | `APPROVAL_STATUS_FIELD` | 固定为 `qfei_process_instance_status` |
12
+ | `APPROVAL_LOCK_FIELD` | 固定为 `qfei_is_locked` |
13
+ | `getApprovalStatusDisplay(value)` | 返回 `{ label, tone }`,原始值为 `unknown` |
14
+ | `hasApprovalDefinition(entity)` | 只检查 `bpmDefinitionKey` 是否为非空字符串;后端负责有效/启用判断 |
15
+ | `isApprovalSystemField(key)` | 精确识别上述两个只读系统字段 |
16
+ | `getRecordApprovalWriteRestriction(record, { approvalEnabled? })` | 返回 `null` 或 `{ reason, message }`;限制普通业务编辑,不判断任务办理权限 |
17
+
18
+ 类型:`ApprovalProcessStatus`、`ApprovalStatusDisplay`、`ApprovalWriteRestriction`。
19
+
20
+ | 平台状态码 | 展示 |
21
+ | --- | --- |
22
+ | `UNSUBMITTED` | - |
23
+ | `IN_APPROVAL` / `RETURNING` | 审批中 |
24
+ | `APPROVED` | 已同意 |
25
+ | `REJECTED` | 已拒绝 |
26
+ | `CANCELLED` | 已撤回 |
27
+ | 未识别值 | 状态未知 |
28
+
29
+ 根入口的显示方法对 `null/undefined/UNSUBMITTED` 返回 `-`,用于业务记录无审批状态的场景;未知非空状态显示“状态未知”。宿主传入原始记录状态,不根据实体绑定补造状态或锁定。
30
+
31
+ 仅 `qfei_is_locked === true` 或流程状态为 `IN_APPROVAL` / `RETURNING` 时返回普通编辑限制。其他状态(包括已结束、已撤回、未提交及缺失状态)返回 `null`,继续由普通 IAM 对象、记录和字段权限决定是否可写;不根据异常元数据推断锁定。实体启用审批本身不会锁定全部记录。第二个参数和返回类型中的历史原因值保留兼容,但当前只返回 `locked` / `in-progress` 限制。不要将 `null` 结果视为最终授权,后端仍负责真实审批状态、并发锁和权限校验。
32
+
33
+ ## React 与 CSS
34
+
35
+ 从 `@qfei-design/make-app-approval/react` 导入 `ApprovalStatus` 和 `ApprovalStatusProps`:
36
+
37
+ ```ts
38
+ type ApprovalStatusProps = {
39
+ value: unknown;
40
+ className?: string;
41
+ style?: React.CSSProperties;
42
+ };
43
+ ```
44
+
45
+ 组件输出文字和语义 `data-tone`,支持 SSR。宿主入口导入一次 `@qfei-design/make-app-approval/styles.css`。可通过 `--make-approval-text`、`--make-approval-surface`、`--make-approval-primary`、`--make-approval-error` 对齐宿主主题。
46
+
47
+ ## 兼容约束
48
+
49
+ - 只使用上述公开入口,不深层导入 `src` 或 `dist`。
50
+ - React 是 peer dependency,构建产物不内嵌 React。
51
+ - 根入口同时支持 UI 和 Node Service;React 组件从独立入口加载。
52
+ - ESM 和 CommonJS 入口提供相同能力;CSS 和类型声明随包交付。
53
+ - `CANCELLED` 为后端已确认的撤回业务状态;不将任务完成等同于整个流程完成。
54
+
55
+ ## 我的审批列表
56
+
57
+ 从 `@qfei-design/make-app-approval/list` 导入 `MyApprovalsPage` 及类型:
58
+
59
+ `MyApprovalsPageProps`、`ApprovalListProvider`、`ApprovalListQuery`、`ApprovalListFilters`、`ApprovalListTab`、`ApprovalListItem`、`ApprovalPage`、`ApprovalStatusOption`、`ApprovalLogger`、`ApprovalDiagnostic`。
60
+
61
+ 宿主控件类型也从 `/list` 导入:`ApprovalUIComponents`、`ApprovalInputProps`、`ApprovalSelectProps`、`ApprovalDateRangePickerProps`、`ApprovalButtonProps`、`ApprovalTabsProps`。
62
+
63
+ | 属性 | 必填 | 说明 |
64
+ | --- | --- | --- |
65
+ | `scopeKey` | 是 | 非空上下文标识;应用/身份/环境变化时改变,清空旧请求和页面状态,不用作鉴权 |
66
+ | `provider` | 是 | `{ loadPage(query, { signal }): Promise<ApprovalPage> }`;宿主请求函数,不是 HTTP 客户端配置 |
67
+ | `components` | 是 | `ApprovalUIComponents`,包含 Input / Select / DateRangePicker / Button / Tabs 的宿主适配组件;缺失时报错,无原生控件回退 |
68
+ | `onOpenApproval` | 是 | `(row: ApprovalListItem) => void \| Promise<void>`,宿主打开详情;Promise 拒绝时包展示安全错误 |
69
+ | `statusOptions` | 否 | `{ value, label }[]`,只填真实后端支持的状态筛选项;默认只有“全部流程状态”,不覆盖状态显示映射 |
70
+ | `pageSize` | 否 | 每批加载条数,1–200 的整数,默认 20;变更重置列表,不生成翻页按钮 |
71
+ | `refreshKey` | 否 | string/number;办理成功后改变,刷新当前查询并保留滚动位置;仅关闭详情无需改变 |
72
+ | `themeKey` | 否 | string/number;动态主题变更后改变,Canvas 重读 CSS 变量 |
73
+ | `logger` | 否 | 安全事件回调;默认控制台日志,不包含原始异常或业务内容 |
74
+ | `className` | 否 | 宿主附加容器样式 |
75
+
76
+ 查询字段为 `tab: 'pending' \| 'approved'`、`keyword`、`processStatus`、`startDate`、`endDate`、`page`、`pageSize`。这是接口数据页,页码从 1 开始;页面使用滚动加载。筛选空串表示不限,日期为 `YYYY-MM-DD`。返回 `{ rows, total }`,字段与时区约定详见接入说明。
77
+
78
+ 包负责 CanvasTable 虚拟滚动、首批数据/总数初始化、后续数据页请求、取消与旧响应隔离、结构校验、失败重试及有界缓存;查询条件变化后清空缓存并从顶部重新加载。不归类、不本地筛选、不拼接多种任务状态的分页结果。列表默认只读,提供连续序号、行头详情入口和当前缓存记录的键盘文字入口。
79
+
80
+ CanvasTable 的数据页从 0 开始,包在 provider 边界加 1;宿主始终接收同一套一基 `page` 参数。使用 `virtualOptions`、`data:load` 请求身份、`setVirtualPageData` 原子写入以及 `markPageLoadFailed` 释放失败/取消请求。普通滚动请求合并窗口 120ms、并发上限 2。缓存按整页向上取整,至少容纳约 300 行,并随视口高度增加容量以容纳可见页与预加载页;容器尺寸变化后自动检查并补齐新增可见页。详情入口与文字视图读取表格实际缓存,不另设淘汰策略;普通后续页失败重试保留滚动位置及已加载记录,没有上一页/下一页控件。
81
+
82
+ 若分页数据交叠等情况导致缓存校验失败,包停止当前代次的后续请求并暂时移除详情入口,持续显示失败提示。此时重试沿用现有刷新流程,重新取得首批数据和当前视口所需页,保留当前查询条件及有效范围内的滚动位置,恢复一致的数据后重新提供详情入口。
83
+
84
+ 列表从独立浏览器入口加载,CanvasTable 以动态导入按需加载并保持外置。它不会被根入口或 `/react` 状态组件引用。宿主必须安装兼容的 CanvasTable 并导入 CSS。
85
+
86
+ ## 宿主基础组件合同
87
+
88
+ 功能和状态由包管理,基础控件与主题由宿主提供。所有基础控件都按受控方式使用,适配器不自行请求接口或维护另一份筛选条件。
89
+
90
+ | 组件 | 包传入 | 宿主适配职责 |
91
+ | --- | --- | --- |
92
+ | `Input` | `id`、`ariaLabel`、`value`、`onChange(value)`、可选 `placeholder/disabled` | 输入/清空转换为字符串,保留 id、可访问名称、禁用状态和宿主输入样式 |
93
+ | `Select` | Input 的公共属性,加 `options: readonly {value,label}[]` | 渲染后端支持选项,空字符串代表全部;将组件库事件转换为字符串值 |
94
+ | `DateRangePicker` | `id`、`ariaLabel`、`disabled`;`value`/`onChange` 为 `readonly [string, string]`,`placeholder` 为可选起止文案二元组 | 使用宿主真正的日期范围组件;日期为 `YYYY-MM-DD`,完整范围一起回传,清空回传 `["", ""]`;不转换 UTC 时间戳或自行处理 Service 日期边界 |
95
+ | `Button` | `children`、`htmlType: button/submit`、`variant: primary/default/text`,可选 `onClick/disabled/loading/ariaLabel` | 映射宿主按钮属性;查询使用真正的 submit 按钮,其他操作使用 button;保留键盘、禁用和加载行为 |
96
+ | `Tabs` | `items: {value,label}[]`、`value`、`onChange(value)`、`ariaLabel`、`children` | 只在当前活动面板挂载一次 children;提供组件库页签样式、键盘操作与 tab/panel 可访问关联 |
97
+
98
+ `Tabs.value/onChange` 使用 `ApprovalListTab`。包固定“待我处理/已审批”,宿主不改变页签的业务含义。日期格式和起止关系由包在查询时统一校验;不合法时不调用 provider。
99
+
100
+ `components` 中的组件类型应在模块作用域定义并保持稳定,避免输入时重新挂载、丢失焦点。可以传入已定义组件组成的新对象,但不要在宿主 render 内定义组件函数。宿主组件须撑满包提供的字段容器。
101
+
102
+ 包 CSS 只控制页面/字段容器、间距、CanvasTable 和状态区域,不再对后代 button/input/select 或组件库内部结构写通用覆盖。控件的颜色、圆角、高度、焦点、弹层及禁用样式由宿主主题管理。Antd/dayjs 仅用于本仓库演示、测试和随包示例,不是审批包的运行时或 peer 依赖。
103
+
104
+ ### 列表布局与剩余高度
105
+
106
+ - 列表外层不绘制卡片边框和圆角;筛选区使用 `padding: 0 4px 12px`,左右 4px 用于容纳宿主控件的聚焦外框,工作区留白仍由宿主提供。表格网格线保留。
107
+ - 宿主传入 `className="make-app-approval--fill"` 时,包内结果区、表格框架和 Canvas 占满剩余高度;加载、空列表和失败状态共用这条高度链。不使用固定视口减数。
108
+ - 宿主须提供确定的内容区高度,并让注入的 Tabs 根节点、中间容器和活动面板使用 `display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0`。页签栏不收缩,列表页面不承担滚动;CanvasTable 自身管理滚动。
109
+ - Tabs 内部样式由宿主设置,包不引用 `.ant-tabs-*` 等组件库内部选择器。容器变化由已有 ResizeObserver 自动通知 CanvasTable,无需宿主重新创建表格。
110
+ - 未使用撑满类时,保留嵌入式表格的 `--make-approval-table-height`(默认 440px);撑满模式覆盖此固定高度。
111
+
112
+ 以上无边框及撑满布局属于当前源码更新,公网 0.1.0 尚未包含。旧版宿主可临时添加局部兼容样式,升级后移除包侧覆盖,保留宿主 Tabs 高度链。
113
+
114
+ 使用列表时必须传入 `components`。根入口和 `/react` 状态组件用法不变。完整 Antd 适配器见 [examples/antd-components.tsx](examples/antd-components.tsx),其他 React 组件库实现同一合同即可。
115
+
116
+ 列表入口将缺失状态和 UNSUBMITTED 显示为“状态未知”,不显示“—”,不合并、去重或过滤后端业务行。该兼容展示不代表这些值是合法审批结果。
117
+
118
+ ## 审批任务详情(本轮新增)
119
+
120
+ `/detail` 提供 `ApprovalTaskDetail`,由宿主放进既有抽屉、弹窗或页面;不在包内创建第二套弹层。必传 `scopeKey`、`reference: { processInstanceId, taskId }`、`provider: { loadDetail, submitAction }`、`components: { Button, Select, TextArea }`、`renderField`、`validateFields` 和 `onActionSuccess`。
121
+
122
+ `loadDetail` 返回 `ApprovalTaskDetailData`:任务标识、名称、流程状态、`fields` 完整字段描述、`formData`、`fieldPermission` 和 `actions`。`fields` 是宿主补齐的字段元数据,**不是声称后端任务详情已有同名字段**。宿主以任务授权为准取得元数据,不使用普通 IAM 截断后的字段集合代替。权限范围在 provider 边界标准化为 `FIELDS/NONE`;未知范围不能按全部字段处理。省略权限按无权限,省略动作按无动作。
123
+
124
+ 包只为可见字段调用 `renderField`,传入该字段的值、只读/禁用状态、错误与受控回调;不会把隐藏字段值传给渲染器。宿主复用已有字段控件、格式化、关联候选和附件能力。可见但不可编辑的字段必须只读;提交只包含用户修改且当前任务允许编辑的字段。审批按钮只来自 `actions`:APPROVE=同意、REJECT=拒绝、RETURN=退回修改、SUBMIT=提交。审批意见在退回/拒绝时必填、同意时选填;SUBMIT 沿用 `commentPolicy`,退回目标按 `returnTargets`;没有授权的操作不显示。
125
+
126
+ 包在发送动作前重新读取当前任务授权;权限或操作变化时阻止旧提交。可见字段的服务端值变化时,同样停止此次提交、展示最新值并要求重新确认;保留未冲突的字段草稿和审批意见,清除冲突字段的草稿并明确提示。隐藏字段的值变化不触发可见内容冲突。宿主 `submitAction` 必须走任务动作接口,不能通过普通记录更新绕过审批锁。后端保留最终身份、权限和并发校验。网络失败不自动重发动作,重新读取详情后才能继续;成功后即锁定当前办理界面,宿主回调负责关闭/刷新,不在前端移动页签记录。
127
+
128
+
129
+ 可选 `renderLayout?: (slots: ApprovalDetailLayoutSlots) => ReactNode` 接收 `title/actions/content/footer`。宿主将 actions 放入抽屉关闭 X 左侧,content 包含随正文滚动的审批意见;footer 为兼容旧接线保留且始终为空,不需要设置固定底栏。加载/错误也经过此布局。未传时使用包默认页面布局。可选 `history?: ReactNode` 为宿主流程内容插槽,省略时不显示流程区域,包不请求或合成历史数据。
130
+
131
+ 同意、拒绝、退回修改统一只校验当前可编辑且 `required === true` 的业务字段,SUBMIT 复用相同规则。包先校验必填空值并标红,再调用宿主 `validateFields` 补充这些字段的校验;回调只收到该字段集合及其值,没有待校验字段时不调用。只读字段与可编辑非必填字段不校验,也不接受它们的错误来阻塞动作;非必填字段的授权修改仍包含在 recordPatch 中。0、false 为有效填写值。
132
+
133
+ 审批意见在可办理详情正文中展示,随内容滚动,切换动作保留输入;退回/拒绝必填、同意选填,SUBMIT 沿用 commentPolicy。可见必填字段名称后始终显示红色星号,只读字段仍不参与校验;意见星号随当前动作变化。点击顶部动作聚焦意见输入框,宿主 TextArea 必须透传 id。控件与抽屉仍由宿主提供。完整 Antd 抽屉接线见 `examples/detail-integration.tsx`。
134
+
135
+ ## 申请人撤回合同
136
+
137
+ 根入口新增 `ApprovalWithdrawalInput`(`bpmKey/comment`)、`ApprovalWithdrawalResult`(`processInstanceId/recordId/status/processStatus/processEnded`)、`APPROVAL_WITHDRAW_COMMENT_MAX_LENGTH`(1024)和 `getApprovalWithdrawalCommentError(value)`。校验返回中文错误或 `undefined`;按 trim 后的非空内容和长度检查,宿主提交 trim 后的意见。
138
+
139
+ 包提供统一“已撤回”状态和意见校验,不请求接口、不推断流程发起人。宿主负责业务详情撤回入口、意见输入/确认、UI → Service 请求、错误 Toast、成功后刷新详情和列表;后端负责实例定位、发起人校验、结束/并发校验与业务记录异步解锁。撤回请求使用 `bpmKey` 而非 `processInstanceId`。发起人标识和停用流程的 key 来源由宿主按真实接口提供,不允许用业务申请人字段猜测;撤回后的普通编辑按根入口限制和普通 IAM 共同判断。
140
+
141
+ ## 0.2.0 日期范围组件迁移
142
+
143
+ 申请时间现改为必传 `components.DateRangePicker`,旧 `components.DatePicker` / `ApprovalDatePickerProps` 合同不再使用。宿主应改为组件库原生范围控件(Antd 为 `DatePicker.RangePicker`),并采用 `ApprovalDateRangePickerProps`,同一事件回传起止字符串二元组。缺少范围适配器直接报错,不回退到双单日期输入框。公开查询 DTO 的 startDate/endDate 不变。
144
+
145
+ 重置会清空筛选值,并重新挂载范围组件以丢弃尚未提交的半选草稿;普通输入及列表刷新不会重新挂载。宿主无需额外实现重置接口。
146
+
147
+ 该合同从 0.2.0 开始提供,公网原始 0.1.0 不包含这项改动。使用 0.1.0 补丁的宿主升级时,应同时升级 UI/Service 到 0.2.0,并移除对应 pnpm 补丁。
package/README.md CHANGED
@@ -1,3 +1,66 @@
1
- # Temporary Holding Version
1
+ # @qfei-design/make-app-approval
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Make App 审批公共 npm 包,统一审批相关 UI、交互和样式。源码由本独立项目维护;ExpensePoc 是首个接入应用。
4
+
5
+ **本次版本为 0.2.0。** 日期范围控件合同相较 0.1.0 有不兼容调整,升级前请阅读 [迁移说明](PUBLIC_API.md#020-日期范围组件迁移)。 已有基于 CanvasTable 的桌面审批列表、流程状态组件和普通记录编辑限制。已提供按节点授权的办理详情与动作组件;真实接口联调、流程表和移动审批页面尚未完成。接入先读 [包与宿主职责及接入步骤](docs/host-integration.md),再读 [公开 API](PUBLIC_API.md)。
6
+
7
+ ## 当前提供
8
+
9
+ - 根入口:流程状态映射、`bpmDefinitionKey` 识别、系统只读字段识别、普通记录编辑限制。纯逻辑,不依赖 React 或请求,可供 UI 和 App Service 使用。
10
+ - `/react`:`ApprovalStatus` 状态组件。
11
+ - `/list`:`MyApprovalsPage`,包内统一页签、搜索筛选、CanvasTable 虚拟滚动、分批加载、加载/空/错误/重试;宿主提供基础组件 `components`、查询 provider 和详情回调。
12
+ - `/detail`:`ApprovalTaskDetail`,统一节点字段权限、授权操作、意见/退回校验、确认提交和异常处理;宿主提供控件、字段渲染和任务 provider。三种操作统一只校验可编辑必填字段;通过 renderLayout 将按钮放在抽屉关闭 X 左侧,审批意见随正文滚动,退回/拒绝时必填、同意时选填。
13
+ - `/styles.css`:状态与列表的局部样式,可使用宿主主题变量。
14
+ - ESM / CommonJS 双格式、TypeScript 声明、行为测试及实际构建产物检查。
15
+
16
+ ## 本地开发
17
+
18
+ Node.js `>=22.12.0`,pnpm `10.20.0`,与现有 make-app 公共包一致。
19
+
20
+ ```sh
21
+ corepack pnpm install --frozen-lockfile
22
+ corepack pnpm run verify
23
+ corepack pnpm dev
24
+ corepack pnpm pack --pack-destination /tmp/make-app-approval
25
+ ```
26
+
27
+ `verify` 依次执行 CI 配置检查、测试、类型检查、构建和公开入口产物检查。CanvasTable 所属的 `@qfei-design` 作用域从 npmjs 安装,锁文件中的 CanvasTable 下载地址也指向 npmjs。其他依赖沿用公司 npm-group;安装前需在当前环境中注入只读变量 `NEXUS_NPM_READ_AUTH`,`.npmrc` 只引用变量名,不保存凭据值。
28
+
29
+ `dev` 启动明确标注模拟数据的列表演示,`build:demo` 验证演示构建。宿主模板 [examples/host-integration.tsx](examples/host-integration.tsx) 参与类型检查并随 npm 包交付。列表使用可选 peer `@qfei-design/canvas-table@^1.3.1`;使用列表的 UI 必须安装,单独使用根入口的 Service 无需安装。
30
+
31
+ ## 接入示例
32
+
33
+ Make App 的 UI 和 Service 固定使用相同的已发布版本。升级到 0.2.0 时移除旧版审批包的 pnpm 补丁;远端依赖安装直接从 npmjs 获取发布产物。
34
+
35
+ ```tsx
36
+ import {
37
+ APPROVAL_STATUS_FIELD,
38
+ getRecordApprovalWriteRestriction,
39
+ hasApprovalDefinition,
40
+ } from "@qfei-design/make-app-approval";
41
+ import { ApprovalStatus } from "@qfei-design/make-app-approval/react";
42
+ import "@qfei-design/make-app-approval/styles.css";
43
+
44
+ const approvalEnabled = hasApprovalDefinition(entity);
45
+ const restriction = getRecordApprovalWriteRestriction(record, { approvalEnabled });
46
+ const status = <ApprovalStatus value={record[APPROVAL_STATUS_FIELD]} />;
47
+ ```
48
+
49
+ `restriction` 为空时仍须检查原有操作、字段和行级权限。业务记录状态缺失时展示 `-`,未知非空状态展示“状态未知”;不要把加载失败当作未发起。已打开的表单在保存前仍需检查最新状态,后端写接口负责最终校验。
50
+
51
+ ## 包与宿主的分工
52
+
53
+ - 包目前统一列表、任务详情内容、节点字段权限和审批操作;宿主提供抽屉/页面容器及业务字段控件。
54
+ - 宿主处理导航、路由、应用上下文、登录及 App Service 请求。包处理列表刷新;宿主用 `refreshKey` 通知办理成功并刷新其他业务页面。包不读取 Token,不直接访问 Make 后端。
55
+ - `components` 必传:`Input`、`Select`、`DateRangePicker`、`Button`、`Tabs`。包固定查询/重置/日期校验/刷新功能,宿主适配控件事件和主题;没有原生控件兜底,也不要求宿主安装 Antd。Antd 宿主可复制 [适配示例](examples/antd-components.tsx),继承既有 `ConfigProvider`。
56
+ - 后端决定“待我处理 / 已审批”的归类、筛选、总数、排序和分页;前端不合并多次分页结果再过滤。
57
+ - 新建审批字段按 `bpm-start-config.fields` 接入 UI 和 Service,不能与普通 IAM 字段再取交集。已确认上游为网关 origin + `/make/workflow/v1/bpm-start-config`,字段元数据完整、与 Schema 一致;ExpensePoc 新建表单已完成代码接入、尚待真实字段和发起联调;该新建表单不属于包组件。全部 Workflow 资源共用 `/make/workflow/v1/` 前缀,网络适配仍由宿主提供。
58
+ - 普通业务入口禁编与任务内审批/退回修改是不同入口;包不会通过解锁业务记录来办理审批。
59
+
60
+ 完整公开 API 见 [PUBLIC_API.md](PUBLIC_API.md)。自动接入工具先读 [package.ai.json](package.ai.json)。
61
+
62
+ ## 发布
63
+
64
+ 采用与 `make-app-observability` 一致的 `@qfei/component-release-tools` 和 GitLab CI 流程。alpha/beta 由 dev/test 手动流水线触发,只发布内部 Nexus;latest 由受保护的 `vX.Y.Z` 标签手动任务触发,将同一份产物发布到内部 Nexus 和 npmjs。分支验证同时覆盖 React 19 和 React 18。
65
+
66
+ 完整的分支流程、初始化命令、CI 变量和失败重试说明见 [发布规范](docs/publishing.md)。远端仓库为 [make-app-approval](https://git.qtech.cn/front-end/make/make-app-approval),本地通过 `origin` 关联。0.1.0 已发布;本次 0.2.0 的发布状态以 GitLab 任务和 registry 查询为准。本地只运行验证和打包,不运行发布命令。不要仅凭本地版本号认定 registry 已有对应版本。
@@ -0,0 +1,76 @@
1
+ {
2
+ "packageName": "@qfei-design/make-app-approval",
3
+ "features": [
4
+ {
5
+ "id": "approval-status",
6
+ "status": "implemented",
7
+ "exports": [
8
+ "getApprovalStatusDisplay",
9
+ "ApprovalStatus"
10
+ ]
11
+ },
12
+ {
13
+ "id": "ordinary-record-edit-guard",
14
+ "status": "implemented",
15
+ "exports": [
16
+ "getRecordApprovalWriteRestriction",
17
+ "isApprovalSystemField",
18
+ "hasApprovalDefinition"
19
+ ]
20
+ },
21
+ {
22
+ "id": "my-approvals",
23
+ "status": "implemented",
24
+ "reason": "CanvasTable 列表与宿主 provider 约定已实现;真实 BPM 对接仍待后端最终接口",
25
+ "exports": [
26
+ "MyApprovalsPage"
27
+ ],
28
+ "docPath": "docs/host-integration.md"
29
+ },
30
+ {
31
+ "id": "host-ui-components",
32
+ "status": "implemented",
33
+ "reason": "宿主必传 Input/Select/DateRangePicker/Button/Tabs;包固定功能并仅提供布局,包含可验证的 Antd 适配示例",
34
+ "exports": [
35
+ "ApprovalUIComponents"
36
+ ],
37
+ "docPath": "docs/host-integration.md"
38
+ },
39
+ {
40
+ "id": "task-detail",
41
+ "status": "implemented",
42
+ "reason": "节点授权、统一可编辑必填校验、宿主控件/字段/布局槽、红色必填星号、顶部操作与正文内滚动意见已实现;真实接口联调尚未完成",
43
+ "exports": [
44
+ "ApprovalTaskDetail"
45
+ ],
46
+ "docPath": "docs/host-integration.md"
47
+ },
48
+ {
49
+ "id": "approval-history",
50
+ "status": "waiting-for-api",
51
+ "reason": "按约定等待后端统一表格接口"
52
+ },
53
+ {
54
+ "id": "mobile-approval-pages",
55
+ "status": "planned",
56
+ "reason": "当前尚未提供移动审批页面,后续复用既有 make-app-mobile"
57
+ },
58
+ {
59
+ "id": "live-bpm-integration",
60
+ "status": "waiting-for-api",
61
+ "reason": "Workflow 前缀已确认;任务页签归类查询、分页与列表字段最终约定待确认"
62
+ },
63
+ {
64
+ "id": "applicant-withdrawal-contract",
65
+ "status": "implemented",
66
+ "reason": "提供 CANCELLED 状态、撤回请求/响应类型及意见校验;宿主入口所需数据来源和撤回后编辑规则待确认,不包含独立撤回组件或 HTTP 请求",
67
+ "exports": [
68
+ "ApprovalWithdrawalInput",
69
+ "ApprovalWithdrawalResult",
70
+ "APPROVAL_WITHDRAW_COMMENT_MAX_LENGTH",
71
+ "getApprovalWithdrawalCommentError"
72
+ ],
73
+ "docPath": "docs/host-integration.md"
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,2 @@
1
+ import type { ApprovalTaskDetailProps } from "./types.js";
2
+ export declare const ApprovalTaskDetail: (props: ApprovalTaskDetailProps) => import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { ApprovalDetailEvent, ApprovalTaskDetailProps } from "./types.js";
2
+ export declare const reportDetail: (logger: ApprovalTaskDetailProps["logger"], event: ApprovalDetailEvent) => void;
@@ -0,0 +1,2 @@
1
+ /** 比较宿主返回的字段值;对象键顺序不影响内容,数组顺序保留。 */
2
+ export declare const sameFieldValue: (left: unknown, right: unknown) => boolean;
@@ -0,0 +1,3 @@
1
+ export { ApprovalTaskDetail } from "./approval-task-detail.js";
2
+ export { readApprovalTaskDetail, getApprovalTaskFields, buildApprovalTaskAction, ApprovalTaskValidationError, } from "./task-policy.js";
3
+ export type * from "./types.js";
@@ -0,0 +1,18 @@
1
+ import type { ApprovalActionType, ApprovalActionOption, ApprovalTaskField, ApprovalTaskAction, ApprovalTaskDetailData } from "./types.js";
2
+ export declare const readApprovalTaskDetail: (value: unknown) => ApprovalTaskDetailData;
3
+ export declare const getApprovalTaskFields: (detail: ApprovalTaskDetailData) => {
4
+ editable: boolean;
5
+ key: string;
6
+ label: string;
7
+ required?: boolean;
8
+ schema?: Readonly<Record<string, unknown>>;
9
+ }[];
10
+ export declare class ApprovalTaskValidationError extends Error {
11
+ readonly fieldErrors: Readonly<Record<string, string>>;
12
+ constructor(message: string, fieldErrors?: Readonly<Record<string, string>>);
13
+ }
14
+ /** 三个审批动作按产品规则;发起人 SUBMIT 保留已有节点配置。 */
15
+ export declare const isApprovalCommentRequired: (action: ApprovalActionOption) => boolean;
16
+ /** 调用方仅传当前可编辑且必填的字段;0 和 false 均为已填写。 */
17
+ export declare const getApprovalRequiredFieldErrors: (fields: readonly ApprovalTaskField[], values: Readonly<Record<string, unknown>>) => Readonly<Record<string, string>>;
18
+ export declare const buildApprovalTaskAction: (detail: ApprovalTaskDetailData, actionType: ApprovalActionType, patch: Readonly<Record<string, unknown>>, comment: string, targetNodeKey: string) => ApprovalTaskAction;
@@ -0,0 +1,111 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import type { ApprovalButtonProps, ApprovalSelectProps, ApprovalInputProps } from "../list/ui-components.js";
3
+ export type ApprovalActionType = "APPROVE" | "REJECT" | "RETURN" | "SUBMIT";
4
+ export type ApprovalActionOption = Readonly<{
5
+ actionType: ApprovalActionType;
6
+ /** SUBMIT 沿用节点配置;RETURN/REJECT 意见必填,APPROVE 意见选填。 */
7
+ commentPolicy: "REQUIRED" | "OPTIONAL";
8
+ returnTargets?: readonly Readonly<{
9
+ nodeKey: string;
10
+ nodeName: string;
11
+ }>[];
12
+ }>;
13
+ export type ApprovalFieldPermission = Readonly<{
14
+ scope: "FIELDS";
15
+ fields: readonly string[];
16
+ }> | Readonly<{
17
+ scope: "NONE";
18
+ }>;
19
+ export type ApprovalTaskField = Readonly<{
20
+ key: string;
21
+ label: string;
22
+ required?: boolean;
23
+ /** 宿主提供完整字段元数据;包不绑定某一 App 的 Schema 类型。 */
24
+ schema?: Readonly<Record<string, unknown>>;
25
+ }>;
26
+ export type ApprovalTaskReference = Readonly<{
27
+ processInstanceId: string;
28
+ taskId: string;
29
+ }>;
30
+ export type ApprovalTaskDetailData = ApprovalTaskReference & Readonly<{
31
+ taskName: string;
32
+ processStatus: string;
33
+ fields: readonly ApprovalTaskField[];
34
+ formData: Readonly<Record<string, unknown>>;
35
+ fieldPermission?: Readonly<{
36
+ viewPermission?: ApprovalFieldPermission;
37
+ editPermission?: ApprovalFieldPermission;
38
+ }>;
39
+ actions: readonly ApprovalActionOption[];
40
+ }>;
41
+ export type ApprovalTaskAction = ApprovalTaskReference & Readonly<{
42
+ actionType: ApprovalActionType;
43
+ comment: string;
44
+ targetNodeKey?: string;
45
+ recordPatch: Readonly<Record<string, unknown>>;
46
+ }>;
47
+ export type ApprovalTaskActionResult = ApprovalTaskReference & Readonly<{
48
+ actionType: ApprovalActionType;
49
+ processStatus: string;
50
+ processEnded: boolean;
51
+ }>;
52
+ export type ApprovalDetailProvider = Readonly<{
53
+ loadDetail: (reference: ApprovalTaskReference, context: {
54
+ signal: AbortSignal;
55
+ }) => Promise<ApprovalTaskDetailData>;
56
+ submitAction: (input: ApprovalTaskAction, context: {
57
+ signal: AbortSignal;
58
+ }) => Promise<ApprovalTaskActionResult>;
59
+ }>;
60
+ export type ApprovalTextAreaProps = ApprovalInputProps & Readonly<{
61
+ maxLength: number;
62
+ required: boolean;
63
+ }>;
64
+ export type ApprovalDetailComponents = Readonly<{
65
+ Button: ComponentType<ApprovalButtonProps>;
66
+ Select: ComponentType<ApprovalSelectProps>;
67
+ TextArea: ComponentType<ApprovalTextAreaProps>;
68
+ }>;
69
+ export type ApprovalFieldRenderProps = Readonly<{
70
+ field: ApprovalTaskField;
71
+ id: string;
72
+ value: unknown;
73
+ readOnly: boolean;
74
+ disabled: boolean;
75
+ error?: string;
76
+ onChange?: (value: unknown) => void;
77
+ }>;
78
+ export type ApprovalFieldValidationContext = Readonly<{
79
+ /** 仅当前可见、可编辑且必填的字段;非必填字段不参与任何前端校验。 */
80
+ fields: readonly ApprovalTaskField[];
81
+ /** 仅以上字段的当前值。 */
82
+ values: Readonly<Record<string, unknown>>;
83
+ actionType: ApprovalActionType;
84
+ signal: AbortSignal;
85
+ }>;
86
+ export type ApprovalDetailEvent = Readonly<{
87
+ operation: "detail" | "action" | "notify";
88
+ phase: "start" | "success" | "error" | "cancel" | "skip";
89
+ actionType?: ApprovalActionType;
90
+ }>;
91
+ export type ApprovalDetailLayoutSlots = Readonly<{
92
+ title: ReactNode;
93
+ actions: ReactNode;
94
+ content: ReactNode;
95
+ /** 兼容旧布局接线,当前始终为空。审批意见已包含在 content 中,随正文滚动。 */
96
+ footer: ReactNode;
97
+ }>;
98
+ export type ApprovalTaskDetailProps = Readonly<{
99
+ scopeKey: string;
100
+ reference: ApprovalTaskReference;
101
+ provider: ApprovalDetailProvider;
102
+ components: ApprovalDetailComponents;
103
+ renderField: (props: ApprovalFieldRenderProps) => ReactNode;
104
+ /** 宿主 Drawer 把 actions 放在关闭按钮左侧;content 包含审批意见,不另设固定底栏。 */
105
+ renderLayout?: (slots: ApprovalDetailLayoutSlots) => ReactNode;
106
+ /** 已取得的审批流程展示内容;包不请求或推断流程历史。未提供时不展示该区域。 */
107
+ history?: ReactNode;
108
+ validateFields: (context: ApprovalFieldValidationContext) => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
109
+ onActionSuccess: (result: ApprovalTaskActionResult) => void | Promise<void>;
110
+ logger?: (event: ApprovalDetailEvent) => void;
111
+ }>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react/jsx-runtime"),l=require("react"),be=require("./react.cjs"),ge=require("./index.cjs"),E=e=>!!e&&typeof e=="object"&&!Array.isArray(e),b=e=>typeof e=="string"&&e.trim().length>0,ve=["APPROVE","REJECT","RETURN","SUBMIT"],C=e=>{if(e===void 0)return[];if(!E(e))throw new Error("审批字段权限格式无效");if(e.scope==="NONE"&&e.fields===void 0)return[];if(e.scope!=="FIELDS"||!Array.isArray(e.fields)||!e.fields.every(b)||new Set(e.fields).size!==e.fields.length)throw new Error("审批字段权限范围无效");return e.fields},je=e=>E(e)&&b(e.key)&&b(e.label)&&(e.required===void 0||typeof e.required=="boolean")&&(e.schema===void 0||E(e.schema)),we=e=>{if(!E(e)||!ve.some(s=>s===e.actionType)||e.commentPolicy!=="REQUIRED"&&e.commentPolicy!=="OPTIONAL")return!1;const t=e.returnTargets;return t===void 0||Array.isArray(t)&&t.every(s=>E(s)&&b(s.nodeKey)&&b(s.nodeName))&&new Set(t.map(s=>s.nodeKey)).size===t.length},W=e=>{if(!E(e)||!b(e.taskId)||!b(e.processInstanceId)||!b(e.taskName)||!b(e.processStatus)||!Array.isArray(e.fields)||!e.fields.every(je)||!E(e.formData))throw new Error("审批详情数据不完整");const t=e.fields.map(n=>n.key);if(new Set(t).size!==t.length)throw new Error("审批字段标识重复");if(e.fieldPermission!==void 0&&!E(e.fieldPermission))throw new Error("审批字段权限格式无效");const s=e.fieldPermission,o=C(s?.viewPermission),p=C(s?.editPermission);if(o.some(n=>!t.includes(n))||p.some(n=>!o.includes(n)))throw new Error("审批字段元数据或权限不一致");const d=e.actions??[];if(!Array.isArray(d)||!d.every(we)||new Set(d.map(n=>n.actionType)).size!==d.length)throw new Error("审批操作配置无效");return{taskId:e.taskId,processInstanceId:e.processInstanceId,taskName:e.taskName,processStatus:e.processStatus,fields:e.fields,formData:e.formData,actions:d,fieldPermission:{viewPermission:{scope:"FIELDS",fields:o},editPermission:{scope:"FIELDS",fields:p}}}},P=e=>{const t=new Set(C(e.fieldPermission?.viewPermission)),s=new Set(C(e.fieldPermission?.editPermission));return e.fields.filter(o=>t.has(o.key)).map(o=>({...o,editable:s.has(o.key)&&!ge.isApprovalSystemField(o.key)}))};class k extends Error{constructor(t,s={}){super(t),this.fieldErrors=s,this.name="ApprovalTaskValidationError"}fieldErrors}const z=e=>e.actionType==="RETURN"||e.actionType==="REJECT"||e.actionType==="SUBMIT"&&e.commentPolicy==="REQUIRED",Te=(e,t)=>Object.fromEntries(e.filter(s=>{const o=t[s.key];return o==null||typeof o=="string"&&!o.trim()||Array.isArray(o)&&o.length===0}).map(s=>[s.key,`请填写${s.label}`])),Q=(e,t,s,o,p)=>{const d=e.actions.find(i=>i.actionType===t);if(!d)throw new k("当前节点不允许此操作,请刷新详情");const n=new Set(P(e).filter(i=>i.editable).map(i=>i.key));if(Object.keys(s).some(i=>!n.has(i)))throw new k("字段权限已变化,请重新加载详情");if(z(d)&&!o.trim())throw new k("请填写审批意见");if(o.length>1024)throw new k("审批意见不能超过 1024 个字符");if(t==="RETURN"&&!d.returnTargets?.some(i=>i.nodeKey===p))throw new k("请选择后台配置的退回节点");return{taskId:e.taskId,processInstanceId:e.processInstanceId,actionType:t,comment:o.trim(),recordPatch:{...s},...t==="RETURN"?{targetNodeKey:p}:{}}},h=(e,t)=>{try{e?e(t):t.phase==="error"?console.warn("[make-app-approval]",t):console.info("[make-app-approval]",t)}catch{console.warn("[make-app-approval] diagnostic sink failed")}},G=(e,t)=>{if(Object.is(e,t))return!0;if(e instanceof Date&&t instanceof Date)return Object.is(e.getTime(),t.getTime());if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((p,d)=>G(p,t[d]));if(!e||!t||typeof e!="object"||typeof t!="object"||![Object.prototype,null].includes(Object.getPrototypeOf(e))||![Object.prototype,null].includes(Object.getPrototypeOf(t)))return!1;const s=e,o=t;return Object.keys(s).length===Object.keys(o).length&&Object.keys(s).every(p=>Object.hasOwn(o,p)&&G(s[p],o[p]))},R={APPROVE:"同意",REJECT:"拒绝",RETURN:"退回修改",SUBMIT:"提交"},H=(e,t)=>e.taskId===t.reference.taskId&&e.processInstanceId===t.reference.processInstanceId,Ee=({title:e,actions:t,content:s,footer:o})=>a.jsxs("section",{className:"make-app-approval-detail","aria-label":"审批详情",children:[a.jsxs("header",{className:"make-app-approval-detail-header",children:[e,t]}),a.jsx("div",{className:"make-app-approval-detail-content",children:s}),o&&a.jsx("footer",{className:"make-app-approval-detail-footer",children:o})]}),ce=e=>a.jsxs("div",{className:"make-app-approval-detail-title",children:[a.jsx("h2",{children:"审批详情"}),e&&a.jsx(be.ApprovalStatus,{value:e})]}),Ae=e=>{const[t,s]=l.useState(0),[o,p]=l.useState(),d=l.useRef(e);d.current=e,l.useEffect(()=>{const i=new AbortController,f=d.current;return h(f.logger,{operation:"detail",phase:"start"}),(async()=>{try{const c=W(await f.provider.loadDetail(f.reference,{signal:i.signal}));if(i.signal.aborted)return;if(!H(c,f))throw new Error("Task identity mismatch");p({revision:t,detail:c}),h(f.logger,{operation:"detail",phase:"success"})}catch{if(i.signal.aborted)return;p({revision:t,error:!0}),h(f.logger,{operation:"detail",phase:"error"})}})(),()=>{i.abort(),h(f.logger,{operation:"detail",phase:"cancel"})}},[t]);const n=o?.revision===t?o:void 0;return a.jsx(xe,{...e,data:n?.detail,loadError:n?.error===!0,reload:()=>s(i=>i+1)},t)},xe=e=>{const{components:{Button:t,Select:s,TextArea:o},renderField:p,reload:d}=e,n=l.useId(),[i,f]=l.useState(),c=i??e.data,[g,q]=l.useState({}),[$,S]=l.useState(),[B,X]=l.useState(""),[U,D]=l.useState(""),[F,L]=l.useState({}),[Y,v]=l.useState(""),[j,Z]=l.useState(!1),[O,le]=l.useState(!1),[N,de]=l.useState(!1),J=l.useRef(!1),K=l.useRef(new AbortController);l.useEffect(()=>(K.current=new AbortController,()=>K.current.abort()),[]);const _=c?P(c):[],w=c?.actions.find(r=>r.actionType===$),ee=w?z(w):!1,te=c?.actions.filter(z)??[],ae=j||O||N,re=e.renderLayout??Ee,pe=async()=>{if(!c||!$||J.current||O||N)return;J.current=!0,Z(!0),v(""),L({});const r=K.current.signal,y=$;let A=!1;h(e.logger,{operation:"action",phase:"start",actionType:y});try{Q(c,y,g,B,U);const m=W(await e.provider.loadDetail(e.reference,{signal:r}));if(r.aborted)return;if(!H(m,e))throw new Error("Task identity mismatch");const he=P(c),V=P(m);if(JSON.stringify(he)!==JSON.stringify(V)||JSON.stringify(c.actions)!==JSON.stringify(m.actions)){f(m),q({}),S(void 0),X(""),D(""),v("当前节点的字段或操作权限已变化,请核对后重新操作"),h(e.logger,{operation:"action",phase:"skip",actionType:y});return}const se=V.filter(u=>!G(c.formData[u.key],m.formData[u.key]));if(se.length){const u=new Set(se.map(x=>x.key)),M=Object.keys(g).some(x=>u.has(x));f(m),q(Object.fromEntries(Object.entries(g).filter(([x])=>!u.has(x)))),S(void 0),D(""),v(M?"申请内容已更新,冲突字段的本地修改已清除,其他修改已保留,请核对后重新操作":"申请内容已更新,您的修改已保留,请核对后重新操作"),h(e.logger,{operation:"action",phase:"skip",actionType:y});return}const I=V.filter(u=>u.editable&&u.required===!0),oe=Object.fromEntries(I.map(u=>[u.key,Object.hasOwn(g,u.key)?g[u.key]:m.formData[u.key]])),ne=Te(I,oe);if(Object.keys(ne).length)throw new k("请检查表单中标红的字段",ne);const fe=I.length?await e.validateFields({fields:I,values:oe,actionType:y,signal:r}):{};if(r.aborted)return;const ie=Object.fromEntries(Object.entries(fe).filter(([u,M])=>I.some(x=>x.key===u)&&!!M));if(Object.keys(ie).length)throw new k("请检查表单中标红的字段",ie);const ke=Q(m,y,g,B,U);A=!0;const T=await e.provider.submitAction(ke,{signal:r});if(r.aborted)return;if(!T||!H(T,e)||T.actionType!==y||typeof T.processEnded!="boolean"||typeof T.processStatus!="string"||!T.processStatus.trim())throw new Error("Action result mismatch");de(!0),S(void 0),f({...m,processStatus:T.processStatus}),v("处理成功"),h(e.logger,{operation:"action",phase:"success",actionType:y}),h(e.logger,{operation:"notify",phase:"start",actionType:y});try{await e.onActionSuccess(T),h(e.logger,{operation:"notify",phase:"success",actionType:y})}catch{r.aborted||v("处理已成功,但页面刷新未完成,请刷新列表确认"),h(e.logger,{operation:"notify",phase:"error",actionType:y})}}catch(m){if(r.aborted)return;!A&&m instanceof k?(v(m.message),L(m.fieldErrors)):(le(!0),S(void 0),v(A?"操作结果尚未确认,请重新加载详情核查,勿重复提交":"审批权限或字段校验暂不可用,请重新加载详情")),h(e.logger,{operation:"action",phase:"error",actionType:y})}finally{J.current=!1,r.aborted?h(e.logger,{operation:"action",phase:"cancel",actionType:y}):Z(!1)}};if(!c)return re({title:ce(),actions:null,footer:null,content:e.loadError?a.jsxs("div",{className:"make-app-approval-detail-section",role:"alert",children:["审批详情加载失败,请重试",a.jsx(t,{htmlType:"button",variant:"default",onClick:d,children:"重新加载详情"})]}):a.jsx("div",{className:"make-app-approval-detail-section",role:"status",children:"正在加载审批详情…"})});const me=!O&&!N&&c.actions.length>0?a.jsx("div",{className:"make-app-approval-detail-actions","aria-label":"审批操作",children:c.actions.map(r=>a.jsx(t,{htmlType:"button",variant:r.actionType==="APPROVE"||r.actionType==="SUBMIT"?"primary":"default",disabled:j,onClick:()=>{S(r.actionType),D(""),v(""),document.getElementById(`${n}-comment`)?.focus()},children:R[r.actionType]},r.actionType))}):null,ye=a.jsxs("div",{className:"make-app-approval-detail-content","aria-busy":j,children:[a.jsxs("section",{className:"make-app-approval-detail-section","aria-label":"申请信息",children:[a.jsx("h3",{children:"申请信息"}),a.jsxs("div",{className:"make-app-approval-detail-fields",children:[_.length===0&&a.jsx("p",{children:"当前节点未配置可见字段"}),_.map(r=>a.jsxs("div",{className:"make-app-approval-detail-field","data-invalid":!!F[r.key],children:[a.jsxs("label",{htmlFor:`${n}-${r.key}`,children:[r.label,r.required?a.jsxs("span",{className:"make-app-approval-required","aria-label":"必填",children:[" ","*"]}):null]}),a.jsxs("div",{className:"make-app-approval-detail-value",children:[p({field:r,id:`${n}-${r.key}`,value:Object.hasOwn(g,r.key)?g[r.key]:c.formData[r.key],readOnly:!r.editable,disabled:ae,error:F[r.key],...r.editable&&!ae?{onChange:y=>{q(A=>({...A,[r.key]:y})),L(A=>{const m={...A};return delete m[r.key],m})}}:{}}),F[r.key]&&a.jsx("p",{role:"alert",className:"make-app-approval-detail-error",children:F[r.key]})]})]},r.key))]})]}),e.history!=null&&a.jsxs("section",{className:"make-app-approval-detail-section make-app-approval-detail-history","aria-label":"审批流程",children:[a.jsx("h3",{children:"审批流程"}),e.history]}),Y&&a.jsx("p",{role:N?"status":"alert",className:`make-app-approval-detail-message${N?"":" make-app-approval-detail-error"}`,children:Y}),O&&a.jsx("div",{className:"make-app-approval-detail-message",children:a.jsx(t,{htmlType:"button",variant:"default",onClick:d,children:"重新加载详情"})})]}),ue=!O&&!N&&c.actions.length>0?a.jsxs("div",{className:"make-app-approval-detail-section make-app-approval-detail-opinion",children:[a.jsxs("label",{htmlFor:`${n}-comment`,children:["审批意见",ee&&a.jsxs("span",{className:"make-app-approval-required","aria-label":"必填",children:[" ","*"]})]}),a.jsx(o,{id:`${n}-comment`,ariaLabel:"审批意见",value:B,onChange:X,placeholder:te.length?`请输入审批意见,${te.map(r=>R[r.actionType]).join("、")}时必填`:"请输入审批意见",disabled:j,required:ee,maxLength:1024}),w&&a.jsxs("section",{className:"make-app-approval-action-confirm","aria-label":`确认${R[w.actionType]}`,children:[a.jsxs("h3",{children:["确认",R[w.actionType]]}),w.actionType==="RETURN"&&a.jsxs("div",{className:"make-app-approval-detail-target",children:[a.jsx("label",{htmlFor:`${n}-target`,children:"退回节点"}),a.jsx(s,{id:`${n}-target`,ariaLabel:"退回节点",value:U,onChange:D,disabled:j,options:[{value:"",label:"请选择退回节点"},...(w.returnTargets??[]).map(r=>({value:r.nodeKey,label:r.nodeName}))]})]}),a.jsxs("div",{className:"make-app-approval-detail-actions",children:[a.jsxs(t,{htmlType:"button",variant:"primary",disabled:j,loading:j,onClick:()=>{pe()},children:["确认",R[w.actionType]]}),a.jsx(t,{htmlType:"button",variant:"default",disabled:j,onClick:()=>S(void 0),children:"取消"})]})]})]}):null;return re({title:ce(c.processStatus),actions:me,content:a.jsxs(a.Fragment,{children:[ye,ue]}),footer:null})},Se=e=>{if(!e.scopeKey.trim()||!e.reference.taskId.trim()||!e.reference.processInstanceId.trim())throw new Error("ApprovalTaskDetail requires scopeKey and task reference");for(const t of["Button","Select","TextArea"])if(!e.components?.[t])throw new Error(`ApprovalTaskDetail requires components.${t}`);return a.jsx(Ae,{...e},JSON.stringify([e.scopeKey,e.reference.processInstanceId,e.reference.taskId]))};exports.ApprovalTaskDetail=Se;exports.ApprovalTaskValidationError=k;exports.buildApprovalTaskAction=Q;exports.getApprovalTaskFields=P;exports.readApprovalTaskDetail=W;