自写页 SDK:在受控范围内开发项目页面

SDK 负责把 HQControl 的受控运行数据能力提供给项目 HTML 页面。它提供实时 Tag 快照与订阅、类型化读写、历史、报警、当前用户、权限、语言、导航、设备弹窗和表格辅助方法。SDK 不限定页面形态;项目页面可以按现场需要组织为报表、看板、诊断页、质量页、能耗页或客户专用页面。

SDK
页面位置部署在 HQControl 的项目 HTML 页面,运行在 sandbox iframe 中
最小接入声明 Tag 范围 -> ready() 获取 API -> 读取、订阅、查询历史或报警
适用页面班次报表、能耗看板、设备诊断和客户专用页面

SDK: 自写页 SDK:在受控范围内开发项目页面

自写页 SDK 面向部署在 HQControl 运行端的项目 HTML 页面。页面在 sandbox iframe 中运行,通过 `HQVISU_CUSTOM_PAGE.ready()` 获取 SDK 1.5.0 页面 API,在页面能力范围内读取、订阅和受控写入 Tag,并查询历史、报警、会话、权限和语言数据。项目可以用它开发班次报表、能耗汇总、设备诊断、客户看板等业务页面;页面 UI、计算口径和交互由项目代码实现,数据权限和工业操作边界由 HQControl 宿主执行。

1. 自写页 SDK 负责什么

SDK 负责把 HQControl 的受控运行数据能力提供给项目 HTML 页面。它提供实时 Tag 快照与订阅、类型化读写、历史、报警、当前用户、权限、语言、导航、设备弹窗和表格辅助方法。SDK 不限定页面形态;项目页面可以按现场需要组织为报表、看板、诊断页、质量页、能耗页或客户专用页面。

  • 适合:班次产量报表、能耗日报、设备健康页、质量追溯页、工单看板、客户展示页。
  • 页面形式:普通 HTML,可包含独立 CSS、JavaScript、图表库和组件逻辑。
  • 数据来源:声明范围内的 Tag、历史数据、报警数据和语言资源。
  • 责任边界:HQControl 提供数据和权限边界,页面展示和业务计算由项目代码负责。
  • SDK 版本:当前手册按 hq-custom-page.js 的 SDK 1.5.0 能力编写。

2. 运行模型和安全边界

自写页由 HQControl 以 sandbox iframe 方式加载。页面不直接继承主应用内部对象,也不绕过登录、权限、Tag 范围和服务端校验。页面先加载 hq-common.jshq-custom-page.js,再通过 HQVISU_CUSTOM_PAGE.ready() 等待宿主桥并取得页面 API。

  • 静态声明或动态精确使用声明共同形成页面能力;宿主仍逐个校验最终完整 Tag,不能按前缀搜索或枚举目录。
  • 实时读取、历史和报警是不同能力范围;动态实时读取不会自动扩大历史或报警范围。
  • 写入必须同时满足页面范围、宿主能力、当前用户 tag:write 权限和 Tag 可控性。
  • 导航、弹窗、报警和写入等能力由当前宿主声明;页面必须先用 api.supports() 判断,不能假定每个宿主都提供。
  • 在普通浏览器直接打开页面文件时没有真实宿主桥,ready() 会超时;本地开发应显式使用随版本提供的 Mock Host。

页面代码本身可以做什么

SDK 管理的是 HQControl 宿主能力,不是普通 JavaScript 的许可清单。自写页可以使用 DOM、CSS、组件和图表库,也可以在浏览器及 iframe sandbox 允许的范围内使用标准交互能力。

  • 支持普通 DOM 和组件逻辑,以及 alertconfirmprompt
  • 支持 window.open、表单提交、浏览器导航和用户主动触发的下载;弹窗拦截和用户激活规则仍由浏览器决定。
  • 支持 Clipboard API,但必须同时满足 HTTPS/安全上下文、用户激活、浏览器权限和企业策略。
  • 不支持 Service Worker 注册;页面不能获得 Portal Cookie、登录 Token、Admin DOM、配置 API 或原始宿主服务对象。
  • 页面不能直接轮询 PLC,也没有报警确认、搁置等未公开写接口。
  • api.supports() 只判断宿主桥能力,不判断浏览器 API 是否存在或获准。

3. 最小接入步骤

开发自写页时先写声明,再引入 SDK,最后在 ready() 之后访问数据。不要在 ready() 解析前读取 HQVISU_HOST,此时宿主桥可能还没有注入。

  • 在 HTML 中放置 type="application/json"id="hqvisu-custom-page" 的声明块。
  • 声明 readableTags 和需要写入的 writableTags,只写页面真实需要的点位。
  • 按顺序引入 /runtime/hqVisuDesigner/hq-common.js/runtime/hqVisuDesigner/hq-custom-page.js
  • 调用 HQVISU_CUSTOM_PAGE.ready(),在回调或 await 之后使用 SDK API。
  • 先做实时值和质量显示,再增加历史、报警、写入和表格。
  • 上线前在 HQControl 运行端打开页面,不以本地文件直接打开作为验收结果。
