refactor: tighten shadcn admin ui polish
This commit is contained in:
@@ -35,43 +35,41 @@ export function AppDataTable<Row>({
|
||||
onRowClick
|
||||
}: AppDataTableProps<Row>) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-surface">
|
||||
{toolbar ? <div className="border-b border-border p-3">{toolbar}</div> : null}
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-card shadow-sm">
|
||||
{toolbar ? <div className="border-b border-border bg-muted/20 p-3">{toolbar}</div> : null}
|
||||
{rows.length === 0 ? (
|
||||
<div className="p-4">
|
||||
<AppEmptyState title={emptyTitle} description={emptyDescription} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table className={tableClassName}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<Table className={tableClassName}>
|
||||
<TableHeader className="bg-muted/30">
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.key} className={column.className}>
|
||||
{column.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow
|
||||
key={getRowKey(row)}
|
||||
className={cn(onRowClick ? 'cursor-pointer' : undefined)}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.key} className={column.className}>
|
||||
{column.header}
|
||||
</TableHead>
|
||||
<TableCell key={column.key} className={column.className}>
|
||||
{column.cell(row)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow
|
||||
key={getRowKey(row)}
|
||||
className={cn(onRowClick ? 'cursor-pointer' : undefined)}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.key} className={column.className}>
|
||||
{column.cell(row)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{footer ? <div className="border-t border-border px-3 py-2">{footer}</div> : null}
|
||||
{footer ? <div className="border-t border-border bg-muted/20 px-3 py-2">{footer}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ type AppEmptyStateProps = {
|
||||
|
||||
export function AppEmptyState({ title, description, action }: AppEmptyStateProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-border bg-surface px-4 py-8 text-center">
|
||||
<div className="rounded-lg border border-dashed border-border bg-background px-4 py-8 text-center">
|
||||
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
||||
{action ? <div className="mt-4 flex justify-center">{action}</div> : null}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
type AppSectionProps = {
|
||||
title: string
|
||||
@@ -14,8 +14,8 @@ export function AppSection({ title, description, action, children }: AppSectionP
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
|
||||
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
||||
<CardTitle className="text-sm">{title}</CardTitle>
|
||||
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||
</div>
|
||||
{action}
|
||||
</CardHeader>
|
||||
|
||||
@@ -19,14 +19,14 @@ export function AppTopbar() {
|
||||
<Badge variant="info">本地环境</Badge>
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<Button variant="ghost" size="icon" aria-label="通知">
|
||||
<Bell className="size-4" />
|
||||
<Bell data-icon="inline-start" />
|
||||
</Button>
|
||||
<Button variant="ghost" className="gap-2">
|
||||
<UserRound className="size-4" />
|
||||
<UserRound data-icon="inline-start" />
|
||||
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
||||
</Button>
|
||||
<Button variant="outline" className="gap-2" onClick={() => void logout()}>
|
||||
<LogOut className="size-4" />
|
||||
<LogOut data-icon="inline-start" />
|
||||
<span>退出</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AlertCircle, LockKeyhole, UserRound } from 'lucide-react'
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -60,7 +60,6 @@ export function LoginPage() {
|
||||
<Card className="w-full max-w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>登录平台</CardTitle>
|
||||
<CardDescription>使用管理员账号进入企业后台</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-4" onSubmit={handleSubmit}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AppSection } from '@/components/app/AppSection'
|
||||
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
@@ -37,13 +37,12 @@ export function DesignSystemPreview() {
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
||||
<AppPageHeader
|
||||
title="设计系统预览"
|
||||
description="业务页面必须基于 shadcn 原子组件和 App 组件层开发。"
|
||||
actions={<Button>创建工单</Button>}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-sm font-medium">按钮</h2>
|
||||
<CardTitle className="text-sm">按钮</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
<Button>主要操作</Button>
|
||||
@@ -55,7 +54,7 @@ export function DesignSystemPreview() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-sm font-medium">状态与优先级</h2>
|
||||
<CardTitle className="text-sm">状态与优先级</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
<Badge>默认</Badge>
|
||||
@@ -88,17 +87,16 @@ export function DesignSystemPreview() {
|
||||
rows={previewRows}
|
||||
getRowKey={(row) => row.id}
|
||||
emptyTitle="暂无数据"
|
||||
emptyDescription="符合条件的数据会显示在这里。"
|
||||
/>
|
||||
|
||||
<AppDetailLayout
|
||||
side={
|
||||
<AppSection title="右侧面板" description="用于关键字段、关联方案和 AI 分析。">
|
||||
<AppEmptyState title="等待接入" description="后续阶段会接入 Agent 执行结果。" />
|
||||
<AppSection title="右侧面板">
|
||||
<AppEmptyState title="等待接入" />
|
||||
</AppSection>
|
||||
}
|
||||
>
|
||||
<AppSection title="详情区块" description="用于承载工单概览、问题描述和处理动态。">
|
||||
<AppSection title="详情区块">
|
||||
<p className="text-sm text-muted-foreground">页面级布局只复用 App 组件,业务字段由页面自行传入。</p>
|
||||
</AppSection>
|
||||
</AppDetailLayout>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function SolutionListPage() {
|
||||
title="解决方案库"
|
||||
actions={
|
||||
<Button onClick={() => (window.location.hash = '/solutions/new')}>
|
||||
<Plus className="size-4" />
|
||||
<Plus data-icon="inline-start" />
|
||||
新建方案
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function AuditLogPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="操作审计" description="查看平台关键操作记录,辅助权限治理和问题追踪。" />
|
||||
<AppPageHeader title="操作审计" />
|
||||
|
||||
<AppFilterBar>
|
||||
<Input placeholder="搜索模块、动作、对象或结果" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
@@ -80,7 +80,6 @@ export function AuditLogPage() {
|
||||
rows={filteredLogs}
|
||||
getRowKey={(log) => log.id}
|
||||
emptyTitle="暂无审计日志"
|
||||
emptyDescription="产生关键操作后,审计记录会显示在这里。"
|
||||
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredLogs.length} 条记录</p>}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function SystemMenusPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="菜单权限" description="按后端菜单配置控制前端导航展示和可用状态。" />
|
||||
<AppPageHeader title="菜单权限" />
|
||||
|
||||
<div className="rounded-lg border border-border bg-surface p-3">
|
||||
<Input placeholder="搜索菜单、路径或权限码" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
@@ -93,7 +93,6 @@ export function SystemMenusPage() {
|
||||
rows={filteredMenus}
|
||||
getRowKey={(menu) => menu.id}
|
||||
emptyTitle="暂无菜单"
|
||||
emptyDescription="执行治理初始化后,系统菜单会显示在这里。"
|
||||
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { createRole, listPermissions, listRoles, setRolePermissions } from './api'
|
||||
@@ -72,7 +72,7 @@ export function SystemRolesPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="角色权限" description="定义后台角色,并将系统权限授予对应角色。" />
|
||||
<AppPageHeader title="角色权限" />
|
||||
|
||||
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto]" onSubmit={handleCreate}>
|
||||
<Input placeholder="角色名称" value={roleName} onChange={(event) => setRoleName(event.target.value)} required />
|
||||
@@ -86,7 +86,6 @@ export function SystemRolesPage() {
|
||||
rows={roles}
|
||||
getRowKey={(role) => role.id}
|
||||
emptyTitle="暂无角色"
|
||||
emptyDescription="创建角色后可继续配置权限。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -137,8 +136,7 @@ function RolePermissionDialog({
|
||||
<Dialog open={Boolean(role)} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>配置角色权限</DialogTitle>
|
||||
<DialogDescription>{role?.name} 可访问的菜单和接口能力。</DialogDescription>
|
||||
<DialogTitle>配置角色权限{role ? `:${role.name}` : ''}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid max-h-[56vh] gap-4 overflow-auto pr-1">
|
||||
{Object.entries(groupedPermissions).map(([module, modulePermissions]) => (
|
||||
|
||||
@@ -80,7 +80,7 @@ export function SystemUserGroupsPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="用户组管理" description="按业务团队、系统域或支持队列组织用户。" />
|
||||
<AppPageHeader title="用户组管理" />
|
||||
|
||||
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto]" onSubmit={handleCreate}>
|
||||
<Input placeholder="用户组名称" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required />
|
||||
@@ -96,7 +96,6 @@ export function SystemUserGroupsPage() {
|
||||
rows={groups}
|
||||
getRowKey={(group) => group.id}
|
||||
emptyTitle="暂无用户组"
|
||||
emptyDescription="创建用户组后会显示在这里。"
|
||||
footer={<p className="text-sm text-muted-foreground">共 {groups.length} 个用户组</p>}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
@@ -139,7 +139,7 @@ export function SystemUsersPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
||||
<AppPageHeader title="用户管理" description="维护平台账号、启用状态、角色和业务用户组。" />
|
||||
<AppPageHeader title="用户管理" />
|
||||
|
||||
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto]" onSubmit={handleCreateUser}>
|
||||
<Input placeholder="账号" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required />
|
||||
@@ -160,7 +160,6 @@ export function SystemUsersPage() {
|
||||
rows={filteredUsers}
|
||||
getRowKey={(user) => user.id}
|
||||
emptyTitle="暂无用户"
|
||||
emptyDescription="创建账号后会显示在这里。"
|
||||
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredUsers.length} 个用户</p>}
|
||||
/>
|
||||
) : null}
|
||||
@@ -209,8 +208,7 @@ function UserAccessDialog({
|
||||
<Dialog open={Boolean(user)} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>用户授权</DialogTitle>
|
||||
<DialogDescription>为 {user?.display_name || user?.username} 分配角色和用户组。</DialogDescription>
|
||||
<DialogTitle>用户授权{user ? `:${user.display_name || user.username}` : ''}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
|
||||
@@ -133,7 +133,7 @@ export function TicketCreatePage() {
|
||||
<span>请确认标题和问题描述已足够支持技术人员开始排查。</span>
|
||||
</div>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Send className="size-4" />
|
||||
<Send data-icon="inline-start" />
|
||||
{submitting ? '提交中...' : '提交工单'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ export function TicketCreatePage() {
|
||||
|
||||
<AppSection
|
||||
title="AI 建单辅助"
|
||||
action={<Badge variant="ai"><Bot className="size-3" /> 即将接入</Badge>}
|
||||
action={<Badge variant="ai">即将接入</Badge>}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。
|
||||
|
||||
@@ -124,7 +124,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
|
||||
variant={status === 'closed' ? 'outline' : 'secondary'}
|
||||
onClick={() => openTransition(status)}
|
||||
>
|
||||
{Icon ? <Icon className="size-4" /> : null}
|
||||
{Icon ? <Icon data-icon="inline-start" /> : null}
|
||||
{getStatusActionLabel(ticket.status, status)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ const columns: AppDataTableColumn<Ticket>[] = [
|
||||
}}
|
||||
>
|
||||
详情
|
||||
<ArrowRight className="size-4" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -200,7 +200,7 @@ export function TicketListPage() {
|
||||
title="工单中心"
|
||||
actions={
|
||||
<Button onClick={() => (window.location.hash = '/tickets/new')}>
|
||||
<Plus className="size-4" />
|
||||
<Plus data-icon="inline-start" />
|
||||
新建工单
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -65,10 +65,9 @@ export function WorkbenchPage() {
|
||||
<div className="mx-auto grid max-w-7xl gap-5">
|
||||
<AppPageHeader
|
||||
title="今日工作台"
|
||||
description="集中查看待处理事项、近期工单和常用入口。"
|
||||
actions={
|
||||
<Button onClick={() => (window.location.hash = '/tickets/new')}>
|
||||
<Plus className="size-4" />
|
||||
<Plus data-icon="inline-start" />
|
||||
新建工单
|
||||
</Button>
|
||||
}
|
||||
@@ -90,11 +89,10 @@ export function WorkbenchPage() {
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<AppSection
|
||||
title="最近工单"
|
||||
description="按当前接口返回顺序展示近期工单,后续可接入专门的工作台聚合接口。"
|
||||
action={
|
||||
<Button variant="ghost" onClick={() => (window.location.hash = '/tickets')}>
|
||||
查看全部
|
||||
<ArrowRight className="size-4" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -108,19 +106,19 @@ export function WorkbenchPage() {
|
||||
/>
|
||||
</AppSection>
|
||||
|
||||
<AppSection title="快捷入口" description="高频操作集中放置,减少跨页面跳转。">
|
||||
<AppSection title="快捷入口">
|
||||
<div className="grid gap-2">
|
||||
<Button className="justify-between" variant="secondary" onClick={() => (window.location.hash = '/tickets/new')}>
|
||||
创建工单
|
||||
<ArrowRight className="size-4" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
<Button className="justify-between" variant="outline" onClick={() => (window.location.hash = '/tickets')}>
|
||||
工单中心
|
||||
<ArrowRight className="size-4" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
<Button className="justify-between" variant="outline" onClick={() => (window.location.hash = '/solutions')}>
|
||||
解决方案库
|
||||
<ArrowRight className="size-4" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
</div>
|
||||
</AppSection>
|
||||
|
||||
Reference in New Issue
Block a user