feat: 实现文章搜索功能并优化留言系统
- 添加文章标题搜索功能,支持通过路由参数搜索 - 重构留言板组件,优化留言嵌套结构和交互 - 新增评论演示页面展示嵌套留言功能 - 调整主布局样式和导航菜单路由 - 修复留言板样式问题和数据字段不一致问题
This commit is contained in:
@@ -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
262
src/views/commentDemo.vue
Normal 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>
|
||||
@@ -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 - 文章对象
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user