Pārlūkot izejas kodu

✨ feat(admin): 将管理后台从Ant Design迁移至Shadcn UI

- 使用shadcn/ui组件库替换Ant Design组件
- 优化主布局结构,添加移动端响应式侧边栏
- 重构用户管理页面,使用React Hook Form和Zod验证
- 改进仪表盘UI,添加系统性能监控卡片
- 更新登录页面样式,集成sonner提示系统
- 替换antd图标为lucide-react图标库
- 重构路由配置,使用/admin-shadcn路径
yourname 4 mēneši atpakaļ
vecāks
revīzija
e2384129fe

+ 187 - 155
src/client/admin-shadcn/layouts/MainLayout.tsx

@@ -1,23 +1,26 @@
-import React, { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 import {
   Outlet,
   useLocation,
 } from 'react-router';
 import {
-  Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
-} from 'antd';
-import {
-  MenuFoldOutlined,
-  MenuUnfoldOutlined,
-  BellOutlined,
-  VerticalAlignTopOutlined,
-  UserOutlined
-} from '@ant-design/icons';
+  Bell,
+  Menu,
+  User,
+  ChevronDown
+} from 'lucide-react';
 import { useAuth } from '../hooks/AuthProvider';
-import { useMenu, useMenuSearch, type MenuItem } from '../menu';
+import { useMenu, type MenuItem } from '../menu';
 import { getGlobalConfig } from '@/client/utils/utils';
