【Canvas × Vue】day90-screen-c-integration

作者:mario 发布时间: 2026-09-04 阅读量:4 评论数:0

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 终验收 + 三屏齐活的整屏回归。

评论