Compare commits
2 Commits
b8362e7835
...
266310dea3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
266310dea3 | ||
|
|
ed09611d02 |
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<el-menu-item index="/article-list">
|
<el-menu-item index="/article-list">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>文章</span>
|
<span>目录</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/nonsense">
|
<el-menu-item index="/nonsense">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
@@ -145,7 +145,8 @@ onUnmounted(() => {
|
|||||||
background-color: rgba(0, 0, 0,0 ); /* 白色半透明背景 */
|
background-color: rgba(0, 0, 0,0 ); /* 白色半透明背景 */
|
||||||
}
|
}
|
||||||
.cont2 .el-menu-vertical-demo ul li:hover{
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分类列表样式 */
|
/* 分类列表样式 */
|
||||||
|
|||||||
210
src/index.vue
210
src/index.vue
@@ -1,210 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="elrow-top" :class="elrowtop">
|
|
||||||
<el-row justify="center">
|
|
||||||
<el-col :span="4" v-if="windowwidth">
|
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
|
||||||
<div>清疯不颠</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<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>
|
|
||||||
<el-menu-item index="/aericle">
|
|
||||||
文章
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/nonsense">
|
|
||||||
疯言疯语
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/about">
|
|
||||||
关于
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/message">
|
|
||||||
留言板
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="2" class="search-container" v-if="windowwidth">
|
|
||||||
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
<div class="hero" :class="{ 'newhero': classhero }" v-if="windowwidth">
|
|
||||||
<h1 class="typewriter">{{ heroText }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content-section" :class="{ 'visible': isconts }">
|
|
||||||
<div class="nonsensetitle" v-if="classnonsenset">
|
|
||||||
<div class="nonsensetitleconst">
|
|
||||||
<h1>发癫中QAQ</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 状态模块 -->
|
|
||||||
<leftmodlue class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
|
|
||||||
<!-- 内容模块 -->
|
|
||||||
<RouterView class="RouterViewpage" :class="{ 'nonsensetmargintop': classnonsenset }" />
|
|
||||||
</div>
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="Pagination">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
// import leftmodlue from '@/views/leftmodlue.vue';
|
|
||||||
|
|
||||||
// hero 区域样式控制
|
|
||||||
const classhero = ref(false);
|
|
||||||
// 内容区可见性
|
|
||||||
const isconts = ref(false);
|
|
||||||
// 左侧模块滚动状态
|
|
||||||
const isScrollingleftmodlue = ref(false);
|
|
||||||
// 导航栏样式类名
|
|
||||||
const elrowtop = ref('transparent');
|
|
||||||
// 疯言疯语标题区显示
|
|
||||||
const classnonsenset = ref(false);
|
|
||||||
// 屏幕宽度标记(true为大屏,false为小屏)
|
|
||||||
const windowwidth = ref(true);
|
|
||||||
// 当前激活菜单
|
|
||||||
const activeIndex = ref('/:type');
|
|
||||||
|
|
||||||
// 打字机效果相关
|
|
||||||
const fullHeroText = '如果感到累了撸一管就好了';
|
|
||||||
const heroText = ref('');
|
|
||||||
let heroIndex = 0;
|
|
||||||
let heroTimer: number | undefined;
|
|
||||||
|
|
||||||
const startTypewriter = () => {
|
|
||||||
heroText.value = '';
|
|
||||||
heroIndex = 0;
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
heroTimer = window.setInterval(() => {
|
|
||||||
if (heroIndex < fullHeroText.length) {
|
|
||||||
heroText.value += fullHeroText[heroIndex];
|
|
||||||
heroIndex++;
|
|
||||||
} else {
|
|
||||||
clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单选择跳转
|
|
||||||
*/
|
|
||||||
const handleSelect = (key: string) => {
|
|
||||||
router.push({ path: key });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据路由路径设置页面状态
|
|
||||||
*/
|
|
||||||
const updatePageState = (url: string) => {
|
|
||||||
classhero.value = url !== '/:type';
|
|
||||||
classnonsenset.value = url == '/nonsense';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路由切换时处理页面状态和滚动
|
|
||||||
*/
|
|
||||||
router.beforeEach((to) => {
|
|
||||||
updatePageState(to.path);
|
|
||||||
setActiveIndex(to.path);
|
|
||||||
// 跳转后回到顶部
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
// 首页内容区滚动动画仅大屏下生效
|
|
||||||
if (to.path === '/:type') {
|
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
|
||||||
// 首页时启动打字机
|
|
||||||
startTypewriter();
|
|
||||||
} else {
|
|
||||||
isconts.value = true;
|
|
||||||
heroText.value = '';
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滚动事件处理
|
|
||||||
*/
|
|
||||||
const handleScroll = () => {
|
|
||||||
// 屏幕小于768时只切换导航栏样式,不做内容动画
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 导航栏样式切换
|
|
||||||
if (window.scrollY > 1200) {
|
|
||||||
elrowtop.value = 'hide';
|
|
||||||
} else {
|
|
||||||
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
|
|
||||||
}
|
|
||||||
// 首页内容区滚动动画
|
|
||||||
if (location.pathname === '/:type') {
|
|
||||||
isconts.value = window.scrollY > 200;
|
|
||||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
|
||||||
} else {
|
|
||||||
isconts.value = true;
|
|
||||||
isScrollingleftmodlue.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加滚动监听
|
|
||||||
*/
|
|
||||||
const addScrollListener = () => {
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除滚动监听
|
|
||||||
*/
|
|
||||||
const removeScrollListener = () => {
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 屏幕尺寸变化处理
|
|
||||||
* 小屏时移除滚动监听并设置相关状态
|
|
||||||
* 大屏时添加滚动监听
|
|
||||||
*/
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
windowwidth.value = false;
|
|
||||||
isScrollingleftmodlue.value = true;
|
|
||||||
classnonsenset.value = false;
|
|
||||||
isconts.value = true;
|
|
||||||
} else {
|
|
||||||
windowwidth.value = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前激活菜单
|
|
||||||
*/
|
|
||||||
const setActiveIndex = (locationpathname) => {
|
|
||||||
activeIndex.value = locationpathname;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生命周期钩子
|
|
||||||
onMounted(() => {
|
|
||||||
handleResize(); addScrollListener();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
// 初始进入时如果是首页,启动打字机
|
|
||||||
if (window.location.pathname === '/:type') {
|
|
||||||
startTypewriter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// removeScrollListener();
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style></style>
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<el-col :span="14" justify="center">
|
<el-col :span="14" justify="center">
|
||||||
<div class="grid-content ep-bg-purple-dark">
|
<div class="grid-content ep-bg-purple-dark">
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" :collapse="false" @select="handleSelect">
|
<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>
|
||||||
<el-menu-item index="/article-list">
|
<el-menu-item index="/article-list">
|
||||||
@@ -28,7 +28,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="search-container" v-if="windowwidth">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,10 +89,17 @@ const isScrollingleftmodlue = ref(false);
|
|||||||
const elrowtop = ref('transparent');
|
const elrowtop = ref('transparent');
|
||||||
const classnonsenset = ref(false);
|
const classnonsenset = ref(false);
|
||||||
const windowwidth = ref(true);
|
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 = '如果感到累了撸一管就好了';
|
const fullHeroText = '测试打字机效果';
|
||||||
const heroText = ref('');
|
const heroText = ref('');
|
||||||
let heroIndex = 0;
|
let heroIndex = 0;
|
||||||
let heroTimer: number | undefined;
|
let heroTimer: number | undefined;
|
||||||
@@ -107,11 +128,59 @@ const handleSelect = (key: string) => {
|
|||||||
router.push({ path: key });
|
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) => {
|
const updatePageState = (url: string) => {
|
||||||
classhero.value = url !== '/:type';
|
classhero.value = url !== localhome;
|
||||||
classnonsenset.value = url === '/nonsense';
|
classnonsenset.value = url === '/nonsense';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,7 +198,7 @@ const handleResize = () => {
|
|||||||
windowwidth.value = window.innerWidth > 768;
|
windowwidth.value = window.innerWidth > 768;
|
||||||
|
|
||||||
// 根据屏幕大小调整内容区可见性
|
// 根据屏幕大小调整内容区可见性
|
||||||
if (route.path === '/:type') {
|
if (route.path === localhome) {
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
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;
|
isconts.value = window.scrollY > 200;
|
||||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
isScrollingleftmodlue.value = window.scrollY > 600;
|
||||||
}
|
}
|
||||||
@@ -169,13 +238,13 @@ watch(() => route.path, (newPath) => {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
|
||||||
// 首页内容区滚动动画仅大屏下生效
|
// 首页内容区滚动动画仅大屏下生效
|
||||||
if (newPath === '/:type') {
|
if (newPath === localhome) {
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
isconts.value = window.innerWidth <= 768 ? true : false;
|
||||||
// 首页时启动打字机
|
// 首页时启动打字机
|
||||||
startTypewriter();
|
startTypewriter();
|
||||||
} else {
|
} else {
|
||||||
isconts.value = true;
|
isconts.value = true;
|
||||||
heroText.value = '';
|
heroText.value =fullHeroText;
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
if (heroTimer) clearInterval(heroTimer);
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
@@ -185,6 +254,8 @@ watch(() => route.path, (newPath) => {
|
|||||||
*/
|
*/
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化窗口大小
|
// 初始化窗口大小
|
||||||
|
|
||||||
|
|
||||||
handleResize();
|
handleResize();
|
||||||
|
|
||||||
// 添加事件监听器
|
// 添加事件监听器
|
||||||
@@ -203,4 +274,80 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
@@ -6,6 +6,7 @@ import NonsensePage from '../views/nonsense.vue'
|
|||||||
import MessageBoardPage from '../views/messageboard.vue'
|
import MessageBoardPage from '../views/messageboard.vue'
|
||||||
import AboutMePage from '../views/aboutme.vue'
|
import AboutMePage from '../views/aboutme.vue'
|
||||||
import ArticleContentPage from '../views/articlecontents.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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/all' // 默认跳转到首页,显示所有文章
|
redirect: '/home' // 默认跳转到首页,显示所有文章
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:type',
|
path: '/comment-demo',
|
||||||
name: 'home',
|
name: 'commentDemo',
|
||||||
component: HomePage,
|
component: CommentDemoPage,
|
||||||
meta: {
|
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',
|
path: '/article-list',
|
||||||
name: 'articleList',
|
name: 'articleList',
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ class ArticleService {
|
|||||||
return apiService.get(`/articles/${id}`)
|
return apiService.get(`/articles/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标题查询文章列表
|
||||||
|
* @param {string} title - 文章标题
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getArticlesByTitle(title) {
|
||||||
|
return apiService.get(`/articles/title/${title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
/* 页面通用间距和圆角 */
|
/* 页面通用间距和圆角 */
|
||||||
--main-padding: 100px 10%;
|
--main-padding: 8px 10%;
|
||||||
/* 内容区内边距 */
|
/* 内容区内边距 */
|
||||||
--main-radius: 10px;
|
--main-radius: 10px;
|
||||||
/* 内容区圆角 */
|
/* 内容区圆角 */
|
||||||
@@ -132,7 +132,7 @@ p {
|
|||||||
font-family: 'Microsoft YaHei', 'Ma Shan Zheng', cursive;
|
font-family: 'Microsoft YaHei', 'Ma Shan Zheng', cursive;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
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;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
@@ -258,6 +258,7 @@ p {
|
|||||||
/* hero 收缩状态 */
|
/* hero 收缩状态 */
|
||||||
.hero.newhero {
|
.hero.newhero {
|
||||||
height: var(--hero-height-small);
|
height: var(--hero-height-small);
|
||||||
|
margin-top: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 打字机效果 */
|
/* 打字机效果 */
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const fetchCategories = async () => {
|
|||||||
*/
|
*/
|
||||||
const handleCategoryClick = (typeid: string) => {
|
const handleCategoryClick = (typeid: string) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/:type',
|
path: '/home/aericletype',
|
||||||
query: {
|
query: {
|
||||||
type: typeid
|
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 router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const datas = ref([])
|
const datas = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文章列表
|
* 获取文章列表
|
||||||
*/
|
*/
|
||||||
const fetchArticles = async () => {
|
const fetchArticles = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
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 || []
|
datas.value = res.data || []
|
||||||
console.log('获取文章列表成功:', datas.value)
|
console.log('获取文章列表成功:', datas.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -65,10 +79,10 @@ const fetchArticles = async () => {
|
|||||||
ElMessage.error('获取文章列表失败,请稍后重试')
|
ElMessage.error('获取文章列表失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
console.log('文章列表加载完成')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理文章点击事件
|
* 处理文章点击事件
|
||||||
* @param {Object} article - 文章对象
|
* @param {Object} article - 文章对象
|
||||||
|
|||||||
@@ -67,9 +67,9 @@
|
|||||||
<span class="article-title">{{ articleGroup.articleTitle }}</span>
|
<span class="article-title">{{ articleGroup.articleTitle }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="article-message-content">
|
<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">
|
@mouseleave="hoverId = null">
|
||||||
<div class="message-avatar-container">
|
<div class="message-avatar-container">
|
||||||
<img :src="getAvatar(mainMsg.email)" alt="头像" class="message-avatar" />
|
<img :src="getAvatar(mainMsg.email)" alt="头像" class="message-avatar" />
|
||||||
@@ -84,13 +84,13 @@
|
|||||||
<!-- 回复按钮 -->
|
<!-- 回复按钮 -->
|
||||||
<div class="message-item-bottom">
|
<div class="message-item-bottom">
|
||||||
<button class="reply-btn" @click="handleReply(mainMsg)"
|
<button class="reply-btn" @click="handleReply(mainMsg)"
|
||||||
:class="{ visible: hoverId === mainMsg.id }">
|
:class="{ visible: hoverId === mainMsg.messageid }">
|
||||||
回复
|
回复
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 回复列表 -->
|
<!-- 回复列表 -->
|
||||||
<div v-if="mainMsg.replies && mainMsg.replies.length" class="replies">
|
<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">
|
class="reply-item">
|
||||||
<div class="message-avatar-container">
|
<div class="message-avatar-container">
|
||||||
<img :src="getAvatar(reply.email)" alt="头像"
|
<img :src="getAvatar(reply.email)" alt="头像"
|
||||||
@@ -257,11 +257,11 @@ const fetchMessages = async () => {
|
|||||||
// 处理回复
|
// 处理回复
|
||||||
const handleReply = (msg) => {
|
const handleReply = (msg) => {
|
||||||
replyingTo.value = {
|
replyingTo.value = {
|
||||||
id: msg.id,
|
id: msg.messageid,
|
||||||
nickname: msg.nickname || '匿名用户',
|
nickname: msg.nickname || '匿名用户',
|
||||||
content: msg.content
|
content: msg.content
|
||||||
}
|
}
|
||||||
form.replyid = msg.id
|
form.parentid = msg.messageid
|
||||||
form.content = `@${replyingTo.value.nickname} `
|
form.content = `@${replyingTo.value.nickname} `
|
||||||
// 滚动到输入框
|
// 滚动到输入框
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -282,11 +282,10 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
replyid: null,
|
parentid: null,
|
||||||
content: '',
|
content: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
email: '',
|
email: '',
|
||||||
captcha: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@@ -358,7 +357,6 @@ const post_comment_reply_cancel = () => {
|
|||||||
.message-board {
|
.message-board {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
@@ -580,10 +578,6 @@ const post_comment_reply_cancel = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-board {
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
background: #f8fafd;
|
background: #f8fafd;
|
||||||
|
|||||||
Reference in New Issue
Block a user