Files
AIPortPilot/frontend/src/app/(investor)/compare/page.tsx
T
selfrelease af91d843d8 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 标题显示企业名
- 所有页面标题在单企业选择时显示企业名
- 编译验证通过
2026-07-20 08:40:54 +08:00

165 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 多企业对比工作区 — 2-5 家企业并排对比,数据来自后端 API。 */
"use client";
import { GitCompareArrows } from "lucide-react";
import { CompareMode, type CompareColumn } from "@/components/workbench/CompareMode";
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 { companies } = useCompanyScope();
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [scores, setScores] = useState<HealthScore[]>([]);
const [riskCounts, setRiskCounts] = useState<Record<string, number>>({});
const [isLoading, setIsLoading] = useState(false);
// 默认选中前 3 家企业
const allCompanies = useMemo(() => companies, [companies]);
const effectiveSelected = selectedIds.length > 0
? selectedIds
: allCompanies.slice(0, 3).map((c) => c.id);
// 加载选中企业的健康度和风险数据
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 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>
),
};
});
function toggleCompany(id: string) {
setSelectedIds((prev) => {
if (prev.includes(id)) {
return prev.filter((n) => n !== id);
}
if (prev.length >= 5) return prev;
return [...prev, id];
});
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<GitCompareArrows className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold"></h1>
</div>
{/* 企业选择器 */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">2-5 </span>
{allCompanies.map((c) => (
<button
key={c.id}
type="button"
onClick={() => toggleCompany(c.id)}
className={`rounded-md border px-3 py-1 text-sm transition-colors ${
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"
}`}
>
{c.name}
</button>
))}
</div>
{/* 对比卡片 */}
{isLoading ? (
<LoadingSpinner />
) : columns.length < 2 ? (
<EmptyState description="请至少选择 2 家企业进行对比" />
) : (
<CompareMode columns={columns} />
)}
</div>
);
}