使用FastAPI和React以及MongoDB构建全栈Web应用07 FastAPI实现经典三层架构

发布于:2025-05-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、单文件简单实现

1.1 开发用户增删改查接口

main.py

from fastapi import FastAPI, Request, Query, HTTPException
from fastapi.responses import JSONResponse
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
from bson import ObjectId, errors as bson_errors
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional, List

# 创建FastAPI应用实例
app = FastAPI(
    title="用户管理API",
    description="提供用户的增删改查和分页功能",
    version="1.0.0"
)

# MongoDB 连接配置
MONGODB_URL = "mongodb://zhangdapeng:zhangdapeng520@localhost:27017"
DATABASE_NAME = "blogdb"

# 初始化MongoDB连接
client = AsyncIOMotorClient(MONGODB_URL)
db = client[DATABASE_NAME]
user_collection = db["users"]

# 配置CORS中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 在生产环境中应该指定具体的域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 全局异常处理器
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """
    全局异常处理器,捕获所有未处理的异常并返回统一的错误响应
    """
    error_msg = str(exc)
    if isinstance(exc, bson_errors.InvalidId):
        return JSONResponse(
            status_code=400,
            content={"detail": "无效的ID格式"}
        )
    return JSONResponse(
        status_code=500,
        content={"detail": f"服务器内部错误: {error_msg}"}
    )

# 用户数据模型
class User(BaseModel):
    """
    用户数据模型,定义用户的基本信息字段
    """
    name: str
    age: int
    gender: str

    class Config:
        json_schema_extra = {
            "example": {
                "name": "张三",
                "age": 25,
                "gender": "男"
            }
        }

# 用户分页响应模型
class UserResponse(BaseModel):
    """
    用户列表分页响应模型
    """
    total: int
    users: List[dict]

# API接口实现
@app.post("/users/", response_model=dict, summary="创建新用户")
async def create_user(user: User):
    """
    创建新用户
    
    Args:
        user: 用户信息对象
        
    Returns:
        dict: 包含用户ID和信息的字典
    """
    result = await user_collection.insert_one(user.dict())
    return {"_id": str(result.inserted_id), **user.dict()}

@app.get("/users/", response_model=UserResponse, summary="获取用户列表")
async def get_users(
    page: int = Query(1, gt=0, description="页码"),
    page_size: int = Query(10, gt=0, le=100, description="每页数量")
):
    """
    获取用户列表,支持分页查询
    
    Args:
        page: 当前页码
        page_size: 每页显示的数量
        
    Returns:
        UserResponse: 包含总数和用户列表的响应对象
    """
    skip = (page - 1) * page_size
    total = await user_collection.count_documents({})
    users = await user_collection.find().skip(skip).limit(page_size).to_list(length=page_size)
    
    # 转换ObjectId为字符串
    for user in users:
        user["_id"] = str(user["_id"])
    
    return {"total": total, "users": users}

@app.put("/users/{user_id}", summary="更新用户信息")
async def update_user(user_id: str, user: User):
    """
    更新指定用户的信息
    
    Args:
        user_id: 用户ID
        user: 更新的用户信息
        
    Returns:
        dict: 更新结果信息
    """
    result = await user_collection.update_one(
        {"_id": ObjectId(user_id)},
        {"$set": user.dict()}
    )
    if result.modified_count == 0:
        raise HTTPException(status_code=404, detail="用户不存在")
    return {"message": "用户更新成功"}

@app.delete("/users/{user_id}", summary="删除用户")
async def delete_user(user_id: str):
    """
    删除指定的用户
    
    Args:
        user_id: 要删除的用户ID
        
    Returns:
        dict: 删除结果信息
    """
    result = await user_collection.delete_one({"_id": ObjectId(user_id)})
    if result.deleted_count == 0:
        raise HTTPException(status_code=404, detail="用户不存在")
    return {"message": "用户删除成功"}

@app.get("/users/{user_id}", summary="获取单个用户信息")
async def get_user(user_id: str):
    """
    获取指定用户的详细信息
    
    Args:
        user_id: 用户ID
        
    Returns:
        dict: 用户信息
    """
    user = await user_collection.find_one({"_id": ObjectId(user_id)})
    if user is None:
        raise HTTPException(status_code=404, detail="用户不存在")
    user["_id"] = str(user["_id"])
    return user

# 应用启动入口
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8080)

1.2 创建React项目

package.json

{
  "name": "user_manager",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^7.6.0"
  },
  "devDependencies": {
    "@types/react": "^18.3.21",
    "@types/react-dom": "^18.3.7",
    "@vitejs/plugin-react": "^4.4.1",
    "globals": "^15.15.0",
    "vite": "^6.3.5"
  }
}

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>FastAPI+React用户管理系统</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

src/main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

src/App.jsx

import {Route, BrowserRouter, Routes} from "react-router-dom";
import UserList from "./page/UserList.jsx";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<UserList/>}/>
            </Routes>
        </BrowserRouter>
    )
}

export default App;

src/page/UserList.jsx

function UserList() {
    return (
        <div>
            <h1>用户列表</h1>
            <p>这里是用户列表的内容。</p>
        </div>
    )
}

