第4天-3:评论系统与情感分析
🎯 掘金标题:💬 如何设计一个高互动的博客评论系统(含情感分析)
📝 CSDN标题:Vue 3 + LocalStorage 实现博客评论系统:支持回复、点赞、情感分析
前言
评论区是博主与读者互动的重要场所。一个设计良好的评论系统可以:
- 增加读者参与感和粘性
- 帮助博主了解读者需求
- 形成良好的社区氛围
今天分享如何实现一个功能完备的评论系统,并加入情感分析功能!
功能设计
功能列表
评论系统功能
├── 评论列表展示
│ ├── 按时间排序
│ ├── 回复嵌套显示
│ └── 分页加载
├── 评论交互
│ ├── 发布评论
│ ├── 回复评论
│ ├── 点赞评论
│ └── 删除评论
└── 智能功能
├── 评论字数统计
├── 敏感词过滤
└── 情感分析
核心实现
1. 评论数据结构
// src/types/comment.ts
export interface Comment {
id: string
articleId: string
userId: string
userName: string
userAvatar: string
content: string
createTime: number
likes: number
replies: Comment[]
sentiment?: 'positive' | 'neutral' | 'negative' // 情感分析结果
isLiked?: boolean
}
export interface CommentForm {
content: string
replyTo?: string
}
2. 评论服务
// src/services/comment.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Comment, CommentForm } from '@/types/comment'
// 情感分析函数(简化版)
function analyzeSentiment(text: string): 'positive' | 'neutral' | 'negative' {
const positiveWords = ['赞', '好', '棒', '厉害', '感谢', '有用', '学习了', '支持下', '期待', '喜欢']
const negativeWords = ['差', '烂', '垃圾', '没用', '失望', '无语', '问题', '错误', 'bug']
let score = 0
positiveWords.forEach(word => {
if (text.includes(word)) score++
})
negativeWords.forEach(word => {
if (text.includes(word)) score--
})
if (score > 0) return 'positive'
if (score < 0) return 'negative'
return 'neutral'
}
export const useCommentStore = defineStore('comment', () => {
const comments = ref<Comment[]>([])
// 获取文章评论
const getCommentsByArticle = computed(() => {
return (articleId: string) => comments.value
.filter(c => c.articleId === articleId)
.sort((a, b) => b.createTime - a.createTime)
})
// 加载评论
function loadComments(articleId: string) {
const key = `comments_${articleId}`
const data = localStorage.getItem(key)
if (data) {
comments.value = JSON.parse(data)
}
}
// 保存评论
function saveComments(articleId: string) {
const key = `comments_${articleId}`
const articleComments = comments.value.filter(c => c.articleId === articleId)
localStorage.setItem(key, JSON.stringify(articleComments))
}
// 添加评论
function addComment(articleId: string, form: CommentForm, user: { id: string; name: string; avatar: string }) {
const comment: Comment = {
id: `comment_${Date.now()}_${Math.random().toString(36).slice(2)}`,
articleId,
userId: user.id,
userName: user.name,
userAvatar: user.avatar,
content: form.content,
createTime: Date.now(),
likes: 0,
replies: [],
sentiment: analyzeSentiment(form.content)
}
if (form.replyTo) {
// 添加回复
const parentComment = findComment(comments.value, form.replyTo)
if (parentComment) {
parentComment.replies.push(comment)
}
} else {
// 添加顶层评论
comments.value.unshift(comment)
}
saveComments(articleId)
return comment
}
// 查找评论
function findComment(list: Comment[], id: string): Comment | null {
for (const item of list) {
if (item.id === id) return item
if (item.replies.length > 0) {
const found = findComment(item.replies, id)
if (found) return found
}
}
return null
}
// 点赞评论
function toggleLike(commentId: string) {
const comment = findComment(comments.value, commentId)
if (comment) {
comment.isLiked = !comment.isLiked
comment.likes += comment.isLiked ? 1 : -1
const articleId = comment.articleId
saveComments(articleId)
}
}
// 获取评论统计
function getStats(articleId: string) {
const articleComments = comments.value.filter(c => c.articleId === articleId)
const allComments = [...articleComments]
articleComments.forEach(c => {
allComments.push(...c.replies)
})
const sentiments = {
positive: allComments.filter(c => c.sentiment === 'positive').length,
neutral: allComments.filter(c => c.sentiment === 'neutral').length,
negative: allComments.filter(c => c.sentiment === 'negative').length
}
return {
total: allComments.length,
...sentiments
}
}
return {
comments,
getCommentsByArticle,
loadComments,
addComment,
toggleLike,
getStats
}
})
3. 评论组件
<!-- src/components/comment/CommentSection.vue -->
<template>
<div class="comment-section">
<h3 class="section-title">
💬 评论 ({{ totalComments }})
</h3>
<!-- 评论统计 -->
<div v-if="totalComments > 0" class="sentiment-stats">
<span class="stat positive">😊 赞 {{ stats.positive }}</span>
<span class="stat neutral">😐 中立 {{ stats.neutral }}</span>
<span class="stat negative">😔 待改进 {{ stats.negative }}</span>
</div>
<!-- 评论输入 -->
<div class="comment-input">
<el-avatar :size="40" src="/avatar.png" />
<div class="input-wrapper">
<el-input
v-model="commentContent"
type="textarea"
:rows="3"
:placeholder="replyTo ? `回复 @${replyToName}:` : '写下你的评论...'"
/>
<div class="input-footer">
<span class="char-count">{{ commentContent.length }}/500</span>
<el-button type="primary" :disabled="!commentContent.trim()" @click="submitComment">
发布评论
</el-button>
</div>
</div>
</div>
<!-- 评论列表 -->
<div class="comment-list">
<CommentItem
v-for="comment in comments"
:key="comment.id"
:comment="comment"
@reply="handleReply"
@like="handleLike"
/>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="load-more">
<el-button @click="loadMore">加载更多</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useCommentStore } from '@/services/comment'
import { ElMessage } from 'element-plus'
import CommentItem from './CommentItem.vue'
const props = defineProps<{
articleId: string
}>()
const commentStore = useCommentStore()
const commentContent = ref('')
const replyTo = ref<string>('')
const replyToName = ref('')
const page = ref(1)
const pageSize = 10
const comments = computed(() => {
return commentStore.getCommentsByArticle.value(props.articleId)
.slice(0, page.value * pageSize)
})
const totalComments = computed(() => {
return comments.value.length
})
const hasMore = computed(() => {
return comments.value.length < commentStore.getCommentsByArticle.value(props.articleId).length
})
const stats = computed(() => {
return commentStore.getStats(props.articleId)
})
function handleReply({ id, userName }: { id: string; userName: string }) {
replyTo.value = id
replyToName.value = userName
}
function handleLike(id: string) {
commentStore.toggleLike(id)
}
function submitComment() {
if (commentContent.value.length > 500) {
ElMessage.warning('评论不能超过500字')
return
}
// 模拟当前用户
const user = {
id: 'current_user',
name: '访客',
avatar: '/avatar.png'
}
commentStore.addComment(props.articleId, {
content: commentContent.value,
replyTo: replyTo.value || undefined
}, user)
commentContent.value = ''
replyTo.value = ''
replyToName.value = ''
ElMessage.success('评论发布成功')
}
function loadMore() {
page.value++
}
onMounted(() => {
commentStore.loadComments(props.articleId)
})
</script>
<style scoped>
.comment-section {
margin-top: 40px;
padding: 24px;
background: #fff;
border-radius: 12px;
}
.section-title {
font-size: 20px;
margin-bottom: 16px;
}
.sentiment-stats {
display: flex;
gap: 16px;
margin-bottom: 20px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
}
.stat {
font-size: 14px;
}
.positive { color: #67c23a; }
.neutral { color: #909399; }
.negative { color: #f56c6c; }
.comment-input {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.input-wrapper {
flex: 1;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.char-count {
font-size: 12px;
color: #999;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.load-more {
text-align: center;
margin-top: 20px;
}
</style>
<!-- src/components/comment/CommentItem.vue -->
<template>
<div class="comment-item" :class="{ 'is-reply': isReply }">
<el-avatar :size="isReply ? 32 : 40" :src="comment.userAvatar" />
<div class="comment-content">
<div class="comment-header">
<span class="user-name">{{ comment.userName }}</span>
<span class="sentiment-badge" :class="comment.sentiment">
{{ sentimentText }}
</span>
<span class="create-time">{{ formatTime(comment.createTime) }}</span>
</div>
<div class="comment-body">{{ comment.content }}</div>
<div class="comment-actions">
<span class="action-btn" @click="$emit('like', comment.id)">
{{ comment.isLiked ? '❤️' : '🤍' }} {{ comment.likes }}
</span>
<span class="action-btn" @click="handleReply">
💬 回复
</span>
</div>
<!-- 回复列表 -->
<div v-if="comment.replies?.length > 0" class="replies">
<CommentItem
v-for="reply in comment.replies"
:key="reply.id"
:comment="reply"
:is-reply="true"
@reply="$emit('reply', $event)"
@like="$emit('like', reply.id)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Comment } from '@/types/comment'
const props = defineProps<{
comment: Comment
isReply?: boolean
}>()
defineEmits(['reply', 'like'])
const sentimentText = computed(() => {
const map = {
positive: '好评',
neutral: '中立',
negative: '待改进'
}
return map[props.comment.sentiment || 'neutral']
})
function formatTime(timestamp: number) {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`
return date.toLocaleDateString()
}
function handleReply() {
// 滚动到评论输入框
document.querySelector('.comment-input')?.scrollIntoView({ behavior: 'smooth' })
}
</script>
<style scoped>
.comment-item {
display: flex;
gap: 12px;
}
.comment-item.is-reply {
margin-top: 12px;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.user-name {
font-weight: 600;
color: #333;
}
.sentiment-badge {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.sentiment-badge.positive { background: #e1f3d8; color: #67c23a; }
.sentiment-badge.neutral { background: #f4f4f5; color: #909399; }
.sentiment-badge.negative { background: #fef0f0; color: #f56c6c; }
.create-time {
font-size: 12px;
color: #999;
}
.comment-body {
line-height: 1.6;
color: #333;
}
.comment-actions {
display: flex;
gap: 16px;
margin-top: 8px;
}
.action-btn {
font-size: 13px;
color: #666;
cursor: pointer;
transition: color 0.2s;
}
.action-btn:hover {
color: #409eff;
}
.replies {
margin-top: 16px;
padding-left: 16px;
border-left: 2px solid #ebeef5;
}
</style>
效果展示
评论系统上线后,可以获得:
- 📊 读者反馈数据:通过情感分析了解文章质量
- 💬 社区活跃度:提升读者互动意愿
- 🔍 改进方向:根据负面反馈优化内容
🎯 进阶功能建议
- 接入后端实现用户登录和评论管理
- 添加评论审核功能,防止垃圾评论
- 实现@提及功能,通知被回复的用户
🔗 相关资源
- 在线演示:[fineday.vip]