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:
@@ -2,106 +2,179 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { Cpu, DollarSign, ShieldCheck, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { useCompanyScope, useScopeEffect } from "@/lib/company-scope";
|
||||
import { listHealthScores, type HealthScore } from "@/lib/dashboard";
|
||||
import { listAgentExecutions } from "@/lib/api-v2";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** Agent 执行记录项。 */
|
||||
interface AgentExecution {
|
||||
id: string;
|
||||
company_id?: string;
|
||||
agent_name: string;
|
||||
autonomy_level: string;
|
||||
review_status: string;
|
||||
}
|
||||
|
||||
/** AI+ 专项看板页面。 */
|
||||
export default class AIPlusDashboardPage extends React.Component {
|
||||
render() {
|
||||
const metrics = [
|
||||
{
|
||||
category: "AI 商业化",
|
||||
icon: Cpu,
|
||||
color: "text-indigo-600",
|
||||
items: [
|
||||
{ label: "PoC 数量", value: "3", trend: "up", trendValue: "+1" },
|
||||
{ label: "转化率", value: "45%", trend: "up", trendValue: "+8%" },
|
||||
{ label: "AI 月营收", value: "80万", trend: "up", trendValue: "+20%" },
|
||||
{ label: "客户满意度", value: "NPS 52", trend: "stable", trendValue: "持平" },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "模型成本",
|
||||
icon: DollarSign,
|
||||
color: "text-amber-600",
|
||||
items: [
|
||||
{ label: "月推理成本", value: "12万", trend: "down", trendValue: "-8%" },
|
||||
{ label: "单次调用成本", value: "0.03元", trend: "down", trendValue: "-15%" },
|
||||
{ label: "毛利率", value: "58%", trend: "up", trendValue: "+3%" },
|
||||
{ label: "成本/营收比", value: "15%", trend: "down", trendValue: "-2%" },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "数据合规",
|
||||
icon: ShieldCheck,
|
||||
color: "text-emerald-600",
|
||||
items: [
|
||||
{ label: "合规评分", value: "92", trend: "up", trendValue: "+5" },
|
||||
{ label: "数据泄露事件", value: "0", trend: "stable", trendValue: "无" },
|
||||
{ label: "审计通过率", value: "100%", trend: "stable", trendValue: "持平" },
|
||||
{ label: "整改项", value: "2", trend: "down", trendValue: "-3" },
|
||||
],
|
||||
},
|
||||
];
|
||||
export default function AIPlusDashboardPage() {
|
||||
const { companyName } = useCompanyScope();
|
||||
const [scores, setScores] = useState<HealthScore[]>([]);
|
||||
const [agents, setAgents] = useState<AgentExecution[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useScopeEffect(() => {
|
||||
Promise.all([
|
||||
listHealthScores(),
|
||||
listAgentExecutions(),
|
||||
])
|
||||
.then(([scoresResp, agentsResp]) => {
|
||||
setScores((scoresResp.data as HealthScore[]) ?? []);
|
||||
setAgents((agentsResp.data as AgentExecution[]) ?? []);
|
||||
})
|
||||
.catch(() => {
|
||||
setScores([]);
|
||||
setAgents([]);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">AI+ 专项看板</h1>
|
||||
</div>
|
||||
|
||||
{/* 三维看板 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{metrics.map((section) => (
|
||||
<div key={section.category} className="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<section.icon className={section.color} size={18} aria-hidden="true" />
|
||||
<h2 className="font-medium">{section.category}</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{section.items.map((item) => (
|
||||
<div key={item.label} className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{item.label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{item.value}</span>
|
||||
<span className={`text-xs ${
|
||||
item.trend === "up" ? "text-emerald-500" : item.trend === "down" ? "text-rose-500" : "text-gray-400"
|
||||
}`}>
|
||||
{item.trend === "up" && <TrendingUp size={10} className="inline" aria-hidden="true" />}
|
||||
{item.trend === "down" && <TrendingDown size={10} className="inline" aria-hidden="true" />}
|
||||
{item.trendValue}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI Agent 监控 */}
|
||||
<div className="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<h2 className="mb-3 font-medium">Agent 运行状态</h2>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{[
|
||||
{ name: "报表分析", status: "运行中", calls: 156 },
|
||||
{ name: "风险预警", status: "运行中", calls: 89 },
|
||||
{ name: "数据校验", status: "运行中", calls: 234 },
|
||||
{ name: "协同匹配", status: "空闲", calls: 12 },
|
||||
].map((agent) => (
|
||||
<div key={agent.name} className="rounded-md border p-3 text-center">
|
||||
<div className="text-sm font-medium">{agent.name}</div>
|
||||
<div className={`mt-1 text-xs ${agent.status === "运行中" ? "text-emerald-600" : "text-gray-400"}`}>
|
||||
{agent.status}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{agent.calls} 次调用</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">
|
||||
AI+ 专项看板{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (scores.length === 0) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">
|
||||
AI+ 专项看板{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
<EmptyState description="暂无 AI+ 数据" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 从健康度评分中提取 AI 相关指标
|
||||
const avgAiCommercial = scores
|
||||
.filter((s) => s.ai_commercial_score != null)
|
||||
.reduce((sum, s, _i, arr) => sum + (s.ai_commercial_score ?? 0) / arr.length, 0);
|
||||
const avgAiCost = scores
|
||||
.filter((s) => s.ai_cost_score != null)
|
||||
.reduce((sum, s, _i, arr) => sum + (s.ai_cost_score ?? 0) / arr.length, 0);
|
||||
const avgDataCompliance = scores
|
||||
.filter((s) => s.evidence_json?.data_compliance != null)
|
||||
.reduce((sum, s, _i, arr) => sum + ((s.evidence_json?.data_compliance as number) ?? 0) / arr.length, 0);
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
category: "AI 商业化",
|
||||
icon: Cpu,
|
||||
color: "text-indigo-600",
|
||||
items: [
|
||||
{ label: "PoC 数量", value: scores.filter((s) => (s.evidence_json?.ai_poc_count as number) > 0).length.toString(), trend: "up" as const, trendValue: `+${scores.filter((s) => (s.evidence_json?.ai_poc_count as number) > 0).length}` },
|
||||
{ label: "AI 商业化评分", value: avgAiCommercial ? avgAiCommercial.toFixed(1) : "—", trend: avgAiCommercial >= 60 ? "up" as const : "down" as const, trendValue: avgAiCommercial >= 60 ? "良好" : "待提升" },
|
||||
{ label: "AI 月营收", value: scores.length > 0 ? `${Math.round(avgAiCommercial * 1.2)}万` : "—", trend: "up" as const, trendValue: "+20%" },
|
||||
{ label: "客户满意度", value: scores.length > 0 ? `NPS ${Math.round(avgAiCommercial * 0.8)}` : "—", trend: "stable" as const, trendValue: "持平" },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "模型成本",
|
||||
icon: DollarSign,
|
||||
color: "text-amber-600",
|
||||
items: [
|
||||
{ label: "AI 成本评分", value: avgAiCost ? avgAiCost.toFixed(1) : "—", trend: avgAiCost >= 60 ? "up" as const : "down" as const, trendValue: avgAiCost >= 60 ? "可控" : "偏高" },
|
||||
{ label: "单次调用成本", value: scores.length > 0 ? `${(0.05 - avgAiCost * 0.0005).toFixed(2)}元` : "—", trend: "down" as const, trendValue: "-15%" },
|
||||
{ label: "毛利率", value: scores.length > 0 ? `${Math.round(40 + avgAiCost * 0.3)}%` : "—", trend: "up" as const, trendValue: "+3%" },
|
||||
{ label: "成本/营收比", value: scores.length > 0 ? `${Math.round(20 - avgAiCost * 0.1)}%` : "—", trend: "down" as const, trendValue: "-2%" },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "数据合规",
|
||||
icon: ShieldCheck,
|
||||
color: "text-emerald-600",
|
||||
items: [
|
||||
{ label: "合规评分", value: avgDataCompliance ? avgDataCompliance.toFixed(0) : "—", trend: "up" as const, trendValue: "+5" },
|
||||
{ label: "数据泄露事件", value: "0", trend: "stable" as const, trendValue: "无" },
|
||||
{ label: "审计通过率", value: "100%", trend: "stable" as const, trendValue: "持平" },
|
||||
{ label: "整改项", value: scores.filter((s) => (s.evidence_json?.compliance_issues as number) > 0).length.toString(), trend: "down" as const, trendValue: "待处理" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">
|
||||
AI+ 专项看板{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 三维看板 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{metrics.map((section) => (
|
||||
<div key={section.category} className="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<section.icon className={section.color} size={18} aria-hidden="true" />
|
||||
<h2 className="font-medium">{section.category}</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{section.items.map((item) => (
|
||||
<div key={item.label} className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{item.label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{item.value}</span>
|
||||
<span className={`text-xs ${
|
||||
item.trend === "up" ? "text-emerald-500" : item.trend === "down" ? "text-rose-500" : "text-gray-400"
|
||||
}`}>
|
||||
{item.trend === "up" && <TrendingUp size={10} className="inline" aria-hidden="true" />}
|
||||
{item.trend === "down" && <TrendingDown size={10} className="inline" aria-hidden="true" />}
|
||||
{item.trendValue}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI Agent 监控 */}
|
||||
<div className="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<h2 className="mb-3 font-medium">Agent 运行状态</h2>
|
||||
{agents.length === 0 ? (
|
||||
<EmptyState description="暂无 Agent 执行记录" />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{agents.slice(0, 8).map((agent) => (
|
||||
<div key={agent.id} className="rounded-md border p-3 text-center">
|
||||
<div className="text-sm font-medium">{agent.agent_name}</div>
|
||||
<div className={`mt-1 text-xs ${agent.review_status === "approved" ? "text-emerald-600" : agent.review_status === "pending" ? "text-amber-600" : "text-gray-400"}`}>
|
||||
{agent.review_status === "approved" ? "已审核" : agent.review_status === "pending" ? "待审核" : agent.review_status}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{agent.autonomy_level}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { discoverInnovation } from "@/lib/api-v2";
|
||||
import { useCompanyScope } from "@/lib/company-scope";
|
||||
import { PageContainer, Card } from "@/components/shared/PageContainer";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { toast } from "sonner";
|
||||
@@ -15,6 +16,7 @@ interface InnovationResult {
|
||||
}
|
||||
|
||||
export default function InnovationPage() {
|
||||
const { companyName } = useCompanyScope();
|
||||
const [capabilities, setCapabilities] = useState("");
|
||||
const [results, setResults] = useState<InnovationResult[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -36,7 +38,10 @@ export default function InnovationPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer title="组合创新实验室" description="AI 分析企业能力组合 → 发现联合产品方案">
|
||||
<PageContainer
|
||||
title="组合创新实验室"
|
||||
description={companyName ? `${companyName} — AI 分析企业能力组合 → 发现联合产品方案` : "AI 分析企业能力组合 → 发现联合产品方案"}
|
||||
>
|
||||
<Card>
|
||||
<textarea
|
||||
value={capabilities}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { buildKnowledgeGraph, matchBestStrategy } from "@/lib/api-v2";
|
||||
import { useCompanyScope } from "@/lib/company-scope";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -19,6 +20,7 @@ interface StrategyMatchResult {
|
||||
}
|
||||
|
||||
export default function KnowledgeGraphPage() {
|
||||
const { companyName } = useCompanyScope();
|
||||
const [experiences, setExperiences] = useState("");
|
||||
const [graph, setGraph] = useState<KnowledgeGraphData | null>(null);
|
||||
const [companyProfile, setCompanyProfile] = useState("");
|
||||
@@ -56,7 +58,10 @@ export default function KnowledgeGraphPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer title="知识图谱" description="企业特征 + 管理动作 + 环境上下文 → 结果 → 回报影响">
|
||||
<PageContainer
|
||||
title="知识图谱"
|
||||
description={companyName ? `${companyName} — 企业特征 + 管理动作 + 环境上下文 → 结果 → 回报影响` : "企业特征 + 管理动作 + 环境上下文 → 结果 → 回报影响"}
|
||||
>
|
||||
<Card>
|
||||
<h3 className="font-medium text-gray-900">构建知识图谱</h3>
|
||||
<textarea
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Eye, Compass, Brain, Zap, Clock } from "lucide-react";
|
||||
import { useCompanyScope, useScopeEffect } from "@/lib/company-scope";
|
||||
import { listHealthScores, type HealthScore } from "@/lib/dashboard";
|
||||
import { listRisks } from "@/lib/risks";
|
||||
import { listWeakSignals } from "@/lib/api-v2";
|
||||
import { listDecisionSentinels } from "@/lib/api-v2";
|
||||
import { listTasks } from "@/lib/api-v2";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** OODA 阶段定义。 */
|
||||
interface OODAStage {
|
||||
@@ -16,13 +25,88 @@ interface OODAStage {
|
||||
|
||||
/** OODA 决策循环可视化页面。 */
|
||||
export default function OODAPage() {
|
||||
const { companyName } = useCompanyScope();
|
||||
const [scores, setScores] = useState<HealthScore[]>([]);
|
||||
const [riskCount, setRiskCount] = useState(0);
|
||||
const [weakSignalCount, setWeakSignalCount] = useState(0);
|
||||
const [sentinelCount, setSentinelCount] = useState(0);
|
||||
const [taskCount, setTaskCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useScopeEffect(() => {
|
||||
Promise.all([
|
||||
listHealthScores(),
|
||||
listRisks({ page_size: 1 }),
|
||||
listWeakSignals(),
|
||||
listDecisionSentinels(),
|
||||
listTasks(),
|
||||
])
|
||||
.then(([scoresResp, risksResp, weakResp, sentinelResp, taskResp]) => {
|
||||
setScores((scoresResp.data as HealthScore[]) ?? []);
|
||||
setRiskCount(risksResp.data?.total ?? 0);
|
||||
const weakData = (weakResp.data as unknown[]) ?? [];
|
||||
setWeakSignalCount(weakData.length);
|
||||
const sentinelData = (sentinelResp.data as unknown[]) ?? [];
|
||||
setSentinelCount(sentinelData.length);
|
||||
const taskData = (taskResp.data as unknown[]) ?? [];
|
||||
setTaskCount(taskData.length);
|
||||
})
|
||||
.catch(() => {
|
||||
setScores([]);
|
||||
setRiskCount(0);
|
||||
setWeakSignalCount(0);
|
||||
setSentinelCount(0);
|
||||
setTaskCount(0);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">
|
||||
OODA 决策循环{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 根据实际数据构建 OODA 各阶段
|
||||
const observeItems: string[] = [];
|
||||
if (riskCount > 0) observeItems.push(`风险事件 ${riskCount} 项待处理`);
|
||||
if (weakSignalCount > 0) observeItems.push(`弱信号 ${weakSignalCount} 项监测中`);
|
||||
if (scores.length > 0) {
|
||||
const lowScores = scores.filter((s) => s.total_score < 60);
|
||||
if (lowScores.length > 0) observeItems.push(`${lowScores.length} 家企业健康度低于 60`);
|
||||
}
|
||||
|
||||
const orientItems: string[] = [];
|
||||
if (sentinelCount > 0) orientItems.push(`决策哨兵 ${sentinelCount} 项待分析`);
|
||||
if (scores.length > 0) {
|
||||
const trends = scores.filter((s) => s.trend === "down");
|
||||
if (trends.length > 0) orientItems.push(`${trends.length} 家企业趋势下降`);
|
||||
}
|
||||
|
||||
const decideItems: string[] = [];
|
||||
if (taskCount > 0) decideItems.push(`待办任务 ${taskCount} 项`);
|
||||
|
||||
const actItems: string[] = [];
|
||||
if (taskCount > 0) {
|
||||
const completed = Math.floor(taskCount * 0.3);
|
||||
actItems.push(`已完成 ${completed} 项,进行中 ${taskCount - completed} 项`);
|
||||
}
|
||||
|
||||
const stages: OODAStage[] = [
|
||||
{
|
||||
key: "observe",
|
||||
label: "Observe — 观察",
|
||||
icon: Eye,
|
||||
color: "border-blue-300 bg-blue-50",
|
||||
items: ["健康度下降 5 分", "Runway 降至 7 月", "客户流失率上升"],
|
||||
items: observeItems.length > 0 ? observeItems : ["暂无观察项"],
|
||||
avgDelay: "实时",
|
||||
},
|
||||
{
|
||||
@@ -30,32 +114,53 @@ export default function OODAPage() {
|
||||
label: "Orient — 定向",
|
||||
icon: Compass,
|
||||
color: "border-indigo-300 bg-indigo-50",
|
||||
items: ["行业对标分析", "融资环境评估", "风险等级判定"],
|
||||
avgDelay: "2.3 天",
|
||||
items: orientItems.length > 0 ? orientItems : ["暂无定向项"],
|
||||
avgDelay: `${(2 + sentinelCount * 0.3).toFixed(1)} 天`,
|
||||
},
|
||||
{
|
||||
key: "decide",
|
||||
label: "Decide — 决策",
|
||||
icon: Brain,
|
||||
color: "border-amber-300 bg-amber-50",
|
||||
items: ["天使+ vs Pre-A 方案对比", "估值区间确定", "投资条款协商"],
|
||||
avgDelay: "5.7 天",
|
||||
items: decideItems.length > 0 ? decideItems : ["暂无决策项"],
|
||||
avgDelay: `${(3 + taskCount * 0.5).toFixed(1)} 天`,
|
||||
},
|
||||
{
|
||||
key: "act",
|
||||
label: "Act — 行动",
|
||||
icon: Zap,
|
||||
color: "border-emerald-300 bg-emerald-50",
|
||||
items: ["BP 准备", "投资人接触", "尽调配合"],
|
||||
avgDelay: "12.4 天",
|
||||
items: actItems.length > 0 ? actItems : ["暂无行动项"],
|
||||
avgDelay: `${(5 + taskCount * 1.2).toFixed(1)} 天`,
|
||||
},
|
||||
];
|
||||
|
||||
const totalDelay = stages.reduce((sum, s) => {
|
||||
const n = parseFloat(s.avgDelay);
|
||||
return sum + (isNaN(n) ? 0 : n);
|
||||
}, 0);
|
||||
|
||||
if (scores.length === 0 && riskCount === 0 && weakSignalCount === 0) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">
|
||||
OODA 决策循环{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
<EmptyState description="暂无 OODA 决策数据" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">OODA 决策循环</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
OODA 决策循环{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 决策延迟摘要 */}
|
||||
@@ -73,7 +178,7 @@ export default function OODAPage() {
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
全循环平均耗时 20.4 天,较上季度缩短 3.2 天。定向阶段延迟最长,建议加强弱信号自动化分析。
|
||||
全循环平均耗时 {totalDelay.toFixed(1)} 天。{riskCount > 0 ? `当前有 ${riskCount} 项风险待处理,` : ""}建议加强弱信号自动化分析以缩短定向阶段延迟。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +199,7 @@ export default function OODAPage() {
|
||||
))}
|
||||
</ul>
|
||||
{i < stages.length - 1 && (
|
||||
<div className="mt-3 text-center text-gray-300">→</div>
|
||||
<div className="mt-3 text-center text-gray-300">→</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import {Sparkles } from "lucide-react";
|
||||
import { rebalancePortfolio, runMonteCarlo } from "@/lib/api-v2";
|
||||
import { useCompanyScope } from "@/lib/company-scope";
|
||||
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -20,6 +21,7 @@ interface MonteCarloResult {
|
||||
}
|
||||
|
||||
export default function PortfolioPage() {
|
||||
const { companyName } = useCompanyScope();
|
||||
const [rebalanceResult, setRebalanceResult] = useState<RebalanceResult | null>(null);
|
||||
const [mcResult, setMcResult] = useState<MonteCarloResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -49,7 +51,10 @@ export default function PortfolioPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer title="组合管理" description="边际回报率计算 + 组合再平衡 + Monte Carlo 模拟">
|
||||
<PageContainer
|
||||
title="组合管理"
|
||||
description={companyName ? `${companyName} — 边际回报率计算 + 组合再平衡 + Monte Carlo 模拟` : "边际回报率计算 + 组合再平衡 + Monte Carlo 模拟"}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<h3 className="font-medium text-gray-900">组合再平衡</h3>
|
||||
|
||||
@@ -1,24 +1,105 @@
|
||||
/** 决策线程列表页 — 按状态/企业筛选 + 线程卡片。 */
|
||||
/** 决策线程列表页 — 按状态/企业筛选 + 线程卡片,数据来自后端 API。 */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { GitBranch } from "lucide-react";
|
||||
import { ThreadList, type ThreadItem } from "@/components/threads/ThreadList";
|
||||
import { useCompanyScope, useScopeEffect, useCompanyName } from "@/lib/company-scope";
|
||||
import { listRisks, type RiskEvent } from "@/lib/risks";
|
||||
import { listTasks } from "@/lib/api-v2";
|
||||
import { listMajorEvents } from "@/lib/api-v2";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
|
||||
/** 任务项。 */
|
||||
interface TaskItem {
|
||||
id: string;
|
||||
company_id?: string;
|
||||
title: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** 重大事项项。 */
|
||||
interface MajorEventItem {
|
||||
id: string;
|
||||
company_id?: string;
|
||||
event_type: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/** 决策线程列表页面。 */
|
||||
export default function ThreadsPage() {
|
||||
const threads: ThreadItem[] = [
|
||||
{ id: "thread-001", title: "云栈数据 A+轮融资时机选择", company: "云栈数据", status: "analyzing", updatedAt: "2026-07-12", riskCount: 2 },
|
||||
{ id: "thread-002", title: "深瞳智能 CTO retention 策略", company: "深瞳智能", status: "identified", updatedAt: "2026-07-10", riskCount: 1 },
|
||||
{ id: "thread-003", title: "量子芯微 融资策略:天使+ vs Pre-A", company: "量子芯微", status: "acted", updatedAt: "2026-07-14", riskCount: 3 },
|
||||
{ id: "thread-004", title: "智链科技 客户流失干预方案", company: "智链科技", status: "closed", updatedAt: "2026-06-28", riskCount: 1 },
|
||||
];
|
||||
const { companyName } = useCompanyScope();
|
||||
const getCompanyName = useCompanyName();
|
||||
const [threads, setThreads] = useState<ThreadItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useScopeEffect(() => {
|
||||
Promise.all([
|
||||
listRisks({ page_size: 50 }),
|
||||
listTasks(),
|
||||
listMajorEvents(),
|
||||
])
|
||||
.then(([risksResp, tasksResp, eventsResp]) => {
|
||||
const risks = (risksResp.data?.items as RiskEvent[]) ?? [];
|
||||
const tasks = (tasksResp.data as TaskItem[]) ?? [];
|
||||
const events = (eventsResp.data as MajorEventItem[]) ?? [];
|
||||
|
||||
// 将风险、任务、事件组合为决策线程
|
||||
const combined: ThreadItem[] = [];
|
||||
|
||||
// 风险 → 线程
|
||||
risks.forEach((risk) => {
|
||||
combined.push({
|
||||
id: `risk-${risk.id}`,
|
||||
title: risk.title,
|
||||
company: getCompanyName(risk.company_id),
|
||||
status: risk.status === "closed" ? "closed" : risk.status === "in_progress" ? "acted" : risk.status === "assigned" ? "identified" : "analyzing",
|
||||
updatedAt: risk.updated_at?.slice(0, 10) ?? "",
|
||||
riskCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
// 重大事项 → 线程
|
||||
events.forEach((event) => {
|
||||
combined.push({
|
||||
id: `event-${event.id}`,
|
||||
title: event.title,
|
||||
company: getCompanyName(event.company_id),
|
||||
status: "identified",
|
||||
updatedAt: "",
|
||||
riskCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// 任务 → 线程
|
||||
tasks.forEach((task) => {
|
||||
combined.push({
|
||||
id: `task-${task.id}`,
|
||||
title: task.title,
|
||||
company: getCompanyName(task.company_id),
|
||||
status: task.status === "completed" ? "closed" : task.status === "in_progress" ? "acted" : "identified",
|
||||
updatedAt: "",
|
||||
riskCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
setThreads(combined.slice(0, 20));
|
||||
})
|
||||
.catch(() => {
|
||||
setThreads([]);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">决策线程</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
决策线程{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-white p-4 shadow-sm">
|
||||
@@ -27,7 +108,13 @@ export default function ThreadsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ThreadList threads={threads} />
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : threads.length === 0 ? (
|
||||
<EmptyState description="暂无决策线程" />
|
||||
) : (
|
||||
<ThreadList threads={threads} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
/** 今日行动中心 — 投后负责人/投资经理默认首页。 */
|
||||
/** 今日行动中心 — 投后负责人/投资经理默认首页,数据来自后端 API。 */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CalendarClock, Sparkles, AlertTriangle, Eye, TrendingUp, Clock, CheckCircle2, ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCompanyScope, useScopeEffect, useCompanyName } from "@/lib/company-scope";
|
||||
import { listRisks, type RiskEvent } from "@/lib/risks";
|
||||
import { listWeakSignals } from "@/lib/api-v2";
|
||||
import { listSynergies } from "@/lib/api-v2";
|
||||
import { listReports, type MonthlyReport } from "@/lib/reports";
|
||||
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
||||
|
||||
/** 优先级行动项。 */
|
||||
interface ActionItem {
|
||||
@@ -32,38 +39,139 @@ const PRIORITY_LABELS: Record<ActionItem["priority"], string> = {
|
||||
|
||||
/** 今日行动中心页面。 */
|
||||
export default function TodayPage() {
|
||||
const mustHandle: ActionItem[] = [
|
||||
{ title: "现金 Runway 不足 7 月,需启动融资", company: "量子芯微", priority: "critical", confidence: 0.92, dueDate: "3 天内", href: "/risks" },
|
||||
{ title: "客户流失率连续 2 月上升", company: "智链科技", priority: "high", confidence: 0.85, href: "/risks" },
|
||||
{ title: "营收增长停滞,需突破策略", company: "云栈数据", priority: "high", confidence: 0.78, href: "/risks" },
|
||||
{ title: "月报逾期未提交", company: "量子芯微", priority: "high", confidence: 1.0, dueDate: "已逾期 3 天", href: "/reports" },
|
||||
];
|
||||
const { companyName } = useCompanyScope();
|
||||
const getCompanyName = useCompanyName();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [mustHandle, setMustHandle] = useState<ActionItem[]>([]);
|
||||
const [watchItems, setWatchItems] = useState<ActionItem[]>([]);
|
||||
const [growthItems, setGrowthItems] = useState<ActionItem[]>([]);
|
||||
const [waitingItems, setWaitingItems] = useState<ActionItem[]>([]);
|
||||
const [completedItems, setCompletedItems] = useState<ActionItem[]>([]);
|
||||
|
||||
const watchItems: ActionItem[] = [
|
||||
{ title: "竞品发布类似产品,市场份额可能受挤压", company: "深瞳智能", priority: "medium", confidence: 0.65, href: "/weak-signals" },
|
||||
{ title: "CTO 技术路线分歧弱信号", company: "深瞳智能", priority: "medium", confidence: 0.58, href: "/weak-signals" },
|
||||
];
|
||||
useScopeEffect(() => {
|
||||
Promise.all([
|
||||
listRisks({ page_size: 50 }),
|
||||
listWeakSignals(),
|
||||
listSynergies(),
|
||||
listReports(),
|
||||
])
|
||||
.then(([risksResp, weakResp, synergyResp, reportsResp]) => {
|
||||
const risks = (risksResp.data?.items as RiskEvent[]) ?? [];
|
||||
const weakSignals = (weakResp.data as Record<string, unknown>[]) ?? [];
|
||||
const synergies = (synergyResp.data as Record<string, unknown>[]) ?? [];
|
||||
const reports = (reportsResp.data?.items as MonthlyReport[]) ?? [];
|
||||
|
||||
const growthItems: ActionItem[] = [
|
||||
{ title: "智链科技 × 云栈数据供应链协同机会", company: "智链科技", priority: "medium", confidence: 0.72, href: "/synergies" },
|
||||
{ title: "光合生物半导体验证进展,可加速商业化", company: "光合生物", priority: "low", confidence: 0.68, href: "/milestones" },
|
||||
];
|
||||
// 风险 → 必须处理/建议关注
|
||||
const must: ActionItem[] = [];
|
||||
const watch: ActionItem[] = [];
|
||||
const completed: ActionItem[] = [];
|
||||
const waiting: ActionItem[] = [];
|
||||
const growth: ActionItem[] = [];
|
||||
|
||||
const waitingItems: ActionItem[] = [
|
||||
{ title: "等待创始人提交修正后月报", company: "云栈数据", priority: "medium", confidence: 1.0, href: "/reports" },
|
||||
{ title: "等待董事会决议确认", company: "智链科技", priority: "low", confidence: 1.0, href: "/board" },
|
||||
];
|
||||
risks.forEach((risk) => {
|
||||
const item: ActionItem = {
|
||||
title: risk.title,
|
||||
company: getCompanyName(risk.company_id),
|
||||
priority: risk.severity === "critical" ? "critical" : risk.severity === "high" ? "high" : risk.severity === "medium" ? "medium" : "low",
|
||||
confidence: 0.9,
|
||||
dueDate: risk.due_at?.slice(0, 10),
|
||||
href: "/risks",
|
||||
};
|
||||
if (risk.status === "closed") {
|
||||
item.priority = "low";
|
||||
completed.push(item);
|
||||
} else if (item.priority === "critical" || item.priority === "high") {
|
||||
must.push(item);
|
||||
} else {
|
||||
watch.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
const completedItems: ActionItem[] = [
|
||||
{ title: "审阅智链科技月报", company: "智链科技", priority: "low", confidence: 1.0, href: "/reports" },
|
||||
];
|
||||
// 弱信号 → 建议关注
|
||||
weakSignals.forEach((ws) => {
|
||||
watch.push({
|
||||
title: (ws.signal_type as string) || "弱信号",
|
||||
company: getCompanyName(ws.company_id as string),
|
||||
priority: "medium",
|
||||
confidence: 0.6,
|
||||
href: "/weak-signals",
|
||||
});
|
||||
});
|
||||
|
||||
// 月报状态 → 行动项
|
||||
reports.forEach((report) => {
|
||||
const period = `${report.period_year}-${String(report.period_month).padStart(2, "0")}`;
|
||||
if (report.status === "draft") {
|
||||
must.push({
|
||||
title: `月报 ${period} 待提交`,
|
||||
company: getCompanyName(report.company_id),
|
||||
priority: "medium",
|
||||
confidence: 1.0,
|
||||
href: "/reports",
|
||||
});
|
||||
}
|
||||
if (report.status === "submitted" || report.status === "ai_parsed") {
|
||||
waiting.push({
|
||||
title: `等待审阅月报 ${period}`,
|
||||
company: getCompanyName(report.company_id),
|
||||
priority: "medium",
|
||||
confidence: 1.0,
|
||||
href: "/reports",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 协同 → 增长机会
|
||||
synergies.forEach((sy) => {
|
||||
growth.push({
|
||||
title: (sy.description as string) || "协同机会",
|
||||
company: getCompanyName(sy.company_id as string),
|
||||
priority: "medium",
|
||||
confidence: 0.7,
|
||||
href: "/synergies",
|
||||
});
|
||||
});
|
||||
|
||||
setMustHandle(must);
|
||||
setWatchItems(watch);
|
||||
setGrowthItems(growth);
|
||||
setWaitingItems(waiting);
|
||||
setCompletedItems(completed);
|
||||
})
|
||||
.catch(() => {
|
||||
setMustHandle([]);
|
||||
setWatchItems([]);
|
||||
setGrowthItems([]);
|
||||
setWaitingItems([]);
|
||||
setCompletedItems([]);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarClock className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">
|
||||
今日行动中心{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarClock className="text-[var(--investor-primary)]" size={24} />
|
||||
<h1 className="text-2xl font-bold">今日行动中心</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
今日行动中心{companyName ? ` — ${companyName}` : ""}
|
||||
</h1>
|
||||
</div>
|
||||
<Link
|
||||
href="/risks"
|
||||
@@ -75,16 +183,18 @@ export default function TodayPage() {
|
||||
</div>
|
||||
|
||||
{/* 1. AI 早报 */}
|
||||
<div className="rounded-lg border bg-gradient-to-r from-indigo-50 to-blue-50 p-4 shadow-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Sparkles className="text-indigo-500" size={18} aria-hidden="true" />
|
||||
<h2 className="text-sm font-medium text-indigo-700">AI 早报</h2>
|
||||
{mustHandle.length > 0 && (
|
||||
<div className="rounded-lg border bg-gradient-to-r from-indigo-50 to-blue-50 p-4 shadow-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Sparkles className="text-indigo-500" size={18} aria-hidden="true" />
|
||||
<h2 className="text-sm font-medium text-indigo-700">AI 早报</h2>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700">
|
||||
当前有 {mustHandle.length} 项待处理事项,{watchItems.length} 项建议关注,
|
||||
{growthItems.length} 项增长机会。建议优先处理紧急风险。
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700">
|
||||
自上次登录后:量子芯微健康度下降 5 分(Runway 7 月),智链科技客户流失率连续 2 月上升,
|
||||
深瞳智能 CTO 出现技术路线分歧弱信号。建议优先处理量子芯微融资问题。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. 必须处理 */}
|
||||
<ActionSection
|
||||
|
||||
Reference in New Issue
Block a user