Contents

下面是一个基于 DeepSeek 的开发教程
能否请你给我开发一个详尽的前后端开发教程呢?比如图书管理系统, 可以比较复杂一些, 用于学习

软硬件基础:
Ubuntu 20.04.6 LTS
node v18.17.0
Python 3.12.12
typescript ~5.7.2

技术选型:
FastAPI 高效异步, Vue3 组合式 API 灵活, TDesign 企业级组件库美观且完善, PostgreSQL 稳定可靠

我的环境已经按照如下笔记内容配置好了:
你可以自行决定是否新增数据库或者表等等


FastAPI 与 Vue.js 小示例

环境搭建

vscode 推荐扩展

Python、Black Formatter
Vue、Prettier
PostgreSQL

从源码编译安装 Python

Ubuntu 有 Python,想安装其他版本 python:https://blog.yongit.com/note/1573133.html
注意:此处使用的 Linux 发行版本是 Ubuntu 20.04.6 LTS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sudo apt update
sudo apt upgrade

# 安装编译 Python 所需的依赖项,包括构建工具等
sudo apt install -y build-essential tk-dev libncurses5-dev libncursesw5-dev libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev libexpat1-dev liblzma-dev libffi-dev zlib1g-dev


wget https://www.python.org/ftp/python/3.12.12/Python-3.12.12.tgz

tar zxf Python-3.12.12.tgz

cd Python-3.12.12
./configure --enable-optimizations

# 开始编译
make -j $(nproc)

# 进行安装, altinstall 选项可以避免覆盖系统默认的 Python 3 版本
sudo make altinstall

创建虚拟环境

1
2
3
4
python3.12
python3.12 -m venv venv

source venv/bin/activate

安装 PostgreSQL 数据库

1
2
3
4
5
6
7
8
sudo apt install postgresql postgresql-contrib
pg_config --version

dpkg -l | grep postgresql
sudo -u postgres psql -c "SELECT version();"
# 启动服务
sudo service postgresql restart
sudo -u postgres psql -c "SELECT version();"

初始化数据库, 配置用户

1
2
3
4
5
6
7
8
9
sudo -u postgres psql
-- 以下是 SQL 命令
CREATE DATABASE taskdb;  -- 创建数据库 taskdb
CREATE USER cloud_studio WITH PASSWORD '123456';  -- 创建用户并设置密码
GRANT ALL PRIVILEGES ON DATABASE taskdb TO cloud_studio;  -- 给用户授权
\password postgres  -- 修改用户密码
\q  -- 退出
\l
\c taskdb

开启后端服务

1
python3.12 -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

Vite

Vite 7 要求最低 Node.js 版本是 20.19+ 或 22.12+, 放弃了对 Node.js 18 的支持
故我们需要安装 Vite 6

通过 create-vite 脚手架创建一个 vue3 ts 的开发环境

1
2
3
4
5
npm view vite versions
npm init vite@6.4.1
cd library-test
npm install
npm run dev

修改 package.json 文件中的 scripts.dev 为 vite --host 暴露端口
然后在 vite.config.ts 中添加

1
2
3
server: {
  allowedHosts: ["5173.app.cloudstudio.work"];
}

安装 Router、Pinia、Vitest、Prettier
先全局安装 npm install vue-router@4 pinia
局部安装开发依赖 npm install -D axios prettier tdesign-vue-next

package.json 中的依赖版本:
vue ^3.5.13
vue-router ^4.5.1
pinia ^3.0.3

详尽的前后端开发教程:图书管理系统(FastAPI + Vue3 + TDesign + PostgreSQL)

本教程将带你从零构建一个功能完善的图书管理系统,采用前后端分离架构。后端使用 FastAPI(异步 Python)、前端使用 Vue3(组合式 API + TypeScript)和 TDesign 企业级组件库,数据库选用 PostgreSQL。系统包含用户认证、图书管理、借阅管理、分类管理等核心功能,适合作为进阶学习项目。

目录

  1. 项目概述与功能规划
  2. 技术栈详解
  3. 数据库设计
  4. 后端开发(FastAPI)
    • 4.1 项目初始化与依赖
    • 4.2 项目结构
    • 4.3 数据库连接与模型
    • 4.4 用户认证(JWT)
    • 4.5 路由与业务逻辑
    • 4.6 运行与测试
  5. 前端开发(Vue3 + TypeScript + TDesign)
    • 5.1 项目初始化与依赖
    • 5.2 项目结构
    • 5.3 路由与布局
    • 5.4 状态管理(Pinia)
    • 5.5 API 请求封装
    • 5.6 页面组件实现
    • 5.7 运行与调试
  6. 部署与运行指南
  7. 扩展功能建议

