style: elevate enterprise admin visual foundation
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
|
||||
type AppFilterBarProps = {
|
||||
children: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
@@ -7,9 +9,11 @@ type AppFilterBarProps = {
|
||||
|
||||
export function AppFilterBar({ children, actions }: AppFilterBarProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border bg-surface p-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="grid flex-1 gap-3 md:grid-cols-2 xl:grid-cols-4">{children}</div>
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
<Card className="py-0">
|
||||
<CardContent className="flex flex-col gap-3 p-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="grid flex-1 gap-3 md:grid-cols-2 xl:grid-cols-4">{children}</div>
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ const maxWidthClass: Record<NonNullable<AppPageProps['maxWidth']>, string> = {
|
||||
|
||||
export function AppPage({ children, className, contentClassName, maxWidth = '7xl' }: AppPageProps) {
|
||||
return (
|
||||
<main className={cn('min-h-screen bg-background px-4 py-4 text-foreground sm:px-6 lg:py-6', className)}>
|
||||
<div className={cn('mx-auto flex flex-col gap-4', maxWidthClass[maxWidth], contentClassName)}>
|
||||
<main className={cn('min-h-screen bg-background px-4 py-5 text-foreground sm:px-6 lg:px-8 lg:py-7', className)}>
|
||||
<div className={cn('mx-auto flex flex-col gap-5', maxWidthClass[maxWidth], contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -9,15 +9,18 @@ type AppPageHeaderProps = {
|
||||
|
||||
export function AppPageHeader({ title, description, meta, actions }: AppPageHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-xl font-semibold tracking-normal text-foreground">{title}</h1>
|
||||
{meta}
|
||||
<div className="rounded-lg border border-border/80 bg-surface px-4 py-4 shadow-sm ring-1 ring-border/40">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 h-1 w-10 rounded-full bg-primary" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-xl font-semibold tracking-normal text-foreground">{title}</h1>
|
||||
{meta}
|
||||
</div>
|
||||
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
||||
</div>
|
||||
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ type AppSectionProps = {
|
||||
export function AppSection({ title, description, action, children }: AppSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3">
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 border-b border-border/70 pb-3">
|
||||
<div>
|
||||
<CardTitle className="text-sm">{title}</CardTitle>
|
||||
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||
</div>
|
||||
{action}
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
<CardContent className="pt-0">{children}</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,9 +65,16 @@ type SidebarNavContentProps = {
|
||||
|
||||
function SidebarBrand() {
|
||||
return (
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<p className="text-sm font-semibold text-foreground">AI 工单平台</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">企业技术支持工作台</p>
|
||||
<div className="border-b border-sidebar-border px-5 py-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground shadow-sm">
|
||||
<Bot className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-sidebar-foreground">AI 工单平台</p>
|
||||
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/60">企业服务治理中台</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -77,7 +84,7 @@ function SidebarNavContent({ groups, currentHash, onNavigate }: SidebarNavConten
|
||||
<nav className="flex flex-col gap-5 p-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.code} className="grid gap-1.5">
|
||||
<p className="px-2 text-xs font-medium text-muted-foreground">{group.title}</p>
|
||||
<p className="px-2 text-xs font-medium text-sidebar-foreground/50">{group.title}</p>
|
||||
<div className="grid gap-1">
|
||||
{visibleChildren(group).map((item) => {
|
||||
const Icon = getIcon(item.icon)
|
||||
@@ -85,16 +92,16 @@ function SidebarNavContent({ groups, currentHash, onNavigate }: SidebarNavConten
|
||||
return (
|
||||
<a
|
||||
className={cn(
|
||||
'flex h-9 items-center gap-2 rounded-md px-2.5 text-sm text-muted-foreground transition-colors',
|
||||
'hover:bg-muted hover:text-foreground',
|
||||
active && 'bg-muted text-foreground'
|
||||
'flex h-9 items-center gap-2 rounded-md px-2.5 text-sm text-sidebar-foreground/70 transition-colors',
|
||||
'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
active && 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
|
||||
)}
|
||||
href={item.path}
|
||||
key={item.code}
|
||||
onClick={onNavigate}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{item.title}</span>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
@@ -117,12 +124,12 @@ export function AppSidebar({ mobileOpen = false, onMobileOpenChange }: AppSideba
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="fixed inset-y-0 left-0 z-30 hidden w-72 border-r border-border bg-surface lg:block">
|
||||
<aside className="fixed inset-y-0 left-0 z-30 hidden w-72 border-r border-sidebar-border bg-sidebar text-sidebar-foreground shadow-xl lg:block">
|
||||
<SidebarBrand />
|
||||
<SidebarNavContent groups={groups} currentHash={currentHash} />
|
||||
</aside>
|
||||
<Sheet open={mobileOpen} onOpenChange={onMobileOpenChange}>
|
||||
<SheetContent side="left" className="w-80 gap-0 p-0 lg:hidden">
|
||||
<SheetContent side="left" className="w-80 gap-0 border-sidebar-border bg-sidebar p-0 text-sidebar-foreground lg:hidden">
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>移动端导航</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) {
|
||||
const { logout, user } = useAuth()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-6 backdrop-blur">
|
||||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b border-border/80 bg-surface/95 px-4 shadow-sm backdrop-blur sm:px-6 lg:px-8">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -24,23 +24,23 @@ export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) {
|
||||
>
|
||||
<Menu data-icon="inline-start" />
|
||||
</Button>
|
||||
<span className="font-medium text-foreground">AI 工单平台</span>
|
||||
<ChevronRight className="size-4 text-muted-foreground" />
|
||||
<span className="hidden font-medium text-foreground sm:inline">企业服务治理中台</span>
|
||||
<ChevronRight className="hidden size-4 text-muted-foreground sm:block" />
|
||||
<span className="truncate text-muted-foreground">当前工作区</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="info">本地环境</Badge>
|
||||
<Badge variant="outline" className="hidden sm:inline-flex">本地环境</Badge>
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<Button variant="ghost" size="icon" aria-label="通知">
|
||||
<Bell data-icon="inline-start" />
|
||||
</Button>
|
||||
<Button variant="ghost" className="gap-2">
|
||||
<Button variant="ghost" className="hidden gap-2 sm:inline-flex">
|
||||
<UserRound data-icon="inline-start" />
|
||||
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
||||
</Button>
|
||||
<Button variant="outline" className="gap-2" onClick={() => void logout()}>
|
||||
<LogOut data-icon="inline-start" />
|
||||
<span>退出</span>
|
||||
<span className="hidden sm:inline">退出</span>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user