feat: 实现文章搜索功能并优化留言系统

- 添加文章标题搜索功能,支持通过路由参数搜索
- 重构留言板组件,优化留言嵌套结构和交互
- 新增评论演示页面展示嵌套留言功能
- 调整主布局样式和导航菜单路由
- 修复留言板样式问题和数据字段不一致问题
This commit is contained in:
qingfeng1121
2025-10-16 16:11:58 +08:00
parent ed09611d02
commit 266310dea3
9 changed files with 501 additions and 33 deletions

View File

@@ -90,7 +90,7 @@ const fetchCategories = async () => {
*/
const handleCategoryClick = (typeid: string) => {
router.push({
path: '/:type',
path: '/home/aericletype',
query: {
type: typeid
}

262
src/views/commentDemo.vue Normal file
View File

@@ -0,0 +1,262 @@
<template>
<div class="comment-demo-container">
<h1>嵌套留言Demo</h1>
<!-- 留言列表 -->
<div class="comments-wrapper">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="commentTree.length === 0" class="empty">暂无留言</div>
<div v-else class="comment-list">
<!-- 递归渲染留言树 -->
<CommentItem
v-for="comment in commentTree"
:key="comment.id"
:comment="comment"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, defineComponent, h } from 'vue'
// 模拟留言数据
const mockComments = [
{
id: 'a',
content: '这是主留言A',
parentId: null,
author: '用户A',
createdAt: new Date().toISOString()
},
{
id: 'b',
content: '这是回复A的留言B',
parentId: 'a',
author: '用户B',
createdAt: new Date(Date.now() + 1000 * 60 * 5).toISOString() // 5分钟后
},
{
id: 'c',
content: '这是回复B的留言C',
parentId: 'b',
author: '用户C',
createdAt: new Date(Date.now() + 1000 * 60 * 10).toISOString() // 10分钟后
},
{
id: 'd',
content: '这是另一个主留言D',
parentId: null,
author: '用户D',
createdAt: new Date(Date.now() + 1000 * 60 * 15).toISOString() // 15分钟后
},
{
id: 'e',
content: '这是回复D的留言E',
parentId: 'd',
author: '用户E',
createdAt: new Date(Date.now() + 1000 * 60 * 20).toISOString() // 20分钟后
},
{
id: 'f',
content: '这是回复A的另一条留言F',
parentId: 'a',
author: '用户F',
createdAt: new Date(Date.now() + 1000 * 60 * 25).toISOString() // 25分钟后
},
{
id: 'g',
content: '这是回复C的留言G三级嵌套',
parentId: 'c',
author: '用户G',
createdAt: new Date(Date.now() + 1000 * 60 * 30).toISOString() // 30分钟后
}
]
// 响应式状态
const loading = ref(false)
const commentTree = ref([])
// 递归构建评论树结构的函数
const buildCommentTree = (comments) => {
// 创建评论ID到评论对象的映射方便快速查找
const commentMap = {}
comments.forEach(comment => {
commentMap[comment.id] = { ...comment, children: [] }
})
// 构建树结构
const roots = []
comments.forEach(comment => {
if (!comment.parentId) {
// 没有parentId的是根节点
roots.push(commentMap[comment.id])
} else {
// 有parentId的是子节点添加到父节点的children数组中
if (commentMap[comment.parentId]) {
commentMap[comment.parentId].children.push(commentMap[comment.id])
}
}
})
return roots
}
// 获取留言数据
const fetchComments = async () => {
try {
loading.value = true
// 模拟异步请求
await new Promise(resolve => setTimeout(resolve, 500))
// 处理数据,构建树结构
commentTree.value = buildCommentTree(mockComments)
console.log('构建的留言树:', commentTree.value)
} catch (error) {
console.error('获取留言失败:', error)
} finally {
loading.value = false
}
}
// 留言项组件(递归组件)
const CommentItem = defineComponent({
name: 'CommentItem',
props: {
comment: {
type: Object,
required: true
}
},
setup(props) {
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
return () => h('div', { class: 'comment-item' }, [
h('div', { class: 'comment-header' }, [
h('span', { class: 'comment-author' }, props.comment.author),
h('span', { class: 'comment-time' }, formatDate(props.comment.createdAt))
]),
h('div', { class: 'comment-content' }, props.comment.content),
// 递归渲染子留言
props.comment.children && props.comment.children.length > 0 ?
h('div', { class: 'comment-children' },
props.comment.children.map(comment =>
h(CommentItem, { key: comment.id, comment })
)
) : null
])
}
})
// 组件挂载时获取数据
onMounted(() => {
fetchComments()
})
</script>
<style scoped>
.comment-demo-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.comments-wrapper {
background-color: #f9f9f9;
border-radius: 8px;
padding: 20px;
}
.loading, .empty {
text-align: center;
padding: 40px;
color: #666;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.comment-item {
background-color: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.comment-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.comment-author {
font-weight: 600;
color: #333;
}
.comment-time {
font-size: 14px;
color: #999;
}
.comment-content {
color: #555;
line-height: 1.6;
word-break: break-word;
}
.comment-children {
margin-top: 16px;
margin-left: 30px;
padding-top: 16px;
border-top: 1px solid #eee;
display: flex;
flex-direction: column;
gap: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.comment-demo-container {
padding: 10px;
}
.comments-wrapper {
padding: 15px;
}
.comment-children {
margin-left: 15px;
}
}
</style>

View File

@@ -47,17 +47,31 @@ import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
// 响应式状态
const datas = ref([])
const loading = ref(false)
/**
* 获取文章列表
*/
const fetchArticles = async () => {
try {
try {
loading.value = true
const res = await articleService.getAllArticles()
let res = {} // 使用let而不是const
// 使用route对象获取参数而不是检查pathname
if (route.params.type ) {
console.log('根据type获取文章列表成功')
res = await articleService.getAllArticlesByType(route.params.type)
} else if (route.params.title) {
res = await articleService.getAllArticlesByTitle(route.params.title)
console.log('根据title获取文章列表成功')
} else {
res = await articleService.getAllArticles()
console.log('获取所有文章列表成功')
}
datas.value = res.data || []
console.log('获取文章列表成功:', datas.value)
} catch (error) {
@@ -65,10 +79,10 @@ const fetchArticles = async () => {
ElMessage.error('获取文章列表失败,请稍后重试')
} finally {
loading.value = false
console.log('文章列表加载完成')
}
}
/**
* 处理文章点击事件
* @param {Object} article - 文章对象

View File

@@ -67,9 +67,9 @@
<span class="article-title">{{ articleGroup.articleTitle }}</span>
</div>
<div class="article-message-content">
<div v-for="mainMsg in articleGroup.messages" :key="mainMsg.id" class="message-tree">
<div v-for="mainMsg in articleGroup.messages" :key="mainMsg.messageid" class="message-tree">
<!-- 主留言 -->
<div class="message-item" @mouseenter="hoverId = mainMsg.id"
<div class="message-item" @mouseenter="hoverId = mainMsg.messageid"
@mouseleave="hoverId = null">
<div class="message-avatar-container">
<img :src="getAvatar(mainMsg.email)" alt="头像" class="message-avatar" />
@@ -84,13 +84,13 @@
<!-- 回复按钮 -->
<div class="message-item-bottom">
<button class="reply-btn" @click="handleReply(mainMsg)"
:class="{ visible: hoverId === mainMsg.id }">
:class="{ visible: hoverId === mainMsg.messageid }">
回复
</button>
</div>
<!-- 回复列表 -->
<div v-if="mainMsg.replies && mainMsg.replies.length" class="replies">
<div v-for="reply in mainMsg.replies" :key="reply.id"
<div v-for="reply in mainMsg.replies" :key="reply.messageid"
class="reply-item">
<div class="message-avatar-container">
<img :src="getAvatar(reply.email)" alt="头像"
@@ -257,11 +257,11 @@ const fetchMessages = async () => {
// 处理回复
const handleReply = (msg) => {
replyingTo.value = {
id: msg.id,
id: msg.messageid,
nickname: msg.nickname || '匿名用户',
content: msg.content
}
form.replyid = msg.id
form.parentid = msg.messageid
form.content = `@${replyingTo.value.nickname} `
// 滚动到输入框
setTimeout(() => {
@@ -282,11 +282,10 @@ onMounted(() => {
})
const form = reactive({
replyid: null,
parentid: null,
content: '',
nickname: '',
email: '',
captcha: ''
})
const onSubmit = async () => {
@@ -358,7 +357,6 @@ const post_comment_reply_cancel = () => {
.message-board {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.message-list {
@@ -580,10 +578,6 @@ const post_comment_reply_cancel = () => {
}
}
.message-board {
padding: 24px 0;
}
.message-list {
margin-bottom: 24px;
background: #f8fafd;