refactor: 重构API服务与全局状态管理 style: 优化UI样式与布局 fix: 修复文章列表与详情页的显示问题 docs: 更新类型定义与注释 chore: 更新依赖包与配置文件
826 lines
24 KiB
Vue
826 lines
24 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">
|
||
<!-- 头像 -->
|
||
<img :src="getAvatar()" class="user-avatar">
|
||
<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" @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>
|
||
</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">
|
||
<img :src="getAvatar()" class="user-avatar">
|
||
<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" @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>
|
||
<el-button type="primary" @click="onSubmit" :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,
|
||
replyid: null,
|
||
articleid: null,
|
||
content: '',
|
||
nickname: '',
|
||
email: '',
|
||
captcha: ''
|
||
})
|
||
|
||
// 生成简单验证码
|
||
const generateCaptcha = () => {
|
||
// 随机选择数学题或字符验证码
|
||
const isMathCaptcha = Math.random() > 0.5
|
||
|
||
if (isMathCaptcha) {
|
||
// 简单数学题:加法或减法
|
||
const num1 = Math.floor(Math.random() * 10) + 1
|
||
const num2 = Math.floor(Math.random() * 10) + 1
|
||
const operator = Math.random() > 0.5 ? '+' : '-'
|
||
|
||
let answer
|
||
if (operator === '+') {
|
||
answer = num1 + num2
|
||
} else {
|
||
// 确保减法结果为正
|
||
const larger = Math.max(num1, num2)
|
||
const smaller = Math.min(num1, num2)
|
||
captchaHint.value = `${larger} - ${smaller} = ?`
|
||
captchaAnswer.value = (larger - smaller).toString()
|
||
return
|
||
}
|
||
|
||
captchaHint.value = `${num1} ${operator} ${num2} = ?`
|
||
captchaAnswer.value = answer.toString()
|
||
} else {
|
||
// 简单字符验证码
|
||
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()
|
||
}
|
||
}
|
||
|
||
// 表单验证规则
|
||
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'
|
||
}
|
||
]
|
||
}
|
||
|
||
|
||
// 生成头像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`
|
||
}
|
||
|
||
// 从后端获取留言列表
|
||
const fetchMessages = async () => {
|
||
try {
|
||
loading.value = true
|
||
let res = null
|
||
|
||
// 优先使用props传递的articleid,其次使用globalStore中的数据
|
||
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
|
||
}
|
||
|
||
form.articleid = articleid
|
||
|
||
// 根据是否有文章ID选择不同的API调用
|
||
if (articleid) {
|
||
res = await messageService.getMessagesByArticleId(articleid)
|
||
} else {
|
||
res = await messageService.getAllMessages()
|
||
// 过滤掉articleid不为空的留言,只保留articleid为空或不存在的留言
|
||
if (res && res.data) {
|
||
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
|
||
}
|
||
}
|
||
|
||
// 验证响应结果
|
||
if (!res || !res.data) {
|
||
console.warn('未获取到留言数据')
|
||
messageBoardData.value = []
|
||
return
|
||
}
|
||
|
||
const allMessages = res.data
|
||
|
||
// 处理所有留言,为主留言添加replies数组
|
||
const allMessagesWithReplies = allMessages.map(msg => ({
|
||
...msg,
|
||
replies: []
|
||
}))
|
||
|
||
// 分离主留言和回复
|
||
const mainMessages = []
|
||
const replies = []
|
||
|
||
allMessagesWithReplies.forEach(msg => {
|
||
if (msg.parentid && msg.parentid > 0) {
|
||
replies.push(msg)
|
||
} else {
|
||
mainMessages.push(msg)
|
||
}
|
||
})
|
||
|
||
// 将回复添加到对应的主留言中
|
||
replies.forEach(reply => {
|
||
// 找到父留言
|
||
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
|
||
if (parentMsg) {
|
||
// 处理@回复的显示名称
|
||
if (reply.replyid) {
|
||
const repliedMsg = replies.find(msg => msg.messageid === reply.replyid)
|
||
if (repliedMsg) {
|
||
reply.displayName = `${reply.nickname}@${repliedMsg.nickname}`
|
||
} else {
|
||
reply.displayName = reply.nickname
|
||
}
|
||
} else {
|
||
reply.displayName = reply.nickname
|
||
}
|
||
parentMsg.replies.push(reply)
|
||
}
|
||
})
|
||
|
||
// 更新留言板数据
|
||
messageBoardData.value = mainMessages
|
||
} catch (error) {
|
||
console.error('获取留言列表失败:', error)
|
||
ElMessage.error('获取留言失败,请稍后重试')
|
||
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理回复
|
||
const handleReply = (msg, reply) => {
|
||
// 检查是否是回复模式
|
||
if (msg !== null) {
|
||
// 回复模式
|
||
form.replyid = reply.messageid
|
||
form.parentid = msg.messageid
|
||
} else {
|
||
// 普通回复模式
|
||
form.replyid = null
|
||
form.parentid = reply.messageid
|
||
}
|
||
replyingTo.value = {
|
||
id: reply.messageid,
|
||
nickname: reply.nickname || '匿名用户',
|
||
content: reply.content
|
||
}
|
||
// 滚动到输入框
|
||
setTimeout(() => {
|
||
document.querySelector('.message-form-section')?.scrollIntoView({ behavior: 'smooth' })
|
||
}, 100)
|
||
}
|
||
|
||
// 取消回复
|
||
const cancelReply = () => {
|
||
replyingTo.value = { id: null, nickname: '', content: '' }
|
||
form.replyid = null
|
||
form.content = ''
|
||
}
|
||
|
||
// 监听articleid变化,重新加载留言
|
||
watch(() => props.comments, (newVal) => {
|
||
if (newVal) {
|
||
fetchMessages()
|
||
}
|
||
}, { immediate: false })
|
||
|
||
// 组件挂载时获取留言列表
|
||
onMounted(() => {
|
||
fetchMessages()
|
||
generateCaptcha() // 页面加载时生成验证码
|
||
})
|
||
|
||
|
||
const onSubmit = async () => {
|
||
if (!formRef.value) return;
|
||
|
||
// 表单验证
|
||
await formRef.value.validate((valid) => {
|
||
if (!valid) {
|
||
ElMessage.warning('请检查表单填写是否正确');
|
||
throw new Error('表单验证失败');
|
||
}
|
||
});
|
||
console.log('提交留言表单:', form)
|
||
try {
|
||
submitting.value = true
|
||
|
||
if (form.parentid) {
|
||
// 回复模式
|
||
const res = await messageService.saveMessage({
|
||
content: form.content,
|
||
nickname: form.nickname,
|
||
email: form.email,
|
||
parentid: form.parentid,
|
||
replyid: form.replyid,
|
||
articleid: form.articleid
|
||
})
|
||
|
||
if (res.success) {
|
||
ElMessage.success('回复成功')
|
||
fetchMessages() // 重新获取列表
|
||
resetForm()
|
||
cancelReply()
|
||
} else {
|
||
ElMessage.error('回复失败:' + (res.message || '未知错误'))
|
||
}
|
||
} else {
|
||
// 普通留言
|
||
const res = await messageService.saveMessage({
|
||
content: form.content,
|
||
nickname: form.nickname,
|
||
email: form.email,
|
||
articleid: form.articleid
|
||
})
|
||
|
||
if (res.success) {
|
||
ElMessage.success('留言成功')
|
||
fetchMessages() // 重新获取列表
|
||
resetForm()
|
||
} else {
|
||
ElMessage.error('留言失败:' + (res.message || '未知错误'))
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// 重置表单
|
||
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 post_comment_reply_cancel = () => {
|
||
form.content = ''
|
||
form.replyid = null
|
||
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
|
||
}
|
||
}
|
||
// 处理删除
|
||
const handleDelete = async (msg) => {
|
||
try {
|
||
// 显示加载状态或禁用按钮
|
||
// msg.isDeleting = true
|
||
if (!confirm('确定删除吗?')) {
|
||
return
|
||
}
|
||
const res = await messageService.deleteMessage(msg.messageid)
|
||
|
||
if (res.success) {
|
||
// 从列表中移除
|
||
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== msg.messageid)
|
||
ElMessage.success('删除成功')
|
||
} else {
|
||
ElMessage.error('删除失败:' + (res.message || '未知错误'))
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
} finally {
|
||
// msg.isDeleting = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* 主容器样式 */
|
||
.message-board-container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 留言列表容器 */
|
||
.message-list-wrapper {
|
||
margin-bottom: 24px;
|
||
background: #f8fafd;
|
||
border-radius: 12px;
|
||
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 {
|
||
background-color: #fff;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.comment-item-wrapper:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* 评论头部信息 */
|
||
.comment-header-info {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* 用户头像 */
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
margin-right: 12px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* 用户元信息 */
|
||
.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 {
|
||
text-align: right;
|
||
font-size: 14px;
|
||
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);
|
||
}
|
||
// 删除按钮
|
||
.delete-button {
|
||
cursor: pointer;
|
||
transition: color 0.3s ease;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
.delete-button:hover {
|
||
color: #e74c3c;
|
||
background-color: rgba(231, 76, 60, 0.1);
|
||
}
|
||
/* 回复列表容器 */
|
||
.reply-list-container {
|
||
margin-top: 16px;
|
||
padding-left: 52px;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding-top: 16px;
|
||
}
|
||
/**
|
||
* 内联表单输入行样式
|
||
* 用于将表单输入项与标签或其他元素对齐
|
||
*/
|
||
.form-input-row--inline {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.form-input-row--inline div:nth-child(2) {
|
||
margin-left: 9%;
|
||
margin-right: 9%;
|
||
}
|
||
|
||
/* 回复项容器 */
|
||
.reply-item-wrapper {
|
||
background-color: #fafafa;
|
||
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: #fff;
|
||
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 {
|
||
background: #f0f9ff;
|
||
border-left: 4px solid #409eff;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 回复预览文本 */
|
||
.reply-preview-text {
|
||
margin-top: 6px;
|
||
padding: 10px;
|
||
background-color: rgba(255, 255, 255, 0.8);
|
||
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;
|
||
}
|
||
|
||
.form-input-row {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.comment-header-info,
|
||
.reply-header-info {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.user-avatar {
|
||
margin-right: 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.reply-list-container {
|
||
padding-left: 20px;
|
||
}
|
||
}
|
||
</style> |