跳到主要内容

按钮

按钮交互事件定义了表单运行时按钮与 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 编辑器不会提示未使用参数。

参数类型必填说明
payloadFormButtonRuntimePayload是按钮执行器发送的运行时入参
contextobject是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.idstring | null表单 schema ID
form.apiNamestring | null当前表单 API 名称
source.buttonIdstring按钮 key,预设按钮常见值如 form.submit
source.buttonType'preset' | 'custom'预设按钮或自定义按钮
source.placementstring运行时位置,如 FORM_HEADER、LIST_ROW、LIST_BATCH
source.operationstring当前动作类型,如 submit、save、saveDraft、custom
draft.dataRecord<string, unknown>当前草稿值,按字段 API 名称索引
record{ id: string } | null当前记录身份;新建页通常为 null;按钮绑定不会预加载记录数据
resultRecord<string, unknown> | null已完成动作的结果,主要在 afterClick 使用
runtime.localestring浏览器语言
runtime.timezonestring浏览器时区

当前表单值从 payload.draft.data 读取。脚本确实需要持久化值时,先确认 payload.record?.id 存在,再调用 context.form.get({ recordId: payload.record.id })。

子表上下文​

字段类型说明
child.formobject子表头部、批量工具栏或行按钮点击时的子表单
child.rowobject | null子表行按钮的当前行草稿数据
selection.childRowsRuntimeChildRow[]子表批量按钮选中的行

子表草稿也可以从 payload.draft.data.__childFormDrafts[childFormApiName] 读取,值为一组子表行对象。

按钮位置额外上下文
FORM_LIST_HEADERchild.form
FORM_LIST_BATCHchild.form 和 selection.childRows
FORM_LIST_ROWchild.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;
};
字段类型说明
okboolean当前阶段是否继续;缺省按 true 处理
preventDefaultboolean为 true 时显式停止当前按钮动作
messagestring展示简单消息
fieldErrorsRecord<string, string>设置字段校验错误
patch.draftRecord<string, unknown>合并到当前草稿状态
patch.recordRecord<string, unknown>合并到当前记录状态
effectsFormRuntimeEffect[]有序执行的运行时 effects
notificationobject | string顶层通知快捷写法
modalobject | 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说明
typestring全部fieldState、fieldOptionFilter、fieldValue、fieldError 或 fieldErrorClear
fieldstring全部主表字段 API 名称
allowedValuesstring[]fieldOptionFilter允许继续选择的稳定选项 value
clearInvalidValuebooleanfieldOptionFilter清除不再允许的当前值
resetbooleanfieldOptionFilter恢复字段原始配置选项
hiddenbooleanfieldState临时隐藏或显示字段
requiredbooleanfieldState临时修改是否必填
readonlybooleanfieldState临时修改是否只读
disabledbooleanfieldState临时修改是否禁用
valueunknownfieldValue新字段值
messagestringfieldError字段错误文案

子表字段 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说明
typestring全部childFieldState、childFieldValue、childFieldError 或 childFieldErrorClear
tablestring全部子表单 API 名称
fieldstring全部子表字段 API 名称
whereApprovalFlowConditionGroup全部行筛选条件
hiddenbooleanchildFieldState隐藏或显示子表字段;隐藏字段会清空草稿值
requiredbooleanchildFieldState临时修改是否必填
readonlybooleanchildFieldState临时修改是否只读
disabledbooleanchildFieldState临时修改是否禁用
valueunknownchildFieldValue新子表字段值
messagestringchildFieldError子表字段错误文案

子表行 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说明
typestring全部childRowState、childRowAdd 或 childRowRemove
tablestring全部子表单 API 名称
selectedbooleanchildRowState将匹配行标记为选中态
backgroundColorstringchildRowState匹配行的运行时背景色
hoverBackgroundColorstringchildRowState鼠标悬停时匹配行的运行时背景色
dataRecord<string, unknown>childRowAdd追加单行草稿
rowsArray<Record<string, unknown>>childRowAdd追加多行草稿,存在时忽略 data
whereApprovalFlowConditionGroupchildRowState、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说明
typestring全部message、notification 或 modal
levelstring全部success、info、warning、error,modal 还支持 confirm
textstringmessage简单消息文本
titlestringnotification、modal标题文本
descriptionstringnotification通知说明
contentstringmodal弹窗内容
okTextstringmodal确认按钮文案
cancelTextstringmodal确认弹窗的取消按钮文案
awaitActionbooleanmodal为 true 时等待用户选择,取消后中断动作
placementstringnotification通知位置
durationnumbernotification展示秒数
closablebooleannotification是否显示关闭入口
centeredbooleanmodal是否在视口中垂直居中弹窗
widthnumber | stringnotification、modal宽度
heightnumber | stringnotification、modal固定高度
maxHeightnumber | stringnotification、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 更适合放成功后的提示和领域副作用;当前不要依赖 refresh effect
  • 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
}
]
}
]
}