-
-const { Header, Sider, Content } = Layout;
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/client/components/ui/dropdown-menu';
+import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/client/components/ui/sheet';
+import { ScrollArea } from '@/client/components/ui/scroll-area';
+import { cn } from '@/client/lib/utils';
+import { Badge } from '@/client/components/ui/badge';
+import { Toaster } from '@/client/components/ui/sonner';
 
 /**
  * 主布局组件
@@ -27,46 +30,17 @@ export const MainLayout = () => {
   const { user } = useAuth();
   const [showBackTop, setShowBackTop] = useState(false);
   const location = useLocation();
+  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
   
   // 使用菜单hook
   const {
     menuItems,
     userMenuItems,
-    openKeys,
     collapsed,
     setCollapsed,
-    handleMenuClick: handleRawMenuClick,
-    onOpenChange
+    handleMenuClick
   } = useMenu();
   
-  // 处理菜单点击
-  const handleMenuClick = (key: string) => {
-    const item = findMenuItem(menuItems, key);
-    if (item && 'label' in item) {
-      handleRawMenuClick(item);
-    }
-  };
-  
-  // 查找菜单项
-  const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
-    for (const item of items) {
-      if (!item) continue;
-      if (item.key === key) return item;
-      if (item.children) {
-        const found = findMenuItem(item.children, key);
-        if (found) return found;
-      }
-    }
-    return null;
-  };
-  
-  // 使用菜单搜索hook
-  const {
-    searchText,
-    setSearchText,
-    filteredMenuItems
-  } = useMenuSearch(menuItems);
-  
   // 获取当前选中的菜单项
   const selectedKey = useMemo(() => {
     const findSelectedKey = (items: MenuItem[]): string | null => {
@@ -102,127 +76,185 @@ export const MainLayout = () => {
     });
   };
 
-  
   // 应用名称 - 从CONFIG中获取或使用默认值
   const appName = getGlobalConfig('APP_NAME') || '应用Starter';
   
-  return (
-    <Layout style={{ minHeight: '100vh' }}>
-      <Sider
-        trigger={null}
-        collapsible
-        collapsed={collapsed}
-        width={240}
-        className="custom-sider"
-        theme='light'
-        style={{
-          overflow: 'auto',
-          height: '100vh',
-          position: 'fixed',
-          left: 0,
-          top: 0,
-          bottom: 0,
-          zIndex: 100,
-          transition: 'all 0.2s ease',
-          boxShadow: '2px 0 8px 0 rgba(29, 35, 41, 0.05)',
-          background: 'linear-gradient(180deg, #001529 0%, #003a6c 100%)',
-        }}
-      >
-        <div className="p-4">
-          <Typography.Title level={2} className="text-xl font-bold truncate">
-            <span className="text-white">{collapsed ? '应用' : appName}</span>
-          </Typography.Title>
-          
-          {/* 菜单搜索框 */}
-          {!collapsed && (
-            <div className="mb-4">
-              <Input.Search
-                placeholder="搜索菜单..."
-                allowClear
-                value={searchText}
-                onChange={(e) => setSearchText(e.target.value)}
-              />
+
+  // 侧边栏内容
+  const SidebarContent = () => (
+    <div className="flex h-full flex-col">
+      <div className="p-4 border-b">
+        <h2 className="text-lg font-semibold truncate">
+          {collapsed ? '应用' : appName}
+        </h2>
+        {!collapsed && (
+          <div className="mt-4">
+            <Input
+              placeholder="搜索菜单..."
+              className="h-8"
+            />
+          </div>
+        )}
+      </div>
+      
+      <ScrollArea className="flex-1">
+        <nav className="p-2">
+          {menuItems.map((item) => (
+            <div key={item.key}>
+              <Button
+                variant={selectedKey === item.key ? "default" : "ghost"}
+                className={cn(
+                  "w-full justify-start mb-1",
+                  selectedKey === item.key && "bg-primary text-primary-foreground"
+                )}
+                onClick={() => {
+                  handleMenuClick(item);
+                  setIsMobileMenuOpen(false);
+                }}
+              >
+                {item.icon}
+                {!collapsed && <span className="ml-2">{item.label}</span>}
+              </Button>
+              
+              {item.children && !collapsed && (
+                <div className="ml-4">
+                  {item.children.map((child) => (
+                    <Button
+                      key={child.key}
+                      variant={selectedKey === child.key ? "default" : "ghost"}
+                      className={cn(
+                        "w-full justify-start mb-1 text-sm",
+                        selectedKey === child.key && "bg-primary text-primary-foreground"
+                      )}
+                      onClick={() => {
+                        handleMenuClick(child);
+                        setIsMobileMenuOpen(false);
+                      }}
+                    >
+                      {child.icon && <span className="ml-2">{child.icon}</span>}
+                      <span className={child.icon ? "ml-2" : "ml-6"}>{child.label}</span>
+                    </Button>
+                  ))}
+                </div>
+              )}
             </div>
-          )}
-        </div>
-        
-        {/* 菜单列表 */}
-        <Menu
-          theme='dark'
-          mode="inline"
-          items={filteredMenuItems}
-          openKeys={openKeys}
-          selectedKeys={[selectedKey]}
-          onOpenChange={onOpenChange}
-          onClick={({ key }) => handleMenuClick(key)}
-          inlineCollapsed={collapsed}
-          style={{
-            backgroundColor: 'transparent',
-            borderRight: 'none'
-          }}
-        />
-      </Sider>
+          ))}
+        </nav>
+      </ScrollArea>
+    </div>
+  );
+
+  return (
+    <div className="flex h-screen bg-background">
+      <Toaster />
       
-      <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
-        <div className="sticky top-0 z-50 bg-white shadow-sm transition-all duration-200 h-16 flex items-center justify-between pl-2"
-          style={{
-            boxShadow: '0 1px 8px rgba(0,21,41,0.12)',
-            borderBottom: '1px solid #f0f0f0'
-          }}
-        >
-          <Button
-            type="text"
-            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
-            onClick={() => setCollapsed(!collapsed)}
-            className="w-16 h-16"
-          />
-          
-          <Space size="middle" className="mr-4">
-            <Badge count={5} offset={[0, 5]}>
-              <Button 
-                type="text" 
-                icon={<BellOutlined />}
-              />
-            </Badge>
-            
-            <Dropdown menu={{ items: userMenuItems }}>
-              <Space className="cursor-pointer">
-                <Avatar 
-                  src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
-                  icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
-                />
-                <span>
-                  {user?.nickname || user?.username}
-                </span>
-              </Space>
-            </Dropdown>
-          </Space>
-        </div>
-        
-        <Content className="m-6" style={{ overflow: 'initial', transition: 'all 0.2s ease' }}>
-          <div className="site-layout-content p-6 rounded-lg bg-white shadow-sm transition-all duration-300 hover:shadow-md">
+      {/* Desktop Sidebar */}
+      <aside className={cn(
+        "hidden md:block border-r bg-background transition-all duration-200",
+        collapsed ? "w-16" : "w-64"
+      )}>
+        <SidebarContent />
+      </aside>
+
+      {/* Mobile Sidebar */}
+      <Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
+        <SheetContent side="left" className="w-64 p-0">
+          <SheetHeader className="p-4">
+            <SheetTitle>{appName}</SheetTitle>
+          </SheetHeader>
+          <SidebarContent />
+        </SheetContent>
+      </Sheet>
+
+      <div className="flex-1 flex flex-col overflow-hidden">
+        {/* Header */}
+        <header className="flex h-16 items-center justify-between border-b bg-background px-4">
+          <div className="flex items-center gap-2">
+            <Button
+              variant="ghost"
+              size="icon"
+              className="md:hidden"
+              onClick={() => setIsMobileMenuOpen(true)}
+            >
+              <Menu className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="ghost"
+              size="icon"
+              className="hidden md:block"
+              onClick={() => setCollapsed(!collapsed)}
+            >
+              <Menu className="h-4 w-4" />
+            </Button>
+          </div>
+
+          <div className="flex items-center gap-4">
+            <Button variant="ghost" size="icon" className="relative">
+              <Bell className="h-4 w-4" />
+              <Badge className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center text-xs">
+                5
+              </Badge>
+            </Button>
+
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Button variant="ghost" className="relative h-8 w-8 rounded-full">
+                  <Avatar className="h-8 w-8">
+                    <AvatarImage
+                      src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
+                      alt={user?.username || 'User'}
+                    />
+                    <AvatarFallback>
+                      <User className="h-4 w-4" />
+                    </AvatarFallback>
+                  </Avatar>
+                </Button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent className="w-56" align="end" forceMount>
+                <DropdownMenuLabel className="font-normal">
+                  <div className="flex flex-col space-y-1">
+                    <p className="text-sm font-medium leading-none">
+                      {user?.nickname || user?.username}
+                    </p>
+                    <p className="text-xs leading-none text-muted-foreground">
+                      {user?.email}
+                    </p>
+                  </div>
+                </DropdownMenuLabel>
+                <DropdownMenuSeparator />
+                {userMenuItems.map((item) => (
+                  item.type === 'separator' ? (
+                    <DropdownMenuSeparator key={item.key} />
+                  ) : (
+                    <DropdownMenuItem key={item.key} onClick={item.onClick}>
+                      {item.icon && item.icon}
+                      <span>{item.label}</span>
+                    </DropdownMenuItem>
+                  )
+                ))}
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </div>
+        </header>
+
+        {/* Main Content */}
+        <main className="flex-1 overflow-auto p-4">
+          <div className="max-w-7xl mx-auto">
             <Outlet />
           </div>
           
-          {/* 回到顶部按钮 */}
+          {/* Back to top button */}
           {showBackTop && (
             <Button
-              type="primary"
-              shape="circle"
-              icon={<VerticalAlignTopOutlined />}
-              size="large"
+              size="icon"
+              className="fixed bottom-4 right-4 rounded-full shadow-lg"
               onClick={scrollToTop}
-              style={{
-                position: 'fixed',
-                right: 30,
-                bottom: 30,
-                zIndex: 1000,
-                boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
-              }}
-            />
+            >
+              <ChevronDown className="h-4 w-4 rotate-180" />
+            </Button>
           )}
-        </Content>
-      </Layout>
-    </Layout>
+        </main>
+      </div>
+    </div>
   );