<!doctype html>
<html>
<head><meta charset="utf-8"><title>产线报表</title></head>
<body>
  <section id="app">正在连接 HQControl...</section>

  <script type="application/json" id="hqvisu-custom-page">
  {
    "readableTags": ["<device-code>.<temperature-tag-code>", "<device-code>.<run-state-tag-code>"],
    "writableTags": ["<device-code>.<setpoint-tag-code>"]
  }
  </script>
  <script src="/runtime/hqVisuDesigner/hq-common.js"></script>
  <script src="/runtime/hqVisuDesigner/hq-custom-page.js"></script>
  <script>
  HQVISU_CUSTOM_PAGE.ready().then((api) => {
    if (!api.supports("tag.read")) {
      document.getElementById("app").textContent = "当前宿主未提供实时 Tag 读取能力";
      return;
    }
    document.getElementById("app").textContent =
      "当前温度:" + api.readNumber("<device-code>.<temperature-tag-code>", 0).toFixed(1) + " ℃";
  }).catch((err) => {
    document.getElementById("app").textContent = "SDK 初始化失败:" + err.message;
  });
  </script>
</body>
</html>

4. 点位声明写法

服务端会静态解析自写页 HTML 中的 JSON 声明块和受支持的页面数据声明,并据此签发页面能力。Tag 必须使用完整点分编码,例如 <device-code>.<temperature-tag-code>;位引用只支持规范的 [0][31]。声明无效或超过上限时,页面能力不会按预期签发。

  • 可用配置块 ID:hqvisu-custom-pagehqvisu-custom-page-confighqvisu-page-confighqvisu-dependencies
  • 可读字段别名:readableTagstagstagNamesreadTagswatchTags
  • 可写字段别名:writableTagswriteTagsoutputs 中的相同声明也会被收集。
  • dependencies 下支持 tagstagNamesreadableTagsreadTagswritableTagswriteTags
  • 字段值可以是数组,也可以是用换行、逗号或分号分隔的字符串。
  • 实时读写可声明 <device-code>.<status-word-tag-code>[3];历史查询不支持位引用,必须查询父 Tag。
  • SDK 1.5.0 会自动跟踪配置块中声明的可读点;ready({ tags: [...] }) 只用于兼容旧页面或追加订阅。
  • 手册中的 <device-code><tag-code> 是占位符;发布前必须替换为项目实际编码,不能原样填写。
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": [
    "<device-code>.<temperature-tag-code>",
    "<device-code>.<pressure-tag-code>",
    "<device-code>.<run-state-tag-code>",
    "<device-code>.<output-tag-code>",
    "<device-code>.<energy-tag-code>"
  ],
  "writableTags": ["<device-code>.<setpoint-tag-code>"]
}
</script>

动态精确使用:参数、循环和字符串组合得到 Tag

如果页面要根据设备参数、循环或函数在运行时得到最终完整 Tag,可声明 exact-on-use。它表示宿主可以逐个受理运行时得到的精确 Tag,不是前缀通配、目录枚举或无限权限。每个最终 Tag 仍经过格式、存在性、启用状态、页面会话和读写规则校验。

  • liveTagRead.maxActiveTags 可设 1 至 8000;示例使用页面上限 8000。
  • liveTagWrite.maxActiveTags 的正式上限为 2000。
  • 适合:从页面参数选择设备、遍历已知设备清单、函数拼接完整 Tag、组件按业务对象绑定 Tag。
  • 不支持:按 <device-code>.* 搜索、读取 Tag 目录、枚举所有设备或绕过服务端逐项校验。
  • 动态声明只扩展实时精确读写;历史、报警、设备弹窗和预取仍依据各自范围。
  • 静态 Tag 仍推荐用于首屏预取、初始订阅、诊断和最小权限交付证据。
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<run-state-tag-code>"],
  "dependencies": {
    "runtimeAccess": {
      "liveTagRead": { "mode": "exact-on-use", "maxActiveTags": 8000 },
      "liveTagWrite": { "mode": "exact-on-use", "maxActiveTags": 2000 }
    }
  }
}
</script>

const deviceCode = new URLSearchParams(location.search).get("device");
const temperatureTag = `${deviceCode}.<temperature-tag-code>`;
api.track(temperatureTag);

5. ready() 与生命周期

