feat(frontend): P5+P6 多主体画像 + 移动端抽屉导航

P5 多主体画像:
- /profiles 页面:根据角色展示不同视角(GP/投后负责人/投资经理/创始人/管理员)

P6 移动端适配:
- 投资人端 Sidebar 折叠为抽屉导航(Menu 按钮触发 + 遮罩层 + 6 域完整导航)
- 移动端导航点击后自动关闭抽屉
This commit is contained in:
selfrelease
2026-07-19 19:10:38 +08:00
parent 778c2e8c5c
commit 3a905da35b
2 changed files with 149 additions and 4 deletions
+75 -4
View File
@@ -4,7 +4,8 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { Menu, X } from "lucide-react";
import { CopilotWidget } from "@/components/shared/CopilotWidget";
import { RiskToastNotifier } from "@/components/shared/RiskToastNotifier";
import { AuthGuard } from "@/components/shared/AuthGuard";
@@ -19,6 +20,7 @@ import { getNavDomains, NAV_FOOTER } from "@/lib/navConfig";
export default function InvestorLayout({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const pathname = usePathname();
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const domains = useMemo(() => getNavDomains(user?.role ?? "investor"), [user?.role]);
@@ -76,10 +78,79 @@ export default function InvestorLayout({ children }: { children: React.ReactNode
{/* 内容区 */}
<main className="flex-1 bg-[var(--investor-content-bg)]">
{/* 移动端顶部 Header */}
<header className="sticky top-0 z-10 flex h-14 items-center bg-[var(--investor-sidebar-bg)] px-4 text-white md:hidden">
<span className="font-bold">AIPortPilot</span>
{/* 移动端顶部 Header + 抽屉导航 */}
<header className="sticky top-0 z-30 flex h-14 items-center justify-between bg-[var(--investor-sidebar-bg)] px-4 text-white md:hidden">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setMobileNavOpen(true)}
className="text-white"
aria-label="打开导航菜单"
>
<Menu size={20} aria-hidden="true" />
</button>
<span className="font-bold">AIPortPilot</span>
</div>
</header>
{/* 移动端抽屉导航 */}
{mobileNavOpen && (
<div className="fixed inset-0 z-40 md:hidden">
<div className="absolute inset-0 bg-black/50" onClick={() => setMobileNavOpen(false)} />
<div className="absolute left-0 top-0 h-full w-64 overflow-y-auto bg-[var(--investor-sidebar-bg)] text-white">
<div className="flex h-14 items-center justify-between px-4">
<span className="font-bold">AIPortPilot</span>
<button
type="button"
onClick={() => setMobileNavOpen(false)}
className="text-white"
aria-label="关闭导航菜单"
>
<X size={20} aria-hidden="true" />
</button>
</div>
<nav className="flex flex-col gap-1 px-3 py-2">
{domains.map((domain) => (
<div key={domain.key} className="mb-2">
<div className="px-3 py-1 text-xs font-medium text-white/40">{domain.title}</div>
{domain.items.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + "/");
return (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileNavOpen(false)}
className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors ${
isActive
? "bg-white/15 text-white"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
<item.icon size={16} aria-hidden="true" />
{item.label}
</Link>
);
})}
</div>
))}
<div className="mt-auto border-t border-white/10 pt-2">
{NAV_FOOTER.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileNavOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 hover:bg-white/10"
>
<item.icon size={16} aria-hidden="true" />
{item.label}
</Link>
))}
</div>
</nav>
</div>
</div>
)}
<div className="container mx-auto max-w-7xl px-4 py-6">
<AuthGuard>{children}</AuthGuard>
</div>
@@ -0,0 +1,74 @@
/** 多主体画像 — 根据角色展示不同视角的企业画像。 */
"use client";
import { Users, Building2, TrendingUp, AlertTriangle, Target, Lightbulb } from "lucide-react";
import { useAuth } from "@/lib/auth-context";
/** 角色视角配置。 */
const ROLE_PERSPECTIVES: Record<string, { title: string; focus: string[]; icon: typeof Users }> = {
gp: {
title: "GP 视角 — 组合层面",
focus: ["组合健康度分布", "基金 IRR 追踪", "退出时机", "LP 汇报"],
icon: TrendingUp,
},
post_invest_lead: {
title: "投后负责人视角 — 运营层面",
focus: ["企业健康度趋势", "风险队列", "任务推进", "月报审阅"],
icon: Target,
},
investor: {
title: "投资经理视角 — 执行层面",
focus: ["今日行动", "企业跟进", "协同匹配", "数据校验"],
icon: Lightbulb,
},
founder: {
title: "创始人视角 — 经营层面",
focus: ["经营 KPI", "融资准备", "投资人沟通", "团队建设"],
icon: Building2,
},
admin: {
title: "管理员视角 — 系统层面",
focus: ["租户管理", "用户权限", "审计日志", "数据源监控"],
icon: Users,
},
};
/** 多主体画像页面。 */
export default function ProfilePage() {
const { user } = useAuth();
const role = user?.role ?? "investor";
const perspective = ROLE_PERSPECTIVES[role] ?? ROLE_PERSPECTIVES.investor;
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<perspective.icon className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold"></h1>
</div>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<h2 className="mb-3 font-medium">{perspective.title}</h2>
<div className="grid grid-cols-2 gap-3">
{perspective.focus.map((item, i) => (
<div key={i} className="rounded-md border p-3 text-sm">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-indigo-500" />
<span className="font-medium">{item}</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
</p>
</div>
))}
</div>
</div>
{/* 角色切换提示 */}
<div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 text-sm text-indigo-700">
<span className="font-medium">{role}</span>
</div>
</div>
);
}