1. 项目概述与功能规划

图书管理系统的目标是让图书馆管理员和普通用户能够便捷地管理图书和借阅记录。主要角色:

  • 管理员:管理图书(增删改查)、管理分类、管理用户、查看所有借阅记录。
  • 普通用户:浏览图书、借书、还书、查看个人借阅历史。

核心功能模块

  • 用户模块:注册、登录、个人信息管理、权限区分。
  • 图书模块:图书信息(书名、作者、ISBN、出版社、出版年份、分类、库存数量等)、图书检索(按分类、书名、作者等)、图书封面图片上传。
  • 借阅模块:借书、还书、续借、借阅记录查询、逾期判断。
  • 分类模块:图书分类的增删改查。

非功能性需求:API 遵循 RESTful 风格,使用 JWT 进行身份验证,前端响应式布局,数据库事务保证借阅操作的原子性。


2. 技术栈详解

层级 技术/工具 说明
后端 FastAPI 高性能异步框架,自动生成 OpenAPI 文档
SQLAlchemy 2.0 ORM,支持异步操作
Pydantic 数据验证与序列化
python-jose[cryptography] JWT 生成与验证
passlib[bcrypt] 密码哈希
asyncpg PostgreSQL 异步驱动
Alembic 数据库迁移
前端 Vue 3 组合式 API + <script setup>
TypeScript 类型安全
Vite 6 构建工具
TDesign (vue-next) 腾讯企业级组件库,美观且功能丰富
Vue Router 4 前端路由
Pinia 状态管理
axios HTTP 客户端
数据库 PostgreSQL 12+ 稳定可靠的关系型数据库
工具 Postman / Insomnia API 测试
pgAdmin / DBeaver 数据库管理

3. 数据库设计

根据功能规划,设计以下表:

  • users:用户表
  • books:图书表
  • categories:分类表
  • borrow_records:借阅记录表

ER 图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
+----------------+       +----------------+       +----------------+
|    users       |       |   books        |       |  categories    |
+----------------+       +----------------+       +----------------+
| id (PK)        |<----->| id (PK)        |       | id (PK)        |
| username       |       | title          |       | name           |
| password_hash  |       | author         |       | description    |
| email          |       | isbn           |       +----------------+
| full_name      |       | publisher      |
| is_active      |       | publish_year   |
| is_admin       |       | category_id(FK)|---->  +----------------+
| created_at     |       | total_copies   |       | borrow_records |
+----------------+       | available_copies|      +----------------+
                         | cover_url      |       | id (PK)        |
                         | description    |       | user_id(FK)    |
                         +----------------+       | book_id(FK)    |
                                                  | borrow_date    |
                                                  | due_date       |
                                                  | return_date    |
                                                  | status         |
                                                  +----------------+

SQL 创建语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
-- 分类表
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE,
    description TEXT
);

-- 用户表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    full_name VARCHAR(100),
    is_active BOOLEAN DEFAULT TRUE,
    is_admin BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 图书表
CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    author VARCHAR(100) NOT NULL,
    isbn VARCHAR(20) UNIQUE,
    publisher VARCHAR(100),
    publish_year INTEGER,
    category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
    total_copies INTEGER NOT NULL DEFAULT 1,
    available_copies INTEGER NOT NULL DEFAULT 1,
    cover_url TEXT,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 借阅记录表
