开发流程
数据库
CREATE TABLE `book` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
`img` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图片',
`author` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者',
`price` double DEFAULT NULL COMMENT '价格',
`publisher` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '出版社',
`publishtime` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '出版时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='图书信息';
后端接口
开发 model
class Book(models.Model):
"""图书信息"""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, null=True, blank=True)
img = models.CharField(max_length=255, null=True, blank=True)
author = models.CharField(max_length=255, null=True, blank=True)
price = models.FloatField(max_length=10, null=True, blank=True)
publisher = models.FloatField(max_length=255, null=True, blank=True)
publishtime = models.FloatField(max_length=255, null=True, blank=True)
class Meta:
db_table = 'book'
开发 api,配置 api
from typing import List
from django.core.paginator import Paginator, EmptyPage
from ninja import Router, ModelSchema
from core.api.base import Result
from core.models import Book
# 创建的是路由对象
router = Router()
# 定义前端发送过来的数据结构
class DataIn(ModelSchema):
class Meta:
model = Book
fields = '__all__' # 或者指定字段列表 或者['username', 'name' ]
fields_optional = '__all__' # 把所有选中的字段都变成可选 或者['username', 'name']
# --- API 接口 ---
@router.post("/add", response=Result)
def add(request, data: DataIn):
"""新增数据"""
Book.objects.create(**data.dict(exclude={"id"}))
return Result.success()
@router.put("/update", response=Result)
def add(request, data: DataIn):
"""更新数据"""
update_dict = data.dict(exclude_unset=True)
Book.objects.filter(id=data.id).update(**update_dict)
return Result.success()
@router.delete("/delete/{book_id}", response=Result)
def delete(request, book_id: int):
"""删除管理员"""
Book.objects.filter(id=book_id).delete()
return Result.success()
@router.delete("/deleteBatch", response=Result)
def delete_batch(request, ids: List[int]):
"""批量删除管理员"""
Book.objects.filter(id__in=ids).delete()
return Result.success()
@router.get("/selectAll", response=Result)
def select_all(request, name: str = ""):
"""查询所有数据"""
data = Book.objects.filter(name__contains=name)
return Result.success(data)
@router.get("/selectById/{book_id}", response=Result)
def select_by_id(request, book_id: int):
"""查询所有数据"""
data = Book.objects.filter(id=book_id).first()
return Result.success(data)
@router.get("/selectPage", response=Result)
def select_page(request, pageNum: int = 1, pageSize: int = 10, name: str = ""):
"""查询分页数据"""
# 参数验证和修正
pageNum = max(1, pageNum) # 确保页码至少为1
pageSize = max(1, min(pageSize, 100)) # 限制每页大小 1-100
# 构建查询集 (QuerySet)
query = Book.objects.all().order_by('-id') # 按 ID 倒序
query = query.filter(name__icontains=name)
# 使用 Django 内置分页器
paginator = Paginator(query, pageSize)
# 获取当前页数据 防止页码越界,可以使用 get_page(pageNum)
data_list = []
try:
data_list = paginator.page(pageNum).object_list
except EmptyPage as e:
print('未查询到数据')
# 构造返回结构
res_data = {
"list": data_list, # 列表数据
"total": paginator.count # 总条数
}
return Result.success(res_data)
配置 api init.py
from .book_api import router as book_router
api.add_router("/book", book_router, tags=["图书信息模块"])
前端
开发 vue3 页面 配置路由和菜单
<template>
<div>
<div class="card" style="margin-bottom: 5px;">
<el-input v-model="data.name" style="width: 300px; margin-right: 10px" placeholder="请输入名称查询"></el-input>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="info" style="margin: 0 10px" @click="reset">重置</el-button>
</div>
<div class="card" style="margin-bottom: 5px">
<div style="margin-bottom: 10px">
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<el-table :data="data.tableData" stripe>
<!-- 图书名称列 -->
<el-table-column label="图书名称" prop="name" min-width="180"></el-table-column>
<!-- 图书图片列:简单展示图片,限制宽高 -->
<el-table-column label="图书图片" align="center" width="100">
<template #default="scope">
<el-image
v-if="scope.row.img"
:src="scope.row.img"
fit="cover"
style="width: 60px; height: 80px; border-radius: 4px;"
preview-src-list="[scope.row.img]"
></el-image>
<span v-else>无图片</span>
</template>
</el-table-column>
<!-- 作者列 -->
<el-table-column label="作者" prop="author" align="center" min-width="120"></el-table-column>
<!-- 价格列:格式化保留两位小数 -->
<el-table-column label="价格(元)" prop="price" align="center" width="100">
<template #default="scope">
{{ scope.row.price ? scope.row.price.toFixed(2) : '0.00' }}
</template>
</el-table-column>
<!-- 出版社列 -->
<el-table-column label="出版社" prop="publisher" align="center" min-width="150"></el-table-column>
<!-- 出版时间列 -->
<el-table-column label="出版时间" prop="publishtime" align="center" width="120"></el-table-column>
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="card">
<el-pagination @current-change="load" background layout="total, prev, pager, next" v-model:page-size="data.pageSize" v-model:current-page="data.pageNum" :total="data.total"/>
</div>
<el-dialog title="图书信息" width="40%" v-model="data.formVisible" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="data.form" :rules="data.rules" label-width="100px" style="padding-right: 50px">
<el-form-item label="名称" prop="name">
<el-input v-model="data.form.name" autocomplete="off" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="图片" prop="img">
<el-upload :action="uploadUrl" list-type="picture" :on-success="handleImgSuccess">
<el-button type="primary">上传图片</el-button>
</el-upload>
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input v-model="data.form.author" autocomplete="off" placeholder="请输入作者" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number style="width: 200px" v-model="data.form.price" autocomplete="off" placeholder="请输入价格" />
</el-form-item>
<el-form-item label="出版社" prop="publisher">
<el-input v-model="data.form.publisher" autocomplete="off" placeholder="请输入出版社" />
</el-form-item>
<el-form-item label="出版时间" prop="publishtime">
<el-date-picker value-format="YYYY-MM-DD" format="YYYY-MM-DD" v-model="data.form.publishtime" type="date" placeholder="选择出版时间" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import request from "@/utils/request";
import {reactive, ref} from "vue";
import {ElMessageBox, ElMessage} from "element-plus";
// 文件上传的接口地址
const uploadUrl = import.meta.env.VITE_BASE_URL + '/files/upload'
const formRef = ref()
const data = reactive({
user: JSON.parse(localStorage.getItem('system-user') || '{}'),
pageNum: 1,
pageSize: 10,
total: 0,
formVisible: false,
form: {},
tableData: [],
name: null,
rules: {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入名称', trigger: 'blur' }
],
avatar: [
{ required: true, message: '请上传头像', trigger: 'blur' }
],
}
})
// 分页查询
const load = () => {
request.get('/book/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name
}
}).then(res => {
data.tableData = res.data?.list
data.total = res.data?.total
})
}
// 新增
const handleAdd = () => {
data.form = {}
data.formVisible = true
}
// 编辑
const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row))
data.formVisible = true
}
// 新增保存
const add = () => {
request.post('/book/add', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 编辑保存
const update = () => {
request.put('/book/update', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 弹窗保存
const save = () => {
formRef.value.validate(valid => {
if (valid) {
// data.form有id就是更新,没有就是新增
data.form.id ? update() : add()
}
})
}
// 删除
const handleDelete = (id) => {
ElMessageBox.confirm('删除后数据无法恢复,您确定删除吗?', '删除确认', { type: 'warning' }).then(res => {
request.delete('/book/delete/' + id).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
} else {
ElMessage.error(res.msg)
}
})
}).catch(err => {})
}
// 重置
const reset = () => {
data.name = null
load()
}
// 处理文件上传的钩子
const handleImgSuccess = (res) => {
data.form.img = res.data // res.data就是文件上传返回的文件路径,获取到路径后赋值表单的属性
}
load()
</script>