页面化
页面化交互事件定义了新建页、编辑页与 布局设计 中配置的 Studio 脚本之间的运行时协议。
当布局需要动态必填、动态只读、子表校验、默认值、异常行高亮或提示弹窗时,使用页面脚本。
配置位置
- 进入 工作管理 > 表单管理
- 打开目标表单
- 进入 布局设计
- 选择新建页或编辑页实际使用的布局
- 打开 页面事件 Tab
- 选择 Studio 脚本;如果脚本包含多个文件,可以继续选择入口文件
- 开启需要的触发时机
- 保存布局
配置会写入表单 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.id | string | null | 表单 schema ID |
form.apiName | string | null | 表单 API 名称 |
source.layoutId | string | null | 当前布局 ID |
draft.data | Record<string, unknown> | 当前草稿值,按字段 API 名称索引 |
record | { id: string } | null | 当前记录身份;新建页通常为 null;页面绑定不会预加载记录数据 |
source.page | 'form' | 'view' | 'list' | 当前页面类型 |
runtime.locale | string | 浏览器语言 |
runtime.timezone | string | 浏览器时区 |
主表字段值可以从 payload.draft.data[fieldApiName] 读取。脚本确实需要持久化值时,先确认 payload.record?.id 存在,再调用 context.form.get({ recordId: payload.record.id })。
子表上下文
| 字段 | 类型 | 说明 |
|---|---|---|
child.form | object | null | 子表触发器执行时的子表单 |
child.row.data | Record<string, unknown> | 当前子表行草稿 |
source.previousData | Record<string, unknown> | null | 变化前子表行草稿 |
source.changedField | string | null | 当前变化字段 |
source.changedFields | string[] | 本次变化字段列表 |
source.previousValue | unknown | null | 变化前字段值 |
source.value | unknown | 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;
};
| 字段 | 类型 | 说明 |
|---|---|---|
ok | boolean | 当前脚本流程是否继续 |
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 | 顶层弹窗快捷写法 |
返回结构与按钮交互事件共用。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,
},
],
};
| 字段 | 类型 | 说明 |
|---|---|---|
field | string | 目标主表字段 API 名称 |
allowedValues | string[] | 允许继续选择的稳定选项 value |
clearInvalidValue | boolean | 为 true 时,单选值不再允许会清空,多选值不再允许会被移除 |
reset | boolean | 为 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 |
table | string | 是 | 子表单 API 名称 |
where | ApprovalFlowConditionGroup | 否 | 行筛选条件;省略时作用于所有当前行 |
selected | boolean | 否 | 是否临时标记为选中行 |
子表行增删
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
| Effect | form 新建/编辑页 | 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会停止当前脚本流程,但不会替代触发该事件的用户操作