feat(frontend): 全部页面接入后端API并同步全局企业选择

- ai-plus: Class组件→函数组件,接入 listHealthScores + listAgentExecutions API
- ooda: 硬编码→接入 risks/weak-signals/sentinels/tasks API 构建OODA各阶段
- threads: 硬编码→接入 risks/tasks/events API 组合决策线程
- today: 硬编码→接入 risks/weak-signals/synergies/reports API 构建行动项
- compare: 硬编码企业名→使用全局企业列表 + listHealthScores + listRisks API
- innovation/knowledge-graph/portfolio: 加 useCompanyScope 标题显示企业名
- 所有页面标题在单企业选择时显示企业名
- 编译验证通过
This commit is contained in:
selfrelease
2026-07-20 08:40:54 +08:00
parent 006fd7de7e
commit af91d843d8
8 changed files with 654 additions and 185 deletions
+121 -42
View File
@@ -1,54 +1,127 @@
/** 多企业对比工作区 — 2-5 家企业并排对比。 */
/** 多企业对比工作区 — 2-5 家企业并排对比,数据来自后端 API。 */
"use client";
import { GitCompareArrows } from "lucide-react";
import { CompareMode, type CompareColumn } from "@/components/workbench/CompareMode";
import { useState } from "react";
import { useState, useEffect, useMemo } from "react";
import { useCompanyScope } from "@/lib/company-scope";
import { listHealthScores, type HealthScore } from "@/lib/dashboard";
import { listRisks } from "@/lib/risks";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
/** 企业对比数据。 */
interface CompanyCompareData {
healthScore: number;
runway: number;
highRisks: number;
trend: string | null;
aiCommercial: number | null;
aiCost: number | null;
}
/** 多企业对比页面。 */
export default function ComparePage() {
const [selected, setSelected] = useState<string[]>(["智链科技", "云栈数据", "深瞳智能"]);
const { companies } = useCompanyScope();
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [scores, setScores] = useState<HealthScore[]>([]);
const [riskCounts, setRiskCounts] = useState<Record<string, number>>({});
const [isLoading, setIsLoading] = useState(false);
const allCompanies = ["智链科技", "云栈数据", "深瞳智能", "量子芯微", "光合生物"];
// 默认选中前 3 家企业
const allCompanies = useMemo(() => companies, [companies]);
const effectiveSelected = selectedIds.length > 0
? selectedIds
: allCompanies.slice(0, 3).map((c) => c.id);
const companyData: Record<string, { healthScore: number; runway: number; highRisks: number }> = {
"智链科技": { healthScore: 78.5, runway: 18, highRisks: 0 },
"云栈数据": { healthScore: 62.0, runway: 15, highRisks: 1 },
"深瞳智能": { healthScore: 85.5, runway: 24, highRisks: 0 },
"量子芯微": { healthScore: 48.0, runway: 7, highRisks: 2 },
"光合生物": { healthScore: 68.5, runway: 20, highRisks: 0 },
};
// 加载选中企业的健康度和风险数据
async function loadData(companyIds: string[]) {
if (companyIds.length < 2) return;
setIsLoading(true);
try {
// 并行加载健康度(全量)和各企业风险数
const [scoresResp, ...riskResps] = await Promise.all([
listHealthScores(),
...companyIds.map((id) =>
listRisks({ company_id: id, page_size: 1 })
),
]);
const allScores = (scoresResp.data as HealthScore[]) ?? [];
setScores(allScores);
const columns: CompareColumn[] = selected.map((name) => ({
id: name,
name,
...companyData[name],
content: (
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">800</span>
const counts: Record<string, number> = {};
riskResps.forEach((resp, i) => {
const id = companyIds[i];
counts[id] = resp.data?.total ?? 0;
});
setRiskCounts(counts);
} catch {
// ignore
} finally {
setIsLoading(false);
}
}
// 当选中企业变化时加载数据
useEffect(() => {
if (effectiveSelected.length >= 2) {
loadData(effectiveSelected);
}
}, [effectiveSelected.join(",")]);
// 构建对比列
const columns: CompareColumn[] = effectiveSelected.map((id) => {
const company = allCompanies.find((c) => c.id === id);
const score = scores.find((s) => s.company_id === id);
const data: CompanyCompareData = {
healthScore: score?.total_score ?? 0,
runway: 0,
highRisks: riskCounts[id] ?? 0,
trend: score?.trend ?? null,
aiCommercial: score?.ai_commercial_score ?? null,
aiCost: score?.ai_cost_score ?? null,
};
return {
id,
name: company?.name ?? id.slice(0, 8),
healthScore: data.healthScore,
runway: data.runway,
highRisks: data.highRisks,
content: (
<div className="space-y-2 text-xs">
{data.aiCommercial != null && (
<div className="flex justify-between">
<span className="text-muted-foreground">AI </span>
<span className="font-medium">{data.aiCommercial.toFixed(1)}</span>
</div>
)}
{data.aiCost != null && (
<div className="flex justify-between">
<span className="text-muted-foreground">AI </span>
<span className="font-medium">{data.aiCost.toFixed(1)}</span>
</div>
)}
{data.trend && (
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className={`font-medium ${data.trend === "up" ? "text-emerald-600" : data.trend === "down" ? "text-rose-600" : "text-gray-500"}`}>
{data.trend === "up" ? "↑" : data.trend === "down" ? "↓" : "→"}
</span>
</div>
)}
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium text-emerald-600">+15%</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">AI </span>
<span className="font-medium">3 PoC</span>
</div>
</div>
),
}));
),
};
});
function toggleCompany(name: string) {
setSelected((prev) => {
if (prev.includes(name)) {
return prev.filter((n) => n !== name);
function toggleCompany(id: string) {
setSelectedIds((prev) => {
if (prev.includes(id)) {
return prev.filter((n) => n !== id);
}
if (prev.length >= 5) return prev;
return [...prev, name];
return [...prev, id];
});
}
@@ -62,24 +135,30 @@ export default function ComparePage() {
{/* 企业选择器 */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">2-5 </span>
{allCompanies.map((name) => (
{allCompanies.map((c) => (
<button
key={name}
key={c.id}
type="button"
onClick={() => toggleCompany(name)}
onClick={() => toggleCompany(c.id)}
className={`rounded-md border px-3 py-1 text-sm transition-colors ${
selected.includes(name)
effectiveSelected.includes(c.id)
? "border-indigo-300 bg-indigo-50 text-indigo-600"
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
}`}
>
{name}
{c.name}
</button>
))}
</div>
{/* 对比卡片 */}
<CompareMode columns={columns} />
{isLoading ? (
<LoadingSpinner />
) : columns.length < 2 ? (
<EmptyState description="请至少选择 2 家企业进行对比" />
) : (
<CompareMode columns={columns} />
)}
</div>
);
}