-};
+};

+ 41 - 21
src/client/admin-shadcn/menu.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
 import { useAuth } from './hooks/AuthProvider';
-import type { MenuProps } from 'antd';
 import {
-  UserOutlined,
-  DashboardOutlined,
-  TeamOutlined,
-  InfoCircleOutlined,
-} from '@ant-design/icons';
+  Users,
+  Settings,
+  User,
+  LogOut,
+  BarChart3,
+  LayoutDashboard
+} from 'lucide-react';
 
 export interface MenuItem {
   key: string;
@@ -16,6 +17,7 @@ export interface MenuItem {
   children?: MenuItem[];
   path?: string;
   permission?: string;
+  onClick?: () => void;
 }
 
 /**
@@ -68,37 +70,60 @@ export const useMenu = () => {
   const navigate = useNavigate();
   const { logout: handleLogout } = useAuth();
   const [collapsed, setCollapsed] = React.useState(false);
-  const [openKeys, setOpenKeys] = React.useState<string[]>([]);
 
   // 基础菜单项配置
   const menuItems: MenuItem[] = [
     {
       key: 'dashboard',
       label: '控制台',
-      icon: <DashboardOutlined />,
+      icon: <LayoutDashboard className="h-4 w-4" />,
       path: '/admin/dashboard'
     },
     {
       key: 'users',
       label: '用户管理',
-      icon: <TeamOutlined />,
+      icon: <Users className="h-4 w-4" />,
       path: '/admin/users',
       permission: 'user:manage'
     },
+    {
+      key: 'analytics',
+      label: '数据分析',
+      icon: <BarChart3 className="h-4 w-4" />,
+      path: '/admin/analytics',
+      permission: 'analytics:view'
+    },
+    {
+      key: 'settings',
+      label: '系统设置',
+      icon: <Settings className="h-4 w-4" />,
+      path: '/admin/settings',
+      permission: 'settings:manage'
+    },
   ];
 
   // 用户菜单项
-  const userMenuItems: MenuProps['items'] = [
+  const userMenuItems = [
     {
       key: 'profile',
       label: '个人资料',
-      icon: <UserOutlined />
+      icon: <User className="mr-2 h-4 w-4" />,
+      onClick: () => navigate('/admin/profile')
+    },
+    {
+      key: 'settings',
+      label: '账户设置',
+      icon: <Settings className="mr-2 h-4 w-4" />,
+      onClick: () => navigate('/admin/account-settings')
+    },
+    {
+      type: 'separator',
+      key: 'divider',
     },
     {
       key: 'logout',
       label: '退出登录',
-      icon: <InfoCircleOutlined />,
-      danger: true,
+      icon: <LogOut className="mr-2 h-4 w-4" />,
       onClick: () => handleLogout()
     }
   ];
@@ -108,21 +133,16 @@ export const useMenu = () => {
     if (item.path) {
       navigate(item.path);
     }
-  };
-
-  // 处理菜单展开变化
-  const onOpenChange = (keys: string[]) => {
-    const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
-    setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
+    if (item.onClick) {
+      item.onClick();
+    }
   };
 
   return {
     menuItems,
     userMenuItems,
-    openKeys,
     collapsed,
     setCollapsed,
     handleMenuClick,
-    onOpenChange
   };
 };

+ 226 - 64
src/client/admin-shadcn/pages/Dashboard.tsx

@@ -1,75 +1,237 @@
 import React from 'react';
-import {
-  Card, Row, Col, Typography, Statistic, Space
-} from 'antd';
-import {
-  UserOutlined, BellOutlined, EyeOutlined
-} from '@ant-design/icons';
-
-const { Title } = Typography;
+import { Users, Bell, Eye, TrendingUp, TrendingDown, Activity } from 'lucide-react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Badge } from '@/client/components/ui/badge';
+import { Progress } from '@/client/components/ui/progress';
 
 // 仪表盘页面
 export const DashboardPage = () => {
+  const stats = [
+    {
+      title: '活跃用户',
+      value: '112,893',
+      icon: Users,
+      color: 'text-blue-500',
+      bgColor: 'bg-blue-50',
+      trend: 12.5,
+      trendDirection: 'up',
+      description: '较昨日增长 12.5%',
+    },
+    {
+      title: '系统消息',
+      value: '93',
+      icon: Bell,
+      color: 'text-yellow-500',
+      bgColor: 'bg-yellow-50',
+      trend: 0,
+      trendDirection: 'neutral',
+      description: '其中 5 条未读',
+    },
+    {
+      title: '在线用户',
+      value: '1,128',
+      icon: Eye,
+      color: 'text-purple-500',
+      bgColor: 'bg-purple-50',
+      trend: 32.1,
+      trendDirection: 'up',
+      description: '当前在线率 32.1%',
+    },
+  ];
+
+  const recentActivities = [
+    {
+      id: 1,
+      user: '张三',
+      action: '登录系统',
+      time: '2分钟前',
+      status: 'success',
+    },
+    {
+      id: 2,
+      user: '李四',
+      action: '创建了新用户',
+      time: '5分钟前',
+      status: 'info',
+    },
+    {
+      id: 3,
+      user: '王五',
+      action: '删除了用户',
+      time: '10分钟前',
+      status: 'warning',
+    },
+    {
+      id: 4,
+      user: '赵六',
+      action: '修改了配置',
+      time: '15分钟前',
+      status: 'info',
+    },
+  ];
+
+  const systemMetrics = [
+    {
+      name: 'CPU使用率',
+      value: 65,
+      color: 'bg-green-500',
+    },
+    {
+      name: '内存使用率',
+      value: 78,
+      color: 'bg-blue-500',
+    },
+    {
+      name: '磁盘使用率',
+      value: 45,
+      color: 'bg-purple-500',
+    },
+    {
+      name: '网络使用率',
+      value: 32,
+      color: 'bg-orange-500',
+    },
+  ];
+
   return (
-    <div>
-      <div className="mb-6 flex justify-between items-center">
-        <Title level={2}>仪表盘</Title>
+    <div className="space-y-6">
+      <div>
+        <h1 className="text-3xl font-bold tracking-tight">仪表盘</h1>
+        <p className="text-muted-foreground">
+          欢迎回来!这里是系统概览和关键指标。
+        </p>
       </div>
-      <Row gutter={[16, 16]}>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>活跃用户</Typography.Title>
-              <UserOutlined style={{ fontSize: 24, color: '#1890ff' }} />
-            </div>
-            <Statistic
-              value={112893}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              prefix={<span style={{ color: '#52c41a' }}>↑</span>}
-              suffix="人"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              较昨日增长 12.5%
-            </div>
-          </Card>
-        </Col>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>系统消息</Typography.Title>
-              <BellOutlined style={{ fontSize: 24, color: '#faad14' }} />
-            </div>
-            <Statistic
-              value={93}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              prefix={<span style={{ color: '#faad14' }}>●</span>}
-              suffix="条"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              其中 5 条未读
-            </div>
-          </Card>
-        </Col>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>在线用户</Typography.Title>
-              <EyeOutlined style={{ fontSize: 24, color: '#722ed1' }} />
+
+      {/* 统计卡片 */}
+      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+        {stats.map((stat, index) => {
+          const Icon = stat.icon;
+          return (
+            <Card key={index} className="transition-all duration-300 hover:shadow-lg">
+              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                <CardTitle className="text-sm font-medium">
+                  {stat.title}
+                </CardTitle>
+                <div className={`p-2 rounded-full ${stat.bgColor}`}>
+                  <Icon className={`h-4 w-4 ${stat.color}`} />
+                </div>
+              </CardHeader>
+              <CardContent>
+                <div className="text-2xl font-bold">{stat.value}</div>
+                <div className="flex items-center space-x-2">
+                  {stat.trendDirection === 'up' && (
+                    <TrendingUp className="h-4 w-4 text-green-500" />
+                  )}
+                  {stat.trendDirection === 'down' && (
+                    <TrendingDown className="h-4 w-4 text-red-500" />
+                  )}
+                  <p className="text-xs text-muted-foreground">{stat.description}</p>
+                </div>
+              </CardContent>
+            </Card>
+          );
+        })}
+      </div>
+
+      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
+        {/* 系统性能 */}
+        <Card className="lg:col-span-4">
+          <CardHeader>
+            <CardTitle>系统性能</CardTitle>
+            <CardDescription>
+              当前系统各项资源的使用情况
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-4">
+              {systemMetrics.map((metric, index) => (
+                <div key={index} className="space-y-2">
+                  <div className="flex justify-between text-sm">
+                    <span className="font-medium">{metric.name}</span>
+                    <span className="text-muted-foreground">{metric.value}%</span>
+                  </div>
+                  <Progress value={metric.value} className="h-2" />
+                </div>
+              ))}
             </div>