export default UserList;

运行:

pnpm i
pnpm dev

1.3 引入zdpreact.css

src/zdpreact.css

/* 表格基础样式 */
.table {
    width: 100%;                  /* 表格宽度占满容器 */
    border-collapse: collapse;    /* 合并表格边框 */
    margin-bottom: 20px;         /* 底部外边距 */
}

/* 表格头部单元格样式 */
.table th {
    background-color: #f2f2f2;   /* 表头背景色 */
    padding: 12px;               /* 内边距 */
    border: 1px solid #ddd;      /* 边框样式 */
    text-align: left;            /* 文字左对齐 */
}

/* 表格数据单元格样式 */
.table td {
    padding: 12px;               /* 内边距 */
    border: 1px solid #ddd;      /* 边框样式 */
}

/* 分页容器样式 */
.pagination {
    display: flex;               /* 弹性布局 */
    justify-content: center;     /* 水平居中 */
    align-items: center;         /* 垂直居中 */
    gap: 10px;                  /* 按钮之间的间距 */
    margin-top: 20px;           /* 顶部外边距 */
}

/* 分页按钮基础样式 */
.pagination-button {
    padding: 8px 12px;          /* 内边距 */
    border: 1px solid #ddd;     /* 边框样式 */
    background-color: white;    /* 背景色 */
    cursor: pointer;            /* 鼠标指针样式 */
    border-radius: 4px;         /* 圆角 */
    transition: all 0.3s;       /* 过渡效果 */
}

/* 分页按钮悬停效果 */
.pagination-button:hover:not(:disabled) {
    background-color: #f0f0f0;  /* 悬停背景色 */
    border-color: #999;         /* 悬停边框色 */
}

/* 当前页码按钮样式 */
.pagination-button.active {
    background-color: #1890ff; /* 激活状态背景色 */
    color: white;              /* 激活状态文字颜色 */
    border-color: #1890ff;     /* 激活状态边框色 */
}

/* 禁用按钮样式 */
.pagination-button:disabled {
    background-color: #f5f5f5;  /* 禁用状态背景色 */
    color: #d9d9d9;            /* 禁用状态文字颜色 */
    cursor: not-allowed;       /* 禁用状态鼠标样式 */
    border-color: #d9d9d9;     /* 禁用状态边框色 */
}


/* 表格操作按钮样式 */
.table-button {
    padding: 4px 8px;          /* 内边距 */
    border: 1px solid #ddd;    /* 边框样式 */
    background-color: white;   /* 背景色 */
    cursor: pointer;           /* 鼠标指针样式 */
    border-radius: 4px;        /* 圆角 */
    transition: all 0.3s;      /* 过渡效果 */
}

.table-button:hover {
    background-color: #f0f0f0; /* 悬停背景色 */
    border-color: #999;        /* 悬停边框色 */
}

/* 删除按钮特殊样式 */
.table-button.delete {
    color: #ff4d4f;           /* 文字颜色 */
    border-color: #ff4d4f;    /* 边框颜色 */
}

.table-button.delete:hover {
    background-color: #fff1f0; /* 悬停背景色 */
    border-color: #ff7875;     /* 悬停边框色 */
}

/* 分页选择器样式 */
.pagination-select {
    padding: 8px 12px;          /* 内边距 */
    border: 1px solid #ddd;     /* 边框样式 */
    border-radius: 4px;         /* 圆角 */
    background-color: white;    /* 背景色 */
    cursor: pointer;            /* 鼠标指针样式 */
    margin-left: 10px;         /* 左边距 */
    outline: none;             /* 移除默认轮廓 */
}

.pagination-select:hover {
    border-color: #999;        /* 悬停边框色 */
}

.pagination-select:focus {
    border-color: #1890ff;     /* 聚焦边框色 */
}


/* 表格工具栏样式 */
.table-toolbar {
    margin-bottom: 16px;
    display: flex;
    justify-content: flex-end;
}

/* 主要按钮样式 */
.table-button.primary {
    background-color: #1890ff;
    color: white;
    border-color: #1890ff;
}

.table-button.primary:hover {
    background-color: #40a9ff;
    border-color: #40a9ff;
}

/* 表单样式 */
.form {
    width: 100%;
    max-width: 400px;
    padding: 20px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.form-item {
    margin-bottom: 16px;
}

.form-item label {
    display: block;
    margin-bottom: 8px;
}

/* 表单项输入框和选择框通用样式 */
.form-item input,
.form-item select {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    font-size: 14px;
    line-height: 1.5;
    transition: all 0.3s;
    box-sizing: border-box;
    background-color: #fff;
}

/* 输入框和选择框悬停样式 */
.form-item input:hover,
.form-item select:hover {
    border-color: #40a9ff;
}

/* 输入框和选择框聚焦样式 */
.form-item input:focus,
.form-item select:focus {
    border-color: #40a9ff;
    outline: none;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}

/* 选择框特殊样式 */
.form-item select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.5L1.5 4h9z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 8px center;
    padding-right: 30px;
}

