Files
AIPortPilot/frontend/src/app/(investor)/portfolio/page.tsx
T
selfrelease f4ddcab2ca test(frontend): UIUX E2E 测试 — 新增路由导航/创始人端/Admin端/工作台/移动端适配
新增 6 个 E2E 测试文件,覆盖 2-task-uiux.md 全部 50 项任务:
- uiux-navigation.spec.ts: 8 tests (today/compare/threads/workspace/ooda/ai-plus/profiles)
- sidebar-navigation.spec.ts: 6 tests (投资人 Sidebar 6 业务域)
- founder-uiux.spec.ts: 7 tests (创始人端 6 域导航)
- admin-uiux.spec.ts: 3 tests (Admin 6 管理域 + 商业秘密保护)
- workbench-uiux.spec.ts: 5 tests (Highlights/WorkMode/Tab/InsightRail)
- mobile-uiux.spec.ts: 5 tests (移动端抽屉导航 + 创始人底部导航)

同时修复全部 no-explicit-any 警告,替换为 TypeScript 接口定义。

测试结果: 41 E2E passed, 405 backend passed
2026-07-19 20:37:25 +08:00

98 lines
3.4 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 { useState } from "react";
import {Sparkles } from "lucide-react";
import { rebalancePortfolio, runMonteCarlo } from "@/lib/api-v2";
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
import { toast } from "sonner";
/** 再平衡结果。 */
interface RebalanceResult {
irr_impact?: number;
dpi_impact?: number;
}
/** Monte Carlo 结果。 */
interface MonteCarloResult {
percentile_p5?: string;
percentile_p50?: string;
percentile_p95?: string;
}
export default function PortfolioPage() {
const [rebalanceResult, setRebalanceResult] = useState<RebalanceResult | null>(null);
const [mcResult, setMcResult] = useState<MonteCarloResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleRebalance = async () => {
setIsLoading(true);
try {
const resp = await rebalancePortfolio([]);
setRebalanceResult(resp.data as RebalanceResult);
} catch {
toast.error("分析失败");
} finally {
setIsLoading(false);
}
};
const handleMonteCarlo = async () => {
setIsLoading(true);
try {
const resp = await runMonteCarlo([0.15, 0.22, 0.08, 0.35, 0.12]);
setMcResult(resp.data as MonteCarloResult);
} catch {
toast.error("模拟失败");
} finally {
setIsLoading(false);
}
};
return (
<PageContainer title="组合管理" description="边际回报率计算 + 组合再平衡 + Monte Carlo 模拟">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card>
<h3 className="font-medium text-gray-900"></h3>
<p className="mt-1 text-sm text-gray-500">AI </p>
<button
onClick={handleRebalance}
disabled={isLoading}
className="mt-3 flex items-center gap-1 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-700 disabled:opacity-50"
>
<Sparkles size={16} />
</button>
{rebalanceResult && (
<div className="mt-3 space-y-2 text-sm">
{rebalanceResult.irr_impact != null && (
<p>IRR <Badge color="green">+{rebalanceResult.irr_impact}%</Badge></p>
)}
{rebalanceResult.dpi_impact != null && (
<p>DPI <Badge color="green">+{rebalanceResult.dpi_impact}</Badge></p>
)}
</div>
)}
</Card>
<Card>
<h3 className="font-medium text-gray-900">Monte Carlo </h3>
<p className="mt-1 text-sm text-gray-500">10000 IRR/DPI </p>
<button
onClick={handleMonteCarlo}
disabled={isLoading}
className="mt-3 flex items-center gap-1 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-700 disabled:opacity-50"
>
<Sparkles size={16} />
</button>
{mcResult && (
<div className="mt-3 space-y-2 text-sm">
<p>P5: <Badge color="red">{mcResult.percentile_p5}</Badge></p>
<p>P50: <Badge color="amber">{mcResult.percentile_p50}</Badge></p>
<p>P95: <Badge color="green">{mcResult.percentile_p95}</Badge></p>
</div>
)}
</Card>
</div>
</PageContainer>
);
}