Files
MyfronyProject/src/components/LeftModule.vue
qingfeng1121 309aeaedc1 feat: 实现文章状态管理及分类标签展示功能
新增文章状态管理功能,支持草稿、已发表和已删除状态的显示与切换
重构分类和标签展示模块,添加点击跳转功能
优化文章列表页面,增加状态筛选和分页功能
完善疯言疯语模块,支持编辑和删除操作
修复路由跳转和页面刷新问题
2025-11-08 11:16:15 +08:00

647 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="alld">
<div id="top">
<div class="top1">
<h3>小颠公告栏</h3>
</div>
<div class="top2">
<p>站主发癫中请勿靠近</p>
</div>
</div>
<div id="cont">
<div class="cont1">
<h3>小颠片刻</h3>
<p>左眼右右眼左四十五度成就美</p>
</div>
<div class="cont2">
<el-menu :default-active="activeIndex" class="el-menu-vertical-demo" @select="handleSelect">
<el-menu-item index="/home">
<el-icon>
</el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/articlelist">
<el-icon>
</el-icon>
<span>目录</span>
</el-menu-item>
<el-menu-item index="/nonsense">
<el-icon>
</el-icon>
<span>疯言疯语</span>
</el-menu-item>
</el-menu>
</div>
</div>
<div id="bot" :class="{ 'botrelative': scrollY }">
<el-tabs v-model="activeName" stretch="true" class="demo-tabs">
<el-tab-pane label="个人简介" name="first">
<div class="mylogo">
<el-avatar class="mylogo_avatar" :src="state.circleUrl" />
</div>
<a href="#">
<h6 class="mylogo_name logo-text">清疯不颠</h6>
</a>
<h6 class="mylogo_description">重度精神失常患者</h6>
<div class="stat-container">
<div>
<a href="#" class="stat-link">
<span class="site-state-item-count">{{ articleCount }}</span>
<span class="site-state-item-name">文章</span>
</a>
</div>
<div>
<a href="#" class="stat-link" @click.prevent="showCategories">
<span class="site-state-item-count">{{ categoryCount }}</span>
<span class="site-state-item-name">分类</span>
</a>
</div>
<div>
<a href="#" class="stat-link" @click.prevent="showAttributes">
<span class="site-state-item-count">{{ AttributeCount }}</span>
<span class="site-state-item-name">标签</span>
</a>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="功能" name="second">
</el-tab-pane>
</el-tabs>
</div>
<!-- 分类蒙板组件 -->
<Transition name="modal">
<div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有分类</h3>
<button class="category-modal-close" @click="closeCategoryModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="category in categories"
:key="category.typeid"
class="category-button"
@click="handleCategoryClick(category)"
>
{{ category.typename }} <span class="category-button-count">({{ category.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
<!-- 标签蒙板组件 -->
<Transition name="modal">
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有标签</h3>
<button class="category-modal-close" @click="closeAttributeModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="attribute in attributes"
:key="attribute.attributeid"
class="category-button"
@click="handleAttributeClick(attribute)"
>
{{ attribute.attributename }} <span class="category-button-count">({{ attribute.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { articleService, categoryService, categoryAttributeService } from "@/services";
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 当前激活菜单
const activeIndex = ref('/:type')
const router = useRouter()
const activeName = ref('first')
const state = reactive({
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
sizeList: ['small', '', 'large'] as const,
})
// 分类相关状态
const categories = ref<any[]>([])
const showCategoryModal = ref(false)
// 标签相关状态
const attributes = ref<any[]>([])
const showAttributeModal = ref(false)
// 处理菜单选择跳转
const handleSelect = (key: string) => {
router.push({ path: key })
}
// 路由切换时同步菜单高亮
router.beforeEach((to) => {
activeIndex.value = to.path
})
// 文章数量状态
const articleCount = ref(0)
// 分类数量状态
const categoryCount = ref(0)
// 标签数量状态
const AttributeCount = ref(0)
// 获取文章数量
const fetchArticleCount = async () => {
try {
const response = await articleService.getAllArticles();
articleCount.value = response.data?.length || 0
} catch (error) {
console.error('获取文章数量失败:', error)
articleCount.value = 0
}
}
// 获取分类数据
const fetchCategories = async () => {
try {
const response = await categoryService.getAllCategories();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据
categories.value = response.data?.map((category: any) => ({
...category,
count: 0
})) || [];
categories.value.forEach(async (category: any) => {
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.typeid)
if (attributeResponse.data?.length) {
category.count = attributeResponse.data?.length || 0
}
})
categoryCount.value = categories.value.length
} catch (error) {
console.error('获取分类失败:', error)
// 如果API调用失败使用模拟数据
categories.value = [
];
categoryCount.value = categories.value.length
}
}
// 获取标签数据
const fetchAttributes = async () => {
try {
const response = await categoryAttributeService.getAllAttributes();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据
attributes.value = response.data?.map((attribute: any) => ({
...attribute,
count: 0
})) || [];
attributes.value.forEach(async (attribute: any) => {
const articleResponse = await articleService.getArticlesByAttributeId(attribute.attributeid)
if (articleResponse.data?.length) {
attribute.count = articleResponse.data?.length || 0
}
})
AttributeCount.value = attributes.value.length
} catch (error) {
console.error('获取标签失败:', error)
// 如果API调用失败使用模拟数据
attributes.value = [
];
AttributeCount.value = attributes.value.length
}
}
// 显示分类蒙板
const showCategories = () => {
showCategoryModal.value = true
}
// 关闭分类蒙板
const closeCategoryModal = () => {
showCategoryModal.value = false
}
// 处理分类点击
const handleCategoryClick = (category: any) => {
// 这里可以根据实际需求跳转到对应分类的文章列表页
console.log('点击了分类:', category.typename)
// 示例router.push(`/article-list?category=${category.typeid}`)
closeCategoryModal()
}
// 显示标签蒙板
const showAttributes = () => {
showAttributeModal.value = true
}
// 关闭标签蒙板
const closeAttributeModal = () => {
showAttributeModal.value = false
}
// 处理标签点击
const handleAttributeClick = (attribute: any) => {
// 重置全局属性状态
globalStore.removeValue('attribute')
globalStore.setValue('attribute', {
id: attribute.attributeid,
name: attribute.typename
})
console.log(attribute)
router.push({
path: '/home/aericletype',
})
closeAttributeModal()
}
// 控制底部模块吸顶效果
const scrollY = ref(false)
const handleScroll = () => {
scrollY.value = window.scrollY > 1100
}
// 生命周期管理事件监听,防止内存泄漏
onMounted(() => {
window.addEventListener('scroll', handleScroll)
fetchArticleCount() // 组件挂载时获取文章数量
fetchCategories() // 组件挂载时获取分类数据
fetchAttributes() // 组件挂载时获取标签数据
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<style scoped>
/* 整体布局外层每个子div底部间距 */
#alld div {
margin-bottom: 15px;
}
/* 顶部公告栏样式 */
#top {
height: 100px;
border-radius: 10px;
background-color: rgba(102, 161, 216, 0.9);
/* 蓝色半透明背景 */
}
#alld #top .top1 {
padding-top: 20px;
margin-bottom: 0;
text-align: center;
color: white;
}
#alld #top .top2 {
text-align: center;
color: white;
}
/* 内容区域样式 */
#cont {
padding:0 0 10px 0;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */
}
#cont .cont1{
margin-bottom: 5px;
}
#cont .cont2{
margin-bottom: 0px;
}
.cont1 {
text-align: center;
padding: 25px 10px 25px 10px;
background-color: rgba(102, 161, 216, 0.9);
/* 蓝色半透明背景 */
border-radius: 10px 10px 0 0;
}
.cont1 h3 {
margin-bottom: 10px;
}
.cont1 p {
color: white;
font-size: 14px;
}
/* 菜单样式 */
.cont2 .el-menu-vertical-demo {
display: block;
background-color: rgba(0, 0, 0, 0);
/* 白色半透明背景 */
}
.cont2 .el-menu-vertical-demo li {
font-size: 14px;
height: 35px;
}
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) {
/* border-radius: 0 0 10px 10px; */
/* margin-bottom: 10px; */
}
.cont2 .el-menu-vertical-demo .el-menu-item:hover {
background-color: rgba(64, 158, 255, 0.9);
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active:hover {
color: black;
/* 蓝色半透明背景 */
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active {
color: var(--nav-is-active);
}
/* 分类列表样式 */
.cont3 {
margin-top: 20px;
padding: 15px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9);
}
.cont3 h3 {
text-align: center;
color: #333;
margin-bottom: 15px;
font-size: 16px;
}
.category-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
background-color: rgba(240, 240, 240, 0.5);
}
.category-item:hover {
background-color: rgba(52, 152, 219, 0.2);
transform: translateX(5px);
}
.category-name {
color: #333;
font-size: 14px;
}
.category-count {
color: #7f8c8d;
font-size: 12px;
background-color: rgba(127, 140, 141, 0.1);
padding: 2px 8px;
border-radius: 12px;
}
/* 底部标签页样式 */
#bot {
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */
padding: 10px;
}
.site-state-item-count {
display: block;
text-align: center;
color: #32325d;
font-weight: bold;
}
.demo-tabs {
transition: all 0.3s ease-in-out;
}
.el-tabs__nav-scroll .el-tabs__nav .el-tabs__item {
margin-left: 100px;
padding: 10px 20px;
}
.mylogo_name {
font-size: 13px;
}
.mylogo_description {
font-size: 13px;
opacity: 0.8;
color: #c21f30;
margin: 10px 0;
}
.stat-container {
display: flex;
gap: 10px;
justify-content: center;
font-size: 10px;
}
.stat-container div:not(:first-child) {
border-left: 1px solid #ccc;
padding-left: 10px;
}
/* 头像样式 */
#pane-first .mylogo {
text-align: center;
padding: 6px;
margin-bottom: 0px;
}
.mylogo_avatar {
height: 60px;
width: 60px;
}
/* 直接应用到el-avatar组件上的悬浮效果 */
.el-avatar.mylogo_avatar {
transition: transform 0.3s ease;
}
.el-avatar.mylogo_avatar:hover {
transform: scale(1.2);
}
/* 吸顶效果 */
.botrelative {
position: sticky;
top: 20px;
z-index: 100;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 分类蒙板样式 */
.category-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.category-modal-content {
background-color: white;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.category-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.category-modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.category-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.category-modal-close:hover {
background-color: #f5f5f5;
color: #333;
}
.category-modal-body {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
max-height: 60vh;
overflow-y: auto;
}
.category-button {
background-color: rgba(102, 161, 216, 0.1);
border: 1px solid rgba(102, 161, 216, 0.3);
border-radius: 6px;
padding: 10px 15px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
color: #333;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.category-button:hover {
background-color: rgba(102, 161, 216, 0.3);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(102, 161, 216, 0.2);
}
.category-button-count {
font-size: 12px;
color: #66a1d8;
font-weight: 500;
}
/* 蒙板动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-active .category-modal-content,
.modal-leave-active .category-modal-content {
transition: transform 0.3s ease;
}
.modal-enter-from .category-modal-content {
transform: scale(0.9);
}
.modal-leave-to .category-modal-content {
transform: scale(0.9);
}
/* 滚动条样式 */
.category-modal-body::-webkit-scrollbar {
width: 6px;
}
.category-modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>