ready(options | callback) 等待宿主桥并创建页面 API,返回 Promise<PageApi>;传回调时仍会返回同一个 Promise。声明中的可读 Tag 与 options.tags 会合并、去重并开始跟踪。页面不在 HQControl 运行端或 Mock Host 中打开时,常见错误是 HQVISU_HOST is not available. Open this page from the HQ runtime shell.

  • tags:追加首批跟踪 Tag;通常直接用静态声明即可。
  • timeoutMs:等待普通宿主对象,默认 5000ms;bridgeTimeoutMs:嵌入桥握手,默认约 750ms。
  • writeTimeoutMs / writeAckTimeoutMs:写入宿主确认,默认 10000ms。
  • historyTimeoutMs:历史请求,默认 15000ms;alarmsTimeoutMs:报警请求,默认 15000ms;localeTimeoutMs:切换语言请求。
  • transport: "postMessage"postMessageOnly: true:强制嵌入桥,仅用于兼容和诊断,普通项目页不应自行指定。
  • destroy():页面卸载或重建时取消 SDK 订阅,避免旧回调继续运行。
const api = await HQVISU_CUSTOM_PAGE.ready({
  timeoutMs: 5000,
  historyTimeoutMs: 15000,
  alarmsTimeoutMs: 15000
});

window.addEventListener("beforeunload", () => api.destroy(), { once: true });

SDK 全局对象 API

正式全局对象是 HQVISU_CUSTOM_PAGE,兼容别名为 HQVisuCustomPage。下列方法在 ready() 前可用;除表格渲染外,它们不访问工业数据。

  • version:返回字符串 1.5.0
  • ready(options | callback):返回 Promise<PageApi>
  • normalizeTagName(value):转成字符串、去首尾空白,并去掉包住整个值的一层单引号或双引号;返回字符串。
  • tagList(value):接受字符串、嵌套数组或单值,按换行/逗号/分号拆分,规范化、过滤空值和形如 <TagName> 的旧模板变量并去重;返回 string[]
  • renderTable(target, rows, options):渲染轻量 HTML 表格;返回 { rowCount, columnCount }
const normalizedTag = HQVISU_CUSTOM_PAGE.normalizeTagName("  'packaging-line-01.temperature'  ");
const reportTags = HQVISU_CUSTOM_PAGE.tagList([
  "packaging-line-01.temperature, packaging-line-01.pressure",
  "packaging-line-01.temperature"
]);

PageApi 根对象和能力矩阵

ready() 返回的根对象是页面开发的正式入口。api.host 保留低层宿主 facade,宽度可能随传输方式不同;业务页面优先使用根对象及 api.authapi.permissionsapi.historyapi.alarms 等稳定封装。

  • 属性:versionbridgeVersionschemaRevision、冻结的 capabilities、低层 host
  • 发现:getCapabilities() 返回可修改副本;supports(name) 返回布尔值,未知能力返回 false。
  • 命名空间:authpermissionslanguagehistoryalarmsnavigationdialog
  • 根方法:tracksubscribe、全部读取和写入方法、queryHistorytranslate / trenderTablebindJsonTabledestroy
  • 能力名:page.parameterstag.readtag.writehistory.readuser.readnavigationpopupalarm.readlog.write
  • supports() 只说明当前传输暴露此能力,不代表用户有权限,也不会扩大页面范围。

当前用户和权限 API

稳定会话结果为 { user, permissions }。登录用户的 user 包含 usernamedisplayNamepermissions;匿名直接 Screen Gateway 返回 user: null 和空权限数组。SDK 不向页面暴露 Token、Cookie、角色或页面能力密钥。

  • api.auth.getSession():返回会话快照;为兼容同步和异步宿主,统一写成 await api.auth.getSession()
  • api.auth.getCurrentUser():返回用户快照或 null。
  • api.auth.hasPermission(code) / api.permissions.has(code):返回 Promise<boolean>
  • api.auth.requirePermission(code) / api.permissions.require(code):有权限时解析为 true,无权限时 Promise 拒绝。
  • api.host.auth 是低层宿主表面,某些传输还提供 getUsergetUsernamegetDisplayName;业务代码不要依赖这些可选宽度。
const api = await HQVISU_CUSTOM_PAGE.ready();
const session = await api.auth.getSession();
const canWrite = api.supports("tag.write")
  && await api.permissions.has("tag:write");

renderCurrentUser(session.user);
setWriteControlsEnabled(canWrite);

6. 读取实时值和质量

