'use client' import Link from 'next/link' import { BellRing, Blocks, BookOpen, Bot, BriefcaseBusiness, Building2, ChartColumn, ChartNoAxesCombined, ChevronDown, ClipboardCheck, Gauge, GaugeCircle, LibraryBig, Network, Settings2, ShieldCheck, SlidersHorizontal, TrendingUp, UsersRound, Workflow, WandSparkles, X, type LucideIcon, } from 'lucide-react' import { useEffect, useRef, useState, type ReactNode } from 'react' import DepartmentAccountMenu from './DepartmentAccountMenu' import DepartmentHeader from './DepartmentHeader' import { WORKSPACE_SECTIONS, type WorkspaceSectionKey } from './workspace-navigation' const SECTION_ICONS: Record = { dashboard: Gauge, business: BriefcaseBusiness, assets: Bot, organization: UsersRound, governance: Settings2, } const ITEM_ICONS: Record = { 'core-metrics': GaugeCircle, 'business-trends': TrendingUp, 'team-capacity': UsersRound, 'performance-conversion': ChartNoAxesCombined, 'material-library': LibraryBig, 'brand-knowledge': BookOpen, 'automation-workflows': Workflow, 'value-metrics': ChartColumn, 'organization-structure': Network, 'team-functions': Blocks, 'performance-review': ClipboardCheck, permissions: ShieldCheck, 'editing-capabilities': SlidersHorizontal, notifications: BellRing, } function readPathname() { return typeof window === 'undefined' ? '/' : window.location.pathname } function getPageTitle(pathname: string) { if (pathname.startsWith('/assets/detail-generator')) return '详情页生成' if (pathname === '/') return '部门总览' for (const section of WORKSPACE_SECTIONS) { const item = section.items.find((candidate) => pathname.startsWith(candidate.href)) if (item) return item.title } return '品牌中心部门看板' } export default function DepartmentShell({ children }: { children: ReactNode }) { const [pathname, setPathname] = useState('/') const [navigationCollapsed, setNavigationCollapsed] = useState(false) const [mobileNavigationOpen, setMobileNavigationOpen] = useState(false) const [isMobileViewport, setIsMobileViewport] = useState(false) const [canUseDetailGenerator, setCanUseDetailGenerator] = useState(false) const navigationTriggerRef = useRef(null) const mobileDialogRef = useRef(null) const contentRootRef = useRef(null) const navigationSections = WORKSPACE_SECTIONS.map((section) => section.key === 'assets' && canUseDetailGenerator ? { ...section, items: [...section.items, { key: 'detail-generator', title: '详情页生成', href: '/assets/detail-generator', status: 'connected' as const }] } : section) const activeSection = navigationSections.find((section) => section.items.some((item) => pathname.startsWith(item.href))) const [expandedSections, setExpandedSections] = useState(activeSection ? [activeSection.key] : ['dashboard']) useEffect(() => { setPathname(readPathname()) }, []) useEffect(() => { const controller = new AbortController() fetch('/api/detail-generator/capabilities', { cache: 'no-store', signal: controller.signal }) .then((response) => response.ok ? response.json() : null) .then((capability) => setCanUseDetailGenerator(capability?.canUse === true)) .catch(() => setCanUseDetailGenerator(false)) return () => controller.abort() }, []) useEffect(() => { if (typeof window.matchMedia !== 'function') return const media = window.matchMedia('(max-width: 1023px)') const syncViewport = () => { setIsMobileViewport(media.matches) if (!media.matches) setMobileNavigationOpen(false) } syncViewport() media.addEventListener('change', syncViewport) return () => media.removeEventListener('change', syncViewport) }, []) useEffect(() => { if (!mobileNavigationOpen) return const dialog = mobileDialogRef.current const contentRoot = contentRootRef.current const navigationTrigger = navigationTriggerRef.current const previousOverflow = document.body.style.overflow contentRoot?.setAttribute('inert', '') document.body.style.overflow = 'hidden' const focusableSelector = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])' const focusables = dialog ? Array.from(dialog.querySelectorAll(focusableSelector)) : [] ;(dialog?.querySelector('[aria-current="page"]') || focusables[0])?.focus() const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault() setMobileNavigationOpen(false) return } if (event.key !== 'Tab' || focusables.length === 0) return const first = focusables[0] const last = focusables[focusables.length - 1] if (event.shiftKey && document.activeElement === first) { event.preventDefault() last.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault() first.focus() } } document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('keydown', handleKeyDown) document.body.style.overflow = previousOverflow contentRoot?.removeAttribute('inert') navigationTrigger?.focus() } }, [mobileNavigationOpen]) useEffect(() => { if (activeSection && !expandedSections.includes(activeSection.key)) { setExpandedSections((current) => [...current, activeSection.key]) } }, [activeSection, expandedSections]) const toggleSection = (key: WorkspaceSectionKey) => { setExpandedSections((current) => current.includes(key) ? current.filter((candidate) => candidate !== key) : [...current, key]) } const toggleNavigation = () => { if (typeof window.matchMedia === 'function' && window.matchMedia('(max-width: 1023px)').matches) { setMobileNavigationOpen((current) => !current) } else { setNavigationCollapsed((current) => !current) } } const sidebar = ( ) return (
{mobileNavigationOpen && ( <> ) }