按钮
按钮交互事件定义了表单运行时按钮与 Studio 脚本之间的运行时协议。
它适用于 按钮设计 中配置的预设按钮、自定义按钮、点击前脚本、自定义动作脚本和点击后脚本。
适用场景
当按钮动作需要以下能力时,使用按钮脚本:
- 在动作继续前做表单校验
- 在继续前要求用户确认
- 在保存或提交前修改主表或子表草稿值
- 从自定义按钮新增或删除子表行
- 展示运行时消息、通知或弹窗
执行阶段
| 阶段 | 触发时机 | 是否可以中断按钮动作 |
|---|---|---|
beforeClick | 按钮动作开始前 | 是 |
action | 自定义按钮主动作 | 是 |
afterClick | 按钮动作成功后 | 否 |
对预设按钮来说,action 是系统内置动作。对自定义按钮来说,action 会执行配置的 Studio 脚本。
脚本函数
type FormButtonRuntimePayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'form_button';
phase: 'beforeClick' | 'action' | 'afterClick';
};
draft: { data: Record<string, unknown> } | null;
};
type StudioRuntimeContext = Record<string, unknown>;
export default async function main(
payload: FormButtonRuntimePayload,
_context: StudioRuntimeContext,
) {
return {
ok: true,
effects: [],
};
}
脚本不需要 Studio 运行时上下文时,参数名使用 _context 即可,这样严格 TypeScript 编辑器不会提示未使用参数。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
payload | FormButtonRuntimePayload | 是 | 按钮执行器发送的运行时入参 |
context | object | 是 | Studio 执行器提供的运行时上下文 |
入参 Payload
type FormButtonRuntimePayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'form_button';
buttonId: string;
buttonType: 'preset' | 'custom';
placement: string;
operation: string;
phase: 'beforeClick' | 'action' | 'afterClick';
};
form: RuntimeFormRef | null;
record: RuntimeRecord | null;
draft: { data: Record<string, unknown> } | null;
selection: { records: RuntimeRecord[]; childRows: RuntimeChildRow[] };
child: RuntimeChildContext | null;
input: Record<string, unknown>;
result: unknown | null;
runtime: RuntimeMetadata;
};
基础上下文
| 字段 | 类型 | 说明 |
|---|---|---|
schema | 'soln.runtime-context/v1' | 统一运行时上下文版本 |
source.phase | 'beforeClick' | 'action' | 'afterClick' | 当前执行阶段 |
form.id | string | null | 表单 schema ID |
form.apiName | string | null | 当前表单 API 名称 |
source.buttonId | string | 按钮 key,预设按钮常见值如 form.submit |
source.buttonType | 'preset' | 'custom' | 预设按钮或自定义按钮 |
source.placement | string | 运行时位置,如 FORM_HEADER、LIST_ROW、LIST_BATCH |
source.operation | string | 当前动作类型,如 submit、save、saveDraft、custom |
draft.data | Record<string, unknown> | 当前草稿值,按字段 API 名称索引 |
record | { id: string } | null | 当前记录身份;新建页通常为 null;按钮绑定不会预加载记录数据 |
result | Record<string, unknown> | null | 已完成动作的结果,主要在 afterClick 使用 |
runtime.locale | string | 浏览器语言 |
runtime.timezone | string | 浏览器时区 |
当前表单值从 payload.draft.data 读取。脚本确实需要持久化值时,先确认 payload.record?.id 存在,再调用 context.form.get({ recordId: payload.record.id })。
子表上下文
| 字段 | 类型 | 说明 |
|---|---|---|
child.form | object | 子表头部、批量工具栏或行按钮点击时的子表单 |
child.row | object | null | 子表行按钮的当前行草稿数据 |
selection.childRows | RuntimeChildRow[] | 子表批量按钮选中的行 |
子表草稿也可以从 payload.draft.data.__childFormDrafts[childFormApiName] 读取,值为一组子表行对象。
| 按钮位置 | 额外上下文 |
|---|---|
FORM_LIST_HEADER | child.form |
FORM_LIST_BATCH | child.form 和 selection.childRows |
FORM_LIST_ROW | child.form 和 child.row |
返回 Response
未知字段会被忽略。
type FormButtonRuntimeResponse = {
ok?: boolean;
preventDefault?: boolean;
message?: string;
fieldErrors?: Record<string, string>;
patch?: {
draft?: Record<string, unknown>;
record?: Record<string, unknown>;
};
effects?: FormRuntimeEffect[];
notification?: RuntimeNotificationShortcut;
modal?: RuntimeModalShortcut;
};
| 字段 | 类型 | 说明 |
|---|---|---|
ok | boolean | 当前阶段是否继续;缺省按 true 处理 |
preventDefault | boolean | 为 true 时显式停止当前按钮动作 |
message | string | 展示简单消息 |
fieldErrors | Record<string, string> | 设置字段校验错误 |
patch.draft | Record<string, unknown> | 合并到当前草稿状态 |
patch.record | Record<string, unknown> | 合并到当前记录状态 |
effects | FormRuntimeEffect[] | 有序执行的运行时 effects |
notification | object | string | 顶层通知快捷写法 |
modal | object | string | 顶层弹窗快捷写法 |
当 ok 为 false、preventDefault 为 true,或带 awaitAction: true 的确认弹窗被取消时,运行时会停止当前按钮动作。
Effects
Effects 是运行时 UI 指令,不会修改字段设计配置。
| Effect | 用途 |
|---|---|
fieldState | 覆盖主表字段隐藏、必填、只读或禁用状态 |
fieldOptionFilter | 按稳定选项 value 过滤主表选项字段的配置选项 |
fieldValue | 设置主表字段值 |
fieldError | 展示主表字段校验错误 |
fieldErrorClear | 清除主表字段校验错误 |
childFieldState | 覆盖子表字段隐藏、必填、只读或禁用状态 |
childRowState | 覆盖子表行状态 |
childFieldValue | 设置子表字段值 |
childFieldError | 展示子表字段校验错误 |
childFieldErrorClear | 清除子表字段校验错误 |
childRowAdd | 追加一行或多行子表草稿 |
childRowRemove | 删除一行或多行子表草稿 |
message | 展示简单消息 |
notification | 展示通知 |
modal | 展示弹窗 |
refresh | 请求运行时刷新 |
主表字段 Effects
{
type: 'fieldState',
field: 'closeReason',
hidden: false,
required: true,
readonly: false,
disabled: false,
}
{
type: 'fieldValue',
field: 'status',
value: 'submitted',
}
{
type: 'fieldError',
field: 'amount',
message: 'Amount must be greater than zero.',
}
| 字段 | 类型 | 适用 effect | 说明 |
|---|---|---|---|
type | string | 全部 | fieldState、fieldOptionFilter、fieldValue、fieldError 或 fieldErrorClear |
field | string | 全部 | 主表字段 API 名称 |
allowedValues | string[] | fieldOptionFilter | 允许继续选择的稳定选项 value |
clearInvalidValue | boolean | fieldOptionFilter | 清除不再允许的当前值 |
reset | boolean | fieldOptionFilter | 恢复字段原始配置选项 |
hidden | boolean | fieldState | 临时隐藏或显示字段 |
required | boolean | fieldState | 临时修改是否必填 |
readonly | boolean | fieldState | 临时修改是否只读 |
disabled | boolean | fieldState | 临时修改是否禁用 |
value | unknown | fieldValue | 新字段值 |
message | string | fieldError | 字段错误文案 |
子表字段 Effects
使用 table 指定子表单 API 名称,也可以使用 childTable、formApiName 或 childFormApiName。
使用 where 按子表字段值筛选行。省略 where 时,effect 作用于当前子表的所有行。
{
type: 'childFieldState',
table: 'orderItems',
field: 'discount',
where: {
logic: 'and',
children: [
{ field: 'productType', operator: 'eq', value: 'service' },
],
},
readonly: true,
required: false,
}
{
type: 'childFieldValue',
table: 'orderItems',
field: 'currency',
where: {
logic: 'and',
children: [
{ field: 'region', operator: 'eq', value: 'US' },
],
},
value: 'USD',
}
{
type: 'childFieldError',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [
{ field: 'quantity', operator: 'lte', value: 0 },
],
},
message: 'Quantity must be greater than zero.',
}
{
type: 'childFieldErrorClear',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [
{ field: 'quantity', operator: 'gt', value: 0 },
],
},
}
| 字段 | 类型 | 适用 effect | 说明 |
|---|---|---|---|
type | string | 全部 | childFieldState、childFieldValue、childFieldError 或 childFieldErrorClear |
table | string | 全部 | 子表单 API 名称 |
field | string | 全部 | 子表字段 API 名称 |
where | ApprovalFlowConditionGroup | 全部 | 行筛选条件 |
hidden | boolean | childFieldState | 隐藏或显示子表字段;隐藏字段会清空草稿值 |
required | boolean | childFieldState | 临时修改是否必填 |
readonly | boolean | childFieldState | 临时修改是否只读 |
disabled | boolean | childFieldState | 临时修改是否禁用 |
value | unknown | childFieldValue | 新子表字段值 |
message | string | childFieldError | 子表字段错误文案 |
子表行 Effects
{
type: 'childRowAdd',
table: 'orderItems',
rows: [
{
productName: 'Standard service',
quantity: 1,
currency: 'USD',
},
{
productName: 'Implementation',
quantity: 1,
currency: 'USD',
},
],
}
{
type: 'childRowRemove',
table: 'orderItems',
where: {
logic: 'and',
children: [
{ field: 'status', operator: 'eq', value: 'cancelled' },
],
},
}
| 字段 | 类型 | 适用 effect | 说明 |
|---|---|---|---|
type | string | 全部 | childRowState、childRowAdd 或 childRowRemove |
table | string | 全部 | 子表单 API 名称 |
selected | boolean | childRowState | 将匹配行标记为选中态 |
backgroundColor | string | childRowState | 匹配行的运行时背景色 |
hoverBackgroundColor | string | childRowState | 鼠标悬停时匹配行的运行时背景色 |
data | Record<string, unknown> | childRowAdd | 追加单行草稿 |
rows | Array<Record<string, unknown>> | childRowAdd | 追加多行草稿,存在时忽略 data |
where | ApprovalFlowConditionGroup | childRowState、childRowRemove | 更新或删除当前草稿中的匹配子表行 |
反馈类 Effects
{
type: 'message',
level: 'warning',
text: 'Please review the highlighted fields.',
}
{
type: 'notification',
level: 'info',
title: 'Validation completed',
description: 'The record passed the external validation.',
placement: 'topRight',
duration: 4,
closable: true,
width: 480,
maxHeight: 320,
}
{
type: 'modal',
level: 'confirm',
title: 'Continue submission?',
content: 'This amount is high. Continue submitting this record?',
okText: 'Continue',
cancelText: 'Cancel submit',
awaitAction: true,
}
| 字段 | 类型 | 适用 effect | 说明 |
|---|---|---|---|
type | string | 全部 | message、notification 或 modal |
level | string | 全部 | success、info、warning、error,modal 还支持 confirm |
text | string | message | 简单消息文本 |
title | string | notification、modal | 标题文本 |
description | string | notification | 通知说明 |
content | string | modal | 弹窗内容 |
okText | string | modal | 确认按钮文案 |
cancelText | string | modal | 确认弹窗的取消按钮文案 |
awaitAction | boolean | modal | 为 true 时等待用户选择,取消后中断动作 |
placement | string | notification | 通知位置 |
duration | number | notification | 展示秒数 |
closable | boolean | notification | 是否显示关闭入口 |
centered | boolean | modal | 是否在视口中垂直居中弹窗 |
width | number | string | notification、modal | 宽度 |
height | number | string | notification、modal | 固定高度 |
maxHeight | number | string | notification、modal | 最大高度 |
顶层 notification 和 modal 快捷写法接受同一组参数,但不需要 type。
刷新
{
type: 'refresh',
target: 'record',
}
| 字段 | 类型 | 说明 |
|---|---|---|
type | 'refresh' | 刷新请求 |
target | 'record' | 'list' | 'children' | 'all' | 刷新目标;实际支持范围取决于当前页面 |
示例
提交前校验
type FormButtonRuntimePayload = {
draft?: {
data?: Record<string, unknown>;
};
};
export default async function main(payload: FormButtonRuntimePayload) {
const amount = Number(payload.draft?.data?.amount || 0);
if (amount <= 0) {
return {
ok: false,
message: 'Amount must be greater than zero.',
effects: [
{
type: 'fieldError',
field: 'amount',
message: 'Amount must be greater than zero.',
},
],
};
}
return { ok: true };
}
继续前确认
type FormButtonRuntimePayload = {
draft?: {
data?: Record<string, unknown>;
};
};
export default async function main(payload: FormButtonRuntimePayload) {
const amount = Number(payload.draft?.data?.amount || 0);
if (amount > 10000) {
return {
ok: true,
modal: {
level: 'confirm',
title: 'Continue submission?',
content: 'This amount is high. Continue submitting this record?',
okText: 'Continue',
cancelText: 'Cancel submit',
awaitAction: true,
},
};
}
return { ok: true };
}
根据字段值调整 UI
type FormButtonRuntimePayload = {
draft?: {
data?: Record<string, unknown>;
};
};
export default async function main(payload: FormButtonRuntimePayload) {
const status = payload.draft?.data?.status;
const closed = status === 'closed';
return {
ok: true,
effects: [
{
type: 'fieldState',
field: 'closeReason',
hidden: !closed,
required: closed,
readonly: false,
},
],
};
}
从自定义按钮新增子表行
type FormButtonRuntimePayload = {
child: { form: { apiName: string } } | null;
};
export default async function main(payload: FormButtonRuntimePayload) {
const table = payload.child?.form.apiName || 'orderItems';
return {
ok: true,
effects: [
{
type: 'childRowAdd',
table,
rows: [
{ productName: 'Standard service', quantity: 1 },
{ productName: 'Implementation', quantity: 1 },
],
},
{
type: 'message',
level: 'success',
text: 'Rows added.',
},
],
};
}
从自定义按钮删除匹配子表行
type FormButtonRuntimePayload = {
child: {
form: { apiName: string };
row: { data: Record<string, unknown> };
} | null;
};
export default async function main(payload: FormButtonRuntimePayload) {
const table = payload.child?.form.apiName;
const sku = String(payload.child?.row.data.sku || '').trim();
if (!table || !sku) {
return { ok: false, message: 'No child row key found.' };
}
return {
ok: true,
effects: [
{
type: 'childRowRemove',
table,
where: {
logic: 'and',
children: [
{ field: 'sku', operator: 'eq', value: sku },
],
},
},
{
type: 'message',
level: 'success',
text: 'Row removed.',
},
],
};
}
提交前批量修改草稿值
在 beforeClick 阶段使用 patch.draft,可以在系统保存或提交动作执行前一次合并多个主表字段。该 patch 只修改当前页面草稿,后续按钮动作成功后才会持久化。
type FormButtonRuntimePayload = {
source: { type: 'form_button'; phase: string; operation: string };
draft: { data: Record<string, unknown> } | null;
};
export default async function main(payload: FormButtonRuntimePayload) {
if (payload.source.phase !== 'beforeClick') {
return { ok: true };
}
return {
ok: true,
patch: {
draft: {
submittedAt: new Date().toISOString(),
submissionSource: 'runtime_button',
},
},
};
}
当前实现会把 patch.draft 和 patch.record 都合并到当前表单状态,两者都不会绕过按钮动作直接写入租户数据库。按钮脚本应优先使用语义明确的 patch.draft。
设置并清除字段错误
校验失败时返回 fieldError。校验恢复正常后,应通过 fieldErrorClear 明确清除之前的错误。
export default async function main(payload: FormButtonRuntimePayload) {
const amount = Number(payload.draft?.data?.amount || 0);
return amount > 0
? {
ok: true,
effects: [{ type: 'fieldErrorClear', field: 'amount' }],
}
: {
ok: false,
effects: [
{
type: 'fieldError',
field: 'amount',
message: '金额必须大于零。',
},
],
};
}
多个主表字段只需要直接设置错误时,也可以使用顶层 fieldErrors:
return {
ok: false,
fieldErrors: {
amount: '请填写有效金额。',
ownerId: '请选择负责人。',
},
};
设置并清除子表字段错误
下面的提交前脚本校验所有子表草稿行,并在数据恢复有效后清除对应错误。
export default async function main(payload: FormButtonRuntimePayload) {
const drafts = payload.draft?.data?.__childFormDrafts;
const orderItems =
drafts && typeof drafts === 'object'
? (drafts as Record<string, unknown>).orderItems
: null;
const rows = Array.isArray(orderItems)
? (orderItems as Array<Record<string, unknown>>)
: [];
const hasInvalidQuantity = rows.some(
(row) => Number(row.quantity || 0) <= 0,
);
return {
ok: !hasInvalidQuantity,
effects: [
hasInvalidQuantity
? {
type: 'childFieldError',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [{ field: 'quantity', operator: 'lte', value: 0 }],
},
message: '数量必须大于零。',
}
: {
type: 'childFieldErrorClear',
table: 'orderItems',
field: 'quantity',
},
],
};
}
省略 where 的 childFieldErrorClear 会清除该子表当前所有行中目标字段的错误。
临时过滤并恢复选项
fieldOptionFilter 只过滤字段设计中已有的稳定 option value,不接受展示 label。
export default async function main(payload: FormButtonRuntimePayload) {
const domestic = payload.draft?.data?.region === 'domestic';
return {
ok: true,
effects: [
domestic
? {
type: 'fieldOptionFilter',
field: 'shippingMethod',
allowedValues: ['standard', 'express'],
clearInvalidValue: true,
}
: {
type: 'fieldOptionFilter',
field: 'shippingMethod',
reset: true,
},
],
};
}
使用列表批量按钮的选中记录
LIST_BATCH 按钮从 selection.records 读取选中记录。不要从 URL 或页面状态猜测记录 ID。
type FormButtonRuntimePayload = {
source: { type: 'form_button'; phase: string; placement: string };
selection: {
records: Array<{ id: string; data: Record<string, unknown> }>;
};
};
export default async function main(payload: FormButtonRuntimePayload) {
const records = payload.selection.records;
if (payload.source.placement !== 'LIST_BATCH' || records.length === 0) {
return {
ok: false,
message: '请至少选择一条记录。',
};
}
return {
ok: true,
notification: {
level: 'info',
title: '批量处理已准备',
description: `即将处理 ${records.length} 条记录。`,
},
};
}
该示例只展示选择上下文。真正修改记录时,应在 Studio Runtime 中使用 context.form.batchUpdate 等领域方法,并传入已选记录的真实 ID。
使用子表批量按钮的选中行
FORM_LIST_BATCH 按钮通过 child.form 确认子表,通过 selection.childRows 读取用户选中的草稿行。
export default async function main(payload: FormButtonRuntimePayload) {
const table = payload.child?.form.apiName;
const selectedRows = payload.selection.childRows;
if (!table || selectedRows.length === 0) {
return { ok: false, message: '请至少选择一条子表行。' };
}
return {
ok: true,
message: `已选择 ${selectedRows.length} 条 ${table} 子表行。`,
};
}
在 AfterClick 中使用动作结果
afterClick 只在按钮动作成功后执行,已完成动作的返回值位于 payload.result。它适合展示结果通知或执行成功后的领域副作用,不能再阻止已经完成的按钮动作。
type FormButtonRuntimePayload = {
source: { type: 'form_button'; phase: string };
result: Record<string, unknown> | null;
};
export default async function main(payload: FormButtonRuntimePayload) {
if (payload.source.phase !== 'afterClick') {
return { ok: true };
}
const recordId = String(payload.result?.recordId || '').trim();
return {
ok: true,
notification: {
level: 'success',
title: '操作已完成',
description: recordId
? `记录 ${recordId} 已保存。`
: '记录已保存。',
duration: 4,
},
};
}
不要依赖某个未声明的 result 字段。应先根据对应按钮动作的实际返回结果进行判空和类型检查。
读取已持久化记录
编辑页按钮的 payload.draft.data 是当前未保存草稿;payload.record 只提供记录身份。需要数据库中已保存的值时,通过 Studio Runtime 查询租户侧记录。
type StudioRuntimeContext = {
form: {
get(input: { recordId: string }): Promise<{
data?: Record<string, unknown>;
}>;
};
};
export default async function main(
payload: FormButtonRuntimePayload,
context: StudioRuntimeContext,
) {
if (!payload.record?.id) {
return { ok: false, message: '当前页面还没有已保存记录。' };
}
const persisted = await context.form.get({ recordId: payload.record.id });
const savedStatus = String(persisted.data?.status || '');
return savedStatus === 'locked'
? {
ok: false,
modal: {
level: 'warning',
title: '记录已锁定',
content: '已保存记录处于锁定状态,不能继续当前操作。',
},
}
: { ok: true };
}
按执行阶段组织一个脚本
同一个脚本文件被不同阶段复用时,应根据 source.phase 明确分支。beforeClick 负责校验,action 负责自定义主动作,afterClick 只处理成功后的反馈或副作用。
export default async function main(payload: FormButtonRuntimePayload) {
if (payload.source.phase === 'beforeClick') {
return payload.draft?.data?.ownerId
? { ok: true }
: {
ok: false,
fieldErrors: { ownerId: '请选择负责人。' },
};
}
if (payload.source.phase === 'action') {
return {
ok: true,
message: '自定义动作已执行。',
};
}
return {
ok: true,
notification: {
level: 'success',
title: '按钮流程已完成',
},
};
}
Refresh 当前支持情况
按钮返回协议允许声明以下 effect:
return {
ok: true,
effects: [{ type: 'refresh', target: 'record' }],
};
当前表单按钮运行时会校验和归一化 record、list、children 与 all,但尚未实际执行刷新处理器。不要依赖该 effect 重新加载页面数据。按钮动作需要更新当前草稿时返回 fieldValue、子表 effect 或 patch.draft;需要写入租户数据库时使用 Studio Runtime 的表单领域方法。
运行规则
beforeClick适合做校验、确认和动作前拦截- 带
awaitAction: true的确认弹窗在用户取消时会停止动作 action是自定义按钮主逻辑,预设按钮的主逻辑仍由系统执行afterClick更适合放成功后的提示和领域副作用;当前不要依赖refresheffect- Effects 只覆盖运行时状态,不会写回字段设计配置
- 隐藏字段不会在新建页和编辑页渲染
- 必填校验会遵循当前运行时字段状态
关联的声明式运行时规则
表单运行时页面也可以从表单 custom configuration 中读取声明式规则。
{
"formRuntimeRules": [
{
"when": {
"logic": "and",
"children": [
{
"field": "status",
"operator": "eq",
"value": "closed"
}
]
},
"effects": [
{
"type": "fieldState",
"field": "closeReason",
"hidden": false,
"required": true
}
],
"otherwiseEffects": [
{
"type": "fieldState",
"field": "closeReason",
"hidden": true,
"required": false
}
]
}
]
}