/* 表单项标签样式 */
.form-item label {
    display: block;
    margin-bottom: 8px;
    color: #000000d9;
    font-size: 14px;
}

.form-buttons {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 20px;
}


/* 模态框遮罩层 */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}

/* 模态框容器 */
.modal {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    min-width: 400px;
}

/* 模态框头部 */
.modal-header {
    padding: 16px 24px;
    border-bottom: 1px solid #f0f0f0;
}

.modal-header h3 {
    margin: 0;
    color: #000000d9;
    font-weight: 500;
    font-size: 16px;
    line-height: 1.4;
}

/* 模态框内容区 */
.modal-content {
    padding: 24px;
}

/* 模态框中的表单样式 */
.modal-form {
    width: auto;
    max-width: none;
    padding: 0;
    background: none;
    box-shadow: none;
}

/* 模态框按钮容器 */
.modal-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 24px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
}


/* 表格容器样式 */
.table-container {
    width: 100%;
    overflow-x: auto;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* 空状态样式 */
.table-empty-state,
.table-loading-state {
    padding: 40px 0;
    text-align: center;
    color: #666;
}

.empty-icon {
    font-size: 48px;
    margin-bottom: 16px;
}

.empty-text,
.loading-text {
    font-size: 14px;
    margin: 0;
    color: #666;
}

/* 加载状态样式 */
.loading-spinner {
    display: inline-block;
    width: 30px;
    height: 30px;
    border: 3px solid #f3f3f3;
    border-top: 3px solid #3498db;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin-bottom: 16px;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* 表格悬停效果 */
.table tr:hover {
    background-color: #f5f5f5;
}

/* 确认删除弹窗样式 */
.confirm-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.65);
    backdrop-filter: blur(4px);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
    animation: confirmModalFadeIn 0.3s ease;
}

.confirm-modal {
    background: white;
    border-radius: 12px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
    min-width: 400px;
    padding: 24px;
    transform-origin: center;
    animation: confirmModalScaleIn 0.3s ease;
}

.confirm-modal-title {
    margin: 0 0 16px 0;
    color: #1f2937;
    font-size: 20px;
    font-weight: 600;
    line-height: 1.4;
}

.confirm-modal-content {
    margin: 0 0 24px 0;
    color: #4b5563;
    font-size: 16px;
    line-height: 1.6;
}

.confirm-modal-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 24px;
}

.confirm-modal-button {
    padding: 8px 20px;
    border: none;
    border-radius: 6px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
}

.confirm-modal-button-delete {
    color: white;
    background-color: #dc2626;
}

.confirm-modal-button-delete:hover {
    background-color: #b91c1c;
    transform: translateY(-1px);
}

.confirm-modal-button-cancel {
    color: #4b5563;
    background-color: #f3f4f6;
}

.confirm-modal-button-cancel:hover {
    background-color: #e5e7eb;
    transform: translateY(-1px);
}

@keyframes confirmModalFadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

@keyframes confirmModalScaleIn {
    from {
        opacity: 0;
        transform: scale(0.95);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

修改 src/App.jsx

import {Route, BrowserRouter, Routes} from "react-router-dom";
import "./zdpreact.css"
import UserList from "./page/UserList.jsx";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<UserList/>}/>
            </Routes>
        </BrowserRouter>
    )
}

export default App;

1.4 引入zdpreq.js

src/api/zdpreq.js

/**
 * HTTP请求客户端类
 * 封装了基本的HTTP请求方法,提供统一的接口和错误处理
 */
class ZDPReq {
    /**
     * 构造函数
     * @param {Object} config - 配置对象
     * @param {string} config.baseURL - API基础URL,默认为http://localhost:8080
     * @param {number} config.timeout - 请求超时时间(毫秒),默认为5000ms
     */
    constructor(config = {}) {
        this.config = {
            baseURL: 'http://localhost:8080',
            timeout: 5000,
            ...config
        };
    }

