表单、字段与关系页签
这一章处理最常见的客户端需求:字段联动、显示隐藏、只读、关系页签切换,以及保存前的客户端 DOM 同步。
适用版本: 主线以 R31 的 Form / Responsive Form 配置能力为准;JavaScript 示例仅用于已验证的 Classic client。
执行层: 浏览器客户端;所有授权与数据完整性规则还要在服务端执行。
主要依据: R31 原厂手册 Unit 7(Classic / Responsive Form)与 Unit 8(Relationship),11.0 SP9 旧手册 Unit 11、Appendix F,以及本项目提供的字段和页签代码素材。
边界
这里的大部分代码属于 Classic client compatibility。它改善用户体验,但不能决定用户是否真的有权读取或修改数据。凡是影响业务结果的规则,必须在 Permission、Lifecycle Permission 或 Server Event 中再次校验。
先识别运行上下文
同一段 JavaScript 放在不同事件中,this、document.thisItem 和事件参数的含义并不相同。不要用 parent[1]、parent[2] 之类的 frame 下标猜测上下文。
| 绑定位置 | 常见上下文 | 适合做什么 |
|---|---|---|
| Form Event | 当前 Form 与客户端 Item | 初始化字段状态、建立事件监听 |
| Field Event | 当前字段、Form 和客户端 Item | 字段联动、输入提示、固定搜索条件 |
| Relationship Grid Event | relationship ID、property name、grid context | 行级验证、更新客户端关系 DOM |
| Item Action | 被选中的 Item 或 Item 集合 | 打开窗口、调用服务端 Method |
在每个 Method 开头写清楚绑定位置和所需参数:
/**
* Runtime: client
* Bind to: Classic Form / onFormPopulated
* Input: document.thisItem
* Security: UI only; server validation is still required
*/字段显示与隐藏
标注:Classic client compatibility
以下写法只访问 Classic Form 已渲染的字段。它会跳过不存在的字段,避免因为不同 View 的字段集合不同而中断整个 Form。
function setClassicFieldVisible(fieldName, visible) {
const field = getFieldByName(fieldName);
if (!field) {
console.warn(`Field is not rendered: ${fieldName}`);
return;
}
field.style.visibility = visible ? "visible" : "hidden";
}
const currentItem = document.thisItem;
if (!currentItem) {
top.aras.AlertError("无法取得当前表单数据。");
return;
}
const mode = currentItem.getProperty("change_mode", "");
const showDetails = mode === "add";
["reason", "before_value", "after_value"].forEach((fieldName) => {
setClassicFieldVisible(fieldName, showDetails);
});Responsive Form
R31 原厂配置手册已经把 Responsive Form 作为正式建模能力。若目标 Release 支持规则化显示或不同 View,优先用配置表达条件;只有配置无法完成时,才进入客户端兼容层。
禁用字段
标注:Classic client compatibility
const component = getFieldComponentByName("review_result");
if (component && typeof component.setDisabled === "function") {
component.setDisabled(true);
}禁用控件不会阻止用户通过 AML、集成程序或其他界面修改属性。对应规则仍应放在服务端:
Innovator inn = this.getInnovator();
if (!CanCurrentUserChangeReviewResult(this))
{
return inn.newError("当前状态不允许修改审核结果。");
}
return this;上面的 CanCurrentUserChangeReviewResult 是项目自己的服务端规则,不应依赖浏览器传来的布尔值。
字段联动与脏数据标记
标注:Classic client compatibility
不要只改输入框的 value。应调用目标 Release 支持的字段变更入口,让客户端 Item DOM、脏数据状态和依赖表达式一起更新。
function setFormProperty(propertyName, value) {
if (typeof window.handleItemChange !== "function") {
throw new Error("当前 Release 未提供已验证的字段变更入口。");
}
window.handleItemChange(propertyName, value == null ? "" : String(value));
}
setFormProperty("supplier_status", "pending");handleItemChange 在不同客户端版本中的位置和参数可能变化,所以应由一个小型 adapter 包装,而不是散落在每个 Method 中。
等待关系页签就绪
标注:Classic client compatibility
原始素材里有多个无限 setTimeout 循环。一旦目标页签不存在,它们会永久轮询。下面的 helper 有明确超时,并在失败时给出可诊断错误。
function waitForRelationshipTabbar(timeoutMs = 5000) {
const startedAt = Date.now();
return new Promise((resolve, reject) => {
function inspect() {
const tabbar = parent.relationships?.relTabbar;
if (tabbar) {
resolve(tabbar);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error("关系页签在限定时间内没有完成加载。"));
return;
}
window.setTimeout(inspect, 50);
}
inspect();
});
}按条件显示关系页签
标注:Classic client compatibility
关系页签应以 RelationshipType 的名称解析 ID;不要把某个数据库的真实 GUID 写进通用文档。
const TAB_RULES = {
design: ["Example Design Detail"],
purchasing: ["Example Supplier Detail"],
};
const MANAGED_RELATIONSHIPS = [
"Example Design Detail",
"Example Supplier Detail",
];
async function applyRelationshipTabRule(mode) {
const tabbar = await waitForRelationshipTabbar();
for (const relationshipName of MANAGED_RELATIONSHIPS) {
const relationshipId = top.aras.getRelationshipTypeId(relationshipName);
if (!relationshipId) {
console.warn(`RelationshipType not found: ${relationshipName}`);
continue;
}
const visible = (TAB_RULES[mode] || []).includes(relationshipName);
tabbar.setTabVisible(relationshipId, visible);
}
}
const mode = document.thisItem?.getProperty("change_mode", "") || "";
applyRelationshipTabRule(mode).catch((error) => {
console.error(error);
top.aras.AlertError("关系页签初始化失败,请刷新后重试。");
});调用 setTabVisible 的名称、大小写和可用性必须在目标 Release 验证。旧资料中同时存在 SetTabVisible、setTabVisible、按 label 查找页签等写法,不能混用。
更新关系行的客户端缓存
标注:Stable(IOM DOM) + Private API(立即重绘)
先更新父 Item 的关系 DOM。这样父 Item 保存时,数据才会进入正常的保存事务。
function isGuid(value) {
return /^[0-9a-f]{32}$/i.test(value || "");
}
function setRelationshipProperty(parentItem, relationshipId, propertyName, value) {
if (!parentItem || !isGuid(relationshipId)) {
throw new Error("关系行上下文无效。");
}
const matches = parentItem.getItemsByXPath(
`Relationships/Item[@id='${relationshipId}']`
);
if (matches.getItemCount() !== 1) {
throw new Error("无法唯一定位关系行。");
}
const relationship = matches.getItemByIndex(0);
relationship.setProperty(propertyName, String(value ?? ""));
const action = relationship.getAction();
if (!action || action === "get") {
relationship.setAction("update");
}
}这里保留原有 add / delete 等动作,只把空值或只读查询态 get 转为 update。
立即修改可见单元格通常会接触 grid.items_Experimental 或内部 layout。这属于 Private API。正确做法是:
- 把私有重绘逻辑放在一个按 Release 命名的 adapter 中。
- 数据更新只走上面的 Item DOM helper。
- adapter 不可用时,允许用户保存后通过受支持的刷新动作看到结果。
- 为插入、编辑、删除、取消保存和切换页签分别做回归测试。
编辑页签的双层控制
标注:Classic client compatibility
客户端事件可以根据缓存属性返回 false,阻止用户在网格里编辑:
function canEditRelationshipRow(parentItem, relationshipId) {
if (!parentItem || !/^[0-9a-f]{32}$/i.test(relationshipId || "")) {
return false;
}
const rows = parentItem.getItemsByXPath(
`Relationships/Item[@id='${relationshipId}']`
);
if (rows.getItemCount() !== 1) {
return false;
}
return rows.getItemByIndex(0).getProperty("business_locked", "0") !== "1";
}但相同判断还要放进关系 ItemType 的 Server Event。否则用户仍可绕过浏览器直接提交 AML。
为什么不要这样做
用 eval 调用内部 expression 方法
标注:Reject
eval("expression_..._setExpression(false)") 同时依赖生成方法名、字段 ID、Classic Form 实现和非严格模式。它还会把真正的错误吞掉。改用目标 Release 明确支持的字段组件接口,并把业务授权放到服务端。
用 frame 数组索引刷新
标注:Reject
parent[2].onSearchCommand() 假设关系 frame 永远位于固定下标。只要页面布局或客户端版本改变,就可能刷新错误窗口。刷新动作也应由 adapter 根据语义定位目标,而不是依赖序号。
隐藏页签来保护数据
标注:Reject
隐藏只是视觉效果。真正的数据读取由 Permission、Can Discover、Lifecycle Permission、Team 或项目自己的服务端规则决定。
验证清单
- [ ] 记录 Innovator Release、客户端类型和绑定事件。
- [ ] 新建、查看、编辑、锁定、他人锁定状态分别验证。
- [ ] 页面没有目标字段或目标 RelationshipType 时能够安全退出。
- [ ] 轮询有超时,事件监听器不会重复注册。
- [ ] 取消父 Item 保存不会遗留提前创建的关系或子 Item。
- [ ] 相同业务规则已在服务端执行,浏览器绕过测试仍会失败。
