|
|
@@ -472,7 +472,7 @@ function CoverageFlowBoard({ data }: { data: any }) {
|
|
|
|
|
|
// ─── 关系列卡片 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
-function RelationCard({ type, item, activeId, shouldScrollIntoView = false, selectedLeafNames, directMatch = false, dimmed = false, showAllSourceTags = false, metrics, onSingleClick, onSourceNodeClick }: {
|
|
|
+function RelationCard({ type, item, activeId, shouldScrollIntoView = false, selectedLeafNames, directMatch = false, dimmed = false, showAllSourceTags = false, metrics, reqPlanBData, onSingleClick, onSourceNodeClick }: {
|
|
|
type: string;
|
|
|
item: any;
|
|
|
activeId: string | null;
|
|
|
@@ -482,6 +482,7 @@ function RelationCard({ type, item, activeId, shouldScrollIntoView = false, sele
|
|
|
dimmed?: boolean;
|
|
|
showAllSourceTags?: boolean;
|
|
|
metrics?: { nodeCount: number; reqCount: number; procCount: number; capCount: number; toolCount: number };
|
|
|
+ reqPlanBData?: any;
|
|
|
onSingleClick: (nodeId: string) => void;
|
|
|
onSourceNodeClick?: (nodeName: string) => void;
|
|
|
}) {
|
|
|
@@ -628,6 +629,15 @@ function RelationCard({ type, item, activeId, shouldScrollIntoView = false, sele
|
|
|
// ─── 详情抽屉内容 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap, selectedReqId }: { type: string; data: any; dbData: any; onOpenPost: (postId: string, post: any) => void; nodePostsMap?: Record<string, string[]>; selectedReqId?: string | null }) {
|
|
|
+ if (type === 'itemset') {
|
|
|
+ return (
|
|
|
+ <ItemsetPostsDrawer
|
|
|
+ itemset={data}
|
|
|
+ onOpenPost={onOpenPost}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
if (type === 'req') {
|
|
|
return (
|
|
|
<RequirementPostsDrawer
|
|
|
@@ -1088,68 +1098,7 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap, selectedR
|
|
|
</div>
|
|
|
</>
|
|
|
)}
|
|
|
- {type === 'itemset' && (
|
|
|
- <>
|
|
|
- <div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
|
|
- <div className="text-xs font-bold text-amber-600 mb-2">Pattern 节点组合</div>
|
|
|
- <div className="flex flex-wrap gap-1.5">
|
|
|
- {(data.leaf_names || []).map((name: string) => (
|
|
|
- <span key={name} className="px-2 py-0.5 rounded-full text-xs bg-amber-100 text-amber-800 border border-amber-200 font-medium">{name}</span>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- <div className="mt-3 flex items-center gap-3 text-[11px] text-amber-700">
|
|
|
- <span>支持度 <span className="font-bold">{data.absolute_support}</span></span>
|
|
|
- <span className="text-amber-400">·</span>
|
|
|
- <span className="font-mono text-amber-500">#{data.id}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {(() => {
|
|
|
- const filteredAssocs = selectedReqId
|
|
|
- ? (data.req_associations || []).filter((assoc: any) => assoc.req_id === selectedReqId)
|
|
|
- : (data.req_associations || []);
|
|
|
-
|
|
|
- if (filteredAssocs.length === 0) {
|
|
|
- return <div className="text-xs text-slate-400 text-center py-4">暂无关联需求数据</div>;
|
|
|
- }
|
|
|
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <div className="text-xs font-bold text-slate-500 mb-2">关联需求 ({filteredAssocs.length})</div>
|
|
|
- <div className="space-y-3">
|
|
|
- {filteredAssocs.map((assoc: any) => {
|
|
|
- const trueNodes = (assoc.judgments || []).filter((j: any) => j.represents).map((j: any) => j.node);
|
|
|
- const falseNodes = (assoc.judgments || []).filter((j: any) => !j.represents).map((j: any) => j.node);
|
|
|
- return (
|
|
|
- <div key={assoc.req_id} className="bg-white border border-slate-100 rounded-xl p-3 space-y-2">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <span className="font-mono text-[10px] text-indigo-500 shrink-0">{assoc.req_id}</span>
|
|
|
- <span className="text-xs text-slate-700 leading-snug line-clamp-2">{assoc.req_text}</span>
|
|
|
- </div>
|
|
|
- {assoc.judgments?.length > 0 && (
|
|
|
- <div className="flex flex-wrap gap-1">
|
|
|
- {(assoc.judgments as any[]).map((j: any) => (
|
|
|
- <span key={j.node} title={j.reason} className={cn(
|
|
|
- "px-1.5 py-0.5 rounded-full text-[10px] border font-medium cursor-help",
|
|
|
- j.represents
|
|
|
- ? "bg-emerald-50 text-emerald-700 border-emerald-200"
|
|
|
- : "bg-slate-50 text-slate-400 border-slate-200 line-through"
|
|
|
- )}>{j.node}</span>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {trueNodes.length === 0 && falseNodes.length > 0 && (
|
|
|
- <div className="text-[10px] text-slate-400">无节点被判断为代表该需求</div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- );
|
|
|
- })}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- })()}
|
|
|
- </>
|
|
|
- )}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
@@ -1432,6 +1381,72 @@ function StrategyResourcesGrid({ resourceIds, onOpenPost }: { resourceIds: strin
|
|
|
|
|
|
// ─── 业务需求帖子聚合抽屉 ──────────────────────────────────────────────────────
|
|
|
|
|
|
+function ItemsetPostsDrawer({
|
|
|
+ itemset,
|
|
|
+ onOpenPost,
|
|
|
+}: {
|
|
|
+ itemset: any;
|
|
|
+ onOpenPost: (postId: string, post: any) => void;
|
|
|
+}) {
|
|
|
+ const [posts, setPosts] = useState<Record<string, any>>({});
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+
|
|
|
+ const postIds = itemset.post_ids || [];
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (postIds.length === 0) return;
|
|
|
+ setLoading(true);
|
|
|
+ setPosts({});
|
|
|
+ batchGetPosts(postIds)
|
|
|
+ .then(map => setPosts(map))
|
|
|
+ .catch(err => { console.error('Failed to load itemset posts:', err); })
|
|
|
+ .finally(() => setLoading(false));
|
|
|
+ }, [itemset.id, postIds]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="flex flex-col gap-3 h-full overflow-hidden min-w-0">
|
|
|
+ <div className="shrink-0 bg-amber-50 p-4 rounded-xl border border-amber-100">
|
|
|
+ <div className="text-xs font-bold text-amber-600 mb-2">Pattern 节点组合</div>
|
|
|
+ <div className="flex flex-wrap gap-1.5">
|
|
|
+ {(itemset.leaf_names || []).map((name: string) => (
|
|
|
+ <span key={name} className="px-2 py-0.5 rounded-full text-xs bg-amber-100 text-amber-800 border border-amber-200 font-medium">{name}</span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ <div className="mt-3 flex items-center gap-3 text-[11px] text-amber-700">
|
|
|
+ <span>支持度 <span className="font-bold">{itemset.absolute_support}</span></span>
|
|
|
+ <span className="text-amber-400">·</span>
|
|
|
+ <span className="font-mono text-amber-500">#{itemset.id}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex-1 min-h-0 overflow-y-auto pr-1 custom-scrollbar">
|
|
|
+ <div className="text-xs font-bold text-slate-500 mb-2">关联帖子 ({postIds.length})</div>
|
|
|
+ <div className="grid grid-cols-1 gap-3">
|
|
|
+ {loading && (
|
|
|
+ <div className="flex items-center justify-center gap-2 text-slate-400 rounded-xl border border-slate-100 bg-slate-50 min-h-[160px]">
|
|
|
+ <div className="w-4 h-4 border-2 border-indigo-200 border-t-indigo-500 rounded-full animate-spin"></div>
|
|
|
+ 加载中...
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {!loading && postIds.length === 0 && (
|
|
|
+ <div className="flex items-center justify-center text-xs text-slate-300 font-bold rounded-xl border border-slate-100 bg-slate-50 min-h-[160px]">
|
|
|
+ 该 Pattern 暂无关联帖子
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {!loading && postIds.map((pid: string) => {
|
|
|
+ const post = posts[pid];
|
|
|
+ return (
|
|
|
+ <div key={pid} className="w-full">
|
|
|
+ <PostCard postId={pid} post={post} compact onClick={() => onOpenPost(pid, post)} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function RequirementPostsDrawer({
|
|
|
requirement,
|
|
|
nodePostsMap,
|
|
|
@@ -1735,10 +1750,7 @@ function PatternColumn({
|
|
|
return (
|
|
|
<span key={name} className={cn(
|
|
|
"px-2 py-0.5 rounded-full text-xs border",
|
|
|
- isMatched && role === 'substance' ? "bg-orange-50 text-orange-700 border-orange-200 font-bold" :
|
|
|
- isMatched && role === 'form' ? "bg-sky-50 text-sky-700 border-sky-200 font-bold" :
|
|
|
- isMatched && role === 'both' ? "bg-emerald-50 text-emerald-700 border-emerald-200 font-bold" :
|
|
|
- isMatched ? "bg-slate-100 text-slate-700 border-slate-300 font-bold" :
|
|
|
+ isMatched ? "bg-orange-50 text-orange-700 border-orange-200 font-bold" :
|
|
|
"bg-slate-50 text-slate-500 border-slate-200"
|
|
|
)}>{name}</span>
|
|
|
);
|
|
|
@@ -1759,7 +1771,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
const [reqPlanBData, setReqPlanBData] = useState<any>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- fetch('/requirements_planb.json')
|
|
|
+ fetch('/requirements_planb.json?t=' + Date.now())
|
|
|
.then(res => res.json())
|
|
|
.then(data => setReqPlanBData(data))
|
|
|
.catch(console.error);
|
|
|
@@ -3339,7 +3351,12 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
const node = nameToNodeMap[nodeName];
|
|
|
if (!node) return;
|
|
|
|
|
|
- const nodePostIds = collectNodePostIds(node);
|
|
|
+ const nodePostIds: string[] = [];
|
|
|
+ (node?.elements || []).forEach((el: any) => {
|
|
|
+ (el.post_ids || []).forEach((pid: string) => {
|
|
|
+ if (pid && !nodePostIds.includes(pid)) nodePostIds.push(pid);
|
|
|
+ });
|
|
|
+ });
|
|
|
const nodePostIdSet = new Set(nodePostIds);
|
|
|
const explicitPosts = typeof sn === 'object'
|
|
|
? [
|
|
|
@@ -3776,6 +3793,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
key={item.id}
|
|
|
type={col.t}
|
|
|
item={item}
|
|
|
+ reqPlanBData={reqPlanBData}
|
|
|
metrics={
|
|
|
col.t === 'req' ? reqMetricsMap[item.id] :
|
|
|
col.t === 'proc' ? procMetricsMap[item.id] :
|