跳到主要内容

页面化

页面化交互事件定义了新建页、编辑页与 布局设计 中配置的 Studio 脚本之间的运行时协议。

当布局需要动态必填、动态只读、子表校验、默认值、异常行高亮或提示弹窗时,使用页面脚本。

配置位置​

  1. 进入 工作管理 > 表单管理
  2. 打开目标表单
  3. 进入 布局设计
  4. 选择新建页或编辑页实际使用的布局
  5. 打开 页面事件 Tab
  6. 选择 Studio 脚本;如果脚本包含多个文件,可以继续选择入口文件
  7. 开启需要的触发时机
  8. 保存布局

配置会写入表单 custom configuration 的 formPageScripts[layoutId] 和 formChildPageScripts[layoutId],不需要修改数据库结构。

触发器​

主表触发器​

触发器Runtime phase说明
页面加载load新建页或编辑页打开后执行一次
字段变化change主表字段值变化后执行;可以监听全部字段,也可以监听指定主表字段

子表触发器​

触发器Runtime phase说明
子表行新增childRowAdd子表新增一行后执行
子表行删除childRowRemove子表删除一行后执行
子表字段变化childFieldChange子表字段变化后执行;可以监听全部子表字段,也可以监听指定的 childFormApiName.fieldApiName

字段变化触发会由运行时做短暂防抖,避免每个输入中间态都执行脚本。

脚本函数​

type FormPageRuntimePayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'form_page';
phase: 'load' | 'change' | 'childRowAdd' | 'childFieldChange' | 'childRowRemove';
};
draft: {
data: Record<string, unknown>;
};
};

type StudioRuntimeContext = Record<string, unknown>;

export default async function main(
payload: FormPageRuntimePayload,
_context: StudioRuntimeContext,
) {
return {
ok: true,
effects: [],
};
}

入参 Payload​

type FormPageRuntimePayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'form_page';
page: 'form' | 'view' | 'list';
layoutId: string | null;
phase: 'load' | 'change' | 'childRowAdd' | 'childFieldChange' | 'childRowRemove';
changedFields: string[];
changedField: string | null;
previousData: Record<string, unknown> | null;
previousValue: unknown | null;
value: unknown | null;
};
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'load' | 'change' | 'childRowAdd' | 'childFieldChange' | 'childRowRemove'当前运行阶段
form.idstring | null表单 schema ID
form.apiNamestring | null表单 API 名称
source.layoutIdstring | null当前布局 ID
draft.dataRecord<string, unknown>当前草稿值,按字段 API 名称索引
record{ id: string } | null当前记录身份;新建页通常为 null;页面绑定不会预加载记录数据
source.page'form' | 'view' | 'list'当前页面类型
runtime.localestring浏览器语言
runtime.timezonestring浏览器时区

主表字段值可以从 payload.draft.data[fieldApiName] 读取。脚本确实需要持久化值时,先确认 payload.record?.id 存在,再调用 context.form.get({ recordId: payload.record.id })。

子表上下文​

字段类型说明
child.formobject | null子表触发器执行时的子表单
child.row.dataRecord<string, unknown>当前子表行草稿
source.previousDataRecord<string, unknown> | null变化前子表行草稿
source.changedFieldstring | null当前变化字段
source.changedFieldsstring[]本次变化字段列表
source.previousValueunknown | null变化前字段值
source.valueunknown | null变化后字段值

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

返回 Response​

未知字段会被忽略。

