Compare commits

...

4 Commits

Author SHA1 Message Date
qingfeng1121
f4263af343 refactor(前端): 重构前端代码结构并优化功能
重构路由配置和API调用逻辑,统一分页处理方式
优化分类和标签模块的交互,提取蒙版组件到主布局
调整样式和布局,增强响应式设计
更新接口字段名以保持前后端一致性
添加网站运行时间显示功能
2025-12-18 15:20:14 +08:00
qingfeng1121
0dc24cfa85 refactor(layout): 优化 hero 区域滚动效果和样式
移除 hero 区域的顶部 margin 控制,改为使用 transform 实现平滑滚动
简化滚动逻辑,调整 hero 区域在不同页面的显示效果
更新 CSS 变量定义,移除无用样式
2025-12-18 15:19:46 +08:00
qingfeng1121
183d98a699 ci: 更新API代理目标地址为生产环境
将开发环境的本地代理地址改为线上生产环境地址,以便于测试和生产环境对接
2025-12-15 17:54:13 +08:00
qingfeng1121
ede67faafd feat: 优化前端布局和代理配置
refactor: 移除调试日志并优化代码结构
style: 调整响应式设计和UI细节
fix: 修复路由和导航相关的问题
2025-12-12 17:14:04 +08:00
21 changed files with 964 additions and 533 deletions

View File

