refactor: standardize admin page shell

This commit is contained in:
hailin
2026-07-02 15:39:03 +08:00
parent 09df56c21b
commit 2a63f97f35
13 changed files with 65 additions and 52 deletions

View File

@@ -0,0 +1,25 @@
import type * as React from 'react'
import { cn } from '@/lib/utils'
type AppPageProps = {
children: React.ReactNode
className?: string
contentClassName?: string
maxWidth?: '6xl' | '7xl'
}
const maxWidthClass: Record<NonNullable<AppPageProps['maxWidth']>, string> = {
'6xl': 'max-w-6xl',
'7xl': 'max-w-7xl'
}
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)}>
{children}
</div>
</main>
)
}

View File

@@ -2,6 +2,7 @@ import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataT
import { AppDetailLayout } from '@/components/app/AppDetailLayout' import { AppDetailLayout } from '@/components/app/AppDetailLayout'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
@@ -33,8 +34,7 @@ const previewColumns: AppDataTableColumn<PreviewRow>[] = [
export function DesignSystemPreview() { export function DesignSystemPreview() {
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl" contentClassName="gap-6">
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<AppPageHeader <AppPageHeader
title="设计系统预览" title="设计系统预览"
actions={<Button>创建工单</Button>} actions={<Button>创建工单</Button>}
@@ -102,7 +102,6 @@ export function DesignSystemPreview() {
<p className="text-sm text-muted-foreground">页面级布局只复用 App 组件,业务字段由页面自行传入。</p> <p className="text-sm text-muted-foreground">页面级布局只复用 App 组件,业务字段由页面自行传入。</p>
</AppSection> </AppSection>
</AppDetailLayout> </AppDetailLayout>
</div> </AppPage>
</main>
) )
} }

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import { ArrowLeft, Save } from 'lucide-react' import { ArrowLeft, Save } from 'lucide-react'
import { AppField } from '@/components/app/AppField' import { AppField } from '@/components/app/AppField'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
@@ -77,8 +78,7 @@ export function SolutionCreatePage() {
} }
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl" contentClassName="grid gap-5">
<div className="mx-auto grid max-w-6xl gap-5">
<AppPageHeader <AppPageHeader
title="新建解决方案" title="新建解决方案"
actions={ actions={
@@ -202,7 +202,6 @@ export function SolutionCreatePage() {
</AppSection> </AppSection>
</div> </div>
</form> </form>
</div> </AppPage>
</main>
) )
} }

View File

@@ -4,6 +4,7 @@ import { Plus } from 'lucide-react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
@@ -73,8 +74,7 @@ export function SolutionListPage() {
: solutions : solutions
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader <AppPageHeader
title="解决方案库" title="解决方案库"
actions={ actions={
@@ -137,7 +137,6 @@ export function SolutionListPage() {
footer={<p className="text-sm text-muted-foreground">当前显示 {visibleSolutions.length} 条方案</p>} footer={<p className="text-sm text-muted-foreground">当前显示 {visibleSolutions.length} 条方案</p>}
/> />
) : null} ) : null}
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import * as React from 'react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -65,8 +66,7 @@ export function AuditLogPage() {
] ]
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="操作审计" /> <AppPageHeader title="操作审计" />
<AppFilterBar> <AppFilterBar>
@@ -83,7 +83,6 @@ export function AuditLogPage() {
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredLogs.length} 条记录</p>} footer={<p className="text-sm text-muted-foreground">当前显示 {filteredLogs.length} 条记录</p>}
/> />
) : null} ) : null}
</div> </AppPage>
</main>
) )
} }

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -78,8 +79,7 @@ export function SystemMenusPage() {
] ]
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="菜单权限" /> <AppPageHeader title="菜单权限" />
<div className="rounded-lg border border-border bg-surface p-3"> <div className="rounded-lg border border-border bg-surface p-3">
@@ -96,7 +96,6 @@ export function SystemMenusPage() {
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>} footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>}
/> />
) : null} ) : null}
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import * as React from 'react'
import { AppField } from '@/components/app/AppField' import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -71,8 +72,7 @@ export function SystemRolesPage() {
] ]
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="角色权限" /> <AppPageHeader title="角色权限" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto] md:items-end" onSubmit={handleCreate}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto] md:items-end" onSubmit={handleCreate}>
@@ -98,8 +98,7 @@ export function SystemRolesPage() {
onClose={() => setEditingRole(null)} onClose={() => setEditingRole(null)}
onSaved={load} onSaved={load}
/> />
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import * as React from 'react'
import { AppField } from '@/components/app/AppField' import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -79,8 +80,7 @@ export function SystemUserGroupsPage() {
] ]
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="用户组管理" /> <AppPageHeader title="用户组管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto] md:items-end" onSubmit={handleCreate}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto] md:items-end" onSubmit={handleCreate}>
@@ -106,7 +106,6 @@ export function SystemUserGroupsPage() {
footer={<p className="text-sm text-muted-foreground">共 {groups.length} 个用户组</p>} footer={<p className="text-sm text-muted-foreground">共 {groups.length} 个用户组</p>}
/> />
) : null} ) : null}
</div> </AppPage>
</main>
) )
} }

