feat(workbench): complete company view — health trend, major events, milestones, team, synergy + dashboard click-through

This commit is contained in:
selfrelease
2026-07-19 22:01:49 +08:00
parent 0197f89018
commit 3d22bd8a3d
5 changed files with 444 additions and 63 deletions
+97
View File
@@ -18,16 +18,25 @@ from app.schemas.company import (
AgreementBrief,
BoardMeetingBrief,
HealthScoreBrief,
HealthScoreHistoryPoint,
MajorEventBrief,
MilestoneBrief,
ReportBrief,
RiskBrief,
SynergyBrief,
TeamMemberBrief,
WeakSignalBrief,
)
from app.models.agreement import InvestmentAgreement
from app.models.board import BoardMeeting
from app.models.financial_data import FinancialData
from app.models.health_score import HealthScore
from app.models.major_event import MajorEvent
from app.models.milestone import MilestoneTree
from app.models.report import MonthlyReport
from app.models.risk import RiskEvent
from app.models.synergy import SynergyOpportunity
from app.models.talent import TeamMember
from app.models.weak_signal import WeakSignal
router = APIRouter(prefix="/companies", tags=["companies"])
@@ -224,14 +233,102 @@ async def get_company_detail(
latest_fin = latest_fin_result.scalar_one_or_none()
latest_financial = latest_fin.data_json if latest_fin else None
# 健康度历史趋势(最近 12 条)
history_result = await db.execute(
select(HealthScore)
.where(HealthScore.company_id == company_id)
.order_by(HealthScore.calculated_at.asc())
.limit(12)
)
history_scores = history_result.scalars().all()
health_score_history = [
HealthScoreHistoryPoint(
period=s.calculated_at.strftime("%Y-%m"),
total_score=s.total_score,
calculated_at=s.calculated_at,
)
for s in history_scores
]
# 重大事项(最近 10 条)
events_result = await db.execute(
select(MajorEvent)
.where(MajorEvent.company_id == company_id)
.order_by(MajorEvent.created_at.desc())
.limit(10)
)
events = events_result.scalars().all()
major_events = [
MajorEventBrief(
id=e.id, event_type=e.event_type, title=e.title,
description=e.description, severity=e.severity,
status=e.status, occurred_at=e.occurred_at,
) for e in events
]
# 里程碑
milestone_result = await db.execute(
select(MilestoneTree)
.where(MilestoneTree.company_id == company_id)
.order_by(MilestoneTree.target_date.desc().nulls_last())
.limit(20)
)
milestones = milestone_result.scalars().all()
milestone_briefs = [
MilestoneBrief(
id=m.id, name=m.name, status=m.status, is_current=m.is_current,
target_date=m.target_date, actual_date=m.actual_date,
description=m.description,
) for m in milestones
]
# 团队成员
team_result = await db.execute(
select(TeamMember)
.where(TeamMember.company_id == company_id)
.order_by(TeamMember.is_key_person.desc(), TeamMember.joined_at.desc())
.limit(20)
)
team_members_data = team_result.scalars().all()
team_members = [
TeamMemberBrief(
id=t.id, name=t.name, role=t.role, is_key_person=t.is_key_person,
stability_score=t.stability_score, joined_at=t.joined_at,
) for t in team_members_data
]
# 协同机会(涉及该企业的)
synergy_result = await db.execute(
select(SynergyOpportunity)
.where(
(SynergyOpportunity.company_a_id == company_id) |
(SynergyOpportunity.company_b_id == company_id)
)
.order_by(SynergyOpportunity.created_at.desc())
.limit(10)
)
synergies = synergy_result.scalars().all()
synergy_briefs = [
SynergyBrief(
id=s.id, type=s.type, title=s.title,
description=s.description, status=s.status,
match_reason=s.match_reason,
) for s in synergies
]
return success(data=CompanyDetailResponse(
company=CompanyResponse.model_validate(company, from_attributes=True),
health_score=health_brief,
health_score_history=health_score_history,
recent_reports=report_briefs,
open_risks=risk_briefs,
recent_weak_signals=signal_briefs,
active_agreements=agreement_briefs,
recent_board_meetings=meeting_briefs,
major_events=major_events,
milestones=milestone_briefs,
team_members=team_members,
synergy_opportunities=synergy_briefs,
financial_data_count=fin_count,
latest_financial=latest_financial,
))
+59
View File
@@ -130,15 +130,74 @@ class BoardMeetingBrief(BaseModel):
meeting_at: datetime | None = None
class MajorEventBrief(BaseModel):
"""重大事项摘要。"""
id: str
event_type: str
title: str
description: str | None = None
severity: str = "medium"
status: str = "identified"
occurred_at: datetime | None = None
class MilestoneBrief(BaseModel):
"""里程碑摘要。"""
id: str
name: str
status: str = "planned"
is_current: bool = False
target_date: datetime | None = None
actual_date: datetime | None = None
description: str | None = None
class TeamMemberBrief(BaseModel):
"""团队成员摘要。"""
id: str
name: str
role: str | None = None
is_key_person: bool = False
stability_score: float | None = None
joined_at: datetime | None = None
class SynergyBrief(BaseModel):
"""协同机会摘要。"""
id: str
type: str
title: str
description: str | None = None
status: str = "discovered"
match_reason: str | None = None
class HealthScoreHistoryPoint(BaseModel):
"""健康度历史数据点。"""
period: str
total_score: float
calculated_at: datetime
class CompanyDetailResponse(BaseModel):
"""企业详情聚合响应 — 工作台使用。"""
company: CompanyResponse
health_score: HealthScoreBrief | None = None
health_score_history: list[HealthScoreHistoryPoint] = []
recent_reports: list[ReportBrief] = []
open_risks: list[RiskBrief] = []
recent_weak_signals: list[WeakSignalBrief] = []
active_agreements: list[AgreementBrief] = []
recent_board_meetings: list[BoardMeetingBrief] = []
major_events: list[MajorEventBrief] = []
milestones: list[MilestoneBrief] = []
team_members: list[TeamMemberBrief] = []
synergy_opportunities: list[SynergyBrief] = []
financial_data_count: int = 0
latest_financial: dict | None = None
@@ -10,21 +10,75 @@ import { HighlightsPanel } from "@/components/workbench/HighlightsPanel";
import { WorkModeSwitcher, type WorkMode } from "@/components/shared/WorkModeSwitcher";
import { InsightRail } from "@/components/shared/InsightRail";
import { apiFetch } from "@/lib/api";
import { AlertTriangle, FileText, ClipboardList, ScrollText, Network, DollarSign, Activity, Users, Bot } from "lucide-react";
import { AlertTriangle, FileText, ClipboardList, ScrollText, Network, DollarSign, Activity, Users, Bot, Flag, Calendar, TrendingUp } from "lucide-react";
/** 企业详情数据结构。 */
interface CompanyDetail {
company: { id: string; name: string; industry?: string };
health_score: HealthScore | null;
health_score_history: HealthScoreHistoryPoint[];
recent_reports: ReportItem[];
open_risks: RiskItem[];
recent_weak_signals: WeakSignalItem[];
active_agreements: AgreementItem[];
recent_board_meetings: BoardMeetingItem[];
major_events: MajorEventItem[];
milestones: MilestoneItem[];
team_members: TeamMemberItem[];
synergy_opportunities: SynergyItem[];
latest_financial?: Record<string, unknown>;
financial_data_count?: number;
}
/** 健康度历史数据点。 */
interface HealthScoreHistoryPoint {
period: string;
total_score: number;
calculated_at: string;
}
/** 重大事项项。 */
interface MajorEventItem {
id: string;
event_type: string;
title: string;
description?: string;
severity: string;
status: string;
occurred_at?: string;
}
/** 里程碑项。 */
interface MilestoneItem {
id: string;
name: string;
status: string;
is_current: boolean;
target_date?: string;
actual_date?: string;
description?: string;
}
/** 团队成员项。 */
interface TeamMemberItem {
id: string;
name: string;
role?: string;
is_key_person: boolean;
stability_score?: number;
joined_at?: string;
}
/** 协同机会项。 */
interface SynergyItem {
id: string;
type: string;
title: string;
description?: string;
status: string;
match_reason?: string;
}
/** 健康度评分结构。 */
type HealthScore = {
total_score: number;
@@ -105,7 +159,7 @@ export default function WorkbenchPage() {
return <EmptyState title="未找到企业信息" />;
}
const { company, health_score, recent_reports, open_risks, recent_weak_signals, active_agreements, recent_board_meetings } = detail;
const { company, health_score, health_score_history, recent_reports, open_risks, recent_weak_signals, active_agreements, recent_board_meetings, major_events, milestones, team_members, synergy_opportunities } = detail;
const highRisksCount = (open_risks || []).filter((r) => r.severity === "high" || r.severity === "critical").length;
const pendingTasksCount = (recent_reports || []).filter((r) => r.status === "pending" || r.status === "draft").length;
@@ -137,13 +191,14 @@ export default function WorkbenchPage() {
{activeTab === "overview" && <OverviewTab detail={detail} />}
{activeTab === "financial" && <FinancialTab detail={detail} />}
{activeTab === "operational" && <OperationalTab detail={detail} />}
{activeTab === "org" && <OrgTab />}
{activeTab === "org" && <OrgTab teamMembers={team_members} />}
{activeTab === "ai" && <AITab detail={detail} />}
{activeTab === "risk" && <RiskTab risks={open_risks} signals={recent_weak_signals} />}
{activeTab === "events" && <EventsTab events={major_events} milestones={milestones} />}
{activeTab === "reports" && <ReportsTab reports={recent_reports} />}
{activeTab === "board" && <BoardTab meetings={recent_board_meetings} />}
{activeTab === "agreements" && <AgreementsTab agreements={active_agreements} />}
{activeTab === "synergy" && <SynergyTab />}
{activeTab === "synergy" && <SynergyTab synergies={synergy_opportunities} />}
</TabPanel>
</div>
@@ -156,24 +211,79 @@ export default function WorkbenchPage() {
/** 概览 Tab — 健康度雷达图 + 维度详情。 */
function OverviewTab({ detail }: { detail: CompanyDetail }) {
const { health_score } = detail;
const { health_score, health_score_history } = detail;
if (!health_score) {
return <EmptyState title="暂无健康度评分数据" />;
}
return (
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<h3 className="mb-2 text-sm font-medium"></h3>
<HealthRadar scores={health_score} dimensions={DIMENSIONS_14} size={320} />
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<h3 className="mb-2 text-sm font-medium"></h3>
<HealthRadar scores={health_score} dimensions={DIMENSIONS_14} size={320} />
</div>
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<h3 className="mb-2 text-sm font-medium"></h3>
<HealthDimensionDetail scores={health_score} dimensions={DIMENSIONS_14_DETAIL} />
</div>
</div>
{/* 健康度趋势图 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<h3 className="mb-2 text-sm font-medium"></h3>
<HealthDimensionDetail scores={health_score} dimensions={DIMENSIONS_14_DETAIL} />
<div className="flex items-center gap-2">
<TrendingUp size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"></h3>
</div>
{health_score_history && health_score_history.length > 0 ? (
<HealthTrendMini data={health_score_history} />
) : (
<p className="mt-2 text-sm text-muted-foreground"></p>
)}
</div>
</div>
);
}
/** 健康度趋势迷你折线图。 */
function HealthTrendMini({ data }: { data: HealthScoreHistoryPoint[] }) {
const width = 500;
const height = 180;
const padding = { top: 15, right: 15, bottom: 30, left: 35 };
const chartW = width - padding.left - padding.right;
const chartH = height - padding.top - padding.bottom;
const periods = data.map((d) => d.period);
const scores = data.map((d) => d.total_score);
const xStep = periods.length > 1 ? chartW / (periods.length - 1) : 0;
const yScale = (val: number) => chartH - (val / 100) * chartH;
const linePath = scores
.map((s, i) => `${i === 0 ? "M" : "L"} ${padding.left + i * xStep} ${padding.top + yScale(s)}`)
.join(" ");
return (
<svg width={width} height={height} role="img" aria-label="健康度趋势图" className="mt-2">
{[0, 25, 50, 75, 100].map((v) => (
<g key={v}>
<line
x1={padding.left} y1={padding.top + yScale(v)}
x2={width - padding.right} y2={padding.top + yScale(v)}
stroke="var(--border)" strokeWidth={1} strokeDasharray={v === 0 ? "none" : "2,2"}
/>
<text x={padding.left - 6} y={padding.top + yScale(v) + 4} textAnchor="end" className="text-[10px] fill-muted-foreground">{v}</text>
</g>
))}
<path d={linePath} fill="none" stroke="var(--investor-primary)" strokeWidth={2} />
{scores.map((s, i) => (
<g key={i}>
<circle cx={padding.left + i * xStep} cy={padding.top + yScale(s)} r={3} fill="var(--investor-primary)" />
<text x={padding.left + i * xStep} y={padding.top + yScale(s) - 8} textAnchor="middle" className="text-[9px] font-medium fill-foreground">{s.toFixed(0)}</text>
<text x={padding.left + i * xStep} y={height - padding.bottom + 14} textAnchor="middle" className="text-[9px] fill-muted-foreground">{periods[i]}</text>
</g>
))}
</svg>
);
}
/** 财务 Tab。 */
function FinancialTab({ detail }: { detail: CompanyDetail }) {
const { latest_financial, financial_data_count } = detail;
@@ -223,14 +333,40 @@ function OperationalTab({ detail }: { detail: CompanyDetail }) {
}
/** 组织 Tab。 */
function OrgTab() {
function OrgTab({ teamMembers }: { teamMembers: TeamMemberItem[] }) {
return (
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<Users size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"></h3>
<div className="space-y-4">
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<Users size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"> ({teamMembers?.length || 0})</h3>
</div>
{teamMembers && teamMembers.length > 0 ? (
<div className="mt-3 space-y-2">
{teamMembers.map((m) => (
<div key={m.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{m.name}</span>
{m.is_key_person && (
<span className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700"></span>
)}
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{m.role && <span>{m.role}</span>}
{m.stability_score != null && (
<span>: {(m.stability_score * 100).toFixed(0)}%</span>
)}
{m.joined_at && (
<span>: {new Date(m.joined_at).toLocaleDateString("zh-CN")}</span>
)}
</div>
</div>
))}
</div>
) : (
<EmptyState title="暂无团队成员数据" />
)}
</div>
<EmptyState title="组织数据待月报结构化后展示" />
</div>
);
}
@@ -383,15 +519,130 @@ function AgreementsTab({ agreements }: { agreements: AgreementItem[] }) {
);
}
/** 重大事项 Tab。 */
function EventsTab({ events, milestones }: { events: MajorEventItem[]; milestones: MilestoneItem[] }) {
const severityColor = (s: string) =>
s === "critical" ? "bg-rose-100 text-rose-700" :
s === "high" ? "bg-amber-100 text-amber-700" :
s === "medium" ? "bg-blue-100 text-blue-700" :
"bg-muted text-muted-foreground";
const statusLabel = (s: string) =>
s === "identified" ? "已识别" :
s === "confirmed" ? "已确认" :
s === "addressed" ? "已处理" : s;
const milestoneStatusColor = (s: string) =>
s === "completed" ? "bg-emerald-100 text-emerald-700" :
s === "in_progress" ? "bg-blue-100 text-blue-700" :
s === "abandoned" ? "bg-muted text-muted-foreground line-through" :
"bg-amber-100 text-amber-700";
return (
<div className="space-y-4">
{/* 重大事项 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<Flag size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"> ({events?.length || 0})</h3>
</div>
{events && events.length > 0 ? (
<div className="mt-3 space-y-2">
{events.map((e) => (
<div key={e.id} className="rounded-md border border-[var(--border)] px-3 py-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{e.title}</span>
<div className="flex items-center gap-2">
<span className={`rounded px-1.5 py-0.5 text-xs ${severityColor(e.severity)}`}>{e.severity}</span>
<span className="text-xs text-muted-foreground">{statusLabel(e.status)}</span>
</div>
</div>
{e.description && <p className="mt-1 text-xs text-muted-foreground">{e.description}</p>}
{e.occurred_at && (
<p className="mt-1 text-xs text-muted-foreground">: {new Date(e.occurred_at).toLocaleDateString("zh-CN")}</p>
)}
</div>
))}
</div>
) : (
<EmptyState title="暂无重大事项" />
)}
</div>
{/* 里程碑 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<Calendar size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"> ({milestones?.length || 0})</h3>
</div>
{milestones && milestones.length > 0 ? (
<div className="mt-3 space-y-2">
{milestones.map((m) => (
<div key={m.id} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
<div className="flex items-center gap-2">
{m.is_current && <span className="h-2 w-2 rounded-full bg-emerald-500" />}
<span className={`text-sm font-medium ${m.status === "abandoned" ? "line-through text-muted-foreground" : ""}`}>{m.name}</span>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{m.target_date && <span>: {new Date(m.target_date).toLocaleDateString("zh-CN")}</span>}
{m.actual_date && <span className="text-emerald-600">: {new Date(m.actual_date).toLocaleDateString("zh-CN")}</span>}
<span className={`rounded px-1.5 py-0.5 ${milestoneStatusColor(m.status)}`}>{m.status}</span>
</div>
</div>
))}
</div>
) : (
<EmptyState title="暂无里程碑数据" />
)}
</div>
</div>
);
}
/** 协同 Tab。 */
function SynergyTab() {
function SynergyTab({ synergies }: { synergies: SynergyItem[] }) {
const typeLabel = (t: string) =>
t === "customer" ? "客户" :
t === "talent" ? "人才" :
t === "funding" ? "融资" :
t === "supply_chain" ? "供应链" :
t === "tech" ? "技术" : t;
const statusColor = (s: string) =>
s === "completed" ? "bg-emerald-100 text-emerald-700" :
s === "executing" ? "bg-blue-100 text-blue-700" :
s === "authorized" ? "bg-indigo-100 text-indigo-700" :
s === "confirmed" ? "bg-amber-100 text-amber-700" :
s === "declined" ? "bg-rose-100 text-rose-700" :
"bg-muted text-muted-foreground";
return (
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<Network size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium"></h3>
<h3 className="text-sm font-medium"> ({synergies?.length || 0})</h3>
</div>
<EmptyState title="请前往协同中心查看" />
{synergies && synergies.length > 0 ? (
<div className="mt-3 space-y-2">
{synergies.map((s) => (
<div key={s.id} className="rounded-md border border-[var(--border)] px-3 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{s.title}</span>
<span className="rounded bg-muted px-1.5 py-0.5 text-xs">{typeLabel(s.type)}</span>
</div>
<span className={`rounded px-1.5 py-0.5 text-xs ${statusColor(s.status)}`}>{s.status}</span>
</div>
{s.description && <p className="mt-1 text-xs text-muted-foreground">{s.description}</p>}
{s.match_reason && (
<p className="mt-1 text-xs text-muted-foreground">: {s.match_reason}</p>
)}
</div>
))}
</div>
) : (
<EmptyState title="暂无协同机会" />
)}
</div>
);
}
+15 -42
View File
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import { Building2, Heart, AlertTriangle, FileText, TrendingUp, TrendingDown, Minus } from "lucide-react";
import Link from "next/link";
import { getDashboardSummary, type DashboardSummary } from "@/lib/dashboard";
import { listCompanies, type Company } from "@/lib/companies";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge";
@@ -15,19 +14,13 @@ import { HealthHeatmap, HealthTrends } from "@/components/dashboard/HealthHeatma
*/
export default function InvestorDashboardPage() {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [companies, setCompanies] = useState<Company[]>([]);
const [selectedCompanyId, setSelectedCompanyId] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const [summaryResp, companiesResp] = await Promise.all([
getDashboardSummary(),
listCompanies({ page_size: 100 }),
]);
if (summaryResp.data) setSummary(summaryResp.data);
if (companiesResp.data?.items) setCompanies(companiesResp.data.items);
const resp = await getDashboardSummary();
if (resp.data) setSummary(resp.data);
} catch {
// ignore
} finally {
@@ -83,35 +76,11 @@ export default function InvestorDashboardPage() {
},
];
// 筛选后的评分列表
const filteredScores = selectedCompanyId
? summary?.recent_scores.filter((s) => s.company_id === selectedCompanyId) ?? []
: summary?.recent_scores ?? [];
// 选中的企业名称
const selectedCompany = companies.find((c) => c.id === selectedCompanyId);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground"></h1>
<p className="mt-1 text-sm text-muted-foreground">
{selectedCompany ? `${selectedCompany.name} — 企业健康度概览` : "Portfolio 全局健康度与风险概览"}
</p>
</div>
{/* 企业筛选器 */}
<select
value={selectedCompanyId}
onChange={(e) => setSelectedCompanyId(e.target.value)}
className="rounded-md border bg-white px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--investor-primary)]"
>
<option value=""></option>
{companies.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<div>
<h1 className="text-2xl font-bold text-foreground"></h1>
<p className="mt-1 text-sm text-muted-foreground">Portfolio </p>
</div>
{/* KPI 卡片 */}
@@ -144,10 +113,14 @@ export default function InvestorDashboardPage() {
</Link>
</div>
{filteredScores.length > 0 ? (
{summary && summary.recent_scores.length > 0 ? (
<div className="space-y-3">
{filteredScores.map((score) => (
<div key={score.id} className="flex items-center justify-between border-b pb-3 last:border-0 last:pb-0">
{summary.recent_scores.map((score) => (
<Link
key={score.id}
href={`/companies/${score.company_id}/workbench`}
className="flex items-center justify-between border-b pb-3 last:border-0 last:pb-0 transition-colors hover:bg-muted/30 rounded-md px-2 -mx-2"
>
<div className="flex items-center gap-3">
<HealthScoreBadge score={Math.round(score.total_score)} />
<div>
@@ -175,7 +148,7 @@ export default function InvestorDashboardPage() {
</span>
)}
</div>
</div>
</Link>
))}
</div>
) : (
@@ -187,10 +160,10 @@ export default function InvestorDashboardPage() {
<div className="space-y-4">
<div className="rounded-lg border bg-white p-5 shadow-sm">
<h2 className="mb-4 text-lg font-semibold"></h2>
<HealthHeatmap companyId={selectedCompanyId || undefined} />
<HealthHeatmap />
</div>
<div className="rounded-lg border bg-white p-5 shadow-sm">
<HealthTrends companyId={selectedCompanyId || undefined} />
<HealthTrends />
</div>
</div>
</div>
+2 -1
View File
@@ -3,7 +3,7 @@
import {type ReactNode } from "react";
import {
LayoutDashboard, DollarSign, Activity, Users, Bot,
AlertTriangle, FileText, ClipboardList, ScrollText, Network,
AlertTriangle, FileText, ClipboardList, ScrollText, Network, Flag,
} from "lucide-react";
/** 工作台 Tab 定义。 */
@@ -14,6 +14,7 @@ const TABS = [
{ id: "org", label: "组织", icon: Users },
{ id: "ai", label: "AI+专项", icon: Bot },
{ id: "risk", label: "风险", icon: AlertTriangle },
{ id: "events", label: "重大事项", icon: Flag },
{ id: "reports", label: "月报", icon: FileText },
{ id: "board", label: "董事会", icon: ClipboardList },
{ id: "agreements", label: "协议", icon: ScrollText },