自写页 SDK:在受控范围内开发项目页面
SDK 负责把 HQControl 的受控运行数据能力提供给项目 HTML 页面。它提供实时 Tag 快照与订阅、类型化读写、历史、报警、当前用户、权限、语言、导航、设备弹窗和表格辅助方法。SDK 不限定页面形态;项目页面可以按现场需要组织为报表、看板、诊断页、质量页、能耗页或客户专用页面。
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.js 和 hq-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 和组件逻辑,以及
alert、confirm、prompt。 - 支持
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-page、hqvisu-custom-page-config、hqvisu-page-config、hqvisu-dependencies。 - 可读字段别名:
readableTags、tags、tagNames、readTags、watchTags。 - 可写字段别名:
writableTags、writeTags;outputs中的相同声明也会被收集。 dependencies下支持tags、tagNames、readableTags、readTags、writableTags、writeTags。- 字段值可以是数组,也可以是用换行、逗号或分号分隔的字符串。
- 实时读写可声明
<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.auth、api.permissions、api.history、api.alarms 等稳定封装。
- 属性:
version、bridgeVersion、schemaRevision、冻结的capabilities、低层host。 - 发现:
getCapabilities()返回可修改副本;supports(name)返回布尔值,未知能力返回 false。 - 命名空间:
auth、permissions、language、history、alarms、navigation、dialog。 - 根方法:
track、subscribe、全部读取和写入方法、queryHistory、translate/t、renderTable、bindJsonTable、destroy。 - 能力名:
page.parameters、tag.read、tag.write、history.read、user.read、navigation、popup、alarm.read、log.write。 supports()只说明当前传输暴露此能力,不代表用户有权限,也不会扩大页面范围。
当前用户和权限 API
稳定会话结果为 { user, permissions }。登录用户的 user 包含 username、displayName、permissions;匿名直接 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是低层宿主表面,某些传输还提供getUser、getUsername、getDisplayName;业务代码不要依赖这些可选宽度。
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/readTagNumber、readDouble/readTagDouble:用 JavaScriptNumber()转换,不能得到有限数字时返回 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):返回good、bad、stale、pending或unavailable;缺失且没有 fallback 时返回pending。readTagQualityGood(tag):仅质量等于good时返回 true。getMeta(tag)返回元信息对象,常见字段有quality、timestamp和source。
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)。不要假定订阅回调一定承担首屏渲染,推荐先读最近快照,再订阅后续变化。
- 每个变化项至少有
tagName、fullCode、value,并合并quality、timestamp、source等可用元信息。 - 每页最多保持 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.valueType或options.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。failed、cancelled、page_changed或accepted: 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 必须具有页面历史读取范围。
- 请求字段:
tags、start、end、interval、aggregate,可选points。 tags必填、去重后最多 16 个;历史不支持[0]至[31]位引用,必须查询父 Tag。start、end必须是 RFC 3339 时间,最大范围 366 天。interval支持m、h、d、w单位,例如15m、1h、1d、1w,最小 1 分钟。aggregate支持avg、min、max、sum、count、first、last、delta、raw。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是服务器实际使用的间隔;点数预算不足时可能比请求间隔更粗。rangeStart、rangeEnd是实际采用的 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 配置的 alarm、alarmScope 或 pageAlarm 下声明维度;页面可读 Tag 也会自动加入报警 Tag 范围。
- 支持维度:
tags、plcCodes、functionGroupCodes、deviceGroupCodes、signalGroupCodes。 - 没有任何报警范围时,活动和历史报警查询返回空范围。
- 动态
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 }包装对象。- 查询字段:
start、end、severity、state、source/q、limit、offset、cursorTs、cursorId。 - 历史查询省略时间时默认最近 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 中每个报警项都可以直接用于列表、卡片或详情页。页面应按字段存在性兼容旧数据,不要根据显示文本反推状态。
- 身份:
id、deviceId、deviceCode、deviceName、tagId、tagCode、fullCode、displayName。 - 内容:
messageText、alarmKind、severity、state、displayValue、quality。 - 时间:
activeTs、recoverTs、closeTs、ackTs、shelvedUntil、serverTs。 - 标志:
suppressed;部分数据还可能带ackUserId、areaCode、objectIdentifier、groupCode。 count是本页实际返回数量,hasMore表示还有下一页;下一页使用nextCursorTs和nextCursorId可避免 offset 翻页期间数据变化造成重复或遗漏。- SDK 只公开报警读取和订阅;自写页不能调用未公开的确认、搁置或关闭接口。
导航 API
导航正式方法是 gotoPage、gotoAndon、gotoScreen 和 openUrl。调用前检查 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。- 选项支持
openInNewWindow、frameTarget、parameters: [{ 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 只表示请求已分派。
plc或deviceCode必填,且该 PLC 至少有一个 Tag 位于页面可读范围。focusTagNames可选,最多 64 个;每个 Tag 都必须在页面可读范围。templateCode/id可指定弹窗模板。diagnosisControl、groupRightIndex是可选弹窗上下文字段。- 设备弹窗不能扩大页面读写范围,也不等于页面获得 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.translate 和 api.t 是 api.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.js 和 hq-custom-page.js。Mock 只帮助验证页面渲染、订阅和异常态,不模拟真实安全、权限、审计、写入确认、历史库或 PLC。
createHQVisuMockHost(options)返回一个开发宿主对象。options.values、meta、user、alarms、locale设置初始状态。queryHistory、queryAlarmHistory可提供自定义查询结果。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.*,便于能力检查、验证和生命周期清理。
- 读取:
ReadTag、ReadTagBoolean、ReadTagInteger、ReadTagDouble、ReadTagString、ReadTagJson、ReadTagDateTime、ReadTagQuality、ReadTagQualityGood。 - 读取别名:
GetTagValueAsBoolean、GetTagValueAsInteger、GetTagValueAsDouble、GetTagValueAsString、GetTagValueAsJson、GetTagValueAsDateTime、GetTagQuality、IsTagQualityGood。 - 写入:
WriteTag、六个WriteTag<Type>方法,以及六个SetTagValueAs<Type>别名。 - 用户:
GetCurrentUser、GetCurrentUsername、GetCurrentDisplayName。 - 语言:
HQVISU_LANGUAGE_PROVIDER、HQVISU_USE_LANGUAGE、HQVISU_TRANSLATE、HQVISU_TRANSLATIONS。
14. 错误处理和交付检查
同步读取使用 fallback 兜底;写入、历史、报警和语言切换是 Promise,必须用 try/catch 或 .catch() 处理。桥协议标准错误码为 INVALID_ARGUMENT、CAPABILITY_DENIED、PERMISSION_DENIED、NOT_FOUND、TIMEOUT、UNAVAILABLE、INTERNAL_ERROR;展示时优先记录 error.code 和 error.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);
}
图示用于说明自写 HTML 页面如何把实时快照、历史查询、报警摘要和表格辅助组织为项目报表页;页面样式和业务计算由项目代码实现。