读取方法是同步的,返回宿主推送到页面的最近快照,不会每次调用都向服务器发请求。读取值不会因为质量异常自动失效,页面必须单独检查质量。所有读取都建议传 fallback,避免首屏、断线或没有可用快照时显示 undefined

  • read(tag, fallback):原始快照值;别名方法没有额外网络请求。
  • readNumber / readTagNumberreadDouble / readTagDouble:用 JavaScript Number() 转换,不能得到有限数字时返回 fallback。
  • readInteger / readTagInteger:数字向零截断后返回,转换失败返回 fallback。
  • readBoolean / readTagBoolean:返回布尔;识别布尔、非零/零数字,以及 true/false、1/0、yes/no、on/off 字符串。
  • readString / readTagString:非 null 值转成字符串,否则返回 fallback。
  • readJson / readTagJson:对象值直接返回,字符串尝试 JSON 解析,空值或解析失败返回 fallback。
  • readDateTime / readTagDateTime:返回有效 Date 对象,不能解析时返回 fallback,不是 ISO 字符串。
  • readTagQuality(tag, fallback):返回 goodbadstalependingunavailable;缺失且没有 fallback 时返回 pending
  • readTagQualityGood(tag):仅质量等于 good 时返回 true。getMeta(tag) 返回元信息对象,常见字段有 qualitytimestampsource
const temp = api.readNumber("<device-code>.<temperature-tag-code>", 0);
const running = api.readBoolean("<device-code>.<run-state-tag-code>", false);
const qualityGood = api.readTagQualityGood("<device-code>.<temperature-tag-code>");

renderKpi({
  title: "当前温度",
  value: temp.toFixed(1),
  unit: "℃",
  state: qualityGood ? "normal" : "bad-quality",
  subtitle: running ? "产线运行中" : "产线停止"
});

7. 订阅实时变化

track(tags) 规范化并开始跟踪 Tag,返回去重后的 Tag 名数组。subscribe(tags, callback) 返回幂等取消函数;回调签名是 (items, api)。不要假定订阅回调一定承担首屏渲染,推荐先读最近快照,再订阅后续变化。

  • 每个变化项至少有 tagNamefullCodevalue,并合并 qualitytimestampsource 等可用元信息。
  • 每页最多保持 8000 个活动唯一 Tag;无效、缺失、停用或拒绝的候选不会永久占用配额。
  • 线协议每批最多 256 个 Tag,SDK 最多四批并发;一个大页面仍只有一个逻辑实时订阅和一个 WebSocket。
  • 回调中可用第二个参数重新读取一致的当前快照;高频页面应集中订阅一组 Tag 后统一刷新 UI。
  • 离开页面、切换数据范围或销毁组件时调用取消函数;destroy() 会集中清理剩余订阅。
const stopRealtime = api.subscribe([
  "<device-code>.<temperature-tag-code>",
  "<device-code>.<run-state-tag-code>",
  "<device-code>.<output-tag-code>"
], (items, host) => {
  updateRealtimeCards({
    temperature: host.readNumber("<device-code>.<temperature-tag-code>", 0),
    running: host.readBoolean("<device-code>.<run-state-tag-code>", false),
    output: host.readNumber("<device-code>.<output-tag-code>", 0)
  });
});

// 不再需要实时刷新时
// stopRealtime();

8. 写入 Tag

所有写入方法都返回 Promise。工业写入必须通过四层门槛:页面可写范围、宿主 tag.write 能力、当前用户 tag:write 权限和 Tag 自身允许控制。宿主还要求可信用户操作;页面加载时、定时器或无用户激活的自动写入会被阻止。同一页面一次只允许一个正在等待确认或完成的写操作。

  • writeTag / write(tag, value, options):原值写入;options.valueTypeoptions.type 可指定类型。
  • writeTagBoolean:按 JavaScript 真值转布尔,字符串 "false" 仍为 true,表单值应先显式解析;writeTagInteger:数字向零截断;writeTagDouble:转为 Number。
  • writeTagString:null 转为空字符串,其余转为字符串;writeTagJson:按 JSON 值提交;writeTagDateTime:Date 转 ISO 字符串。
  • options.reason 是审计原因;当 Tag 配置要求填写原因时必须非空。设定值、复位和控制动作应先由操作员确认。
  • 常见失败:Tag 越界或不存在、无权限、Tag 不可控、值非法或过大、原因缺失、写入繁忙或太频繁、页面已切换、宿主不可用或超时。
async function increaseSetpoint() {
  if (!api.supports("tag.write") || !await api.permissions.has("tag:write")) {
    showToast("当前页面或用户不具备 Tag 写入能力", "error");
    return;
  }
  const current = api.readNumber("<device-code>.<setpoint-tag-code>", 0);
  const next = current + 1;
  if (!confirm(`确认把设定值写为 ${next.toFixed(1)}?`)) return;

  try {
    const result = await api.writeTagDouble("<device-code>.<setpoint-tag-code>", next, {
      reason: "操作员调整班次设定值"
    });
    handleWriteResult(result);
  } catch (error) {
    showToast("写入失败:" + error.message, "error");
  }
}

写入结果:必须同时判断 accepted 和 status