-            <Statistic
-              value={1128}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              suffix="人"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              当前在线率 32.1%
+          </CardContent>
+        </Card>
+
+        {/* 最近活动 */}
+        <Card className="lg:col-span-3">
+          <CardHeader>
+            <CardTitle>最近活动</CardTitle>
+            <CardDescription>
+              系统最新操作记录
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-4">
+              {recentActivities.map((activity) => (
+                <div key={activity.id} className="flex items-center space-x-4">
+                  <div className="flex-shrink-0">
+                    <div className={`p-2 rounded-full ${
+                      activity.status === 'success' ? 'bg-green-100' :
+                      activity.status === 'warning' ? 'bg-yellow-100' : 'bg-blue-100'
+                    }`}>
+                      <Activity className={`h-4 w-4 ${
+                        activity.status === 'success' ? 'text-green-600' :
+                        activity.status === 'warning' ? 'text-yellow-600' : 'text-blue-600'
+                      }`} />
+                    </div>
+                  </div>
+                  <div className="flex-1 space-y-1">
+                    <p className="text-sm font-medium">
+                      {activity.user} {activity.action}
+                    </p>
+                    <p className="text-sm text-muted-foreground">
+                      {activity.time}
+                    </p>
+                  </div>
+                </div>
+              ))}
             </div>
