Files
AIPortPilot/frontend/src/app/(investor)/digital-twins/page.tsx
T
selfrelease e81aa6828e feat(scope): add company scope switcher — two views (by-task all companies / by-company all tasks)
- 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
2026-07-20 07:47:53 +08:00

144 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
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";
import { toast } from "sonner";
/** 数字孪生模型项。 */
interface DigitalTwinItem {
accuracy_score?: number;
}
/** 模拟结果。 */
interface SimulationResult {
projected_outcome: string;
confidence?: number;
}
export default function DigitalTwinsPage() {
const { companyId } = useCompanyScope();
const [items, setItems] = useState<DigitalTwinItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [companyData, setCompanyData] = useState("");
const [scenario, setScenario] = useState("");
const [simResult, setSimResult] = useState<SimulationResult | null>(null);
useEffect(() => {
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()) {
toast.error("请输入企业数据");
return;
}
try {
const resp = await buildDigitalTwin(companyData);
toast.success("数字孿生模型已构建");
setItems([resp.data as DigitalTwinItem, ...items]);
} catch {
toast.error("构建失败");
}
};
const handleSimulate = async () => {
if (!scenario.trim()) {
toast.error("请输入场景描述");
return;
}
try {
const resp = await simulateTwin({}, scenario);
setSimResult(resp.data as SimulationResult);
} catch {
toast.error("模拟失败");
}
};
return (
<PageContainer title="数字孪生" description="企业模型 + 场景模拟 + 精度追踪">
{!companyId ? (
<EmptyState title="请先在侧边栏选择企业" description="数字孪生需要指定具体企业,请在左上角企业选择器中选择" />
) : (
<>
<Card>
<h3 className="font-medium text-gray-900"></h3>
<textarea
value={companyData}
onChange={(e) => setCompanyData(e.target.value)}
placeholder="输入企业数据..."
className="mt-2 w-full rounded-md border border-gray-300 p-3 text-sm"
rows={3}
/>
<button
onClick={handleBuild}
className="mt-2 flex items-center gap-1 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-700"
>
<Sparkles size={16} />
</button>
</Card>
<Card>
<h3 className="font-medium text-gray-900"></h3>
<input
type="text"
value={scenario}
onChange={(e) => setScenario(e.target.value)}
placeholder="输入场景描述(如:融资 5000 万)"
className="mt-2 w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm"
/>
<button
onClick={handleSimulate}
className="mt-2 flex items-center gap-1 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-700"
>
<Sparkles size={16} />
</button>
{simResult && (
<div className="mt-3 text-sm text-gray-600">
<p>{simResult.projected_outcome}</p>
{simResult.confidence != null && (
<p className="mt-1"><Badge color="blue">{(simResult.confidence * 100).toFixed(0)}%</Badge></p>
)}
</div>
)}
</Card>
{isLoading ? (
<LoadingSpinner />
) : items.length === 0 ? (
<EmptyState description="暂无数字孪生模型" />
) : (
<div className="space-y-3">
{items.map((item, i) => (
<Card key={i}>
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900"></h3>
{item.accuracy_score != null && (
<Badge color={item.accuracy_score > 0.7 ? "green" : "amber"}>
{(item.accuracy_score * 100).toFixed(0)}%
</Badge>
)}
</div>
</Card>
))}
</div>
)}
</>
)}
</PageContainer>
);
}