    /**
     * 创建超时Promise
     * 用于实现请求超时控制,当请求超过指定时间后自动reject
     * @param {number} timeout - 超时时间(毫秒)
     * @returns {Promise} 超时Promise
     * @private
     */
    _createTimeoutPromise(timeout) {
        return new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error('请求超时'));
            }, timeout);
        });
    }

    /**
     * 处理HTTP响应
     * 统一处理响应状态和错误,将响应转换为JSON格式
     * @param {Response} response - fetch API的响应对象
     * @returns {Promise<Object>} 解析后的JSON数据
     * @throws {Error} 当响应状态不为2xx时抛出错误
     * @private
     */
    async _processResponse(response) {
        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw new Error(error.detail || `请求失败: ${response.status}`);
        }
        return response.json();
    }

    /**
     * 构建完整的请求URL
     * 将baseURL、路径和查询参数组合成完整的URL
     * @param {string} url - 请求路径
     * @param {Object} params - URL查询参数对象
     * @returns {string} 完整的URL字符串
     * @private
     */
    _createFullURL(url, params) {
        const fullURL = new URL(this.config.baseURL + url);
        if (params) {
            Object.entries(params).forEach(([key, value]) => {
                fullURL.searchParams.append(key, value);
            });
        }
        return fullURL.toString();
    }

    /**
     * 发送HTTP请求的核心方法
     * 处理请求配置、超时控制和错误处理
     * @param {string} method - HTTP方法(GET、POST、PUT、DELETE等)
     * @param {string} url - 请求路径
     * @param {Object} options - 请求配置选项
     * @param {Object} [options.data] - 请求体数据
     * @param {Object} [options.params] - URL查询参数
     * @returns {Promise<Object>} 响应数据
     * @throws {Error} 请求失败时抛出错误
     * @private
     */
    async _request(method, url, options = {}) {
        const { data, params } = options;
        const fullURL = this._createFullURL(url, params);

        try {
            const fetchOptions = {
                method,
                headers: {
                    'Content-Type': 'application/json'
                }
            };

            if (data) {
                fetchOptions.body = JSON.stringify(data);
            }

            const response = await Promise.race([
                fetch(fullURL, fetchOptions),
                this._createTimeoutPromise(this.config.timeout)
            ]);

            return this._processResponse(response);
        } catch (error) {
            throw new Error(error.message || '请求失败');
        }
    }

    /**
     * 发送GET请求
     * @param {string} url - 请求路径
     * @param {Object} [params] - URL查询参数
     * @returns {Promise<Object>} 响应数据
     */
    async get(url, params) {
        return this._request('GET', url, { params });
    }

    /**
     * 发送POST请求
     * @param {string} url - 请求路径
     * @param {Object} data - 请求体数据
     * @returns {Promise<Object>} 响应数据
     */
    async post(url, data) {
        return this._request('POST', url, { data });
    }

    /**
     * 发送PUT请求
     * @param {string} url - 请求路径
     * @param {Object} data - 请求体数据
     * @returns {Promise<Object>} 响应数据
     */
    async put(url, data) {
        return this._request('PUT', url, { data });
    }

    /**
     * 发送DELETE请求
     * @param {string} url - 请求路径
     * @returns {Promise<Object>} 响应数据
     */
    async delete(url) {
        return this._request('DELETE', url);
    }
}

// 创建并导出HTTP客户端实例
export const zdpreq = new ZDPReq();

1.5 封装用户API接口请求

src/api/userApi.js

import { zdpreq } from './zdpreq';

// 用户API对象
export const userApi = {
    // 获取用户列表
    getPageUser: async (page = 1, pageSize = 10) => {
        try {
            return await zdpreq.get('/users/', {
                page,
                page_size: pageSize
            });
        } catch (error) {
            throw new Error(error.message || '获取用户列表失败');
        }
    },

    // 获取单个用户信息
    getUser: async (userId) => {
        try {
            return await zdpreq.get(`/users/${userId}`);
        } catch (error) {
            throw new Error(error.message || '获取用户信息失败');
        }
    },

    // 创建新用户
    addUser: async (userData) => {
        try {
            return await zdpreq.post('/users/', userData);
        } catch (error) {
            throw new Error(error.message || '创建用户失败');
        }
    },

    // 更新用户信息
    updateUser: async (userId, userData) => {
        try {
            return await zdpreq.put(`/users/${userId}`, userData);
        } catch (error) {
            throw new Error(error.message || '更新用户失败');
        }
    },

    // 删除用户
    deleteUser: async (userId) => {
        try {
            return await zdpreq.delete(`/users/${userId}`);
        } catch (error) {
            throw new Error(error.message || '删除用户失败');
        }
    }
};

1.6 引入表格组件

src/zdpreact/component/Table.jsx

import React from 'react';

/**
 * 通用表格组件
 * @zdpreact
 * @description 这是一个可复用的表格组件,支持自定义列配置和数据渲染
 * 
 * @param {Object[]} columns - 表格列配置数组
 * @param {string} columns[].key - 列的唯一标识,用于数据映射
 * @param {string} columns[].title - 列的标题文本
 * @param {function} [columns[].render] - 可选的自定义渲染函数,用于自定义单元格内容
 * 
 * @param {Object[]} data - 表格数据数组,每个对象代表一行数据
 * @param {boolean} loading - 加载状态标志,控制是否显示加载动画
 */
function Table({ columns, data, loading }) {
    /**
     * 渲染空数据状态的UI组件
     * @returns {JSX.Element} 返回空状态的JSX元素
     */
    const renderEmpty = () => (
        <div className="table-empty-state">
            <div className="empty-icon">📭</div>
            <p className="empty-text">暂无数据</p>
        </div>
    );

    /**
     * 渲染加载状态的UI组件
     * @returns {JSX.Element} 返回加载状态的JSX元素
     */
    const renderLoading = () => (
        <div className="table-loading-state">
            <div className="loading-spinner"></div>
            <p className="loading-text">加载中...</p>
        </div>
    );

    return (
        <div className="table-container">
            <table className="table">
                {/* 表头部分 */}
                <thead>
                    <tr>
                        {columns.map(column => (
                            <th key={column.key}>{column.title}</th>
                        ))}
                    </tr>
                </thead>
                {/* 表格主体部分 */}
                <tbody>
                    {loading ? (
                        // 加载状态显示加载动画
                        <tr>
                            <td colSpan={columns.length}>
                                {renderLoading()}
                            </td>
                        </tr>
                    ) : data.length > 0 ? (
                        // 有数据时渲染数据行
                        data.map((row, rowIndex) => (
                            <tr key={row.id || rowIndex}>
                                {columns.map(column => (
                                    <td key={column.key}>
                                        {/* 如果列配置中有render函数则使用自定义渲染,否则直接显示数据 */}
                                        {column.render ? column.render(row) : row[column.key]}
                                    </td>
                                ))}
                            </tr>
                        ))
                    ) : (
                        // 无数据时显示空状态
                        <tr>
                            <td colSpan={columns.length}>
                                {renderEmpty()}
                            </td>
                        </tr>
                    )}
                </tbody>
            </table>
        </div>
    );
}