type FormPageRuntimeResponse = {
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当前脚本流程是否继续
preventDefaultboolean为 true 时显式停止当前脚本流程
messagestring展示简单消息
fieldErrorsRecord<string, string>设置字段校验错误
patch.draftRecord<string, unknown>合并到当前草稿状态
patch.recordRecord<string, unknown>合并到当前记录状态
effectsFormRuntimeEffect[]有序执行的运行时 effects
notificationobject | string顶层通知快捷写法
modalobject | string顶层弹窗快捷写法

返回结构与按钮交互事件共用。message、notification、modal、refresh、fieldErrors、patch 的完整行为请查看 按钮。

Effects​

页面脚本可以返回所有通用运行时 effects,也支持子表行选中状态 effect。

Effect用途
fieldState覆盖主表字段隐藏、必填、只读或禁用状态
fieldOptionFilter按稳定选项 value 过滤主表选项字段的配置选项
fieldValue设置主表字段值
fieldError展示主表字段校验错误
fieldErrorClear清除主表字段校验错误
childFieldState覆盖子表字段隐藏、必填、只读或禁用状态
childRowState临时把子表行标记为选中态
childFieldValue设置子表字段值
childFieldError展示子表字段校验错误
childFieldErrorClear清除子表字段校验错误
childRowAdd追加一行或多行子表草稿
childRowRemove删除一行或多行子表草稿
message展示简单消息
notification展示通知
modal展示弹窗
refresh请求运行时刷新

主表字段 Effects​

return {
ok: true,
effects: [
{
type: 'fieldState',
field: 'closeReason',
hidden: false,
required: true,
readonly: false,
},
],
};

主表字段 effect 的字段定义与 按钮文档中的主表字段 Effects 一致。

select、multi_select、radio、checkbox_group 这类少量静态选项字段需要页面级选项级联时,可以使用 fieldOptionFilter。该 effect 会按 allowedValues 过滤目标字段已有配置选项,不接收 label;label、标签颜色和多语言文案仍然来自字段配置。

return {
ok: true,
effects: [
{
type: 'fieldOptionFilter',
field: 'closeReason',
allowedValues: ['resolved', 'duplicate'],
clearInvalidValue: true,
},
],
};
字段类型说明
fieldstring目标主表字段 API 名称
allowedValuesstring[]允许继续选择的稳定选项 value
clearInvalidValueboolean为 true 时,单选值不再允许会清空,多选值不再允许会被移除
resetboolean为 true 时,移除运行时过滤并恢复字段原始配置选项

恢复完整选项列表:

return {
ok: true,
effects: [
{
type: 'fieldOptionFilter',
field: 'closeReason',
reset: true,
},
],
};

子表字段 Effects​

使用 table 指定子表单 API 名称,也可以使用 childTable、formApiName 或 childFormApiName。

使用 where 按子表字段值筛选行。省略 where 时,effect 作用于当前子表的所有行。

return {
ok: true,
effects: [
{
type: 'childFieldState',
table: 'orderItems',
field: 'discount',
where: {
logic: 'and',
children: [
{ field: 'productType', operator: 'eq', value: 'service' },
],
},
readonly: true,
required: false,
},
],
};
return {
ok: true,
effects: [
{
type: 'childFieldValue',
table: 'orderItems',
field: 'currency',
where: {
logic: 'and',
children: [
{ field: 'region', operator: 'eq', value: 'US' },
],
},
value: 'USD',
},
],
};
return {
ok: true,
effects: [
{
type: 'childFieldError',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [
{ field: 'quantity', operator: 'lte', value: 0 },
],
},
message: 'Quantity must be greater than zero.',
},
],
};

子表字段 effect 的字段定义与 按钮文档中的子表字段 Effects 一致。

子表行状态​

使用 where 按子表字段值筛选行。省略 where 时,状态作用于当前子表所有行。

被标记为 selected: true 的行会使用运行时配置的表格选中行背景 token。

return {
ok: true,
effects: [
{
type: 'childRowState',
table: 'orderItems',
where: {
logic: 'and',
children: [
{ field: 'status', operator: 'eq', value: 'delayed' },
],
},
selected: true,
},
],
};
字段类型必填说明
type'childRowState'是标识子表行状态 effect
tablestring是子表单 API 名称
whereApprovalFlowConditionGroup否行筛选条件;省略时作用于所有当前行
selectedboolean否是否临时标记为选中行

子表行增删​

return {
ok: true,
effects: [
{
type: 'childRowAdd',
table: 'orderItems',
rows: [
{
productName: 'Standard service',
quantity: 1,
currency: 'USD',
},
{
productName: 'Implementation',
quantity: 1,
currency: 'USD',
},
],
},
],
};
return {
ok: true,
effects: [
{
type: 'childRowRemove',
table: 'orderItems',
where: {
logic: 'and',
children: [
{ field: 'status', operator: 'eq', value: 'cancelled' },
],
},
},
],
};

子表行增删 effect 的字段定义与 按钮文档中的子表行 Effects 一致。

示例​

状态关闭时要求填写原因​

type FormPageRuntimePayload = {
draft?: {
data?: Record<string, unknown>;
};
};

export default async function main(payload: FormPageRuntimePayload) {
const closed = payload.draft?.data?.status === 'closed';

return {
ok: true,
effects: [
{
type: 'fieldState',
field: 'closeReason',
hidden: !closed,
required: closed,
},
],
};
}

子表新增行时设置默认值​

type FormPageRuntimePayload = {
source: {
type: 'form_page';
phase: string;
};
child: {
form: { apiName: string };
row: { data: Record<string, unknown> };
} | null;
};

