Skip to content

表单、字段与关系页签

这一章处理最常见的客户端需求:字段联动、显示隐藏、只读、关系页签切换,以及保存前的客户端 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 放在不同事件中,thisdocument.thisItem 和事件参数的含义并不相同。不要用 parent[1]parent[2] 之类的 frame 下标猜测上下文。

绑定位置常见上下文适合做什么
Form Event当前 Form 与客户端 Item初始化字段状态、建立事件监听
Field Event当前字段、Form 和客户端 Item字段联动、输入提示、固定搜索条件
Relationship Grid Eventrelationship ID、property name、grid context行级验证、更新客户端关系 DOM
Item Action被选中的 Item 或 Item 集合打开窗口、调用服务端 Method

在每个 Method 开头写清楚绑定位置和所需参数:

js
/**
 * 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。

js
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

js
const component = getFieldComponentByName("review_result");

if (component && typeof component.setDisabled === "function") {
  component.setDisabled(true);
}

禁用控件不会阻止用户通过 AML、集成程序或其他界面修改属性。对应规则仍应放在服务端:

csharp
Innovator inn = this.getInnovator();

if (!CanCurrentUserChangeReviewResult(this))
{
    return inn.newError("当前状态不允许修改审核结果。");
}

return this;

上面的 CanCurrentUserChangeReviewResult 是项目自己的服务端规则,不应依赖浏览器传来的布尔值。

字段联动与脏数据标记

标注:Classic client compatibility

不要只改输入框的 value。应调用目标 Release 支持的字段变更入口,让客户端 Item DOM、脏数据状态和依赖表达式一起更新。

js
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 有明确超时,并在失败时给出可诊断错误。

js
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 写进通用文档。

js
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 验证。旧资料中同时存在 SetTabVisiblesetTabVisible、按 label 查找页签等写法,不能混用。

更新关系行的客户端缓存

标注:Stable(IOM DOM) + Private API(立即重绘)

先更新父 Item 的关系 DOM。这样父 Item 保存时,数据才会进入正常的保存事务。

js
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。正确做法是:

  1. 把私有重绘逻辑放在一个按 Release 命名的 adapter 中。
  2. 数据更新只走上面的 Item DOM helper。
  3. adapter 不可用时,允许用户保存后通过受支持的刷新动作看到结果。
  4. 为插入、编辑、删除、取消保存和切换页签分别做回归测试。

编辑页签的双层控制

标注:Classic client compatibility

客户端事件可以根据缓存属性返回 false,阻止用户在网格里编辑:

js
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。
  • [ ] 相同业务规则已在服务端执行,浏览器绕过测试仍会失败。

官方延伸阅读

本站内容仅供学习与参考