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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user