af91d843d8
- 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 标题显示企业名 - 所有页面标题在单企业选择时显示企业名 - 编译验证通过
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
/** 多企业对比工作区 — 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>
|
||
);
|
||
}
|