]> acesimba.cloud Git - StationAIDemo.git/commitdiff
新增 AIDC 专题组(8 页模拟数据,独立于真实场站)
authorwang.yu(王瑜) <wang.yu@advantech.com>
Mon, 24 Aug 2026 03:41:20 +0000 (11:41 +0800)
committerwang.yu(王瑜) <wang.yu@advantech.com>
Mon, 24 Aug 2026 03:41:20 +0000 (11:41 +0800)
- 新建 Aidc.tsx:8 个 AIDC 页面(chain/quality/patrol/redundancy/
  liquid/synergy/security/simulation)全部使用原型模拟数据,不接真实
  MU/AIDC/iStation 接口;含数据中心图标(AidcIcon)、菜单项(aidcModules)、
  主容器(AidcPanel) 与 .aidc-root 作用域 CSS
- 修改 App.tsx:左侧"场站运营"段内、原场站项之上插入 AIDC 子分组;
  active 状态类型扩展为 SkillKey|AidcKey,aidc- 前缀路由到 AidcPanel
- 通过 tsc 类型检查与 vite 生产构建验证

frontend/src/Aidc.tsx [new file with mode: 0644]
frontend/src/App.tsx

diff --git a/frontend/src/Aidc.tsx b/frontend/src/Aidc.tsx
new file mode 100644 (file)
index 0000000..6fdae9d
--- /dev/null
@@ -0,0 +1,1032 @@
+import { useState, type CSSProperties } from "react";
+
+/* ============================================================
+ * AIDC 专题(StationAIDemo)
+ * 一组全新功能页面,使用设计原型中的模拟数据,不连接真实场站。
+ * 样式作用域限定在 .aidc-root,沿用原型的设计语言。
+ * ============================================================ */
+
+export type AidcKey =
+  | "aidc-chain"
+  | "aidc-quality"
+  | "aidc-patrol"
+  | "aidc-redundancy"
+  | "aidc-liquid"
+  | "aidc-synergy"
+  | "aidc-security"
+  | "aidc-simulation";
+
+export const aidcModules: { key: AidcKey; name: string; sub: string; mark: string }[] = [
+  { key: "aidc-chain", name: "关键链路态势", sub: "连续性总览", mark: "链" },
+  { key: "aidc-quality", name: "数据可信度中心", sub: "AI 数据底座", mark: "数" },
+  { key: "aidc-patrol", name: "智能巡检与交接班", sub: "少人值守", mark: "巡" },
+  { key: "aidc-redundancy", name: "冗余与连续性保障", sub: "N+1 / 2N", mark: "冗" },
+  { key: "aidc-liquid", name: "液冷安全运营", sub: "CDU 与液冷区域", mark: "液" },
+  { key: "aidc-synergy", name: "电冷算协同", sub: "GPU 负荷联动", mark: "协" },
+  { key: "aidc-security", name: "多模态安全", sub: "环境与视频联动", mark: "安" },
+  { key: "aidc-simulation", name: "扩容与故障仿真", sub: "What-if 决策", mark: "仿" },
+];
+
+/* 数字机房 / 数据中心图标:机柜 + 服务器槽位 + 算力节点 */
+export function AidcIcon({ style }: { style?: CSSProperties }) {
+  return (
+    <svg viewBox="0 0 24 24" width={16} height={16} style={style} aria-hidden fill="none" stroke="currentColor" strokeWidth={1.6}>
+      <rect x="3.4" y="3" width="17.2" height="18" rx="2.3" />
+      <line x1="3.4" y1="8.4" x2="20.6" y2="8.4" />
+      <line x1="3.4" y1="13.6" x2="20.6" y2="13.6" />
+      <circle cx="7" cy="5.7" r="0.95" fill="currentColor" stroke="none" />
+      <circle cx="7" cy="10.9" r="0.95" fill="currentColor" stroke="none" />
+      <circle cx="7" cy="16.1" r="0.95" fill="currentColor" stroke="none" />
+      <rect x="11.4" y="10" width="6.2" height="3.6" rx="0.9" fill="currentColor" stroke="none" opacity="0.85" />
+    </svg>
+  );
+}
+
+/* ---------- 模拟数据(来自设计原型,不连接真实场站) ---------- */
+const stations = {
+  core: { name: "Demo ORG · 智算中心一期", cabinets: 96, itLoad: 5.42, trust: 97.8, pue: 1.24, alerts: 2, factor: 1 },
+  edge: { name: "东区边缘算力舱", cabinets: 18, itLoad: 0.71, trust: 99.1, pue: 1.19, alerts: 0, factor: 0.32 },
+  liquid: { name: "高密液冷实验站", cabinets: 36, itLoad: 2.18, trust: 95.6, pue: 1.16, alerts: 3, factor: 0.58 },
+} as const;
+type StationKey = keyof typeof stations;
+
+const fmt = (n: number, d = 1) => Number(n).toFixed(d);
+
+/* ---------- 通用小组件 ---------- */
+const Badge = ({ text, kind }: { text: string; kind?: string }) => (
+  <span className={`i-badge ${kind ?? ""}`}>{text}</span>
+);
+
+const Metric = ({ label, value, note, cls }: { label: string; value: string; note: string; cls?: string }) => {
+  const tone = cls?.includes("warn") ? "warn" : cls?.includes("bad") ? "bad" : cls?.includes("good") ? "good" : "";
+  return (
+    <div className={`i-panel i-kpi ${tone}`}>
+      <div className="i-metric-label">{label}</div>
+      <div className={`i-metric-value ${cls ?? ""}`}>{value}</div>
+      <div className="i-metric-note">{note}</div>
+    </div>
+  );
+};
+
+const EventRow = ({ time, title, sub }: { time: string; title: string; sub: string }) => (
+  <div className="i-event">
+    <span>{time}</span>
+    <span className="i-event-line" />
+    <div>
+      <strong>{title}</strong>
+      <div className="i-panel-sub">{sub}</div>
+    </div>
+  </div>
+);
+
+const AiInsight = ({ summary, points, prompt }: { summary: string; points: string[]; prompt: string }) => {
+  const [open, setOpen] = useState(false);
+  const [regen, setRegen] = useState(false);
+  return (
+    <section className="i-ai">
+      <div className="i-ai-head">
+        <span aria-hidden>✦</span>
+        <strong>AI Insight · 场站级结论</strong>
+        {Badge({ text: "可追溯" } as any)}
+      </div>
+      <p>{summary}</p>
+      <ul>
+        {points.map((x, i) => (
+          <li key={i}>{x}</li>
+        ))}
+      </ul>
+      <div className="i-ai-actions">
+        <button className="i-button i-quiet" type="button" onClick={() => setOpen((v) => !v)}>
+          {open ? "收起生成提示词" : "查看生成提示词"}
+        </button>
+        <button className="i-button i-primary" type="button" onClick={() => setRegen(true)}>
+          {regen ? "已重新生成" : "重新生成"}
+        </button>
+      </div>
+      {open && (
+        <div className="i-prompt is-open">
+          <label className="i-label">
+            生成提示词
+            <textarea className="i-textarea" defaultValue={prompt} />
+          </label>
+          <span className="i-panel-sub i-generation-state">模拟提示词可人工调整;不会调用真实模型。</span>
+        </div>
+      )}
+    </section>
+  );
+};
+
+const PageHead = ({
+  icon,
+  title,
+  desc,
+  stationKey,
+  setStationKey,
+}: {
+  icon: string;
+  title: string;
+  desc: string;
+  stationKey: StationKey;
+  setStationKey: (k: StationKey) => void;
+}) => (
+  <>
+    <div className="i-pagehead">
+      <div className="i-page-icon" aria-hidden>
+        {icon}
+      </div>
+      <div>
+        <div className="i-breadcrumb">AIDC 专题 / {title}</div>
+        <h1>{title}</h1>
+        <p>{desc}</p>
+      </div>
+      <div className="i-page-actions">
+        <button className="i-button i-quiet" type="button">
+          刷新源数据
+        </button>
+        <button className="i-button i-primary" type="button">
+          重新生成分析
+        </button>
+      </div>
+    </div>
+    <div className="i-contextbar">
+      <div className="i-contextcell">
+        <span className="i-context-label">当前资产组</span>
+        <select
+          className="i-select"
+          value={stationKey}
+          onChange={(e) => setStationKey(e.target.value as StationKey)}
+          aria-label="切换资产组"
+        >
+          {Object.entries(stations).map(([k, st]) => (
+            <option key={k} value={k}>
+              {st.name}
+            </option>
+          ))}
+        </select>
+      </div>
+      <div className="i-contextcell">
+        <span className="i-context-label">组织 / 站点</span>
+        <strong className="i-context-value">Demo Org · AIDC 一期</strong>
+      </div>
+      <div className="i-contextcell">
+        <span className="i-context-label">统计范围</span>
+        <strong className="i-context-value">今日 00:00—当前</strong>
+      </div>
+      <div className="i-contextcell">
+        <span className="i-context-label">数据状态</span>
+        <strong className="i-context-value i-warn-text">源站可见快照 · 模拟补充</strong>
+      </div>
+    </div>
+  </>
+);
+
+/* ---------- 8 个页面 ---------- */
+function ChainPage({ s, stationKey, setStationKey }: { s: any; stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  const chainData: [string, string, string, string][] =
+    stationKey === "edge"
+      ? [
+          ["L01", "供电连续性", "正常", "good"],
+          ["L02", "风冷热管理", "正常", "good"],
+          ["L03", "液冷换热", "未配置", ""],
+          ["L04", "环境安全", "正常", "good"],
+          ["L05", "事件处置", "正常", "good"],
+          ["L06", "能效容量", "正常", "good"],
+        ]
+      : [
+          ["L01", "供电连续性", "冗余关注", "warn"],
+          ["L02", "风冷热管理", "正常", "good"],
+          ["L03", "液冷换热", stationKey === "liquid" ? "液质关注" : "正常", stationKey === "liquid" ? "warn" : "good"],
+          ["L04", "环境安全", "正常", "good"],
+          ["L05", "事件处置", s.alerts ? `${s.alerts}项待闭环` : "正常", s.alerts ? "warn" : "good"],
+          ["L06", "能效容量", "正常", "good"],
+        ];
+  return (
+    <>
+      <PageHead icon="◇" title="关键链路态势" desc="从场站连续运行角度统一观察供电、制冷、液冷、安全、事件与容量六条链路。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "场站运行态势", value: s.alerts ? "需要关注" : "稳定", note: s.alerts ? `${s.alerts} 项风险需要确认` : "六条链路无未闭环风险", cls: s.alerts ? "i-warn-text" : "i-good-text" } as any)}
+        {Metric({ label: "关键链路", value: "6 条", note: `${chainData.filter((x) => x[3] === "good").length} 条状态正常` } as any)}
+        {Metric({ label: "IT 实时负荷", value: `${fmt(s.itLoad, 2)} MW`, note: `${s.cabinets} 个机柜纳入场站范围` } as any)}
+        {Metric({ label: "冗余后电力余量", value: `${fmt(1.28 * s.factor + 0.42, 2)} MW`, note: "满足当前 N+1 约束", cls: "i-good-text" } as any)}
+      </div>
+      <section className="i-panel">
+        <div className="i-panel-head">
+          <div>
+            <h2>六条关键运行链路</h2>
+            <div className="i-panel-sub">状态由点位、告警、拓扑和数据质量联合确定</div>
+          </div>
+          {Badge({ text: s.alerts ? "2 条关注" : "全部正常", kind: s.alerts ? "warn" : "good" } as any)}
+        </div>
+        <div className="i-chain">
+          {chainData.map((x, i) => (
+            <div key={i} className={`i-chain-node ${x[3] === "warn" ? "warn" : x[3] === "bad" ? "bad" : ""}`}>
+              <strong>
+                {x[0]} {x[1]}
+              </strong>
+              <span>{x[2]}</span>
+            </div>
+          ))}
+        </div>
+      </section>
+      <div className="i-grid two" style={{ marginTop: 12 }}>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>供电连续性链</h2>
+              <div className="i-panel-sub">市电 → ATS → UPS → PDU → 算力负载</div>
+            </div>
+            {Badge({ text: "冗余关注", kind: "warn" } as any)}
+          </div>
+          <div className="i-schematic">
+            <div className="i-flow-row">
+              <div className="i-flow-unit">
+                <div className="i-flow-dot">市电</div>
+                <span>10kV 正常</span>
+              </div>
+              <div className="i-flow-unit">
+                <div className="i-flow-dot">ATS</div>
+                <span>主路供电</span>
+              </div>
+              <div className="i-flow-unit warn">
+                <div className="i-flow-dot">UPS</div>
+                <span>B路检修</span>
+              </div>
+              <div className="i-flow-unit">
+                <div className="i-flow-dot">PDU</div>
+                <span>负载 68%</span>
+              </div>
+              <div className="i-flow-unit">
+                <div className="i-flow-dot">GPU</div>
+                <span>{fmt(s.itLoad, 2)} MW</span>
+              </div>
+            </div>
+          </div>
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>链路关注事项</h2>
+            <span className="i-panel-sub">按影响排序</span>
+          </div>
+          <EventRow time="14:12" title="UPS-B 进入计划检修" sub="当前仍满足 N+1,建议限制新增负荷。" />
+          <EventRow time="13:48" title="冷通道 A3 温度短时抬升" sub="已恢复,未达到事件升级条件。" />
+          <EventRow time="11:05" title="采集网关补传完成" sub="缺失的 7 分钟数据已标记并补齐。" />
+        </section>
+      </div>
+      <AiInsight
+        summary="当前场站总体可运行,但 UPS-B 检修使供电冗余进入敏感窗口。若未来 4 小时 GPU 负荷超过 6.1 MW,冗余后余量将低于建议值。"
+        points={["检修窗口内暂缓高功率训练任务上架。", "持续观察 A3 冷通道入口温度,确认是否与负荷迁移相关。"]}
+        prompt="基于六条关键链路、实时负荷、拓扑、未闭环告警和未来4小时预测,生成场站连续性结论。仅引用可信度高于95%的数据,并列出证据时间。"
+      />
+    </>
+  );
+}
+
+function QualityPage({ s }: { s: any }) {
+  const rows: [string, string, number, string][] = [
+    ["供配电", "42 / 42", 100, "正常"],
+    ["风冷系统", "31 / 32", 96.9, "1个点位延迟"],
+    ["液冷系统", s.key === "edge" ? "0 / 0" : "47 / 48", s.key === "edge" ? 100 : 97.9, s.key === "edge" ? "未配置" : "1个点位跳变"],
+    ["环境安全", "24 / 24", 100, "正常"],
+    ["能效计量", "11 / 12", 91.7, "IT末端电表缺报"],
+  ];
+  return (
+    <>
+      <PageHead icon="✓" title="数据可信度中心" desc="让所有AI结论先回答“数据是否足够可信”,避免把采集问题误判为运行问题。" stationKey={s.key} setStationKey={s.setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "综合可信度", value: `${s.trust}%`, note: "较昨日 +0.6%", cls: "i-good-text" } as any)}
+        {Metric({ label: "应接 / 已接点位", value: "158 / 155", note: "覆盖完整性 98.1%" } as any)}
+        {Metric({ label: "10分钟采集成功率", value: "99.42%", note: "目标不低于 99.0%", cls: "i-good-text" } as any)}
+        {Metric({ label: "异常数据项", value: "3", note: "缺失 1 · 延迟 1 · 跳变 1", cls: "i-warn-text" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>专业数据覆盖与质量</h2>
+              <div className="i-panel-sub">模拟点位字典与实际接入对照</div>
+            </div>
+          </div>
+          {rows.map((r, i) => (
+            <div key={i} style={{ marginBottom: 14 }}>
+              <div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
+                <strong>{r[0]}</strong>
+                <span>
+                  {r[1]} · {r[3]}
+                </span>
+              </div>
+              <div className={`i-bar ${r[2] < 95 ? "warn" : "good"}`}>
+                <span style={{ width: `${r[2]}%` }} />
+              </div>
+            </div>
+          ))}
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>需要处理的数据问题</h2>
+            {Badge({ text: "3 项", kind: "warn" } as any)}
+          </div>
+          <EventRow time="持续 22分" title="ENE-IT-03 无新数据" sub="可能影响 PUE 实时值,当前改用最近可信值。" />
+          <EventRow time="持续 8分" title="AIR-A3-17 延迟" sub="网关队列积压,尚未影响高温判定。" />
+          <EventRow time="14:21" title="LC-PH-02 瞬时跳变" sub="已隔离异常样本,等待仪表复核。" />
+        </section>
+      </div>
+      <AiInsight
+        summary="当前数据可以支撑运行态势和告警分析,但不建议使用实时 PUE 作为节能策略依据,原因是 IT 末端电表 ENE-IT-03 已连续缺报 22 分钟。"
+        points={["日报中应标记 PUE 为“估算值”。", "优先检查电表通讯与边缘网关映射,不建议直接更换仪表。"]}
+        prompt="分析接入覆盖、数据新鲜度、跳变、累计值反向和时钟偏差。输出可用性结论以及受影响的AI功能,不得将数据异常描述为设备故障。"
+      />
+    </>
+  );
+}
+
+function PatrolPage({ s }: { s: any }) {
+  const routes: [string, string, string, string][] = [
+    ["供电连续性链", "12/12", "100", "good"],
+    ["风冷热管理链", "9/10", "90", "warn"],
+    ["液冷换热链", "11/12", "92", "warn"],
+    ["环境安全链", "8/8", "100", "good"],
+    ["能效容量链", "6/8", "75", "warn"],
+  ];
+  return (
+    <>
+      <PageHead icon="巡" title="智能巡检与交接班" desc="按六条关键链路自动汇总巡检证据、未闭环事项和下一班关注重点。" stationKey={s.key} setStationKey={s.setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "今日巡检完成率", value: "92%", note: "46 / 50 项已完成", cls: "i-good-text" } as any)}
+        {Metric({ label: "AI 自动核验", value: "38 项", note: "实时点位与趋势自动判断" } as any)}
+        {Metric({ label: "需要人工确认", value: "4 项", note: "现场目视或仪表复核", cls: "i-warn-text" } as any)}
+        {Metric({ label: "交接事项", value: `${Math.max(2, s.alerts + 1)} 项`, note: "其中 1 项为重点关注" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>今日巡检路线</h2>
+              <div className="i-panel-sub">白班 · 08:00—16:00</div>
+            </div>
+            {Badge({ text: "执行中", kind: "good" } as any)}
+          </div>
+          {routes.map((r, i) => (
+            <div key={i} style={{ marginBottom: 14 }}>
+              <div style={{ display: "flex", justifyContent: "space-between" }}>
+                <strong>{r[0]}</strong>
+                <span>{r[1]}</span>
+              </div>
+              <div className={`i-bar ${r[3]}`}>
+                <span style={{ width: `${r[2]}%` }} />
+              </div>
+            </div>
+          ))}
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>交接班摘要</h2>
+            <span className="i-panel-sub">AI 草稿</span>
+          </div>
+          <EventRow time="重点" title="UPS-B 检修持续至 18:00" sub="关注冗余后负载,暂缓新增训练任务。" />
+          <EventRow time="待确认" title="CDU-02 过滤器压差偏高" sub="需现场读取机械表并上传照片。" />
+          <EventRow time="已完成" title="A3 通道温度复核" sub="实测与平台数据偏差 0.3℃,可接受。" />
+        </section>
+      </div>
+      <section className="i-panel" style={{ marginTop: 12 }}>
+        <div className="i-panel-head">
+          <h2>人工确认项</h2>
+          <button className="i-button i-primary" type="button">
+            开始现场巡检
+          </button>
+        </div>
+        <div className="i-table-wrap">
+          <table>
+            <thead>
+              <tr>
+                <th>区域</th>
+                <th>巡检内容</th>
+                <th>AI 预判</th>
+                <th>需要的人工证据</th>
+                <th>时限</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td>液冷区 B</td>
+                <td>CDU-02 过滤器压差</td>
+                <td>{Badge({ text: "疑似缓慢堵塞", kind: "warn" } as any)}</td>
+                <td>机械表读数、滤网外观照片</td>
+                <td>15:30</td>
+              </tr>
+              <tr>
+                <td>电池间</td>
+                <td>电池组 B-17 温度</td>
+                <td>{Badge({ text: "趋势正常", kind: "good" } as any)}</td>
+                <td>红外测温复核</td>
+                <td>16:00</td>
+              </tr>
+              <tr>
+                <td>冷通道 A3</td>
+                <td>局部气流组织</td>
+                <td>{Badge({ text: "需要观察" } as any)}</td>
+                <td>通道门状态、风口遮挡情况</td>
+                <td>16:00</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </section>
+      <AiInsight
+        summary="本班运行总体平稳。建议下一班首先确认 CDU-02 过滤器压差,并持续关注 UPS-B 检修窗口内的负荷变化。其余异常均已恢复或有明确责任人。"
+        points={["4项人工确认任务中,2项涉及无法由传感器确认的现场状态。", "交接摘要已引用告警、趋势和工单证据,可由值班长审核后发布。"]}
+        prompt="生成交接班摘要:按连续性影响排序,只保留未完成、已恢复但需观察、以及下一班必须执行的事项;每条结论必须引用时间和数据来源。"
+      />
+    </>
+  );
+}
+
+function RedundancyPage({ s, stationKey, setStationKey }: { s: any; stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  const [failure, setFailure] = useState<"ups" | "chiller" | "cdu">("ups");
+  const failMap: Record<string, [string, string, string, string]> = {
+    ups: ["UPS-B 退出", "其余 UPS 负载率 82%", "仍满足 N+1", "warn"],
+    chiller: ["CH-03 退出", "其余冷源负载率 76%", "仍满足 N+1", "good"],
+    cdu: ["CDU-02 退出", "B区 12 个机柜需迁移", "局部不满足冗余", "bad"],
+  };
+  const fm = failMap[failure];
+  return (
+    <>
+      <PageHead icon="N+1" title="冗余与连续性保障" desc="把设备状态转换为场站在单点故障或检修条件下还能否持续运行的结论。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "当前保障等级", value: "N+1", note: "供电与冷源均满足目标", cls: "i-good-text" } as any)}
+        {Metric({ label: "供电冗余后余量", value: `${fmt(0.96 * s.factor + 0.58, 2)} MW`, note: "UPS-B 退出检修工况" } as any)}
+        {Metric({ label: "制冷冗余后余量", value: `${fmt(620 * s.factor + 210, 0)} kW`, note: "冷机 CH-03 退出工况" } as any)}
+        {Metric({ label: "连续性风险", value: s.alerts ? "中低" : "低", note: "未来 4 小时无红线突破", cls: s.alerts ? "i-warn-text" : "i-good-text" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>单点故障模拟</h2>
+              <div className="i-panel-sub">选择退出对象,查看冗余后场站状态</div>
+            </div>
+          </div>
+          <label className="i-label" style={{ marginBottom: 15 }}>
+            模拟场景
+            <select className="i-select" value={failure} onChange={(e) => setFailure(e.target.value as any)}>
+              <option value="ups">UPS-B 故障退出</option>
+              <option value="chiller">冷机 CH-03 故障退出</option>
+              <option value="cdu">CDU-02 故障退出</option>
+            </select>
+          </label>
+          <div className="i-schematic" style={{ marginTop: 12 }}>
+            <div style={{ textAlign: "center" }}>
+              <div className="i-metric-label">模拟结果</div>
+              <div className={`i-metric-value ${fm[3] === "bad" ? "i-bad-text" : fm[3] === "warn" ? "i-warn-text" : "i-good-text"}`}>{fm[2]}</div>
+              <p>
+                {fm[0]} · {fm[1]}
+              </p>
+              <div style={{ marginTop: 12 }}>
+                {Badge({ text: fm[3] === "bad" ? "需要处置方案" : "可持续运行", kind: fm[3] } as any)}
+              </div>
+            </div>
+          </div>
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>连续性约束</h2>
+            {Badge({ text: "3 项满足", kind: "good" } as any)}
+          </div>
+          <div style={{ marginBottom: 18 }}>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <strong>供电链冗余后负载率</strong>
+              <span>82%</span>
+            </div>
+            <div className="i-gauge warn">
+              <span style={{ width: "82%" }} />
+            </div>
+            <div className="i-panel-sub">建议上限 85%</div>
+          </div>
+          <div style={{ marginBottom: 18 }}>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <strong>冷源冗余后负载率</strong>
+              <span>76%</span>
+            </div>
+            <div className="i-gauge">
+              <span style={{ width: "76%" }} />
+            </div>
+            <div className="i-panel-sub">建议上限 85%</div>
+          </div>
+          <div>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <strong>备用发电保障时间</strong>
+              <span>8.6 h</span>
+            </div>
+            <div className="i-gauge">
+              <span style={{ width: "72%" }} />
+            </div>
+            <div className="i-panel-sub">目标不低于 8 小时</div>
+          </div>
+        </section>
+      </div>
+      <AiInsight
+        summary="在 UPS-B 退出场景下,场站仍满足 N+1,但剩余 UPS 负载率升至 82%,已接近建议上限。高功率任务集中启动可能造成短时越限。"
+        points={["将未来4小时新增IT负荷控制在0.35 MW以内。", "若检修延期,建议提前协调算力调度,避免训练任务集中启动。"]}
+        prompt="基于供电和制冷拓扑、额定容量、实时负荷以及维护窗口,模拟单点退出后的剩余能力。明确区分确定性计算与预测值。"
+      />
+    </>
+  );
+}
+
+function LiquidPage({ stationKey, setStationKey }: { stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  return (
+    <>
+      <PageHead icon="液" title="液冷安全运营" desc="从液冷区域整体安全出发,联合观察流量、压差、温差、液质和泄漏。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "液冷区域状态", value: "需要关注", note: "CDU-02 压差缓慢上升", cls: "i-warn-text" } as any)}
+        {Metric({ label: "承载 IT 负荷", value: "1.86 MW", note: "覆盖 24 个高密机柜" } as any)}
+        {Metric({ label: "二次侧供液温度", value: "31.4 ℃", note: "控制目标 30—33℃", cls: "i-good-text" } as any)}
+        {Metric({ label: "漏液检测", value: "正常", note: "128 个检测分区在线", cls: "i-good-text" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>液冷链路运行</h2>
+              <div className="i-panel-sub">设施水 → CDU → 歧管 → 冷板 → GPU</div>
+            </div>
+            {Badge({ text: "1项关注", kind: "warn" } as any)}
+          </div>
+          <div className="i-table-wrap">
+            <table>
+              <thead>
+                <tr>
+                  <th>回路</th>
+                  <th>供 / 回液温度</th>
+                  <th>流量</th>
+                  <th>过滤器压差</th>
+                  <th>液质</th>
+                  <th>状态</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td>CDU-01 · A区</td>
+                  <td>31.1 / 38.2 ℃</td>
+                  <td>824 L/min</td>
+                  <td>18 kPa</td>
+                  <td>正常</td>
+                  <td>{Badge({ text: "正常", kind: "good" } as any)}</td>
+                </tr>
+                <tr>
+                  <td>CDU-02 · B区</td>
+                  <td>31.4 / 39.1 ℃</td>
+                  <td>796 L/min</td>
+                  <td>29 kPa</td>
+                  <td>正常</td>
+                  <td>{Badge({ text: "压差关注", kind: "warn" } as any)}</td>
+                </tr>
+                <tr>
+                  <td>CDU-03 · C区</td>
+                  <td>30.9 / 37.8 ℃</td>
+                  <td>851 L/min</td>
+                  <td>17 kPa</td>
+                  <td>正常</td>
+                  <td>{Badge({ text: "正常", kind: "good" } as any)}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>液冷区域风险图</h2>
+            <span className="i-panel-sub">12 个逻辑分区</span>
+          </div>
+          <div className="i-rack-grid">
+            {Array.from({ length: 12 }, (_, i) => (
+              <div key={i} className={`i-rack ${i === 7 ? "warn" : ""}`}>
+                <strong>B{String(i + 1).padStart(2, "0")}</strong>
+                <div className="i-panel-sub">{i === 7 ? "压差关联" : "正常"}</div>
+              </div>
+            ))}
+          </div>
+          <div className="i-panel-sub" style={{ marginTop: 11 }}>
+            橙色区域表示受 CDU-02 压差趋势影响,不代表已经发生漏液。
+          </div>
+        </section>
+      </div>
+      <AiInsight
+        summary="CDU-02 过滤器压差过去 7 天由 21 kPa 缓慢升至 29 kPa,流量仍满足当前负荷。现阶段更符合过滤器逐步堵塞特征,不属于断流或泄漏事件。"
+        points={["建议在下一维护窗口检查并清洁过滤器。", "暂不建议切换回路;若压差超过35 kPa或流量下降8%,应升级为重要事件。"]}
+        prompt="联合分析CDU一次侧与二次侧温度、流量、压差、冷却液质量及漏液状态,识别液冷区域风险并给出证据。不得仅凭单一阈值判断故障。"
+      />
+    </>
+  );
+}
+
+function SynergyPage({ s, stationKey, setStationKey }: { s: any; stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  return (
+    <>
+      <PageHead icon="联" title="电力—制冷—算力协同" desc="预测 GPU 负荷变化对供电和制冷余量的影响,提前发现容量瓶颈。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "GPU 集群负载", value: "74%", note: "未来 2 小时预计升至 86%" } as any)}
+        {Metric({ label: "IT 实时功率", value: `${fmt(s.itLoad, 2)} MW`, note: "15分钟均值" } as any)}
+        {Metric({ label: "制冷负荷率", value: "69%", note: "冗余后 81%" } as any)}
+        {Metric({ label: "当前 PUE", value: s.pue, note: "模拟计量边界", cls: "i-good-text" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>未来 6 小时协同预测</h2>
+              <div className="i-panel-sub">归一化负荷指数</div>
+            </div>
+            {Badge({ text: "预测数据" } as any)}
+          </div>
+          <svg className="i-chart" viewBox="0 0 720 260" role="img" aria-label="GPU负荷、IT功率和制冷负荷的六小时趋势">
+            <line className="gridline" x1="55" y1="35" x2="700" y2="35" />
+            <line className="gridline" x1="55" y1="95" x2="700" y2="95" />
+            <line className="gridline" x1="55" y1="155" x2="700" y2="155" />
+            <line className="gridline" x1="55" y1="215" x2="700" y2="215" />
+            <text x="18" y="39">100</text>
+            <text x="24" y="99">75</text>
+            <text x="24" y="159">50</text>
+            <text x="24" y="219">25</text>
+            <text x="52" y="244">14:00</text>
+            <text x="252" y="244">16:00</text>
+            <text x="464" y="244">18:00</text>
+            <text x="662" y="244">20:00</text>
+            <polyline className="series-blue" points="55,103 160,95 270,78 380,57 490,69 600,85 700,92" />
+            <polyline className="series-purple" points="55,121 160,112 270,101 380,82 490,91 600,106 700,113" />
+            <polyline className="series-warn" points="55,135 160,129 270,116 380,94 490,97 600,115 700,123" />
+          </svg>
+          <div className="i-legend">
+            <span>
+              <i className="i-key" /> GPU负荷
+            </span>
+            <span>
+              <i className="i-key purple" /> IT功率
+            </span>
+            <span>
+              <i className="i-key warn" /> 制冷负荷
+            </span>
+          </div>
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>瓶颈预测</h2>
+            {Badge({ text: "18:00 峰值", kind: "warn" } as any)}
+          </div>
+          <div style={{ marginBottom: 18 }}>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <strong>电力冗余后余量</strong>
+              <span>0.62 MW</span>
+            </div>
+            <div className="i-gauge warn">
+              <span style={{ width: "84%" }} />
+            </div>
+          </div>
+          <div style={{ marginBottom: 18 }}>
+            <div style={{ display: "flex", justifyContent: "space-between" }}>
+              <strong>制冷冗余后余量</strong>
+              <span>0.81 MW</span>
+            </div>
+            <div className="i-gauge">
+              <span style={{ width: "76%" }} />
+            </div>
+          </div>
+          <div>
+            <strong>预计瓶颈:供电容量</strong>
+            <p className="i-panel-sub">若训练任务按计划同时启动,供电先于冷量接近冗余红线。</p>
+          </div>
+        </section>
+      </div>
+      <AiInsight
+        summary="计划中的两个训练任务会在 18:00 左右形成叠加峰值。场站不会过载,但供电冗余后余量将降至 0.62 MW,安全裕度明显收窄。"
+        points={["建议将任务 AI-Train-204 延后 45 分钟启动。", "调整后预计峰值降低 0.38 MW,制冷系统无需额外启机。"]}
+        prompt="结合GPU任务计划、IT末端功率、UPS负载、冷机与CDU负荷预测未来6小时瓶颈。给出调度建议和预估影响,不直接下发控制。"
+      />
+    </>
+  );
+}
+
+function SecurityPage({ stationKey, setStationKey }: { stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  return (
+    <>
+      <PageHead icon="视" title="多模态安全与无人值守" desc="把门禁、视频、消防、漏水和环境事件关联成可核验的场站安全事件。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid metrics">
+        {Metric({ label: "当前安全态势", value: "正常", note: "无紧急事件", cls: "i-good-text" } as any)}
+        {Metric({ label: "视频在线率", value: "31 / 32", note: "CAM-B07 维护中" } as any)}
+        {Metric({ label: "今日关联事件", value: "7", note: "AI 自动排除 5 条" } as any)}
+        {Metric({ label: "待人工复核", value: "1", note: "液冷区门长开 96 秒", cls: "i-warn-text" } as any)}
+      </div>
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>事件证据链</h2>
+              <div className="i-panel-sub">门禁、视频与环境数据按时间关联</div>
+            </div>
+            {Badge({ text: "待复核", kind: "warn" } as any)}
+          </div>
+          <EventRow time="13:42:08" title="维护人员刷卡进入液冷区" sub="身份:张伟 · 工单 WO-20260824-17" />
+          <EventRow time="13:42:11" title="CAM-L02 检测到 1 人进入" sub="人员数量与授权记录一致。" />
+          <EventRow time="13:43:47" title="液冷区门保持开启 96 秒" sub="超过 60 秒建议值,未发现尾随人员。" />
+          <EventRow time="13:45:20" title="门已关闭,环境参数正常" sub="事件未升级,保留人工复核。" />
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <h2>关联画面</h2>
+            <span className="i-panel-sub">模拟视频占位</span>
+          </div>
+          <div className="i-camera-grid">
+            <div className="i-camera">
+              <span>CAM-L01 入口</span>
+            </div>
+            <div className="i-camera">
+              <span>CAM-L02 通道</span>
+            </div>
+            <div className="i-camera">
+              <span>CAM-L03 CDU区</span>
+            </div>
+            <div className="i-camera">
+              <span>CAM-L04 出口</span>
+            </div>
+          </div>
+        </section>
+      </div>
+      <AiInsight
+        summary="本次事件存在门长开,但刷卡身份、视频人数和维护工单一致,且期间未出现尾随、漏液或环境异常。建议按一般事件由值班人员快速复核,不升级安全告警。"
+        points={["AI结论来自4类证据,置信度94%。", "视频仅用于事件核验,原始控制仍由门禁和消防系统负责。"]}
+        prompt="关联同一时间和区域内的门禁、视频、消防、漏水与环境事件,输出事件性质、证据链和置信度。不得替代消防或门禁控制。"
+      />
+    </>
+  );
+}
+
+function SimulationPage({ s, stationKey, setStationKey }: { s: any; stationKey: StationKey; setStationKey: (k: StationKey) => void }) {
+  const [cabinets, setCabinets] = useState(8);
+  const [power, setPower] = useState(80);
+  const [util, setUtil] = useState(72);
+  const added = (cabinets * power * util) / 100 / 1000;
+  const total = s.itLoad + added;
+  const risk = total > 6.2 * Math.max(0.45, s.factor);
+  const spcn = [
+    ["Space 空间", "68%", `${Math.min(96, 68 + cabinets * 0.28).toFixed(0)}%`, "不超过 90%"],
+    ["Power 电力", "72%", `${Math.min(99, 72 + added * 12).toFixed(0)}%`, "冗余后不超过 85%"],
+    ["Cooling 制冷", "65%", `${Math.min(98, 65 + added * 9).toFixed(0)}%`, "冗余后不超过 85%"],
+    ["Network 网络", "61%", `${Math.min(95, 61 + cabinets * 0.45).toFixed(0)}%`, "端口不超过 90%"],
+  ];
+  return (
+    <>
+      <PageHead icon="算" title="扩容与故障仿真" desc="在上架新机柜前,比较空间、电力、制冷和网络四维容量以及冗余风险。" stationKey={stationKey} setStationKey={setStationKey} />
+      <div className="i-grid two">
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>方案输入</h2>
+              <div className="i-panel-sub">模拟增加高密算力机柜</div>
+            </div>
+            {Badge({ text: "What-if" } as any)}
+          </div>
+          <label className="i-label" style={{ marginBottom: 15 }}>
+            新增机柜:<strong>{cabinets} 个</strong>
+            <input className="i-range" type="range" min={1} max={24} value={cabinets} onChange={(e) => setCabinets(Number(e.target.value))} />
+          </label>
+          <label className="i-label" style={{ marginBottom: 15 }}>
+            单柜设计功率:<strong>{power} kW</strong>
+            <input className="i-range" type="range" min={20} max={140} step={10} value={power} onChange={(e) => setPower(Number(e.target.value))} />
+          </label>
+          <label className="i-label" style={{ marginBottom: 15 }}>
+            预计平均利用率:<strong>{util}%</strong>
+            <input className="i-range" type="range" min={30} max={95} value={util} onChange={(e) => setUtil(Number(e.target.value))} />
+          </label>
+          <button className="i-button i-primary" type="button">
+            运行方案模拟
+          </button>
+        </section>
+        <section className="i-panel">
+          <div className="i-panel-head">
+            <div>
+              <h2>模拟结论</h2>
+              <div className="i-panel-sub">基于当前台账和实时负荷</div>
+            </div>
+            {Badge({ text: risk ? "需要调整" : "可实施", kind: risk ? "warn" : "good" } as any)}
+          </div>
+          <div className="i-grid two">
+            {Metric({ label: "新增 IT 负荷", value: `${fmt(added, 2)} MW`, note: `${cabinets} 柜 × ${power} kW × ${util}%` } as any)}
+            {Metric({ label: "扩容后 IT 负荷", value: `${fmt(total, 2)} MW`, note: risk ? "接近冗余约束" : "仍有可用余量", cls: risk ? "i-warn-text" : "i-good-text" } as any)}
+          </div>
+        </section>
+      </div>
+      <section className="i-panel" style={{ marginTop: 12 }}>
+        <div className="i-panel-head">
+          <div>
+            <h2>SPCN 四维容量影响</h2>
+            <div className="i-panel-sub">红线按冗余后容量计算</div>
+          </div>
+        </div>
+        <div className="i-table-wrap">
+          <table>
+            <thead>
+              <tr>
+                <th>维度</th>
+                <th>当前利用率</th>
+                <th>扩容后利用率</th>
+                <th>冗余约束</th>
+                <th>判断</th>
+              </tr>
+            </thead>
+            <tbody>
+              {spcn.map((r, i) => {
+                const v = Number(r[2].replace("%", ""));
+                const warn = v > (r[0].startsWith("Power") || r[0].startsWith("Cooling") ? 85 : 90);
+                return (
+                  <tr key={i}>
+                    <td>{r[0]}</td>
+                    <td>{r[1]}</td>
+                    <td>{r[2]}</td>
+                    <td>{r[3]}</td>
+                    <td>{Badge({ text: warn ? "需要调整" : "满足", kind: warn ? "warn" : "good" } as any)}</td>
+                  </tr>
+                );
+              })}
+            </tbody>
+          </table>
+        </div>
+      </section>
+      <AiInsight
+        summary="当前方案可以上架,但供电将成为最先接近约束的维度。若保持80 kW单柜设计功率,建议将8个机柜分两批投运,并在第二批投运前确认UPS-B检修已结束。"
+        points={["空间和网络容量充足;冷量满足N+1。", "一次性满载投运会使供电冗余后利用率升至86%,超过建议值。"]}
+        prompt="对新增机柜数量、单柜设计功率和利用率进行SPCN四维容量仿真,并叠加单点故障、PUE和维护窗口影响。比较至少两个可执行方案。"
+      />
+    </>
+  );
+}
+
+/* ---------- 主容器 ---------- */
+const AIDC_CSS = `
+.aidc-root{
+  --i-blue: light-dark(#008fd3, #37baf5);
+  --i-blue-dark: light-dark(#005b86, #8dd9fa);
+  --i-ink: light-dark(#323233, #e8f0f5);
+  --i-muted: light-dark(#737b82, #9fb0bd);
+  --i-bg: light-dark(#fafafa, #10161b);
+  --i-surface: light-dark(#ffffff, #182128);
+  --i-surface-2: light-dark(#f7f9fb, #202b33);
+  --i-border: light-dark(#d8e0e6, #35434d);
+  --i-nav: light-dark(#e8eaec, #1b242b);
+  --i-nav-active: light-dark(#dceff8, #173747);
+  --i-good: light-dark(#1b9b72, #55d3a9);
+  --i-warn: light-dark(#ee9b25, #ffc15d);
+  --i-bad: light-dark(#d64e4e, #ff8585);
+  --i-purple: light-dark(#6b55c9, #ae9cff);
+  --i-good-soft: light-dark(#edf9f4, #153329);
+  --i-warn-soft: light-dark(#fff7e9, #392b17);
+  --i-bad-soft: light-dark(#fff0f0, #3a2021);
+  --i-purple-soft: light-dark(#f3f0ff, #292344);
+  --i-shadow: light-dark(rgba(18,42,58,.12), rgba(0,0,0,.28));
+  color-scheme: light dark;
+  color: var(--i-ink);
+  background: var(--i-bg);
+  font-family: "Noto Sans SC", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
+  font-size: 16px;
+  line-height: normal;
+  padding: 18px 20px 34px;
+  min-height: 100%;
+}
+.aidc-root *{box-sizing:border-box}
+.aidc-root button, .aidc-root select, .aidc-root input, .aidc-root textarea{font:inherit}
+.aidc-root button{cursor:pointer}
+.aidc-root .i-pagehead{display:flex;align-items:flex-start;gap:14px;margin-bottom:14px}
+.aidc-root .i-page-icon{width:44px;height:44px;background:var(--i-blue);color:light-dark(#ffffff,#07151c);display:grid;place-items:center;font-size:20px;flex:0 0 auto}
+.aidc-root h1,.aidc-root h2,.aidc-root h3,.aidc-root p{margin:0}
+.aidc-root h1{font-size:23px;line-height:29px;font-weight:700}
+.aidc-root h2{font-size:17px;font-weight:700}
+.aidc-root h3{font-size:14px;font-weight:700}
+.aidc-root .i-breadcrumb{color:var(--i-blue);font-size:12px;font-weight:700;margin-bottom:2px}
+.aidc-root .i-pagehead p{color:var(--i-muted);margin-top:4px;font-size:13px}
+.aidc-root .i-page-actions{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
+.aidc-root .i-label{display:grid;gap:4px;color:var(--i-muted)}
+.aidc-root .i-select,.aidc-root .i-input,.aidc-root .i-textarea{color:var(--i-ink);background:var(--i-surface);border:1px solid var(--i-border);border-radius:2px;padding:7px 9px;min-height:34px}
+.aidc-root .i-button{color:var(--i-blue-dark);background:var(--i-surface);border:1px solid var(--i-blue);border-radius:2px;padding:7px 12px}
+.aidc-root .i-button.i-primary{color:light-dark(#ffffff,#06131a);background:var(--i-blue)}
+.aidc-root .i-button.i-quiet{border-color:var(--i-border);color:var(--i-ink)}
+.aidc-root .i-contextbar{display:grid;grid-template-columns:1.2fr 1fr .9fr 1.1fr;background:var(--i-surface);border-top:1px solid var(--i-border);border-bottom:1px solid var(--i-border);margin:0 -20px 16px}
+.aidc-root .i-contextcell{min-height:58px;padding:9px 14px;border-right:1px solid var(--i-border);display:grid;align-content:center;gap:4px}
+.aidc-root .i-contextcell:last-child{border-right:0}
+.aidc-root .i-context-label{color:var(--i-muted);font-size:12px}
+.aidc-root .i-context-value{font-size:13px;font-weight:700}
+.aidc-root .i-contextbar .i-select{width:100%;padding:5px 8px;min-height:30px;font-size:13px;font-weight:700}
+.aidc-root .i-statusline{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px;padding:9px 12px;background:var(--i-surface);border:1px solid var(--i-border)}
+.aidc-root .i-statusline .i-spacer{margin-left:auto}
+.aidc-root .i-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:999px;font-size:12px;background:var(--i-nav-active);color:var(--i-blue-dark)}
+.aidc-root .i-badge.good{background:var(--i-good-soft);color:var(--i-good)}
+.aidc-root .i-badge.warn{background:var(--i-warn-soft);color:var(--i-warn)}
+.aidc-root .i-badge.bad{background:var(--i-bad-soft);color:var(--i-bad)}
+.aidc-root .i-grid{display:grid;gap:12px}
+.aidc-root .i-grid.metrics{grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:12px}
+.aidc-root .i-grid.two{grid-template-columns:minmax(0,1.65fr) minmax(280px,1fr)}
+.aidc-root .i-panel{background:var(--i-surface);border:1px solid var(--i-border);padding:16px;min-width:0}
+.aidc-root .i-kpi{min-height:112px;display:grid;align-content:center;gap:5px;border-top:3px solid var(--i-blue)}
+.aidc-root .i-kpi.good{border-top-color:var(--i-good)}
+.aidc-root .i-kpi.warn{border-top-color:var(--i-warn)}
+.aidc-root .i-kpi.bad{border-top-color:var(--i-bad)}
+.aidc-root .i-panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}
+.aidc-root .i-panel-sub{color:var(--i-muted);font-size:12px}
+.aidc-root .i-metric-label{color:var(--i-muted)}
+.aidc-root .i-metric-value{font-size:28px;font-weight:700;font-variant-numeric:tabular-nums;margin-top:3px}
+.aidc-root .i-metric-note{color:var(--i-muted);font-size:12px;margin-top:4px}
+.aidc-root .i-good-text{color:var(--i-good)}
+.aidc-root .i-warn-text{color:var(--i-warn)}
+.aidc-root .i-bad-text{color:var(--i-bad)}
+.aidc-root .i-table-wrap{overflow-x:auto}
+.aidc-root table{border-collapse:collapse;width:100%;min-width:620px}
+.aidc-root th,.aidc-root td{text-align:left;padding:9px 8px;border-bottom:1px solid var(--i-border);vertical-align:top}
+.aidc-root th{color:var(--i-muted);font-weight:500;background:var(--i-surface-2)}
+.aidc-root .i-bar{height:7px;background:var(--i-nav);overflow:hidden;margin-top:6px}
+.aidc-root .i-bar>span{display:block;height:100%;background:var(--i-blue)}
+.aidc-root .i-bar.good>span{background:var(--i-good)}
+.aidc-root .i-bar.warn>span{background:var(--i-warn)}
+.aidc-root .i-chain{display:grid;grid-template-columns:repeat(6,minmax(92px,1fr));gap:8px;align-items:stretch}
+.aidc-root .i-chain-node{position:relative;padding:11px 9px;background:var(--i-surface-2);border-top:3px solid var(--i-good);min-height:78px}
+.aidc-root .i-chain-node.warn{border-top-color:var(--i-warn);background:var(--i-warn-soft)}
+.aidc-root .i-chain-node.bad{border-top-color:var(--i-bad);background:var(--i-bad-soft)}
+.aidc-root .i-chain-node strong{display:block;font-weight:500}
+.aidc-root .i-chain-node span{color:var(--i-muted);font-size:12px;display:block;margin-top:5px}
+.aidc-root .i-ai{margin-top:12px;background:linear-gradient(120deg,var(--i-purple-soft),var(--i-nav-active));border:1px solid var(--i-purple);border-left:5px solid var(--i-purple);padding:14px 15px}
+.aidc-root .i-ai-head{display:flex;align-items:center;gap:9px;color:var(--i-purple);margin-bottom:8px}
+.aidc-root .i-ai-head strong{font-size:15px;font-weight:500}
+.aidc-root .i-ai p{color:var(--i-ink)}
+.aidc-root .i-ai ul{margin:7px 0 0 20px;padding:0}
+.aidc-root .i-ai-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:11px}
+.aidc-root .i-prompt{display:none;margin-top:10px}
+.aidc-root .i-prompt.is-open{display:grid;gap:8px}
+.aidc-root .i-prompt textarea{width:100%;min-height:86px;resize:vertical}
+.aidc-root .i-schematic{min-height:250px;background:var(--i-surface-2);padding:18px;display:grid;align-content:center;gap:18px}
+.aidc-root .i-flow-row{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;position:relative}
+.aidc-root .i-flow-row::before{content:"";position:absolute;top:25px;left:7%;right:7%;height:2px;background:var(--i-blue)}
+.aidc-root .i-flow-unit{z-index:1;text-align:center}
+.aidc-root .i-flow-dot{width:50px;height:50px;margin:0 auto 7px;border-radius:50%;background:var(--i-surface);border:3px solid var(--i-blue);display:grid;place-items:center;font-weight:500}
+.aidc-root .i-flow-unit.warn .i-flow-dot{border-color:var(--i-warn);background:var(--i-warn-soft)}
+.aidc-root .i-gauge{height:13px;background:var(--i-nav);overflow:hidden;margin:9px 0 5px}
+.aidc-root .i-gauge span{display:block;height:100%;background:var(--i-good)}
+.aidc-root .i-gauge.warn span{background:var(--i-warn)}
+.aidc-root .i-event{display:grid;grid-template-columns:76px 14px 1fr;gap:8px;padding:7px 0}
+.aidc-root .i-event-line{position:relative}
+.aidc-root .i-event-line::before{content:"";position:absolute;left:5px;top:0;bottom:-15px;width:2px;background:var(--i-border)}
+.aidc-root .i-event-line::after{content:"";position:absolute;left:1px;top:7px;width:10px;height:10px;border-radius:50%;background:var(--i-blue)}
+.aidc-root .i-camera-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
+.aidc-root .i-camera{aspect-ratio:16/9;background:linear-gradient(145deg,var(--i-nav),var(--i-surface-2));display:grid;place-items:center;color:var(--i-muted);position:relative;overflow:hidden}
+.aidc-root .i-camera::before{content:"";width:58%;height:45%;border:2px solid var(--i-border);transform:perspective(80px) rotateX(8deg)}
+.aidc-root .i-camera span{position:absolute;left:8px;bottom:6px;background:var(--i-surface);padding:2px 6px;font-size:12px}
+.aidc-root .i-rack-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:7px}
+.aidc-root .i-rack{min-height:63px;background:var(--i-good-soft);border-top:3px solid var(--i-good);padding:6px;text-align:center}
+.aidc-root .i-rack.warn{background:var(--i-warn-soft);border-top-color:var(--i-warn)}
+.aidc-root .i-range{width:100%;accent-color:var(--i-blue)}
+.aidc-root .i-chart{width:100%;min-height:230px;color:var(--i-muted)}
+.aidc-root .i-chart text{fill:var(--i-muted);font-size:12px}
+.aidc-root .i-chart .gridline{stroke:var(--i-border);stroke-width:1}
+.aidc-root .i-chart .series-blue{fill:none;stroke:var(--i-blue);stroke-width:3}
+.aidc-root .i-chart .series-purple{fill:none;stroke:var(--i-purple);stroke-width:3}
+.aidc-root .i-chart .series-warn{fill:none;stroke:var(--i-warn);stroke-width:3;stroke-dasharray:5 4}
+.aidc-root .i-legend{display:flex;gap:16px;flex-wrap:wrap;color:var(--i-muted);font-size:12px}
+.aidc-root .i-key{display:inline-block;width:18px;height:3px;vertical-align:middle;margin-right:5px;background:var(--i-blue)}
+.aidc-root .i-key.purple{background:var(--i-purple)}
+.aidc-root .i-key.warn{background:var(--i-warn)}
+.aidc-root .i-empty-note{color:var(--i-muted);padding:14px 0}
+@media (min-width:1440px){
+  .aidc-root .i-grid.metrics{grid-template-columns:repeat(4,minmax(0,1fr))}
+  .aidc-root .i-grid.two{grid-template-columns:minmax(0,1.65fr) minmax(420px,1fr)}
+}
+@media (max-width:900px){
+  .aidc-root .i-contextbar{grid-template-columns:1fr 1fr}
+  .aidc-root .i-grid.two{grid-template-columns:1fr}
+  .aidc-root .i-chain{grid-template-columns:repeat(3,minmax(92px,1fr))}
+}
+@media (max-width:560px){
+  .aidc-root .i-pagehead{flex-wrap:wrap}
+  .aidc-root .i-page-actions{margin-left:0;width:100%;justify-content:flex-start}
+  .aidc-root .i-page-actions label{width:100%}
+  .aidc-root .i-contextcell:nth-child(2){border-right:0}
+  .aidc-root .i-contextcell:nth-child(-n+2){border-bottom:1px solid var(--i-border)}
+  .aidc-root .i-grid.metrics,.aidc-root .i-grid.three{grid-template-columns:1fr}
+  .aidc-root .i-chain{grid-template-columns:repeat(2,1fr)}
+  .aidc-root .i-rack-grid{grid-template-columns:repeat(3,1fr)}
+  .aidc-root .i-camera-grid{grid-template-columns:1fr}
+}
+`;
+
+export function AidcPanel({ pageKey }: { pageKey: AidcKey }) {
+  const [stationKey, setStationKey] = useState<StationKey>("core");
+  const s = stations[stationKey];
+  const shared = { s: { ...s, key: stationKey, setStationKey }, stationKey, setStationKey };
+  return (
+    <div className="aidc-root">
+      <style>{AIDC_CSS}</style>
+      {pageKey === "aidc-chain" && <ChainPage s={shared.s} stationKey={stationKey} setStationKey={setStationKey} />}
+      {pageKey === "aidc-quality" && <QualityPage s={shared.s} />}
+      {pageKey === "aidc-patrol" && <PatrolPage s={shared.s} />}
+      {pageKey === "aidc-redundancy" && <RedundancyPage s={shared.s} stationKey={stationKey} setStationKey={setStationKey} />}
+      {pageKey === "aidc-liquid" && <LiquidPage stationKey={stationKey} setStationKey={setStationKey} />}
+      {pageKey === "aidc-synergy" && <SynergyPage s={shared.s} stationKey={stationKey} setStationKey={setStationKey} />}
+      {pageKey === "aidc-security" && <SecurityPage stationKey={stationKey} setStationKey={setStationKey} />}
+      {pageKey === "aidc-simulation" && <SimulationPage s={shared.s} stationKey={stationKey} setStationKey={setStationKey} />}
+    </div>
+  );
+}
index ae901af81983041b4e52717e12fb126429e65d8f..2372aca311e0e9af16d2c135d0f756457dbcf1e5 100644 (file)
@@ -1,5 +1,6 @@
 import { useEffect, useMemo, useState, type CSSProperties } from "react";
 import { analyzeChat, analyzeModule, analyzeSummary, loadPeriodComparison, type PeriodComparisonResponse } from "./api";
+import { AidcPanel, AidcIcon, aidcModules, type AidcKey } from "./Aidc";
 
 type ScenarioKey = "daily" | "qa" | "period" | "benchmark" | "maintenance" | "health" | "prediction" | "alarm" | "efficiency" | "capacity" | "strategy" | "emergency";
 type DesignSectionKey = ScenarioKey | "architecture";
@@ -525,7 +526,7 @@ function MiniBars({values=[38,42,45,51,57,64,78,86],tone="violet"}:{values?:numb
 }
 
 export default function App() {
-  const [active,setActive] = useState<SkillKey>("daily");
+  const [active,setActive] = useState<SkillKey | AidcKey>("daily");
   const [navCollapsed,setNavCollapsed] = useState(false);
   const [mobileMenuOpen,setMobileMenuOpen] = useState(false);
   // 手机端切换场景后自动关闭菜单
@@ -697,6 +698,10 @@ export default function App() {
           </div>
           {(["场站运营","运营分析","风险与策略"] as const).map(section=><div className="scenario-menu-group" key={section}>
             <div className="sidebar-section-label">{section}</div>
+            {section==="场站运营" && <div className="aidc-subgroup">
+              <div className="sidebar-section-label aidc-label"><AidcIcon style={{width:15,height:15,color:"#0a8fd3"}}/><span>AIDC</span></div>
+              {aidcModules.map(m=><button key={m.key} className={active===m.key?"active":""} onClick={()=>setActive(m.key)}><span className="scenario-menu-icon aidc-tone" style={{background:"#0a8fd3",color:"#fff"}}>{m.mark}</span><span>{m.name}</span></button>)}
+            </div>}
             {skills.filter(skill=>skill.section===section).map(skill=><button key={skill.key} className={active===skill.key?"active":""} onClick={()=>setActive(skill.key)}><span className={`scenario-menu-icon ${skill.tone}`}>{skill.icon}</span><span>{skill.title}</span></button>)}
           </div>)}
           <div className="sidebar-section-label">系统连接</div>
@@ -706,7 +711,8 @@ export default function App() {
       </aside>
 
       {active==="settings" ? <MuSettings config={muConfig} setConfig={setMuConfig} secret={muSecret} setSecret={setMuSecret} connection={connection} detail={connectionDetail} endpoints={connectionEndpoints} sentHeaders={connectionSentHeaders} onTest={()=>connectData(muConfig,muSecret)} onSave={saveMuConfig} onReset={()=>{setMuConfig(DEFAULT_MU_CONFIG);setMuSecret("");window.sessionStorage.removeItem("station-ai-mu-config");setConnectionDetail("已恢复默认配置");setConnectionEndpoints([]);setConnectionSentHeaders([])}} /> : active==="design" ?
-      <DesignNotesPage /> : <ScenarioWorkspace scenario={activeSkill} connection={connection} devices={devices} alarms={alarms.length} updatedAt={updatedAt} assetGroups={assetGroups} selectedGroup={selectedGroup} deviceDataMode={deviceDataMode} muConfig={muConfig} muSecret={muSecret} onGroupChange={changeAssetGroup} onRefresh={()=>connectData(muConfig,muSecret,selectedGroupId)} />}
+      <DesignNotesPage /> : (active as string).startsWith("aidc-") ?
+      <AidcPanel pageKey={active as AidcKey} /> : <ScenarioWorkspace scenario={activeSkill} connection={connection} devices={devices} alarms={alarms.length} updatedAt={updatedAt} assetGroups={assetGroups} selectedGroup={selectedGroup} deviceDataMode={deviceDataMode} muConfig={muConfig} muSecret={muSecret} onGroupChange={changeAssetGroup} onRefresh={()=>connectData(muConfig,muSecret,selectedGroupId)} />}
     </main>
     <footer className="site-footer">
       <span className="footer-item">StationAIDemo · 场站 AI 分析</span>