feat: 实现文章搜索功能并优化留言系统
- 添加文章标题搜索功能,支持通过路由参数搜索 - 重构留言板组件,优化留言嵌套结构和交互 - 新增评论演示页面展示嵌套留言功能 - 调整主布局样式和导航菜单路由 - 修复留言板样式问题和数据字段不一致问题
This commit is contained in:
@@ -145,7 +145,8 @@ onUnmounted(() => {
|
||||
background-color: rgba(0, 0, 0,0 ); /* 白色半透明背景 */
|
||||
}
|
||||
.cont2 .el-menu-vertical-demo ul li:hover{
|
||||
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
|
||||
background-color: rgba(220, 53, 69, 0.9); /* 红色半透明背景 */
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 分类列表样式 */
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-col :span="14" justify="center">
|
||||
<div class="grid-content ep-bg-purple-dark">
|
||||
<el-menu :default-active="activeIndex" class="el-menu-demo" :collapse="false" @select="handleSelect">
|
||||
<el-menu-item index="/:type">
|
||||
<el-menu-item index="/home">
|
||||
首页
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/article-list">
|
||||
@@ -28,7 +28,21 @@
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="2" class="search-container" v-if="windowwidth">
|
||||
<!-- 搜索框可以在这里添加 -->
|
||||
<!-- 搜索功能 -->
|
||||
<div class="search-wrapper">
|
||||
<button class="search-icon-btn" @click="toggleSearchBox" :class="{ 'active': isSearchBoxOpen }">
|
||||
<i class="el-icon-search">1</i>
|
||||
</button>
|
||||
<div class="search-box-container" :class="{ 'open': isSearchBoxOpen }">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文章..."
|
||||
class="search-input"
|
||||
@keyup.enter="performSearch"
|
||||
@blur="closeSearchBoxWithDelay"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -75,7 +89,14 @@ const isScrollingleftmodlue = ref(false);
|
||||
const elrowtop = ref('transparent');
|
||||
const classnonsenset = ref(false);
|
||||
const windowwidth = ref(true);
|
||||
const activeIndex = ref('/:type');
|
||||
const activeIndex = ref('/home');
|
||||
const localhome= '/home';
|
||||
|
||||
|
||||
// 搜索相关状态
|
||||
const isSearchBoxOpen = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
let searchCloseTimer: number | undefined;
|
||||
|
||||
// 打字机效果相关
|
||||
const fullHeroText = '测试打字机效果';
|
||||
@@ -107,11 +128,59 @@ const handleSelect = (key: string) => {
|
||||
router.push({ path: key });
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换搜索框显示/隐藏
|
||||
*/
|
||||
const toggleSearchBox = () => {
|
||||
isSearchBoxOpen.value = !isSearchBoxOpen.value;
|
||||
|
||||
// 如果打开搜索框,清除之前的延时关闭定时器
|
||||
if (isSearchBoxOpen.value && searchCloseTimer) {
|
||||
clearTimeout(searchCloseTimer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭搜索框
|
||||
*/
|
||||
const closeSearchBox = () => {
|
||||
isSearchBoxOpen.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 带延迟关闭搜索框(处理点击搜索按钮后的情况)
|
||||
*/
|
||||
const closeSearchBoxWithDelay = () => {
|
||||
if (searchCloseTimer) {
|
||||
clearTimeout(searchCloseTimer);
|
||||
}
|
||||
|
||||
searchCloseTimer = window.setTimeout(() => {
|
||||
isSearchBoxOpen.value = false;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行搜索操作
|
||||
*/
|
||||
const performSearch = () => {
|
||||
if (searchKeyword.value.trim()) {
|
||||
// 这里可以根据实际需求实现搜索逻辑
|
||||
console.log('搜索关键词:', searchKeyword.value);
|
||||
router.push({ path: `/home/aericletitle/${searchKeyword.value}`});
|
||||
}
|
||||
|
||||
// 搜索后保持搜索框打开状态
|
||||
if (searchCloseTimer) {
|
||||
clearTimeout(searchCloseTimer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据路由路径设置页面状态
|
||||
*/
|
||||
const updatePageState = (url: string) => {
|
||||
classhero.value = url !== '/:type';
|
||||
classhero.value = url !== localhome;
|
||||
classnonsenset.value = url === '/nonsense';
|
||||
};
|
||||
|
||||
@@ -129,7 +198,7 @@ const handleResize = () => {
|
||||
windowwidth.value = window.innerWidth > 768;
|
||||
|
||||
// 根据屏幕大小调整内容区可见性
|
||||
if (route.path === '/:type') {
|
||||
if (route.path === localhome) {
|
||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
||||
}
|
||||
};
|
||||
@@ -152,7 +221,7 @@ const handleScroll = () => {
|
||||
}
|
||||
|
||||
// 首页内容区滚动动画
|
||||
if (route.path === '/:type') {
|
||||
if (route.path === localhome) {
|
||||
isconts.value = window.scrollY > 200;
|
||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
||||
}
|
||||
@@ -169,13 +238,13 @@ watch(() => route.path, (newPath) => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
// 首页内容区滚动动画仅大屏下生效
|
||||
if (newPath === '/:type') {
|
||||
if (newPath === localhome) {
|
||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
||||
// 首页时启动打字机
|
||||
startTypewriter();
|
||||
} else {
|
||||
isconts.value = true;
|
||||
heroText.value = '';
|
||||
heroText.value =fullHeroText;
|
||||
if (heroTimer) clearInterval(heroTimer);
|
||||
}
|
||||
}, { immediate: true });
|
||||
@@ -185,6 +254,8 @@ watch(() => route.path, (newPath) => {
|
||||
*/
|
||||
onMounted(() => {
|
||||
// 初始化窗口大小
|
||||
|
||||
|
||||
handleResize();
|
||||
|
||||
// 添加事件监听器
|
||||
@@ -203,4 +274,80 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 搜索框样式 */
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
/* padding: 8px; */
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-icon-btn:hover,
|
||||
.search-icon-btn.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-box-container {
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
/* box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); */
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.search-box-container.open {
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
/* padding: 0 8px; */
|
||||
border: none;
|
||||
}
|
||||
|
||||
.search-btn,
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.search-btn:hover,
|
||||
.close-btn:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 防止搜索框在小屏幕上重叠 */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.search-box-container.open {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import NonsensePage from '../views/nonsense.vue'
|
||||
import MessageBoardPage from '../views/messageboard.vue'
|
||||
import AboutMePage from '../views/aboutme.vue'
|
||||
import ArticleContentPage from '../views/articlecontents.vue'
|
||||
import CommentDemoPage from '../views/commentDemo.vue'
|
||||
|
||||
/**
|
||||
* 路由配置数组
|
||||
@@ -14,16 +15,56 @@ import ArticleContentPage from '../views/articlecontents.vue'
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/all' // 默认跳转到首页,显示所有文章
|
||||
redirect: '/home' // 默认跳转到首页,显示所有文章
|
||||
},
|
||||
{
|
||||
path: '/:type',
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
path: '/comment-demo',
|
||||
name: 'commentDemo',
|
||||
component: CommentDemoPage,
|
||||
meta: {
|
||||
title: '首页'
|
||||
title: '嵌套留言Demo'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: HomePage,
|
||||
meta: { title: '首页' },
|
||||
children: [
|
||||
{
|
||||
path: 'aericletype/:type',
|
||||
name: 'homeByType'
|
||||
},
|
||||
{
|
||||
path: 'aericletitle/:title',
|
||||
name: 'homeByTitle'
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/home',
|
||||
// name: 'home',
|
||||
// component: HomePage,
|
||||
// meta: {
|
||||
// title: '首页'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: '/home/aericletype/:type',
|
||||
// name: 'homeByType',
|
||||
// component: HomePage,
|
||||
// meta: {
|
||||
// title: '首页'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: '/home/aericletitle/:title',
|
||||
// name: 'homeByTitle',
|
||||
// component: HomePage,
|
||||
// meta: {
|
||||
// title: '首页'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: '/article-list',
|
||||
name: 'articleList',
|
||||
|
||||
@@ -23,6 +23,14 @@ class ArticleService {
|
||||
return apiService.get(`/articles/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标题查询文章列表
|
||||
* @param {string} title - 文章标题
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getArticlesByTitle(title) {
|
||||
return apiService.get(`/articles/title/${title}`)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
:root {
|
||||
/* 页面通用间距和圆角 */
|
||||
--main-padding: 100px 10%;
|
||||
--main-padding: 8px 10%;
|
||||
/* 内容区内边距 */
|
||||
--main-radius: 10px;
|
||||
/* 内容区圆角 */
|
||||
@@ -132,7 +132,7 @@ p {
|
||||
font-family: 'Microsoft YaHei', 'Ma Shan Zheng', cursive;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(94.75deg, rgb(60, 172, 247) 0%, rgb(131, 101, 253) 43.66%, rgb(255, 141, 112) 64.23%, rgb(247, 201, 102) 83.76%, rgb(172, 143, 100) 100%);
|
||||
background: linear-gradient(94.75deg, rgb(60, 172, 247) 0%, rgb(131, 101, 253) 43.66%, rgb(255, 141, 112) 64.23%, rgb(247, 201, 102) 83.76%, rgb(172, 143, 100) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
@@ -258,6 +258,7 @@ p {
|
||||
/* hero 收缩状态 */
|
||||
.hero.newhero {
|
||||
height: var(--hero-height-small);
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
|
||||
@@ -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