跳至主要内容

自写页面 SDK 接口参考

SDK 为项目 HTML 提供实时 Tag、类型化读写、历史、报警、用户、权限、语言、导航、设备弹窗及表格接口。页面布局和业务计算由项目代码负责。

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

SDK: 自写页面 SDK 接口参考

原生 HTML/Vue 接入与公共 API 参考。Tag 读写沿用 PPT 的 ReadTag*/WriteTag*,语言由系统顶栏 LANG 切换。

自写页 SDK 负责什么

SDK 为项目 HTML 提供实时 Tag、类型化读写、历史、报警、用户、权限、语言、导航、设备弹窗及表格接口。页面布局和业务计算由项目代码负责。

  • 适合:班次产量报表、能耗日报、设备健康页、质量追溯页、工单看板、客户展示页。
  • 页面形式:普通 HTML、原生 ES Module、构建后的 Vue 页面;CSS、图表和组件随工程发布。
  • 数据来源:声明范围内的 Tag、历史数据、报警数据和语言资源。
  • 责任边界:Mingcedar 提供数据和权限边界,页面展示和业务计算由项目代码负责。
  • SDK 版本:1.5.0。

完成标志 首次接入请看“SDK 快速上手”。除完整页面/组件外,片段均假设已取得 api,并已准备所需页面元素和项目渲染函数。

运行来源与权限边界

项目页面在独立 Screen Origin 的 iframe 中运行,与 Portal 管理端隔离。宿主桥校验来源、页面窗口、会话及文档身份。页面可以使用本来源的 Cookie、存储、URL 参数和普通 Web Worker,不支持 Service Worker;无法取得 Portal Cookie、管理端 DOM 或配置接口。数据和写入仍受页面声明及服务端权限控制。

页面代码本身可以做什么

自写页可使用 DOM、CSS、组件、图表库及浏览器和 iframe sandbox 允许的交互能力;SDK 管理 Mingcedar 宿主能力。

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

SDK 已自动加载,模块引入按需使用

Runtime 自动为发布的 HTML/Vue 页面加载 SDK,直接 await HQVISU_CUSTOM_PAGE.ready() 即可,无需重复添加 script。

  • 偏好 ES Module 的页面可 import { ready } from "/runtime/hqVisuDesigner/hq-custom-page.mjs";模块会复用已安装的 SDK。
  • 仅在自备 Mock 等未自动加载的环境,才需要按 hq-common.js → hq-custom-page.js 手动加载;加载文件本身不会创建正式运行权限。
  • 静态 Tag 声明仍需放在入口 HTML;无需安装 SDK npm 包。Vue/Vite 开发服务器和本地 file:// 不等于正式运行环境。
JavaScriptJavaScript
HQVISU_CUSTOM_PAGE.ready().then(async (api) => {
  // Use the examples below here. Reuse this api object.
}).catch(error => console.error(error.message));

点位声明写法

服务端解析 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] 只用于读取,例如 ReadTagBoolean(name, null)。写位用 WriteTagBit("<device-code>.<status-word-tag-code>", 3, "set"),父 Tag 同时声明可读、可写;不能把 [3] 传给任何写入方法。历史也查询父 Tag。
  • 手册中的 <device-code>、<tag-code> 是占位符;发布前必须替换为项目实际编码,不能原样填写。
  • ready({ tags }) 只请求跟踪,不能扩大服务端可写、历史、报警或 Popup 范围;把需要操作且需要读回的 Tag 同时放进 readableTags 和 writableTags。
HTMLHTML
<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>",
    "<device-code>.<setpoint-tag-code>"
  ],
  "writableTags": ["<device-code>.<setpoint-tag-code>"]
}
</script>

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

运行时通过参数、循环或函数生成完整 Tag 时,可声明 exact-on-use,逐个请求精确 Tag。它不支持通配、目录枚举或无限权限;每个 Tag 仍校验格式、存在性、启用状态、页面会话及读写规则。

  • liveTagRead.maxActiveTags 范围为 1–8000;示例按小页面配置 64 个读点、16 个写点。按实际需要设置,额度不代表性能承诺。
  • liveTagWrite.maxActiveTags 的正式上限为 2000。
  • 适合:从页面参数选择设备、遍历已知设备清单、函数拼接完整 Tag、组件按业务对象绑定 Tag。
  • 不支持:按 <device-code>.* 搜索、读取 Tag 目录、枚举所有设备或绕过服务端逐项校验。
  • 动态声明只扩展实时精确读写;历史、报警、设备弹窗和预取仍依据各自范围。
  • 固定点位建议使用静态声明,便于首屏预取、初始订阅和范围管理。
HTMLHTML
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<run-state-tag-code>"],
  "dependencies": {
    "runtimeAccess": {
      "liveTagRead": { "mode": "exact-on-use", "maxActiveTags": 64 },
      "liveTagWrite": { "mode": "exact-on-use", "maxActiveTags": 16 }
    }
  }
}
</script>

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

ready() 与资源生命周期

HQVISU_CUSTOM_PAGE.ready(options?)、ready(callback) 或 ESM ready(options?) 返回 Promise<PageApi>。初始化成功表示桥已建立,不保证每个 Tag 已有 GOOD 数据。每个 HTML 文档共享一个 SDK;Vue 组件重复挂载应复用它。

  • tags: string[] 追加跟踪点;普通页面优先静态声明。初始化错误用 try/catch 显示,不循环重试 ready。
  • subscribe、alarms.subscribe、language.onChange、bindJsonTable 都返回取消函数。组件卸载时逐项调用自己的取消函数。
  • destroy() 永久结束整个文档,后续同文档 ready() 返回 RUNTIME_PAGE_DESTROYED。普通 Vue unmount 不调用 destroy;整页永久结束时才调用。
  • pagehide 的 persisted=true 表示 BFCache 暂存,不永久销毁;异步挂载期间已卸载的组件不得再注册订阅。
  • timeoutMs 设置等待宿主的超时(示例 10000 ms),不是 PLC 读写超时;ready 成功后仍需检查各 Tag 的质量。
