1090 lines
30 KiB
Vue
1090 lines
30 KiB
Vue
<template>
|
||
<div>
|
||
<div class="message-board-container">
|
||
<!-- 留言内容区 -->
|
||
<div class="message-list-wrapper">
|
||
<h3 class="message-board-title">留言板</h3>
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-state-container">
|
||
<el-skeleton :count="5" />
|
||
</div>
|
||
|
||
<!-- 留言列表 -->
|
||
<div class="comment-list-container">
|
||
<div v-for="comment in messageBoardData" :key="comment.messageid" class="comment-item-wrapper">
|
||
<div class="comment-header-info">
|
||
<!-- 头像 -->
|
||
<div class="avatar-container">
|
||
<img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)" class="user-avatar" alt="头像">
|
||
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(comment.displayName || comment.nickname)">
|
||
{{ getInitialLetter(comment.displayName || comment.nickname) }}
|
||
</div>
|
||
</div>
|
||
<div class="user-meta-info">
|
||
<div class="user-nickname">{{ comment.displayName || comment.nickname }}</div>
|
||
<div class="comment-time">{{ formatDate(comment.createdAt) || '刚刚' }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="comment-content-text" v-html="comment.content"></div>
|
||
<div class="comment-actions-bar">
|
||
<span class="like-button" v-if="false" @click="handleLike(comment)">
|
||
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes
|
||
}}</span>
|
||
👍 赞
|
||
</span>
|
||
<span class="reply-button" @click="handleReply(null, comment)">回复</span>
|
||
<!-- 删除按钮 -->
|
||
<span class="delete-button" @click="handleDelete(comment.messageid)"
|
||
v-if="globalStore.Login">删除</span>
|
||
</div>
|
||
<!-- 回复列表 -->
|
||
<div v-if="comment.replies && comment.replies && comment.replies.length > 0"
|
||
class="reply-list-container">
|
||
<div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper">
|
||
<div class="reply-header-info">
|
||
<div class="avatar-container">
|
||
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)" class="user-avatar" alt="头像">
|
||
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(reply.displayName || reply.nickname)">
|
||
{{ getInitialLetter(reply.displayName || reply.nickname) }}
|
||
</div>
|
||
</div>
|
||
<div class="user-meta-info">
|
||
<div class="user-nickname">{{ reply.displayName || reply.nickname }}</div>
|
||
<div class="comment-time">{{ formatDate(reply.createdAt) || '刚刚' }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="reply-content-text">{{ reply.content }}</div>
|
||
<div class="reply-actions-bar">
|
||
|
||
<span class="like-button" v-if="false" @click="handleLike(reply)">
|
||
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes
|
||
}}</span>
|
||
👍 赞
|
||
</span>
|
||
<span class="reply-button" @click="handleReply(comment, reply)">回复</span>
|
||
<!-- 删除按钮 -->
|
||
<span class="delete-button" @click="handleDelete(reply.messageid)"
|
||
v-if="globalStore.Login">删除</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 无留言提示 -->
|
||
<div v-if="!loading && messageBoardData.length === 0" class="empty-message-state">
|
||
还没有留言,快来抢沙发吧!
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 留言输入区 -->
|
||
<div class="comment-form-section">
|
||
<h2 class="comment-form-title">发送评论(请正确填写邮箱地址,否则将会当成垃圾评论处理)</h2>
|
||
<div v-if="replyingTo.id" class="reply-preview-container">
|
||
<span>
|
||
正在回复 <b>{{ replyingTo.nickname }}</b> 的评论:
|
||
</span>
|
||
<div class="reply-preview-text">
|
||
{{ replyingTo.content }}
|
||
</div>
|
||
<button class="cancel-reply-button" @click="cancelReply">取消回复</button>
|
||
</div>
|
||
<el-form :model="form" :rules="rules" ref="formRef" label-width="0">
|
||
<el-form-item prop="content">
|
||
<el-input v-model="form.content" placeholder="评论内容" type="textarea" rows="4" clearable
|
||
:disabled="submitting" />
|
||
</el-form-item>
|
||
<div class="form-input-row form-input-row--inline">
|
||
<el-form-item prop="nickname">
|
||
<el-input v-model="form.nickname" placeholder="昵称" clearable :disabled="submitting" />
|
||
</el-form-item>
|
||
<el-form-item prop="email">
|
||
<el-input v-model="form.email" placeholder="邮箱/QQ号" clearable :disabled="submitting" />
|
||
</el-form-item>
|
||
<el-form-item prop="captcha">
|
||
<div class="captcha-input-wrapper">
|
||
<el-input v-model="form.captcha" placeholder="验证码" clearable :disabled="submitting"
|
||
@focus="showCaptchaHint = true" @blur="showCaptchaHint = false" />
|
||
<div class="captcha-hint-popup" @click="generateCaptcha" v-show="showCaptchaHint">
|
||
{{ captchaHint }}
|
||
<span class="refresh-icon">↻</span>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="form-input-row">
|
||
<el-form-item class="submit-button-container">
|
||
<el-button type="primary" @click="onSubmit" class="form-submit-button" :loading="submitting"
|
||
:disabled="!form.content || !form.nickname">
|
||
发送
|
||
</el-button>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
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) // 加载状态
|
||
const submitting = ref(false) // 提交状态
|
||
const replyingTo = ref({ id: null, nickname: '', content: '' }) // 正在回复的信息
|
||
const formRef = ref() // 表单引用
|
||
|
||
// 验证码相关状态
|
||
const captchaHint = ref('') // 验证码提示
|
||
const captchaAnswer = ref('') // 验证码答案
|
||
const showCaptchaHint = ref(false) // 是否显示验证码提示
|
||
|
||
// 表单数据
|
||
const form = reactive({
|
||
parentid: null, // 父留言ID
|
||
replyid: null, // 被回复留言ID
|
||
articleid: null, // 文章ID
|
||
content: '', // 评论内容
|
||
nickname: '', // 昵称
|
||
email: '', // 邮箱
|
||
captcha: '' // 验证码
|
||
})
|
||
|
||
// ============================== 表单验证规则 ==============================
|
||
|
||
const rules = {
|
||
content: [
|
||
{ required: true, message: '请输入评论内容', trigger: 'blur' },
|
||
{ min: 1, max: 500, message: '评论内容长度应在1-500个字符之间', trigger: 'blur' }
|
||
],
|
||
nickname: [
|
||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||
{ min: 2, max: 20, message: '昵称长度应在2-20个字符之间', trigger: 'blur' }
|
||
],
|
||
email: [
|
||
{ required: true, message: '请输入邮箱/QQ号', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
// 验证邮箱格式或QQ号格式(5-11位数字)
|
||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||
const qqRegex = /^[1-9]\d{4,10}$/;
|
||
if (emailRegex.test(value) || qqRegex.test(value)) {
|
||
callback();
|
||
} else {
|
||
callback(new Error('请输入有效的邮箱地址或QQ号'));
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
captcha: [
|
||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value.toLowerCase() !== captchaAnswer.value) {
|
||
callback(new Error('验证码错误,请重新输入'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}
|
||
|
||
// ============================== 验证码模块 ==============================
|
||
|
||
/**
|
||
* 生成验证码
|
||
* 随机选择生成数学题验证码或字符验证码
|
||
*/
|
||
const generateCaptcha = () => {
|
||
// 随机选择数学题或字符验证码
|
||
const isMathCaptcha = Math.random() > 0.5
|
||
|
||
if (isMathCaptcha) {
|
||
generateMathCaptcha()
|
||
} else {
|
||
generateCharCaptcha()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成数学题验证码
|
||
* 包含简单的加法或减法运算
|
||
*/
|
||
const generateMathCaptcha = () => {
|
||
// 简单数学题:加法或减法
|
||
const num1 = Math.floor(Math.random() * 10) + 1
|
||
const num2 = Math.floor(Math.random() * 10) + 1
|
||
const operator = Math.random() > 0.5 ? '+' : '-'
|
||
|
||
if (operator === '+') {
|
||
const answer = num1 + num2
|
||
captchaHint.value = `${num1} ${operator} ${num2} = ?`
|
||
captchaAnswer.value = answer.toString()
|
||
} else {
|
||
// 确保减法结果为正
|
||
const larger = Math.max(num1, num2)
|
||
const smaller = Math.min(num1, num2)
|
||
captchaHint.value = `${larger} - ${smaller} = ?`
|
||
captchaAnswer.value = (larger - smaller).toString()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成字符验证码
|
||
* 随机生成4位大小写字母和数字的组合
|
||
*/
|
||
const generateCharCaptcha = () => {
|
||
const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'
|
||
let captcha = ''
|
||
for (let i = 0; i < 4; i++) {
|
||
captcha += chars.charAt(Math.floor(Math.random() * chars.length))
|
||
}
|
||
captchaHint.value = captcha
|
||
captchaAnswer.value = captcha.toLowerCase()
|
||
}
|
||
|
||
// ============================== UI辅助模块 ==============================
|
||
|
||
/**
|
||
* 获取Gravatar头像URL
|
||
* @param {string} email - 用户邮箱
|
||
* @returns {string|null} - 头像URL或null
|
||
*/
|
||
const getAvatarUrl = (email) => {
|
||
if (!email) return null;
|
||
|
||
// 简单验证邮箱格式
|
||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||
if (!emailRegex.test(email)) return null;
|
||
|
||
// 使用邮箱的MD5哈希(这里简化处理,实际项目中应该使用md5库)
|
||
return `https://www.gravatar.com/avatar/${email}?d=404&s=40`;
|
||
}
|
||
|
||
/**
|
||
* 获取名字的首字母
|
||
* 用于生成字母头像
|
||
* @param {string} name - 用户昵称
|
||
* @returns {string} - 首字母或问号
|
||
*/
|
||
const getInitialLetter = (name) => {
|
||
if (!name || typeof name !== 'string') return '?';
|
||
|
||
// 移除可能的@回复前缀
|
||
const cleanName = name.replace(/^.+@\s*/, '');
|
||
|
||
// 获取第一个字符并转为大写
|
||
return cleanName.charAt(0).toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* 获取首字母头像的样式
|
||
* 根据名字生成一致的颜色
|
||
* @param {string} name - 用户昵称
|
||
* @returns {Object} - 包含背景色和文字颜色的样式对象
|
||
*/
|
||
const getLetterAvatarStyle = (name) => {
|
||
// 预定义的颜色数组
|
||
const colors = [
|
||
'#4A90E2', '#50E3C2', '#F5A623', '#D0021B', '#9013FE',
|
||
'#B8E986', '#BD10E0', '#50E3C2', '#417505', '#7ED321',
|
||
'#BD10E0', '#F8E71C', '#8B572A', '#9B9B9B', '#4A4A4A'
|
||
];
|
||
|
||
// 根据名字生成一致的颜色索引
|
||
let hash = 0;
|
||
if (name) {
|
||
for (let i = 0; i < name.length; i++) {
|
||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||
}
|
||
}
|
||
const colorIndex = Math.abs(hash) % colors.length;
|
||
|
||
return {
|
||
backgroundColor: colors[colorIndex],
|
||
color: 'white'
|
||
};
|
||
}
|
||
|
||
// ============================== 数据处理模块 ==============================
|
||
|
||
/**
|
||
* 处理留言数据,构建留言与回复的层级结构
|
||
* @param {Array} messages - 原始留言数据
|
||
* @returns {Array} - 处理后的留言数据(包含回复数组)
|
||
*/
|
||
const processMessageData = (messages) => {
|
||
// 为主留言添加replies数组
|
||
const allMessagesWithReplies = messages.map(msg => ({
|
||
...msg,
|
||
replies: []
|
||
}))
|
||
|
||
// 分离主留言和回复
|
||
const mainMessages = []
|
||
const replies = []
|
||
|
||
allMessagesWithReplies.forEach(msg => {
|
||
if (msg.parentid && msg.parentid > 0) {
|
||
replies.push(msg)
|
||
} else {
|
||
mainMessages.push(msg)
|
||
}
|
||
})
|
||
|
||
// 将回复添加到对应的主留言中并处理@回复显示
|
||
processRepliesForMainMessages(mainMessages, replies)
|
||
|
||
return mainMessages
|
||
}
|
||
|
||
/**
|
||
* 处理回复数据,将其添加到对应的主留言中
|
||
* @param {Array} mainMessages - 主留言数组
|
||
* @param {Array} replies - 回复数组
|
||
*/
|
||
const processRepliesForMainMessages = (mainMessages, replies) => {
|
||
replies.forEach(reply => {
|
||
// 找到父留言
|
||
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
|
||
if (parentMsg) {
|
||
// 处理@回复的显示名称
|
||
processReplyDisplayName(reply, replies)
|
||
parentMsg.replies.push(reply)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 处理回复的显示名称
|
||
* 如果是回复其他回复,添加@标记
|
||
* @param {Object} reply - 回复对象
|
||
* @param {Array} allReplies - 所有回复数组
|
||
*/
|
||
const processReplyDisplayName = (reply, allReplies) => {
|
||
if (reply.replyid) {
|
||
const repliedMsg = allReplies.find(msg => msg.messageid === reply.replyid)
|
||
if (repliedMsg) {
|
||
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
|
||
} else {
|
||
reply.displayName = reply.nickname
|
||
}
|
||
} else {
|
||
reply.displayName = reply.nickname
|
||
}
|
||
}
|
||
|
||
// ============================== API调用模块 ==============================
|
||
|
||
/**
|
||
* 从后端获取留言列表
|
||
* 根据articleid决定获取文章留言还是全局留言
|
||
*/
|
||
const fetchMessages = async () => {
|
||
try {
|
||
loading.value = true
|
||
let res = null
|
||
|
||
// 获取文章ID(优先使用props,其次使用全局状态)
|
||
const articleid = getArticleId()
|
||
form.articleid = articleid
|
||
|
||
// 根据是否有文章ID选择不同的API调用
|
||
res = await (articleid
|
||
? messageService.getMessagesByArticleId(articleid)
|
||
: fetchAllMessages()
|
||
)
|
||
|
||
// 验证响应结果
|
||
if (!res || !res.data) {
|
||
handleEmptyResponse()
|
||
return
|
||
}
|
||
|
||
// 处理留言数据
|
||
messageBoardData.value = processMessageData(res.data)
|
||
} catch (error) {
|
||
handleFetchError(error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文章ID
|
||
* 优先级:props.comments > globalStore中的articleInfo
|
||
* @returns {number|null} - 文章ID或null
|
||
*/
|
||
const getArticleId = () => {
|
||
let articleid = props.comments || null
|
||
|
||
if (!articleid) {
|
||
// 安全获取文章ID,如果globalStore中没有articleInfo则返回null
|
||
const articleData = globalStore.getValue('articleInfo')
|
||
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData)
|
||
? articleData.articleid
|
||
: null
|
||
}
|
||
|
||
return articleid
|
||
}
|
||
|
||
/**
|
||
* 获取所有留言并过滤
|
||
* 只保留articleid为空或不存在的全局留言
|
||
* @returns {Promise} - API响应
|
||
*/
|
||
const fetchAllMessages = async () => {
|
||
const res = await messageService.getAllMessages()
|
||
// 过滤掉articleid不为空的留言,只保留articleid为空或不存在的留言
|
||
if (res && res.data) {
|
||
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
|
||
}
|
||
return res
|
||
}
|
||
|
||
/**
|
||
* 处理空响应
|
||
*/
|
||
const handleEmptyResponse = () => {
|
||
console.warn('未获取到留言数据')
|
||
messageBoardData.value = []
|
||
}
|
||
|
||
/**
|
||
* 处理获取留言错误
|
||
* @param {Error} error - 错误对象
|
||
*/
|
||
const handleFetchError = (error) => {
|
||
console.error('获取留言列表失败:', error)
|
||
ElMessage.error('获取留言失败,请稍后重试')
|
||
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
|
||
}
|
||
|
||
/**
|
||
* 提交留言
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const submitMessage = async () => {
|
||
const res = await messageService.saveMessage({
|
||
content: form.content,
|
||
nickname: form.nickname,
|
||
email: form.email,
|
||
parentid: form.parentid,
|
||
replyid: form.replyid,
|
||
articleid: form.articleid
|
||
})
|
||
|
||
return res
|
||
}
|
||
|
||
/**
|
||
* 点赞留言
|
||
* @param {number} messageId - 留言ID
|
||
* @returns {Promise} - API响应
|
||
*/
|
||
const likeMessage = async (messageId) => {
|
||
return await messageService.likeMessage(messageId)
|
||
}
|
||
|
||
/**
|
||
* 删除留言
|
||
* @param {number} messageId - 留言ID
|
||
* @returns {Promise} - API响应
|
||
*/
|
||
const deleteMessage = async (messageId) => {
|
||
return await messageService.deleteMessage(messageId)
|
||
}
|
||
|
||
// ============================== 交互操作模块 ==============================
|
||
|
||
/**
|
||
* 处理回复操作
|
||
* @param {Object|null} msg - 主留言对象,null表示直接回复主留言
|
||
* @param {Object} reply - 被回复的留言对象
|
||
*/
|
||
const handleReply = (msg, reply) => {
|
||
// 设置回复相关表单数据
|
||
setupReplyFormData(msg, reply)
|
||
|
||
// 记录正在回复的信息
|
||
replyingTo.value = {
|
||
id: reply.messageid,
|
||
nickname: reply.nickname || '匿名用户',
|
||
content: reply.content
|
||
}
|
||
|
||
// 滚动到输入框
|
||
scrollToInput()
|
||
}
|
||
|
||
/**
|
||
* 设置回复表单数据
|
||
* @param {Object|null} msg - 主留言对象
|
||
* @param {Object} reply - 被回复的留言对象
|
||
*/
|
||
const setupReplyFormData = (msg, reply) => {
|
||
if (msg !== null) {
|
||
// 回复模式:回复某条回复
|
||
form.replyid = reply.messageid
|
||
form.parentid = msg.messageid
|
||
} else {
|
||
// 普通回复模式:回复主留言
|
||
form.replyid = null
|
||
form.parentid = reply.messageid
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 滚动到输入框区域
|
||
*/
|
||
const scrollToInput = () => {
|
||
setTimeout(() => {
|
||
document.querySelector('.message-form-section')?.scrollIntoView({
|
||
behavior: 'smooth'
|
||
})
|
||
}, 100)
|
||
}
|
||
|
||
/**
|
||
* 取消回复
|
||
*/
|
||
const cancelReply = () => {
|
||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||
form.replyid = null
|
||
form.content = ''
|
||
}
|
||
|
||
/**
|
||
* 重置表单
|
||
*/
|
||
const resetForm = () => {
|
||
form.replyid = null
|
||
form.content = ''
|
||
form.nickname = ''
|
||
form.email = ''
|
||
form.captcha = ''
|
||
form.parentid = null
|
||
form.articleid = null
|
||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||
}
|
||
|
||
/**
|
||
* 提交评论
|
||
*/
|
||
const onSubmit = async () => {
|
||
if (!formRef.value) return;
|
||
|
||
try {
|
||
// 表单验证
|
||
await validateForm()
|
||
// console.log('提交留言表单:', form)
|
||
|
||
submitting.value = true
|
||
|
||
// 发送留言并处理结果
|
||
await handleMessageSubmission()
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证表单
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const validateForm = async () => {
|
||
return new Promise((resolve, reject) => {
|
||
formRef.value.validate((valid) => {
|
||
if (!valid) {
|
||
ElMessage.warning('请检查表单填写是否正确');
|
||
reject(new Error('表单验证失败'));
|
||
} else {
|
||
resolve();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理留言提交
|
||
* @returns {Promise<void>}
|
||
*/
|
||
const handleMessageSubmission = async () => {
|
||
const res = await submitMessage()
|
||
|
||
if (res.success) {
|
||
// 提交成功
|
||
ElMessage.success(form.parentid ? '回复成功' : '留言成功')
|
||
await fetchMessages() // 重新获取列表
|
||
resetForm()
|
||
|
||
// 如果是回复模式,取消回复状态
|
||
if (form.parentid) {
|
||
cancelReply()
|
||
}
|
||
} else {
|
||
// 提交失败
|
||
ElMessage.error(`${form.parentid ? '回复' : '留言'}失败:${res.message || '未知错误'}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理点赞操作
|
||
* @param {Object} msg - 留言对象
|
||
*/
|
||
const handleLike = async (msg) => {
|
||
try {
|
||
// 显示加载状态
|
||
msg.isLiking = true
|
||
|
||
const res = await 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
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理删除操作
|
||
* @param {number} messageId - 留言ID
|
||
*/
|
||
const handleDelete = async (messageId) => {
|
||
// 确认删除
|
||
if (!confirm('确定删除吗?')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const res = await deleteMessage(messageId)
|
||
|
||
if (res.success) {
|
||
// 从列表中移除
|
||
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== messageId)
|
||
ElMessage.success('删除成功')
|
||
} else {
|
||
ElMessage.error('删除失败:' + (res.message || '未知错误'))
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 另一个取消回复的方法(冗余但保留以兼容现有代码)
|
||
*/
|
||
const post_comment_reply_cancel = () => {
|
||
form.content = ''
|
||
form.replyid = null
|
||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||
}
|
||
|
||
// ============================== 生命周期和监听器 ==============================
|
||
|
||
// 监听articleid变化,重新加载留言
|
||
watch(() => props.comments, (newVal) => {
|
||
if (newVal) {
|
||
fetchMessages()
|
||
}
|
||
}, { immediate: false })
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
fetchMessages() // 获取留言列表
|
||
generateCaptcha() // 生成验证码
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* 主容器样式 */
|
||
.message-board-container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 留言列表容器 */
|
||
.message-list-wrapper {
|
||
margin-bottom: 24px;
|
||
border-radius: 12px;
|
||
background-color: rgba(255, 255, 255, 0.85);
|
||
padding: 16px;
|
||
min-height: 120px;
|
||
}
|
||
|
||
/* 留言板标题 */
|
||
.message-board-title {
|
||
color: #2c3e50;
|
||
margin-bottom: 16px;
|
||
font-size: 1.2rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 加载状态样式 */
|
||
.loading-state-container {
|
||
padding: 40px 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 评论列表容器 */
|
||
.comment-list-container {
|
||
// background-color: #f5f5f5;
|
||
}
|
||
|
||
/* 评论项容器 */
|
||
.comment-item-wrapper {
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border-radius: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
|
||
/* 评论头部信息 */
|
||
.comment-header-info {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.avatar-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
/* 用户头像 */
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* 首字母头像 */
|
||
.letter-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: white;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* 用户元信息 */
|
||
.user-meta-info {
|
||
flex: 1;
|
||
text-align: left;
|
||
}
|
||
|
||
/* 用户名 */
|
||
.user-nickname {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
color: #409eff;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* 评论时间 */
|
||
.comment-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
/* 评论内容文本 */
|
||
.comment-content-text {
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
margin-bottom: 12px;
|
||
word-break: break-word;
|
||
color: #333;
|
||
}
|
||
|
||
/* 评论操作栏 */
|
||
.comment-actions-bar {
|
||
padding-bottom: 8px;
|
||
text-align: right;
|
||
font-size: 14px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
color: #666;
|
||
}
|
||
|
||
/* 点赞按钮 */
|
||
.like-button {
|
||
margin-right: 15px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.like-button:hover {
|
||
color: #e74c3c;
|
||
background-color: rgba(231, 76, 60, 0.1);
|
||
}
|
||
|
||
.like-button:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
/* 点赞数量 */
|
||
.like-count {
|
||
margin-right: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 回复按钮 */
|
||
.reply-button {
|
||
cursor: pointer;
|
||
transition: color 0.3s ease;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.reply-button:hover {
|
||
color: #409eff;
|
||
background-color: rgba(64, 158, 255, 0.1);
|
||
}
|
||
|
||
|
||
|
||
/* 回复列表容器 */
|
||
.reply-list-container {
|
||
margin-top: 16px;
|
||
padding-left: 52px;
|
||
padding-top: 16px;
|
||
}
|
||
|
||
/**
|
||
* 内联表单输入行样式
|
||
* 用于将表单输入项水平均匀分布
|
||
*/
|
||
.form-input-row--inline {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-input-row--inline .el-form-item {
|
||
flex: 1;
|
||
margin-right: 0;
|
||
margin-left: 0;
|
||
width: calc(33.33% - 10px);
|
||
}
|
||
|
||
.form-input-row--inline .el-input {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 回复项容器 */
|
||
.reply-item-wrapper {
|
||
border-bottom: 1px solid #f0f0f0;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.reply-item-wrapper:hover {
|
||
// background-color: #f0f0f0;
|
||
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
/* 回复头部信息 */
|
||
.reply-header-info {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* 回复内容文本 */
|
||
.reply-content-text {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
margin-bottom: 8px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 回复操作栏 */
|
||
.reply-actions-bar {
|
||
font-size: 12px;
|
||
color: #666;
|
||
text-align: right;
|
||
}
|
||
|
||
/* 空状态提示 */
|
||
.empty-message-state {
|
||
text-align: center;
|
||
color: #bbb;
|
||
padding: 32px 0;
|
||
// background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* 评论表单区域 */
|
||
.comment-form-section {
|
||
background-color: rgba(255, 255, 255, 0.85);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
/* 评论表单标题 */
|
||
.comment-form-title {
|
||
color: #2c3e50;
|
||
margin-bottom: 18px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
/* 回复预览容器 */
|
||
.reply-preview-container {
|
||
border-left: 4px solid #409eff;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 回复预览文本 */
|
||
.reply-preview-text {
|
||
margin-top: 6px;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
font-style: italic;
|
||
color: #666;
|
||
font-size: 0.98rem;
|
||
}
|
||
|
||
/* 取消回复按钮 */
|
||
.cancel-reply-button {
|
||
background: #fff;
|
||
color: #409eff;
|
||
border: 1px solid #409eff;
|
||
border-radius: 6px;
|
||
padding: 4px 12px;
|
||
cursor: pointer;
|
||
margin-top: 10px;
|
||
font-size: 0.95rem;
|
||
transition: background 0.2s, color 0.2s;
|
||
}
|
||
|
||
.cancel-reply-button:hover {
|
||
background: #409eff;
|
||
color: #fff;
|
||
}
|
||
|
||
/* 验证码输入包装器 */
|
||
.captcha-input-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 验证码提示弹窗 */
|
||
.captcha-hint-popup {
|
||
position: absolute;
|
||
top: -30px;
|
||
right: 0;
|
||
background-color: #f0f9ff;
|
||
border: 1px solid #d9ecff;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #1890ff;
|
||
white-space: nowrap;
|
||
z-index: 10;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.captcha-hint-popup:hover {
|
||
background-color: #e6f7ff;
|
||
border-color: #91d5ff;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.message-board-container {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.message-list-wrapper {
|
||
padding: 12px;
|
||
}
|
||
|
||
.comment-form-section {
|
||
padding: 16px;
|
||
}
|
||
.el-form-item {
|
||
width: 100%;
|
||
}
|
||
.form-input-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.comment-header-info,
|
||
.reply-header-info {
|
||
align-items: flex-start;
|
||
}
|
||
.form-submit-button {
|
||
width: 100%;
|
||
}
|
||
.user-avatar {
|
||
margin-right: 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.reply-list-container {
|
||
padding-left: 20px;
|
||
}
|
||
}
|
||
</style> |