成功解析的 Promise 表示宿主返回了写入结果,不等于设备一定已达到目标值。核心结果为 { accepted, commandId, journalId, status, automaticRetryAllowed, currentValue }currentValue 是完整 TagValue 快照,可用于刷新显示。

  • done / proxied:请求已完成;仍应以返回快照或后续实时值显示最终状态。
  • readback_failed / readback_mismatch:写动作可能已执行,但回读失败或不一致,必须提示操作员核对现场。
  • latest_superseded:写后已有更新值覆盖本次结果,应显示最新快照,不把旧目标值当作当前值。
  • pending_verification / outcome_uncertain:结果无法安全确认,绝对不能自动重试;提示操作员核对 PLC/Tag 实际状态和 commandId
  • failedcancelledpage_changedaccepted: false:没有得到正常完成结果,按状态提示,不宣称写入成功。
  • automaticRetryAllowed 为 false 或缺失时不得自行重放;工业页面不要用 catch 后立即重试。
function handleWriteResult(result) {
  const status = String(result?.status || "");
  if (status === "outcome_uncertain" || status === "pending_verification") {
    showBlockingNotice("写入结果待人工核对,请勿重试", result?.commandId);
    return;
  }
  if (result?.accepted !== true) {
    showToast("写入未受理:" + (status || "unknown"), "error");
    return;
  }
  if (["readback_failed", "readback_mismatch", "latest_superseded"].includes(status)) {
    showToast("写入需核对:" + status, "warning");
    renderTagSnapshot(result.currentValue);
    return;
  }
  showToast("写入完成:" + status, "success");
  renderTagSnapshot(result.currentValue);
}

9. 查询历史和趋势

api.history.query(request) 与根别名 api.queryHistory(request) 都返回 Promise<HistoryQueryResult>。请求只查询历史库,不直接轮询 PLC;所有 Tag 必须具有页面历史读取范围。

  • 请求字段:tagsstartendintervalaggregate,可选 points
  • tags 必填、去重后最多 16 个;历史不支持 [0][31] 位引用,必须查询父 Tag。
  • startend 必须是 RFC 3339 时间,最大范围 366 天。
  • interval 支持 mhdw 单位,例如 15m1h1d1w,最小 1 分钟。
  • aggregate 支持 avgminmaxsumcountfirstlastdeltaraw
  • delta 适合电表、水表、产量累积量等报表口径。
  • points 省略或为 0 时默认 20000,最大 20000,且不能小于去重后的 Tag 数。
const weekEnergy = await api.history.query({
  tags: [
    "<primary-device-code>.<energy-tag-code>",
    "<secondary-device-code>.<energy-tag-code>"
  ],
  start: "2026-07-01T00:00:00+08:00",
  end: "2026-07-08T00:00:00+08:00",
  interval: "1d",
  aggregate: "delta",
  points: 20000
});

renderEnergyReport(weekEnergy.items, {
  interval: weekEnergy.effectiveInterval,
  truncated: weekEnergy.truncated
});

历史返回结果怎么用

结果结构为 { items, effectiveInterval, rangeStart, rangeEnd, truncated }items 按请求 Tag 返回序列;每项为 { tag, tagId, displayName, points },每个点为 { t, value, quality }

  • tag 是完整 Tag 编码;tagId 是服务器标识;displayName 可能为空。
  • points[].t 是 RFC 3339 时间;value 是原始值或聚合值;quality 是该点质量。
  • effectiveInterval 是服务器实际使用的间隔;点数预算不足时可能比请求间隔更粗。
  • rangeStartrangeEnd 是实际采用的 UTC/RFC 3339 范围。
  • truncated 为 true 表示服务器为满足点数预算放粗了间隔;页面应向用户显示实际间隔。
  • 没有数据时仍应按 Tag 得到空 points,页面应显示空态,不把空数组当成 0。
for (const series of weekEnergy.items || []) {
  const chartPoints = (series.points || []).map((point) => ({
    time: new Date(point.t),
    value: point.value,
    quality: point.quality
  }));
  drawEnergySeries(series.displayName || series.tag, chartPoints);
}
showActualInterval(weekEnergy.effectiveInterval, weekEnergy.truncated === true);

报警范围声明

报警范围由服务器根据页面声明签发,客户端查询参数不能扩大。可在 JSON 配置的 alarmalarmScopepageAlarm 下声明维度;页面可读 Tag 也会自动加入报警 Tag 范围。

  • 支持维度:tagsplcCodesfunctionGroupCodesdeviceGroupCodessignalGroupCodes
  • 没有任何报警范围时,活动和历史报警查询返回空范围。
  • 动态 liveTagRead 不自动扩大报警范围;需要报警的 Tag 或分组必须静态声明。
  • 服务端会用签发能力覆盖查询中的范围字段,页面不能通过附加 JSON 字段查看更多报警。
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<temperature-tag-code>"],
  "alarmScope": {
    "plcCodes": ["<device-code>"],
    "functionGroupCodes": ["<function-group-code>"],
    "deviceGroupCodes": [],
    "signalGroupCodes": []
  }
}
</script>

