feat(hwpod): 补齐拓扑与节点接入控制台

This commit is contained in:
root
2026-07-13 04:09:31 +02:00
parent 63a5cfff4a
commit 652f1f9c6b
14 changed files with 1369 additions and 122 deletions
@@ -0,0 +1,120 @@
<!--
SPEC: PJ2026-010405 云端控制台
Implementation reference: draft-2026-07-13-p0-cloud-console.
-->
<script setup lang="ts">
import { computed } from "vue";
import type { HwpodDeviceTopologyItem } from "@/types";
const props = defineProps<{ device: HwpodDeviceTopologyItem }>();
const emit = defineEmits<{ select: [id: string] }>();
const statusLabel = computed(() => ({
available: "可用",
busy: "执行中",
offline: "节点离线",
"capability-mismatch": "能力不匹配",
"workspace-blocked": "工作区阻塞",
unprobed: "待探测"
}[props.device.status] ?? props.device.status));
function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
if (!props.device.online) return "offline";
if (kind === "workspace" && props.device.availability.ok === false) return "blocked";
const prefix = kind === "debug" ? "debug." : kind === "io" ? "io." : "";
if (prefix && props.device.missingCapabilities.some((capability) => capability.startsWith(prefix))) return "mismatch";
if (props.device.busy) return "active";
return props.device.available ? "ready" : "pending";
}
</script>
<template>
<article class="pcb-card" :data-status="device.status" :data-testid="`hwpod-device-${device.hwpodId}`">
<header class="pcb-card__header">
<div>
<span class="pcb-card__eyebrow">{{ device.nodeName }}</span>
<h2>{{ device.name }}</h2>
</div>
<span class="pcb-status" :data-status="device.status"><i aria-hidden="true" />{{ statusLabel }}</span>
</header>
<svg class="pcb-map" viewBox="0 0 520 220" role="img" :aria-label="`${device.name} HWPOD 四要素状态图`">
<defs>
<pattern :id="`grid-${device.hwpodId}`" width="16" height="16" patternUnits="userSpaceOnUse">
<path d="M 16 0 L 0 0 0 16" class="pcb-grid-line" />
</pattern>
</defs>
<rect x="8" y="8" width="504" height="204" rx="20" class="pcb-board" />
<rect x="8" y="8" width="504" height="204" rx="20" :fill="`url(#grid-${device.hwpodId})`" />
<path d="M260 110 H128 V56 H84" class="pcb-trace" :data-state="linkState('target')" />
<path d="M260 110 H128 V164 H84" class="pcb-trace" :data-state="linkState('workspace')" />
<path d="M260 110 H392 V56 H436" class="pcb-trace" :data-state="linkState('debug')" />
<path d="M260 110 H392 V164 H436" class="pcb-trace" :data-state="linkState('io')" />
<rect x="210" y="76" width="100" height="68" rx="12" class="pcb-chip" />
<text x="260" y="104" text-anchor="middle" class="pcb-chip-label">HWPOD</text>
<text x="260" y="124" text-anchor="middle" class="pcb-chip-id">{{ device.hwpodId.slice(0, 18) }}</text>
<g class="pcb-pad" :data-state="linkState('target')"><circle cx="66" cy="56" r="20" /><text x="66" y="61" text-anchor="middle">TGT</text></g>
<g class="pcb-pad" :data-state="linkState('workspace')"><circle cx="66" cy="164" r="20" /><text x="66" y="169" text-anchor="middle">WS</text></g>
<g class="pcb-pad" :data-state="linkState('debug')"><circle cx="454" cy="56" r="20" /><text x="454" y="61" text-anchor="middle">DBG</text></g>
<g class="pcb-pad" :data-state="linkState('io')"><circle cx="454" cy="164" r="20" /><text x="454" y="169" text-anchor="middle">IO</text></g>
</svg>
<dl class="element-grid">
<div><dt>目标板</dt><dd>{{ device.elements.target.label }}</dd><small>{{ device.elements.target.mcu || '未声明 MCU' }}</small></div>
<div><dt>工作区</dt><dd>{{ device.elements.workspace.label }}</dd><small>{{ device.elements.workspace.toolchain || '未声明工具链' }}</small></div>
<div><dt>调试探头</dt><dd>{{ device.elements.debugProbe.label }}</dd><small>{{ device.elements.debugProbe.type || '未声明类型' }}</small></div>
<div><dt>IO 探头</dt><dd>{{ device.elements.ioProbe.label }}</dd><small>{{ device.elements.ioProbe.kind }}</small></div>
</dl>
<p v-if="device.blockers[0]" class="pcb-blocker"><strong>{{ device.blockers[0].code }}</strong>{{ device.blockers[0].summary }}</p>
<footer>
<span>{{ device.declaredCapabilities.length }} 项声明能力</span>
<span v-if="device.operation">{{ device.operation.status }} · {{ device.operation.opCount }} ops</span>
<button type="button" @click="emit('select', device.hwpodId)">查看详情</button>
</footer>
</article>
</template>
<style scoped>
.pcb-card { min-width: 0; border: 1px solid #cbd2cf; border-top: 3px solid #2e6b67; border-radius: 12px; background: #fbfaf6; box-shadow: 0 12px 28px rgb(20 34 32 / 8%); overflow: hidden; }
.pcb-card[data-status="busy"] { border-top-color: #b86d1d; }
.pcb-card[data-status="offline"], .pcb-card[data-status="workspace-blocked"] { border-top-color: #8d453b; }
.pcb-card[data-status="capability-mismatch"] { border-top-color: #8a6b16; }
.pcb-card__header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; padding: 18px 20px 12px; }
.pcb-card__header h2 { margin: 3px 0 0; color: #152421; font-size: 1.08rem; letter-spacing: .01em; overflow-wrap: anywhere; }
.pcb-card__eyebrow { color: #687572; font: 600 .72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
.pcb-status { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; padding: 5px 9px; border: 1px solid #b7c6c1; border-radius: 999px; color: #244943; background: #eef7f2; font-size: .76rem; font-weight: 700; }
.pcb-status i { width: 7px; height: 7px; border-radius: 50%; background: currentcolor; }
.pcb-status[data-status="busy"] { color: #8b4c0a; background: #fff4df; border-color: #e6bd80; }
.pcb-status[data-status="offline"], .pcb-status[data-status="workspace-blocked"] { color: #7b332c; background: #fff0ed; border-color: #ddb2ac; }
.pcb-status[data-status="capability-mismatch"] { color: #6f560b; background: #fff8d8; border-color: #d8c376; }
.pcb-map { display: block; width: calc(100% - 28px); max-height: 220px; margin: 0 14px; }
.pcb-board { fill: #142c28; stroke: #31514a; stroke-width: 2; }
.pcb-grid-line { fill: none; stroke: rgb(137 184 168 / 9%); stroke-width: 1; }
.pcb-trace { fill: none; stroke: #789087; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; }
.pcb-trace[data-state="ready"] { stroke: #50c5a4; }
.pcb-trace[data-state="active"] { stroke: #efad51; stroke-dasharray: 8 8; animation: trace-flow .75s linear infinite; }
.pcb-trace[data-state="mismatch"] { stroke: #e5ca54; stroke-dasharray: 5 6; }
.pcb-trace[data-state="blocked"], .pcb-trace[data-state="offline"] { stroke: #bf675d; }
.pcb-chip { fill: #0d1917; stroke: #7ea398; stroke-width: 2; }
.pcb-chip-label { fill: #eef9f4; font: 700 15px/1 ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .12em; }
.pcb-chip-id { fill: #91aaa2; font: 10px/1 ui-monospace, SFMono-Regular, Consolas, monospace; }
.pcb-pad circle { fill: #213d37; stroke: #789087; stroke-width: 3; }
.pcb-pad text { fill: #d7e5df; font: 700 10px/1 ui-monospace, SFMono-Regular, Consolas, monospace; }
.pcb-pad[data-state="ready"] circle { stroke: #50c5a4; }
.pcb-pad[data-state="active"] circle { stroke: #efad51; }
.pcb-pad[data-state="mismatch"] circle { stroke: #e5ca54; }
.pcb-pad[data-state="blocked"] circle, .pcb-pad[data-state="offline"] circle { stroke: #bf675d; }
.element-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 12px 18px; border: 1px solid #d6dcda; background: #d6dcda; }
.element-grid div { min-width: 0; padding: 10px 12px; background: #f7f6f1; }
.element-grid dt { color: #73807c; font-size: .67rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.element-grid dd { margin: 4px 0 2px; color: #172b27; font-size: .84rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.element-grid small { display: block; color: #6b7673; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcb-blocker { margin: 12px 18px; padding: 10px 12px; border-left: 3px solid #aa5d37; background: #fff4e9; color: #5f3a29; font-size: .78rem; line-height: 1.45; }
.pcb-blocker strong { display: block; margin-bottom: 3px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
footer { display: flex; align-items: center; gap: 12px; padding: 12px 18px 16px; color: #697571; font-size: .74rem; }
footer button { margin-left: auto; border: 1px solid #446c63; border-radius: 7px; background: #183d36; color: #f4fbf8; padding: 7px 11px; font-weight: 700; cursor: pointer; }
@keyframes trace-flow { to { stroke-dashoffset: -16; } }
@media (prefers-reduced-motion: reduce) { .pcb-trace { animation: none !important; } }
@media (max-width: 560px) { .element-grid { grid-template-columns: 1fr; } .pcb-card__header { padding-inline: 14px; } footer { align-items: flex-start; flex-wrap: wrap; } }
</style>