182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import {
|
||
FileText,
|
||
FolderArchive,
|
||
Settings,
|
||
Globe,
|
||
FileStack,
|
||
Settings2,
|
||
ChevronDown,
|
||
} from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
import { Separator } from "@/components/ui/separator";
|
||
import { Button } from "@/components/ui/button";
|
||
import { HistoricalReferences } from "@/components/historical-references";
|
||
import { cloudDocsApi, portalLinksApi, type CloudDocLinkRead, type PortalLinkRead } from "@/lib/api/client";
|
||
|
||
export function AppSidebar() {
|
||
const pathname = usePathname();
|
||
const [cloudDocs, setCloudDocs] = useState<CloudDocLinkRead[]>([]);
|
||
const [portalLinks, setPortalLinks] = useState<PortalLinkRead[]>([]);
|
||
const [projectArchiveOpen, setProjectArchiveOpen] = useState(false);
|
||
|
||
const loadCloudDocs = useCallback(async () => {
|
||
try {
|
||
const list = await cloudDocsApi.list();
|
||
setCloudDocs(list);
|
||
} catch {
|
||
setCloudDocs([]);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadCloudDocs();
|
||
}, [loadCloudDocs]);
|
||
|
||
useEffect(() => {
|
||
const onCloudDocsChanged = () => loadCloudDocs();
|
||
window.addEventListener("cloud-docs-changed", onCloudDocsChanged);
|
||
return () => window.removeEventListener("cloud-docs-changed", onCloudDocsChanged);
|
||
}, [loadCloudDocs]);
|
||
|
||
const loadPortalLinks = useCallback(async () => {
|
||
try {
|
||
const list = await portalLinksApi.list();
|
||
setPortalLinks(list);
|
||
} catch {
|
||
setPortalLinks([]);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadPortalLinks();
|
||
}, [loadPortalLinks]);
|
||
|
||
useEffect(() => {
|
||
const onPortalLinksChanged = () => loadPortalLinks();
|
||
window.addEventListener("portal-links-changed", onPortalLinksChanged);
|
||
return () => window.removeEventListener("portal-links-changed", onPortalLinksChanged);
|
||
}, [loadPortalLinks]);
|
||
|
||
const nav = [
|
||
{ href: "/workspace", label: "需求与方案", icon: FileText },
|
||
{ href: "/finance", label: "财务归档", icon: FolderArchive },
|
||
{ href: "/settings", label: "设置", icon: Settings },
|
||
];
|
||
|
||
return (
|
||
<aside className="flex w-56 flex-col border-r bg-card text-card-foreground">
|
||
<div className="p-4">
|
||
<Link href="/" className="font-semibold text-foreground">
|
||
Ops-Core
|
||
</Link>
|
||
<p className="text-xs text-muted-foreground mt-0.5">自动化办公中台</p>
|
||
</div>
|
||
<Separator />
|
||
<nav className="flex flex-1 flex-col gap-1 p-2">
|
||
{nav.map((item) => (
|
||
<Link key={item.href} href={item.href}>
|
||
<Button
|
||
variant={pathname === item.href ? "secondary" : "ghost"}
|
||
size="sm"
|
||
className="w-full justify-start"
|
||
>
|
||
<item.icon className="mr-2 h-4 w-4" />
|
||
{item.label}
|
||
</Button>
|
||
</Link>
|
||
))}
|
||
</nav>
|
||
<Separator />
|
||
{/* 项目档案放在云文档之前,支持收纳折叠 */}
|
||
<div className="px-2 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setProjectArchiveOpen((v) => !v)}
|
||
className="w-full flex items-center justify-between text-xs text-muted-foreground hover:text-foreground"
|
||
>
|
||
<span>项目档案</span>
|
||
<ChevronDown
|
||
className={cn(
|
||
"h-3 w-3 transition-transform",
|
||
projectArchiveOpen ? "rotate-180" : "rotate-0"
|
||
)}
|
||
/>
|
||
</button>
|
||
</div>
|
||
{projectArchiveOpen && <HistoricalReferences />}
|
||
<Separator />
|
||
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col">
|
||
<div className="p-2 shrink-0">
|
||
<div className="flex items-center justify-between px-2 mb-2">
|
||
<p className="text-xs font-medium text-muted-foreground">云文档</p>
|
||
<Link
|
||
href="/settings/cloud-docs"
|
||
className="text-xs text-muted-foreground hover:text-foreground"
|
||
title="管理云文档入口"
|
||
>
|
||
<Settings2 className="h-3.5 w-3.5" />
|
||
</Link>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
{cloudDocs.length === 0 ? (
|
||
<p className="text-xs text-muted-foreground px-2">暂无入口,去设置添加</p>
|
||
) : (
|
||
cloudDocs.map((doc) => (
|
||
<a
|
||
key={doc.id}
|
||
href={doc.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className={cn(
|
||
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||
)}
|
||
>
|
||
<FileStack className="h-4 w-4 shrink-0" />
|
||
<span className="truncate">{doc.name}</span>
|
||
</a>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="p-2 shrink-0">
|
||
<div className="flex items-center justify-between px-2 mb-2">
|
||
<p className="text-xs font-medium text-muted-foreground">快捷门户</p>
|
||
<Link
|
||
href="/settings/portal-links"
|
||
className="text-xs text-muted-foreground hover:text-foreground"
|
||
title="管理快捷门户"
|
||
>
|
||
<Settings2 className="h-3.5 w-3.5" />
|
||
</Link>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
{portalLinks.length === 0 ? (
|
||
<p className="text-xs text-muted-foreground px-2">暂无入口,去设置添加</p>
|
||
) : (
|
||
portalLinks.map((link) => (
|
||
<a
|
||
key={link.id}
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className={cn(
|
||
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||
)}
|
||
>
|
||
<Globe className="h-4 w-4 shrink-0" />
|
||
<span className="truncate">{link.name}</span>
|
||
</a>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
);
|
||
}
|