Files
MyfronyProject/src/views/messageboard.vue
qingfeng1121 ede67faafd feat: 优化前端布局和代理配置
refactor: 移除调试日志并优化代码结构
style: 调整响应式设计和UI细节
fix: 修复路由和导航相关的问题
2025-12-12 17:14:04 +08:00

1090 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>