CREATE TABLE borrow_records (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
    borrow_date DATE NOT NULL DEFAULT CURRENT_DATE,
    due_date DATE NOT NULL,
    return_date DATE,
    status VARCHAR(20) DEFAULT 'borrowed' CHECK (status IN ('borrowed', 'returned', 'overdue')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建索引
CREATE INDEX idx_books_title ON books(title);
CREATE INDEX idx_books_author ON books(author);
CREATE INDEX idx_borrow_records_user ON borrow_records(user_id);
CREATE INDEX idx_borrow_records_book ON borrow_records(book_id);

4. 后端开发(FastAPI)

4.1 项目初始化与依赖

确保在虚拟环境中安装以下依赖(使用 pippip3.12):

1
pip install fastapi uvicorn[standard] sqlalchemy asyncpg python-jose[cryptography] passlib[bcrypt] python-multipart alembic

4.2 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
backend/
├── app/
│   ├── __init__.py
│   ├── main.py                 # 应用入口
│   ├── core/                    # 核心配置
│   │   ├── config.py            # 配置类(数据库URL、JWT密钥等)
│   │   ├── security.py          # 密码哈希、JWT工具
│   │   └── database.py          # 数据库引擎、会话工厂
│   ├── models/                  # SQLAlchemy 模型
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── book.py
│   │   ├── category.py
│   │   └── borrow.py
│   ├── schemas/                 # Pydantic 模型(请求/响应)
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── book.py
│   │   ├── category.py
│   │   └── borrow.py
│   ├── api/                     # API 路由
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py          # 登录、注册
│   │   │   ├── users.py
│   │   │   ├── books.py
│   │   │   ├── categories.py
│   │   │   └── borrows.py
│   ├── crud/                    # CRUD 操作(可选,直接写在路由也可)
│   │   └── ...
│   └── dependencies/            # 依赖项(如获取当前用户)
│       └── auth.py
├── alembic/                      # 数据库迁移脚本
├── alembic.ini
└── requirements.txt

4.3 数据库连接与模型

app/core/database.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base

SQLALCHEMY_DATABASE_URL = "postgresql+asyncpg://cloud_studio:123456@localhost/taskdb"

engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

Base = declarative_base()

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

app/models/user.py(示例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from app.core.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, nullable=False)
    password_hash = Column(String(255), nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    full_name = Column(String(100))
    is_active = Column(Boolean, default=True)
    is_admin = Column(Boolean, default=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now())

类似地定义其他模型,注意外键关系。

4.4 用户认证(JWT)

app/core/security.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

app/dependencies/auth.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.database import get_db
from app.models.user import User
from app import crud

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        user_id: int = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = await crud.get_user_by_id(db, user_id)
    if user is None:
        raise credentials_exception
    return user

async def get_current_active_user(
    current_user: User = Depends(get_current_user)
):
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

async def get_current_admin_user(
    current_user: User = Depends(get_current_active_user)
):
    if not current_user.is_admin:
        raise HTTPException(status_code=403, detail="Not enough permissions")
    return current_user

4.5 路由与业务逻辑

以图书管理为例,在 api/v1/books.py 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from app.core.database import get_db
from app import crud, schemas
from app.dependencies.auth import get_current_active_user, get_current_admin_user
from app.models.user import User

router = APIRouter(prefix="/books", tags=["books"])

@router.get("/", response_model=List[schemas.BookOut])
async def get_books(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100),
    category_id: Optional[int] = None,
    search: Optional[str] = None,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user)  # 需要登录
):
    # 调用 crud 获取图书列表(支持过滤和搜索)
    books = await crud.get_books(db, skip=skip, limit=limit, category_id=category_id, search=search)
    return books

@router.get("/{book_id}", response_model=schemas.BookOut)
async def get_book(
    book_id: int,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_active_user)
):
    book = await crud.get_book_by_id(db, book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    return book

@router.post("/", response_model=schemas.BookOut, status_code=201)
async def create_book(
    book_in: schemas.BookCreate,
    db: AsyncSession = Depends(get_db),
    current_admin: User = Depends(get_current_admin_user)  # 仅管理员
):
    # 检查 ISBN 是否已存在
    existing = await crud.get_book_by_isbn(db, book_in.isbn)
    if existing:
        raise HTTPException(status_code=400, detail="ISBN already exists")
    new_book = await crud.create_book(db, book_in)
    return new_book

@router.put("/{book_id}", response_model=schemas.BookOut)
async def update_book(
    book_id: int,
    book_in: schemas.BookUpdate,
    db: AsyncSession = Depends(get_db),
    current_admin: User = Depends(get_current_admin_user)
):
    book = await crud.get_book_by_id(db, book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    updated = await crud.update_book(db, book, book_in)
    return updated

@router.delete("/{book_id}", status_code=204)
async def delete_book(
    book_id: int,
    db: AsyncSession = Depends(get_db),
    current_admin: User = Depends(get_current_admin_user)
):
    book = await crud.get_book_by_id(db, book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    await crud.delete_book(db, book)

类似地实现其他路由,包括认证、分类、借阅等。借阅逻辑需要事务保证库存更新与记录创建原子性,可使用 SQLAlchemy 的 async with db.begin()

注意:CRUD 操作可以使用通用函数,如 get_book_by_idcreate_book 等,在 app/crud/ 中实现。这里不再赘述。

4.6 运行与测试

app/main.py 中创建 FastAPI 实例并注册路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from fastapi import FastAPI
from app.api.v1 import auth, users, books, categories, borrows

app = FastAPI(title="Library Management System API")

app.include_router(auth.router, prefix="/api/v1")
app.include_router(users.router, prefix="/api/v1")
app.include_router(books.router, prefix="/api/v1")
app.include_router(categories.router, prefix="/api/v1")
app.include_router(borrows.router, prefix="/api/v1")

@app.get("/")
async def root():
    return {"message": "Welcome to Library API"}

运行命令(已在用户笔记中):

1
python3.12 -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

访问 http://localhost:8000/docs 可查看自动生成的 Swagger 文档。


5. 前端开发(Vue3 + TypeScript + TDesign)

5.1 项目初始化与依赖

根据用户笔记,已创建 Vite 6 项目,并安装了 Vue Router、Pinia、axios、TDesign。确保 package.json 包含:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "dependencies": {
    "vue": "^3.5.13",
    "vue-router": "^4.5.1",
    "pinia": "^3.0.3",
    "axios": "^1.7.9",
    "tdesign-vue-next": "^1.10.0"
  },
  "devDependencies": {
    "typescript": "~5.7.2",
    "vite": "^6.0.0",
    "@vitejs/plugin-vue": "^5.2.1"
  }
}

5.2 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
frontend/
├── public/
├── src/
│   ├── assets/
│   ├── components/            # 可复用组件
│   │   ├── Layout/
│   │   │   ├── AppLayout.vue
│   │   │   └── AppHeader.vue
│   │   └── Common/
│   ├── views/                  # 页面视图
│   │   ├── auth/
│   │   │   ├── Login.vue
│   │   │   └── Register.vue
│   │   ├── books/
│   │   │   ├── BookList.vue
│   │   │   ├── BookDetail.vue
│   │   │   ├── BookForm.vue
│   │   │   └── ...
│   │   ├── borrow/
│   │   │   ├── MyBorrows.vue
│   │   │   └── AdminBorrows.vue
│   │   ├── categories/
│   │   │   └── CategoryManager.vue
│   │   └── user/
│   │       ├── Profile.vue
│   │       └── UserManager.vue (admin)
│   ├── router/                 # 路由配置
│   │   └── index.ts
│   ├── stores/                 # Pinia stores
│   │   ├── auth.ts
│   │   ├── book.ts
│   │   └── borrow.ts
│   ├── api/                    # API 请求封装
│   │   ├── http.ts             # axios 实例
│   │   ├── auth.ts
│   │   ├── books.ts
│   │   └── ...
│   ├── types/                  # TypeScript 类型定义
│   │   ├── user.ts
│   │   ├── book.ts
│   │   └── ...
│   ├── utils/                  # 工具函数
│   │   └── date.ts
│   ├── App.vue
│   └── main.ts
├── index.html
├── vite.config.ts
├── tsconfig.json
└── package.json

5.3 路由与布局

router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { createRouter, createWebHistory } from "vue-router";
import { useAuthStore } from "@/stores/auth";

const routes = [
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/auth/Login.vue"),
    meta: { requiresGuest: true },
  },
  {
    path: "/register",
    name: "Register",
    component: () => import("@/views/auth/Register.vue"),
    meta: { requiresGuest: true },
  },
  {
    path: "/",
    component: () => import("@/components/Layout/AppLayout.vue"),
    meta: { requiresAuth: true },
    children: [
      { path: "", redirect: { name: "Books" } },
      {
        path: "books",
        name: "Books",
        component: () => import("@/views/books/BookList.vue"),
      },
      {
        path: "books/:id",
        name: "BookDetail",
        component: () => import("@/views/books/BookDetail.vue"),
      },
      {
        path: "books/:id/edit",
        name: "BookEdit",
        component: () => import("@/views/books/BookForm.vue"),
        meta: { requiresAdmin: true },
      },
      {
        path: "books/new",
        name: "BookNew",
        component: () => import("@/views/books/BookForm.vue"),
        meta: { requiresAdmin: true },
      },
      {
        path: "my-borrows",
        name: "MyBorrows",
        component: () => import("@/views/borrow/MyBorrows.vue"),
      },
      {
        path: "admin/borrows",
        name: "AdminBorrows",
        component: () => import("@/views/borrow/AdminBorrows.vue"),
        meta: { requiresAdmin: true },
      },
      {
        path: "admin/users",
        name: "UserManager",
        component: () => import("@/views/user/UserManager.vue"),
        meta: { requiresAdmin: true },
      },
      {
        path: "admin/categories",
        name: "CategoryManager",
        component: () => import("@/views/categories/CategoryManager.vue"),
        meta: { requiresAdmin: true },
      },
      {
        path: "profile",
        name: "Profile",
        component: () => import("@/views/user/Profile.vue"),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore();
  const isAuthenticated = authStore.isAuthenticated;
  const isAdmin = authStore.user?.isAdmin;

  if (to.meta.requiresAuth && !isAuthenticated) {
    next("/login");
  } else if (to.meta.requiresGuest && isAuthenticated) {
    next("/");
  } else if (to.meta.requiresAdmin && !isAdmin) {
    next("/"); // 或 403 页面
  } else {
    next();
  }
});

export default router;

AppLayout.vue:使用 TDesign 的布局组件,如 t-layoutt-headert-asidet-content

5.4 状态管理(Pinia)

创建 stores/auth.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import {
  login as apiLogin,
  logout as apiLogout,
  getUserInfo,
} from "@/api/auth";
import type { User, LoginCredentials } from "@/types/user";

export const useAuthStore = defineStore("auth", () => {
  const token = ref<string | null>(localStorage.getItem("token"));
  const user = ref<User | null>(null);

  const isAuthenticated = computed(() => !!token.value);
  const isAdmin = computed(() => user.value?.isAdmin ?? false);

  async function login(credentials: LoginCredentials) {
    const response = await apiLogin(credentials);
    token.value = response.token;
    user.value = response.user;
    localStorage.setItem("token", response.token);
  }

  async function logout() {
    await apiLogout();
    token.value = null;
    user.value = null;
    localStorage.removeItem("token");
  }

  async function fetchUser() {
    if (token.value) {
      const userData = await getUserInfo();
      user.value = userData;
    }
  }

  return { token, user, isAuthenticated, isAdmin, login, logout, fetchUser };
});

其他 store(如 book.ts)用于管理图书列表状态,方便组件间共享。

5.5 API 请求封装

api/http.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import axios from "axios";
import { useAuthStore } from "@/stores/auth";
import router from "@/router";

const http = axios.create({
  baseURL: "http://localhost:8000/api/v1",
  timeout: 10000,
});

http.interceptors.request.use((config) => {
  const authStore = useAuthStore();
  if (authStore.token) {
    config.headers.Authorization = `Bearer ${authStore.token}`;
  }
  return config;
});

http.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      const authStore = useAuthStore();
      authStore.logout();
      router.push("/login");
    }
    return Promise.reject(error);
  }
);