export default async function main(payload: FormPageRuntimePayload) {
const lineNo = payload.child?.row.data.lineNo;
if (
payload.source.phase !== 'childRowAdd' ||
payload.child?.form.apiName !== 'orderItems' ||
!lineNo
) {
return { ok: true };
}

return {
ok: true,
effects: [
{
type: 'childFieldValue',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [
{ field: 'lineNo', operator: 'eq', value: lineNo },
],
},
value: 1,
},
],
};
}

页面加载时追加子表行​

type FormPageRuntimePayload = {
source: { type: 'form_page'; phase: string };
draft?: {
data?: Record<string, unknown>;
};
};

export default async function main(payload: FormPageRuntimePayload) {
if (payload.source.phase !== 'load') {
return { ok: true };
}

const shouldSeedItems = payload.draft?.data?.template === 'standard';

return {
ok: true,
effects: shouldSeedItems
? [
{
type: 'childRowAdd',
table: 'orderItems',
rows: [
{ productName: 'Standard service', quantity: 1 },
{ productName: 'Implementation', quantity: 1 },
],
},
]
: [],
};
}

校验子表字段变化​

type FormPageRuntimePayload = {
source: {
type: 'form_page';
phase: string;
changedField: string | null;
value: unknown | null;
};
child: {
form: { apiName: string };
row: { data: Record<string, unknown> };
} | null;
};

export default async function main(payload: FormPageRuntimePayload) {
const quantity = Number(payload.source.value || 0);
const isQuantityChange =
payload.source.phase === 'childFieldChange' &&
payload.child?.form.apiName === 'orderItems' &&
payload.source.changedField === 'quantity';

if (!isQuantityChange || quantity > 0) {
return { ok: true };
}

return {
ok: false,
effects: [
{
type: 'childFieldError',
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [
{ field: 'sku', operator: 'eq', value: payload.child?.row.data.sku },
],
},
message: 'Quantity must be greater than zero.',
},
],
};
}

高亮需要关注的子表行​

export default async function main() {
return {
ok: true,
effects: [
{
type: 'childRowState',
table: 'orderItems',
where: {
logic: 'and',
children: [
{ field: 'status', operator: 'eq', value: 'delayed' },
],
},
selected: true,
},
],
};
}

页面加载后显示简单消息​

message 适合不需要标题和详细说明的简短反馈。下面的脚本只在“页面加载”事件中显示消息,字段变化时不会重复显示。

type FormPageRuntimePayload = {
source: { type: 'form_page'; phase: string };
};

export default async function main(payload: FormPageRuntimePayload) {
if (payload.source.phase !== 'load') {
return { ok: true };
}

return {
ok: true,
effects: [
{
type: 'message',
level: 'info',
text: '保存前请检查客户信息。',
},
],
};
}

level 支持 success、info、warning 和 error。只需要默认样式时,也可以使用顶层快捷写法:

return {
ok: true,
message: '草稿数据已加载。',
};

字段变化后显示详细通知​

反馈需要标题、说明、展示位置或较长停留时间时使用 notification。使用下面的脚本时,应把页面事件配置为监听 customerType 字段。

type FormPageRuntimePayload = {
source: {
type: 'form_page';
phase: string;
changedFields: string[];
};
draft?: { data?: Record<string, unknown> };
};

export default async function main(payload: FormPageRuntimePayload) {
const isEnterpriseCustomer =
payload.source.phase === 'change' &&
payload.source.changedFields.includes('customerType') &&
payload.draft?.data?.customerType === 'enterprise';

if (!isEnterpriseCustomer) {
return { ok: true };
}

return {
ok: true,
effects: [
{
type: 'notification',
level: 'warning',
title: '需要企业客户审核',
description: '保存记录前请完成风险评估。',
placement: 'topRight',
duration: 6,
closable: true,
},
],
};
}

只需要显示一个通知时,可以使用顶层快捷写法:

return {
ok: true,
notification: {
level: 'success',
title: '默认值已应用',
description: '已根据客户信息填写币种和付款条件。',
placement: 'topRight',
duration: 4,
},
};

使用弹窗说明阻断原因​

需要用户明确阅读的信息使用 modal。下面的示例在金额超过页面限制时,同时标记字段错误并显示弹窗。

type FormPageRuntimePayload = {
source: {
type: 'form_page';
phase: string;
changedFields: string[];
};
draft?: { data?: Record<string, unknown> };
};

