From d95f0d002d51a7728b57b0a7936d56aaeb89dcaa Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:23:53 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=92=AD=E6=8A=A5=E6=9D=BF?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=AF=A6=E6=83=85=E5=B1=95=E7=A4=BA=E4=B8=8A?= =?UTF-8?q?=E6=B8=B8=E8=8A=82=E7=82=B9=E4=BF=A1=E6=81=AF=E4=B8=8E=E9=94=9A?= =?UTF-8?q?=E5=AE=9A=E8=B5=84=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上游/下游节点从只显示 id 改为悬停弹出节点名片(类型/状态/来源/时间/摘要/payload); 展开时顺带展示该节点锚定的资产(类型标签 + 可辨识文本)。 服务端 /exploration/nodes 新增 assets 字段(节点id→资产),读 exploration_anchors 并复用发现列表的资产打标逻辑;同页节点互引时用本页 items 兜底 refs 查找表。 --- server/server.go | 37 +++++- .../tasks/detail/_tabs/broadcast-tab.tsx | 112 ++++++++++++++++-- web/src/lib/api.ts | 1 + web/src/lib/mock/data.ts | 90 +++++++++++--- web/src/lib/mock/handler.ts | 8 +- web/src/lib/types.ts | 2 + 6 files changed, 214 insertions(+), 36 deletions(-) diff --git a/server/server.go b/server/server.go index c52eba5..6043523 100644 --- a/server/server.go +++ b/server/server.go @@ -2542,15 +2542,40 @@ func (s *Server) explorationNodes(w http.ResponseWriter, r *http.Request) { refs[i64s(n.ID)] = taskNodeDTO(n) } writeJSON(w, 200, map[string]any{ - "items": taskNodeDTOs(nodes), - "total": total, - "page": page, - "size": size, - "edges": edgeDTOs(edges), - "refs": refs, + "items": taskNodeDTOs(nodes), + "total": total, + "page": page, + "size": size, + "edges": edgeDTOs(edges), + "refs": refs, + "assets": s.nodeAnchoredAssets(t, append(ids, neighbourIDs...)), }) } +// nodeAnchoredAssets resolves the exploration_anchors of the given nodes into +// display-ready asset labels, keyed by node id. Anchors are provenance decoration +// for the 播报板 — a failure here must not cost the caller its page, so errors are +// logged and degrade to "no assets". +func (s *Server) nodeAnchoredAssets(t *Task, nodeIDs []int64) map[string][]FindingAssetDTO { + out := map[string][]FindingAssetDTO{} + anchors, err := t.Store.NodeAssets(nodeIDs) + if err != nil { + log.Printf("[broadcast] node assets: %v", err) + return out + } + var flat []int64 + for _, ids := range anchors { + flat = append(flat, ids...) + } + assets := s.resolveAssetIDs(flat) + for nodeID, ids := range anchors { + if dtos := findingAssetDTOs(ids, assets); len(dtos) > 0 { + out[i64s(nodeID)] = dtos + } + } + return out +} + // csvValues splits a comma-separated query parameter, dropping empty entries. func csvValues(s string) []string { out := []string{} diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx index 966174c..8b9fd12 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx @@ -24,12 +24,14 @@ import { import { StatusBadge } from "@/components/status-badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { api } from "@/lib/api"; import { type Tone, toneClasses, toneDot } from "@/lib/status"; -import type { Edge, ExploreKind, TaskNode } from "@/lib/types"; +import { taskAssetTypeLabel } from "@/lib/task-assets"; +import type { Edge, ExploreKind, FindingAsset, NewAssetType, TaskNode } from "@/lib/types"; import { cn } from "@/lib/utils"; const PAGE_SIZES = [20, 50, 100]; @@ -207,15 +209,79 @@ function KindChip({ kind }: { kind: string }) { ); } +// 节点锚定的资产:类型标签 + 可辨识文本。数据随播报页一起下发(node id → 资产), +// 展开时直接展示,不额外请求。 +function AssetList({ assets, dense = false }: { assets: FindingAsset[]; dense?: boolean }) { + if (assets.length === 0) return null; + return ( +
{a.label}
+ #{node.id}
+ {summary || "(无摘要)"}
+
+ {prettyPayload(node.payload)}
+
+
- #{node.id}
-
+ #{node.id}