05. 使用AI帮助开发图书列表页面和详情展示页面
UserBooks.vue
<template>
<div>
<div style="margin-bottom: 30px; display: flex; align-items: center">
<div style="flex: 1">
<div style="display: flex; align-items: center">
<div @click="changeCategory(null)" style="padding-bottom: 5px; margin-right: 20px; cursor: pointer" :class="{'category-active' : data.activeCategoryId === null }">全部图书</div>
<div @click="changeCategory(item.id)" style="padding-bottom: 5px; margin-right: 20px; cursor: pointer" :class="{'category-active' : data.activeCategoryId === item.id }" v-for="item in data.categoryList" :key="item.id">{{ item.name }}</div>
</div>
</div>
<div>
<el-input clearable @clear="load" style="width: 300px; height: 40px" v-model="data.name" placeholder="请输入图书名称、作者或ISBN搜索"></el-input>
<el-button type="primary" style="height: 40px; margin-left: 10px" @click="load">搜 索</el-button>
</div>
</div>
<el-row :gutter="20">
<el-col :span="6" style="margin-bottom: 20px" v-for="item in data.tableData" :key="item.id">
<div class="card item" style="padding: 0; cursor: pointer" @click="router.push('/manager/bookDetail?id=' + item.id)">
<img style="width: 100%; height: 450px; border-radius: 5px 5px 0 0;" :src="item.img" alt="">
<div style="padding: 10px">
<div style="font-size: 18px; margin-bottom: 10px; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ item.name }}</div>
<div style="color: #666; font-size: 14px; margin-bottom: 10px">作者:{{ item.author }}</div>
<div style="display: flex; align-items: center">
<div style="flex: 1">
<div style="color: #f56c6c; font-size: 20px; font-weight: bold">¥{{ item.price }}</div>
</div>
<div style="color: #999; font-size: 13px">{{ item.publisher }}</div>
</div>
</div>
</div>
</el-col>
</el-row>
<div v-if="data.total" style="margin-top: 20px; margin-bottom: 50px">
<el-pagination @current-change="load" layout="total, prev, pager, next" :page-size="data.pageSize" v-model:current-page="data.pageNum" :total="data.total"/>
</div>
</div>
</template>
<script setup>
import { reactive } from "vue";
import request from "@/utils/request.js";
import router from "@/router/index.js";
import { ElMessage } from "element-plus";
const data = reactive({
user: JSON.parse(localStorage.getItem('xm-user') || '{}'),
tableData: [],
total: 0,
pageNum: 1, // 当前的页码
pageSize: 8, // 每页的个数
name: null,
activeCategoryId: null,
categoryList: []
})
// 加载分类列表数据
request.get('/category/selectAll').then(res => {
if (res.code === '200') {
data.categoryList = res.data
}
})
// 切换分类
const changeCategory = (categoryId) => {
data.activeCategoryId = categoryId
data.pageNum = 1
load()
}
// 加载图书分页数据
const load = () => {
request.get('/books/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
categoryId: data.activeCategoryId
}
}).then(res => {
if (res.code === '200') {
data.tableData = res.data?.list || []
data.total = res.data?.total
} else {
ElMessage.error(res.msg)
}
})
}
load()
</script>
<style scoped>
.item {
transition: all 0.5s;
background-color: white;
border-radius: 5px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.item:hover {
transform: translateY(-5px);
}
.category-active {
color: #006cff;
border-bottom: 2px solid #006cff;
}
</style>
BookDetail.vue
<template>
<div style="width: 70%; margin: 0 auto; padding: 20px" class="card">
<div style="margin-bottom: 20px">
<h1 style="font-size: 26px; color: #111; margin-bottom: 20px">{{ data.book.name }}</h1>
</div>
<div style="display: flex; gap: 30px; margin-bottom: 40px">
<div style="width: 155px">
<img :src="data.book.img" style="width: 155px; height: 230px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.2)" />
</div>
<div style="flex: 1; font-size: 14px; color: #666; line-height: 24px">
<div style="margin-bottom: 5px"><span style="color: #111">作者:</span> <span style="color: #666">{{ data.book.author }}</span></div>
<div style="margin-bottom: 5px"><span style="color: #111">出版社:</span> {{ data.book.publisher }}</div>
<div style="margin-bottom: 5px"><span style="color: #111">出版年:</span> {{ data.book.year }}</div>
<div style="margin-bottom: 5px"><span style="color: #111">定价:</span> <span style="color: #f56c6c; font-weight: bold">¥{{ data.book.price }}</span></div>
<div style="margin-bottom: 5px"><span style="color: #111">ISBN:</span> {{ data.book.isbn }}</div>
<div style="margin-bottom: 5px"><span style="color: #111">分类:</span> {{ data.book.categoryName }}</div>
<div style="margin-top: 20px">
<el-button type="warning" plain @click="collect">收藏</el-button>
<el-button type="primary" plain @click="handleComment">去评论</el-button>
</div>
</div>
<div style="width: 200px; border-left: 1px solid #eee; padding-left: 20px">
<div style="color: #999; font-size: 12px">图书评分</div>
<div style="display: flex; align-items: center; gap: 10px; margin-top: 5px">
<span style="font-size: 28px; color: #111">9.2</span>
<el-rate v-model="data.rate" disabled />
</div>
<div style="color: #37a; font-size: 12px; margin-top: 10px">12040人评价</div>
</div>
</div>
<div style="margin-top: 40px">
<div style="font-size: 16px; color: #007722; margin-bottom: 10px; font-weight: bold">内容简介 · · · · · ·</div>
<div style="font-size: 14px; color: #111; line-height: 1.6; white-space: pre-wrap">
{{ data.book.description || '暂无简介' }}
</div>
</div>
<div style="margin-top: 40px">
<div style="font-size: 16px; color: #007722; margin-bottom: 10px; font-weight: bold">图书评价 · · · · · ·<span style="color: #37a">(全部12040条)</span></div>
<div style="font-size: 14px; color: #111; line-height: 1.6; white-space: pre-wrap">
暂无评价
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from "vue";
import request from "@/utils/request.js";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
const route = useRoute()
const data = reactive({
id: route.query.id,
book: {},
rate: 4.5
})
// 获取图书详情
const load = () => {
request.get('/books/selectById/' + data.id).then(res => {
if (res.code === '200') {
data.book = res.data || {}
} else {
ElMessage.error(res.msg)
}
})
}
const collect = () => {
ElMessage.success("已加入收藏列表")
}
const handleComment = () => {
ElMessage.info("前往评论...")
}
load()
</script>
<style scoped>
/* 保持你的简洁风格,基本不写 class */
h1 {
word-break: break-all;
}
</style>
根据 分类 id 查询
# 分页查询
@router.get("/selectPage")
async def select_page(name: str = "", categoryId: int = 0, pageNum: int = 1, pageSize: int = 10):
# 同时获取分页数据和总数
query = Books.filter(name__contains=name).prefetch_related("category")
if categoryId > 0:
query = query.filter(category__id=categoryId)
# 获取分页数据
books_list = await query.order_by("-id").offset((pageNum - 1) * pageSize).limit(pageSize)
books_list = [
{
**BooksPydantic.model_validate(books).model_dump(),
"categoryId": books.category.id if books.category else None,
"categoryName": books.category.name if books.category else None,
}
for books in books_list
]
# 计算总数
total = await query.count()
# 封装分页数据
pageinfo = PageInfo(total=total, list=books_list)
return Result.success(pageinfo)
单个查询
# 单个查询
@router.get("/selectById/{books_id}")
async def select_one(books_id: int):
books = await Books.get_or_none(id=books_id).prefetch_related("category")
book_dict = BooksPydantic.model_validate(books).model_dump() # 转换成字典数据
book_dict['categoryName'] = books.category.name if books.category else None
return Result.success(book_dict)