JavaScriptJavaScript
const api = await HQVISU_CUSTOM_PAGE.ready();
const stop = api.subscribe(["<device-code>.<tag-code>"], renderValue);
// renderValue is your UI renderer.
window.addEventListener("pagehide", event => {
  if (!event.persisted) { stop(); api.destroy(); }
});

SDK 全局对象 API

全局对象为 HQVISU_CUSTOM_PAGE,兼容别名为 HQVisuCustomPage。以下成员可在 ready() 前访问;页面数据仍须等待宿主就绪。

  • version:返回字符串 1.5.0。
  • ready(options | callback):返回 Promise<PageApi>。
  • whenVisualReady({ timeoutMs? }):等待宿主允许的首个画面快照就绪,返回 Promise<void>;一般页面使用 ready() 获取 api 即可。
  • destroy():永久结束当前文档的 SDK 连接;组件卸载时只取消自身订阅。
  • normalizeTagName(value):转成字符串、去首尾空白,并去掉包住整个值的一层单引号或双引号;返回字符串。
  • tagList(value):接受字符串、嵌套数组或单值,按换行/逗号/分号拆分,规范化、过滤空值和形如 <TagName> 的旧模板变量并去重;返回 string[]。
  • renderTable(target, rows, options):渲染轻量 HTML 表格;返回 { rowCount, columnCount }。
JavaScriptJavaScript
const tag = "<device-code>.<tag-code>";
const normalized = HQVISU_CUSTOM_PAGE.normalizeTagName(tag);
const tags = HQVISU_CUSTOM_PAGE.tagList([tag, tag]);

PageApi 根对象和能力矩阵

使用 ready() 返回的根方法及 api.auth、api.permissions、api.history、api.alarms 等稳定接口。低层 api.host 的接口随传输方式变化。

  • 属性: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() 只说明当前传输暴露此能力,不代表用户有权限,也不会扩大页面范围。
  • log.write 是可选能力:supports("log.write") 为 false 时不能调用或宣称日志写入成功。PageApi 没有独立的 log.write 根接口。

当前用户和权限 API

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

  • api.auth.getSession():返回会话快照;为兼容同步和异步宿主,统一写成 await api.auth.getSession()。
  • api.auth.getCurrentUser() / getUser() → 用户快照或 null;getUsername() → 用户名字符串;getDisplayName() → 显示名,无显示名时回退用户名。
  • api.auth.hasPermission(code) / api.permissions.has(code):返回 Promise<boolean>。
  • api.auth.requirePermission(code) / api.permissions.require(code):有权限时解析为 true,无权限时 Promise 拒绝。
  • 上述方法由 PageApi.auth 提供。优先使用此公共封装,不直接依赖 api.host 中随传输变化的低层对象。
JavaScriptJavaScript
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);

读取接口:与 PPT 使用同一套名称

读取同步返回当前已采集快照,不直接读取 PLC。类型化读取显式传 fallback 时,非 GOOD/缺质量或转换失败返回该 fallback;省略 fallback 不能代替质量检查。ReadTag 返回原始值,不做类型转换或质量过滤。需要区分真实 0 与不可用时,用 ReadTagDouble(name, null) 并同时显示质量。

  • ReadTag(name, fallback?) → 原始快照值;不进行类型转换。
  • ReadTagBoolean(name, fallback?) → boolean;识别布尔、有限数值、true/false、1/0、on/off、yes/no;默认 false。
  • ReadTagInteger(name, fallback?) → 向零截断的安全整数;默认 0。超出安全整数范围用原始值或字符串,不用 Number 强转。
  • ReadTagDouble(name, fallback?) → 有限 number;默认 0。
  • ReadTagString(name, fallback?) → 显示文本,displayValue 优先,数值遵循 precision/round;默认空字符串。GOOD 空字符串是有效值。
  • ReadTagJson(name, fallback?) → 解析后的 JSON 值;默认 null,返回值不一定是对象,也可能为数组或标量。
  • ReadTagDateTime(name, fallback?) → 有效 Date 或 fallback;默认 null。PLC 日历型的 UTC 字段规则见日期章节。
  • ReadCalculatedTag(name, fallback?) → 计算 Tag 的 number/boolean,校验计算来源和 GOOD;默认 null。使用计算 Tag 稳定编码并声明页面读范围。
  • ReadTagQuality(name, fallback?) → "good" | "bad"; ReadTagQualityGood(name) → boolean. 无质量时默认为 bad,不传 good 来掩盖缺失质量。
  • getMeta(name) → 元数据对象;dataType、qualityDetail、timestamp、source 等字段按实际快照提供。stale/pending 等诊断细节不成为 ReadTagQuality 的第三种返回值。
JavaScriptJavaScript
const name = "<device-code>.<tag-code>";
const raw = api.ReadTag(name);
const quality = api.ReadTagQuality(name); // "good" | "bad"
const usable = api.ReadTagQualityGood(name);
const value = api.ReadTagDouble(name, null);
document.getElementById("value").textContent = usable && value !== null ? String(value) : "—";
const meta = api.getMeta(name); // source, dataType, ts/persistenceVersion when supplied
// Choose the reader matching each Tag's type; do not cast one Tag to every type.
const running = api.ReadTagBoolean("<device-code>.<run-state-tag-code>", null);
const count = api.ReadTagInteger("<device-code>.<count-tag-code>", null);
const label = api.ReadTagString("<device-code>.<text-tag-code>", "—");
const rows = api.ReadTagJson("<device-code>.<json-tag-code>", null);
const date = api.ReadTagDateTime("<device-code>.<datetime-tag-code>", null);
const calculated = api.ReadCalculatedTag("<calculated-tag-code>", null);