10. 查询和订阅报警

报警命名空间提供同步宿主快照、异步活动报警、异步历史查询和变化订阅。getActive()getActiveAlarms() 都返回 Promise<AlarmQueryResult>queryHistory(request) 返回相同分页结果结构。

  • getSnapshot() 同步返回宿主已有快照;不同宿主可能返回数组,也可能返回 { alarms }{ activeAlarms } 包装对象。
  • 查询字段:startendseveritystatesource / qlimitoffsetcursorTscursorId
  • 历史查询省略时间时默认最近 24 小时;单次最大跨度 24 小时。
  • limit 省略或为 0 时是 500,最大也是 500;超过 500 会被截到 500。offset 必须非负。
  • 结果为 { items, count, hasMore, nextCursorTs, nextCursorId };有下一页时优先把两个 nextCursor 字段传回下一次查询。
  • subscribe(callback) 返回幂等取消函数,回调为 (items, api);不同宿主可能传完整快照或变化项,应把它作为刷新信号,不要盲目追加。
function alarmItemsFromSnapshot(snapshot) {
  if (Array.isArray(snapshot)) return snapshot;
  if (Array.isArray(snapshot?.alarms)) return snapshot.alarms;
  if (Array.isArray(snapshot?.activeAlarms)) return snapshot.activeAlarms;
  return [];
}

renderAlarmSummary(alarmItemsFromSnapshot(api.alarms.getSnapshot()));
const activeResult = await api.alarms.getActiveAlarms();
renderAlarmSummary(activeResult.items || []);

const historyResult = await api.alarms.queryHistory({
  start: "2026-07-07T00:00:00+08:00",
  end: "2026-07-08T00:00:00+08:00",
  severity: ["critical", "high"],
  state: ["active", "recovered"],
  limit: 200
});

const stopAlarms = api.alarms.subscribe(() => scheduleActiveAlarmRefresh());

报警项和分页结果字段

items 中每个报警项都可以直接用于列表、卡片或详情页。页面应按字段存在性兼容旧数据,不要根据显示文本反推状态。

  • 身份:iddeviceIddeviceCodedeviceNametagIdtagCodefullCodedisplayName
  • 内容:messageTextalarmKindseveritystatedisplayValuequality
  • 时间:activeTsrecoverTscloseTsackTsshelvedUntilserverTs
  • 标志:suppressed;部分数据还可能带 ackUserIdareaCodeobjectIdentifiergroupCode
  • count 是本页实际返回数量,hasMore 表示还有下一页;下一页使用 nextCursorTsnextCursorId 可避免 offset 翻页期间数据变化造成重复或遗漏。
  • SDK 只公开报警读取和订阅;自写页不能调用未公开的确认、搁置或关闭接口。

导航 API

导航正式方法是 gotoPagegotoAndongotoScreenopenUrl。调用前检查 api.supports("navigation")。宿主放置的新窗口、全屏看板、Screen 跳转和外部 URL 通常要求可信用户操作;嵌入桥返回 true 只表示请求已分派,不表示目标页面最终加载完成。

  • navigation.gotoPage(pageName, options):跳转页面包内安全的相对 HTML 页面。
  • navigation.gotoAndon(pageName, options):由宿主打开安灯/全屏页面。
  • navigation.gotoScreen(screenCode):跳转 HQControl Screen。
  • navigation.openUrl(url, options):宿主模式只接受 HTTP(S) URL。
  • 选项支持 openInNewWindowframeTargetparameters: [{ key, value }]
  • 参数最多 64 个;key 最长 256 字符,value 最长 2048 字符;无效参数会被忽略或截断。
document.getElementById("open-detail").addEventListener("click", () => {
  const dispatched = api.navigation.gotoPage("device-detail.html", {
    frameTarget: "_self",
    parameters: [
      { key: "device", value: "<device-code>" },
      { key: "source", value: "shift-report" }
    ]
  });
  if (!dispatched) showToast("页面跳转未被宿主受理", "error");
});

设备弹窗 API

api.dialog.openDevicePopup(payload) 请求宿主打开正式设备弹窗,api.dialog.close() 请求关闭当前宿主弹窗。调用前检查 api.supports("popup"),并从真实点击事件中调用。返回 true 只表示请求已分派。

  • plcdeviceCode 必填,且该 PLC 至少有一个 Tag 位于页面可读范围。
  • focusTagNames 可选,最多 64 个;每个 Tag 都必须在页面可读范围。
  • templateCode / id 可指定弹窗模板。
  • diagnosisControlgroupRightIndex 是可选弹窗上下文字段。
  • 设备弹窗不能扩大页面读写范围,也不等于页面获得 Popup 内部服务对象。
