1.后端
1.文件

2.配置
# app/core/database.py
"""
数据库配置文件
这个文件定义了 Tortoise-ORM 连接 MySQL 数据库所需的所有配置信息
"""
# from app.config.settings import settings
# TORTOISE_ORM 是 Tortoise-ORM 规定的配置字典变量名
# 后面用 register_tortoise 或 Aerich 时都会引用这个字典
TORTOISE_ORM = {
# 1. 连接配置 —— 定义数据库连接信息
"connections": {
# "default" 是默认连接的名字,必须有一个 default
"default": {
# engine:指定数据库后端引擎,MySQL 使用 tortoise.backends.mysql
"engine": "tortoise.backends.mysql",
# credentials:数据库连接凭证,包含主机、端口、用户名、密码等
"credentials": {
"host": "127.0.0.1", # MySQL 服务器地址
"port": 3306, # MySQL 端口,默认 3306
"user": "root", # 数据库用户名
"password": "123456", # 数据库密码(请根据实际情况修改)
"database": "Fast_api_shx", # 数据库名称
"minsize": 1, # 连接池最小连接数
"maxsize": 5, # 连接池最大连接数
"charset": "utf8mb4", # 字符集,支持 emoji
"echo": True # 是否打印 SQL 语句(开发环境建议开启)
}
}
},
# 2. 应用配置 —— 指定模型所在的模块
"apps": {
# "models" 是应用的名字,可以自定义,但 Aerich 需要使用这个名字
"models": {
# models 列表:指定包含 Tortoise 模型类的 Python 模块路径
# "aerich.models" 是 Aerich 的内置模型,用于记录迁移历史,必须包含
"models": ["models.models", "aerich.models"],
# default_connection:指定这个应用使用哪个数据库连接
"default_connection": "default",
}
},
# 3. 时区配置
"use_tz": False, # 是否使用时区
"timezone": "Asia/Shanghai", # 时区设置
"echo": True # ✅ 关键:打开 SQL 打印
}
我们用来连接数据库的配置
迁移命令
第一次 :aerich init -t config.TORTOISE_ORM ,aerich init-db
后续 :aerich migrate ,aerich upgrade
迁移后就可以调用mysql数据库中的数据了
3.接口
models模型 简单模型创建
from tortoise import fields, models
class Category(models.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=255)
order = fields.IntField()
class Meta:
table = "category"
class Book(models.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=255)
price = fields.DecimalField(max_digits=10, decimal_places=2)
numbers= fields.IntField(default=0)
category = fields.ForeignKeyField("models.Category", related_name="books")
class Meta:
table = "book"
1.查询所有 分页
@book_api.get("/", summary="分页查询图书列表")
async def get_book_list(page: int = 1, page_size: int = 10):
"""
根据页码和每页条数分页查询图书数据
page:当前页码,默认值1
page_size:单页展示数据条数,默认值10
返回内容:当前页码、每页条数、当前页量、总数据量、当前页图书数
"""
book, total, count_total = await BookService.get_book_list(page, page_size)
return {
"page": page,
"page_size": page_size,
"total": total,
"count_total": count_total,
"data": book
}
class BookService:
# 分页查询图书列表
@staticmethod
async def get_book_list(page, page_size):
"""分页查询图书"""
# 计算数据偏移量
offset = (page – 1) * page_size
# 分页查询,同时预加载关联分类信息,避免多次查库
booke = await Book.all().prefetch_related("category").offset(offset).limit(page_size)
# 整理返回前端需要的字段
booke_list = [
{
"id": book.id,
"name": book.name,
"price": book.price,
"numbers": book.numbers,
"category_id": book.category.id,
"category": book.category.name
} for book in booke
]
total = len(booke_list)
# 查询数据库全部图书总数量
count_total = await Book.all().count()
return booke_list, total, count_total
从前端接受要分页的page和page_size 如果没有就是默认第一页的10条数据如果有传到service
offset = (page – 1) * page_size 计算偏移量 然后分页查询
定义book_list将数据加入 然后下面是分类的链接表 返回查询列表 总数量 当前查询数量
2.查询指定数据
@book_api.get("/{book_id}", summary="根据ID查询图书详情")
async def get_book_detail(book_id: int):
"""
依据图书id查询单本图书完整信息
book_id:图书主键id,路径参数
返回:包含状态、提示、图书详情
"""
return await BookService.get_detail_by_id(book_id)
@staticmethod
async def get_detail_by_id(book_id: int):
"""根据ID查询图书详情"""
try:
# 根据id查图书,顺带查出分类名称
book = await Book.get(id=book_id).prefetch_related("category")
return {
"code": 200,
"msg": "查询成功",
"data": {
"id": book.id,
"name": book.name,
"price": book.price,
"numbers": book.numbers,
"category_id": book.category.id,
"category": book.category.name
}
}
except DoesNotExist:
# 不存在
return {"code": 400, "msg": "图书不存在", "data": None}
从前端返回要查询的book_id 然后传到接口 进行查询 如果book_id存在就返回信息 如果不再就返回不存在的提示信息就可以了
3.添加数据
@book_api.post("/", summary="新增图书")
async def add_book(form: BookCreateSchema):
"""
新增一本图书,请求体BookCreateSchema做字段校验,所有字段必须全部传入
form:前端提交的图书表单信息
返回:新增成功/失败的状态提示
"""
return await BookService.create_book(form.model_dump())
@staticmethod
async def create_book(info: dict):
"""新增图书入库"""
await Book.create(**info)
return {"code": 200, "msg": "新增成功"}
请求体
from pydantic import BaseModel
class BookCreateSchema(BaseModel):
name: str
price: float
numbers: int
category_id: int
class BookUpdateSchema(BaseModel):
name: str | None = None
price: float | None = None
numbers: int | None = None
category_id: int | None = None
将前端返回添加的信息 直接添加到数据库中
4.修改数据
@book_api.put("/{book_id}", summary="编辑修改图书")
async def edit_book(book_id: int, form: BookUpdateSchema):
"""
根据图书id修改图书信息,支持局部更新,不传的字段不会改动
book_id:需要修改的图书id,路径参数
form:前端提交的修改字段,字段全部可选
返回:修改成功或失败提示
"""
return await BookService.update_book(book_id, form.model_dump())
@staticmethod
async def update_book(book_id: int, info: dict):
"""修改图书,自动过滤None空字段,只更新传入的参数"""
update_info = {k: v for k, v in info.items() if v is not None}
update_count = await Book.filter(id=book_id).update(**update_info)
if update_count == 0:
return {"code": 400, "msg": "图书不存在,修改失败"}
return {"code": 200, "msg": "修改成功"}
从前端返回两个值 一个是要修改的id另外一个是修改的数据 在请求体中 都可以为空None 先判断修改的数据是否存在 在判断我们要修改的数据都有哪些然后1进行update修改
5.删除数据
@book_api.delete("/{book_id}", summary="删除图书")
async def delete_book(book_id: int):
"""
根据图书id删除对应图书数据
book_id:待删除图书主键id
返回:删除成功/失败提示
"""
return await BookService.delete_book(book_id)
@staticmethod
async def delete_book(book_id: int):
"""根据ID删除图书"""
del_count = await Book.filter(id=book_id).delete()
if del_count == 0:
return {"code": 400, "msg": "图书不存在,删除失败"}
return {"code": 200, "msg": "删除成功"}
传要删除的数据id判断是否存在然后删除
2.前端

