Day 90 · 屏 C 整合 — 配置驱动的拓扑页:组装与包装
零件齐活(骨架/数据/交互/动画),今天组装整屏:ScreenC 从占位符变成完整页面——声明式拓扑配置、TechCard 包装、图例说明、告警联动提示。今天的增量不大(又是装配日),但要处理一个真问题:拓扑配置与设备数据的对账——配置里的节点 id 必须与 store 快照的 deviceId 对得上,对不上的节点是"永远灰色的死节点"。
目录
一、ScreenC 完整组装
<!-- src/screens/ScreenC.vue -->
<script setup lang="ts">
import { ref } from "vue";
import TechCard from "../components/TechCard.vue";
import TopoCanvas from "../components/TopoCanvas.vue";
import TopoDetailPanel from "../components/TopoDetailPanel.vue";
import TopoLegend from "../components/TopoLegend.vue";
defineOptions({ name: "ScreenC" }); // KeepAlive include(Day 82 纪律)
const hoverId = ref<string | null>(null);
</script>
<template>
<section class="screen screen-c">
<div class="topo-grid">
<!-- 主体:拓扑画布(占大头) -->
<div class="topo__main">
<TechCard title="产线拓扑">
<template #extra>
<!-- 图例:状态色说明书(第三节) -->
<TopoLegend />
</template>
<TopoCanvas @hover="(id) => (hoverId = id)" />
<!-- hover 详情浮层(Day 88) -->
<TopoDetailPanel :device-id="hoverId" />
</TechCard>
</div>
<!-- 侧栏:告警联动提示(第四节) -->
<div class="topo__side">
<TechCard title="当前告警分布">
<AlarmSummary />
</TechCard>
</div>
</div>
</section>
</template>
二、拓扑配置:声明式与对账
2.1 配置现状(Day 68 迁移版)
// core/topo-config.ts
export const TOPO_CONFIG = {
nodes: [
{ id: "f1", label: "1号加热炉", x: 80, y: 120, w: 140, h: 64 },
{ id: "f2", label: "2号加热炉", x: 80, y: 240, w: 140, h: 64 },
/* … 共 20 个节点 */
],
edges: [
{ from: "f1", to: "t1" },
/* … */
],
};
2.2 对账问题
配置的 id 与快照的 deviceId 是两套来源(手工配置 vs 服务器约定)。错位后果:节点永远拿不到状态(灰色)、脉冲永不触发。今天的对账工具:
// dev-only 对账(开发期跑一次,不进生产——Day 63 模拟数据纪律的姊妹)
/**
* 配置对账:检查 TOPO_CONFIG 的节点 id 与快照 deviceId 的匹配度
* 放在 devtools console 手动调用(不写进组件)
*/
export function auditTopoConfig(snapshot: PanelSnapshot): void {
const configIds = new Set(TOPO_CONFIG.nodes.map((n) => n.id));
const dataIds = new Set(snapshot.devices.map((d) => d.deviceId));
const dead = [...configIds].filter((id) => !dataIds.has(id)); // 配了没数据
const missing = [...dataIds].filter((id) => !configIds.has(id)); // 有数据没配
console.table({
"配置节点数": configIds.size,
"快照设备数": dataIds.size,
"死节点(灰色)": dead,
"未上拓扑": missing,
});
}
记待办:拓扑配置可视化编辑(把第 7 周编辑器能力回流)——阶段 2 后期议题,本周保持 JSON 手配。
三、图例组件:状态色说明书
<!-- src/components/TopoLegend.vue -->
<script setup lang="ts">
/**
* 拓扑图例:状态色说明(展示组件——Day 74"傻组件"纪律)
* 数据驱动的增值:每种状态的实时计数(不是死图例)
*/
import { computed } from "vue";
import { useDashboardStore } from "../stores/dashboard";
const store = useDashboardStore();
const items = computed(() => {
const counts = new Map<string, number>();
for (const d of store.latest?.devices ?? []) {
counts.set(d.state, (counts.get(d.state) ?? 0) + 1);
}
return [
{ state: "ok", label: "运行正常", color: "var(--state-ok)", count: counts.get("ok") ?? 0 },
{ state: "warn", label: "温度偏高", color: "var(--state-warn)", count: counts.get("warn") ?? 0 },
{ state: "danger", label: "危险告警", color: "var(--state-danger)", count: counts.get("danger") ?? 0 },
{ state: "offline", label: "离线", color: "var(--text-dim)", count: counts.get("offline") ?? 0 },
];
});
</script>
<template>
<div class="topo-legend">
<span v-for="i in items" :key="i.state" class="topo-legend__item">
<i class="topo-legend__dot" :style="{ background: i.color }" />
{{ i.label }} {{ i.count }}
</span>
</div>
</template>
图例为什么放 #extra 插槽:状态色是"读图钥匙",必须与画布同视野——TechCard 的 extra 位(Day 74)正好是右上角的钥匙孔。
四、告警联动:拓扑页的告警提示
4.1 侧栏告警摘要
<!-- src/components/AlarmSummary.vue(屏 C 侧栏) -->
<script setup lang="ts">
/**
* 告警分布摘要:按车间/产线聚合(展示组件)
* 与屏 A 滚动条、屏 B 列表同源(store.alarms)——三种视图一份状态
*/
import { computed } from "vue";
import { useDashboardStore } from "../stores/dashboard";
const store = useDashboardStore();
const byLevel = computed(() => {
const g = { danger: 0, warn: 0 } as Record<string, number>;
for (const a of store.activeAlarms) g[a.level] = (g[a.level] ?? 0) + 1;
return g;
});
// 告警涉及的设备(与拓扑 dangerIds 天然一致——同一数据源)
const affectedDevices = computed(() =>
[...new Set(store.activeAlarms.map((a) => a.deviceId))]
);
</script>
<template>
<div class="alarm-summary">
<div class="alarm-summary__row alarm-summary__row--danger">
<span>危险</span><span class="num">{{ byLevel.danger }}</span>
</div>
<div class="alarm-summary__row alarm-summary__row--warn">
<span>警告</span><span class="num">{{ byLevel.warn }}</span>
</div>
<div class="alarm-summary__note">
涉及设备 {{ affectedDevices.length }} 台(拓扑页红色脉冲节点)
</div>
</div>
</template>
4.2 三屏告警视图的全景(架构验证点)
同一份 store.alarms:
屏 A:滚动条(单条轮换)
屏 B:完整列表(可确认)
屏 C:分布摘要 + 脉冲节点(空间定位)
全局:DangerDialog 弹窗(Day 82 Teleport)
四种视图、一份状态、零同步代码——这是第 2 个月"单连接纪律"到第 3 个月"单一状态源"的完整演进。面试聊状态管理时,这就是你的架构叙事。
五、常见坑点
坑 1:配置对账的"灰色死节点"没人发现
死节点混在正常节点里(都画着,只是不变色)——肉眼巡检很难发现。auditTopoConfig 的 console.table 是唯一可靠手段;上线前跑一次对账列入部署清单(Week 14 会再遇到)。
坑 2:详情面板被 TechCard 的 overflow 裁剪
TechCard 的 body 若有 overflow: hidden(部分卡片样式),浮层面板会被切。修复:屏 C 的卡片覆写 body overflow(:deep() 或全局类),或面板 Teleport 到卡片外——本周选覆写(简单)。
坑 3:图例计数与拓扑脉冲不一致
图例读 store.latest,脉冲读 renderer 的 dangerIds(Day 87 update 链)——两边都来自 store,但时序上可能差一拍(watch 触发顺序)。这不是 bug(下一个快照对齐),但若测试时发现"图例显示 1 个 danger、脉冲还没亮"——等 2s 再下结论,别急着"修"。
坑 4:告警确认后摘要没减
AlarmSummary 读 activeAlarms(getters)——ackAlarm 后应该立即减。若没减,检查 ack 动作是否绕过了 actions(Day 80 坑 2 的直接改 state)。
六、自测挑战
T1 · 屏 C 组装(60 分钟)
完成第一至四节:整屏布局、图例计数实时、侧栏摘要与脉冲节点一致(允许 2s 时差)、hover 详情全链路顺畅。
T2 · 对账演练(20 分钟)
故意在配置里加一个不存在的设备 id,用 auditTopoConfig 抓出死节点;再删掉服务器一个设备模拟"未上拓扑",观察 console.table 的报告。对账工具的价值在故意犯错时最直观。
T3 · 三屏告警视图一致性(40 分钟)
Devtools 造 1 danger + 2 warn:验证屏 A 滚动条、屏 B 列表、屏 C 摘要与脉冲、全局弹窗——四视图同源同帧(各屏激活检查)。录屏存档(架构叙事素材)。
七、总结
| 环节 | 要点 |
|---|---|
| 组装 | 零件装配日:配置 + 画布 + 图例 + 摘要 |
| 对账 | 配置 id × 数据 deviceId——auditTopoConfig 上线必跑 |
| 图例 | 状态色说明书放 extra 位;计数是数据驱动增值 |
| 告警全景 | 四视图一份状态零同步——单一状态源的架构红利 |
| 溢出 | 浮层 vs 卡片裁剪:覆写或 Teleport |
屏 C 成形。明天 BOSS 战:屏 C 终验收 + 三屏齐活的整屏回归。