export default Table;

1.7 引入分页组件

src/zdpreact/component/Pagination.jsx

import React from 'react';

// 分页组件
function Pagination({ currentPage, totalPages, onPageChange, pageSize, onPageSizeChange }) {
    // 定义可选的每页显示数量
    const pageSizeOptions = [2, 5, 10, 20, 50, 100];

    return (
        <div className="pagination">
            {/* 上一页按钮 */}
            <button 
                className="pagination-button"
                onClick={() => onPageChange(currentPage - 1)}
                disabled={currentPage === 1}
            >
                上一页
            </button>

            {/* 页码按钮 */}
            {Array.from({ length: totalPages }, (_, index) => (
                <button
                    key={index + 1}
                    className={`pagination-button ${currentPage === index + 1 ? 'active' : ''}`}
                    onClick={() => onPageChange(index + 1)}
                >
                    {index + 1}
                </button>
            ))}

            {/* 下一页按钮 */}
            <button 
                className="pagination-button"
                onClick={() => onPageChange(currentPage + 1)}
                disabled={currentPage === totalPages}
            >
                下一页
            </button>

            {/* 每页显示数量选择器 */}
            <select 
                className="pagination-select"
                value={pageSize}
                onChange={(e) => onPageSizeChange(Number(e.target.value))}
            >
                {pageSizeOptions.map(size => (
                    <option key={size} value={size}>
                        {size} 条/页
                    </option>
                ))}
            </select>
        </div>
    );
}

export default Pagination;

1.8 引入表单模态框组件

src/zdpreact/component/FormModal.jsx

import React from 'react';

/**
 * 表单模态框组件
 * @param {Object} props 组件属性
 * @param {boolean} props.visible 是否显示模态框
 * @param {string} props.title 模态框标题
 * @param {Array} props.fields 表单字段配置
 * @param {Object} props.initialValues 表单初始值
 * @param {function} props.onSubmit 表单提交回调
 * @param {function} props.onCancel 取消按钮回调
 */
function FormModal({ visible, title, fields, initialValues = {}, onSubmit, onCancel }) {
    if (!visible) return null;

    const handleSubmit = (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const data = {};
        fields.forEach(field => {
            const value = formData.get(field.name);
            data[field.name] = field.type === 'number' ? parseInt(value) : value;
        });
        onSubmit(data);
    };

    return (
        <div className="modal-overlay">
            <div className="modal">
                <div className="modal-header">
                    <h3>{title}</h3>
                </div>
                <div className="modal-content">
                    <form className="form modal-form" onSubmit={handleSubmit}>
                        {fields.map(field => (
                            <div className="form-item" key={field.name}>
                                <label>{field.label}:</label>
                                {field.type === 'select' ? (
                                    <select 
                                        name={field.name} 
                                        required={field.required}
                                        defaultValue={initialValues[field.name] || ''}
                                    >
                                        <option value="">{field.placeholder || '请选择'}</option>
                                        {field.options.map(option => (
                                            <option key={option.value} value={option.value}>
                                                {option.label}
                                            </option>
                                        ))}
                                    </select>
                                ) : (
                                    <input
                                        type={field.type}
                                        name={field.name}
                                        required={field.required}
                                        placeholder={field.placeholder}
                                        defaultValue={initialValues[field.name] || ''}
                                    />
                                )}
                            </div>
                        ))}
                        <div className="modal-buttons">
                            <button type="submit" className="table-button primary">
                                确认
                            </button>
                            <button 
                                type="button" 
                                className="table-button"
                                onClick={onCancel}
                            >
                                取消
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    );
}

export default FormModal;

1.9 引入确认模态框组件

src/zdpreact/component/ConfirmModal.jsx

import React from 'react';

/**
 * 确认模态框组件
 * @param {Object} props 组件属性
 * @param {boolean} props.visible 是否显示模态框
 * @param {string} props.title 模态框标题
 * @param {string} props.content 模态框内容
 * @param {function} props.onConfirm 确认按钮回调
 * @param {function} props.onCancel 取消按钮回调
 */
