13. 开发论坛帖子点赞和评论功能


SQL
CREATE TABLE `likes` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` int DEFAULT NULL COMMENT '用户ID',
`article_id` int DEFAULT NULL COMMENT '帖子ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='点赞信息';
开发后端接口
Likes
package com.example.entity;
public class Likes {
private Integer id;
private Integer userId;
private Integer articleId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getArticleId() {
return articleId;
}
public void setArticleId(Integer articleId) {
this.articleId = articleId;
}
}
LikesMapper
<?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.LikesMapper">
<select id="selectAll" resultType="com.example.entity.Likes">
select * from `likes`
<where>
<if test="userId != null"> and likes.user_id = #{userId}</if>
<if test="articleId != null"> and likes.article_id = #{articleId}</if>
</where>
order by id desc
</select>
<insert id="insert" parameterType="com.example.entity.Likes" useGeneratedKeys="true">
insert into `likes`
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="userId != null">user_id,</if>
<if test="articleId != null">article_id,</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="userId != null">#{userId},</if>
<if test="articleId != null">#{articleId},</if>
</trim>
</insert>
<update id="updateById" parameterType="com.example.entity.Likes">
update `likes`
<set>
<if test="id != null">
id = #{id},
</if>
<if test="userId != null">
user_id = #{userId},
</if>
<if test="articleId != null">
article_id = #{articleId},
</if>
</set>
where id = #{id}
</update>
</mapper>
详情页面
<template>
<div class="card" style="width: 60%; margin: 0 auto; padding: 50px; position: relative">
<el-button @click="router.back()" style="position: absolute; top: 10px; left: 10px">返回论坛列表</el-button>
<div style="position: absolute; top: 10px; right: 10px; width: fit-content; cursor: pointer">
<img src="@/assets/imgs/赞-1.png" alt="" style="width: 30px" v-if="!data.likes?.id" @click="addLike">
<img src="@/assets/imgs/赞.png" alt="" style="width: 30px" v-if="data.likes?.id" @click="removeLike">
</div>
<div style="font-size: 28px; font-weight: bold; text-align: center; margin-bottom: 15px">{{ data.article.title }}</div>
<div style="font-size: 14px; color: #666; text-align: center; margin-bottom: 30px">发布人:{{ data.article.userName }} <span style="margin-left: 20px">发布时间:{{ data.article.time }}</span></div>
<div v-html="data.article.content"></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') || '{}'),
id: router.currentRoute.value.query.id,
article: {},
likes: {}
})
request.get('/article/selectById/' + data.id).then(res => {
data.article = res.data
})
const loadLikes = () => {
request.get('/likes/selectAll', {
params: {
articleId: data.id,
userId: data.user.id
}
}).then(res => {
data.likes = res.data.length ? res.data[0] : {}
})
}
loadLikes()
const addLike = () => {
request.post('/likes/add', { userId : data.user.id, articleId: data.id }).then(res => {
ElMessage.success('操作成功')
loadLikes()
})
}
</script>
评论表
CREATE TABLE `comments` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` int DEFAULT NULL COMMENT '用户ID',
`article_id` int DEFAULT NULL COMMENT '帖子ID',
`content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '内容',
`time` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论信息';
后端接口
Comments
package com.example.entity;
public class Comments {
/**ID */
private Integer id;
/**用户ID */
private Integer userId;
private String userName;
/**帖子ID */
private Integer articleId;
private String articleTitle;
private String avatar;
/**内容 */
private String content;
private String time;
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getArticleTitle() {
return articleTitle;
}
public void setArticleTitle(String articleTitle) {
this.articleTitle = articleTitle;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getArticleId() {
return articleId;
}
public void setArticleId(Integer articleId) {
this.articleId = articleId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
CommentsMapper.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.CommentsMapper">
<select id="selectAll" resultType="com.example.entity.Comments">
select comments.*, user.name as userName, user.avatar, article.title as articleTitle from `comments`
left join user on comments.user_id = user.id
left join article on comments.article_id = article.id
<where>
<if test="userId != null"> and comments.user_id = #{userId}</if>
<if test="articleId != null"> and comments.article_id = #{articleId}</if>
</where>
order by comments.id desc
</select>
<insert id="insert" parameterType="com.example.entity.Comments" useGeneratedKeys="true">
insert into `comments`
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="userId != null">user_id,</if>
<if test="articleId != null">article_id,</if>
<if test="content != null">content,</if>
<if test="time != null">time,</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="userId != null">#{userId},</if>
<if test="articleId != null">#{articleId},</if>
<if test="content != null">#{content},</if>
<if test="time != null">#{time},</if>
</trim>
</insert>
<update id="updateById" parameterType="com.example.entity.Comments">
update `comments`
<set>
<if test="id != null">
id = #{id},
</if>
<if test="userId != null">
user_id = #{userId},
</if>
<if test="articleId != null">
article_id = #{articleId},
</if>
<if test="content != null">
content = #{content},
</if>
<if test="time != null">
time = #{time},
</if>
</set>
where id = #{id}
</update>
</mapper>
管理页面
<template>
<div>
<div class="card" style="margin-bottom: 5px;">
<el-input v-model="data.articleTitle" 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="articleTitle" label="帖子标题"></el-table-column>
<el-table-column prop="content" label="内容" show-overflow-tooltip></el-table-column>
<el-table-column prop="userName" label="发布人"></el-table-column>
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button type="primary" @click="handleEdit(scope.row)" v-if="data.user.role === '普通用户'">编辑</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>
</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({
user: JSON.parse(localStorage.getItem('system-user') || '{}'),
pageNum: 1,
pageSize: 10,
total: 0,
formVisible: false,
form: {},
tableData: [],
articleTitle: null,
})
// 分页查询
const load = () => {
request.get('/comments/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
articleTitle: data.articleTitle,
userId: data.user.role === '管理员' ? null : data.user.id,
}
}).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('/comments/add', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 编辑保存
const update = () => {
request.put('/comments/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('/comments/delete/' + id).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
} else {
ElMessage.error(res.msg)
}
})
}).catch(err => {})
}
// 重置
const reset = () => {
data.articleTitle = null
load()
}
load()
</script>
详情页
<template>
<div style="width: 60%; margin: 0 auto;">
<div class="card" style="padding: 50px; position: relative; margin-bottom: 20px">
<el-button @click="router.back()" style="position: absolute; top: 10px; left: 10px">返回论坛列表</el-button>
<div style="position: absolute; top: 10px; right: 10px; width: fit-content; cursor: pointer">
<img src="@/assets/imgs/赞-1.png" alt="" style="width: 30px" v-if="!data.likes?.id" @click="addLike">
<img src="@/assets/imgs/赞.png" alt="" style="width: 30px" v-if="data.likes?.id" @click="removeLike">
</div>
<div style="font-size: 28px; font-weight: bold; text-align: center; margin-bottom: 15px">{{ data.article.title }}</div>
<div style="font-size: 14px; color: #666; text-align: center; margin-bottom: 30px">发布人:{{ data.article.userName }} <span style="margin-left: 20px">发布时间:{{ data.article.time }}</span></div>
<div v-html="data.article.content"></div>
</div>
<div class="card" style="padding: 20px">
<div style="font-size: 20px; margin-bottom: 15px; font-weight: bold">评论列表({{ data.total }})</div>
<div style="margin-bottom: 20px">
<el-input type="textarea" :maxlength="200" v-model="data.content" placeholder="请输入评论内容,限200字" :rows="3"></el-input>
<div style="text-align: right">
<el-button style="margin-top: 5px;" type="primary" @click="addComment">发布评论</el-button>
</div>
</div>
<div v-for="item in data.commentList" :key="item.id" style="display: flex; gap: 10px; margin-bottom: 20px">
<img :src="item.avatar" alt="" style="width: 50px; height: 50px; border-radius: 50%">
<div style="flex: 1; padding-bottom: 10px; border-bottom: 1px solid #ddd; ">
<div style="margin-bottom: 10px">{{ item.userName }} <span style="margin-left: 10px; color: #666; font-size: 13px">{{ item.time }}</span></div>
<div style="color: #666">{{ item.content }}</div>
</div>
</div>
<div style="margin-top: 20px">
<el-pagination @current-change="loadComment" layout="total, prev, pager, next" v-model:page-size="data.pageSize" v-model:current-page="data.pageNum" :total="data.total"/>
</div>
</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') || '{}'),
id: router.currentRoute.value.query.id,
article: {},
likes: {},
pageNum: 1,
pageSize: 5,
total: 0,
content: null,
commentList: []
})
request.get('/article/selectById/' + data.id).then(res => {
data.article = res.data
})
const loadLikes = () => {
request.get('/likes/selectAll', {
params: {
articleId: data.id,
userId: data.user.id
}
}).then(res => {
data.likes = res.data.length ? res.data[0] : {}
})
}
loadLikes()
const addLike = () => {
request.post('/likes/add', { userId : data.user.id, articleId: data.id }).then(res => {
ElMessage.success('操作成功')
loadLikes()
})
}
// 分页查询
const loadComment = () => {
request.get('/comments/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
articleId: data.id
}
}).then(res => {
data.commentList = res.data?.list
data.total = res.data?.total
})
}
loadComment()
const addComment = () => {
request.post('/comments/add', { userId : data.user.id, articleId: data.id, content: data.content}).then(res => {
ElMessage.success('评论成功')
data.content = null // 清空旧评论
loadComment()
})
}
</script>