|
|
@@ -1,10 +1,10 @@
|
|
|
-import { useState, useEffect, useMemo, useRef, type ReactNode, type WheelEvent } from 'react';
|
|
|
+import { useState, useEffect, useMemo, useRef, Fragment, type ReactNode, type WheelEvent } from 'react';
|
|
|
import { createPortal } from 'react-dom';
|
|
|
import { Target, Wrench, FileText, ListTree, Cpu, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
|
import { CategoryTree } from '../components/dashboard/CategoryTree';
|
|
|
import { SideDrawer } from '../components/common/SideDrawer';
|
|
|
import { cn } from '../lib/utils';
|
|
|
-import { getRequirements, getCapabilities, getTools, getKnowledge, getResource, batchGetPosts, getStrategies } from '../services/api';
|
|
|
+import { getRequirements, getCapabilities, getTools, getKnowledge, getResource, batchGetPosts, getStrategies, getDashboardSnapshot, batchGetResources } from '../services/api';
|
|
|
|
|
|
// --- Dashboard 内存级全局缓存 (避免路由切换时重复发起耗时请求) ---
|
|
|
let globalCacheLoaded = false;
|
|
|
@@ -476,9 +476,8 @@ function CoverageFlowBoard({ data }: { data: any }) {
|
|
|
const farColor = selectedIsSource ? edge.targetColor : edge.sourceColor;
|
|
|
const farValue = selectedIsSource ? edge.targetCovered : edge.sourceCovered;
|
|
|
return (
|
|
|
- <>
|
|
|
+ <Fragment key={`cross-badge-${edge.index}`}>
|
|
|
<div
|
|
|
- key={`cross-badge-start-${edge.index}`}
|
|
|
className="absolute -translate-x-1/2 -translate-y-1/2 z-50"
|
|
|
style={{ left: anchorX, top: edge.source === selectedDetailNode ? edge.sourceBadgeY : edge.targetBadgeY }}
|
|
|
>
|
|
|
@@ -490,7 +489,6 @@ function CoverageFlowBoard({ data }: { data: any }) {
|
|
|
</div>
|
|
|
</div>
|
|
|
<div
|
|
|
- key={`cross-badge-end-${edge.index}`}
|
|
|
className="absolute -translate-x-1/2 -translate-y-1/2 z-50"
|
|
|
style={{ left: farX, top: farY }}
|
|
|
>
|
|
|
@@ -501,7 +499,7 @@ function CoverageFlowBoard({ data }: { data: any }) {
|
|
|
{farValue}
|
|
|
</div>
|
|
|
</div>
|
|
|
- </>
|
|
|
+ </Fragment>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
@@ -1099,12 +1097,12 @@ function StrategyResourcesGrid({ resourceIds, onOpenPost }: { resourceIds: strin
|
|
|
|
|
|
async function loadResources() {
|
|
|
setLoading(true);
|
|
|
- const map: Record<string, any> = {};
|
|
|
-
|
|
|
- for (const rid of resourceIds) {
|
|
|
- if (!isMounted) break;
|
|
|
- try {
|
|
|
- const res = await getResource(encodeURIComponent(rid));
|
|
|
+ try {
|
|
|
+ const resources = await batchGetResources(resourceIds);
|
|
|
+ if (!isMounted) return;
|
|
|
+ const map: Record<string, any> = {};
|
|
|
+ for (const rid of resourceIds) {
|
|
|
+ const res = resources[rid];
|
|
|
if (res) {
|
|
|
map[rid] = {
|
|
|
title: res.title,
|
|
|
@@ -1117,16 +1115,13 @@ function StrategyResourcesGrid({ resourceIds, onOpenPost }: { resourceIds: strin
|
|
|
'本地 Case': res.metadata?.local_case_id,
|
|
|
}
|
|
|
};
|
|
|
- // 每次加载完立刻更新 UI,提供渐进式呈现
|
|
|
- setPosts({ ...map });
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- console.error(`Failed to fetch resource ${rid}`, e);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- if (isMounted) {
|
|
|
- setLoading(false);
|
|
|
+ if (isMounted) setPosts(map);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to batch fetch resources', e);
|
|
|
+ } finally {
|
|
|
+ if (isMounted) setLoading(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1554,35 +1549,41 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- setDashboardLoadingText('连接服务端:获取预备目录...');
|
|
|
- const treeRes = await fetch('/category_tree.json');
|
|
|
- const data = await treeRes.json();
|
|
|
- setTreeData(data);
|
|
|
- const leaves = getLeafNodes([data]);
|
|
|
+ setDashboardLoadingText('获取数据快照...');
|
|
|
|
|
|
- setDashboardLoadingText('获取底座数据 (1/4): 核心需求池...');
|
|
|
- const reqRes = await getRequirements(1000, 0);
|
|
|
+ let data: any;
|
|
|
+ let reqs: any[], caps: any[], tools: any[], procs: any[], know: any[];
|
|
|
|
|
|
- setDashboardLoadingText('获取底座数据 (2/4): 能力组合池...');
|
|
|
- const capRes = await getCapabilities(1000, 0);
|
|
|
-
|
|
|
- setDashboardLoadingText('获取底座数据 (3/4): 外部工具映射...');
|
|
|
- const toolRes = await getTools(1000, 0);
|
|
|
-
|
|
|
- setDashboardLoadingText('获取底座数据 (4/4): 顶层策略与工序集...');
|
|
|
- const procRes = await getStrategies(1000, 0);
|
|
|
-
|
|
|
- let knowRes: any = { results: [] };
|
|
|
try {
|
|
|
- // setDashboardLoadingText('底层依赖获取:知识碎片集...');
|
|
|
- knowRes = await getKnowledge(1, 1000);
|
|
|
- } catch (e) { /* optional */ }
|
|
|
-
|
|
|
- const reqs = reqRes.results || [];
|
|
|
- const caps = capRes.results || [];
|
|
|
- const tools = toolRes.results || [];
|
|
|
- const procs = procRes.strategies || [];
|
|
|
- const know = knowRes.results || [];
|
|
|
+ // 优先使用后端预计算快照(单请求,带服务端缓存)
|
|
|
+ const snapshot = await getDashboardSnapshot();
|
|
|
+ data = snapshot.tree;
|
|
|
+ reqs = snapshot.reqs || [];
|
|
|
+ caps = snapshot.caps || [];
|
|
|
+ tools = snapshot.tools || [];
|
|
|
+ procs = snapshot.procs || [];
|
|
|
+ know = snapshot.know || [];
|
|
|
+ } catch {
|
|
|
+ // 快照接口不可用时,回退到并行请求
|
|
|
+ setDashboardLoadingText('回退模式:并行获取底座数据...');
|
|
|
+ const [treeRes, reqRes, capRes, toolRes, procRes, knowRes] = await Promise.all([
|
|
|
+ fetch('/category_tree.json').then(r => r.json()),
|
|
|
+ getRequirements(1000, 0),
|
|
|
+ getCapabilities(1000, 0),
|
|
|
+ getTools(1000, 0),
|
|
|
+ getStrategies(1000, 0),
|
|
|
+ getKnowledge(1, 1000).catch(() => ({ results: [] })),
|
|
|
+ ]);
|
|
|
+ data = treeRes;
|
|
|
+ reqs = reqRes.results || [];
|
|
|
+ caps = capRes.results || [];
|
|
|
+ tools = toolRes.results || [];
|
|
|
+ procs = procRes.strategies || [];
|
|
|
+ know = knowRes.results || [];
|
|
|
+ }
|
|
|
+
|
|
|
+ setTreeData(data);
|
|
|
+ const leaves = getLeafNodes([data]);
|
|
|
setDbData({ reqs, caps, tools, know, procs });
|
|
|
|
|
|
const nameToNode: Record<string, any> = {};
|