export default http;

然后为每个模块编写 API 函数,如 api/books.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import http from "./http";
import type {
  Book,
  BookCreate,
  BookUpdate,
  PaginatedBooks,
} from "@/types/book";

export const getBooks = (params: {
  skip?: number;
  limit?: number;
  categoryId?: number;
  search?: string;
}) => http.get<PaginatedBooks>("/books", { params });

export const getBook = (id: number) => http.get<Book>(`/books/${id}`);

export const createBook = (data: BookCreate) => http.post<Book>("/books", data);

export const updateBook = (id: number, data: BookUpdate) =>
  http.put<Book>(`/books/${id}`, data);

export const deleteBook = (id: number) => http.delete(`/books/${id}`);

5.6 页面组件实现

以图书列表页 BookList.vue 为例,使用 TDesign 表格组件:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<template>
  <t-card title="图书管理">
    <t-space direction="vertical" size="large" style="width: 100%">
      <t-row justify="space-between">
        <t-col :span="8">
          <t-input
            v-model="searchText"
            placeholder="搜索书名或作者"
            clearable
            @enter="handleSearch"
          >
            <template #suffixIcon><SearchIcon /></template>
          </t-input>
        </t-col>
        <t-col>
          <t-button v-if="isAdmin" theme="primary" @click="goToAdd"
            >新增图书</t-button
          >
        </t-col>
      </t-row>

      <t-table
        :data="books"
        :columns="columns"
        :loading="loading"
        row-key="id"
        :pagination="pagination"
        @page-change="onPageChange"
      >
        <template #cover="{ row }">
          <t-image
            :src="row.coverUrl || defaultCover"
            :style="{ width: '50px', height: '70px' }"
            fit="cover"
          />
        </template>
        <template #category="{ row }">{{ row.category?.name }}</template>
        <template #available="{ row }"
          >{{ row.availableCopies }}/{{ row.totalCopies }}</template
        >
        <template #action="{ row }">
          <t-space>
            <t-link theme="primary" @click="viewDetail(row.id)">详情</t-link>
            <t-link v-if="isAdmin" theme="primary" @click="editBook(row.id)"
              >编辑</t-link
            >
            <t-link v-if="isAdmin" theme="danger" @click="confirmDelete(row)"
              >删除</t-link
            >
          </t-space>
        </template>
      </t-table>
    </t-space>
  </t-card>
