diff --git a/web/src/components/PageHeader.tsx b/web/src/components/PageHeader.tsx index 845a1bc..4f7045f 100644 --- a/web/src/components/PageHeader.tsx +++ b/web/src/components/PageHeader.tsx @@ -15,34 +15,35 @@ export interface Action { interface PageHeaderProps { title: string; - description?: string; + extra?: React.ReactNode; actions?: Action[]; } /** * 统一的页面头部组件 */ -export const PageHeader: React.FC = ({title, description, actions}) => { +export const PageHeader: React.FC = ({title, extra, actions}) => { return ( -
-
-

{title}

- {description &&

{description}

} +
+
+

{title}

- {actions && actions.length > 0 && ( -
- {actions.map((action) => ( - + {(extra || (actions && actions.length > 0)) && ( +
+ {extra} + {actions?.map((action) => ( +
+ +
))}
)} diff --git a/web/src/index.css b/web/src/index.css index cc79c53..d970f15 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -2,71 +2,14 @@ @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); - -/* Tailwind v4 暗色模式配置 */ @variant dark (&:where(.dark, .dark *)); -/* 通用细滚动条样式 */ -.tag-filter-scrollbar, -.thin-scrollbar { - scrollbar-width: thin; - scrollbar-color: rgb(203, 213, 225) transparent; - scrollbar-gutter: stable; -} - -.tag-filter-scrollbar::-webkit-scrollbar, -.thin-scrollbar::-webkit-scrollbar { - height: 6px; - width: 6px; -} - -.tag-filter-scrollbar::-webkit-scrollbar-track, -.thin-scrollbar::-webkit-scrollbar-track { - background: rgb(241, 245, 249); - border-radius: 9999px; -} - -.tag-filter-scrollbar::-webkit-scrollbar-thumb, -.thin-scrollbar::-webkit-scrollbar-thumb { - background: rgb(203, 213, 225); - border-radius: 9999px; - transition: background-color 0.2s; -} - -.tag-filter-scrollbar::-webkit-scrollbar-thumb:hover, -.thin-scrollbar::-webkit-scrollbar-thumb:hover { - background: rgb(148, 163, 184); -} - -/* 暗色模式 */ -.dark .tag-filter-scrollbar, -.dark .thin-scrollbar { - scrollbar-color: rgb(71, 85, 105) transparent; -} - -.dark .tag-filter-scrollbar::-webkit-scrollbar-track, -.dark .thin-scrollbar::-webkit-scrollbar-track { - background: rgb(30, 41, 59); -} - -.dark .tag-filter-scrollbar::-webkit-scrollbar-thumb, -.dark .thin-scrollbar::-webkit-scrollbar-thumb { - background: rgb(71, 85, 105); -} - -.dark .tag-filter-scrollbar::-webkit-scrollbar-thumb:hover, -.dark .thin-scrollbar::-webkit-scrollbar-thumb:hover { - background: rgb(100, 116, 139); -} - @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -85,95 +28,89 @@ --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); + --radius: 0.75rem; + --background: oklch(0.98 0.004 238); + --foreground: oklch(0.19 0.025 248); --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); + --card-foreground: oklch(0.19 0.025 248); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --popover-foreground: oklch(0.19 0.025 248); + --primary: oklch(0.546 0.245 262.881); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.58 0.16 255); } .dark { - --background: oklch(0.145 0 0); + --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); + --card: oklch(0.21 0.006 285.885); --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); + --popover: oklch(0.21 0.006 285.885); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); + --primary: oklch(0.623 0.214 259.815); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.274 0.006 286.033); --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); } @layer base { - * { - @apply border-border outline-ring/50; + * { @apply border-border outline-ring/50; } + html { min-width: 320px; } + body { + @apply bg-background text-foreground; + min-width: 320px; + min-height: 100vh; + margin: 0; + font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file +} + +.tag-filter-scrollbar, +.thin-scrollbar { + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; +} + +.tag-filter-scrollbar::-webkit-scrollbar, +.thin-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.tag-filter-scrollbar::-webkit-scrollbar-thumb, +.thin-scrollbar::-webkit-scrollbar-thumb { + border-radius: 999px; + background: #cbd5e1; +} + +.dark .tag-filter-scrollbar, +.dark .thin-scrollbar { + scrollbar-color: #475569 transparent; +} + +.dark .tag-filter-scrollbar::-webkit-scrollbar-thumb, +.dark .thin-scrollbar::-webkit-scrollbar-thumb { + background: #475569; +} diff --git a/web/src/layouts/AdminLayout/Header.tsx b/web/src/layouts/AdminLayout/Header.tsx index 4abbceb..25b4fe2 100644 --- a/web/src/layouts/AdminLayout/Header.tsx +++ b/web/src/layouts/AdminLayout/Header.tsx @@ -1,11 +1,12 @@ import {type RefObject} from 'react'; -import {useNavigate} from 'react-router-dom'; +import {useLocation, useNavigate} from 'react-router-dom'; import type {MenuProps} from 'antd'; -import {App as AntApp, Avatar, Button, Dropdown, Space} from 'antd'; -import {BookOpen, Eye, LogOut, Moon, Sun, User as UserIcon} from 'lucide-react'; +import {App as AntApp, Avatar, Button, Dropdown, Tooltip} from 'antd'; +import {BookOpen, ChevronRight, Eye, LogOut, Moon, Sun, User as UserIcon} from 'lucide-react'; import {logout} from '@/api/auth'; import {useRuntimeConfig} from '@/api/runtime'; import type {User} from '@/types'; +import {menuItems} from './menu'; interface HeaderProps { userInfo: User | null; @@ -16,8 +17,13 @@ interface HeaderProps { export const AdminHeader = ({userInfo, appliedTheme, themeButtonRef, onToggleTheme}: HeaderProps) => { const navigate = useNavigate(); + const location = useLocation(); const {message: messageApi, modal} = AntApp.useApp(); const {data: runtime} = useRuntimeConfig(); + const activeItem = menuItems.find((item) => location.pathname === item.path || location.pathname.startsWith(`${item.path}/`)); + const currentPageLabel = location.pathname.startsWith('/admin/agents-install/') + ? '部署指南' + : activeItem?.label || '管理后台'; const handleLogout = () => { modal.confirm({ @@ -41,61 +47,71 @@ export const AdminHeader = ({userInfo, appliedTheme, themeButtonRef, onToggleThe ]; return ( -
-
-
-
+
+
+
+
Logo { e.currentTarget.src = '/logo.png'; }} />
-
-

{runtime?.system.nameZh}

-

控制台

+
+

{runtime?.system.nameZh || 'Pika'}

+

管理控制台

- +
+ 控制中心 + + {currentPageLabel} +
+ +
+
+ +
- - + + + - +
); diff --git a/web/src/layouts/AdminLayout/MobileNav.tsx b/web/src/layouts/AdminLayout/MobileNav.tsx index 6831495..93c33b7 100644 --- a/web/src/layouts/AdminLayout/MobileNav.tsx +++ b/web/src/layouts/AdminLayout/MobileNav.tsx @@ -1,38 +1,75 @@ +import {useEffect, useState} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; +import {ChevronRight, Menu, X} from 'lucide-react'; import {menuItems} from './menu'; import {cn} from '@/lib/utils'; +import {useRuntimeConfig} from '@/api/runtime'; -/** 移动端底部导航。菜单可横向滚动,避免固定列数截断。 */ +/** 小屏抽屉导航,与桌面侧栏保持一致的信息层级。 */ export const AdminMobileNav = () => { + const [open, setOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); + const {data: runtime} = useRuntimeConfig(); + + useEffect(() => { + document.body.style.overflow = open ? 'hidden' : ''; + return () => { document.body.style.overflow = ''; }; + }, [open]); + + const goTo = (path: string) => { + navigate(path); + setOpen(false); + }; return ( - + <> + + {open && ( +
+ +
+
管理
+ + +
+ )} + ); }; diff --git a/web/src/layouts/AdminLayout/Sider.tsx b/web/src/layouts/AdminLayout/Sider.tsx index 5aa4db1..dc71079 100644 --- a/web/src/layouts/AdminLayout/Sider.tsx +++ b/web/src/layouts/AdminLayout/Sider.tsx @@ -1,64 +1,59 @@ import {useLocation, useNavigate} from 'react-router-dom'; -import {menuItems, SIDEBAR_WIDTH, HEADER_HEIGHT} from './menu'; +import {menuItems} from './menu'; import {cn} from '@/lib/utils'; import {useRuntimeConfig} from '@/api/runtime'; -import type {VersionInfo} from '@/api/version'; -export const AdminSider = ({version}: {version?: VersionInfo}) => { +export const AdminSider = () => { const location = useLocation(); const navigate = useNavigate(); const {data: runtime} = useRuntimeConfig(); return ( -