11. 带你实现商品收藏功能
SQL
CREATE TABLE `collect` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
`goods_id` int DEFAULT NULL COMMENT '商品ID',
`user_id` int DEFAULT NULL COMMENT '用户ID',
`time` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收藏时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户收藏';
收藏后端逻辑
Collect.java
package com.example.entity;
import java.math.BigDecimal;
public class Collect {
/**ID */
private Integer id;
/**商品ID */
private Integer goodsId;
private String goodsName;
private String goodsImg;
private BigDecimal goodsPrice;
/**用户ID */
private Integer userId;
private String userName;
/**收藏时间 */
private String time;
public String getGoodsImg() {
return goodsImg;
}
public void setGoodsImg(String goodsImg) {
this.goodsImg = goodsImg;
}
public BigDecimal getGoodsPrice() {
return goodsPrice;
}
public void setGoodsPrice(BigDecimal goodsPrice) {
this.goodsPrice = goodsPrice;
}
public String getGoodsName() {
return goodsName;
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getGoodsId() {
return goodsId;
}
public void setGoodsId(Integer goodsId) {
this.goodsId = goodsId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
CollectMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.CollectMapper">
<select id="selectAll" resultType="com.example.entity.Collect">
select collect.*, goods.name as goodsName, goods.img as goodsImg, goods.price as goodsPrice, user.name as userName from `collect`
left join goods on collect.goods_id = goods.id
left join user on collect.user_id = user.id
<where>
<if test="goodsName != null"> and goods.name like concat('%', #{goodsName}, '%')</if>
<if test="userId != null"> and collect.user_id = #{userId}</if>
<if test="goodsId != null"> and collect.goods_id = #{goodsId}</if>
</where>
order by collect.id desc
</select>
<select id="selectById" resultType="com.example.entity.Collect">
select * from `collect` where id = #{id}
</select>
<delete id="deleteById">
delete from `collect` where id = #{id}
</delete>
<insert id="insert" parameterType="com.example.entity.Collect" useGeneratedKeys="true">
insert into `collect`
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="goodsId != null">goods_id,</if>
<if test="userId != null">user_id,</if>
<if test="time != null">time,</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="goodsId != null">#{goodsId},</if>
<if test="userId != null">#{userId},</if>
<if test="time != null">#{time},</if>
</trim>
</insert>
<update id="updateById" parameterType="com.example.entity.Collect">
update `collect`
<set>
<if test="id != null">
id = #{id},
</if>
<if test="goodsId != null">
goods_id = #{goodsId},
</if>
<if test="userId != null">
user_id = #{userId},
</if>
<if test="time != null">
time = #{time},
</if>
</set>
where id = #{id}
</update>
</mapper>
收藏的前端页面
管理页面 Collect.vue
<template>
<div>
<div class="card" style="margin-bottom: 5px;">
<el-input v-model="data.goodsName" 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">
<el-table :data="data.tableData" stripe>
<el-table-column prop="goodsName" label="商品名称"></el-table-column>
<el-table-column prop="userName" label="用户名称"></el-table-column>
<el-table-column prop="time" label="收藏时间"></el-table-column>
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<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>
</div>
</template>
<script setup>
import request from "@/utils/request";
import {reactive, ref} from "vue";
import {ElMessageBox, ElMessage} from "element-plus";
const formRef = ref()
const data = reactive({
pageNum: 1,
pageSize: 10,
total: 0,
formVisible: false,
form: {},
tableData: [],
goodsName: null,
rules: {
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
]
}
})
// 分页查询
const load = () => {
request.get('/collect/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
goodsName: data.goodsName
}
}).then(res => {
data.tableData = res.data?.list
data.total = res.data?.total
})
}
load()
// 新增
const handleAdd = () => {
data.form = {}
data.formVisible = true
}
// 编辑
const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row))
data.formVisible = true
}
// 新增保存
const add = () => {
request.post('/collect/add', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 编辑保存
const update = () => {
request.put('/collect/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('/collect/delete/' + id).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
} else {
ElMessage.error(res.msg)
}
})
}).catch(err => {})
}
// 重置
const reset = () => {
data.goodsName = null
load()
}
</script>
商品详情页面 GoodsDetail.vue
<template>
<div class="front-container" style="width: 50%">
<div class="card" style="padding: 20px; display: flex; grid-gap: 20px; margin-bottom: 10px">
<img :src="data.goods.img" alt="" style="width: 300px; height: 300px">
<div style="flex: 1">
<div style="display: flex; align-items: flex-start; grid-gap: 20px; margin-bottom: 10px">
<div style="font-size: 22px; font-weight: bold; line-height: 25px; flex: 1">
<el-tag style="margin-right: 5px; float: left; background-color: red; color: white" type="danger" v-if="data.goods.recommend === '是'">推荐</el-tag>
{{ data.goods.name }}
</div>
<div style="width: 60px; cursor: pointer; color: #666" @click="addCollect" v-if="!data.userCollect?.id">
<el-icon style="position: relative; top: 3px" size="18"><Star /></el-icon>收藏
</div>
<div style="width: 100px; cursor: pointer; color: orange" @click="removeCollect" v-if="data.userCollect?.id">
<el-icon style="position: relative; top: 3px" size="18"><StarFilled /></el-icon>取消收藏
</div>
</div>
<div style="margin-bottom: 20px">
<span style="color: red; font-size: 18px">¥</span><b style="color: red; font-size: 30px">{{ data.goods.price }}</b>
<span style="color: #666; margin-left: 20px">累计销量 {{ data.goods.saleCount }}</span>
<span style="color: #666; margin-left: 20px">剩余库存 {{ data.goods.store }}</span>
</div>
<div style="margin-bottom: 20px; padding: 10px; border-radius: 5px; background-color: #e8e4e4; line-height: 25px; text-align: justify">{{ data.goods.description }}</div>
<div>
<el-input-number style="width: 150px; height: 40px" :min="1" v-model="data.num"></el-input-number>
<el-button style="height: 40px; margin-left: 5px" type="danger">加入购物车</el-button>
<el-button style="height: 40px; margin-left: 5px" type="danger">立即购买</el-button>
</div>
<div style="margin-top: 10px; color: #666">校园小卖部销售并发货的商品,由小卖部提供发票和相应的售后服务。请您放心购买!</div>
</div>
</div>
<div class="card" style="padding: 20px; margin-bottom: 50px">
<div style="font-size: 20px; padding-bottom: 10px; border-bottom: 1px solid #ddd">
<span @click="changeTab('商品详情')" style="cursor: pointer" :class="{'current-active': data.current === '商品详情' }">商品详情</span>
<span @click="changeTab('商品评论')" :class="{'current-active': data.current === '商品评论' }" style="cursor: pointer; margin-left: 20px">商品评论</span>
</div>
<div v-if="data.current === '商品详情'" style="padding: 10px" v-html="data.goods.content"></div>
<div v-if="data.current === '商品评论'" style="min-height: 700px">
<div v-if="data.commentList.length === 0" style="padding: 50px; text-align: center; color: #666">暂无评论...</div>
<div v-if="data.commentList.length > 0" style="padding: 20px; text-align: center">
<!-- 显示评论列表-->
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from "vue";
import router from "@/router";
import request from "@/utils/request";
import {ElMessage} from "element-plus";
const data = reactive({
user: JSON.parse(localStorage.getItem('system-user') || '{}'),
id: router.currentRoute.value.query.id,
goods: {},
num: 1,
current: '商品详情',
commentList: [],
userCollect: {}
})
// 当前的商品是否被当前登录的用户收藏过
const loadCollect = () => {
request.get('/collect/selectAll', {
params:{
goodsId: data.id,
userId: data.user.id
}
}).then(res => {
if (res.data?.length > 0) { // 查询到数据了 表示用户收藏过了
data.userCollect = res.data[0]
} else {
data.userCollect = {}
}
})
}
loadCollect()
// 取消收藏
const removeCollect = () => {
request.delete('/collect/delete/' + data.userCollect.id).then(res => {
if (res.code === '200') {
ElMessage.success('操作成功')
loadCollect()
} else {
ElMessage.error(res.msg)
}
})
}
const addCollect = () => {
request.post('/collect/add', { goodsId: data.id, userId: data.user.id }).then(res => {
if (res.code === '200') {
ElMessage.success('操作成功')
loadCollect()
} else {
ElMessage.error(res.msg)
}
})
}
const changeTab = (tabName) => {
data.current = tabName
}
const load = () => {
request.get('/goods/selectById/' + data.id).then(res => {
data.goods = res.data
})
}
load()
</script>
<style>
.current-active {
color: red;
border-bottom: 2px solid red;
padding-bottom: 10px
}
</style>
用户个人收藏页面 UserCollect.vue
<template>
<div class="front-container">
<div style="font-size: 20px; font-weight: bold; margin-bottom: 20px">我收藏的商品({{ data.total }})</div>
<el-row :gutter="20">
<el-col :span="6" v-for="item in data.tableData" :key="item.id">
<div @click="router.push('/front/goodsDetail?id=' + item.goodsId)" class="card"
style="cursor: pointer; width: 100%; padding: 0; border-radius: 5px; margin-bottom: 20px">
<img :src="item.goodsImg" alt="" style="width: 100%; height: 260px; border-radius: 5px 5px 0 0">
<div style="padding: 5px">
<div class="line1" style="font-size: 18px; margin-bottom: 10px">{{ item.goodsName }}</div>
<div style="display: flex; align-items: center">
<div style="flex: 1; color: red">¥<b style="font-size: 20px">{{ item.goodsPrice }}</b></div>
<el-button type="danger" @click.stop="cancel(item.id)">取消收藏</el-button>
</div>
</div>
</div>
</el-col>
</el-row>
<div v-if="data.total > 0">
<el-pagination style="background-color: white; width: fit-content; padding: 5px 10px; border-radius: 5px" @current-change="load" layout="total, prev, pager, next" v-model: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";
import router from "@/router";
import {ElMessage} from "element-plus";
const data = reactive({
user: JSON.parse(localStorage.getItem('system-user') || '{}'),
pageNum: 1,
pageSize: 10,
total: 0,
tableData: [],
})
// 分页查询
const load = () => {
request.get('/collect/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
userId: data.user.id
}
}).then(res => {
data.tableData = res.data?.list
data.total = res.data?.total
})
}
load()
// 取消收藏
const cancel = (collectId) => {
request.delete('/collect/delete/' + collectId).then(res => {
if (res.code === '200') {
ElMessage.success('操作成功')
load()
} else {
ElMessage.error(res.msg)
}
})
}
</script>
页面自动滑动到顶部
router/index.js 设置跳转配置
router.beforeEach(() => {
window.scroll({ top: 0, behavior: "smooth" })
})