function ConfirmModal({ visible, title, content, onConfirm, onCancel }) {
    if (!visible) return null;

    return (
        <div className="confirm-modal-overlay">
            <div className="confirm-modal">
                <h3 className="confirm-modal-title">{title}</h3>
                <p className="confirm-modal-content">{content}</p>
                <div className="confirm-modal-buttons">
                    <button 
                        className="confirm-modal-button confirm-modal-button-delete"
                        onClick={onConfirm}
                    >
                        确认
                    </button>
                    <button 
                        className="confirm-modal-button confirm-modal-button-cancel"
                        onClick={onCancel}
                    >
                        取消
                    </button>
                </div>
            </div>
        </div>
    );
}

export default ConfirmModal;

1.10 实现用户增删改查功能

src/page/UserList.jsx

import { useState, useEffect } from 'react';
import Pagination from '../zdpreact/component/Pagination.jsx';
import Table from '../zdpreact/component/Table.jsx';
import ConfirmModal from '../zdpreact/component/ConfirmModal.jsx';
import FormModal from '../zdpreact/component/FormModal.jsx';
import { userApi } from '../api/userApi.js';

/**
 * 用户列表组件
 * 实现用户的增删改查功能,包含分页、搜索、表单验证等功能
 */
function UserList() {
    // ================ 状态管理 ================

    // 用户数据相关状态
    const [users, setUsers] = useState([]); // 用户列表数据
    const [loading, setLoading] = useState(false); // 加载状态
    const [error, setError] = useState(null); // 错误信息

    // 分页相关状态
    const [currentPage, setCurrentPage] = useState(1); // 当前页码
    const [itemsPerPage, setItemsPerPage] = useState(10); // 每页条数
    const [totalItems, setTotalItems] = useState(0); // 总记录数

    // 模态框状态管理
    const [deleteConfirm, setDeleteConfirm] = useState({
        visible: false,
        record: null
    });

    const [addUserModal, setAddUserModal] = useState({
        visible: false
    });

    const [editUserModal, setEditUserModal] = useState({
        visible: false,
        record: null
    });

    // ================ 数据加载 ================

    /**
     * 加载用户列表数据
     * 包含错误处理和加载状态管理
     */
    const loadUsers = async () => {
        try {
            setLoading(true);
            setError(null);
            const response = await userApi.getPageUser(currentPage, itemsPerPage);
            setUsers(response.users);
            setTotalItems(response.total);
        } catch (err) {
            setError('加载用户数据失败:' + (err.message || '未知错误'));
            console.error('加载用户数据失败:', err);
        } finally {
            setLoading(false);
        }
    };

    // 监听分页参数变化,重新加载数据
    useEffect(() => {
        loadUsers();
    }, [currentPage, itemsPerPage]);

    // ================ 事件处理 ================

    /**
     * 处理每页显示数量变化
     * @param {number} newSize - 新的每页显示数量
     */
    const handlePageSizeChange = (newSize) => {
        setItemsPerPage(newSize);
        setCurrentPage(1); // 切换每页数量时重置为第一页
    };

    /**
     * 处理新增用户按钮点击
     */
    const showAddUserModal = () => {
        setAddUserModal({ visible: true });
    };

    /**
     * 处理编辑按钮点击
     * @param {Object} record - 当前行用户数据
     */
    const handleEdit = (record) => {
        setEditUserModal({
            visible: true,
            record: { ...record } // 创建数据副本,避免直接修改原数据
        });
    };

    /**
     * 显示删除确认框
     * @param {Object} record - 当前行用户数据
     */
    const showDeleteConfirm = (record) => {
        setDeleteConfirm({
            visible: true,
            record
        });
    };

    // ================ 模态框操作处理 ================

    /**
     * 处理删除确认
     */
    const handleDeleteConfirm = async () => {
        try {
            setLoading(true);
            await userApi.deleteUser(deleteConfirm.record._id);
            await loadUsers();
            setDeleteConfirm({
                visible: false,
                record: null
            });
        } catch (err) {
            setError('删除用户失败:' + (err.message || '未知错误'));
            console.error('删除用户失败:', err);
        } finally {
            setLoading(false);
        }
    };

    /**
     * 处理删除取消
     */
    const handleDeleteCancel = () => {
        setDeleteConfirm({
            visible: false,
            record: null
        });
    };

    /**
     * 处理新增用户确认
     * @param {Object} formData - 表单数据
     */
    const handleAddUserConfirm = async (formData) => {
        try {
            setLoading(true);
            await userApi.addUser(formData);
            await loadUsers();
            setAddUserModal({ visible: false });
        } catch (err) {
            setError('新增用户失败:' + (err.message || '未知错误'));
            console.error('新增用户失败:', err);
        } finally {
            setLoading(false);
        }
    };

    /**
     * 处理新增用户取消
     */
    const handleAddUserCancel = () => {
        setAddUserModal({ visible: false });
    };

    /**
     * 处理编辑用户确认
     * @param {Object} formData - 表单数据
     */
    const handleEditUserConfirm = async (formData) => {
        try {
            setLoading(true);
            await userApi.updateUser(editUserModal.record._id, formData);
            await loadUsers();
            setEditUserModal({
                visible: false,
                record: null
            });
        } catch (err) {
            setError('更新用户失败:' + (err.message || '未知错误'));
            console.error('更新用户失败:', err);
        } finally {
            setLoading(false);
        }
    };

    /**
     * 处理编辑用户取消
     */
    const handleEditUserCancel = () => {
        setEditUserModal({
            visible: false,
            record: null
        });
    };

    // ================ 配置项 ================

    // 表格列定义
    const columns = [
        { key: 'name', title: '姓名' },
        { key: 'age', title: '年龄' },
        { key: 'gender', title: '性别' },
        {
            key: 'action',
            title: '操作',
            render: (record) => (
                <div style={{ display: 'flex', gap: '8px' }}>
                    <button
                        className="table-button"
                        onClick={() => handleEdit(record)}
                        disabled={loading}
                    >
                        编辑
                    </button>
                    <button
                        className="table-button delete"
                        onClick={() => showDeleteConfirm(record)}
                        disabled={loading}
                    >
                        删除
                    </button>
                </div>
            )
        }
    ];

    // 用户表单字段配置
    const userFields = [
        {
            name: 'name',
            label: '姓名',
            type: 'text',
            required: true
        },
        {
            name: 'age',
            label: '年龄',
            type: 'number',
            required: true
        },
        {
            name: 'gender',
            label: '性别',
            type: 'select',
            required: true,
            options: [
                { value: '男', label: '男' },
                { value: '女', label: '女' }
            ]
        }
    ];

    // ================ 渲染 ================

    return (
        <div className="App">
            <header className="App-header">
                <h1>用户列表</h1>

                {/* 错误信息显示 */}
                {error && (
                    <div className="error-message" style={{ color: 'red', margin: '10px 0' }}>
                        {error}
                    </div>
                )}

                {/* 操作栏 */}
                <div className="table-toolbar">
                    <button
                        className="table-button primary"
                        onClick={showAddUserModal}
                        disabled={loading}
                    >
                        新增用户
                    </button>
                </div>

                {/* 用户列表表格 */}
                <Table
                    columns={columns}
                    data={users}
                    loading={loading}
                />

                {/* 分页组件 */}
                <Pagination
                    currentPage={currentPage}
                    totalPages={Math.ceil(totalItems / itemsPerPage)}
                    onPageChange={setCurrentPage}
                    pageSize={itemsPerPage}
                    onPageSizeChange={handlePageSizeChange}
                />

                {/* 删除确认框 */}
                <ConfirmModal
                    visible={deleteConfirm.visible}
                    title="确认删除"
                    content={`确定要删除用户 "${deleteConfirm.record?.name}" 吗?`}
                    onConfirm={handleDeleteConfirm}
                    onCancel={handleDeleteCancel}
                />

                {/* 新增用户表单 */}
                <FormModal
                    visible={addUserModal.visible}
                    title="新增用户"
                    fields={userFields}
                    onSubmit={handleAddUserConfirm}
                    onCancel={handleAddUserCancel}
                />

                {/* 编辑用户表单 */}
                <FormModal
                    visible={editUserModal.visible}
                    title="编辑用户"
                    fields={userFields}
                    initialValues={editUserModal.record}
                    onSubmit={handleEditUserConfirm}
                    onCancel={handleEditUserCancel}
                />
            </header>
        </div>
    );
}