订阅实时变化

track(tags) 规范化并跟踪 Tag,返回去重后的名称数组。subscribe(tags, callback) 返回幂等取消函数,回调为 (items, api)。先读取快照渲染首屏,再订阅变化。

  • 每个变化项包含 tagName 和 value;fullCode、quality、timestamp、source 等元信息按实际数据提供。
  • 每页最多保持 8000 个活动唯一 Tag;无效、缺失、停用或拒绝的候选不会永久占用配额。
  • 线协议每批最多 256 个 Tag,SDK 最多四批并发;一个大页面仍只有一个逻辑实时订阅和一个 WebSocket。
  • 回调中可用第二个参数重新读取一致的当前快照;高频页面应集中订阅一组 Tag 后统一刷新 UI。
  • 离开页面、切换数据范围或销毁组件时调用取消函数;destroy() 会集中清理剩余订阅。
JavaScriptJavaScript
const tag = "<device-code>.<process-tag-code>";
function renderValue() {
  const value = api.ReadTagDouble(tag, null);
  document.getElementById("value").textContent =
    api.ReadTagQualityGood(tag) && value !== null ? String(value) : "—";
}
renderValue();
const stopRealtime = api.subscribe([tag], renderValue);
// stopRealtime() when this view no longer needs the subscription.

写入接口:参数、前置条件与返回值

所有类型写入采用 (name, value, options?),统一用 await 接收结果:Host 可以同步返回、返回 Promise 或 undefined,不能直接假定存在 .then()。页面需具备 tag.write 能力、静态/合法动态写范围、账号 tag:write 权限、Tag 控制许可及有效质量;正式执行还会重新校验 Owner 和用户动作。

  • WriteTagBoolean(name, value, options?):使用 boolean、0/1 或明确的布尔文本;"false" 表示 false,不用 JavaScript 真值规则。
  • WriteTagInteger(name, value, options?):number 必须是安全整数;BigInt 或十进制整数文本保留精度,设备实际范围由后端校验。
  • WriteTagDouble(name, value, options?):有限数值或合法数字文本;拒绝 NaN、Infinity 和空输入。
  • WriteTagString(name, value, options?):文本;空字符串是明确的写入值,仍需满足 PLC 编码与长度限制。
  • WriteTagJson(name, value, options?):传实际 JSON 对象/数组/值,不先 JSON.stringify 对象;恢复原始空文本应使用 WriteTagString。
  • WriteTagDateTime(name, value, options?):Date 或日期文本;PLC 日历元数据决定序列化规则,见下方日期章节。
  • WriteTag(name, value, options?) / WriteTag({ TagName, Value, ValueType, ... }):通用入口,也接受 camelCase payload;false、0、空字符串和 JSON 值不会被丢弃。
  • WriteTagBit(name, bitIndex, bitAction, options?):name 为父 Tag,需 GOOD 质量及支持的 8/16/32 位整数类型;位号须在该类型位宽内且为 0–31,动作为 set/reset/toggle。空白位号非法。协议可能只支持部分动作;Modbus 需明确 FC22 支持,不支持 toggle,不用整字读改写代替。
  • options.reason 提供操作原因;typed 方法固定本次类型和调用来源。权限预检 await api.auth.hasPermission("tag:write"),不要把 capability 名 tag.write 当权限码。
  • 从用户点击处理函数发起;页面确认不替代服务配置的 Host 确认。禁止在定时器、加载或订阅中自动写入。
在 ready 回调内绑定写入按钮(id="write",结果 id="result")JavaScript
const button = document.getElementById("write");
const output = document.getElementById("result");
button.disabled = !(api.supports("tag.write") && await api.auth.hasPermission("tag:write"));
button.onclick = async () => {
  if (button.disabled) return;
  const tag = "<device-code>.<tag-code>";
  if (!api.ReadTagQualityGood(tag)) { output.textContent = "Tag unavailable"; return; }
  if (!confirm("Write 12.5?")) return;
  button.disabled = true;
  try {
    const result = await api.WriteTagDouble(tag, 12.5, { reason: "Operator setpoint update" });
    const done = result?.accepted === true && result?.status === "done"
      && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true
      && result?.transportOnly !== true;
    output.textContent = (done ? "Command completed; verify collected value: " : "Result unconfirmed; verify before another write: ")
      + JSON.stringify(result ?? null);
  } catch (error) {
    output.textContent = "Result unconfirmed: " + error.message + " " + (error.commandId || error.result?.commandId || "");
  }
  // Reconcile the result before another write. Never retry automatically.
};

判断命令结果,再核对正常采集读回

accepted=true 只表示受理。status、commandId、journalId 和不确定性字段共同描述命令;currentValue 可能是 runtime-write 即时回显,不能当成后续正常采集证据。代码中 nextValue、reason 由页面已验证的输入提供。

  • done:命令完成,仍核对正常采集来源、较新时间/版本及质量后再认定设备值到位;不要把目标值直接覆盖实时显示。
  • proxied:可能是转发结果,继续检查实际完成证据;sent 或 transportOnly=true 仅证明传输,不能显示“PLC 已执行”。
  • readback_failed/readback_mismatch/latest_superseded:显示待核对,不把命令回显作为正确采集值。
  • pending_verification/outcome_uncertain、超时、断线、undefined 或未知状态:保持操作阻断,记录 commandId,核对命令记录与实际值,不自动重试。
  • accepted=false、cancelled、failed、page_changed:未得到成功完成证据,分别显示真实结果。