document.getElementById("open-device-popup").addEventListener("click", () => {
  const dispatched = api.dialog.openDevicePopup({
    deviceCode: "<device-code>",
    templateCode: "<popup-template-code>",
    focusTagNames: [
      "<device-code>.<temperature-tag-code>",
      "<device-code>.<run-state-tag-code>"
    ]
  });
  if (!dispatched) showToast("设备弹窗未被宿主受理", "error");
});

11. 完整案例:班次运行与能耗报表

完整业务页应先定义 Tag 和班次口径,完成首屏快照,再建立实时订阅,并把历史与报警作为可失败的异步区域加载。组件销毁时返回统一清理函数;这样实时区、历史区和报警区互不耦合,单个查询失败不会让整页白屏。

  • 实时 KPI:用 readNumber / subscribe 展示当前产量、温度、运行状态、质量。
  • 趋势曲线:用 history.query 获取小时、天、周维度的数据,再交给页面图表库渲染。
  • 能耗或产量报表:累积表使用 aggregate: "delta",普通模拟量使用 avg/min/max
  • 报警摘要:用 alarms.getSnapshot()getActiveAlarms()queryHistory() 汇总当前风险和历史记录。
  • 数据表格:简单表格可用 renderTable,复杂表格可由项目组件渲染。
  • 异常态:空数据、越权、质量 bad/stale/pending、查询超时都要有页面提示。
async function startShiftReport(api, shiftStart, shiftEnd) {
  const tags = {
    output: "<device-code>.<output-tag-code>",
    energy: "<device-code>.<energy-tag-code>",
    running: "<device-code>.<run-state-tag-code>"
  };

  function renderLive() {
    drawKpiCards({
      output: api.readNumber(tags.output, 0),
      energy: api.readNumber(tags.energy, 0),
      running: api.readBoolean(tags.running, false),
      outputQuality: api.readTagQuality(tags.output, "pending")
    });
  }

  renderLive();
  const stopRealtime = api.subscribe(Object.values(tags), renderLive);

  try {
    const trend = await api.history.query({
      tags: [tags.energy],
      start: shiftStart,
      end: shiftEnd,
      interval: "1h",
      aggregate: "delta"
    });
    drawEnergyTrend(trend.items || [], trend.effectiveInterval);
  } catch (error) {
    renderTrendError(error.message);
  }

  try {
    const alarms = await api.alarms.getActiveAlarms();
    api.renderTable("#alarm-table", alarms.items || [], {
      columns: [
        { key: "severity", label: "等级" },
        { key: "messageText", label: "报警内容" },
        { key: "activeTs", label: "发生时间" }
      ]
    });
  } catch (error) {
    renderAlarmError(error.message);
  }

  return () => stopRealtime();
}

12. renderTable 与 bindJsonTable

api.renderTable 与全局 HQVISU_CUSTOM_PAGE.renderTable 是同一轻量表格辅助,不是报表系统。它把数组、{ rows: [...] }{ items: [...] } 渲染成 HTML table,并返回 { rowCount, columnCount }bindJsonTable 完成首次渲染后订阅 JSON Tag,返回取消函数。

  • target 可以是 CSS 选择器或 DOM 元素。
  • options.columns 可传字符串数组,也可传 { key, label } 对象数组。
  • 未提供 columns 时,SDK 会根据前 25 行数据推断列。
  • options.className 可指定表格 class,页面自行写 CSS。
  • 单元格通过 textContent 写入,不把 Tag 内容当 HTML 执行;目标元素不存在时返回 { rowCount: 0, columnCount: 0 }
  • bindJsonTable(target, tagName, options) 接受数组 JSON、{ rows }{ items };空值或解析失败时渲染空表。
  • 复杂排序、分页、冻结列和导出 Excel 应由项目页面或第三方表格组件实现。
api.renderTable("#quality-table", {
  rows: [
    { tag: "<device-code>.<temperature-tag-code>", value: "72.4", quality: "good" },
    { tag: "<device-code>.<pressure-tag-code>", value: "0.68", quality: "good" }
  ]
}, {
  className: "report-table",
  columns: [
    { key: "tag", label: "点位" },
    { key: "value", label: "当前值" },
    { key: "quality", label: "质量" }
  ]
});

// JSON Tag 内容变化时自动刷新表格
const stopTable = api.bindJsonTable("#event-table", "<device-code>.<event-rows-tag-code>", {
  columns: ["time", "type", "message"]
});

13. 多语言