export default UserList;

二、经典三层架构

2.1 MongoDB数据库依赖

app/dependency/database.py

from typing import AsyncGenerator
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
from fastapi import Depends

# MongoDB配置
MONGO_URL = "mongodb://zhangdapeng:zhangdapeng520@localhost:27017/"
DATABASE_NAME = "users_db"

# 创建MongoDB客户端
client = AsyncIOMotorClient(MONGO_URL)


async def get_database() -> AsyncGenerator[AsyncIOMotorDatabase, None]:
    """
    获取数据库连接的依赖函数
    """
    try:
        db = client[DATABASE_NAME]
        yield db
    finally:
        # 在这里可以添加清理代码如果需要
        pass


# 获取用户集合的依赖函数
async def get_user_collection():
    """
    获取用户集合的依赖函数
    """
    db = client[DATABASE_NAME]
    return db["users"]

2.2 跨域中间件

app/middleware/cors_middleware.py

from fastapi.middleware.cors import CORSMiddleware


def setup_cors_middleware(app):
    """配置CORS中间件"""
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],

2.3 全局异常处理

app/middleware/error_handler.py

from fastapi import Request
from fastapi.responses import JSONResponse
from bson import errors as bson_errors


def setup_error_handler(app):
    """配置全局异常处理器"""

    @app.exception_handler(Exception)
    async def global_exception_handler(request: Request, exc: Exception):
        error_msg = str(exc)
        if isinstance(exc, bson_errors.InvalidId):
            return JSONResponse(
                status_code=400,
                content={"message": "Invalid ObjectId"},
            )
        return JSONResponse(
            status_code=500,
            content={"message": f"服务器内部错误: {error_msg}"},
        )

2.4 初始化所有中间件

app/middleware/__init__.py

from .cors_middleware import setup_cors_middleware
from .error_handler import setup_error_handler