</template>

<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import { useBookStore } from "@/stores/book";
import { useAuthStore } from "@/stores/auth";
import { SearchIcon } from "tdesign-icons-vue-next";
import type { Book } from "@/types/book";

const router = useRouter();
const bookStore = useBookStore();
const authStore = useAuthStore();

const isAdmin = computed(() => authStore.isAdmin);
const books = computed(() => bookStore.books);
const loading = ref(false);
const searchText = ref("");
const pagination = ref({
  current: 1,
  pageSize: 10,
  total: 0,
});

const columns = [
  { colKey: "cover", title: "封面", width: 100 },
  { colKey: "title", title: "书名", width: 200 },
  { colKey: "author", title: "作者", width: 150 },
  { colKey: "isbn", title: "ISBN", width: 150 },
  { colKey: "category", title: "分类", width: 100 },
  { colKey: "available", title: "库存", width: 100 },
  { colKey: "action", title: "操作", width: 200 },
];

const defaultCover = "https://via.placeholder.com/50x70?text=No+Cover";

async function fetchBooks() {
  loading.value = true;
  const params = {
    skip: (pagination.value.current - 1) * pagination.value.pageSize,
    limit: pagination.value.pageSize,
    search: searchText.value || undefined,
  };
  const result = await bookStore.fetchBooks(params);
  pagination.value.total = result.total;
  loading.value = false;
}

