Files
iAOP/web/mobile/mobile.js
bot_dev1 8b68d015c2 fix: 组件配色全面对齐 Ant Design 5 token + 修复 iframe 内页签初始激活(issue #131)
组件审查结论:全站组件为自绘 HTML+CSS 仿 AntD(纯静态无组件库),
pro.css 已统一设计 token,但各页 CSS/JS 残留一批旧配色硬编码,
pro.css 覆盖不到,本次全部清理:

- 旧 iOS 红 #ff3b30 → --ant-error #ff4d4f(告警 P0/严重度/DLP 条/
  工艺 alarm 态/diff 删除行)
- 旧橙 #f5a623 → --ant-warning #faad14(P1 告警/warning 态/暂存徽章)
- 旧 iOS 绿 #2ecc71 → --ant-success #52c41a(running 态/prod 徽章/
  索引完成/路由本地标签/权限矩阵)
- 杂色蓝统一 → --ant-primary #1677ff:发送键 #0ea5e9、P2 告警
  #3aa0ff、编排画布残留青 rgba(24,211,200)、旧气泡 #1d4ed8
- 主色按钮上的深色文字 #04202a → #fff(对比度修正)
- 灰 #64748b → --ant-text-tertiary rgba(0,0,0,0.45)
- 各页 CSS :root 深色默认变量值(#0b1220/#13203a/#18d3c8 等死值)
  对齐为 AntD 亮色默认值,修正"深色主题"过时注释
- 删除已无任何引用的旧深色登录页样式 auth/auth.css

顺带修复实测发现的 bug:SPA 外壳内直达 admin/studio 子页时页签
不切换——studio.js/admin.js 的 Tab 事件在异步 boot(user) 中绑定,
bindPageTabs 的 window load 初始激活可能早于绑定完成,改为带重试
的激活(直到 Tab 点亮,最多 10s)。实测 #/admin/#alerts 直达后
page-alerts 正确激活。
2026-08-06 08:20:17 +08:00

288 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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: "#ff4d4f", bg: "rgba(255,77,79,0.12)", border: "#ff4d4f" },
P1: { fg: "#faad14", bg: "rgba(250,173,20,0.12)", border: "#faad14" },
P2: { fg: "#1677ff", bg: "rgba(22,119,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);
saveBriefToHistory(brief);
})
.catch(function (e) { status.textContent = "生成失败:" + e.message; })
.finally(function () { btn.disabled = false; });
}
/* ---------------- 启动 ---------------- */
/* issue #158:历史摘要本地保存与查看(最近 10 次生成) */
var BRIEFS_KEY = "iaop.mobile.briefs.v1";
function getBriefHistory() {
try { return JSON.parse(localStorage.getItem(BRIEFS_KEY)) || []; }
catch (e) { return []; }
}
function saveBriefToHistory(p) {
var list = getBriefHistory();
list.unshift(p);
localStorage.setItem(BRIEFS_KEY, JSON.stringify(list.slice(0, 10)));
renderBriefHistory();
}
function renderBriefHistory() {
var list = getBriefHistory();
var row = document.getElementById("brief-history-row");
var sel = document.getElementById("brief-history");
if (!list.length) { row.hidden = true; return; }
row.hidden = false;
sel.innerHTML = "";
var ph = document.createElement("option");
ph.value = "-1"; ph.textContent = "查看历史摘要(" + list.length + " 条)…";
sel.appendChild(ph);
list.forEach(function (p, i) {
var opt = document.createElement("option");
opt.value = String(i);
opt.textContent = p.shift + " · " + (p.generatedAt || "").slice(0, 16).replace("T", " ");
sel.appendChild(opt);
});
sel.onchange = function () {
var idx = parseInt(sel.value, 10);
if (idx >= 0 && list[idx]) {
brief = list[idx];
renderBrief(brief);
document.getElementById("gen-status").textContent = "(查看历史摘要)";
document.getElementById("push-status").textContent = "";
}
};
}
(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();
renderBriefHistory(); // issue #158:启动时恢复历史摘要入口
})();