浏览代码

Merge branch 'starter' of 139-template-116/d8d-vite-starter into starter

18617351030 5 月之前
父节点
当前提交
4436e23508

+ 124 - 0
.roo/rules/13-ui-style.md

@@ -0,0 +1,124 @@
+# 管理后台界面开发规范
+
+## 1. 布局规范
+
+### 1.1 整体布局结构
+- 采用三栏布局:侧边导航栏 + 顶部操作栏 + 主内容区
+- 侧边栏固定宽度240px,支持折叠功能
+- 顶部导航栏高度固定为64px
+- 主内容区边距统一为24px
+
+### 1.2 响应式设计
+- 桌面端:完整三栏布局
+- 平板端:可折叠侧边栏
+- 移动端:侧边栏转为抽屉式导航
+
+### 1.3 容器样式
+- 卡片容器使用白色背景(#ffffff)
+- 卡片阴影使用 `shadow-sm transition-all duration-300 hover:shadow-md`
+- 卡片边框使用 `border: none`
+- 卡片圆角统一为 `border-radius: 6px`
+
+## 2. 色彩规范
+
+### 2.1 主色调
+- 主色:蓝色(#1890ff),用于主要按钮、选中状态和关键交互元素
+- 辅助色:绿色(#52c41a)用于成功状态,红色(#ff4d4f)用于错误状态,黄色(#faad14)用于警告状态
+
+### 2.2 中性色
+- 背景色:浅灰(#f5f5f5)用于页面背景,白色(#ffffff)用于卡片背景
+- 文本色:深灰(#1f2937)用于主要文本,中灰(#6b7280)用于次要文本,浅灰(#9ca3af)用于提示文本
+
+## 3. 组件样式规范
+
+### 3.1 按钮样式
+- 主要按钮:使用主色调背景,白色文字
+- 按钮高度统一为40px,大型按钮使用48px
+- 按钮圆角统一为4px
+- 按钮文本使用14px字体
+- 按钮添加悬停效果:`hover:shadow-lg transition-all duration-200`
+
+### 3.2 表单元素
+- 输入框高度统一为40px
+- 输入框前缀图标颜色使用主色调
+- 表单标签宽度统一为80px
+- 表单布局使用垂直布局,标签在上,输入框在下
+- 输入框聚焦状态:`focus:border-primary focus:ring-1 focus:ring-primary`
+
+### 3.3 表格样式
+- 表格添加边框:`bordered`
+- 表头背景色使用浅灰(#f9fafb)
+- 表格行添加交替背景色:`rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`
+- 支持横向滚动:`scroll={{ x: 'max-content' }}`
+
+### 3.4 卡片组件
+- 卡片标题区使用 `flex items-center justify-between` 布局
+- 统计数字使用28px字体大小
+- 添加卡片图标时使用24px大小,颜色与统计项主题匹配
+- 卡片底部添加辅助信息,使用12px浅灰色字体
+
+## 4. 页面规范
+
+### 4.1 页面标题
+- 页面标题使用 `Title level={2}` 组件
+- 标题区添加 `mb-6 flex justify-between items-center` 样式
+- 标题右侧可放置操作按钮组
+
+### 4.2 登录页面
+- 使用渐变背景:`bg-gradient-to-br from-blue-50 to-indigo-100`
+- 登录卡片居中显示,添加阴影效果:`shadow-lg`
+- 登录表单添加图标前缀增强可读性
+- 底部添加版权信息和测试账号提示
+
+### 4.3 数据展示页面
+- 数据卡片使用响应式布局,在不同屏幕尺寸下自动调整列数
+- 关键数据使用 `Statistic` 组件展示
+- 添加数据趋势指示和环比增长信息
+- 数据加载状态使用 `loading` 属性
+
+## 5. 交互规范
+
+### 5.1 悬停效果
+- 可交互元素添加悬停效果
+- 卡片悬停效果:`hover:shadow-md transition-all duration-300`
+- 按钮悬停效果:`hover:shadow-lg transition-all duration-200`
+
+### 5.2 模态框
+- 模态框使用 `destroyOnClose` 属性确保每次打开都是新实例
+- 模态框居中显示:`centered`
+- 禁止点击遮罩关闭:`maskClosable={false}`
+- 表单模态框使用垂直布局
+
+### 5.3 反馈机制
+- 操作成功/失败使用 `message` 组件提供反馈
+- 加载状态使用 `loading` 属性显示加载指示器
+- 删除等危险操作使用 `Popconfirm` 组件二次确认
+
+## 5.4 消息提示规范
+- 统一使用App.useApp()获取message实例
+  ```typescript
+  import { App } from 'antd';
+  const { message } = App.useApp();
+  ```
+- 消息提示使用明确的类型区分:
+  ```typescript
+  message.success('操作成功');
+  message.error('操作失败');
+  message.warning('警告信息');
+  message.info('提示信息');
+  ```
+- 消息显示时长统一使用默认值,重要操作可适当延长:`message.success('操作成功', 3);`
+
+## 6. 图标规范
+
+### 6.1 图标选择
+- 用户相关:UserOutlined
+- 密码相关:LockOutlined
+- 搜索相关:SearchOutlined
+- 消息相关:BellOutlined
+- 眼睛相关:EyeOutlined/EyeInvisibleOutlined
+
+### 6.2 图标样式
+- 功能图标大小统一为24px
+- 前缀图标颜色与主题匹配
+- 操作图标使用 `Button type="link"` 样式

+ 24 - 6
src/client/admin/index.tsx

@@ -1,11 +1,12 @@
 import { createRoot } from 'react-dom/client'
 import { RouterProvider } from 'react-router';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { App as AntdApp } from 'antd'
+import { App as AntdApp , ConfigProvider} from 'antd'
 import dayjs from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import localeData from 'dayjs/plugin/localeData';
 import 'dayjs/locale/zh-cn';
+import zhCN from 'antd/locale/zh_CN';
 
 import { AuthProvider } from './hooks/AuthProvider';
 import { router } from './routes';
@@ -24,11 +25,28 @@ const queryClient = new QueryClient();
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
-      <AntdApp>
-        <AuthProvider>
-          <RouterProvider router={router} />
-        </AuthProvider>
-      </AntdApp>
+      <ConfigProvider locale={zhCN} theme={{
+        token: {
+          colorPrimary: '#1890ff',
+          borderRadius: 4,
+          colorBgContainer: '#f5f5f5',
+        },
+        components: {
+          Button: {
+            borderRadius: 4,
+          },
+          Card: {
+            borderRadius: 6,
+            boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
+          }
+        }
+      }}>
+        <AntdApp>
+          <AuthProvider>
+            <RouterProvider router={router} />
+          </AuthProvider>
+        </AntdApp>
+      </ConfigProvider>
     </QueryClientProvider>
   )
 };

+ 10 - 4
src/client/admin/layouts/MainLayout.tsx

@@ -125,11 +125,12 @@ export const MainLayout = () => {
           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">
-            {collapsed ? '应用' : appName}
+            <span className="text-white">{collapsed ? '应用' : appName}</span>
           </Typography.Title>
           
           {/* 菜单搜索框 */}
@@ -147,7 +148,7 @@ export const MainLayout = () => {
         
         {/* 菜单列表 */}
         <Menu
-          theme='light'
+          theme='dark'
           mode="inline"
           items={filteredMenuItems}
           openKeys={openKeys}
@@ -155,13 +156,18 @@ export const MainLayout = () => {
           onOpenChange={onOpenChange}
           onClick={({ key }) => handleMenuClick(key)}
           inlineCollapsed={collapsed}
+          style={{
+            backgroundColor: 'transparent',
+            borderRight: 'none'
+          }}
         />
       </Sider>
       
       <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 4px rgba(0,21,41,0.08)'
+            boxShadow: '0 1px 8px rgba(0,21,41,0.12)',
+            borderBottom: '1px solid #f0f0f0'
           }}
         >
           <Button
@@ -194,7 +200,7 @@ export const MainLayout = () => {
         </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">
+          <div className="site-layout-content p-6 rounded-lg bg-white shadow-sm transition-all duration-300 hover:shadow-md">
             <Outlet />
           </div>
           

+ 4 - 1
src/client/admin/menu.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
+import { useAuth } from './hooks/AuthProvider';
 import type { MenuProps } from 'antd';
 import {
   UserOutlined,
@@ -65,6 +66,7 @@ export const useMenuSearch = (menuItems: MenuItem[]) => {
 
 export const useMenu = () => {
   const navigate = useNavigate();
+  const { logout: handleLogout } = useAuth();
   const [collapsed, setCollapsed] = React.useState(false);
   const [openKeys, setOpenKeys] = React.useState<string[]>([]);
 
@@ -96,7 +98,8 @@ export const useMenu = () => {
       key: 'logout',
       label: '退出登录',
       icon: <InfoCircleOutlined />,
-      danger: true
+      danger: true,
+      onClick: () => handleLogout()
     }
   ];
 

+ 44 - 13
src/client/admin/pages/Dashboard.tsx

@@ -1,7 +1,10 @@
 import React from 'react';
-import { 
-  Card, Row, Col, Typography, Statistic
+import {
+  Card, Row, Col, Typography, Statistic, Space
 } from 'antd';
+import {
+  UserOutlined, BellOutlined, EyeOutlined
+} from '@ant-design/icons';
 
 const { Title } = Typography;
 
@@ -9,33 +12,61 @@ const { Title } = Typography;
 export const DashboardPage = () => {
   return (
     <div>
-      <Title level={2}>仪表盘</Title>
-      <Row gutter={16}>
-        <Col span={8}>
-          <Card>
+      <div className="mb-6 flex justify-between items-center">
+        <Title level={2}>仪表盘</Title>
+      </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
-              title="活跃用户"
               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 span={8}>
-          <Card>
+        <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
-              title="系统消息"
               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 span={8}>
-          <Card>
+        <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>
             <Statistic
-              title="在线用户"
               value={1128}
               loading={false}
+              valueStyle={{ fontSize: 28 }}
+              suffix="人"
             />
+            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
+              当前在线率 32.1%
+            </div>
           </Card>
         </Col>
       </Row>

+ 31 - 17
src/client/admin/pages/Login.tsx

@@ -8,6 +8,9 @@ import {
 } from 'antd';
 import {
   UserOutlined,
+  LockOutlined,
+  EyeOutlined,
+  EyeInvisibleOutlined
 } from '@ant-design/icons';
 import { useNavigate } from 'react-router';
 import {
@@ -54,15 +57,19 @@ export const LoginPage = () => {
   };
   
   return (
-    <div className="min-h-screen flex items-center justify-center bg-gray-50 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 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
       <div className="max-w-md w-full space-y-8">
-        <div>
-          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
-            登录管理后台
+        <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>
+          <h2 className="mt-2 text-center text-3xl font-extrabold text-gray-900">
+            管理后台登录
           </h2>
+          <p className="mt-2 text-gray-500">请输入您的账号和密码</p>
         </div>
         
-        <Card>
+        <Card className="shadow-lg border-none transition-all duration-300 hover:shadow-xl">
           <Form
             form={form}
             name="login"
@@ -73,40 +80,47 @@ export const LoginPage = () => {
             <Form.Item
               name="username"
               rules={[{ required: true, message: '请输入用户名' }]}
+              label="用户名"
             >
-              <Input 
-                prefix={<UserOutlined />} 
-                placeholder="用户名" 
+              <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 
-                placeholder="密码" 
+              <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" 
+              <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-4 text-center text-gray-500">
-            <p>测试账号: admin / admin123</p>
-            {/* <p>普通账号: user1 / 123456</p> */}
+          <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>
       </div>

+ 33 - 6
src/client/admin/pages/Users.tsx

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import {
-  Button, Table, Space, Form, Input, Select,
-  message, Modal, Card, Typography, Tag, Popconfirm
+  Button, Table, Space, Form, Input, Select, Modal, Card, Typography, Tag, Popconfirm,
+  App
 } from 'antd';
 import { useQuery } from '@tanstack/react-query';
 import dayjs from 'dayjs';
@@ -19,6 +19,7 @@ const { Title } = Typography;
 
 // 用户管理页面
 export const UsersPage = () => {
+  const { message } = App.useApp();
   const [searchParams, setSearchParams] = useState({
     page: 1,
     limit: 10,
@@ -155,6 +156,11 @@ export const UsersPage = () => {
       dataIndex: 'email',
       key: 'email',
     },
+    {
+      title: '真实姓名',
+      dataIndex: 'name',
+      key: 'name',
+    },
     {
       title: '角色',
       dataIndex: 'role',
@@ -196,9 +202,11 @@ export const UsersPage = () => {
   
   return (
     <div>
-      <Title level={2}>用户管理</Title>
-      <Card>
-        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+      <div className="mb-6 flex justify-between items-center">
+        <Title level={2}>用户管理</Title>
+      </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>
@@ -226,6 +234,9 @@ export const UsersPage = () => {
             showTotal: (total) => `共 ${total} 条记录`
           }}
           onChange={handleTableChange}
+          bordered
+          scroll={{ x: 'max-content' }}
+          rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
         />
       </Card>
 
@@ -239,14 +250,20 @@ export const UsersPage = () => {
           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个字符' }
@@ -258,7 +275,7 @@ export const UsersPage = () => {
           <Form.Item
             name="nickname"
             label="昵称"
-            rules={[{ required: true, message: '请输入昵称' }]}
+            rules={[{ required: false, message: '请输入昵称' }]}
           >
             <Input placeholder="请输入昵称" />
           </Form.Item>
@@ -285,10 +302,19 @@ export const UsersPage = () => {
             <Input placeholder="请输入手机号" />
           </Form.Item>
 
+          <Form.Item
+            name="name"
+            label="真实姓名"
+            rules={[{ required: false, message: '请输入真实姓名' }]}
+          >
+            <Input placeholder="请输入真实姓名" />
+          </Form.Item>
+
           {!editingUser && (
             <Form.Item
               name="password"
               label="密码"
+              required
               rules={[
                 { required: true, message: '请输入密码' },
                 { min: 6, message: '密码至少6个字符' }
@@ -301,6 +327,7 @@ export const UsersPage = () => {
           <Form.Item
             name="isDisabled"
             label="状态"
+            required
             rules={[{ required: true, message: '请选择状态' }]}
           >
             <Select placeholder="请选择状态">

+ 6 - 0
src/server/api/users/post.ts

@@ -14,6 +14,12 @@ const CreateUserSchema = UserSchema.omit({
   id: true,
   createdAt: true,
   updatedAt: true,
+}).extend({
+  phone: UserSchema.shape.phone.optional(),
+  email: UserSchema.shape.email.optional(),
+  nickname: UserSchema.shape.nickname.optional(),
+  name: UserSchema.shape.name.optional(),
+  avatar: UserSchema.shape.avatar.optional(),
 })
 
 const createUserRoute = createRoute({