function handleSearch() {
  pagination.value.current = 1;
  fetchBooks();
}

function onPageChange(pageInfo: any) {
  pagination.value.current = pageInfo.current;
  pagination.value.pageSize = pageInfo.pageSize;
  fetchBooks();
}

function viewDetail(id: number) {
  router.push(`/books/${id}`);
}

function editBook(id: number) {
  router.push(`/books/${id}/edit`);
}

function goToAdd() {
  router.push("/books/new");
}

function confirmDelete(book: Book) {
  // 使用 TDesign 的 Dialog 组件确认
  // 确认后调用 bookStore.deleteBook(book.id) 并刷新列表
}

onMounted(fetchBooks);
</script>

其他页面类似,利用 TDesign 丰富的组件(表单、对话框、消息提示等)可快速构建美观界面。

5.7 运行与调试

vite.config.ts 中配置代理以解决开发时的跨域问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  server: {
    host: true,
    proxy: {
      "/api": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
    },
  },
});

然后运行:

1
npm run dev

访问 http://localhost:5173 即可。


6. 部署与运行指南

后端生产部署

使用 Gunicorn + Uvicorn workers:

1
2
pip install gunicorn
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

或使用 Docker 容器化。

前端生产构建

1
npm run build

dist 目录下的文件部署到 Nginx 等静态服务器,并配置反向代理到后端 API。

环境变量配置

后端敏感信息(数据库密码、JWT 密钥)应通过环境变量或 .env 文件加载,可使用 python-dotenv


7. 扩展功能建议

  • 图书封面上传:集成云存储(如七牛云、阿里云 OSS)或本地存储。
  • 邮件通知:借书到期提醒。
  • 图书预约:当图书被借完时允许预约。
  • 统计分析:借阅排行榜、图书流通统计图表。
  • 微信小程序:基于同一套 API 开发小程序端。

本教程提供了一个完整的图书管理系统前后端实现思路和关键代码片段,你可以根据自己的需求进一步完善。如果遇到问题,欢迎查阅官方文档或社区资源。祝你学习愉快!