feat: 新增疯言疯语功能并优化UI样式
- 添加疯言疯语服务及页面,支持随机字符颜色变化效果 - 引入汉仪唐韵字体并优化全局字体设置 - 重构日期工具函数,优化时间显示格式 - 改进左侧模块布局,添加文章/分类/标签统计 - 优化浮动按钮组件,增加动态过渡效果 - 调整多个页面的背景透明度,提升视觉一致性 - 完善文章保存页面样式和交互逻辑 - 更新关于页面内容,增加个人介绍和技术栈展示 - 修复路由状态管理问题,优化页面跳转逻辑
This commit is contained in:
@@ -14,7 +14,12 @@
|
||||
<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="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>
|
||||
@@ -23,16 +28,26 @@
|
||||
<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 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-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="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>
|
||||
@@ -41,12 +56,14 @@
|
||||
<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 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>
|
||||
<span class="delete-button" @click="handleDelete(reply.messageid)"
|
||||
v-if="globalStore.Login">删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,14 +101,8 @@
|
||||
</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"
|
||||
/>
|
||||
<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>
|
||||
@@ -122,10 +133,10 @@ import { formatDate } from '@/utils/dateUtils'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
comments: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
comments: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
const globalStore = useGlobalStore()
|
||||
const messageBoardData = ref([]) // 留言板留言(articleid为空的主留言及其回复)
|
||||
@@ -152,13 +163,13 @@ const form = reactive({
|
||||
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
|
||||
@@ -170,7 +181,7 @@ const generateCaptcha = () => {
|
||||
captchaAnswer.value = (larger - smaller).toString()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
captchaHint.value = `${num1} ${operator} ${num2} = ?`
|
||||
captchaAnswer.value = answer.toString()
|
||||
} else {
|
||||
@@ -227,10 +238,53 @@ const rules = {
|
||||
}
|
||||
|
||||
|
||||
// 生成头像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`
|
||||
// 获取Gravatar头像URL
|
||||
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库)
|
||||
// 注意:在实际项目中应该使用正确的MD5哈希函数
|
||||
return `https://www.gravatar.com/avatar/${email}?d=404&s=40`;
|
||||
}
|
||||
|
||||
// 获取名字的首字母
|
||||
const getInitialLetter = (name) => {
|
||||
if (!name || typeof name !== 'string') return '?';
|
||||
|
||||
// 移除可能的@回复前缀
|
||||
const cleanName = name.replace(/^.+@\s*/, '');
|
||||
|
||||
// 获取第一个字符
|
||||
const firstChar = cleanName.charAt(0).toUpperCase();
|
||||
|
||||
return firstChar;
|
||||
}
|
||||
|
||||
// 获取首字母头像的样式
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
// 从后端获取留言列表
|
||||
@@ -238,18 +292,18 @@ 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)
|
||||
@@ -260,26 +314,26 @@ const fetchMessages = async () => {
|
||||
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)
|
||||
@@ -287,7 +341,7 @@ const fetchMessages = async () => {
|
||||
mainMessages.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 将回复添加到对应的主留言中
|
||||
replies.forEach(reply => {
|
||||
// 找到父留言
|
||||
@@ -297,7 +351,7 @@ const fetchMessages = async () => {
|
||||
if (reply.replyid) {
|
||||
const repliedMsg = replies.find(msg => msg.messageid === reply.replyid)
|
||||
if (repliedMsg) {
|
||||
reply.displayName = `${reply.nickname}@${repliedMsg.nickname}`
|
||||
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
|
||||
} else {
|
||||
reply.displayName = reply.nickname
|
||||
}
|
||||
@@ -307,7 +361,7 @@ const fetchMessages = async () => {
|
||||
parentMsg.replies.push(reply)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 更新留言板数据
|
||||
messageBoardData.value = mainMessages
|
||||
} catch (error) {
|
||||
@@ -365,7 +419,7 @@ onMounted(() => {
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
|
||||
// 表单验证
|
||||
await formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
@@ -444,9 +498,9 @@ 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
|
||||
@@ -470,7 +524,7 @@ const handleDelete = async (msg) => {
|
||||
return
|
||||
}
|
||||
const res = await messageService.deleteMessage(msg.messageid)
|
||||
|
||||
|
||||
if (res.success) {
|
||||
// 从列表中移除
|
||||
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== msg.messageid)
|
||||
@@ -497,11 +551,12 @@ const handleDelete = async (msg) => {
|
||||
/* 留言列表容器 */
|
||||
.message-list-wrapper {
|
||||
margin-bottom: 24px;
|
||||
background: #f8fafd;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
padding: 16px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* 留言板标题 */
|
||||
.message-board-title {
|
||||
color: #2c3e50;
|
||||
@@ -519,23 +574,17 @@ const handleDelete = async (msg) => {
|
||||
|
||||
/* 评论列表容器 */
|
||||
.comment-list-container {
|
||||
background-color: #f5f5f5;
|
||||
// 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 {
|
||||
@@ -544,15 +593,37 @@ const handleDelete = async (msg) => {
|
||||
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%;
|
||||
margin-right: 12px;
|
||||
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;
|
||||
@@ -584,8 +655,10 @@ const handleDelete = async (msg) => {
|
||||
|
||||
/* 评论操作栏 */
|
||||
.comment-actions-bar {
|
||||
padding-bottom: 8px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@@ -627,6 +700,7 @@ const handleDelete = async (msg) => {
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
.delete-button {
|
||||
cursor: pointer;
|
||||
@@ -635,17 +709,19 @@ const handleDelete = async (msg) => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内联表单输入行样式
|
||||
* 用于将表单输入项与标签或其他元素对齐
|
||||
@@ -654,6 +730,7 @@ const handleDelete = async (msg) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-input-row--inline div:nth-child(2) {
|
||||
margin-left: 9%;
|
||||
margin-right: 9%;
|
||||
@@ -661,7 +738,7 @@ const handleDelete = async (msg) => {
|
||||
|
||||
/* 回复项容器 */
|
||||
.reply-item-wrapper {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
@@ -669,8 +746,8 @@ const handleDelete = async (msg) => {
|
||||
}
|
||||
|
||||
.reply-item-wrapper:hover {
|
||||
background-color: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
// background-color: #f0f0f0;
|
||||
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 回复头部信息 */
|
||||
@@ -700,13 +777,13 @@ const handleDelete = async (msg) => {
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
padding: 32px 0;
|
||||
background-color: #f8f9fa;
|
||||
// background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 评论表单区域 */
|
||||
.comment-form-section {
|
||||
background: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
@@ -721,7 +798,6 @@ const handleDelete = async (msg) => {
|
||||
|
||||
/* 回复预览容器 */
|
||||
.reply-preview-container {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #409eff;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
@@ -733,7 +809,6 @@ const handleDelete = async (msg) => {
|
||||
.reply-preview-text {
|
||||
margin-top: 6px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 4px;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
@@ -794,31 +869,31 @@ const handleDelete = async (msg) => {
|
||||
.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user