From e81aa6828e52834b265856e8ce8e0e9fe4003706 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Mon, 20 Jul 2026 07:47:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(scope):=20add=20company=20scope=20switcher?= =?UTF-8?q?=20=E2=80=94=20two=20views=20(by-task=20all=20companies=20/=20b?= =?UTF-8?q?y-company=20all=20tasks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompanyScopeContext + Provider with localStorage persistence - Sidebar company selector (desktop + mobile) - apiFetch auto-injects company_id on GET requests - Dashboard summary API supports company_id filter - Milestones/Financial/DigitalTwins pages use scope instead of manual input - HealthHeatmap/HealthTrends components react to scope changes --- backend/app/routers/dashboard.py | 29 ++-- .../src/app/(investor)/dashboard/page.tsx | 6 +- .../src/app/(investor)/digital-twins/page.tsx | 20 ++- .../src/app/(investor)/financial/page.tsx | 20 +-- frontend/src/app/(investor)/layout.tsx | 129 +++++++++++++++++- .../src/app/(investor)/milestones/page.tsx | 23 ++-- .../components/dashboard/HealthHeatmap.tsx | 15 +- frontend/src/lib/api.ts | 27 +++- frontend/src/lib/companies.ts | 2 +- frontend/src/lib/company-scope.tsx | 88 ++++++++++++ 10 files changed, 306 insertions(+), 53 deletions(-) create mode 100644 frontend/src/lib/company-scope.tsx diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 0d8d1a9..b8af94a 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -28,10 +28,11 @@ _DIMENSION_KEYS = [ @router.get("/summary", response_model=ApiResponse[DashboardSummary]) async def get_dashboard_summary( + company_id: str | None = Query(default=None, description="指定企业 ID,不传则汇总全租户"), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): - """获取仪表盘汇总数据。""" + """获取仪表盘汇总数据。支持按企业过滤。""" tenant_id = user.tenant_id # 企业总数 @@ -40,40 +41,52 @@ async def get_dashboard_summary( ) total_companies = companies_result.scalar_one() - # 平均健康度 - avg_result = await db.execute( + # 平均健康度(按企业过滤时只算该企业) + avg_query = ( select(func.avg(HealthScore.total_score)) .join(Company, HealthScore.company_id == Company.id) .where(Company.tenant_id == tenant_id) ) + if company_id: + avg_query = avg_query.where(HealthScore.company_id == company_id) + avg_result = await db.execute(avg_query) avg_score = avg_result.scalar_one() avg_health_score = float(avg_score) if avg_score else 0.0 # 高风险事件数 - risk_result = await db.execute( + risk_query = ( select(func.count()) .select_from(RiskEvent) .join(Company, RiskEvent.company_id == Company.id) .where(Company.tenant_id == tenant_id, RiskEvent.status.in_(["open", "assigned", "in_progress"])) ) + if company_id: + risk_query = risk_query.where(RiskEvent.company_id == company_id) + risk_result = await db.execute(risk_query) high_risk_count = risk_result.scalar_one() # 待审阅月报数 - pending_result = await db.execute( + pending_query = ( select(func.count()) .select_from(MonthlyReport) .join(Company, MonthlyReport.company_id == Company.id) .where(Company.tenant_id == tenant_id, MonthlyReport.status.in_(["submitted", "ai_parsed"])) ) + if company_id: + pending_query = pending_query.where(MonthlyReport.company_id == company_id) + pending_result = await db.execute(pending_query) pending_reports = pending_result.scalar_one() # 最近评分(最多 10 条) - recent_result = await db.execute( + recent_query = ( select(HealthScore, Company.name.label("company_name")) .join(Company, HealthScore.company_id == Company.id) .where(Company.tenant_id == tenant_id) - .order_by(HealthScore.calculated_at.desc()) - .limit(10) + ) + if company_id: + recent_query = recent_query.where(HealthScore.company_id == company_id) + recent_result = await db.execute( + recent_query.order_by(HealthScore.calculated_at.desc()).limit(10) ) recent_rows = recent_result.all() recent_scores = [] diff --git a/frontend/src/app/(investor)/dashboard/page.tsx b/frontend/src/app/(investor)/dashboard/page.tsx index 43b7811..ebd98e6 100644 --- a/frontend/src/app/(investor)/dashboard/page.tsx +++ b/frontend/src/app/(investor)/dashboard/page.tsx @@ -4,6 +4,7 @@ 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 { useCompanyScope } from "@/lib/company-scope"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge"; @@ -13,6 +14,7 @@ import { HealthHeatmap, HealthTrends } from "@/components/dashboard/HealthHeatma * 投资人端 — 驾驶舱首页。 */ export default function InvestorDashboardPage() { + const { companyName } = useCompanyScope(); const [summary, setSummary] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -79,8 +81,8 @@ export default function InvestorDashboardPage() { return (
-

投资机构驾驶舱

-

Portfolio 全局健康度与风险概览

+

{companyName ? `${companyName} — 企业驾驶舱` : "投资机构驾驶舱"}

+

{companyName ? "单企业健康度与风险概览" : "Portfolio 全局健康度与风险概览"}

{/* KPI 卡片 */} diff --git a/frontend/src/app/(investor)/digital-twins/page.tsx b/frontend/src/app/(investor)/digital-twins/page.tsx index a85744c..3023c46 100644 --- a/frontend/src/app/(investor)/digital-twins/page.tsx +++ b/frontend/src/app/(investor)/digital-twins/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import {Sparkles } from "lucide-react"; import { listDigitalTwins, buildDigitalTwin, simulateTwin } from "@/lib/api-v2"; +import { useCompanyScope } from "@/lib/company-scope"; import { PageContainer, Card, Badge } from "@/components/shared/PageContainer"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { EmptyState } from "@/components/shared/EmptyState"; @@ -20,6 +21,7 @@ interface SimulationResult { } export default function DigitalTwinsPage() { + const { companyId } = useCompanyScope(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [companyData, setCompanyData] = useState(""); @@ -27,11 +29,17 @@ export default function DigitalTwinsPage() { const [simResult, setSimResult] = useState(null); useEffect(() => { - listDigitalTwins("") - .then(() => setItems([])) + if (!companyId) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setItems([]); + setIsLoading(false); + return; + } + listDigitalTwins(companyId) + .then((resp) => setItems((resp.data as DigitalTwinItem[]) ?? [])) .catch(() => setItems([])) .finally(() => setIsLoading(false)); - }, []); + }, [companyId]); const handleBuild = async () => { if (!companyData.trim()) { @@ -62,6 +70,10 @@ export default function DigitalTwinsPage() { return ( + {!companyId ? ( + + ) : ( + <>

构建数字孪生模型