/* iAOP 移动端只读驾驶舱 + 交接班摘要(issue #136 / PRD 2.2 场景C、7.5、9)。 * * 纯静态(无构建链),iOS14+/Android 10+(PRD 9:viewport-fit、env(safe-area)、 * 无 ES2022+ 语法);只读(无写操作入口,200 并发只读用户基线 = 纯静态资源 * 天然可水平扩展,PRD 9 性能)。 * * 与 Web 驾驶舱共享布局资产(不重复实现):直接 fetch 编译产物 * ../cockpit/plans/ti-cl4.json —— 布局 RenderPlan(四状态/KPI/告警位) * ../cockpit/plans/alarm_panel.ti.json—— severityColors 三级配色 * 交接班摘要卡片 props 对齐 core/shift-handover/handover.py 的 * render_handover_brief 输出(sections/events/todos/collapseThreshold/ * fontSize/requireConfirm)。 * 摘要生成直连推理服务 /v1/chat/completions,计时对照 PRD 验收线 ≤ 2 分钟。 */ "use strict"; var INFER_BASE = "http://39.101.182.167:30800"; var PLAN_URL = "../cockpit/plans/ti-cl4.json"; var ALARM_PLAN_URL = "../cockpit/plans/alarm_panel.ti.json"; var severityStyles = { P0: { fg: "#ff3b30", bg: "rgba(255,59,48,0.12)", border: "#ff3b30" }, P1: { fg: "#f5a623", bg: "rgba(245,166,35,0.12)", border: "#f5a623" }, P2: { fg: "#3aa0ff", bg: "rgba(58,160,255,0.10)" } }; var brief = null; // 当前交接班摘要 props(render_handover_brief 同构) function el(tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; } /* 与 Web 驾驶舱一致的确定性模拟数据源(接真实总线时替换) */ function seededRandom(seedStr) { var h = 1779033703 ^ seedStr.length; for (var i = 0; i < seedStr.length; i++) { h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); h ^= h >>> 16; return (h >>> 0) / 4294967296; }; } /* ---------------- 只读驾驶舱(共享布局资产渲染) ---------------- */ function renderMobileCockpit(plan) { document.getElementById("page-title").textContent = plan.title || "iAOP 移动驾驶舱"; Object.keys(plan.themeTokens || {}).forEach(function (k) { document.documentElement.style.setProperty(k, plan.themeTokens[k]); }); // 四状态工艺(process_view.stages,与 Web 端同源) var stagesBox = document.getElementById("m-stages"); stagesBox.innerHTML = ""; var pv = null; plan.widgets.forEach(function (w) { if (w.type === "process_view") pv = w; }); var states = (pv && pv.props.states) || ["running", "warning", "alarm", "offline"]; ((pv && pv.props.stages) || []).forEach(function (s, i) { var st = states[i % states.length]; var chip = el("div", "stage-chip s-" + st); chip.appendChild(el("div", "n", s.name || s.id)); if (s.device) chip.appendChild(el("div", "d", s.device)); chip.appendChild(el("div", "s", st)); stagesBox.appendChild(chip); }); // KPI 卡片(两列网格,只读) var kpiBox = document.getElementById("m-kpis"); kpiBox.innerHTML = ""; plan.widgets.forEach(function (w) { if (w.type !== "kpi_card") return; // 30s 时间桶入种子:刷新/定时重渲染时数值滚动(接真实总线后替换) var rnd = seededRandom((w.props.metric || "") + "|" + Math.floor(Date.now() / 30000)); var card = el("div", "kpi"); card.appendChild(el("div", "v", (60 + rnd() * 40).toFixed(2))); card.appendChild(el("div", "l", w.props.label || w.props.metric)); if (w.description) card.appendChild(el("div", "d", w.description)); kpiBox.appendChild(card); }); // 告警(只读,配色与 Web 端同源) var alarmBox = document.getElementById("m-alarms"); alarmBox.innerHTML = ""; var demo = [ { severity: "P0", msg: "CLF-01.TEMP 温度骤升越上限" }, { severity: "P1", msg: "RF-01.VAC 真空度波动" }, { severity: "P2", msg: "ST-01.STEAM 流量偏低" } ]; demo.forEach(function (a) { var st = severityStyles[a.severity] || {}; var item = el("div", "alarm"); item.style.color = st.fg || "inherit"; item.style.background = st.bg || "transparent"; if (st.border) item.style.borderLeftColor = st.border; item.appendChild(el("span", "sev", a.severity)); item.appendChild(el("span", null, a.msg)); alarmBox.appendChild(item); }); } /* ---------------- 交接班摘要(render_handover_brief props 同构渲染) ---------------- */ function renderBrief(p) { document.body.className = "fontsize-" + (p.fontSize || "md"); var box = document.getElementById("brief"); box.innerHTML = ""; var head = el("div", "card"); head.appendChild(el("div", "card-title", p.shift + " · 交班人 " + p.operator + " → " + p.nextShift)); head.appendChild(el("div", "hint", "生成于 " + p.generatedAt + " · Prompt " + p.promptTemplate + " v" + p.promptVersion)); box.appendChild(head); // 章节开关完全由配置驱动(showOverview 等,切换模板零改码) function section(show, title, bodyText) { if (!show || !bodyText) return; var card = el("div", "card brief-sec"); card.appendChild(el("div", "sec-title", title)); card.appendChild(el("div", "sec-body", bodyText)); box.appendChild(card); } var parts = p.summary.split(/\n(?=[一二三四五]、)/); // 按章节模板切分摘要 section(p.showOverview, "生产概况", parts[0] || p.summary); section(p.showAbnormal, "异常事项", parts[1] || ""); section(p.showSafety, "安全注意事项", parts[2] || ""); section(p.showEnergy, "能耗", parts[3] || ""); if (p.showTodos && p.todos.length) { var card = el("div", "card brief-sec"); card.appendChild(el("div", "sec-title", "待办(交下一班)")); var threshold = p.collapseThreshold || 4; var shown = p.todos.slice(0, threshold); var rest = p.todos.slice(threshold); shown.forEach(function (t) { card.appendChild(el("div", "todo", "· " + t.title + (t.priority ? "(" + t.priority + ")" : "") + (t.due ? " · " + t.due : ""))); }); if (rest.length || p.todoOverflow) { var fold = el("button", "fold-btn", "展开更多 +" + (rest.length + (p.todoOverflow || 0))); fold.onclick = function () { rest.forEach(function (t) { card.insertBefore(el("div", "todo", "· " + t.title), fold); }); fold.remove(); }; card.appendChild(fold); } box.appendChild(card); } // 关键事件(含折叠策略) if (p.events.length) { var ec = el("div", "card brief-sec"); ec.appendChild(el("div", "sec-title", "关键事件")); p.events.forEach(function (e) { var row = el("div", "event"); row.appendChild(el("span", "t", e.time)); row.appendChild(el("span", null, "[" + e.severity + "] " + e.title)); ec.appendChild(row); }); if (p.eventOverflow) ec.appendChild(el("div", "hint", "另有 " + p.eventOverflow + " 条已折叠")); box.appendChild(ec); } if (p.requireConfirm) { box.appendChild(el("div", "confirm-tag", "⚠ 本班含 P0/安全事件,摘要需值班长确认后生效")); } document.getElementById("push-card").hidden = false; } function generateBrief() { var shift = document.getElementById("shift-select").value; var btn = document.getElementById("btn-gen"); var status = document.getElementById("gen-status"); btn.disabled = true; var started = Date.now(); status.textContent = "生成中…(PRD 验收线 ≤ 2 分钟)"; fetch(INFER_BASE + "/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: "iaop-ti-cl4-v1", max_tokens: 800, messages: [ { role: "system", content: "你是交接班报告生成助手。按章节输出:一、生产概况;" + "二、异常事项;三、安全注意事项;四、能耗;五、待办。每章 1-3 条,简体中文。" }, { role: "user", content: "生成" + shift + "交接班摘要:氯化炉运行平稳," + "P1 真空度告警 1 起已确认;电耗 8200kWh;待办:更换 CLF-01 热电偶。" } ] }) }).then(function (r) { return r.json(); }) .then(function (d) { var summary = (d.choices && d.choices[0] && d.choices[0].message.content) || "(空响应)"; var cost = ((Date.now() - started) / 1000).toFixed(1); status.textContent = "生成完成,耗时 " + cost + "s" + (cost <= 120 ? "(达标 ≤2min)" : "(超出验收线)"); brief = { schema: "iAOP-cockpit-handover-brief-v1", shift: shift, operator: "张工", nextShift: "下一班", summary: summary, generatedAt: new Date().toISOString(), promptTemplate: "shift_handover", promptVersion: "1.0.1", sections: ["overview", "abnormal", "safety", "energy", "todos"], events: [ { time: "09:12", title: "真空度波动告警", severity: "P1", detail: "RF-01.VAC" }, { time: "10:40", title: "更换热电偶备件确认", severity: "P2", detail: "CLF-01" } ], todos: [ { title: "更换 CLF-01 热电偶", priority: "高", due: "下一班" }, { title: "复核真空泵运行曲线", priority: "中", due: "24h 内" } ], eventOverflow: 0, todoOverflow: 0, collapseThreshold: 4, fontSize: "md", requireConfirm: false, showOverview: true, showAbnormal: true, showSafety: true, showEnergy: true, showTodos: true }; renderBrief(brief); }) .catch(function (e) { status.textContent = "生成失败:" + e.message; }) .finally(function () { btn.disabled = false; }); } /* ---------------- 启动 ---------------- */ (function init() { document.querySelectorAll("#tabbar .tab").forEach(function (tab) { tab.onclick = function () { document.querySelectorAll("#tabbar .tab").forEach(function (t) { t.classList.remove("active"); }); document.querySelectorAll(".view").forEach(function (v) { v.classList.remove("active"); }); tab.classList.add("active"); document.getElementById("view-" + tab.dataset.view).classList.add("active"); }; }); document.getElementById("btn-gen").onclick = generateBrief; document.getElementById("btn-push").onclick = function () { // 推送下一班(PRD 场景C):真实推送通道就绪前以状态回执演示 document.getElementById("push-status").textContent = "已推送下一班(" + new Date().toLocaleTimeString("zh-CN") + ")"; }; fetch(ALARM_PLAN_URL).then(function (r) { return r.ok ? r.json() : null; }) .then(function (p) { if (p && p.severityStyles) severityStyles = p.severityStyles; }) .catch(function () {}); // issue #157:只读数据手动刷新(重新加载布局资产并重渲染 KPI/告警) function loadPlan() { fetch(PLAN_URL).then(function (r) { return r.json(); }) .then(renderMobileCockpit) .catch(function (e) { document.getElementById("m-stages").innerHTML = ""; document.getElementById("m-stages").appendChild( el("div", "hint", "布局资产加载失败:" + e.message + "(请从 web/ 根目录起服务)")); }); } document.getElementById("btn-refresh").onclick = loadPlan; loadPlan(); })();