自写页面 SDK 接口参考
SDK 为项目 HTML 提供实时 Tag、类型化读写、历史、报警、用户、权限、语言、导航、设备弹窗及表格接口。页面布局和业务计算由项目代码负责。
第一次接入?从 SDK 入门教程开始,本页用于查询接口参数与返回值。
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:// 不等于正式运行环境。
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。
<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 目录、枚举所有设备或绕过服务端逐项校验。 - 动态声明只扩展实时精确读写;历史、报警、设备弹窗和预取仍依据各自范围。
- 固定点位建议使用静态声明,便于首屏预取、初始订阅和范围管理。
<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 的质量。
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 }。
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 中随传输变化的低层对象。
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 的第三种返回值。
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()会集中清理剩余订阅。
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 确认。禁止在定时器、加载或订阅中自动写入。
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:未得到成功完成证据,分别显示真实结果。
// 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 数。
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。
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 是两项功能。
<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() 是无参便捷入口。
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 是分派结果,必须看到目标页面才能判断加载成功。
// 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 确认。
// 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。
展开完整源代码
<!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 应由项目页面或第三方表格组件实现。
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 没有动态改名接口,不重复握手来刷新。
// 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 包部署。
<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 并检查实际结果。封装业务方法时,应保留质量检查、权限校验和命令结果处理。
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 质量,不显示一个笼统成功提示。
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。
展开完整源代码
<!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。
<!-- 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。只读项目去掉可写声明与操作区域。
// 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。卸载时只取消组件订阅;若等待期间已卸载,停止注册。本例提交后保持按钮禁用,刷新或重新挂载会重置状态。再次写入前核对上次命令和正常采集值;需跨刷新阻断时,保存并恢复待核对状态。
展开完整源代码
<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/ 绝对服务路径,不跟随资源相对基址。
- 运行项目的 npm run build,检查 dist/index.html 仍含 hqvisu-custom-page 静态 JSON 声明。
- 将 dist 的内容按原层级上传到“画面文件”同一个目标目录,包含 index.html 与构建 assets/*.js、CSS。不能只上传 .vue 源文件或只上传 index.html。
- 运行菜单选这个目录的 index.html,检查 SDK 就绪、Tag GOOD、LANG 切换、组件离开再进入及真实操作结果。
// 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。
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(" / ");
}