export default async function main(payload: FormPageRuntimePayload) {
const amount = Number(payload.draft?.data?.amount || 0);
const amountChanged =
payload.source.phase === 'change' &&
payload.source.changedFields.includes('amount');

if (!amountChanged || amount <= 100000) {
return { ok: true };
}

return {
ok: false,
fieldErrors: {
amount: '金额超过当前页面限制。',
},
modal: {
level: 'warning',
title: '该金额需要审批',
content: '金额超过 100,000 时,必须通过审批流程提交。',
okText: '知道了',
centered: true,
width: 520,
},
};
}

ok: false 会停止后续页面脚本流程,但不会撤销触发页面事件的字段变化。如果还需要把草稿标记为无效或修改草稿值,应配合 fieldErrors 或 fieldValue effect。

确认弹窗使用 level: 'confirm' 和 awaitAction: true。用户取消后,页面脚本结果会被标记为已阻止,但页面事件仍不会回滚触发它的字段变化。

return {
ok: true,
effects: [
{
type: 'modal',
level: 'confirm',
title: '需要确认',
content: '请确认已经检查客户风险信息。',
okText: '已检查',
cancelText: '返回检查',
awaitAction: true,
},
],
};

不要在确认弹窗后继续放置草稿修改 effect,并期待取消操作能让整个 effects 数组回滚。页面事件中的确认弹窗应作为独立反馈使用;如果确认结果必须控制一项用户操作,应使用按钮的 beforeClick 脚本。

同时修改草稿并显示反馈​

一次返回需要执行多个操作时使用 effects。顶层 message、notification 和 modal 是单个反馈 effect 的快捷写法;需要控制执行顺序时应使用 effects。

export default async function main() {
return {
ok: true,
effects: [
{
type: 'fieldValue',
field: 'status',
value: 'draft',
},
{
type: 'message',
level: 'success',
text: '初始状态已设置为草稿。',
},
],
};
}

设置并清除主表字段错误​

字段无效时返回 fieldError;字段恢复有效后必须明确返回 fieldErrorClear,否则之前显示的错误不会自动消失。使用该示例时,把页面事件配置为监听 startDate 和 endDate。

type FormPageRuntimePayload = {
source: { type: 'form_page'; phase: string; changedFields: string[] };
draft?: { data?: Record<string, unknown> };
};

export default async function main(payload: FormPageRuntimePayload) {
if (payload.source.phase !== 'change') {
return { ok: true };
}

const startDate = String(payload.draft?.data?.startDate || '');
const endDate = String(payload.draft?.data?.endDate || '');
const invalid = Boolean(startDate && endDate && endDate < startDate);

return {
ok: !invalid,
effects: [
invalid
? {
type: 'fieldError',
field: 'endDate',
message: '结束日期不能早于开始日期。',
}
: {
type: 'fieldErrorClear',
field: 'endDate',
},
],
};
}

清除子表字段错误​

childFieldErrorClear 通过 table、field 和可选的 where 清除匹配行的字段错误。下面的脚本只处理当前发生变化的 SKU 行。

export default async function main(payload: FormPageRuntimePayload) {
const row = payload.child?.row?.data;
if (
payload.source.phase !== 'childFieldChange' ||
payload.child?.form.apiName !== 'orderItems' ||
payload.source.changedField !== 'quantity' ||
!row?.sku
) {
return { ok: true };
}

const valid = Number(payload.source.value || 0) > 0;
const target = {
table: 'orderItems',
field: 'quantity',
where: {
logic: 'and',
children: [{ field: 'sku', operator: 'eq', value: row.sku }],
},
};

return {
ok: valid,
effects: [
valid
? { type: 'childFieldErrorClear', ...target }
: {
type: 'childFieldError',
...target,
message: '数量必须大于零。',
},
],
};
}

使用 Patch 批量合并草稿值​

patch.draft 适合一次合并多个主表字段。它是浅合并,不会保存记录,也不会修改字段设计。

export default async function main(payload: FormPageRuntimePayload) {
if (
payload.source.phase !== 'change' ||
!payload.source.changedFields.includes('customerType') ||
payload.draft?.data?.customerType !== 'enterprise'
) {
return { ok: true };
}

return {
ok: true,
patch: {
draft: {
riskLevel: 'high',
requiresApproval: true,
},
},
};
}

当前表单页面实现会把 patch.draft 和 patch.record 都合并到当前表单状态,两者都不会直接写入租户数据库。页面脚本应优先使用语义明确的 patch.draft;只有调用表单保存或提交动作后,草稿值才会持久化。