vite.config.js进行配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
}
}
}
})
然后是app页面
<template>
<div style="padding: 24px">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px">
<h2>图书列表</h2>
<el-button type="primary" @click="openAdd">新增图书</el-button>
</div>
<el-table :data="tableData" border style="width: 100%; margin: 20px 0">
<el-table-column fixed prop="id" label="图书ID" width="100" />
<el-table-column prop="name" label="书名" width="220" />
<el-table-column prop="price" label="价格" width="120" />
<el-table-column prop="numbers" label="库存数量" width="120" />
<el-table-column prop="category_id" label="分类ID" width="120" />
<el-table-column prop="category" label="分类名称" width="180" />
<el-table-column fixed="right" label="操作" min-width="200">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end">
<el-pagination
v-model:current-page="query.page"
v-model:page-size="query.page_size"
:page-sizes="[5,10,20,50]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="totalNum"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!– 新增、编辑共用弹窗 –>
<el-dialog v-model="dialogVisible" title="图书表单" width="520px">
<el-form label-width="90px">
<el-form-item label="书名">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="价格">
<el-input v-model.number="form.price"></el-input>
</el-form-item>
<el-form-item label="库存数量">
<el-input v-model.number="form.numbers"></el-input>
</el-form-item>
<!– 分类下拉选择 –>
<el-form-item label="分类">
<el-select v-model="form.category_id" placeholder="请选择分类" style="width: 100%">
<el-option label="文学小说" value="1" />
<el-option label="计算机编程" value="2" />
<el-option label="历史人文" value="3" />
<el-option label="心理学" value="4" />
<el-option label="美食菜谱" value="5" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定提交</el-button>
</template>
</el-dialog>
<!– 详情弹窗,样式和编辑弹窗完全一致 –>
<el-dialog v-model="detailDialogVisible" title="图书详情" width="520px">
<el-form label-width="90px">
<el-form-item label="书名">
<el-input v-model="detailForm.name" disabled></el-input>
</el-form-item>
<el-form-item label="价格">
<el-input v-model.number="detailForm.price" disabled></el-input>
</el-form-item>
<el-form-item label="库存数量">
<el-input v-model.number="detailForm.numbers" disabled></el-input>
</el-form-item>
<el-form-item label="分类">
<el-input v-model="detailForm.category" disabled></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
const totalNum = ref(0)
const tableData = ref([])
const query = ref({
page: 1,
page_size: 5
})
// 新增编辑弹窗控制
const dialogVisible = ref(false)
// 区分新增/编辑模式 add新增 edit编辑
const operateMode = ref('add')
// 编辑行id
const editBookId = ref(null)
// 新增编辑表单
const form = ref({
name: '',
price: 0,
numbers: 0,
category_id: ''
})
// 详情弹窗控制
const detailDialogVisible = ref(false)
// 详情只读表单
const detailForm = ref({
name: '',
price: 0,
numbers: 0,
category_id: '',
category: ''
})
// 加载图书列表 1.在挂载时加载
// const loadBook = () => {
// axios.get(`/api/book/?page=${query.value.page}&page_size=${query.value.page_size}`)
// .then(res => {
// console.log('后端返回', res.data)
// tableData.value = res.data.data
// totalNum.value = res.data.count_total
// })
// .catch(err => console.error(err))
// }
// 加载图书列表 2.在事件中加载
const loadBook = () => {
axios.get('/api/book/', {params: query.value})
.then(res => {
console.log(res.data)
tableData.value = res.data.data
totalNum.value = res.data.count_total
})
.catch(err => console.error(err))
}
// 每页条数切换
const handleSizeChange = (newSize) => {
query.value.page_size = newSize
query.value.page = 1
loadBook()
}
// 页码切换
const handleCurrentChange = (newPage) => {
query.value.page = newPage
loadBook()
}
// 详情:调用接口打开弹窗,全部.then写法
const handleDetail = (row) => {
console.log('详情数据', row)
axios.get(`/api/book/${row.id}`)
.then(res => {
console.log('详情完整数据', res.data)
// 回填详情表单
detailForm.value = res.data.data
// 打开详情弹窗
detailDialogVisible.value = true
})
.catch(err => {
console.error('详情查询失败', err)
ElMessage.error('详情加载失败')
})
}
// 编辑
const handleEdit = (row) => {
console.log('编辑当前行', row)
// 打开弹窗回填数据
operateMode.value = 'edit'
editBookId.value = row.id
form.value = {
name: row.name,
price: row.price,
numbers: row.numbers,
category_id: row.category_id
}
dialogVisible.value = true
}
// 打开新增弹窗
const openAdd = () => {
operateMode.value = 'add'
editBookId.value = null
// 清空表单
form.value = {
name: '',
price: 0,
numbers: 0,
category_id: ''
}
dialogVisible.value = true
}
// 提交新增/编辑表单
const submitForm = () => {
if (operateMode.value === 'add') {
// 新增
axios.post('/api/book/', form.value)
.then(() => {
ElMessage.success('新增成功')
dialogVisible.value = false
loadBook()
})
.catch(err => {
ElMessage.error('新增失败')
console.error(err)
})
} else {
// 编辑
axios.put(`/api/book/${editBookId.value}`, form.value)
.then(() => {
ElMessage.success('修改成功')
dialogVisible.value = false
loadBook()
})
.catch(err => {
ElMessage.error('修改失败')
console.error(err)
})
}
}
// 删除按钮事件 全部then写法
const handleDelete = (row) => {
ElMessageBox.confirm('确认删除当前图书?删除不可恢复', '删除提示', { type: 'warning' })
.then(() => {
return axios.delete(`/api/book/${row.id}`)
})
.then(() => {
ElMessage.success('删除完成')
loadBook()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
onMounted(loadBook)
</script>




