2025-04-09 09:45:06 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import type React from "react"
|
2025-04-24 12:17:09 +08:00
|
|
|
|
import { useState, useEffect, createContext, useContext } from "react"
|
|
|
|
|
|
import { useRouter, usePathname } from "next/navigation"
|
2025-04-09 09:45:06 +08:00
|
|
|
|
import { Button } from "@/components/ui/button"
|
2025-04-10 11:54:21 +08:00
|
|
|
|
import { Menu, X } from "lucide-react"
|
2025-04-09 17:21:29 +08:00
|
|
|
|
import { Sidebar } from "@/components/layout/sidebar"
|
|
|
|
|
|
import { Header } from "@/components/layout/header"
|
2025-04-10 11:54:21 +08:00
|
|
|
|
import { getAdminInfo } from "@/lib/utils"
|
2025-04-09 09:45:06 +08:00
|
|
|
|
|
2025-04-24 12:17:09 +08:00
|
|
|
|
// 全局标签页管理上下文
|
|
|
|
|
|
interface TabData {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
path: string
|
|
|
|
|
|
closable: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TabContextType {
|
|
|
|
|
|
tabs: TabData[]
|
|
|
|
|
|
activeTab: string
|
|
|
|
|
|
addTab: (tab: Omit<TabData, "id">) => string
|
|
|
|
|
|
closeTab: (tabId: string) => void
|
|
|
|
|
|
setActiveTab: (tabId: string) => void
|
|
|
|
|
|
findTabByPath: (path: string) => TabData | undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const TabContext = createContext<TabContextType | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
|
|
export function useTabContext() {
|
|
|
|
|
|
const context = useContext(TabContext);
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
throw new Error("useTabContext must be used within a TabProvider");
|
|
|
|
|
|
}
|
|
|
|
|
|
return context;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-09 09:45:06 +08:00
|
|
|
|
export default function DashboardLayout({
|
|
|
|
|
|
children,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
2025-04-10 11:54:21 +08:00
|
|
|
|
const router = useRouter()
|
2025-04-24 12:17:09 +08:00
|
|
|
|
const pathname = usePathname()
|
|
|
|
|
|
|
|
|
|
|
|
// 标签页状态管理
|
|
|
|
|
|
const [tabs, setTabs] = useState<TabData[]>([
|
|
|
|
|
|
{ id: "dashboard", label: "仪表盘", path: "/dashboard", closable: false }
|
|
|
|
|
|
])
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState("dashboard")
|
|
|
|
|
|
|
|
|
|
|
|
// 添加标签页
|
|
|
|
|
|
const addTab = (tabData: Omit<TabData, "id">) => {
|
|
|
|
|
|
const id = `tab-${Date.now()}`
|
|
|
|
|
|
const newTab = { id, ...tabData }
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在类似标签(基于路径)
|
|
|
|
|
|
const existingTab = findTabByPath(tabData.path)
|
|
|
|
|
|
|
|
|
|
|
|
if (existingTab) {
|
|
|
|
|
|
// 如果已存在,激活它
|
|
|
|
|
|
setActiveTab(existingTab.id)
|
|
|
|
|
|
// 确保导航到该路径(即使路径匹配也强制导航)
|
|
|
|
|
|
router.push(tabData.path)
|
|
|
|
|
|
return existingTab.id
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果不存在,添加新标签
|
|
|
|
|
|
setTabs(prev => [...prev, newTab])
|
|
|
|
|
|
setActiveTab(id)
|
|
|
|
|
|
// 确保导航到该路径(即使路径匹配也强制导航)
|
|
|
|
|
|
router.push(tabData.path)
|
|
|
|
|
|
return id
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置激活标签(并导航到对应路径)
|
|
|
|
|
|
const setActiveTabAndNavigate = (tabId: string) => {
|
|
|
|
|
|
setActiveTab(tabId)
|
|
|
|
|
|
// 找到标签对应的路径并导航
|
|
|
|
|
|
const tab = tabs.find(tab => tab.id === tabId)
|
|
|
|
|
|
if (tab) {
|
|
|
|
|
|
router.push(tab.path)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭标签页
|
|
|
|
|
|
const closeTab = (tabId: string) => {
|
|
|
|
|
|
// 找到要关闭的标签的索引
|
|
|
|
|
|
const tabIndex = tabs.findIndex(tab => tab.id === tabId)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果标签不存在或者是不可关闭的标签,直接返回
|
|
|
|
|
|
if (tabIndex === -1 || !tabs[tabIndex].closable) return
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的标签数组,移除要关闭的标签
|
|
|
|
|
|
const newTabs = tabs.filter(tab => tab.id !== tabId)
|
|
|
|
|
|
setTabs(newTabs)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果关闭的是当前活动标签,需要激活另一个标签
|
|
|
|
|
|
if (activeTab === tabId) {
|
|
|
|
|
|
// 优先激活关闭标签左侧的标签,如果没有则激活默认的仪表盘标签
|
|
|
|
|
|
const newActiveTab = newTabs[tabIndex - 1]?.id || "dashboard"
|
|
|
|
|
|
setActiveTab(newActiveTab)
|
|
|
|
|
|
|
|
|
|
|
|
// 路由跳转到新激活的标签对应的路径
|
|
|
|
|
|
const newActivePath = newTabs.find(tab => tab.id === newActiveTab)?.path || "/dashboard"
|
|
|
|
|
|
router.push(newActivePath)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据路径查找标签
|
|
|
|
|
|
const findTabByPath = (path: string): TabData | undefined => {
|
|
|
|
|
|
return tabs.find(tab => tab.path === path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听路径变化,自动添加标签
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 不触发/dashboard路径,已有默认标签
|
|
|
|
|
|
if (pathname === "/dashboard") {
|
|
|
|
|
|
setActiveTab("dashboard")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查当前路径是否已有对应标签
|
|
|
|
|
|
const existingTab = findTabByPath(pathname)
|
|
|
|
|
|
|
|
|
|
|
|
if (existingTab) {
|
|
|
|
|
|
// 如果存在,激活它
|
|
|
|
|
|
setActiveTab(existingTab.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果不存在,添加新标签
|
|
|
|
|
|
// 生成标签标题
|
|
|
|
|
|
let label = "新标签"
|
|
|
|
|
|
|
|
|
|
|
|
// 根据路径生成更友好的标签名
|
|
|
|
|
|
if (pathname.includes("/projects")) {
|
|
|
|
|
|
if (pathname === "/dashboard/projects") {
|
|
|
|
|
|
label = "项目列表"
|
|
|
|
|
|
} else if (pathname.includes("/new")) {
|
|
|
|
|
|
label = "新建项目"
|
|
|
|
|
|
} else if (pathname.includes("/edit")) {
|
|
|
|
|
|
label = "编辑项目"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
label = "项目详情"
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (pathname.includes("/admins")) {
|
|
|
|
|
|
label = "管理员"
|
|
|
|
|
|
} else if (pathname.includes("/customers")) {
|
|
|
|
|
|
label = "客户池"
|
|
|
|
|
|
} else if (pathname.includes("/settings")) {
|
|
|
|
|
|
label = "系统设置"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addTab({
|
|
|
|
|
|
label,
|
|
|
|
|
|
path: pathname,
|
|
|
|
|
|
closable: true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [pathname])
|
2025-04-10 11:54:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 认证检查
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const checkAuth = () => {
|
|
|
|
|
|
const adminInfo = getAdminInfo()
|
|
|
|
|
|
if (!adminInfo) {
|
|
|
|
|
|
// 未登录时跳转到登录页
|
|
|
|
|
|
router.push('/login')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
}, [router])
|
2025-04-09 09:45:06 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
2025-04-24 12:17:09 +08:00
|
|
|
|
<TabContext.Provider value={{
|
|
|
|
|
|
tabs,
|
|
|
|
|
|
activeTab,
|
|
|
|
|
|
addTab,
|
|
|
|
|
|
closeTab,
|
|
|
|
|
|
setActiveTab: setActiveTabAndNavigate,
|
|
|
|
|
|
findTabByPath
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div className="flex h-screen overflow-hidden bg-background">
|
|
|
|
|
|
{/* Mobile sidebar toggle */}
|
|
|
|
|
|
<div className="fixed top-4 left-4 z-50 md:hidden">
|
|
|
|
|
|
<Button variant="outline" size="icon" onClick={() => setSidebarOpen(!sidebarOpen)}>
|
|
|
|
|
|
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-04-09 09:45:06 +08:00
|
|
|
|
|
2025-04-24 12:17:09 +08:00
|
|
|
|
{/* Sidebar */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`bg-background flex-shrink-0 transition-all duration-300 ease-in-out ${
|
|
|
|
|
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
|
|
|
|
} md:translate-x-0 fixed md:relative z-40 h-full`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Sidebar />
|
|
|
|
|
|
</div>
|
2025-04-09 09:45:06 +08:00
|
|
|
|
|
2025-04-24 12:17:09 +08:00
|
|
|
|
{/* Main content */}
|
|
|
|
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
|
|
|
|
<Header />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 标签栏 */}
|
|
|
|
|
|
<div className="border-b border-border">
|
|
|
|
|
|
<div className="flex overflow-x-auto">
|
|
|
|
|
|
{tabs.map(tab => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={tab.id}
|
|
|
|
|
|
className={`flex items-center px-4 py-2 border-r border-border cursor-pointer ${
|
|
|
|
|
|
activeTab === tab.id ? "bg-muted font-medium" : "hover:bg-muted/50"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setActiveTabAndNavigate(tab.id)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="truncate max-w-[200px]">{tab.label}</span>
|
|
|
|
|
|
{tab.closable && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="ml-2 p-1 rounded-full hover:bg-muted-foreground/20"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
closeTab(tab.id)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 内容区域 */}
|
|
|
|
|
|
<main className="flex-1 overflow-y-auto p-6">
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
2025-04-09 09:45:06 +08:00
|
|
|
|
</div>
|
2025-04-24 12:17:09 +08:00
|
|
|
|
</TabContext.Provider>
|
2025-04-09 09:45:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|