176 lines
8.9 KiB
React
176 lines
8.9 KiB
React
import { Link, Outlet, useLocation, useNavigate } from '../shared/router.jsx'
|
|
import { cn } from '../lib/utils'
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarInset,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarProvider,
|
|
SidebarRail,
|
|
SidebarTrigger,
|
|
useSidebar,
|
|
} from '../components/ui/sidebar'
|
|
import { Separator } from '../components/ui/separator'
|
|
|
|
const menu = [
|
|
{
|
|
section: '运营',
|
|
items: [
|
|
{ to: '/accounts/import', icon: 'ri-upload-cloud-2-line', label: '账号导入' },
|
|
{ to: '/accounts', icon: 'ri-account-circle-line', label: '我的账号' },
|
|
{ to: '/accounts/monitoring', icon: 'ri-eye-line', label: '监控账号' },
|
|
{ to: '/creator/competitors', icon: 'ri-line-chart-line', label: '竞品分析' },
|
|
{ to: '/creator/workbench', icon: 'ri-chat-3-line', label: '运营工作台' },
|
|
{ to: '/creator/settings', icon: 'ri-settings-3-line', label: '采集设置' },
|
|
{ to: '/tasks', icon: 'ri-task-line', label: '任务中心' },
|
|
],
|
|
},
|
|
{
|
|
section: '资源',
|
|
items: [
|
|
{ to: '/browsers', icon: 'ri-hard-drive-3-line', label: '运行环境' },
|
|
{ to: '/network-exits', icon: 'ri-earth-line', label: '网络出口' },
|
|
{ to: '/gateways', icon: 'ri-router-line', label: '网关管理' },
|
|
],
|
|
},
|
|
]
|
|
|
|
function isActivePath(pathname, to) {
|
|
return pathname === to || pathname.startsWith(`${to}/`)
|
|
}
|
|
|
|
function MenuLink({ item, pathname }) {
|
|
const { isMobile, setOpenMobile } = useSidebar()
|
|
return (
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton asChild isActive={isActivePath(pathname, item.to)} tooltip={item.label}>
|
|
<Link to={item.to} aria-label={item.label} onClick={() => { if (isMobile) setOpenMobile(false) }}>
|
|
<i className={cn(item.icon, 'size-4 shrink-0 text-base leading-none')} aria-hidden="true" />
|
|
<span>{item.label}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
)
|
|
}
|
|
|
|
function AppSidebar({ pathname }) {
|
|
const navigate = useNavigate()
|
|
return (
|
|
<Sidebar variant="inset" collapsible="icon">
|
|
<SidebarHeader>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton asChild size="lg" tooltip="CreatorHub">
|
|
<Link to="/accounts" aria-label="CreatorHub 首页">
|
|
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
|
|
<i className="ri-stack-line text-lg" aria-hidden="true" />
|
|
</span>
|
|
<span className="grid flex-1 text-left text-sm leading-tight">
|
|
<span className="truncate font-semibold">CreatorHub</span>
|
|
<span className="truncate text-xs text-sidebar-foreground/60">运营控制台</span>
|
|
</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
<SidebarContent className="overflow-x-hidden">
|
|
{menu.map((group) => (
|
|
<SidebarGroup key={group.section} className="py-1">
|
|
<SidebarGroupLabel>{group.section}</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{group.items.map((item) => <MenuLink key={item.to} item={item} pathname={pathname} />)}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
))}
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
tooltip="退出登录"
|
|
onClick={() => {
|
|
window.localStorage.removeItem('creatorhub.auth')
|
|
navigate({ to: '/login', replace: true })
|
|
}}
|
|
>
|
|
<i className="ri-logout-box-r-line size-4 shrink-0" aria-hidden="true" />
|
|
<span>退出登录</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarFooter>
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
)
|
|
}
|
|
|
|
const pageMetadataRules = [
|
|
{ test: (path) => path === '/accounts/import', title: '账号导入', subtitle: '管理分享链接解析任务,解析成功后自动加入监控账号。' },
|
|
{ test: (path) => path === '/accounts', title: '我的账号', subtitle: '创建和管理自己维护的账号。' },
|
|
{ test: (path) => path === '/accounts/monitoring', title: '监控账号', subtitle: '管理需要持续跟踪的竞品账号及采集状态。' },
|
|
{ test: (path) => path === '/accounts/new', title: '创建社媒账号', subtitle: '创建账号后,再在编辑页配置登录身份与账号策略。' },
|
|
{ test: (path) => path.endsWith('/edit') && path.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
|
|
{ test: (path) => path.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
|
|
{ test: (path) => path === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
|
|
{ test: (path) => path === '/creator/workbench', title: '运营工作台', subtitle: '评论、线索、私信与写操作均保留来源和明确结果。' },
|
|
{ test: (path) => path === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' },
|
|
{ test: (path) => path === '/competitors' || path.startsWith('/competitors/'), title: '竞品分析', subtitle: '粘贴作品分享内容,自动识别作者并加入监听队列。' },
|
|
{ test: (path) => path.startsWith('/drafts/'), title: '草稿核对', subtitle: '核对版本化草稿,确认后再将操作加入队列。' },
|
|
{ test: (path) => path.startsWith('/attempts/'), title: 'Attempt 详情', subtitle: '查看执行结果、运行环境与网络出口。' },
|
|
{ test: (path) => path === '/tasks', title: '任务中心', subtitle: '逐条处理暂停、版本变化与未知执行结果。' },
|
|
{ test: (path) => path.startsWith('/tasks/'), title: '任务详情', subtitle: '查看任务状态、执行结果与允许的下一步操作。' },
|
|
{ test: (path) => path === '/browsers', title: '运行环境', subtitle: '启动、停止并回收隔离的指纹浏览器运行环境。' },
|
|
{ test: (path) => path === '/browsers/new', title: '创建运行环境', subtitle: '为已授权且暂停的账号绑定网关、指纹与网络出口。' },
|
|
{ test: (path) => path.startsWith('/browsers/'), title: '运行环境详情', subtitle: '查看环境状态、账号绑定和运行实例信息。' },
|
|
{ test: (path) => path === '/network-exits', title: '网络出口', subtitle: '维护固定出口、健康观测与账号绑定。' },
|
|
{ test: (path) => path.startsWith('/network-exits/'), title: '网络出口详情', subtitle: '查看出口配置、健康状态和绑定关系。' },
|
|
{ test: (path) => path === '/gateways', title: '网关管理', subtitle: '注册并检查宿主机 native browser gateway。' },
|
|
{ test: (path) => path.startsWith('/gateways/'), title: '网关详情', subtitle: '查看网关连接信息和运行状态。' },
|
|
]
|
|
|
|
function pageMetadata(pathname) {
|
|
const page = pageMetadataRules.find((item) => item.test(pathname))
|
|
if (page) return { title: page.title, subtitle: page.subtitle }
|
|
for (const group of menu) {
|
|
const match = group.items.find((item) => isActivePath(pathname, item.to))
|
|
if (match) return { title: match.label, subtitle: 'CreatorHub 运营控制台' }
|
|
}
|
|
return { title: 'CreatorHub', subtitle: '运营控制台' }
|
|
}
|
|
|
|
export function CreatorHubLayout({ children }) {
|
|
const { pathname } = useLocation()
|
|
const metadata = pageMetadata(pathname)
|
|
return (
|
|
<SidebarProvider>
|
|
<AppSidebar pathname={pathname} />
|
|
<SidebarInset id="main-content" tabIndex={-1} className="min-w-0 max-w-full">
|
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-card focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-md">
|
|
跳到主要内容
|
|
</a>
|
|
<header className="sticky top-0 z-20 flex h-14 shrink-0 items-center gap-2 border-b bg-background/90 px-4 backdrop-blur">
|
|
<SidebarTrigger className="-ml-1" aria-label="打开或收起导航" />
|
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
<div className="min-w-0">
|
|
<h1 className="truncate text-sm font-semibold text-foreground">{metadata.title}</h1>
|
|
<p className="hidden truncate text-xs text-muted-foreground sm:block">{metadata.subtitle}</p>
|
|
</div>
|
|
</header>
|
|
<main className="w-full min-w-0 flex-1 px-4 py-6 sm:px-6 lg:px-8">{children ?? <Outlet />}</main>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
)
|
|
}
|
|
|
|
export { menu }
|