-          </Card>
-        </Col>
-      </Row>
+          </CardContent>
+        </Card>
+      </div>
+
+      {/* 快捷操作 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>快捷操作</CardTitle>
+          <CardDescription>
+            常用的管理功能
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">用户管理</CardTitle>
+                <CardDescription>查看和管理所有用户</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">系统设置</CardTitle>
+                <CardDescription>配置系统参数</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">数据备份</CardTitle>
+                <CardDescription>执行数据备份操作</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">日志查看</CardTitle>
+                <CardDescription>查看系统日志</CardDescription>
+              </CardHeader>
+            </Card>
+          </div>
+        </CardContent>
+      </Card>
     </div>
   );
 };

+ 133 - 83
src/client/admin-shadcn/pages/Login.tsx

@@ -1,34 +1,43 @@
 import React, { useState } from 'react';
-import {
-  Form,
-  Input,
-  Button,
-  Card,
-  App,
-} from 'antd';
-import {
-  UserOutlined,
-  LockOutlined,
-  EyeOutlined,
-  EyeInvisibleOutlined
-} from '@ant-design/icons';
 import { useNavigate } from 'react-router';
-import {
-  useAuth,
-} from '../hooks/AuthProvider';
+import { useAuth } from '../hooks/AuthProvider';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { toast } from 'sonner';
+import { Eye, EyeOff, User, Lock } from 'lucide-react';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Input } from '@/client/components/ui/input';
+import { cn } from '@/client/lib/utils';
 
+// 表单验证Schema
+const loginSchema = z.object({
+  username: z.string().min(1, '请输入用户名'),
+  password: z.string().min(1, '请输入密码'),
+});
+
+type LoginFormData = z.infer<typeof loginSchema>;
 
 // 登录页面
 export const LoginPage = () => {
-  const { message } = App.useApp();
   const { login } = useAuth();
-  const [form] = Form.useForm();
-  const [loading, setLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [showPassword, setShowPassword] = useState(false);
   const navigate = useNavigate();
   
-  const handleSubmit = async (values: { username: string; password: string }) => {
+  const form = useForm<LoginFormData>({
+    resolver: zodResolver(loginSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+    },
+  });
+
+  const handleSubmit = async (data: LoginFormData) => {
     try {
-      setLoading(true);
+      setIsLoading(true);
       
       // 获取地理位置
       let latitude: number | undefined;
@@ -46,84 +55,125 @@ export const LoginPage = () => {
         console.warn('获取地理位置失败:', geoError);
       }
       
-      await login(values.username, values.password, latitude, longitude);
+      await login(data.username, data.password, latitude, longitude);
       // 登录成功后跳转到管理后台首页
       navigate('/admin/dashboard');
+      toast.success('登录成功!欢迎回来');
     } catch (error: any) {
-      message.error(error instanceof Error ? error.message : '登录失败');
+      toast.error(error instanceof Error ? error.message : '登录失败');
     } finally {
-      setLoading(false);
+      setIsLoading(false);
     }
   };
   
   return (
-    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
+    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
       <div className="max-w-md w-full space-y-8">
         <div className="text-center">
-          <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
-            <UserOutlined style={{ fontSize: 32, color: '#1890ff' }} />
+          <div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
+            <User className="h-8 w-8 text-white" />
           </div>
-          <h2 className="mt-2 text-center text-3xl font-extrabold text-gray-900">
+          <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
             管理后台登录
           </h2>
-          <p className="mt-2 text-gray-500">请输入您的账号和密码</p>
+          <p className="mt-2 text-center text-sm text-gray-600">
+            请输入您的账号和密码继续操作
+          </p>
         </div>
         
-        <Card className="shadow-lg border-none transition-all duration-300 hover:shadow-xl">
-          <Form
-            form={form}
-            name="login"
-            onFinish={handleSubmit}
-            autoComplete="off"
-            layout="vertical"
-          >
-            <Form.Item
-              name="username"
-              rules={[{ required: true, message: '请输入用户名' }]}
-              label="用户名"
-            >
-              <Input
-                prefix={<UserOutlined className="text-primary" />}
-                placeholder="请输入用户名"
-                size="large"
-                className="transition-all duration-200 focus:border-primary focus:ring-1 focus:ring-primary"
-              />
-            </Form.Item>
-            
-            <Form.Item
-              name="password"
-              rules={[{ required: true, message: '请输入密码' }]}
-              label="密码"
-            >
-              <Input.Password
-                prefix={<LockOutlined className="text-primary" />}
-                placeholder="请输入密码"
-                size="large"
-                iconRender={(visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />)}
-                className="transition-all duration-200 focus:border-primary focus:ring-1 focus:ring-primary"
-              />
-            </Form.Item>
-            
-            <Form.Item>
-              <Button
-                type="primary"
-                htmlType="submit"
-                size="large"
-                block
-                loading={loading}
-                className="h-12 text-lg transition-all duration-200 hover:shadow-lg"
-              >
-                登录
-              </Button>
-            </Form.Item>
-          </Form>
-          
-          <div className="mt-6 text-center text-gray-500 text-sm">
-            <p>测试账号: <span className="font-medium">admin</span> / <span className="font-medium">admin123</span></p>
-            <p className="mt-1">© {new Date().getFullYear()} 管理系统. 保留所有权利.</p>
-          </div>
+        <Card className="shadow-xl border-0">
+          <CardHeader className="text-center">
+            <CardTitle className="text-2xl">欢迎登录</CardTitle>
+            <CardDescription>
+              使用您的账户信息登录系统
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <Form {...form}>
+              <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
+                <FormField
+                  control={form.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>用户名</FormLabel>
+                      <FormControl>
+                        <div className="relative">
+                          <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
+                          <Input
+                            placeholder="请输入用户名"
+                            className="pl-10"
+                            {...field}
+                          />
+                        </div>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                
+                <FormField
+                  control={form.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>密码</FormLabel>
+                      <FormControl>
+                        <div className="relative">
+                          <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
+                          <Input
+                            type={showPassword ? 'text' : 'password'}
+                            placeholder="请输入密码"
+                            className="pl-10 pr-10"
+                            {...field}
+                          />
+                          <button
+                            type="button"
+                            className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
+                            onClick={() => setShowPassword(!showPassword)}
+                          >
+                            {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
+                          </button>
+                        </div>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                
+                <Button
+                  type="submit"
+                  className="w-full"
+                  disabled={isLoading}
+                >
+                  {isLoading ? (
+                    <>
+                      <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
+                      登录中...
+                    </>
+                  ) : (
+                    '登录'
+                  )}
+                </Button>
+              </form>
+            </Form>
+          </CardContent>
+          <CardFooter className="flex flex-col items-center space-y-2">
+            <div className="text-sm text-gray-500">
+              测试账号: <span className="font-medium text-gray-700">admin</span> / <span className="font-medium text-gray-700">admin123</span>
+            </div>
+            <div className="text-xs text-gray-400">
+              © {new Date().getFullYear()} 管理系统. 保留所有权利.
+            </div>
+          </CardFooter>
         </Card>
+        
+        <div className="text-center">
+          <p className="text-sm text-gray-500">
+            遇到问题?<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">联系管理员</a>
+          </p>
+        </div>
       </div>
     </div>
   );
-};
+};

+ 399 - 245
src/client/admin-shadcn/pages/Users.tsx

@@ -1,34 +1,65 @@
 import React, { useState } from 'react';
-import {
-  Button, Table, Space, Form, Input, Select, Modal, Card, Typography, Tag, Popconfirm,
-  App
-} from 'antd';
 import { useQuery } from '@tanstack/react-query';
-import dayjs from 'dayjs';
-import { roleClient, userClient } from '@/client/api';
+import { format } from 'date-fns';
+import { Plus, Search, Edit, Trash2, User, Mail, Phone } from 'lucide-react';
+import { userClient, roleClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Badge } from '@/client/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { toast } from 'sonner';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Switch } from '@/client/components/ui/switch';
+import { Label } from '@/client/components/ui/label';
 
 type UserListResponse = InferResponseType<typeof userClient.$get, 200>;
-type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
-type CreateRoleRequest = InferRequestType<typeof roleClient.$post>['json'];
-type UserDetailResponse = InferResponseType<typeof userClient[':id']['$get'], 200>;
 type CreateUserRequest = InferRequestType<typeof userClient.$post>['json'];
 type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json'];
 
-const { Title } = Typography;
+// 表单验证Schema
+const userFormSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符'),
+  nickname: z.string().optional(),
+  email: z.string().email('请输入有效的邮箱地址').optional().or(z.literal('')),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').optional().or(z.literal('')),
+  name: z.string().optional(),
+  password: z.string().min(6, '密码至少6个字符').optional(),
+  isDisabled: z.boolean().default(false),
+});
+
+type UserFormData = z.infer<typeof userFormSchema>;
 
-// 用户管理页面
 export const UsersPage = () => {
-  const { message } = App.useApp();
   const [searchParams, setSearchParams] = useState({
     page: 1,
     limit: 10,
     search: ''
   });
-  const [modalVisible, setModalVisible] = useState(false);
-  const [modalTitle, setModalTitle] = useState('');
+  const [isModalOpen, setIsModalOpen] = useState(false);
   const [editingUser, setEditingUser] = useState<any>(null);
-  const [form] = Form.useForm();
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [userToDelete, setUserToDelete] = useState<number | null>(null);
+
+  const form = useForm<UserFormData>({
+    resolver: zodResolver(userFormSchema),
+    defaultValues: {
+      username: '',
+      nickname: '',
+      email: '',
+      phone: '',
+      name: '',
+      password: '',
+      isDisabled: false,
+    },
+  });
 
   const { data: usersData, isLoading, refetch } = useQuery({
     queryKey: ['users', searchParams],
@@ -48,295 +79,418 @@ export const UsersPage = () => {
   });
 
   const users = usersData?.data || [];
-  const pagination = {
-    current: searchParams.page,
-    pageSize: searchParams.limit,
-    total: usersData?.pagination?.total || 0
-  };
+  const totalCount = usersData?.pagination?.total || 0;
 
   // 处理搜索
-  const handleSearch = (values: any) => {
-    setSearchParams(prev => ({
-      ...prev,
-      search: values.search || '',
-      page: 1
-    }));
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
   };
 
-  // 处理分页变化
-  const handleTableChange = (newPagination: any) => {
-    setSearchParams(prev => ({
-      ...prev,
-      page: newPagination.current,
-      limit: newPagination.pageSize
-    }));
+  // 处理分页
+  const handlePageChange = (page: number, limit: number) => {
+    setSearchParams(prev => ({ ...prev, page, limit }));
   };
 
-  // 打开创建用户模态框
-  const showCreateModal = () => {
-    setModalTitle('创建用户');
+  // 打开创建用户对话框
+  const handleCreateUser = () => {
     setEditingUser(null);
-    form.resetFields();
-    setModalVisible(true);
+    form.reset({
+      username: '',
+      nickname: '',
+      email: '',
+      phone: '',
+      name: '',
+      password: '',
+      isDisabled: false,
+    });
+    setIsModalOpen(true);
   };
 
-  // 打开编辑用户模态框
-  const showEditModal = (user: any) => {
-    setModalTitle('编辑用户');
+  // 打开编辑用户对话框
+  const handleEditUser = (user: any) => {
     setEditingUser(user);
-    form.setFieldsValue(user);
-    setModalVisible(true);
+    form.reset({
+      username: user.username,
+      nickname: user.nickname || '',
+      email: user.email || '',
+      phone: user.phone || '',
+      name: user.name || '',
+      isDisabled: user.isDisabled === 1,
+    });
+    setIsModalOpen(true);
   };
 
-  // 处理模态框确认
-  const handleModalOk = async () => {
+  // 处理表单提交
+  const handleSubmit = async (data: UserFormData) => {
     try {
-      const values = await form.validateFields();
-      
       if (editingUser) {
         // 编辑用户
         const res = await userClient[':id']['$put']({
           param: { id: editingUser.id },
-          json: values
+          json: {
+            ...data,
+            isDisabled: data.isDisabled ? 1 : 0,
+          } as UpdateUserRequest
         });
         if (res.status !== 200) {
           throw new Error('更新用户失败');
         }
-        message.success('用户更新成功');
+        toast.success('用户更新成功');
       } else {
         // 创建用户
         const res = await userClient.$post({
-          json: values
+          json: {
+            ...data,
+            isDisabled: data.isDisabled ? 1 : 0,
+          } as CreateUserRequest
         });
         if (res.status !== 201) {
           throw new Error('创建用户失败');
         }
-        message.success('用户创建成功');
+        toast.success('用户创建成功');
       }
       
-      setModalVisible(false);
-      form.resetFields();
-      refetch(); // 刷新用户列表
+      setIsModalOpen(false);
+      refetch();
     } catch (error) {
-      console.error('表单提交失败:', error);
-      message.error('操作失败,请重试');
+      console.error('操作失败:', error);
+      toast.error('操作失败,请重试');
     }
   };
 
   // 处理删除用户
-  const handleDelete = async (id: number) => {
+  const handleDeleteUser = (id: number) => {
+    setUserToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const confirmDelete = async () => {
+    if (!userToDelete) return;
+    
     try {
       const res = await userClient[':id']['$delete']({
-        param: { id }
+        param: { id: userToDelete }
       });
       if (res.status !== 204) {
         throw new Error('删除用户失败');
       }
-      message.success('用户删除成功');
-      refetch(); // 刷新用户列表
+      toast.success('用户删除成功');
+      refetch();
     } catch (error) {
       console.error('删除用户失败:', error);
-      message.error('删除失败,请重试');
+      toast.error('删除失败,请重试');
+    } finally {
+      setDeleteDialogOpen(false);
+      setUserToDelete(null);
     }
   };
-  
-  const columns = [
-    {
-      title: '用户名',
-      dataIndex: 'username',
-      key: 'username',
-    },
-    {
-      title: '昵称',
-      dataIndex: 'nickname',
-      key: 'nickname',
-    },
-    {
-      title: '邮箱',
-      dataIndex: 'email',
-      key: 'email',
-    },
-    {
-      title: '真实姓名',
-      dataIndex: 'name',
-      key: 'name',
-    },
-    {
-      title: '角色',
-      dataIndex: 'role',
-      key: 'role',
-      render: (role: string) => (
-        <Tag color={role === 'admin' ? 'red' : 'blue'}>
-          {role === 'admin' ? '管理员' : '普通用户'}
-        </Tag>
-      ),
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'created_at',
-      key: 'created_at',
-      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_: any, record: any) => (
-        <Space size="middle">
-          <Button type="link" onClick={() => showEditModal(record)}>
-            编辑
+
+  // 渲染加载骨架
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <h1 className="text-2xl font-bold">用户管理</h1>
+          <Button disabled>
+            <Plus className="mr-2 h-4 w-4" />
+            创建用户
           </Button>
-          <Popconfirm
-            title="确定要删除此用户吗?"
-            onConfirm={() => handleDelete(record.id)}
-            okText="确定"
-            cancelText="取消"
-          >
-            <Button type="link" danger>
-              删除
-            </Button>
-          </Popconfirm>
-        </Space>
-      ),
-    },
-  ];
-  
+        </div>
+        
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-2">
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
   return (
-    <div>
-      <div className="mb-6 flex justify-between items-center">
-        <Title level={2}>用户管理</Title>
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">用户管理</h1>
+        <Button onClick={handleCreateUser}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建用户
+        </Button>
       </div>
-      <Card className="shadow-md transition-all duration-300 hover:shadow-lg">
-        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16, padding: '16px 0' }}>
-          <Form.Item name="search" label="搜索">
-            <Input placeholder="用户名/昵称/邮箱" allowClear />
-          </Form.Item>
-          <Form.Item>
-            <Space>
-              <Button type="primary" htmlType="submit">
+
+      <Card>
+        <CardHeader>
+          <CardTitle>用户列表</CardTitle>
+          <CardDescription>
+            管理系统中的所有用户,共 {totalCount} 位用户
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索用户名、昵称或邮箱..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
                 搜索
               </Button>
-              <Button type="primary" onClick={showCreateModal}>
-                创建用户
-              </Button>
-            </Space>
-          </Form.Item>
-        </Form>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>用户名</TableHead>
+                  <TableHead>昵称</TableHead>
+                  <TableHead>邮箱</TableHead>
+                  <TableHead>真实姓名</TableHead>
+                  <TableHead>角色</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {users.map((user) => (
+                  <TableRow key={user.id}>
+                    <TableCell className="font-medium">{user.username}</TableCell>
+                    <TableCell>{user.nickname || '-'}</TableCell>
+                    <TableCell>{user.email || '-'}</TableCell>
+                    <TableCell>{user.name || '-'}</TableCell>
+                    <TableCell>
+                      <Badge 
+                        variant={user.role === 'admin' ? 'destructive' : 'default'}
+                        className="capitalize"
+                      >
+                        {user.role === 'admin' ? '管理员' : '普通用户'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      <Badge 
+                        variant={user.isDisabled === 1 ? 'secondary' : 'default'}
+                      >
+                        {user.isDisabled === 1 ? '禁用' : '启用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm')}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditUser(user)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeleteUser(user.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
 
-        <Table
-          columns={columns}
-          dataSource={users}
-          loading={isLoading}
-          rowKey="id"
-          pagination={{
-            ...pagination,
-            showSizeChanger: true,
-            showQuickJumper: true,
-            showTotal: (total) => `共 ${total} 条记录`
-          }}
-          onChange={handleTableChange}
-          bordered
-          scroll={{ x: 'max-content' }}
-          rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-        />
+          <div className="flex justify-between items-center mt-4">
+            <div className="text-sm text-muted-foreground">
+              第 {searchParams.page} 页,共 {Math.ceil(totalCount / searchParams.limit)} 页
+            </div>
+            <div className="flex gap-2">
+              <Button
+                variant="outline"
+                size="sm"
+                disabled={searchParams.page <= 1}
+                onClick={() => handlePageChange(searchParams.page - 1, searchParams.limit)}
+              >
+                上一页
+              </Button>
+              <Button
+                variant="outline"
+                size="sm"
+                disabled={searchParams.page >= Math.ceil(totalCount / searchParams.limit)}
+                onClick={() => handlePageChange(searchParams.page + 1, searchParams.limit)}
+              >
+                下一页
+              </Button>
+            </div>
+          </div>
+        </CardContent>
       </Card>
 
-      {/* 创建/编辑用户模态框 */}
-      <Modal
-        title={modalTitle}
-        open={modalVisible}
-        onOk={handleModalOk}
-        onCancel={() => {
-          setModalVisible(false);
-          form.resetFields();
-        }}
-        width={600}
-        centered
-        destroyOnClose
-        maskClosable={false}
-      >
-        <Form
-          form={form}
-          layout="vertical"
-          labelCol={{ span: 5 }}
-          wrapperCol={{ span: 19 }}
-        >
-          <Form.Item
-            name="username"
-            label="用户名"
-            required
-            rules={[
-              { required: true, message: '请输入用户名' },
-              { min: 3, message: '用户名至少3个字符' }
-            ]}
-          >
-            <Input placeholder="请输入用户名" />
-          </Form.Item>
+      {/* 创建/编辑用户对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px]">
+          <DialogHeader>
+            <DialogTitle>
+              {editingUser ? '编辑用户' : '创建用户'}
+            </DialogTitle>
+            <DialogDescription>
+              {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
+            </DialogDescription>
+          </DialogHeader>
+          
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
+              <FormField
+                control={form.control}
+                name="username"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>用户名</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入用户名" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="nickname"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>昵称</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入昵称" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="email"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>邮箱</FormLabel>
+                    <FormControl>
+                      <Input type="email" placeholder="请输入邮箱" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
 
-          <Form.Item
-            name="nickname"
-            label="昵称"
-            rules={[{ required: false, message: '请输入昵称' }]}
-          >
-            <Input placeholder="请输入昵称" />
-          </Form.Item>
+              <FormField
+                control={form.control}
+                name="phone"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>手机号</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入手机号" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
 
-          <Form.Item
-            name="email"
-            label="邮箱"
-            rules={[
-              { required: false, message: '请输入邮箱' },
-              { type: 'email', message: '请输入有效的邮箱地址' }
-            ]}
-          >
-            <Input placeholder="请输入邮箱" />
-          </Form.Item>
+              <FormField
+                control={form.control}
+                name="name"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>真实姓名</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入真实姓名" {...field} />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
 
-          <Form.Item
-            name="phone"
-            label="手机号"
-            rules={[
-              { required: false, message: '请输入手机号' },
-              { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
-            ]}
-          >
-            <Input placeholder="请输入手机号" />
-          </Form.Item>
+              {!editingUser && (
+                <FormField
+                  control={form.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>密码</FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="请输入密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+              )}
 
-          <Form.Item
-            name="name"
-            label="真实姓名"
-            rules={[{ required: false, message: '请输入真实姓名' }]}
-          >
-            <Input placeholder="请输入真实姓名" />
-          </Form.Item>
+              <FormField
+                control={form.control}
+                name="isDisabled"
+                render={({ field }) => (
+                  <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                    <div className="space-y-0.5">
+                      <FormLabel className="text-base">用户状态</FormLabel>
+                      <FormDescription>
+                        禁用后用户将无法登录系统
+                      </FormDescription>
+                    </div>
+                    <FormControl>
+                      <Switch
+                        checked={field.value}
+                        onCheckedChange={field.onChange}
+                      />
+                    </FormControl>
+                  </FormItem>
+                )}
+              />
 
-          {!editingUser && (
-            <Form.Item
-              name="password"
-              label="密码"
-              required
-              rules={[
-                { required: true, message: '请输入密码' },
-                { min: 6, message: '密码至少6个字符' }
-              ]}
-            >
-              <Input.Password placeholder="请输入密码" />
-            </Form.Item>
-          )}
+              <DialogFooter>
+                <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                  取消
+                </Button>
+                <Button type="submit">
+                  {editingUser ? '更新用户' : '创建用户'}
+                </Button>
+              </DialogFooter>
+            </form>
+          </Form>
+        </DialogContent>
+      </Dialog>
 
-          <Form.Item
-            name="isDisabled"
-            label="状态"
-            required
-            rules={[{ required: true, message: '请选择状态' }]}
-          >
-            <Select placeholder="请选择状态">
-              <Select.Option value={0}>启用</Select.Option>
-              <Select.Option value={1}>禁用</Select.Option>
-            </Select>
-          </Form.Item>
-        </Form>
-      </Modal>
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个用户吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete}>
+              删除
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
     </div>
   );
 };

+ 1 - 1
src/client/index.tsx

@@ -1,6 +1,6 @@
 // 如果当前是在 /big 下
 if (window.location.pathname.startsWith('/admin')) {
-  import('./admin/index')
+  import('./admin-shadcn/index')
 } else {
   import('./home/index')
 }