feat: 新增疯言疯语功能并优化UI样式

- 添加疯言疯语服务及页面,支持随机字符颜色变化效果
- 引入汉仪唐韵字体并优化全局字体设置
- 重构日期工具函数,优化时间显示格式
- 改进左侧模块布局,添加文章/分类/标签统计
- 优化浮动按钮组件,增加动态过渡效果
- 调整多个页面的背景透明度,提升视觉一致性
- 完善文章保存页面样式和交互逻辑
- 更新关于页面内容,增加个人介绍和技术栈展示
- 修复路由状态管理问题,优化页面跳转逻辑
This commit is contained in:
qingfeng1121
2025-11-05 16:11:46 +08:00
parent a927ad5a4d
commit ad893b3e5c
19 changed files with 1226 additions and 600 deletions

View File

@@ -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;
}