def setup_middlewares(app):
    """初始化所有中间件"""
    setup_cors_middleware(app)
    setup_error_handler(app)

2.5 用户请求参数

app/schema/user_model.py

from pydantic import BaseModel
from typing import List


class User(BaseModel):
    name: str
    age: int
    gender: str

    class Config:
        json_schema_extra = {
            "example": {
                "name": "张三",
                "age": 30,
                "gender": "男"
            }
        }


class UserResponse(BaseModel):
    total: int
    users: List[dict]

2.6 用户dao层

app/dao/user_dao.py

from bson import ObjectId
from ..dependency.database import get_user_collection


class UserDAO:
    def __init__(self):
        self.collection = None

    async def initialize(self):
        """初始化数据库集合"""
        self.collection = await get_user_collection()

    async def create_user(self, user_dict: dict) -> str:
        """创建用户"""
        await self.initialize()
        result = await self.collection.insert_one(user_dict)
        return str(result.inserted_id)

    async def get_users(self, skip: int, limit: int) -> tuple[list, int]:
        """获取用户列表"""
        await self.initialize()
        users = await self.collection.find().skip(skip).limit(limit).to_list(length=limit)
        total = await self.collection.count_documents({})
        for user in users:
            user["_id"] = str(user["_id"])
        return users, total

    async def update_user(self, user_id: str, user_dict: dict) -> bool:
        """更新用户"""
        await self.initialize()
        result = await self.collection.update_one(
            {"_id": ObjectId(user_id)},
            {"$set": user_dict}
        )
        return result.modified_count > 0

    async def delete_user(self, user_id: str) -> bool:
        """删除用户"""
        await self.initialize()
        result = await self.collection.delete_one({"_id": ObjectId(user_id)})
        return result.deleted_count > 0

    async def get_user(self, user_id: str) -> dict:
        """获取单个用户"""
        await self.initialize()
        user = await self.collection.find_one({"_id": ObjectId(user_id)})
        if user:
            user["_id"] = str(user["_id"])
        return user

2.7 用户service层

app/service/user_service.py

from fastapi import HTTPException
from ..dao.user_dao import UserDAO
from ..schema.user_model import User


class UserService:
    def __init__(self):
        self.user_dao = UserDAO()

    async def create_user(self, user: User) -> dict:
        """
        创建用户服务
        """
        user_dict = user.dict()
        user_id = await self.user_dao.create_user(user_dict)
        return {"message": "用户创建成功", "id": user_id}

    async def get_users(self, skip: int, page_size: int) -> dict:
        """
        获取用户列表服务
        """
        users, total = await self.user_dao.get_users(skip, page_size)
        return {"total": total, "users": users}

    async def update_user(self, user_id: str, user: User) -> dict:
        """
        更新用户服务
        """
        user_dict = user.dict()
        if not await self.user_dao.update_user(user_id, user_dict):
            raise HTTPException(status_code=404, detail="用户不存在")
        return {"message": "用户更新成功"}

    async def delete_user(self, user_id: str) -> dict:
        """
        删除用户服务
        """
        if not await self.user_dao.delete_user(user_id):
            raise HTTPException(status_code=404, detail="用户不存在")
        return {"message": "用户删除成功"}

    async def get_user(self, user_id: str) -> User:
        """
        获取单个用户服务
        """
        user = await self.user_dao.get_user(user_id)
        if user is None:
            raise HTTPException(status_code=404, detail="用户不存在")
        return User(**user)

2.8 用户controller层

app/controller/user_controller.py

from fastapi import APIRouter, Query
from ..schema.user_model import User, UserResponse
from ..service.user_service import UserService

router = APIRouter(tags=["用户管理"])
user_service = UserService()


@router.post("/users/", response_model=dict, summary="创建新用户")
async def create_user(user: User):
    return await user_service.create_user(user)


@router.get("/users/", response_model=UserResponse, summary="获取用户列表")
async def get_users(
        page: int = Query(1, ge=1),
        page_size: int = Query(10, ge=1)):
    skip = (page - 1) * page_size
    return await user_service.get_users(skip, page_size)


@router.put("/users/{id}", response_model=dict, summary="更新用户信息")
async def update_user(id: str, user: User):
    return await user_service.update_user(id, user)


@router.delete("/users/{id}", response_model=dict, summary="删除用户")
async def delete_user(id: str):
    return await user_service.delete_user(id)


@router.get("/users/{id}", response_model=User, summary="获取单个用户信息")
async def get_user(id: str):
    return await user_service.get_user(id)

2.9 入口程序

main.py

from fastapi import FastAPI
from app.controller.user_controller import router as user_router
from app.middleware import setup_middlewares

# 创建app
app = FastAPI(
    title="用户管理API",
    description="提供用户增删改查和分页查询功能",
    version="1.0.0",
)

# 初始化中间件
setup_middlewares(app)

# 注册路由
app.include_router(user_router, tags=["用户管理"])

if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8080)

总结

源滚滚编程提供全套的PDF文档,配套源代码,录播课,私教课和直播课,关注并私信我咨询获取。


网站公告

今日签到

点亮在社区的每一天
去签到