View File

@@ -4,6 +4,7 @@ import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -138,8 +139,7 @@ export function SystemUsersPage() {
] ]
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage>
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="用户管理" /> <AppPageHeader title="用户管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto] md:items-end" onSubmit={handleCreateUser}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto] md:items-end" onSubmit={handleCreateUser}>
@@ -180,8 +180,7 @@ export function SystemUsersPage() {
onClose={() => setEditingUser(null)} onClose={() => setEditingUser(null)}
onSaved={load} onSaved={load}
/> />
</div> </AppPage>
</main>
) )
} }

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react' import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react'
import { AppField } from '@/components/app/AppField' import { AppField } from '@/components/app/AppField'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
@@ -47,8 +48,7 @@ export function TicketCreatePage() {
} }
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl" contentClassName="grid gap-5">
<div className="mx-auto grid max-w-6xl gap-5">
<AppPageHeader title="创建工单" /> <AppPageHeader title="创建工单" />
<form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" onSubmit={handleSubmit}> <form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" onSubmit={handleSubmit}>
<div className="grid gap-4"> <div className="grid gap-4">
@@ -175,7 +175,6 @@ export function TicketCreatePage() {
</AppSection> </AppSection>
</div> </div>
</form> </form>
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import { Bot, CheckCircle2, CircleDot, Send, XCircle } from 'lucide-react'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppDetailLayout } from '@/components/app/AppDetailLayout' import { AppDetailLayout } from '@/components/app/AppDetailLayout'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
@@ -87,23 +88,22 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
if (error) { if (error) {
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl">
<AppEmptyState title="无法加载工单" description={error} /> <AppEmptyState title="无法加载工单" description={error} />
</main> </AppPage>
) )
} }
if (!ticket) { if (!ticket) {
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl">
<AppEmptyState title="正在加载工单" /> <AppEmptyState title="正在加载工单" />
</main> </AppPage>
) )
} }
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage maxWidth="6xl">
<div className="mx-auto flex max-w-6xl flex-col gap-4">
<AppPageHeader <AppPageHeader
title={ticket.title} title={ticket.title}
description={ticket.ticket_no} description={ticket.ticket_no}
@@ -164,7 +164,6 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
onOpenChange={setTransitionDialogOpen} onOpenChange={setTransitionDialogOpen}
onSubmit={handleTransition} onSubmit={handleTransition}
/> />
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import { ArrowRight, Plus } from 'lucide-react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppStatusBadge } from '@/components/app/AppStatusBadge' import { AppStatusBadge } from '@/components/app/AppStatusBadge'
@@ -194,8 +195,7 @@ export function TicketListPage() {
} }
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage contentClassName="gap-5">
<div className="mx-auto flex max-w-7xl flex-col gap-5">
<AppPageHeader <AppPageHeader
title="工单中心" title="工单中心"
actions={ actions={
@@ -285,7 +285,6 @@ export function TicketListPage() {
onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)} onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)}
/> />
) : null} ) : null}
</div> </AppPage>
</main>
) )
} }

View File

@@ -3,6 +3,7 @@ import { ArrowRight, Plus } from 'lucide-react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
@@ -61,8 +62,7 @@ export function WorkbenchPage() {
const recentTickets = tickets.slice(0, 6) const recentTickets = tickets.slice(0, 6)
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <AppPage contentClassName="grid gap-5">
<div className="mx-auto grid max-w-7xl gap-5">
<AppPageHeader <AppPageHeader
title="今日工作台" title="今日工作台"
actions={ actions={
@@ -123,7 +123,6 @@ export function WorkbenchPage() {
</div> </div>
</AppSection> </AppSection>
</div> </div>
</div> </AppPage>
</main>
) )
} }