比较字段变化前后的值​

主表字段变化时,使用 source.previousValue 和 source.value 判断变化方向,避免只根据当前草稿重复提示。

export default async function main(payload: FormPageRuntimePayload) {
const becameHighPriority =
payload.source.phase === 'change' &&
payload.source.changedFields.includes('priority') &&
payload.source.previousValue !== 'high' &&
payload.source.value === 'high';

return becameHighPriority
? {
ok: true,
notification: {
level: 'warning',
title: '优先级已提高',
description: '请补充处理期限和负责人。',
},
}
: { ok: true };
}

子表字段变化时,source.previousData 是变化前的整行数据,child.row.data 是当前行数据。需要同时比较一行中的多个字段时使用这两个对象。

子表删除后重新计算主表汇总​

childRowRemove 页面事件在子表行删除后触发。当前子表草稿可以从 draft.data.__childFormDrafts[childFormApiName] 读取。

export default async function main(payload: FormPageRuntimePayload) {
if (
payload.source.phase !== 'childRowRemove' ||
payload.child?.form.apiName !== 'orderItems'
) {
return { ok: true };
}

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 totalAmount = rows.reduce(
(sum, row) =>
sum + Number(row.quantity || 0) * Number(row.unitPrice || 0),
0,
);

return {
ok: true,
effects: [
{ type: 'fieldValue', field: 'totalAmount', value: totalAmount },
],
};
}

编辑页读取已持久化记录​

payload.draft.data 是当前页面草稿。确实需要数据库中已经保存的值时,先检查 payload.record.id,再通过 Studio Runtime 的 context.form.get 查询租户侧记录。

type StudioRuntimeContext = {
form: {
get(input: { recordId: string }): Promise<{
data?: Record<string, unknown>;
}>;
};
};

export default async function main(
payload: FormPageRuntimePayload,
context: StudioRuntimeContext,
) {
if (payload.source.phase !== 'load' || !payload.record?.id) {
return { ok: true };
}

const persisted = await context.form.get({ recordId: payload.record.id });
const ownerChanged =
persisted.data?.ownerId !== payload.draft?.data?.ownerId;

return ownerChanged
? {
ok: true,
message: '当前负责人和已保存记录中的负责人不同。',
}
: { ok: true };
}

不要根据 URL 参数猜测当前记录 ID,也不要假设 payload.record.data 已经预加载。

ok 与 preventDefault​

ok: false 和 preventDefault: true 在当前页面脚本执行结果中都会返回阻止状态。它们停止后续页面脚本流程,但不会撤销用户刚刚完成的字段修改、子表增删,也不会自动回滚已经应用的 effects。

return {
ok: false,
preventDefault: true,
effects: [
{
type: 'fieldError',
field: 'amount',
message: '请先修正金额。',
},
],
};

通常选择其中一个阻止字段即可,不需要同时返回。需要表达校验失败时优先使用 ok: false;需要明确表达“阻止后续流程”时可以使用 preventDefault: true。

不同页面支持的 Effects​

Effectform 新建/编辑页view 查看页list 列表页
message、notification、modal支持支持支持
refresh接受但尚未执行接受但尚未执行接受但尚未执行
fieldState支持全部状态只保留 hidden: true只保留 hidden: true
childFieldState支持全部状态只保留 hidden: true只保留 hidden: true
childRowState支持支持支持
字段值、字段错误、选项过滤、子表行增删支持不执行不执行

运行时会先过滤当前页面不支持的 effect。不要在查看页或列表页脚本中依赖被过滤的草稿修改效果。

Refresh 当前支持情况​

返回协议预留了以下刷新请求:

return {
ok: true,
effects: [
{
type: 'refresh',
target: 'record',
},
],
};

当前表单页面运行时会校验和归一化 record、list、children 与 all,但页面脚本还没有实际执行刷新处理器。现阶段不要依赖该 effect 重新加载记录、列表或子表数据。页面需要立即更新时,应明确返回 fieldValue、childFieldValue、childRowAdd、childRowRemove 或 patch.draft。

运行规则​

  • Effects 只覆盖运行时状态,不会写回字段设计或布局设计配置
  • 页面脚本字段状态只作用于当前布局的新建页和编辑页
  • 子表字段状态只作用于当前页面渲染出的可编辑子表行
  • 被隐藏的子表字段会由运行时清空当前草稿值
  • preventDefault 和 ok: false 会停止当前脚本流程,但不会替代触发该事件的用户操作