feat(留言板): 实现留言点赞功能并优化留言显示
- 新增留言点赞API接口及前端处理逻辑 - 优化留言时间显示格式,使用统一格式化函数 - 修复留言列表props传递问题,支持外部传入articleid - 移除无用图标和冗余代码,清理样式
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
<!-- 搜索功能 -->
|
<!-- 搜索功能 -->
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
|
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
|
||||||
<i class="el-icon-search">1</i>
|
|
||||||
</button>
|
</button>
|
||||||
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
|
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
|
||||||
<el-input
|
<el-input
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import apiService from './apiService'
|
|||||||
class CategoryAttributeService {
|
class CategoryAttributeService {
|
||||||
/**
|
/**
|
||||||
* 根据ID获取分类属性
|
* 根据ID获取分类属性
|
||||||
* @param {number} id - 属性ID
|
* @param {number} id - 属性ID
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAttributeById(id) {
|
getAttributeById(id) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class MessageService {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getRepliesByParentId(parentId) {
|
getRepliesByParentId(parentId) {
|
||||||
return apiService.get(`/messages/parent/${parentId}`)
|
return apiService.get(`/messages/${parentId}/replies`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +63,7 @@ class MessageService {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getMessageCountByArticleId(articleId) {
|
getMessageCountByArticleId(articleId) {
|
||||||
return apiService.get(`/messages/count/${articleId}`)
|
return apiService.get(`/messages/count/article/${articleId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +83,15 @@ class MessageService {
|
|||||||
deleteMessage(id) {
|
deleteMessage(id) {
|
||||||
return apiService.delete(`/messages/${id}`)
|
return apiService.delete(`/messages/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点赞留言
|
||||||
|
* @param {number} id - 留言ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
likeMessage(id) {
|
||||||
|
return apiService.post(`/messages/${id}/like`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出留言服务实例
|
// 导出留言服务实例
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
<!-- 文章操作 -->
|
<!-- 文章操作 -->
|
||||||
<div class="article-actions">
|
<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>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
<!-- 评论区 -->
|
<!-- 评论区 -->
|
||||||
<div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -84,6 +84,8 @@
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { articleService } from '@/services'
|
import { articleService } from '@/services'
|
||||||
|
import { messageService } from '@/services'
|
||||||
|
import {categoryAttributeService} from '@/services'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { Article } from '@/types'
|
import type { Article } from '@/types'
|
||||||
import { formatDate } from '@/utils/dateUtils'
|
import { formatDate } from '@/utils/dateUtils'
|
||||||
@@ -118,7 +120,7 @@ const fetchArticleDetail = async () => {
|
|||||||
|
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
article.value = res.data
|
article.value = res.data
|
||||||
|
article.value.categoryName = await categoryAttributeService.getAttributeById(article.value.categoryId)|| '未分类'
|
||||||
// 增加文章浏览量
|
// 增加文章浏览量
|
||||||
try {
|
try {
|
||||||
await articleService.incrementArticleViews(Number(articleId))
|
await articleService.incrementArticleViews(Number(articleId))
|
||||||
|
|||||||
@@ -17,12 +17,15 @@
|
|||||||
<img :src="getAvatar()" class="avatar">
|
<img :src="getAvatar()" class="avatar">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="username">{{ comment.displayName || comment.nickname }}</div>
|
<div class="username">{{ comment.displayName || comment.nickname }}</div>
|
||||||
<div class="time">{{ comment.createdAt || '刚刚' }}</div>
|
<div class="time">{{ formatDate(comment.createdAt) || '刚刚' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" v-html="comment.content"></div>
|
<div class="comment-content" v-html="comment.content"></div>
|
||||||
<div class="comment-actions">
|
<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>
|
<span class="reply-btn" @click="handleReply(null, comment)">回复</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 回复列表 -->
|
<!-- 回复列表 -->
|
||||||
@@ -32,11 +35,15 @@
|
|||||||
<img :src="getAvatar()" class="avatar">
|
<img :src="getAvatar()" class="avatar">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="username">{{ reply.displayName || reply.nickname }}</div>
|
<div class="username">{{ reply.displayName || reply.nickname }}</div>
|
||||||
<div class="time">{{ reply.createdAt || '刚刚' }}</div>
|
<div class="time">{{ formatDate(reply.createdAt) || '刚刚' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="reply-content">{{ reply.content }}</div>
|
<div class="reply-content">{{ reply.content }}</div>
|
||||||
<div class="reply-actions">
|
<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>
|
<span class="reply-btn" @click="handleReply(comment, reply)">回复</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,10 +113,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted } from 'vue'
|
import { reactive, ref, onMounted, watch } from 'vue'
|
||||||
import { messageService } from '@/services'
|
import { messageService } from '@/services'
|
||||||
import { ElMessage, ElForm } from 'element-plus'
|
import { ElMessage, ElForm } from 'element-plus'
|
||||||
import { useGlobalStore } from '@/store/globalStore'
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
import { formatDate } from '@/utils/dateUtils'
|
||||||
|
|
||||||
|
// 定义组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
comments: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
const globalStore = useGlobalStore()
|
const globalStore = useGlobalStore()
|
||||||
const messageBoardData = ref([]) // 留言板留言(articleid为空的主留言及其回复)
|
const messageBoardData = ref([]) // 留言板留言(articleid为空的主留言及其回复)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -213,7 +229,7 @@ const rules = {
|
|||||||
// 生成头像URL
|
// 生成头像URL
|
||||||
const getAvatar = (email) => {
|
const getAvatar = (email) => {
|
||||||
if (!email) return 'https://www.gravatar.com/avatar?d=mp&s=40'
|
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
|
loading.value = true
|
||||||
let res = null
|
let res = null
|
||||||
|
|
||||||
// 安全获取文章ID,如果globalStore中没有articlebutn则返回null
|
// 优先使用props传递的articleid,其次使用globalStore中的数据
|
||||||
const articleData = globalStore.getValue('articlebutn')
|
let articleid = props.comments || null
|
||||||
const articleid = (articleData && typeof articleData === 'object' && 'id' in articleData) ? articleData.id : 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
|
form.articleid = articleid
|
||||||
|
|
||||||
// 根据是否有文章ID选择不同的API调用
|
// 根据是否有文章ID选择不同的API调用
|
||||||
@@ -327,6 +349,13 @@ const cancelReply = () => {
|
|||||||
form.content = ''
|
form.content = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听articleid变化,重新加载留言
|
||||||
|
watch(() => props.comments, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
fetchMessages()
|
||||||
|
}
|
||||||
|
}, { immediate: false })
|
||||||
|
|
||||||
// 组件挂载时获取留言列表
|
// 组件挂载时获取留言列表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMessages()
|
fetchMessages()
|
||||||
@@ -410,6 +439,29 @@ const post_comment_reply_cancel = () => {
|
|||||||
replyingTo.value = { id: null, nickname: '', content: '' }
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -508,6 +560,29 @@ const post_comment_reply_cancel = () => {
|
|||||||
text-decoration: underline;
|
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 {
|
.replies {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user