@@ -34,12 +34,12 @@
</div> </div>
</div> </div>
<div id="bot" :class="{ 'botrelative': scrollY }"> <div id="bot" :class="{ 'botrelative': scrollY }">
<el-tabs v-model="activeName" stretch="true" class="demo-tabs"> <el-tabs v-model="activeName" :stretch="true" class="demo-tabs">
<el-tab-pane label="个人简介" name="first"> <el-tab-pane label="个人简介" name="first">
<div class="mylogo"> <div class="mylogo">
<el-avatar class="mylogo_avatar" :src="state.circleUrl" /> <el-avatar class="mylogo_avatar" :src="state.circleUrl" />
</div> </div>
<a href="#"> <a href="http://www.qf1121.top/">
<h6 class="mylogo_name logo-text">清疯不颠</h6> <h6 class="mylogo_name logo-text">清疯不颠</h6>
</a> </a>
<h6 class="mylogo_description">重度精神失常患者</h6> <h6 class="mylogo_description">重度精神失常患者</h6>
@@ -51,13 +51,13 @@
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link" @click.prevent="showCategories"> <a href="#" class="stat-link" @click.prevent="showCategoriesModel">
<span class="site-state-item-count">{{ categoryCount }}</span> <span class="site-state-item-count">{{ categoryCount }}</span>
<span class="site-state-item-name">分类</span> <span class="site-state-item-name">分类</span>
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link" @click.prevent="showAttributes"> <a href="#" class="stat-link" @click.prevent="showAttributesModel">
<span class="site-state-item-count">{{ AttributeCount }}</span> <span class="site-state-item-count">{{ AttributeCount }}</span>
<span class="site-state-item-name">标签</span> <span class="site-state-item-name">标签</span>
</a> </a>
@@ -70,49 +70,7 @@
</el-tabs> </el-tabs>
</div> </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> </div>
</template> </template>
@@ -120,27 +78,22 @@
import { reactive, ref, onMounted, onUnmounted } from 'vue' import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { articleService, categoryService, categoryAttributeService } from "@/services"; import { articleService, categoryService, categoryAttributeService } from "@/services";
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 当前激活菜单 // 当前激活菜单
const activeIndex = ref('/:type') const activeIndex = ref('/:type')
const router = useRouter() const router = useRouter()
const activeName = ref('first') const activeName = ref('first')
const state = reactive({ const state = reactive({
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', circleUrl: '/blogicon.jpg',
squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png', squareUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
sizeList: ['small', '', 'large'] as const, sizeList: ['small', '', 'large'] as const,
}) })
// 分类相关状态 // 分类相关状态
const categories = ref<any[]>([]) // defineEmits
const showCategoryModal = ref(false) const emit = defineEmits(['update-data', 'CategoryModal', 'AttributeModal'])
// 标签相关状态 // 标签相关状态
const attributes = ref<any[]>([])
const showAttributeModal = ref(false)
// 处理菜单选择跳转 // 处理菜单选择跳转
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
router.push({ path: key }) router.push({ path: key })
@@ -161,8 +114,8 @@ const AttributeCount = ref(0)
// 获取文章数量 // 获取文章数量
const fetchArticleCount = async () => { const fetchArticleCount = async () => {
try { try {
const response = await articleService.getAllArticles(); const response = await articleService.getAllArticles();
articleCount.value = response.data?.length || 0 articleCount.value = response.data?.length || 0
} catch (error) { } catch (error) {
console.error('获取文章数量失败:', error) console.error('获取文章数量失败:', error)
articleCount.value = 0 articleCount.value = 0
@@ -171,6 +124,9 @@ const fetchArticleCount = async () => {
// 获取分类数据 // 获取分类数据
const fetchCategories = async () => { const fetchCategories = async () => {
// 分类数据状态
const categories = ref<any[]>([])
try { try {
const response = await categoryService.getAllCategories(); const response = await categoryService.getAllCategories();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
@@ -179,7 +135,7 @@ const fetchCategories = async () => {
count: 0 count: 0
})) || []; })) || [];
categories.value.forEach(async (category: any) => { categories.value.forEach(async (category: any) => {
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.typeid) const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.categoryid)
if (attributeResponse.data?.length) { if (attributeResponse.data?.length) {
category.count = attributeResponse.data?.length || 0 category.count = attributeResponse.data?.length || 0
} }
@@ -194,10 +150,13 @@ const fetchCategories = async () => {
]; ];
categoryCount.value = categories.value.length categoryCount.value = categories.value.length
} }
return categories.value
} }
// 获取标签数据 // 获取标签数据
const fetchAttributes = async () => { const fetchAttributes = async () => {
// 标签数据状态
const attributes = ref<any[]>([])
try { try {
const response = await categoryAttributeService.getAllAttributes(); const response = await categoryAttributeService.getAllAttributes();
// 如果API返回的数据结构不包含count属性我们可以模拟一些数据 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
@@ -216,56 +175,27 @@ const fetchAttributes = async () => {
console.error('获取标签失败:', error) console.error('获取标签失败:', error)
// 如果API调用失败使用模拟数据 // 如果API调用失败使用模拟数据
attributes.value = [ attributes.value = [
]; ];
AttributeCount.value = attributes.value.length AttributeCount.value = attributes.value.length
} }
return attributes.value
} }
// 显示分类蒙板 // 向父组件传递标签数据
const showCategories = () => { const sendData = () => {
showCategoryModal.value = true const data = { fetchAttributes: fetchAttributes(), fetchCategories: fetchCategories() }
emit('update-data', data)
} }
// 关闭分类蒙板 // 显示标签蒙版
const closeCategoryModal = () => { const showAttributesModel = () => {
showCategoryModal.value = false emit('AttributeModal', { ifmodal: true })
}
// 显示分类蒙版
const showCategoriesModel = () => {
emit('CategoryModal', { ifmodal: true })
} }
// 处理分类点击
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.attributename
})
console.log(attribute)
router.push({
path: '/home/aericletype',
})
closeAttributeModal()
}
// 控制底部模块吸顶效果 // 控制底部模块吸顶效果
const scrollY = ref(false) const scrollY = ref(false)
@@ -277,8 +207,7 @@ const handleScroll = () => {
onMounted(() => { onMounted(() => {
window.addEventListener('scroll', handleScroll) window.addEventListener('scroll', handleScroll)
fetchArticleCount() // 组件挂载时获取文章数量 fetchArticleCount() // 组件挂载时获取文章数量
fetchCategories() // 组件挂载时获取分类数据 sendData() // 组件挂载时获取标签数据和分类数据
fetchAttributes() // 组件挂载时获取标签数据
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -314,15 +243,17 @@ onUnmounted(() => {
/* 内容区域样式 */ /* 内容区域样式 */
#cont { #cont {
padding:0 0 10px 0; padding: 0 0 10px 0;
border-radius: 10px; border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */ /* 白色半透明背景 */
} }
#cont .cont1{
#cont .cont1 {
margin-bottom: 5px; margin-bottom: 5px;
} }
#cont .cont2{
#cont .cont2 {
margin-bottom: 0px; margin-bottom: 0px;
} }
@@ -349,16 +280,12 @@ onUnmounted(() => {
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
/* 白色半透明背景 */ /* 白色半透明背景 */
} }
.cont2 .el-menu-vertical-demo li { .cont2 .el-menu-vertical-demo li {
font-size: 14px; font-size: 14px;
height: 35px; 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 { .cont2 .el-menu-vertical-demo .el-menu-item:hover {
background-color: rgba(64, 158, 255, 0.9); background-color: rgba(64, 158, 255, 0.9);
} }
@@ -429,11 +356,12 @@ onUnmounted(() => {
/* 白色半透明背景 */ /* 白色半透明背景 */
padding: 10px; padding: 10px;
} }
.site-state-item-count { .site-state-item-count {
display: block; display: block;
text-align: center; text-align: center;
color: #32325d; color: #32325d;
font-weight: bold; font-weight: bold;
} }
.demo-tabs { .demo-tabs {
@@ -444,14 +372,16 @@ onUnmounted(() => {
margin-left: 100px; margin-left: 100px;
padding: 10px 20px; padding: 10px 20px;
} }
.mylogo_name { .mylogo_name {
font-size: 13px; font-size: 13px;
} }
.mylogo_description { .mylogo_description {
font-size: 13px; font-size: 13px;
opacity: 0.8; opacity: 0.8;
color: #c21f30; color: #c21f30;
margin: 10px 0; margin: 10px 0;
} }
.stat-container { .stat-container {
@@ -508,145 +438,6 @@ onUnmounted(() => {
transform: translateY(0); 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;
/* 确保在任何情况下都能居中显示 */
margin: 0;
padding: 0;
}
.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> </style>

View File

@@ -3,7 +3,7 @@
<el-row justify="center"> <el-row justify="center">
<el-col :span="6" v-if="windowwidth"> <el-col :span="6" v-if="windowwidth">
<div class="grid-content ep-bg-purple-dark"> <div class="grid-content ep-bg-purple-dark">
<div class="logo-text"> <a href="/">清疯不颠</a></div> <div class="logo-text"> <a href="http://www.qf1121.top/">清疯不颠</a></div>
</div> </div>
</el-col> </el-col>
<el-col :span="14" justify="center"> <el-col :span="14" justify="center">
@@ -48,12 +48,13 @@
</div> </div>
<!-- Hero 区域 --> <!-- Hero 区域 -->
<div class="hero" :class="{ 'newhero': classhero }" v-if="windowwidth"> <div class="hero" :class="{ 'small-hero': classsmallhero }" v-if="windowwidth"
:style="{ marginBottom: heroMarginBottom, transform: heroTransform }">
<h1 class="typewriter">{{ heroText }}</h1> <h1 class="typewriter">{{ heroText }}</h1>
</div> </div>
<!-- 提示区域 --> <!-- 内容区域 -->
<div id="content-section" :class="{ 'visible': isconts }"> <div id="content-section" :class="{ 'visible': iscontentvisible }">
<div class="nonsensetitle" v-if="classnonsenset"> <div class="nonsensetitle" v-if="classnonsenset">
<div class="nonsensetitleconst"> <div class="nonsensetitleconst">
<h1>{{ Cardtitle }}</h1> <h1>{{ Cardtitle }}</h1>
@@ -62,18 +63,54 @@
<!-- 左侧模块 --> <!-- 左侧模块 -->
<div class="leftmodluecontainer" v-if="isleftmodluecontainer"> <div class="leftmodluecontainer" v-if="isleftmodluecontainer">
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classmoduleorrouter }" v-if="windowwidth" /> <LeftModule class="leftmodluepage" @update-data="updateData" @CategoryModal="CategoryModal"
@AttributeModal="AttributeModal" :class="{ 'nonsensetmargintop': classmoduleorrouter }" v-if="windowwidth" />
</div> </div>
<!-- 内容模块 --> <!-- 内容模块 -->
<RouterView class="RouterViewpage" <div class="RouterViewpage">
:class="{ 'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" /> <RouterView :class="{ 'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
<!-- 页脚 -->
<Footer class="footer-container" v-if="windowwidth" />
</div>
</div> </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>
<!-- 管理员 -->
<Establish class="establish-container" v-if="Login" /> <Establish class="establish-container" v-if="Login" />
<!-- 页脚 -->
<Footer class="footer-container" v-if="windowwidth" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -95,17 +132,27 @@ const globalStore = useGlobalStore();
const Login = computed(() => globalStore.Login); const Login = computed(() => globalStore.Login);
// ========== 响应式状态定义 ========== // ========== 响应式状态定义 ==========
// 页面标题和样式相关状态 // 页面标题和样式相关状态
const Cardtitle = ref(''); const Cardtitle = ref('');
const classmoduleorrouter = ref(false); const classmoduleorrouter = ref(false);
const classnonsenset = ref(false); const classnonsenset = ref(false);
const classhero = ref(false); const classsmallhero = ref(false);
const elrowtop = ref('transparent'); const elrowtop = ref('transparent');
// hero区域的margin值用于实现滚动时动态变化
const heroMarginBottom = ref('45%');
// hero区域的初始margin值从CSS变量获取
const initialHeroMarginBottom = 45;
// hero是否开始向上移动
const heroIsMoving = ref(false);
// hero的transform值用于实现向上移动和吸附效果
const heroTransform = ref('translateY(450px)');
const heroTransformValue = 450;
// hero的位置状态static(静态)、moving(移动中)、sticky(吸附顶部)
const heroPosition = ref('static');
// 布局相关状态 // 布局相关状态
const isleftmodluecontainer = ref(true); const isleftmodluecontainer = ref(true);
const isconts = ref(false); const iscontentvisible = ref(false);
const isScrollingleftmodlue = ref(false); const isScrollingleftmodlue = ref(false);
const windowwidth = ref(true); const windowwidth = ref(true);
@@ -125,13 +172,103 @@ const heroText = ref('');
let heroIndex = 0; let heroIndex = 0;
let heroTimer: number | undefined; let heroTimer: number | undefined;
// 蒙版相关状态
const categories = ref<any[]>([])
const showCategoryModal = ref(false)
const attributes = ref<any[]>([])
const showAttributeModal = ref(false)
// 显示分类蒙板
const openCategoryModal = () => {
showCategoryModal.value = true;
}
// 关闭分类蒙板
const closeCategoryModal = () => {
showCategoryModal.value = false;
}
// 显示标签蒙板
const openAttributeModal = () => {
showAttributeModal.value = true;
}
// 关闭标签蒙板
const closeAttributeModal = () => {
showAttributeModal.value = false;
}
// 左侧状态栏传值
const updateData = (data: any) => {
// 处理异步数据
if (data.fetchCategories && typeof data.fetchCategories.then === 'function') {
data.fetchCategories.then(result => {
categories.value = result || []
})
} else {
categories.value = data.fetchCategories || []
}
if (data.fetchAttributes && typeof data.fetchAttributes.then === 'function') {
data.fetchAttributes.then(result => {
attributes.value = result || []
})
} else {
attributes.value = data.fetchAttributes || []
}
}
// 分类相关状态
const CategoryModal = async (data: any) => {
if (data.ifmodal) {
openCategoryModal()
// console.log('打开分类蒙板')
} else {
closeCategoryModal()
// console.log('关闭分类蒙板')
}
}
// 标签相关状态
const AttributeModal = async (data: any) => {
if (data.ifmodal) {
openAttributeModal()
// console.log('打开标签蒙板')
} else {
closeAttributeModal()
// console.log('关闭标签蒙板')
}
}
// ========== 蒙版事件 ==========
// 处理分类点击
const handleCategoryClick = (category: any) => {
// 这里可以根据实际需求跳转到对应分类的文章列表页
// 重置全局属性状态
globalStore.removeValue('category')
globalStore.setValue('category', {
id: category.categoryid,
name: category.categoryname
})
console.log(category)
router.push('/home/aericlecategory',)
closeCategoryModal()
}
// 处理标签点击
const handleAttributeClick = (attribute: any) => {
// 重置全局属性状态
globalStore.removeValue('attribute')
globalStore.setValue('attribute', {
id: attribute.attributeid,
name: attribute.attributename
})
console.log(attribute)
router.push('/home/aericletype',)
closeAttributeModal()
}
// ========== 打字机效果模块 ========== // ========== 打字机效果模块 ==========
/** /**
* 初始化并启动打字机效果 * 初始化并启动打字机效果
* @param {string} text - 要显示的完整文本 * @param {string} text - 要显示的完整文本
*/ */
const startTypewriter = () => { const startTypewriter = (text: string) => {
// 重置状态 // 重置状态
heroText.value = ''; heroText.value = '';
heroIndex = 0; heroIndex = 0;
@@ -143,8 +280,8 @@ const startTypewriter = () => {
// 设置新的定时器,逐字显示文本 // 设置新的定时器,逐字显示文本
heroTimer = window.setInterval(() => { heroTimer = window.setInterval(() => {
if (heroIndex < fullHeroText.length) { if (heroIndex < text.length) {
heroText.value += fullHeroText[heroIndex]; heroText.value += text[heroIndex];
heroIndex++; heroIndex++;
} else { } else {
// 文本显示完毕,清除定时器 // 文本显示完毕,清除定时器
@@ -161,8 +298,13 @@ const stopTypewriter = () => {
if (heroTimer) { if (heroTimer) {
clearInterval(heroTimer); clearInterval(heroTimer);
} }
// 直接显示完整文本 // 非首页时清空hero内容
heroText.value = fullHeroText; if (rpsliturl[1] !== localhome) {
heroText.value = '';
} else {
// 首页直接显示完整文本
heroText.value = fullHeroText;
}
}; };
// ========== 导航和路由处理模块 ========== // ========== 导航和路由处理模块 ==========
@@ -199,8 +341,7 @@ const setActiveIndex = (path: string) => {
*/ */
const updatePageState = () => { const updatePageState = () => {
// 根据是否为主页根路径设置hero区域状态 // 根据是否为主页根路径设置hero区域状态
classhero.value = !(rpsliturl[1] == localhome && rpsliturl[2] == undefined); classsmallhero.value = !(rpsliturl[1] == localhome && rpsliturl[2] == undefined);
// 控制左侧模块容器的显示/隐藏 // 控制左侧模块容器的显示/隐藏
isleftmodluecontainer.value = rpsliturl[1] !== "articlesave"; isleftmodluecontainer.value = rpsliturl[1] !== "articlesave";
}; };
@@ -210,16 +351,14 @@ const updatePageState = () => {
*/ */
const updateArticleTitle = () => { const updateArticleTitle = () => {
let articledata: any = null; let articledata: any = null;
// 根据不同路由参数获取文章标题数据 // 根据不同路由参数获取文章标题数据
if (rpsliturl[2] === 'aericletype') { if (rpsliturl[2] === 'aericleattribute') {
// 按属性类型获取 // 按属性类型获取
articledata = globalStore.getValue('attribute')?.name; articledata = globalStore.getValue('attribute')?.name;
console.log('attributeId参数:', articledata);
} }
else if (rpsliturl[2] === 'aericletitle') { else if (rpsliturl[2] === 'aericletitle') {
// 按标题搜索获取 // 按标题搜索获取
articledata = globalStore.getValue('title')?.name; articledata = globalStore.getValue('articleserarch')?.name;
} }
else if (rpsliturl[1] === 'nonsense') { else if (rpsliturl[1] === 'nonsense') {
// 疯言疯语页面特殊处理 // 疯言疯语页面特殊处理
@@ -229,9 +368,9 @@ const updateArticleTitle = () => {
// 确定标题区域的显示状态 // 确定标题区域的显示状态
const shouldHideTitle = const shouldHideTitle =
// 特殊页面不需要显示标题 // 特殊页面不需要显示标题
(rpsliturl[1] === 'article-list' || (rpsliturl[1] === 'articlelist' ||
rpsliturl[1] === 'message' || rpsliturl[1] === 'message' ||
rpsliturl[1] === 'about') || rpsliturl[1] === 'about') ||
// 在主页且无标题数据时,不显示标题 // 在主页且无标题数据时,不显示标题
(rpsliturl[1] === localhome && !articledata); (rpsliturl[1] === localhome && !articledata);
@@ -289,11 +428,12 @@ const closeSearchBoxWithDelay = () => {
const performSearch = () => { const performSearch = () => {
// 验证搜索关键词不为空 // 验证搜索关键词不为空
if (searchKeyword.value.trim()) { if (searchKeyword.value.trim()) {
// 清除全局搜索关键词
globalStore.removeValue('articleserarch');
// 存储搜索关键词到全局状态 // 存储搜索关键词到全局状态
globalStore.setValue('articleserarch', { globalStore.setValue('articleserarch', {
name: searchKeyword.value name: searchKeyword.value
}); });
// 跳转到搜索结果页面 // 跳转到搜索结果页面
router.push({ path: `/home/aericletitle` }); router.push({ path: `/home/aericletitle` });
} }
@@ -311,13 +451,16 @@ const performSearch = () => {
* 根据屏幕宽度调整布局和内容显示 * 根据屏幕宽度调整布局和内容显示
*/ */
const handleResize = () => { const handleResize = () => {
// 更新窗口宽度状态 // 更新窗口宽度状态
windowwidth.value = window.innerWidth > 768; windowwidth.value = window.innerWidth > 768;
// 首页特殊处理:小屏幕下默认显示内容区 // 首页特殊处理:小屏幕下默认显示内容区
if (rpsliturl[1] === localhome) { if (rpsliturl[1] === localhome) {
isconts.value = window.innerWidth <= 768; iscontentvisible.value = window.innerWidth <= 768;
} }
// 移动端首页默认显示内容区,桌面端初始隐藏
iscontentvisible.value = window.innerWidth <= 768;
}; };
/** /**
@@ -325,33 +468,58 @@ const handleResize = () => {
* 根据滚动位置调整导航栏样式和内容显示动画 * 根据滚动位置调整导航栏样式和内容显示动画
*/ */
const handleScroll = () => { const handleScroll = () => {
let scrollY = 0;
scrollY = window.scrollY;
// 小屏幕设备只切换导航栏样式 // 小屏幕设备只切换导航栏样式
if (window.innerWidth < 768) { if (window.innerWidth <= 768) {
updateNavbarStyle(window.scrollY); updateNavbarStyle(scrollY);
return; return;
} }
// 大屏幕设备完整处理 // 大屏幕设备完整处理
updateNavbarStyle(window.scrollY); updateNavbarStyle(scrollY);
// 仅在首页根路径应用滚动动画 // 仅在首页根路径应用滚动动画
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 控制内容区和左侧模块的滚动状态 // 首页滚动动画处理
isconts.value = window.scrollY > 200; if (scrollY <= 350) {
isScrollingleftmodlue.value = window.scrollY > 600; HeroState(scrollY);
}
// 控制左侧模块的滚动状态
isScrollingleftmodlue.value = scrollY > 600;
}
};
// ========== 首页滚动动画模块 ==========
const HeroState = (scrollY: number) => {
const windowHeight = window.innerHeight;
// 计算滚动距离与窗口高度的比例,用于内容渐显
const contentScrollRatio = Math.min(scrollY / windowHeight, 1);
// 计算新的底部margin值从初始值45%逐渐减少到25%
const newMarginBottom = Math.max(initialHeroMarginBottom - (initialHeroMarginBottom * contentScrollRatio), 0);
// 更新hero的底部margin值
heroMarginBottom.value = `${newMarginBottom}%`;
// 计算新的translateY值从初始值450px逐渐减少到150px
const translateYValue = Math.max(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 90);
heroTransform.value = `translateY(${translateYValue}px)`;
// 当滚动超过100px时开始显示滚动到一屏高度时完全显示
iscontentvisible.value = scrollY > 100;
// 当滚动超过287px时logo被顶出屏幕触发移动状态
if (scrollY > 287) {
heroPosition.value = 'moving';
const translateYValue = Math.min(heroTransformValue - (heroTransformValue * contentScrollRatio * 5), 0);
heroTransform.value = `translateY(${translateYValue / 2}px)`;
} }
}; };
/** /**
* 根据滚动位置更新导航栏样式 * 根据滚动位置更新导航栏样式
* @param {number} scrollY - 当前滚动位置 * @param {number} scrollY - 当前滚动位置
*/ */
const updateNavbarStyle = (scrollY: number) => { const updateNavbarStyle = (scrollY: number) => {
// 根据滚动位置设置导航栏样式 // 根据滚动位置设置导航栏样式
if (scrollY > 1200) { // 当滚动超过1200px且屏幕宽度大于768px时隐藏导航栏
if (scrollY > 1200 && window.innerWidth > 768) {
elrowtop.value = 'hide'; // 隐藏导航栏 elrowtop.value = 'hide'; // 隐藏导航栏
} else { } else {
elrowtop.value = scrollY > 100 ? 'solid' : 'transparent'; // 固定或透明样式 elrowtop.value = scrollY > 50 ? 'solid' : 'transparent'; // 固定或透明样式
} }
}; };
@@ -364,7 +532,6 @@ const updateNavbarStyle = (scrollY: number) => {
const handleRouteChange = () => { const handleRouteChange = () => {
// 重新解析路由路径 // 重新解析路由路径
rpsliturl = route.path.split('/'); rpsliturl = route.path.split('/');
// 更新页面相关状态 // 更新页面相关状态
updatePageState(); updatePageState();
setActiveIndex(rpsliturl[1]); setActiveIndex(rpsliturl[1]);
@@ -376,11 +543,18 @@ const handleRouteChange = () => {
// 根据是否为首页决定是否启动打字机效果 // 根据是否为首页决定是否启动打字机效果
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 首页启动打字机效果 // 首页启动打字机效果
startTypewriter(); startTypewriter(fullHeroText);
// 重置hero的margin值为初始值
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
// 重置hero的移动状态
heroTransform.value = `translateY(${heroTransformValue}px)`;
heroIsMoving.value = false;
heroPosition.value = 'static';
} else { } else {
// 非首页直接显示完整文本 iscontentvisible.value = true;
isconts.value = true; startTypewriter(fullHeroText);
stopTypewriter(); heroMarginBottom.value = `${5}%`;
heroTransform.value = ``;
} }
}; };
@@ -392,8 +566,20 @@ const initializePage = () => {
handleResize(); handleResize();
// 启动打字机效果(如果是首页) // 启动打字机效果(如果是首页)
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
startTypewriter(); // 首页启动打字机效果
startTypewriter(fullHeroText);
// 重置hero的margin值为初始值
heroMarginBottom.value = `${initialHeroMarginBottom}%`;
// 重置hero的移动状态
heroIsMoving.value = false;
heroPosition.value = 'static';
// 移动端首页默认显示内容区,桌面端初始隐藏
iscontentvisible.value = window.innerWidth <= 768;
} else {
startTypewriter(fullHeroText);
heroMarginBottom.value = `${2.5}%`;
} }
}; };
// ========== 生命周期钩子 ========== // ========== 生命周期钩子 ==========
@@ -435,8 +621,7 @@ onUnmounted(() => {
*/ */
watch( watch(
() => route.path, () => route.path,
handleRouteChange, handleRouteChange
{ immediate: true } // 立即执行一次,确保初始状态正确
); );
</script> </script>
@@ -514,9 +699,140 @@ watch(
.close-btn:hover { .close-btn:hover {
color: #409eff; color: #409eff;
} }
.footer-container { .footer-container {
margin-top: 20px; margin-top: 20px;
} }
/* 搜索框样式 */
/* ... 现有搜索框样式 ... */
/* 蒙版样式 */
.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;
margin: 0;
padding: 0;
}
.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);
}
/* 页脚样式 */
.footer-container {
margin-top: 20px;
}
/* 防止搜索框在小屏幕上重叠 */ /* 防止搜索框在小屏幕上重叠 */
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.search-box-container.open { .search-box-container.open {

View File

@@ -1,41 +1,43 @@
<template> <template>
<div class="establish-container"> <div class="establish-component-root">
<!-- 弹出的按钮容器 --> <div class="establish-container">
<!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> --> <!-- 弹出的按钮容器 -->
<button v-for="(btn, index) in isbuttonsave()" :class="['action-button', { 'show': isExpanded }]" <!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> -->
:style="getButtonStyle(index)" :key="btn.id" @click="handleButtonClick(btn)"> <button v-for="(btn, index) in isbuttonsave()" :class="['action-button', { 'show': isExpanded }]"
<!-- <i :class="btn.icon"></i> --> :style="getButtonStyle(index)" :key="btn.id" @click="handleButtonClick(btn)">
<span>{{ btn.label }}</span> <!-- <i :class="btn.icon"></i> -->
</button> <span>{{ btn.label }}</span>
<!-- </div> --> </button>
<!-- </div> -->
<!-- 主圆形按钮 --> <!-- 主圆形按钮 -->
<button class="main-button" :class="{ 'active': isExpanded }" @click="toggleExpand"> <button class="main-button" :class="{ 'active': isExpanded }" @click="toggleExpand">
<i class="icon">+</i> <i class="icon">+</i>
</button> </button>
</div> </div>
<!-- 标签蒙板组件 --> <!-- 标签蒙板组件 -->
<Transition name="modal"> <Transition name="modal">
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal"> <div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
<div class="category-modal-content"> <div class="category-modal-content">
<div class="category-modal-header"> <div class="category-modal-header">
<h3>新建标签</h3> <h3>新建标签</h3>
<button class="category-modal-close" @click="closeAttributeModal">×</button> <button class="category-modal-close" @click="closeAttributeModal">×</button>
</div> </div>
<div class="category-modal-body"> <div class="category-modal-body">
<el-select v-model="CategoryAttributeDto.categoryid" placeholder="请选择"> <el-select v-model="CategoryAttributeDto.categoryid" placeholder="请选择">
<el-option v-for="item in categories" :key="item.value" :label="item.label" :value="item.value"> <el-option v-for="item in categories" :key="item.value" :label="item.label" :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
<el-input v-model="CategoryAttributeDto.name" placeholder="输入新标签" /> <el-input v-model="CategoryAttributeDto.name" placeholder="输入新标签" />
<el-button type="primary" <el-button type="primary"
@click="saveAttribute(CategoryAttributeDto)">保存</el-button> @click="saveAttribute(CategoryAttributeDto)">保存</el-button>
</div>
</div> </div>
</div> </div>
</div> </Transition>
</Transition> </div>
</template> </template>
<script setup> <script setup>
@@ -335,14 +337,14 @@ const createCategory = () => {
* @param {string} typename - 分类名称 * @param {string} typename - 分类名称
*/ */
const saveCategory = (typename) => { const saveCategory = (typename) => {
console.log('保存分类') // console.log('保存分类')
categoryService.createCategory({ categoryService.createCategory({
typename: typename typename: typename
}).then(response => { }).then(response => {
if (response.code === 200) { if (response.code === 200) {
ElMessage.success('分类创建成功'); ElMessage.success('分类创建成功');
// 刷新页面 // 刷新页面
router.push({ path: `/aericlelist`}); router.push({ path: `/articlelist`});
} else { } else {
ElMessage.error(response.message || '创建分类失败'); ElMessage.error(response.message || '创建分类失败');
} }
@@ -365,7 +367,7 @@ const createAttribute = async () => {
} catch (error) { } catch (error) {
handleErrorResponse(error, '获取分类失败'); handleErrorResponse(error, '获取分类失败');
} finally { } finally {
console.log(categories.value) // console.log(categories.value)
} }
}; };
@@ -387,7 +389,7 @@ const saveAttribute = (dto) => {
closeAttributeModal() closeAttributeModal()
ElMessage.success('标签创建成功'); ElMessage.success('标签创建成功');
// 刷新页面 // 刷新页面
router.push({ path: `/aericlelist`}); router.push({ path: `/articlelist`});
} else { } else {
ElMessage.error(response.message || '创建标签失败'); ElMessage.error(response.message || '创建标签失败');
} }
@@ -488,7 +490,7 @@ const reloadPage = () => {
* @param {{id: string, label: string, icon: string}} button - 按钮对象 * @param {{id: string, label: string, icon: string}} button - 按钮对象
*/ */
const handleButtonClick = (button) => { const handleButtonClick = (button) => {
console.log('点击了按钮:', button.id, button.label) // console.log('点击了按钮:', button.id, button.label)
// 使用按钮ID进行处理更可靠且易于维护 // 使用按钮ID进行处理更可靠且易于维护
switch (button.id) { switch (button.id) {

View File

@@ -25,10 +25,16 @@ const routes = [
meta: { title: '首页' }, meta: { title: '首页' },
children: [ children: [
{ {
path: 'aericletype', path: 'aericleattribute',
name: 'homeByType', name: 'homeByAttribute',
component: HomePage component: HomePage
}, },
{
path: 'aericlecategory',
name: 'homeByCategory',
component: HomePage
},
{ {
path: 'aericletitle', path: 'aericletitle',
name: 'homeByTitle', name: 'homeByTitle',

View File

@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
// 创建axios实例 // 创建axios实例
const api = axios.create({ const api = axios.create({
baseURL: 'http://www.qf1121.top/api', // API基础URL baseURL: '/api', // API基础URL使用相对路径通过Vite代理转发
timeout: 10000, // 请求超时时间 timeout: 10000, // 请求超时时间
withCredentials: true // 允许跨域请求携带凭证如cookies withCredentials: true // 允许跨域请求携带凭证如cookies
}) })

View File

@@ -13,10 +13,17 @@ class ArticleService {
* @param size 每页大小可选默认为10最大为100 * @param size 每页大小可选默认为10最大为100
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/ */
getArticles(params = {}) { getPagedArticles(params = {}) {
return api.get(`/articles/status/page/${params.status}/${params.page}/${params.size}`, { params }) return api.get(`/articles/status/page?title=${params.title || ''}&categoryid=${params.categoryid || 0}&attributeid=${params.attributeid || 0}&status=${params.status || 1}&page=${params.page || 0}&size=${params.size || 10}`)
}
/**
* 获取分页文章数量
* @param {number} status - 文章状态0未发表 1已发表 2已删除
* @returns {Promise<import('../types').ApiResponse<number>>}
*/
getArticleCountByStatus(status) {
return api.get(`/articles/count/status/${status || 1}`)
} }
/** /**
* 获取已发布文章列表 * 获取已发布文章列表
* @param {import('../types').PaginationParams} params - 查询参数 * @param {import('../types').PaginationParams} params - 查询参数

View File

@@ -14,11 +14,11 @@ class CategoryService {
/** /**
* 获取指定分类 * 获取指定分类
* @param {number} typeid - 分类ID * @param {number} Categoryid - 分类ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
*/ */
getCategory(typeid) { getCategory(Categoryid) {
return api.get(`/categories/${typeid}`) return api.get(`/categories/${Categoryid}`)
} }
/** /**
@@ -32,21 +32,21 @@ class CategoryService {
/** /**
* 更新分类 * 更新分类
* @param {number} typeid - 分类ID * @param {number} Categoryid - 分类ID
* @param {import('../types').CategoryDto} categoryData - 分类数据 * @param {import('../types').CategoryDto} categoryData - 分类数据
* @returns {Promise<import('../types').ApiResponse<import('../types').Category>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Category>>}
*/ */
updateCategory(typeid, categoryData) { updateCategory(Categoryid, categoryData) {
return api.put(`/categories/${typeid}`, categoryData) return api.put(`/categories/${Categoryid}`, categoryData)
} }
/** /**
* 删除分类 * 删除分类
* @param {number} typeid - 分类ID * @param {number} Categoryid - 分类ID
* @returns {Promise<import('../types').ApiResponse<boolean>>} * @returns {Promise<import('../types').ApiResponse<boolean>>}
*/ */
deleteCategory(typeid) { deleteCategory(Categoryid) {
return api.delete(`/categories/${typeid}`) return api.delete(`/categories/${Categoryid}`)
} }
} }

View File

@@ -22,6 +22,29 @@ class MessageService {
return apiService.get(`/messages/${messageid}`) return apiService.get(`/messages/${messageid}`)
} }
/**
* 获取留言数量
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<number>>}
*/
getMessageCountByArticleId(articleid) {
return apiService.get(`/messages/count?articleid=${articleid}`)
}
/**
* 获取分页留言
* @param {number} articleid - 文章ID
* @param {number} pagenum - 页码
* @param {number} pagesize - 每页数量
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/
getMessagesByPage(articleid, pagenum, pagesize) {
// 如果文章ID不存在查询所有留言
if (!articleid) {
return apiService.get(`/messages/page?pageNum=${pagenum}&pageSize=${pagesize}`)
}
return apiService.get(`/messages/page?articleid=${articleid}&pageNum=${pagenum}&pageSize=${pagesize}`)
}
/** /**
* 根据文章ID获取留言 * 根据文章ID获取留言
* @param {number} articleid - 文章ID * @param {number} articleid - 文章ID
@@ -57,15 +80,6 @@ class MessageService {
return apiService.get(`/messages/search?nickname=${nickname}`) return apiService.get(`/messages/search?nickname=${nickname}`)
} }
/**
* 获取文章评论数量
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<number>>}
*/
getMessageCountByArticleId(articleid) {
return apiService.get(`/messages/count/article/${articleid}`)
}
/** /**
* 创建留言 * 创建留言
* @param {import('../types').MessageDto} messageData - 留言数据 * @param {import('../types').MessageDto} messageData - 留言数据

View File

@@ -17,13 +17,12 @@
/* 内容区圆角 */ /* 内容区圆角 */
/* 首页 hero 区域高度和间距 */ /* 首页 hero 区域高度和间距 */
--hero-height: 768px; --hero-height: 100px;
/* hero 默认高度 */ /* hero 默认高度 */
--hero-height-small: 100px; --hero-height-small: 100px;
/* hero 收缩后高度 */ /* hero 收缩后高度 */
/* --hero-margin-top: 2%; */ --hero-margin-top-small: 6%;
/* hero 顶部外边距 */ /* hero 收缩后顶部外边距 */
/* 标题样式 */ /* 标题样式 */
--title-font-size: 3.5rem; --title-font-size: 3.5rem;
/* hero 主标题字号 */ /* hero 主标题字号 */
@@ -368,18 +367,24 @@ p {
/* 首页 hero 区域样式 */ /* 首页 hero 区域样式 */
.hero { .hero {
height: var(--hero-height); height: var(--hero-height);
margin: var(--hero-margin);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
color: white; color: white;
transition: height 0.3s ease; transition: all 0.3s ease;
position: sticky;
top: 0;
z-index: 999;
} }
/* hero 收缩状态 */ /* hero 收缩状态 */
.hero.newhero { .hero.small-hero {
height: var(--hero-height-small); height: var(--hero-height-small);
margin-top: 10%; margin-top: var(--hero-margin-top-small);
/* 去除 hero 收缩状态下的position */
position: static;
} }
/* 打字机效果 */ /* 打字机效果 */
@@ -387,6 +392,10 @@ p {
white-space: pre; white-space: pre;
min-height: 2.5em; min-height: 2.5em;
font-size: var(--title-font-size, 2rem); font-size: var(--title-font-size, 2rem);
/* 水平居中 */
display: flex;
justify-content: center;
align-items: center;
} }
@keyframes blink { @keyframes blink {
@@ -445,8 +454,7 @@ p {
--nonsense-title-margin-bottom: 15px; --nonsense-title-margin-bottom: 15px;
--nav-padding-small: 0 8px; --nav-padding-small: 0 8px;
--nonsenset-margin-top: 10px; --nonsenset-margin-top: 10px;
--body-background-img: url(); --body-background-img: url(../img/bg.jpg);
/* 移动端不显示背景图片 */
} }
.elrow-top { .elrow-top {
@@ -506,3 +514,23 @@ p {
font-size: 1rem; font-size: 1rem;
} }
} }
/* 移动端适配屏幕宽度小于820px时生效 */
@media (max-width: 820px) {
:root {
--nav-padding: 0;
--hero-height:300px;
}
.RouterViewpage {
height: auto;
}
.hero {
margin-top: 50%;
}
.el-col-14 {
flex: 0 0 0;
max-width: 100%;
}
.elrow-top {
}
}

View File

@@ -70,7 +70,7 @@ export interface MessageDto {
* 分类类型接口 * 分类类型接口
*/ */
export interface Category { export interface Category {
typeid: number Categoryid: number
typename: string typename: string
description?: string description?: string
createdAt?: string createdAt?: string
@@ -162,7 +162,7 @@ export interface ApiResponse<T = any> {
* 分页参数接口 * 分页参数接口
*/ */
export interface PaginationParams { export interface PaginationParams {
page?: number pagenum?: number
size?: number pagesize?: number
status?: number status?: number
} }

View File

@@ -1,30 +1,136 @@
<!-- 页脚 --> <!-- 页脚 -->
<template> <template>
<div class="footer">
<div class="footer-content">
<!-- 备案信息 -->
<p class="footer-beian">
备案号
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
皖ICP备2025105428号-1 || 皖公网安备34120202001634号
</a>
</p>
<div class="footer"> <!-- 版权信息 -->
<div class="footer-content"> <p class="footer-copyright">
<p>© 2023 我的网站. 所有权利保留.</p> 网站所有权利保留 &copy; {{ new Date().getFullYear() }} 清疯不颠
<p>联系我们<a href="mailto:xxxx@exxxx.com">xxxx@exxxx.com</a></p> </p>
<!-- 备案 -->
<p>
备案号<a href="https://beian.miit.gov.cn/" target="_blank">
皖ICP备2025105428号-1
</a>
</p>
</div>
<!-- 运行时间 -->
<p class="footer-runtime">
运行时间<span>{{ runtime }}</span>
</p>
</div> </div>
</div>
</template> </template>
<style scoped> <script setup lang="ts">
.footer { import { ref, onMounted, onUnmounted } from 'vue'
padding: 20px;
text-align: center; // 运行时间响应式状态
const runtime = ref('计算中...')
// 网站上线时间 (时间戳)
const LAUNCH_DATE = new Date('2025-12-01T00:00:00').getTime()
// 定时器引用
let runtimeTimer: number | null = null
/**
* 更新网站运行时间
*/
const updateRuntime = () => {
const now = Date.now()
const diff = now - LAUNCH_DATE
// 计算天、时、分、秒
const days = Math.floor(diff / 86400000)
const hours = Math.floor((diff % 86400000) / 3600000)
const minutes = Math.floor((diff % 3600000) / 60000)
const seconds = Math.floor((diff % 60000) / 1000)
// 更新运行时间显示
runtime.value = `${days}${hours} 小时 ${minutes} 分钟 ${seconds}`
} }
// 组件挂载时初始化
onMounted(() => {
// 立即更新一次
updateRuntime()
// 每秒更新一次
runtimeTimer = window.setInterval(updateRuntime, 1000)
})
// 组件卸载时清理定时器
onUnmounted(() => {
if (runtimeTimer) {
clearInterval(runtimeTimer)
runtimeTimer = null
}
})
</script>
<style scoped>
/* 页脚容器 */
.footer {
text-align: center;
margin-top: 3rem;
padding: 2rem 0;
width: 100%;
}
/* 页脚内容 */
.footer-content { .footer-content {
width: 80%; margin: 0 auto;
margin: 0 auto; background-color: rgba(255, 255, 255, 0.9);
background-color: #f5f5f5; border-radius: 12px;
border-radius: 10px; padding: 1.5rem 2rem;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
}
/* 通用段落样式 */
.footer-content p {
margin: 0.5rem 0;
font-size: 0.9rem;
color: #666;
line-height: 1.5;
}
/* 备案信息 */
.footer-beian a {
color: #409eff;
text-decoration: none;
transition: color 0.3s ease;
}
.footer-beian a:hover {
color: #66b1ff;
text-decoration: underline;
}
/* 版权信息 */
.footer-copyright {
font-weight: 500;
color: #333;
}
/* 运行时间 */
.footer-runtime {
font-family: 'Courier New', monospace;
color: #67c23a;
}
/* 响应式设计 */
@media (max-width: 768px) {
.footer {
margin-top: 2rem;
padding: 1rem 0;
}
.footer-content {
padding: 1rem;
margin: 0 1rem;
}
.footer-content p {
font-size: 0.8rem;
}
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
<div class="about-content-wrapper"> <div class="about-content-wrapper">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="about-page-header"> <div class="about-page-header">
<h1 class="about-page-title">关于</h1> <h3 class="about-page-title">关于</h3>
</div> </div>
<!-- 关于内容 --> <!-- 关于内容 -->
@@ -281,14 +281,19 @@ const goToMessageBoard = () => {
.about-page-container { .about-page-container {
padding: 20px 0; padding: 20px 0;
} }
.about-page-header{
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.about-content-wrapper { .about-content-wrapper {
width: 100%;
padding: 20px; padding: 20px;
margin: 0 15px; margin: 0 5%;
} }
.about-page-title { .about-page-title {
font-size: 1.8rem; font-size: 1.2rem;
} }
.about-page-subtitle { .about-page-subtitle {
@@ -306,7 +311,6 @@ const goToMessageBoard = () => {
.contact-options { .contact-options {
flex-direction: column; flex-direction: column;
} }
.skills-display-list { .skills-display-list {
gap: 8px; gap: 8px;
} }

View File

@@ -18,16 +18,16 @@
<div v-else-if="categories.length > 0" class="article-content" id="category-list"> <div v-else-if="categories.length > 0" class="article-content" id="category-list">
<p><strong></strong></p> <p><strong></strong></p>
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span> </strong></div> <div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span> </strong></div>
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid" class="category-group-container"> <div v-for="categoryGroup in categories" :key="categoryGroup.Categoryid" class="category-group-container">
<div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)"> <div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)">
<h2 id="header-id-1">{{ categoryGroup.typename }}</h2> <h2 id="header-id-1">{{ categoryGroup.typename }}</h2>
<!-- 计算该分类组中实际有文章的属性数量 --> <!-- 计算该分类组中实际有文章的属性数量 -->
<span class="badge badge-primary"> {{ categoryGroup.attributes.reduce((total, cat) => total + (cat.articles && cat.articles.length ? cat.articles.length : 0), 0) }} </span> <span class="badge badge-primary"> {{ categoryGroup.attributes.reduce((total, cat) => total + (cat.articles && cat.articles.length ? cat.articles.length : 0), 0) }} </span>
<ul class="category-item-list"> <ul class="category-item-list">
<div v-for="category in categoryGroup.attributes" :key="category.attributeid"> <div v-for="attribute in categoryGroup.attributes" :key="attribute.attributeid">
<li v-if="category.articles && category.articles.length > 0"> <li v-if="attribute.articles && attribute.articles.length > 0">
&nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename}}</kbd></a> &nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(attribute)"><kbd>{{ attribute.attributename}}</kbd></a>
&nbsp; ({{ category.articles.length }}) &nbsp; ({{ attribute.articles.length }})
</li> </li>
</div> </div>
</ul> </ul>
@@ -78,7 +78,7 @@ const fetchCategories = async () => {
// 使用Promise.all等待所有异步操作完成 // 使用Promise.all等待所有异步操作完成
await Promise.all( await Promise.all(
processedCategories.map(async category => { processedCategories.map(async category => {
const attributes = await categoryAttributeService.getAttributesByCategory(category.typeid); const attributes = await categoryAttributeService.getAttributesByCategory(category.categoryid);
if (attributes.code === 200 && Array.isArray(attributes.data)) { if (attributes.code === 200 && Array.isArray(attributes.data)) {
const processedAttributes = await Promise.all( const processedAttributes = await Promise.all(
attributes.data.map(async item => { attributes.data.map(async item => {
@@ -99,7 +99,7 @@ const fetchCategories = async () => {
); );
} }
categories.value = processedCategories; categories.value = processedCategories;
console.log('获取分类列表成功:', categories.value); // console.log('获取分类列表成功:', categories.value);
} catch (err) { } catch (err) {
console.error('获取分类列表失败:', err); console.error('获取分类列表失败:', err);
ElMessage.error('获取分类列表失败,请稍后重试'); ElMessage.error('获取分类列表失败,请稍后重试');
@@ -116,11 +116,10 @@ const handleCategoryClick = (attribute: any) => {
globalStore.removeValue('attribute') globalStore.removeValue('attribute')
globalStore.setValue('attribute', { globalStore.setValue('attribute', {
id: attribute.attributeid, id: attribute.attributeid,
name: attribute.typename name: attribute.attributename
}) })
console.log(attribute)
router.push({ router.push({
path: '/home/aericletype', path: '/home/aericleattribute',
}) })
} }

View File

@@ -483,18 +483,17 @@ onMounted(() => {
.error-state-container, .error-state-container,
.empty-state-container { .empty-state-container {
padding: 20px; padding: 20px;
margin: 0 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.article-main-title { .article-main-title {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.5; line-height: 1.5;
} }
.article-actions-group {
/* 文章操作按钮组 - 右对齐 */
justify-content: flex-end;
}
.article-meta-info { .article-meta-info {
flex-direction: column;
align-items: flex-start;
gap: 8px; gap: 8px;
font-size: 0.9rem; font-size: 0.9rem;
} }

View File

@@ -154,8 +154,8 @@ const loadCategories = async () => {
} }
} }
console.log('分类选项:', optionsData); // console.log('分类选项:', optionsData);
console.log('选中的值:', selectedValues.value); // console.log('选中的值:', selectedValues.value);
} }
} catch (error) { } catch (error) {
console.error('加载分类失败:', error); console.error('加载分类失败:', error);
@@ -196,8 +196,8 @@ const handleSave = (markdown) => {
markdownscontent: Articleform.value.markdownscontent markdownscontent: Articleform.value.markdownscontent
}; };
console.log('发送文章数据:', articleData); // console.log('发送文章数据:', articleData);
console.log('当前认证token是否存在:', !!localStorage.getItem('token')); // console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
// 根据articleid决定调用创建还是更新接口 // 根据articleid决定调用创建还是更新接口
const savePromise = Articleform.value.articleid === 0 const savePromise = Articleform.value.articleid === 0
@@ -205,7 +205,7 @@ const handleSave = (markdown) => {
: articleService.updateArticle(Articleform.value.articleid, articleData); : articleService.updateArticle(Articleform.value.articleid, articleData);
savePromise savePromise
.then(res => { .then(res => {
console.log('API响应:', res); // console.log('API响应:', res);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功'); ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
// 清除全局存储中的article // 清除全局存储中的article

View File

@@ -9,7 +9,7 @@
<!-- 文章列表 --> <!-- 文章列表 -->
<transition-group name="article-item" tag="div" class="article-list-content" v-else> <transition-group name="article-item" tag="div" class="article-list-content" v-else>
<div class="article-card" v-for="article in displayedArticles" :key="article.articleId" <div class="article-card" v-for="article in articleList" :key="article.articleId"
@click="handleArticleClick(article)"> @click="handleArticleClick(article)">
<h6 class="article-title">{{ article.title }}</h6> <h6 class="article-title">{{ article.title }}</h6>
<div v-if="article.marked" class="article-special-tag">标记文章</div> <div v-if="article.marked" class="article-special-tag">标记文章</div>
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<!-- 分页区域 --> <!-- 分页区域 -->
<PaginationComponent class="pagination-container" :list="articleList" :pageSize="10" @changePage="handleCurrentDataUpdate" /> <el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
</transition-group> </transition-group>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="!loading && articleList.length === 0" class="empty-state-container"> <div v-if="!loading && articleList.length === 0" class="empty-state-container">
@@ -55,22 +55,24 @@ const globalStore = useGlobalStore()
// 路由相关 // 路由相关
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
// 分页属性
const pageNum = ref(1) // 当前页码
const pageSize = ref(10) // 每页数量
const totalPages = ref(0) // 总页数
const pageLayout = ref('pager, next')// 分页布局
// 响应式状态 // 响应式状态
const articleList = ref([]) const articleList = ref([])
const displayedArticles = ref([])
const loading = ref(false) const loading = ref(false)
// ========== 分页数据处理 ========== // ========== 分页数据处理 ==========
/** /**
* 处理分页组件的数据更新 * 处理分页组件的数据更新
* @param {Array} data - 分页组件传递的当前页数据 * @param {Array} data - 分页组件传递的当前页数据
*/ */
const handleCurrentDataUpdate = (data) => {
displayedArticles.value = data
console.log('更新后的当前页数据:', data)
}
// ========== 文章数据获取模块 ========== // ========== 文章数据获取模块 ==========
@@ -81,25 +83,30 @@ const handleCurrentDataUpdate = (data) => {
const getArticlesByRoute = async () => { const getArticlesByRoute = async () => {
// 检查URL参数确定获取文章的方式 // 检查URL参数确定获取文章的方式
const pathSegment = route.path.split('/')[2] const pathSegment = route.path.split('/')[2]
console.log('当前路由分段:', pathSegment) // console.log('当前路由分段:', pathSegment)
switch (pathSegment) { switch (pathSegment) {
case 'aericletype': case 'aericleattribute':
// 按属性类型获取文章 // 按属性类型获取文章
const attributeData = globalStore.getValue('attribute') const attributeData = globalStore.getValue('attribute')
return await articleService.getArticlesByAttributeId(attributeData?.id) return await articleService.getPagedArticles({attributeid: attributeData?.id}, pageNum.value, pageSize.value)
case 'aericlecategory':
// 按分类类型获取文章
const categoryData = globalStore.getValue('category')
return await articleService.getPagedArticles({categoryid: categoryData?.id}, pageNum.value, pageSize.value)
case 'aericletitle': case 'aericletitle':
// 按标题搜索文章 // 按标题搜索文章
const titleData = globalStore.getValue('articleserarch') const titleData = globalStore.getValue('articleserarch')
return await articleService.getArticlesByTitle(titleData?.name) console.log('按标题搜索文章:', titleData.name)
return await articleService.getPagedArticles({title: titleData?.name}, pageNum.value, pageSize.value)
case 'aericlestatus': case 'aericlestatus':
// 按状态获取文章 // 按状态获取文章
const statusData = globalStore.getValue('articlestatus') const statusData = globalStore.getValue('articlestatus')
return await articleService.getArticlesByStatus(statusData?.status) return await articleService.getPagedArticles({status: statusData?.status}, pageNum.value, pageSize.value)
default: default:
// 默认获取所有文章 // 默认获取所有文章
console.log('获取所有文章列表') // console.log('获取所有文章列表')
return await articleService.getAllArticles() return await articleService.getPagedArticles({status: 1}, pageNum.value, pageSize.value)
} }
} }
@@ -148,14 +155,6 @@ const enrichArticlesWithExtraInfo = async (articles) => {
return enrichedArticles return enrichedArticles
} }
/**
* 初始化显示文章列表
* @param {Array} articles - 完整文章列表
*/
const initializeDisplayedArticles = (articles) => {
// 初始显示前3条数据
displayedArticles.value = articles.slice(0, 3)
}
/** /**
* 获取文章列表主函数 * 获取文章列表主函数
@@ -168,28 +167,23 @@ const fetchArticles = async () => {
// 1. 根据路由获取文章列表 // 1. 根据路由获取文章列表
response = await getArticlesByRoute() response = await getArticlesByRoute()
// console.log('更新后的文章列表:', response)
// 2. 确保数据存在 // 2. 确保数据存在
if (!response.data || !Array.isArray(response.data)) { if (!response.data.content || !Array.isArray(response.data.content)) {
articleList.value = [] articleList.value = []
displayedArticles.value = []
return return
} }
// 3. 为文章列表补充额外信息 // 3. 为文章列表补充额外信息
const enrichedArticles = await enrichArticlesWithExtraInfo(response.data) const enrichedArticles = await enrichArticlesWithExtraInfo(response.data.content)
// 4. 更新文章列表 // 4. 更新文章列表
articleList.value = enrichedArticles articleList.value = enrichedArticles
// 5. 初始化显示的文章
initializeDisplayedArticles(enrichedArticles)
} catch (error) { } catch (error) {
console.error('获取文章列表失败:', error) console.error('获取文章列表失败:', error)
ElMessage.error('获取文章列表失败,请稍后重试') ElMessage.error('获取文章列表失败,请稍后重试')
} finally { } finally {
console.log('最终文章列表数据:', articleList.value)
loading.value = false loading.value = false
} }
} }
@@ -222,7 +216,24 @@ const handleArticleClick = (article) => {
ElMessage.error('操作失败,请稍后重试') ElMessage.error('操作失败,请稍后重试')
} }
} }
/**
* 处理分页变化事件
* @param {number} newPage - 新的页码
*/
const changePage = (newPage) => {
fetchArticles()
// 根据当前页码优化分页布局
if (page === 1) {
// 第一页只显示页码和下一页按钮
pageLayout.value = 'pager, next'
} else if (page === totalPages.value) {
// 最后一页只显示上一页按钮和页码
pageLayout.value = 'prev, pager'
} else {
// 中间页显示完整的上一页、页码、下一页
pageLayout.value = 'prev, pager, next'
}
}
// ========== 生命周期和监听器 ========== // ========== 生命周期和监听器 ==========
/** /**
@@ -230,18 +241,13 @@ const handleArticleClick = (article) => {
*/ */
const handleRouteChange = () => { const handleRouteChange = () => {
fetchArticles() fetchArticles()
console.log('路由变化,重新获取文章列表') // console.log('路由变化,重新获取文章列表')
} }
/** /**
* 处理文章列表变化的回调函数 * 处理文章列表变化的回调函数
* @param {Array} newList - 新的文章列表 * @param {Array} newList - 新的文章列表
*/ */
const handleArticleListChange = (newList) => {
if (newList && newList.length > 0) {
displayedArticles.value = newList.slice(0, 3)
}
}
// 组件挂载时获取数据 // 组件挂载时获取数据
onMounted(() => { onMounted(() => {
@@ -260,7 +266,6 @@ watch(
// 监听原始文章列表变化,确保初始数据正确显示 // 监听原始文章列表变化,确保初始数据正确显示
watch( watch(
() => articleList.value, () => articleList.value,
handleArticleListChange,
{ deep: true } { deep: true }
) )
</script> </script>
@@ -414,7 +419,6 @@ watch(
/* 响应式设计 - 平板和手机 */ /* 响应式设计 - 平板和手机 */
@media (max-width: 768px) { @media (max-width: 768px) {
.article-list-container { .article-list-container {
padding: 0 10px;
} }
.article-card { .article-card {

View File

@@ -105,7 +105,7 @@ const togglePassword = () => {
// 验证单个字段 // 验证单个字段
const validateField = (field) => { const validateField = (field) => {
console.log('validateField', field) // console.log('validateField', field)
errors.value = {} errors.value = {}
if (field === 'username' && !loginForm.username) { if (field === 'username' && !loginForm.username) {
@@ -150,7 +150,7 @@ const handleLogin = async () => {
ElMessage.error('登录失败,请检查用户名和密码') ElMessage.error('登录失败,请检查用户名和密码')
return return
} }
console.log('登录成功', user) // console.log('登录成功', user)
// 这里应该是实际的登录API调用 // 这里应该是实际的登录API调用
// console.log('登录请求数据:', loginForm) // console.log('登录请求数据:', loginForm)
@@ -162,7 +162,7 @@ const handleLogin = async () => {
globalStore.setValue('loginhomestatus', { globalStore.setValue('loginhomestatus', {
status: 1 // 2:删除 1:已发布 0:发布登录 status: 1 // 2:删除 1:已发布 0:发布登录
}) })
console.log('globalStore.Login', globalStore.Login) // console.log('globalStore.Login', globalStore.Login)
// 保存登录状态token // 保存登录状态token
if (user.token) { if (user.token) {
localStorage.setItem('token', user.token) localStorage.setItem('token', user.token)

View File

@@ -15,8 +15,10 @@
<div class="comment-header-info"> <div class="comment-header-info">
<!-- 头像 --> <!-- 头像 -->
<div class="avatar-container"> <div class="avatar-container">
<img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)" class="user-avatar" alt="头像"> <img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)"
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(comment.displayName || comment.nickname)"> class="user-avatar" alt="头像">
<div v-else class="letter-avatar"
:style="getLetterAvatarStyle(comment.displayName || comment.nickname)">
{{ getInitialLetter(comment.displayName || comment.nickname) }} {{ getInitialLetter(comment.displayName || comment.nickname) }}
</div> </div>
</div> </div>
@@ -27,7 +29,7 @@
</div> </div>
<div class="comment-content-text" v-html="comment.content"></div> <div class="comment-content-text" v-html="comment.content"></div>
<div class="comment-actions-bar"> <div class="comment-actions-bar">
<span class="like-button" @click="handleLike(comment)"> <span class="like-button" v-if="false" @click="handleLike(comment)">
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes <span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes
}}</span> }}</span>
👍 👍
@@ -43,8 +45,10 @@
<div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper"> <div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper">
<div class="reply-header-info"> <div class="reply-header-info">
<div class="avatar-container"> <div class="avatar-container">
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)" class="user-avatar" alt="头像"> <img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)"
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(reply.displayName || reply.nickname)"> class="user-avatar" alt="头像">
<div v-else class="letter-avatar"
:style="getLetterAvatarStyle(reply.displayName || reply.nickname)">
{{ getInitialLetter(reply.displayName || reply.nickname) }} {{ getInitialLetter(reply.displayName || reply.nickname) }}
</div> </div>
</div> </div>
@@ -55,7 +59,8 @@
</div> </div>
<div class="reply-content-text">{{ reply.content }}</div> <div class="reply-content-text">{{ reply.content }}</div>
<div class="reply-actions-bar"> <div class="reply-actions-bar">
<span class="like-button" @click="handleLike(reply)">
<span class="like-button" v-if="false" @click="handleLike(reply)">
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes <span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes
}}</span> }}</span>
👍 👍
@@ -74,7 +79,10 @@
还没有留言快来抢沙发吧 还没有留言快来抢沙发吧
</div> </div>
</div> </div>
<!-- 分页按钮 -->
<div class="pagination-controls" v-if="totalPages > 1">
<el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
</div>
<!-- 留言输入区 --> <!-- 留言输入区 -->
<div class="comment-form-section"> <div class="comment-form-section">
<h2 class="comment-form-title">发送评论请正确填写邮箱地址否则将会当成垃圾评论处理</h2> <h2 class="comment-form-title">发送评论请正确填写邮箱地址否则将会当成垃圾评论处理</h2>
@@ -111,8 +119,8 @@
</el-form-item> </el-form-item>
</div> </div>
<div class="form-input-row"> <div class="form-input-row">
<el-form-item> <el-form-item class="submit-button-container">
<el-button type="primary" @click="onSubmit" :loading="submitting" <el-button type="primary" @click="onSubmit" class="form-submit-button" :loading="submitting"
:disabled="!form.content || !form.nickname"> :disabled="!form.content || !form.nickname">
发送 发送
</el-button> </el-button>
@@ -132,7 +140,6 @@ import { useGlobalStore } from '@/store/globalStore'
import { formatDate } from '@/utils/dateUtils' import { formatDate } from '@/utils/dateUtils'
// ============================== 组件初始化 ============================== // ============================== 组件初始化 ==============================
// 定义组件属性 // 定义组件属性
const props = defineProps({ const props = defineProps({
comments: { comments: {
@@ -157,7 +164,11 @@ const formRef = ref() // 表单引用
const captchaHint = ref('') // 验证码提示 const captchaHint = ref('') // 验证码提示
const captchaAnswer = ref('') // 验证码答案 const captchaAnswer = ref('') // 验证码答案
const showCaptchaHint = ref(false) // 是否显示验证码提示 const showCaptchaHint = ref(false) // 是否显示验证码提示
// 分页状态
const pageNum = ref(1) // 当前页码
const pageSize = ref(5) // 每页数量
const totalPages = ref(0) // 总页数
const pageLayout = ref('pager, next')// 分页布局
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
parentid: null, // 父留言ID parentid: null, // 父留言ID
@@ -168,7 +179,6 @@ const form = reactive({
email: '', // 邮箱 email: '', // 邮箱
captcha: '' // 验证码 captcha: '' // 验证码
}) })
// ============================== 表单验证规则 ============================== // ============================== 表单验证规则 ==============================
const rules = { const rules = {
@@ -397,7 +407,24 @@ const processReplyDisplayName = (reply, allReplies) => {
} }
// ============================== API调用模块 ============================== // ============================== API调用模块 ==============================
/**
* 切换分页
* @param {number} page - 目标页码
*/
const changePage = (page) => {
fetchMessages()
// 根据当前页码优化分页布局
if (page === 1) {
// 第一页只显示页码和下一页按钮
pageLayout.value = 'pager, next'
} else if (page === totalPages.value) {
// 最后一页只显示上一页按钮和页码
pageLayout.value = 'prev, pager'
} else {
// 中间页显示完整的上一页、页码、下一页
pageLayout.value = 'prev, pager, next'
}
}
/** /**
* 从后端获取留言列表 * 从后端获取留言列表
* 根据articleid决定获取文章留言还是全局留言 * 根据articleid决定获取文章留言还是全局留言
@@ -410,13 +437,13 @@ const fetchMessages = async () => {
// 获取文章ID优先使用props其次使用全局状态 // 获取文章ID优先使用props其次使用全局状态
const articleid = getArticleId() const articleid = getArticleId()
form.articleid = articleid form.articleid = articleid
// 获取留言数量
// 根据是否有文章ID选择不同的API调用 messageService.getMessageCountByArticleId(articleid).then(res => {
res = await (articleid if (res.code === 200) {
? messageService.getMessagesByArticleId(articleid) totalPages.value = Math.ceil(res.data / pageSize.value)
: fetchAllMessages() }
) })
res = await (messageService.getMessagesByPage(articleid, pageNum.value - 1, pageSize.value))
// 验证响应结果 // 验证响应结果
if (!res || !res.data) { if (!res || !res.data) {
handleEmptyResponse() handleEmptyResponse()
@@ -425,6 +452,18 @@ const fetchMessages = async () => {
// 处理留言数据 // 处理留言数据
messageBoardData.value = processMessageData(res.data) messageBoardData.value = processMessageData(res.data)
// 根据当前页码更新分页布局
if (pageNum.value === 1) {
// 第一页只显示页码和下一页按钮
pageLayout.value = 'pager, next'
} else if (pageNum.value === totalPages.value) {
// 最后一页只显示上一页按钮和页码
pageLayout.value = 'prev, pager'
} else {
// 中间页显示完整的上一页、页码、下一页
pageLayout.value = 'prev, pager, next'
}
} catch (error) { } catch (error) {
handleFetchError(error) handleFetchError(error)
} finally { } finally {
@@ -460,6 +499,7 @@ const fetchAllMessages = async () => {
const res = await messageService.getAllMessages() const res = await messageService.getAllMessages()
// 过滤掉articleid不为空的留言只保留articleid为空或不存在的留言 // 过滤掉articleid不为空的留言只保留articleid为空或不存在的留言
if (res && res.data) { if (res && res.data) {
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '') res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
} }
return res return res
@@ -600,7 +640,7 @@ const onSubmit = async () => {
try { try {
// 表单验证 // 表单验证
await validateForm() await validateForm()
console.log('提交留言表单:', form) // console.log('提交留言表单:', form)
submitting.value = true submitting.value = true
@@ -641,7 +681,7 @@ const handleMessageSubmission = async () => {
if (res.success) { if (res.success) {
// 提交成功 // 提交成功
ElMessage.success(form.parentid ? '回复成功' : '留言成功') ElMessage.success(form.parentid ? '回复成功' : '留言成功')
await fetchMessages() // 重新获取列表 await fetchMessages(0) // 重新获取列表
resetForm() resetForm()
// 如果是回复模式,取消回复状态 // 如果是回复模式,取消回复状态
@@ -902,16 +942,24 @@ onMounted(() => {
/** /**
* 内联表单输入行样式 * 内联表单输入行样式
* 用于将表单输入项与标签或其他元素对齐 * 用于将表单输入项水平均匀分布
*/ */
.form-input-row--inline { .form-input-row--inline {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 16px;
} }
.form-input-row--inline div:nth-child(2) { .form-input-row--inline .el-form-item {
margin-left: 9%; flex: 1;
margin-right: 9%; margin-right: 0;
margin-left: 0;
width: calc(33.33% - 10px);
}
.form-input-row--inline .el-input {
width: 100%;
} }
/* 回复项容器 */ /* 回复项容器 */
@@ -959,6 +1007,64 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
} }
/* 分页控件样式优化 */
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin: 30px 0;
padding: 16px;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.pagination-controls .el-pagination {
display: flex;
justify-content: center;
align-items: center;
}
.pagination-controls .el-pagination.is-background .el-pager li {
margin: 0 4px;
background-color: rgba(102, 161, 216, 0.1);
border: 1px solid rgba(102, 161, 216, 0.3);
color: #333;
border-radius: 6px;
transition: all 0.3s ease;
}
.pagination-controls .el-pagination.is-background .el-pager li:hover {
background-color: rgba(102, 161, 216, 0.3);
border-color: rgba(102, 161, 216, 0.5);
transform: translateY(-1px);
}
.pagination-controls .el-pagination.is-background .el-pager li.is-active {
background-color: rgba(102, 161, 216, 0.8);
border-color: rgba(102, 161, 216, 0.8);
color: white;
font-weight: 600;
}
.pagination-controls .el-pagination.is-background .btn-prev,
.pagination-controls .el-pagination.is-background .btn-next {
background-color: rgba(102, 161, 216, 0.1);
border: 1px solid rgba(102, 161, 216, 0.3);
color: #333;
border-radius: 6px;
transition: all 0.3s ease;
padding: 0 12px;
margin: 0 4px;
}
.pagination-controls .el-pagination.is-background .btn-prev:hover,
.pagination-controls .el-pagination.is-background .btn-next:hover {
background-color: rgba(102, 161, 216, 0.3);
border-color: rgba(102, 161, 216, 0.5);
color: #333;
}
/* 评论表单区域 */ /* 评论表单区域 */
.comment-form-section { .comment-form-section {
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
@@ -1056,17 +1162,27 @@ onMounted(() => {
padding: 16px; padding: 16px;
} }
.el-form-item {
width: 100%;
}
.form-input-row { .form-input-row {
flex-direction: column; flex-direction: column;
gap: 8px; }
.form-input-row--inline .el-form-item {
width: 100%;
} }
.comment-header-info, .comment-header-info,
.reply-header-info { .reply-header-info {
flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.form-submit-button {
width: 100%;
}
.user-avatar { .user-avatar {
margin-right: 0; margin-right: 0;
margin-bottom: 8px; margin-bottom: 8px;

View File

@@ -1,6 +1,19 @@
<template> <template>
<div class="nonsense-container"> <div class="nonsense-container">
<div class="nonsense-list"> <div class="nonsense-list">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state-container">
<el-skeleton :count="5" />
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-state-container">
<div>加载失败请稍后重试</div>
<el-button type="primary" @click="handleRetry">重试</el-button>
</div>
<!-- 空状态 -->
<div v-if="displayedNonsenseList.length === 0" class="empty-state-container">
<div>暂无吐槽内容</div>
</div>
<div class="nonsense-item" v-for="item in displayedNonsenseList" :key="item.id"> <div class="nonsense-item" v-for="item in displayedNonsenseList" :key="item.id">
<div class="nonsense-meta-info"> <div class="nonsense-meta-info">
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span> <span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span>
@@ -44,10 +57,21 @@ const charRefs = ref(new Map())
const charStyles = ref(new Map()) const charStyles = ref(new Map())
// 显示的吐槽内容列表 // 显示的吐槽内容列表
const displayedNonsenseList = ref([]) const displayedNonsenseList = ref([])
// 加载状态
const loading = ref(false)
// 错误状态
const error = ref(false)
// 处理分页数据更新 // 处理分页数据更新
const handleCurrentDataUpdate = (data) => { const handleCurrentDataUpdate = (data) => {
displayedNonsenseList.value = data displayedNonsenseList.value = data
console.log(data) // console.log(data)
}
// 重试加载
const handleRetry = () => {
error.value = false
loadNonsenseList()
} }
// 定时器引用 // 定时器引用
@@ -57,17 +81,24 @@ let colorChangeTimer = null
* 加载所有吐槽内容 * 加载所有吐槽内容
*/ */
const loadNonsenseList = async () => { const loadNonsenseList = async () => {
// 设置加载状态
loading.value = true
error.value = false
try { try {
const response = await nonsenseService.getNonsenseByStatus(1) const response = await nonsenseService.getNonsenseByStatus(1)
if (response.code === 200) { if (response.code === 200) {
nonsenseList.value = response.data nonsenseList.value = response.data
} else { } else {
ElMessage.error('加载吐槽内容失败') ElMessage.error('加载吐槽内容失败')
error.value = true
} }
} catch (error) { } catch (err) {
console.error('加载吐槽内容失败:', error) console.error('加载吐槽内容失败:', err)
error.value = true
} finally { } finally {
console.log('加载吐槽内容完成') // 结束加载状态
loading.value = false
// console.log('加载吐槽内容完成')
} }
} }
// 编辑吐槽内容 // 编辑吐槽内容
@@ -357,8 +388,7 @@ onBeforeUnmount(() => {
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.nonsense-container { .nonsense-container {
padding: 14px 4px 10px 4px; margin: 0;
margin: 0 8px;
} }
.nonsense-header h1 { .nonsense-header h1 {
@@ -370,9 +400,9 @@ onBeforeUnmount(() => {
line-height: 1.6; line-height: 1.6;
} }
.nonsense-list { /* .nonsense-list {
gap: 12px; gap: 12px;
} } */
.nonsense-item { .nonsense-item {
padding: 14px 16px 10px 16px; padding: 14px 16px 10px 16px;

View File

@@ -50,7 +50,7 @@ const totalPages = computed(() => {
if (!props.list || props.list.length === 0) return 0 if (!props.list || props.list.length === 0) return 0
// 如果列表长度小于pageSize不进行分组返回0表示不分页 // 如果列表长度小于pageSize不进行分组返回0表示不分页
if (props.list.length <= props.pageSize) return 0 if (props.list.length <= props.pageSize) return 0
console.log(props.list.length, props.pageSize) // console.log(props.list.length, props.pageSize)
// 列表长度小于等于pageSize时正常计算页数 // 列表长度小于等于pageSize时正常计算页数
// 如果能整除,直接返回商 // 如果能整除,直接返回商

View File

@@ -26,7 +26,16 @@ export default defineConfig({
}, },
}, },
server: { server: {
host: '0.0.0.0' host: '0.0.0.0',
proxy: {
// 配置API代理
'/api': {
// target: 'http://www.qf1121.top',
target: 'http://localhost:7071',
changeOrigin: true,
rewrite: (path) => path
}
}
}, },
}) })