feat(留言板): 实现留言点赞功能并优化留言显示

- 新增留言点赞API接口及前端处理逻辑
- 优化留言时间显示格式,使用统一格式化函数
- 修复留言列表props传递问题,支持外部传入articleid
- 移除无用图标和冗余代码,清理样式
This commit is contained in:
qingfeng1121
2025-10-23 18:18:40 +08:00
parent 5b3fba7bfb
commit 6c4d14d06a
5 changed files with 101 additions and 15 deletions

View File

@@ -31,7 +31,7 @@
<!-- 搜索功能 -->
<div class="search-wrapper">
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
<i class="el-icon-search">1</i>
</button>
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
<el-input

View File

@@ -7,7 +7,7 @@ import apiService from './apiService'
class CategoryAttributeService {
/**
* 根据ID获取分类属性
* @param {number} id - 属性ID
* @param {number} id - 属性ID
* @returns {Promise}
*/
getAttributeById(id) {

View File

@@ -45,7 +45,7 @@ class MessageService {
* @returns {Promise}
*/
getRepliesByParentId(parentId) {
return apiService.get(`/messages/parent/${parentId}`)
return apiService.get(`/messages/${parentId}/replies`)
}
/**
@@ -63,7 +63,7 @@ class MessageService {
* @returns {Promise}
*/
getMessageCountByArticleId(articleId) {
return apiService.get(`/messages/count/${articleId}`)
return apiService.get(`/messages/count/article/${articleId}`)
}
/**
@@ -83,6 +83,15 @@ class MessageService {
deleteMessage(id) {
return apiService.delete(`/messages/${id}`)
}
/**
* 点赞留言
* @param {number} id - 留言ID
* @returns {Promise}
*/
likeMessage(id) {
return apiService.post(`/messages/${id}/like`)
}
}
// 导出留言服务实例

View File

@@ -49,7 +49,7 @@
<!-- 文章操作 -->
<div class="article-actions">
<el-button type="primary" icon="el-icon-arrow-left" @click="goBack" plain>
<el-button type="primary" @click="goBack" plain>
返回
</el-button>
</div>
@@ -74,7 +74,7 @@
<!-- 评论区 -->
<div>
<messageboard class="message-board" v-if="article && Object.keys(article).length > 0" v-model:comments="article.articleid" />
<messageboard class="message-board" v-if="article && Object.keys(article).length > 0" />
</div>
</div>
@@ -84,6 +84,8 @@
import { useRoute, useRouter } from 'vue-router'
import { ref, onMounted } from 'vue'
import { articleService } from '@/services'
import { messageService } from '@/services'
import {categoryAttributeService} from '@/services'
import { ElMessage } from 'element-plus'
import type { Article } from '@/types'
import { formatDate } from '@/utils/dateUtils'
@@ -118,7 +120,7 @@ const fetchArticleDetail = async () => {
if (res.data) {
article.value = res.data
article.value.categoryName = await categoryAttributeService.getAttributeById(article.value.categoryId)|| '未分类'
// 增加文章浏览量
try {
await articleService.incrementArticleViews(Number(articleId))

View File

@@ -17,12 +17,15 @@
<img :src="getAvatar()" class="avatar">
<div class="user-info">
<div class="username">{{ comment.displayName || comment.nickname }}</div>
<div class="time">{{ comment.createdAt || '刚刚' }}</div>
<div class="time">{{ formatDate(comment.createdAt) || '刚刚' }}</div>
</div>
</div>
<div class="comment-content" v-html="comment.content"></div>
<div class="comment-actions">
<!-- <span v-if="comment.likes" class="likes">{{ comment.likes }} </span> -->
<span class="likes-btn" @click="handleLike(comment)">
<span v-if="comment.likes && comment.likes > 0" class="likes-count">{{ comment.likes }}</span>
👍
</span>
<span class="reply-btn" @click="handleReply(null, comment)">回复</span>
</div>
<!-- 回复列表 -->
@@ -32,11 +35,15 @@
<img :src="getAvatar()" class="avatar">
<div class="user-info">
<div class="username">{{ reply.displayName || reply.nickname }}</div>
<div class="time">{{ reply.createdAt || '刚刚' }}</div>
<div class="time">{{ formatDate(reply.createdAt) || '刚刚' }}</div>
</div>
</div>
<div class="reply-content">{{ reply.content }}</div>
<div class="reply-actions">
<span class="likes-btn" @click="handleLike(reply)">
<span v-if="reply.likes && reply.likes > 0" class="likes-count">{{ reply.likes }}</span>
👍
</span>
<span class="reply-btn" @click="handleReply(comment, reply)">回复</span>
</div>
</div>
@@ -106,10 +113,19 @@
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { reactive, ref, onMounted, watch } from 'vue'
import { messageService } from '@/services'
import { ElMessage, ElForm } from 'element-plus'
import { useGlobalStore } from '@/store/globalStore'
import { formatDate } from '@/utils/dateUtils'
// 定义组件属性
const props = defineProps({
comments: {
type: Number,
default: null
}
})
const globalStore = useGlobalStore()
const messageBoardData = ref([]) // 留言板留言articleid为空的主留言及其回复
const loading = ref(false)
@@ -213,7 +229,7 @@ const rules = {
// 生成头像URL
const getAvatar = (email) => {
if (!email) return 'https://www.gravatar.com/avatar?d=mp&s=40'
return `https://www.gravatar.com/avatar/${email}?d=mp&s=40`
return `https://www.gravatar.com/avatar/${email}?d=mp&s=40`
}
// 从后端获取留言列表
@@ -222,9 +238,15 @@ const fetchMessages = async () => {
loading.value = true
let res = null
// 安全获取文章ID如果globalStore中没有articlebutn则返回null
const articleData = globalStore.getValue('articlebutn')
const articleid = (articleData && typeof articleData === 'object' && 'id' in articleData) ? articleData.id : null
// 优先使用props传递的articleid其次使用globalStore中的数据
let articleid = props.comments || null
if (!articleid) {
// 安全获取文章ID如果globalStore中没有articlebutn则返回null
const articleData = globalStore.getValue('articlebutn')
articleid = (articleData && typeof articleData === 'object' && 'id' in articleData) ? articleData.id : null
}
form.articleid = articleid
// 根据是否有文章ID选择不同的API调用
@@ -327,6 +349,13 @@ const cancelReply = () => {
form.content = ''
}
// 监听articleid变化重新加载留言
watch(() => props.comments, (newVal) => {
if (newVal) {
fetchMessages()
}
}, { immediate: false })
// 组件挂载时获取留言列表
onMounted(() => {
fetchMessages()
@@ -410,6 +439,29 @@ const post_comment_reply_cancel = () => {
replyingTo.value = { id: null, nickname: '', content: '' }
}
// 处理点赞
const handleLike = async (msg) => {
try {
// 显示加载状态或禁用按钮
msg.isLiking = true
const res = await messageService.likeMessage(msg.messageid)
if (res.success && res.data) {
// 更新点赞数
msg.likes = res.data.likes || 0
ElMessage.success('点赞成功')
} else {
ElMessage.error('点赞失败:' + (res.message || '未知错误'))
}
} catch (error) {
console.error('点赞失败:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
msg.isLiking = false
}
}
</script>
<style scoped>
@@ -508,6 +560,29 @@ const post_comment_reply_cancel = () => {
text-decoration: underline;
}
.likes-btn {
margin-right: 15px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
}
.likes-btn:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
.likes-btn:active {
transform: scale(0.95);
}
.likes-count {
margin-right: 4px;
font-weight: 600;
}
.replies {
margin-top: 15px;
padding-left: 20px;