JavaScriptJavaScript
// Call from the existing async click handler, after input/quality/permission checks.
try {
  const result = await api.WriteTagDouble("<device-code>.<setpoint-tag-code>", nextValue, { reason });
  const confirmed = result?.accepted === true && result?.status === "done"
    && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true
    && result?.transportOnly !== true;
  document.getElementById("write-result").textContent =
    (confirmed ? "Command completed; verify normal collected state: " : "Verify before another write: ")
    + JSON.stringify(result ?? null);
  // Do not overwrite the live value with nextValue or result.currentValue.
  // Reconcile the command record and a later GOOD collected sample before another operation.
} catch (error) {
  document.getElementById("write-result").textContent =
    "Result unconfirmed; do not retry automatically. " + error.message;
}

完成标志 PageApi 暂无公共命令记录查询方法。保留回执或错误中的 commandId,由具备权限的管理端/运维集成核对命令记录,并通过 subscribe 观察后续 GOOD 正常采集值和 getMeta 元信息。未确认时保持禁用;核对完成后才由页面允许新的人工操作,不能仅按固定延时、刷新页面或值相等自动解锁。

查询历史和趋势

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 适合电表、水表、产量累积量等报表口径。
  • 下例查询最近 7 天;目标 Tag 须已启用历史记录,页面才可能返回数据。
  • points 省略或为 0 时默认 20000,最大 20000,且不能小于去重后的 Tag 数。
JavaScriptJavaScript
const weekEnd = new Date();
const weekStart = new Date(weekEnd.getTime() - 7 * 24 * 60 * 60 * 1000);

