All checks were successful
docker-images / resolve-build-targets (push) Successful in 4s
ui-regression / playwright-regression (push) Successful in 5m55s
docker-images / build-and-push (admin) (push) Successful in 54s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m8s
docker-images / submit-indexnow (push) Successful in 15s
302 lines
9.9 KiB
TypeScript
302 lines
9.9 KiB
TypeScript
import {
|
||
BarChart3,
|
||
BellRing,
|
||
BookOpenText,
|
||
Download,
|
||
ExternalLink,
|
||
Folders,
|
||
History,
|
||
Image as ImageIcon,
|
||
LayoutDashboard,
|
||
Link2,
|
||
LogOut,
|
||
MessageSquareText,
|
||
Orbit,
|
||
ScrollText,
|
||
Settings,
|
||
Sparkles,
|
||
Tags,
|
||
Workflow,
|
||
} from 'lucide-react'
|
||
import type { ReactNode } from 'react'
|
||
import { NavLink } from 'react-router-dom'
|
||
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Separator } from '@/components/ui/separator'
|
||
import { buildFrontendUrl } from '@/lib/frontend-url'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
const primaryNav = [
|
||
{
|
||
to: '/',
|
||
label: '概览',
|
||
description: '站点运营总览',
|
||
icon: LayoutDashboard,
|
||
},
|
||
{
|
||
to: '/analytics',
|
||
label: '数据分析',
|
||
description: '搜索词与 AI 问答洞察',
|
||
icon: BarChart3,
|
||
},
|
||
{
|
||
to: '/posts',
|
||
label: '文章',
|
||
description: 'Markdown 内容管理',
|
||
icon: ScrollText,
|
||
},
|
||
{
|
||
to: '/categories',
|
||
label: '分类',
|
||
description: '分类目录与聚合统计',
|
||
icon: Folders,
|
||
},
|
||
{
|
||
to: '/tags',
|
||
label: '标签',
|
||
description: '标签库与引用整理',
|
||
icon: Tags,
|
||
},
|
||
{
|
||
to: '/backups',
|
||
label: '备份',
|
||
description: '全站导出与恢复',
|
||
icon: Download,
|
||
},
|
||
{
|
||
to: '/revisions',
|
||
label: '版本',
|
||
description: '历史快照与一键回滚',
|
||
icon: History,
|
||
},
|
||
{
|
||
to: '/comments',
|
||
label: '评论',
|
||
description: '审核与段落回复',
|
||
icon: MessageSquareText,
|
||
},
|
||
{
|
||
to: '/friend-links',
|
||
label: '友链',
|
||
description: '友链申请与互链管理',
|
||
icon: Link2,
|
||
},
|
||
{
|
||
to: '/reviews',
|
||
label: '评测',
|
||
description: '评测内容库',
|
||
icon: BookOpenText,
|
||
},
|
||
{
|
||
to: '/media',
|
||
label: '媒体库',
|
||
description: '对象存储图片管理',
|
||
icon: ImageIcon,
|
||
},
|
||
{
|
||
to: '/subscriptions',
|
||
label: '订阅',
|
||
description: '邮件 / Webhook 推送',
|
||
icon: BellRing,
|
||
},
|
||
{
|
||
to: '/workers',
|
||
label: 'Workers',
|
||
description: '异步任务 / 队列控制台',
|
||
icon: Workflow,
|
||
},
|
||
{
|
||
to: '/audit',
|
||
label: '审计',
|
||
description: '后台操作日志与排障线索',
|
||
icon: History,
|
||
},
|
||
{
|
||
to: '/settings',
|
||
label: '设置',
|
||
description: '品牌、资料与 AI 配置',
|
||
icon: Settings,
|
||
},
|
||
]
|
||
|
||
export function AppShell({
|
||
children,
|
||
username,
|
||
email,
|
||
authSource,
|
||
authProvider,
|
||
loggingOut,
|
||
canLogout,
|
||
onLogout,
|
||
}: {
|
||
children: ReactNode
|
||
username: string | null
|
||
email: string | null
|
||
authSource: string | null
|
||
authProvider: string | null
|
||
loggingOut: boolean
|
||
canLogout: boolean
|
||
onLogout: () => Promise<void>
|
||
}) {
|
||
return (
|
||
<div className="min-h-screen bg-background text-foreground">
|
||
<div className="mx-auto flex min-h-screen w-full max-w-[1600px] gap-6 px-4 py-4 lg:px-6 lg:py-6">
|
||
<aside className="hidden w-[310px] shrink-0 lg:block">
|
||
<div className="sticky top-6 overflow-hidden rounded-[2rem] border border-border/70 bg-card/90 shadow-[0_32px_90px_rgba(15,23,42,0.14)] backdrop-blur">
|
||
<div className="space-y-5 p-6">
|
||
<div className="space-y-3">
|
||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-primary">
|
||
<Orbit className="h-3.5 w-3.5" />
|
||
Termi 后台
|
||
</div>
|
||
<div className="space-y-2">
|
||
<h1 className="text-2xl font-semibold tracking-tight">博客系统控制台</h1>
|
||
<p className="text-sm leading-6 text-muted-foreground">
|
||
一个独立的 React 管理工作台,用来处理发布、审核、运营以及站内 AI 配置。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
<nav className="space-y-2">
|
||
{primaryNav.map((item) => {
|
||
const Icon = item.icon
|
||
|
||
return (
|
||
<NavLink
|
||
key={item.to}
|
||
to={item.to}
|
||
end={item.to === '/'}
|
||
className={({ isActive }) =>
|
||
cn(
|
||
'group flex items-start gap-3 rounded-2xl border px-4 py-3 transition-all',
|
||
isActive
|
||
? 'border-primary/30 bg-primary/10 shadow-[0_12px_30px_rgba(37,99,235,0.14)]'
|
||
: 'border-transparent bg-background/50 hover:border-border/80 hover:bg-accent/55',
|
||
)
|
||
}
|
||
>
|
||
{({ isActive }) => (
|
||
<>
|
||
<div
|
||
className={cn(
|
||
'mt-0.5 flex h-10 w-10 items-center justify-center rounded-xl border',
|
||
isActive
|
||
? 'border-primary/25 bg-primary/12 text-primary'
|
||
: 'border-border/80 bg-secondary text-muted-foreground',
|
||
)}
|
||
>
|
||
<Icon className="h-4 w-4" />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="font-medium">{item.label}</div>
|
||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||
{item.description}
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</NavLink>
|
||
)
|
||
})}
|
||
</nav>
|
||
|
||
<Separator />
|
||
|
||
<div className="rounded-[1.7rem] border border-border/70 bg-background/65 p-5">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div>
|
||
<p className="text-xs uppercase tracking-[0.24em] text-muted-foreground">
|
||
工作台状态
|
||
</p>
|
||
<p className="mt-1 text-sm text-muted-foreground">
|
||
核心后台流程统一运行在当前独立管理端。
|
||
</p>
|
||
</div>
|
||
<Badge variant="success">运行中</Badge>
|
||
</div>
|
||
<div className="mt-4 grid gap-2">
|
||
<div className="rounded-2xl border border-border/60 bg-card/70 px-4 py-3 text-sm text-muted-foreground">
|
||
前台站点与后台管理保持解耦。
|
||
</div>
|
||
<div className="rounded-2xl border border-border/60 bg-card/70 px-4 py-3 text-sm text-muted-foreground">
|
||
后端继续作为统一认证与数据层。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<div className="min-w-0 flex-1 space-y-6">
|
||
<header className="sticky top-4 z-20 rounded-[1.8rem] border border-border/70 bg-card/80 px-5 py-4 shadow-[0_20px_60px_rgba(15,23,42,0.1)] backdrop-blur">
|
||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||
<div className="space-y-2">
|
||
<div className="inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-secondary-foreground">
|
||
<Sparkles className="h-3.5 w-3.5" />
|
||
新版管理工作台
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">
|
||
当前登录:{username ?? 'admin'}
|
||
</p>
|
||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||
{authProvider ?? 'React + shadcn/ui 基础架构'}
|
||
</p>
|
||
{email ? (
|
||
<p className="text-xs text-muted-foreground">{email}</p>
|
||
) : authSource ? (
|
||
<p className="text-xs text-muted-foreground">认证来源:{authSource}</p>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-2 overflow-x-auto lg:hidden">
|
||
{primaryNav.map((item) => (
|
||
<NavLink
|
||
key={item.to}
|
||
to={item.to}
|
||
end={item.to === '/'}
|
||
className={({ isActive }) =>
|
||
cn(
|
||
'rounded-full border px-3 py-2 text-sm whitespace-nowrap transition-colors',
|
||
isActive
|
||
? 'border-primary/30 bg-primary/10 text-primary'
|
||
: 'border-border/70 bg-background/60 text-muted-foreground',
|
||
)
|
||
}
|
||
>
|
||
{item.label}
|
||
</NavLink>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<Button variant="outline" asChild>
|
||
<a href={buildFrontendUrl('/')} target="_blank" rel="noreferrer">
|
||
<ExternalLink className="h-4 w-4" />
|
||
打开前台
|
||
</a>
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => void onLogout()}
|
||
disabled={loggingOut || !canLogout}
|
||
title={canLogout ? undefined : '当前会话由前置 SSO / 代理控制'}
|
||
>
|
||
<LogOut className="h-4 w-4" />
|
||
{canLogout ? (loggingOut ? '退出中...' : '退出登录') : 'SSO 受代理保护'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>{children}</main>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|