diff --git a/web/mobile/README.md b/web/mobile/README.md
new file mode 100644
index 0000000..616781a
--- /dev/null
+++ b/web/mobile/README.md
@@ -0,0 +1,31 @@
+# web/mobile — iAOP 移动端只读驾驶舱与交接班摘要
+
+issue #136 / PRD 2.2 场景C、7.5、9。纯静态(无构建链),适配 iOS14+/Android 10+
+(viewport-fit=cover + env(safe-area-inset),无 ES2022+ 语法)。
+
+## 使用
+
+```bash
+# 从 web/ 根目录起服务(共享 ../cockpit/plans 布局资产)
+cd web && python -m http.server 8085
+# 手机或浏览器 DevTools 移动视口打开 http://127.0.0.1:8085/mobile/index.html
+```
+
+## 设计要点
+
+- **只读驾驶舱**:直接 fetch Web 驾驶舱编译产物 `../cockpit/plans/ti-cl4.json`
+ (RenderPlan)与 `alarm_panel.ti.json`(severityColors)——四状态工艺条 /
+ KPI 两列网格 / 告警列表全部从布局资产渲染,**与 Web 端共享资产、不重复实现**
+ (切换模板 = 换 plan JSON,移动端代码零改动);无写操作入口(顶栏「只读」徽标),
+ 纯静态资源天然支撑 200 并发只读用户基线(PRD 9)。
+- **交接班摘要**:按班次生成(直连推理服务 /v1/chat/completions,实时计时对照
+ PRD 验收线 ≤ 2 分钟);卡片渲染 props 与
+ `core/shift-handover/handover.py` 的 `render_handover_brief` 输出同构——
+ 章节开关(overview/abnormal/safety/energy/todos)、事件/待办折叠
+ (collapseThreshold)、字号档(fontSize sm/md/lg)、P0/安全事件强制
+ requireConfirm 提示、推送下一班回执。
+
+## 边界
+
+数据源为按点位 ID 确定性播种的模拟值(与 Web 驾驶舱一致);时序总线 /
+handover HTTP 服务就绪后替换 fetch 三处即可,页面结构不变。
diff --git a/web/mobile/index.html b/web/mobile/index.html
new file mode 100644
index 0000000..554b2c5
--- /dev/null
+++ b/web/mobile/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+iAOP 移动端驾驶舱
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/mobile/mobile.css b/web/mobile/mobile.css
new file mode 100644
index 0000000..93b05a7
--- /dev/null
+++ b/web/mobile/mobile.css
@@ -0,0 +1,87 @@
+/* iAOP 移动端样式(issue #136):竖屏卡片流,字号档 sm/md/lg 由配置驱动。
+ * 调色与 Web 驾驶舱 THEME_TOKENS dark 一致(共享 themeTokens 注入)。 */
+:root {
+ --cockpit-bg: #0b1220; --cockpit-surface: #13203a; --cockpit-fg: #e6edf6;
+ --cockpit-fg-muted: #8aa0bd; --cockpit-accent: #18d3c8; --cockpit-warn: #f5a623;
+ --cockpit-grid-line: rgba(255,255,255,0.06);
+ --fs-body: 14px;
+}
+.fontsize-sm { --fs-body: 12px; } .fontsize-lg { --fs-body: 16px; }
+* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
+body { margin: 0; font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+ background: var(--cockpit-bg); color: var(--cockpit-fg); font-size: var(--fs-body);
+ padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
+
+#topbar { display: flex; align-items: center; justify-content: space-between;
+ padding: 12px 14px; padding-top: calc(12px + env(safe-area-inset-top));
+ background: var(--cockpit-surface); border-bottom: 1px solid var(--cockpit-grid-line);
+ position: sticky; top: 0; z-index: 2; }
+#topbar h1 { font-size: 17px; margin: 0; color: var(--cockpit-accent); }
+#ro-badge { font-size: 11px; color: var(--cockpit-warn); border: 1px solid var(--cockpit-warn);
+ border-radius: 10px; padding: 2px 8px; }
+
+.view { display: none; padding: 10px 12px; }
+.view.active { display: block; }
+.card { background: var(--cockpit-surface); border: 1px solid var(--cockpit-grid-line);
+ border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; }
+.card-title { font-size: 13px; color: var(--cockpit-fg-muted); margin-bottom: 8px; }
+.hint { font-size: 11px; color: #64748b; }
+.row { display: flex; gap: 8px; align-items: center; }
+
+/* 四状态工艺(横向滑动卡片) */
+#m-stages { display: flex; gap: 8px; overflow-x: auto; }
+.stage-chip { min-width: 96px; border-radius: 8px; padding: 10px; text-align: center;
+ border: 1px solid var(--cockpit-grid-line); flex-shrink: 0; }
+.stage-chip .n { font-weight: 600; }
+.stage-chip .d { font-size: 11px; color: var(--cockpit-fg-muted); margin-top: 3px; }
+.stage-chip .s { font-size: 11px; margin-top: 5px; padding: 1px 8px; border-radius: 9px;
+ display: inline-block; }
+.s-running { box-shadow: inset 0 0 0 1px #2ecc71; }
+.s-running .s { background: rgba(46,204,113,.15); color: #2ecc71; }
+.s-warning { box-shadow: inset 0 0 0 1px var(--cockpit-warn); }
+.s-warning .s { background: rgba(245,166,35,.15); color: var(--cockpit-warn); }
+.s-alarm { box-shadow: inset 0 0 0 1px #ff3b30; }
+.s-alarm .s { background: rgba(255,59,48,.15); color: #ff3b30; }
+.s-offline { opacity: .55; }
+.s-offline .s { background: rgba(138,160,189,.15); color: var(--cockpit-fg-muted); }
+
+/* KPI 两列网格 */
+.kpi-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
+.kpi { background: var(--cockpit-surface); border: 1px solid var(--cockpit-grid-line);
+ border-radius: 10px; padding: 10px 12px; }
+.kpi .v { font-size: 22px; font-weight: 700; color: var(--cockpit-accent); }
+.kpi .l { font-size: 12px; margin-top: 2px; }
+.kpi .d { font-size: 10px; color: var(--cockpit-fg-muted); margin-top: 3px; }
+
+/* 告警(只读,severity 配色同源 alarm_panel.ti.yaml) */
+.alarm { border-radius: 6px; padding: 8px 10px; margin-bottom: 6px; font-size: 12px;
+ border-left: 3px solid transparent; display: flex; gap: 8px; }
+.alarm .sev { font-weight: 700; }
+
+/* 交接班摘要 */
+.brief-sec { margin-bottom: 8px; }
+.brief-sec .sec-title { color: var(--cockpit-accent); font-size: 13px; margin-bottom: 4px; }
+.brief-sec .sec-body { color: var(--cockpit-fg); white-space: pre-wrap; line-height: 1.6; }
+.event { display: flex; gap: 8px; font-size: 12px; padding: 4px 0;
+ border-bottom: 1px solid var(--cockpit-grid-line); }
+.event .t { color: var(--cockpit-fg-muted); flex-shrink: 0; }
+.todo { font-size: 12px; padding: 4px 0; }
+.todo .pri { color: var(--cockpit-warn); }
+.fold-btn { background: none; border: none; color: var(--cockpit-accent);
+ font-size: 12px; padding: 4px 0; cursor: pointer; }
+.confirm-tag { display: inline-block; background: rgba(245,166,35,.15); color: var(--cockpit-warn);
+ padding: 2px 8px; border-radius: 9px; font-size: 11px; margin-top: 6px; }
+
+select, button { padding: 9px 12px; border-radius: 8px; font-size: 14px; }
+select { border: 1px solid var(--cockpit-grid-line); background: var(--cockpit-bg);
+ color: var(--cockpit-fg); flex: 1; }
+button.primary { border: none; background: var(--cockpit-accent); color: #04202a; }
+button:disabled { opacity: .5; }
+
+/* 底部页签 */
+#tabbar { position: fixed; bottom: 0; left: 0; right: 0; display: flex;
+ background: var(--cockpit-surface); border-top: 1px solid var(--cockpit-grid-line);
+ padding-bottom: env(safe-area-inset-bottom); }
+#tabbar .tab { flex: 1; padding: 12px; background: none; border: none;
+ color: var(--cockpit-fg-muted); font-size: 14px; cursor: pointer; }
+#tabbar .tab.active { color: var(--cockpit-accent); }
diff --git a/web/mobile/mobile.js b/web/mobile/mobile.js
new file mode 100644
index 0000000..fda9176
--- /dev/null
+++ b/web/mobile/mobile.js
@@ -0,0 +1,241 @@
+/* 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;
+ var rnd = seededRandom(w.props.metric || "");
+ 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 () {});
+ 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/ 根目录起服务)"));
+ });
+})();