const weekEnergy = await api.history.query({
  tags: [
    "<primary-device-code>.<energy-tag-code>",
    "<secondary-device-code>.<energy-tag-code>"
  ],
  start: weekStart.toISOString(),
  end: weekEnd.toISOString(),
  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 表示聚合间隔被放粗,或原始数据因点数预算而截断。显示实际间隔和结果不完整提示;需要完整原始记录时缩短时间范围后分段查询。
  • 读取序列时使用 series.points || [] 处理空数据;页面显示空态,不把无记录当成数值 0。
JavaScriptJavaScript
for (const series of weekEnergy.items || []) {
  const chartPoints = (series.points || []).map((point) => ({
    time: new Date(point.t),
    value: point.quality === "good" ? point.value : null,
    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 字段查看更多报警。
  • 组编码需带设备前缀;空范围表示未绑定,不是全部报警。showWindow 控制页面右侧报警栏,与设备操作 Popup 是两项功能。
HTMLHTML
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<temperature-tag-code>"],
  "alarmScope": {
    "plcCodes": ["<device-code>"],
    "functionGroupCodes": ["<device-code>.<function-group-code>"],
    "deviceGroupCodes": [],
    "signalGroupCodes": []
  }
}
</script>

查询和订阅报警

报警接口提供同步快照、异步查询和变化订阅。getActive()、getActiveAlarms() 和 queryHistory(request) 均返回 Promise<AlarmQueryResult> 分页结果。

  • getSnapshot() 同步返回宿主已有快照;不同宿主可能返回数组,也可能返回 { alarms } 或 { activeAlarms } 包装对象。
  • 查询字段:start、end、severity、state、source / q、limit、offset、cursorTs、cursorId。
  • 历史查询省略时间时默认最近 24 小时;单次最大跨度 24 小时。
  • 下例查询最近 1 小时;该时段无报警时历史结果为空。
  • limit 省略或为 0 时是 500,最大也是 500;超过 500 会被截到 500。offset 必须非负。
  • 结果为 { items, count, hasMore, nextCursorTs, nextCursorId };有下一页时优先把两个 nextCursor 字段传回下一次查询。
  • subscribe(callback) 返回幂等取消函数,回调为 (items, api);不同宿主可能传完整快照或变化项,应把它作为刷新信号,不要盲目追加。
  • getActive(request?) 接受上述过滤/分页参数;getActiveAlarms() 是无参便捷入口。
JavaScriptJavaScript
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 alarmEnd = new Date();
const alarmStart = new Date(alarmEnd.getTime() - 60 * 60 * 1000);
const historyResult = await api.alarms.queryHistory({
  start: alarmStart.toISOString(),
  end: alarmEnd.toISOString(),
  severity: ["critical", "high"],
  state: ["active", "recovered"],
  limit: 200
});

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

报警项和分页结果字段

用 id 标识事件,用 state 和时间字段显示状态。可选字段缺失时留空;缺少翻译时回退 messageText/displayName,不显示内部 key。

  • 身份: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:上级、下级、assets 根与 URL

自写页 api.navigation.gotoPage 与 PPT hqVisuDesigner.openPage 共用解析规则。无须预先枚举所有目标。页面映射用于名称与面包屑;明确路径不会被同名映射截走。

  • gotoPage(target, options?):./、../、子级、同级路径相对当前画面 URL 解析;本地无扩展名补 .html。解析后在 assets 根内即可跨页面目录。
  • assets/<folder>/... 与 /assets/<folder>/... 指向当前 Runtime Unit 的用户画面根;发布画面或内置画面也可进入这个明确根目标。它们的其它本地相对路径仍保留原包边界。
  • openUrl(target, options?):同一目标规则;外站明确写 http://、https:// 或 //,不把带点的目录名猜成域名。query/hash 保留。
  • gotoAndon(target, options?):请求宿主安灯/全屏窗口。
  • gotoScreen(screenCode):按画面菜单编码跳转,不是任意文件路径。
  • options:openInNewWindow、frameTarget、parameters:[{key,value}]。参数最多64个,key≤256字符、value≤2048字符;目标用 URLSearchParams 读取。
  • 合法 HTTP(S) 地址允许导航;第三方是否允许 iframe 嵌入由其 CSP/X-Frame-Options 决定。Portal 管理路由用顶层/新窗口,不作为 Screen 嵌入。外站不获得 Tag 或 Portal 身份。
  • 越出 assets 根、javascript:/data:/file:、反斜杠和歧义编码路径拒绝。每个目标 HTML 必须有自己的静态数据范围。返回 true 是分派结果,必须看到目标页面才能判断加载成功。
JavaScriptJavaScript
// Inside the ready() scope; the HTML contains the corresponding buttons.
document.getElementById("parent").onclick = () =>
  api.navigation.gotoPage("../overview.html?view=summary#status");
document.getElementById("child").onclick = () =>
  api.navigation.gotoPage("details/equipment", { parameters: [{ key: "device", value: "<device-code>" }] });
document.getElementById("other-package").onclick = () =>
  api.navigation.gotoPage("/assets/<page-folder>/overview.html");
document.getElementById("external").onclick = () =>
  api.navigation.openUrl("https://<server-host>/manual", { openInNewWindow: true });
// In the destination page:
const device = new URLSearchParams(location.search).get("device");

设备弹窗 API

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

  • plc 或 deviceCode 必填,且该 PLC 至少有一个 Tag 位于页面可读范围。
  • focusTagNames 可选,最多 64 个;每个 Tag 都必须在页面可读范围。
  • templateCode / id 可指定弹窗模板。
  • diagnosisControl、groupRightIndex 是可选弹窗上下文字段。
  • 设备弹窗不能扩大页面读写范围,也不等于页面获得 Popup 内部服务对象。
  • 先在“弹窗模板”配置正确的绑定 Tag、控制项、报警及趋势区域;弹窗中的真实写入仍使用账号权限、Tag 控制标志、质量与 Host 确认。
JavaScriptJavaScript
// Add <button id="open-popup">Open equipment</button>
// and <button id="close-popup">Close</button> to the HTML.
// Place this code inside the existing ready() scope.
document.getElementById("open-popup").disabled = !api.supports("popup");
document.getElementById("open-popup").onclick = () => {
  try {
    const dispatched = api.dialog.openDevicePopup({
      plc: "<device-code>",
      templateCode: "<popup-template-code>",
      focusTagNames: ["<device-code>.<setpoint-tag-code>"]
    });
    if (dispatched !== true) throw new Error("Popup request was not dispatched");
    // Verify the actual drawer and target Tag; true is not a load-complete receipt.
  } catch (error) { document.getElementById("status").textContent = error.message; }
};
document.getElementById("close-popup").onclick = () => api.dialog.close();

完整监控页面(按需查看)

完整 HTML 包含实时值、单次设定值写入、活动报警和历史趋势。发布前替换 Tag 及声明,按设备要求调整输入范围。Runtime 自动提供 SDK。

展开完整源代码
HTMLHTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment monitor</title>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<process-tag-code>","<device-code>.<setpoint-tag-code>"],
   "writableTags":["<device-code>.<setpoint-tag-code>"]}
  </script>
  <script>
    window.HQVISU_PAGE_META = { layoutMode: "native" };
    window.HQVISU_PAGE_NAME = "equipment-monitor";
    window.HQVISU_PAGE_ALARM = {
      showWindow: true, tags: ["<device-code>.<process-tag-code>"],
      plcCodes: [], functionGroupCodes: [], deviceGroupCodes: [], signalGroupCodes: []
    };
  </script>
  <style>
    body { font: 16px/1.6 system-ui; margin: 24px; color: #16263c; }
    section { margin-block: 24px; } button, input { font: inherit; padding: 8px; }
    table { border-collapse: collapse; width: 100%; }
    th, td { text-align: left; padding: 8px; border-bottom: 1px solid #d9e1eb; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <h1>Equipment monitor</h1>
  <p id="status" role="status">Connecting to Mingcedar…</p>
  <section><h2>Live value</h2><output id="value">Waiting for data</output></section>
  <section><h2>Setpoint</h2>
    <label>Target (0–100) <input id="setpoint" type="number" min="0" max="100" step="0.1"></label>
    <button id="write" disabled>Write setpoint</button><pre id="write-result"></pre>
  </section>
  <section><h2>Active alarms</h2><button id="alarms" disabled>Refresh alarms</button><div id="alarm-list"></div><p id="alarm-status"></p></section>
  <section><h2>Last hour</h2><button id="history" disabled>Query historical trend</button><div id="trend"></div><p id="trend-status"></p></section>
  <script>
  (async function () {
    const element = (id) => document.getElementById(id);
    const processTag = "<device-code>.<process-tag-code>";
    const setpointTag = "<device-code>.<setpoint-tag-code>";
    const api = await HQVISU_CUSTOM_PAGE.ready({timeoutMs: 10000});
    let stopValues = () => {};
    let stopAlarms = () => {};
    element("status").textContent = "Connected";
    window.addEventListener("pagehide", event => {
      if (!event.persisted) { stopValues(); stopAlarms(); api.destroy(); }
    });
        function refreshValue() {
      const quality = api.ReadTagQuality(processTag);
      const value = api.ReadTagDouble(processTag, null);
      element("value").textContent = quality === "good" && value !== null
        ? String(value) : "Unavailable — quality: " + quality;
    }
    stopValues = api.subscribe([processTag, setpointTag], refreshValue);
    refreshValue();
    element("status").textContent = "Connected. Reads remain subject to data quality.";
        element("write").disabled = !(api.supports("tag.write") && await api.permissions.has("tag:write"));
    element("write").onclick = async () => {
      const input = element("setpoint").value.trim();
      const value = Number(input);
      if (!input || !Number.isFinite(value) || value < 0 || value > 100) {
        element("write-result").textContent = "Enter a number between 0 and 100."; return;
      }
      if (!api.ReadTagQualityGood(setpointTag)) {
        element("write-result").textContent = "Setpoint quality is not good."; return;
      }
      if (!confirm("Write setpoint " + value + "?")) return;
      element("write").disabled = true;
      try {
        const result = await api.WriteTagDouble(setpointTag, value, {reason: "Operator setpoint update"});
        const done = result?.accepted === true && result?.status === "done" && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true && result?.transportOnly !== true;
        element("write-result").textContent = (done ? "Command completed; verify normal collected state: " : "Verify before another write: ") + JSON.stringify(result ?? null);
      } catch (error) {
        element("write-result").textContent = "Result not confirmed. Check actual value and command records. " + error.message;
      }
      // Keep the button disabled after submission; refresh only after result reconciliation.
    };
        let alarmPending = false;
    async function refreshAlarms() {
      if (alarmPending) return;
      alarmPending = true;
      try {
        const result = await api.alarms.getActive({limit: 100, offset: 0});
        api.renderTable(element("alarm-list"), result.items, {columns: [
          {key:"fullCode",label:"Tag"}, {key:"messageText",label:"Message"},
          {key:"severity",label:"Severity"}, {key:"state",label:"State"}
        ]});
        element("alarm-status").textContent = result.hasMore ? "Showing first 100 alarms; narrow the filter or request the next page." : "Current active alarms: " + result.count;
      } catch (error) { element("alarm-status").textContent = error.message; }
      finally { alarmPending = false; }
    }
    element("alarms").disabled = !api.supports("alarm.read");
    element("alarms").onclick = refreshAlarms;
    stopAlarms = api.supports("alarm.read") ? api.alarms.subscribe(refreshAlarms) : () => {};
    if (api.supports("alarm.read")) await refreshAlarms();
        element("history").disabled = !api.supports("history.read");
    element("history").onclick = async () => {
      element("history").disabled = true;
      try {
        const end = new Date();
        const result = await api.history.query({tags:[processTag],
          start:new Date(end.getTime()-3600000).toISOString(), end:end.toISOString(),
          interval:"1m", aggregate:"avg", points:1000});
        const rows = (result.items || []).flatMap(series => (series.points || []).map(point => ({
          tag:series.tag, time:point.t, value:point.quality === "good" ? point.value : null, quality:point.quality
        })));
        api.renderTable(element("trend"), rows);
        element("trend-status").textContent = rows.length
          ? "Interval: " + result.effectiveInterval + (result.truncated ? " (adjusted to point budget)" : "")
          : "No archived values in this time range.";
      } catch (error) { element("trend-status").textContent = error.message; }
      finally { element("history").disabled = false; }
    };
  })().catch(error => { document.getElementById("status").textContent = error.message; });
  </script>
</body>
</html>

renderTable 与 bindJsonTable

api.renderTable 与全局 HQVISU_CUSTOM_PAGE.renderTable 将数组、{ rows } 或 { items } 渲染为 HTML 表格,返回 { 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 };空值或解析失败时渲染空表。它直接显示原始快照,不按 Tag 质量过滤;需要区分失效数据时,使用 subscribe,在回调中检查 ReadTagQualityGood 后再调用 renderTable。
  • 复杂排序、分页、冻结列和导出 Excel 应由项目页面或第三方表格组件实现。
JavaScriptJavaScript
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"]
});

多语言:跟随系统顶栏 LANG

统一使用系统顶栏 LANG,不另设语言按钮。先按当前语言渲染,再订阅变化;只翻译显示文案,保留接口数据、Tag 原值和命令状态码。

  • language.current() / getLocale() → string。
  • language.translate(key, fallback?) / t(key, fallback?);根 api.translate/api.t 同义。缺少 key 返回 fallback,未给 fallback 返回 key。业务翻译 key 在系统多语言配置中维护。
  • language.onChange((locale, translations) => ...) / subscribe(...) → 取消函数,不主动发送初值。
  • language.setLocale(locale) → string | Promise<string>;影响系统语言,不能当组件局部状态。页面内的语言设置应与系统顶栏保持一致。
  • 面包屑名称来自静态页面导航声明;公开 PageApi 没有动态改名接口,不重复握手来刷新。
JavaScriptJavaScript
// The system top-bar LANG is the language selector. Do not add a second one.
function renderText() {
  const locale = api.language.current();
  document.documentElement.lang = locale;
  const fallback = locale.startsWith("zh") ? "设备总览" : "Equipment overview";
  document.getElementById("title").textContent = api.t("equipment.overview.title", fallback);
}
renderText(); // onChange does not supply an initial callback.
const stopLanguage = api.language.onChange(renderText);
// Call stopLanguage() in this page/component's existing cleanup handler.

使用 Mock Host 做本地页面开发

随版本提供的 runtime-page-sdk-mock-host-v1.js 可在没有 Mingcedar 的浏览器中提供开发数据。先创建 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) 推送报警快照。
  • 上线验收必须回到 Mingcedar Runtime,重新验证页面能力、普通用户权限、写入确认和真实查询限制。
  • 本地 Mock 使用配套开发文件;正式部署从 Runtime 提供的 SDK URL 加载,不要将本地开发用的 hq-common.js 当作独立 npm 包部署。
HTMLHTML
<script src="./runtime-page-sdk-mock-host-v1.js"></script>
<script>
window.HQVISU_HOST = createHQVisuMockHost({
  values: { "<device-code>.<process-tag-code>": 72.4 },
  meta: { "<device-code>.<process-tag-code>": { quality: "good" } },
  user: {
    username: "<operator-name>",
    displayName: "班次操作员",
    permissions: []
  }
});
</script>
<script src="./hq-common.js"></script>
<script src="./hq-custom-page.js"></script>

PPT 与自写页:只使用同名公共读写接口

自写页调用 api.ReadTagInteger / api.WriteTagInteger,PPT 调用 ReadTagInteger / WriteTagInteger;大小写、解析、质量 fallback、日期和结果判断一致。升级时替换旧名称。

  • 读取:ReadTag、ReadCalculatedTag、ReadTagBoolean/Integer/Double/String/Json/DateTime、ReadTagQuality、ReadTagQualityGood。
  • 写入:WriteTag、WriteTagBoolean/Integer/Double/String/Json/DateTime、WriteTagBit。
  • 读取同步返回当前快照;写入使用 await 并检查实际结果。封装业务方法时,应保留质量检查、权限校验和命令结果处理。
JavaScriptJavaScript
const api = await HQVISU_CUSTOM_PAGE.ready();
const value = api.ReadTagInteger("<device-code>.<tag-code>", null);
// PPT script reads the same name without the api prefix:
// const value = ReadTagInteger("<device-code>.<tag-code>", null);

错误处理和交付检查

读取失败使用 fallback 与质量提示;初始化、写入、历史和报警用 await/try-catch。写入可能没有返回值,undefined 不是成功。记录 error.code、error.message 和存在的 commandId,不记录权限密钥。

  • 用管理员和普通用户各打开一次页面,确认权限边界符合预期。
  • 检查一个未声明 Tag,确认页面显示拒绝或空态,不出现白屏。
  • 断开或模拟 bad/stale 质量,确认页面不会把异常值当正常值展示。
  • 验证历史查询时间范围、聚合口径、单位、小数位和图表说明。
  • 验证写入动作有确认、失败提示,并按项目要求记录操作原因。
  • 验证 outcome_uncertain / pending_verification 会进入阻断提示,且没有自动重试。
  • 组件退出时取消自身 Tag、报警、语言和表格订阅。只有整页永久离开(pagehide 且 !event.persisted)才调用 api.destroy();BFCache 暂存与 Vue 卸载不销毁整个文档。销毁后不要复用订阅或未完成请求。
  • 打开浏览器控制台,确认没有 SDK 初始化失败、404、JSON 解析错误或未捕获 Promise。
  • 常见错误:INVALID_ARGUMENT(输入)、CAPABILITY_DENIED(页面范围/能力)、PERMISSION_DENIED(账号权限)、NOT_FOUND、TIMEOUT、UNAVAILABLE、RUNTIME_WRITE_OUTCOME_UNCERTAIN、RUNTIME_PAGE_DESTROYED。分清页面未就绪、服务离线和 Tag 质量,不显示一个笼统成功提示。
JavaScriptJavaScript
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 模块:一个完整可复制页面

保存为 equipment-overview.html,替换声明与脚本中的 Tag。mjs 仅导出 ready 和默认对象 {ready};ReadTagInteger 等方法从 await ready() 获取。页面自适应窗口并跟随 LANG。

展开完整源代码
HTMLHTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment overview</title>
  <script>window.HQVISU_PAGE_META = { layoutMode: "native" };</script>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<process-tag-code>"],"writableTags":[]}
  </script>
</head>
<body>
  <h1 id="title">Equipment overview</h1>
  <output id="value">—</output><p id="status" role="status"></p>
  <script type="module">
    import { ready } from "/runtime/hqVisuDesigner/hq-custom-page.mjs";
    const tag = "<device-code>.<process-tag-code>";
    const element = id => document.getElementById(id);
    let page, stopValue, stopLanguage, disposed = false;
    window.addEventListener("pagehide", event => {
      if (event.persisted) return;
      disposed = true;
      stopValue?.(); stopLanguage?.(); page?.destroy();
    });
    try {
      page = await ready({ tags: [tag] });
      if (!disposed) {
        function render() {
          const zh = page.language.current().startsWith("zh");
          document.documentElement.lang = page.language.current();
          element("title").textContent = zh ? "设备总览" : "Equipment overview";
          const quality = page.ReadTagQuality(tag);
          const value = page.ReadTagDouble(tag, null);
          element("value").textContent = quality === "good" && value !== null ? String(value) : "—";
          element("status").textContent = (zh ? "数据质量:" : "Quality: ") + quality;
        }
        render();
        stopValue = page.subscribe([tag], render);
        stopLanguage = page.language.onChange(render);
      }
    } catch (error) {
      if (!disposed) element("status").textContent = (error.code ? error.code + ": " : "") + error.message;
    }
  </script>
</body>
</html>

Vue 接入 1:在入口 HTML 声明范围

在现有 Vue 3/Vite 项目中使用以下 index.html;Vue 本身由项目依赖打包,无需 CDN。readableTags 和 writableTags 中的编码必须与组件 prop 一致。SSR 环境只在 onMounted 后调用 ready,否则报 RUNTIME_PAGE_BROWSER_REQUIRED。

HTMLHTML
<!-- Vite project root: index.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment monitor</title>
  <script>window.HQVISU_PAGE_META = { layoutMode: "native" };</script>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<setpoint-tag-code>"],
   "writableTags":["<device-code>.<setpoint-tag-code>"]}
  </script>
</head>
<body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>

Vue 接入 2:挂载组件

保存为 src/main.js。把下面 prop、入口声明和组件绑定统一为已批准的数值设定 Tag。只读项目去掉可写声明与操作区域。

JavaScriptJavaScript
// src/main.js — use the Vue dependency installed in your project.
import { createApp } from "vue";
import EquipmentPanel from "./EquipmentPanel.vue";
createApp(EquipmentPanel, {
  tagName: "<device-code>.<setpoint-tag-code>"
}).mount("#app");

Vue 接入 3:读取、单次写入、LANG 与卸载

保存为 src/EquipmentPanel.vue。卸载时只取消组件订阅;若等待期间已卸载,停止注册。本例提交后保持按钮禁用,刷新或重新挂载会重置状态。再次写入前核对上次命令和正常采集值;需跨刷新阻断时,保存并恢复待核对状态。

展开完整源代码
VueVue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
const props = defineProps({ tagName: { type: String, required: true } });
const tag = props.tagName; // Remount with a new key when changing this Tag.
const value = ref(null), quality = ref("bad"), locale = ref("en-US");
const target = ref(""), message = ref(""), canWrite = ref(false), submitted = ref(false);
let page, stopValue, stopLanguage, disposed = false, allowed = false;
const text = (zh, en) => locale.value.startsWith("zh") ? zh : en;
function render() {
  value.value = page.ReadTagDouble(tag, null);
  quality.value = page.ReadTagQuality(tag);
  canWrite.value = allowed && quality.value === "good";
}
onMounted(async () => {
  try {
    const sdkUrl = "/runtime/hqVisuDesigner/hq-custom-page.mjs";
    const { ready } = await import(/* @vite-ignore */ sdkUrl);
    if (disposed) return;
    const api = await ready({ tags: [tag] });
    if (disposed) return;
    page = api;
    locale.value = page.language.current();
    stopLanguage = page.language.onChange(next => { if (!disposed) locale.value = next; });
    render();
    stopValue = page.subscribe([tag], render);
    allowed = page.supports("tag.write") && await page.auth.hasPermission("tag:write") === true;
    if (!disposed) render();
  } catch (error) {
    if (!disposed) message.value = error.message;
  }
});
onUnmounted(() => {
  disposed = true;
  stopValue?.(); stopLanguage?.();
  // Do not destroy the shared document SDK on component unmount.
});
async function write() {
  if (submitted.value || !canWrite.value) return;
  const input = String(target.value).trim(), next = Number(input);
  // Replace 0–100 with the approved engineering range for this Tag.
  if (!input || !Number.isFinite(next) || next < 0 || next > 100) {
    message.value = text("请输入 0–100 的数值", "Enter a number from 0 to 100"); return;
  }
  if (!page.ReadTagQualityGood(tag)) return;
  if (!window.confirm(tag + ": " + next)) return;
  submitted.value = true;
  try {
    const result = await page.WriteTagDouble(tag, next, { reason: "Operator setpoint update" });
    if (!disposed) message.value = text("命令回执(需核对正常采集值):", "Command receipt (verify normal collected value): ")
      + JSON.stringify(result ?? null);
  } catch (error) {
    if (!disposed) message.value = text("结果未确认,核对后再操作:", "Result unconfirmed; reconcile before another operation: ") + error.message;
  }
  // Keep submission blocked until the operator reconciles command and collected state.
}
</script>
<template>
  <section>
    <h1>{{ text("设备设定值", "Equipment setpoint") }}</h1>
    <p>{{ tag }}: {{ quality === "good" ? value : "—" }} · {{ quality }}</p>
    <label>{{ text("目标值(0–100)", "Target (0–100)") }} <input v-model="target" type="number" step="0.1"></label>
    <button :disabled="!canWrite || submitted" @click="write">{{ text("写入一次", "Write once") }}</button>
    <pre role="status">{{ message }}</pre>
  </section>
</template>

Vue 接入 4:构建和发布目录

在 Vite 配置中保留现有 Vue 插件,设置 base: "./",让构建产物可放进 assets 的任意子目录。页面 SDK 使用 /runtime/ 绝对服务路径,不跟随资源相对基址。

  1. 运行项目的 npm run build,检查 dist/index.html 仍含 hqvisu-custom-page 静态 JSON 声明。
  2. 将 dist 的内容按原层级上传到“画面文件”同一个目标目录,包含 index.html 与构建 assets/*.js、CSS。不能只上传 .vue 源文件或只上传 index.html。
  3. 运行菜单选这个目录的 index.html,检查 SDK 就绪、Tag GOOD、LANG 切换、组件离开再进入及真实操作结果。
JavaScriptJavaScript
// In the existing vite.config.js:
export default defineConfig({ plugins: [vue()], base: "./" });

日期与大整数:避免悄悄改变数据

DATE_AND_TIME、DTL、LDT 元数据表示 PLC 无时区日历。ReadTagDateTime 返回以 UTC 字段承载该日历的 Date,不是真实 UTC 时刻;用 getUTC* 读取字段。WriteTagDateTime(Date) 写回相同 UTC 字段、不带 Z。其它时刻型 Date 使用 ISO 时刻语义。

  • 例如 PLC 日历 08:30 在上海和其它时区都应保持 08:30;不要直接用本地 getters 或 toLocaleString 解释这个日历容器。
  • Date 仅保留毫秒;DTL/LDT 亚毫秒精度需保留原始文本。日期有效性、PLC 年份和编码范围仍由设备类型校验。
  • 超过 JavaScript 安全整数范围的数值,用 ReadTag/ReadTagString 保留文本,并用 WriteTagInteger 的十进制文本或 BigInt 参数;不要先经过 Number。
JavaScriptJavaScript
const date = api.ReadTagDateTime("<device-code>.<datetime-tag-code>", null);
if (date) {
  const fields = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(),
    date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()];
  document.getElementById("calendar").textContent = fields.join(" / ");
}

再次点击图片或按 Esc 关闭