自写页可以跟随 HQControl 当前语言切换。api.translateapi.tapi.language.translate 的根别名。切换语言会影响整个运行时,不应把 setLocale 当成组件局部状态。

  • language.current() / getLocale():同步返回当前语言码。
  • language.translate(key, fallback) / t(key, fallback):同步返回翻译;缺少 key 时返回 fallback,未传 fallback 时返回 key。
  • language.onChange(callback) / subscribe(callback):回调为 (locale, translations),返回幂等取消函数。
  • language.setLocale(locale):返回 Promise<string>,解析值为实际语言码,并通知订阅者。
function renderTitle() {
  document.getElementById("title").textContent =
    api.t("shiftReport.title", "生产班次报表");
}

renderTitle();
const stopLanguage = api.language.onChange((locale, translations) => renderTitle());

使用 Mock Host 做本地页面开发

随版本提供的 runtime-page-sdk-mock-host-v1.js 可在没有 HQControl 的浏览器中提供开发数据。先创建 Mock Host,再加载 hq-common.jshq-custom-page.js。Mock 只帮助验证页面渲染、订阅和异常态,不模拟真实安全、权限、审计、写入确认、历史库或 PLC。

  • createHQVisuMockHost(options) 返回一个开发宿主对象。
  • options.valuesmetauseralarmslocale 设置初始状态。
  • queryHistoryqueryAlarmHistory 可提供自定义查询结果。
  • host.__mock.setTag(name, value, meta) 推送 Tag 变化;setAlarms(items) 推送报警快照。
  • 上线验收必须回到 HQControl Runtime,重新验证页面能力、普通用户权限、写入确认和真实查询限制。
<script src="./runtime-page-sdk-mock-host-v1.js"></script>
<script>
window.HQVISU_HOST = createHQVisuMockHost({
  values: { "packaging-line-01.temperature": 72.4 },
  meta: { "packaging-line-01.temperature": { quality: "good" } },
  user: {
    username: "shift-operator",
    displayName: "班次操作员",
    permissions: []
  }
});
</script>
<script src="./hq-common.js"></script>
<script src="./hq-custom-page.js"></script>

PPT 页面迁移兼容全局函数

ready() 完成后会安装一组 PPT 运行页兼容全局函数。它们用于迁移已有页面,不是新自写页的首选 API;新代码使用 api.*,便于能力检查、验证和生命周期清理。

  • 读取:ReadTagReadTagBooleanReadTagIntegerReadTagDoubleReadTagStringReadTagJsonReadTagDateTimeReadTagQualityReadTagQualityGood
  • 读取别名:GetTagValueAsBooleanGetTagValueAsIntegerGetTagValueAsDoubleGetTagValueAsStringGetTagValueAsJsonGetTagValueAsDateTimeGetTagQualityIsTagQualityGood
  • 写入:WriteTag、六个 WriteTag<Type> 方法,以及六个 SetTagValueAs<Type> 别名。
  • 用户:GetCurrentUserGetCurrentUsernameGetCurrentDisplayName
  • 语言:HQVISU_LANGUAGE_PROVIDERHQVISU_USE_LANGUAGEHQVISU_TRANSLATEHQVISU_TRANSLATIONS

14. 错误处理和交付检查

同步读取使用 fallback 兜底;写入、历史、报警和语言切换是 Promise,必须用 try/catch.catch() 处理。桥协议标准错误码为 INVALID_ARGUMENTCAPABILITY_DENIEDPERMISSION_DENIEDNOT_FOUNDTIMEOUTUNAVAILABLEINTERNAL_ERROR;展示时优先记录 error.codeerror.message

  • 用管理员和普通用户各打开一次页面,确认权限边界符合预期。
  • 故意移除一个未声明 Tag,确认页面有清晰错误或空态,不出现白屏。
  • 断开或模拟 bad/stale 质量,确认页面不会把异常值当正常值展示。
  • 验证历史查询时间范围、聚合口径、单位、小数位和图表说明。
  • 验证写入动作有确认、失败提示和审计原因。
  • 验证 outcome_uncertain / pending_verification 会进入阻断提示,且没有自动重试。
  • 组件退出时逐个调用 Tag、报警、语言和表格绑定的取消函数;页面退出时调用幂等 api.destroy()。销毁后不要复用订阅或未完成请求。
  • 打开浏览器控制台,确认没有 SDK 初始化失败、404、JSON 解析错误或未捕获 Promise。
try {
  const history = await api.history.query({
    tags: ["<device-code>.<output-tag-code>"],
    start,
    end,
    interval: "1h",
    aggregate: "sum"
  });
  renderTrend(history.items || []);
} catch (err) {
  renderEmptyState("历史查询失败:" + err.message);
}
HQControl 自写页 SDK 报表示例界面
图示 1: 自写页 SDK 报表示例

图示用于说明自写 HTML 页面如何把实时快照、历史查询、报警摘要和表格辅助组织为项目报表页;页面样式和业务计算由项目代码实现。