feat: 添加分类树接口和分页组件,优化留言板功能

refactor: 重构文章服务和留言服务API调用方式
fix: 修复token验证逻辑和全局状态管理问题
style: 更新页脚样式和关于页面内容
docs: 添加类型定义和接口注释
This commit is contained in:
qingfeng1121
2025-12-23 13:57:35 +08:00
parent 8193bab566
commit 0151afcde7
19 changed files with 330 additions and 533 deletions

View File

@@ -0,0 +1,87 @@
<template>
<div class="custom-pagination">
<el-pagination
size="medium"
background
:layout="pageLayout"
v-model:current-page="localPageNum"
hide-on-single-page="true"
@current-change="handlePageChange"
:page-size="pageSize"
:page-count="totalPages"
class="mt-4"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
// 定义组件的属性
const props = defineProps<{
pageNum: number;
pageSize: number;
totalPages: number;
}>();
// 定义组件的事件
const emit = defineEmits<{
(e: 'update:pageNum', value: number): void;
(e: 'pageChange', value: number): void;
}>();
// 响应式状态
const localPageNum = ref(props.pageNum);
const localTotalPages = ref(props.totalPages);
// 计算属性:根据当前页码动态生成分页布局
const pageLayout = computed(() => {
if (localPageNum.value === 1) {
// 第一页只显示页码和下一页按钮
return 'pager, next';
} else if (localPageNum.value === localTotalPages.value) {
// 最后一页只显示上一页按钮和页码
return 'prev, pager';
} else {
// 中间页显示完整的上一页、页码、下一页
return 'prev, pager, next';
}
});
// 监听props变化更新本地状态
watch(
() => props.pageNum,
(newVal) => {
localPageNum.value = newVal;
}
);
watch(
() => props.totalPages,
(newVal) => {
localTotalPages.value = newVal;
}
);
// 处理页码变化
const handlePageChange = (newPage: number) => {
// 更新本地页码
localPageNum.value = newPage;
// 触发update:pageNum事件实现双向绑定
emit('update:pageNum', newPage);
// 触发pageChange事件通知父组件页码变化
emit('pageChange', newPage);
};
</script>
<style scoped>
.custom-pagination {
display: flex;
justify-content: center;
margin: 1rem 0;
}
.mt-4 {
margin-top: 1rem;
}
</style>

View File

@@ -110,7 +110,7 @@
</div>
</Transition>
<!-- 管理员 -->
<Establish class="establish-container" v-if="Login" />
<Establish class="establish-container" v-if="Login" />
</template>
<script lang="ts" setup>
@@ -243,7 +243,7 @@ const handleCategoryClick = (category: any) => {
id: category.categoryid,
name: category.categoryname
})
console.log(category)
// console.log(category)
router.push('/home/aericlecategory',)
closeCategoryModal()
}
@@ -317,24 +317,6 @@ const handleSelect = (key: string) => {
router.push({ path: '/' + key });
};
/**
* 设置当前激活的菜单项并存储路径信息
* @param {string} path - 当前路由路径
*/
const setActiveIndex = (path: string) => {
// 存储当前路径到全局状态
globalStore.setValue('localpath', {
name: path
});
// 特殊处理消息页面,清除文章信息
if (path === 'message') {
globalStore.removeValue('articleInfo');
}
// 更新激活菜单项
activeIndex.value = path;
};
/**
* 根据路由路径设置页面状态
@@ -532,11 +514,13 @@ const updateNavbarStyle = (scrollY: number) => {
const handleRouteChange = () => {
// 重新解析路由路径
rpsliturl = route.path.split('/');
// 移除全局状态值
removeGlobalStoreValue();
// 更新页面相关状态
updatePageState();
setActiveIndex(rpsliturl[1]);
// setActiveIndex(rpsliturl[1]);
updateArticleTitle();
activeIndex.value = rpsliturl[1];
// 页面跳转后回到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -553,7 +537,7 @@ const handleRouteChange = () => {
} else {
iscontentvisible.value = true;
startTypewriter(fullHeroText);
heroMarginBottom.value = `${5}%`;
heroMarginBottom.value = `${2.5}%`;
heroTransform.value = ``;
}
};
@@ -581,7 +565,26 @@ const initializePage = () => {
}
};
const removeGlobalStoreValue = () => {
// 根据所在页面判断是否需要移除全局状态值
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 首页移除全局状态值
// globalStore.removeValue(value);
}
// 不在articlesave移除文章全局状态值
if (rpsliturl[1] !== 'articlesave') {
globalStore.removeValue('updatearticle');
}
// 不在首页移除
if (rpsliturl[1] !== localhome) {
globalStore.removeValue('articlestatus');
globalStore.removeValue('attribute');
}
// 不在article或者不在articlesave移除文章全局状态值
if (rpsliturl[1] !== 'article' && rpsliturl[1] !== 'articlesave') {
globalStore.removeValue('articleInfo');
}
}
// ========== 生命周期钩子 ==========
/**
@@ -621,7 +624,7 @@ onUnmounted(() => {
*/
watch(
() => route.path,
handleRouteChange
handleRouteChange,
);
</script>

View File

@@ -149,7 +149,7 @@ const pageButtons = {
// 文章保存页面按钮
articlesave: [
{ id: 'view-articles', label: '查看文章列表', icon: 'icon-new-tag' },
{ id: 'view-articles', label: '文章', icon: 'icon-new-tag' },
...baseButtons
],
@@ -164,7 +164,8 @@ const pageButtons = {
const isbuttonsave = () => {
try {
// 获取当前页面路径名称
const currentPath = globalStore.getValue('localpath')?.name || 'default';
// const currentPath = globalStore.getValue('localpath')?.name || 'default';
const currentPath = router.currentRoute.value.path.split('/')[1];
// 返回对应页面的按钮配置,如果没有则返回默认配置
return pageButtons[currentPath] || pageButtons.default;
} catch (error) {

View File

@@ -9,12 +9,53 @@ const api = axios.create({
withCredentials: true // 允许跨域请求携带凭证如cookies
})
// 解析JWT token获取过期时间
const parseJwt = (token) => {
try {
// 移除Bearer前缀如果有
const pureToken = token.replace('Bearer ', '')
// 解析payload部分
const base64Url = pureToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
window
.atob(base64)
.split('')
.map((c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
.join('')
)
return JSON.parse(jsonPayload)
} catch (e) {
console.error('解析JWT token失败:', e)
return null
}
}
// 验证token是否过期
const isTokenExpired = (token) => {
const decodedToken = parseJwt(token)
if (!decodedToken || !decodedToken.exp) {
return true // 如果解析失败或没有过期时间认为token无效
}
// 比较过期时间和当前时间(转换为秒)
const currentTime = Math.floor(Date.now() / 1000)
return decodedToken.exp < currentTime
}
// 请求拦截器
api.interceptors.request.use(
config => {
// 从localStorage获取token
let token = localStorage.getItem('token')
if (token) {
// 验证token是否过期
if (isTokenExpired(token)) {
// token过期移除token并跳转到登录页
localStorage.removeItem('token')
ElMessage.error('登录已过期,请重新登录')
// window.location.href = '/login'
return Promise.reject(new Error('token已过期'))
}
// 确保不重复添加Bearer前缀
if (!token.startsWith('Bearer ')) {
config.headers['Authorization'] = `Bearer ${token}`

View File

@@ -7,14 +7,14 @@ import api from './apiService'
class ArticleService {
/**
* 分页查询文章列表
* @param {import('../types').PaginationParams} params - 分页查询参数
* @param {import('../types').ArticlePagination} params - 分页查询参数
* @param status 文章状态0未发表 1已发表 2已删除
* @param page 页码从0开始可选默认为0
* @param size 每页大小可选默认为10最大为100
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getPagedArticles(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}`)
return api.get(`/articles/status/page?title=${params.title || ''}&categoryid=${params.categoryid || ''}&attributeid=${params.attributeid || ''}&status=${params.status !== undefined ? params.status : 1}&page=${params.page || 0}&size=${params.size || 10}`)
}
/**
* 获取已发布文章列表

View File

@@ -10,7 +10,7 @@ class CategoryAttributeService {
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute[]>>}
*/
getAllAttributes() {
return api.get('/category-attributes')
return api.get('/categoryattributes')
}
/**
@@ -19,7 +19,7 @@ class CategoryAttributeService {
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
*/
getAttributeById(attributeid) {
return api.get(`/category-attributes/${attributeid}`)
return api.get(`/categoryattributes/${attributeid}`)
}
/**
@@ -28,7 +28,7 @@ class CategoryAttributeService {
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute[]>>}
*/
getAttributesByCategory(categoryid) {
return api.get(`/category-attributes/category/${categoryid}`)
return api.get(`/categoryattributes/category/${categoryid}`)
}
/**
@@ -37,7 +37,7 @@ class CategoryAttributeService {
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute>>}
*/
createAttribute(attributeData) {
return api.post('/category-attributes', attributeData)
return api.post('/categoryattributes', attributeData)
}
}

View File

@@ -11,6 +11,13 @@ class CategoryService {
getAllCategories() {
return api.get('/categories')
}
/**
* 获取分类树
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryTree[]>>}
*/
getCategoryTree() {
return api.get('/categories/tree')
}
}
// 创建并导出服务实例

View File

@@ -11,7 +11,7 @@ class MessageService {
* @returns {Promise<import('../types').ApiResponse<number>>}
*/
getMessageCountByArticleId(articleid) {
return apiService.get(`/messages/count?articleid=${articleid}`)
return apiService.get(`/messages/count/${articleid}`)
}
/**
@@ -33,9 +33,6 @@ class MessageService {
* @param {number} articleid - 文章ID
* @returns {Promise<import('../types').ApiResponse<import('../types').Message[]>>}
*/
getMessagesByArticleId(articleid) {
return apiService.get(`/messages/article/${articleid}`)
}
/**
* 获取所有留言

View File

@@ -10,12 +10,12 @@ class NonsenseService {
return apiService.get('/nonsense')
}
/**
* 根据状态获取疯言疯语内容
* @param {number} status - 状态值(1:已发表, 0:草稿)
* 根据分页信息获取疯言疯语内容
* @param {import('../types').NosensePageDto} page - 分页信息对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
*/
getNonsenseByStatus(status){
return apiService.get(`/nonsense/status/${status}`)
getNonsenseByStatus(page){
return apiService.get(`/nonsense/page?status=${page.status}&pageNum=${page.pageNum}&pageSize=${page.pageSize}`)
}
/**
* 保存疯言疯语内容

View File

@@ -32,9 +32,18 @@ export interface ArticleDto {
viewCount?: number
likes?: number
markdownscontent: string
}
/**
* 文章分页接口
*/
export interface ArticlePagination {
status?: number | undefined
title?: string | undefined
attributeid?: number | undefined
categoryid?: number | undefined
pagenum?: number | undefined
pagesize?: number | undefined
}
/**
* 留言类型接口
*/

View File

@@ -12,7 +12,7 @@
<!-- 版权信息 -->
<p class="footer-copyright">
网站所有权利保留 &copy; {{ new Date().getFullYear() }} 清疯不颠
网站所有权利保留 &copy; {{ new Date().getFullYear() }} <a href="https://www.qf1121.top" target="_blank">清疯不颠</a>
</p>
<!-- 运行时间 -->
@@ -74,6 +74,8 @@ onUnmounted(() => {
margin-top: 3rem;
padding: 2rem 0;
width: 100%;
position: relative;
bottom: 0;
}
/* 页脚内容 */

View File

@@ -92,7 +92,7 @@
<a href="https://gradle.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">Gradle</el-tag></a>
<a href="https://www.oracle.com/java/technologies/java17.html" target="_blank"
class="skill-tag-link"><el-tag type="success">Java 17+</el-tag></a>
class="skill-tag-link"><el-tag type="success">Java 8</el-tag></a>
</div>
</div>
@@ -124,7 +124,6 @@ const goToMessageBoard = () => {
<style scoped>
/* 主容器样式 */
.about-page-container {
width: 80%;
}
/* 内容包装器样式 */

View File

@@ -19,11 +19,11 @@
<h1 class="article-main-title">{{ article.title }}</h1>
<div class="article-meta-info">
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt) }}</span>
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
<span class="article-publish-date">{{ formatRelativeTime(article.createtime) }}</span>
<span v-if="article.attributename" class="article-category-badge">{{ article.attributename }} </span>
<span v-if="article.viewcount" class="article-views-count">{{ article.viewcount }} 阅读</span>
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
<span v-if="article.commentcount > 0" class="article-comments-count">{{ article.commentcount }} 评论</span>
<div class="article-status-badge-container" v-if="globalStore.Login">
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
@@ -100,7 +100,7 @@ import { ElMessage } from 'element-plus'
/**
* 导入类型和工具函数
*/
import type { Article } from '@/types'
// import type { Article } from '@/types'
import { formatRelativeTime } from '@/utils/dateUtils'
/**
@@ -126,10 +126,10 @@ const globalStore = useGlobalStore()
/**
* 响应式状态定义
*/
const article = ref<Article | null>(null) // 文章详情数据初始为null避免类型不匹配
const article = ref<any | null>(null) // 文章详情数据初始为null避免类型不匹配
const loading = ref(false) // 加载状态
const error = ref('') // 错误信息
const relatedArticles = ref<Article[]>([]) // 相关文章列表
const relatedArticles = ref<any[]>([]) // 相关文章列表
// =========================================================================
// 文章数据处理模块
@@ -147,10 +147,11 @@ const getArticleId = (): number | null => {
* 获取文章详情数据
* @returns {Promise<Article | null>} 文章数据或null
*/
const fetchArticleData = async (): Promise<Article | null> => {
const fetchArticleData = async (): Promise<any | null> => {
try {
// 从全局状态获取文章信息
const response = await globalStore.getValue('articleInfo')
console.log('获取文章数据:', response)
return response || null
} catch (err) {
console.error('获取文章数据失败:', err)
@@ -162,11 +163,11 @@ const fetchArticleData = async (): Promise<Article | null> => {
* 增加文章浏览量
* @param {Article} articleData 文章数据
*/
const incrementViewCount = (articleData: Article): void => {
if (articleData.viewCount) {
articleData.viewCount++
const incrementViewCount = (articleData: any): void => {
if (articleData.viewcount) {
articleData.viewcount++
} else {
articleData.viewCount = 1
articleData.viewcount = 1
}
}

View File

@@ -44,16 +44,15 @@
<!-- 编辑区域 -->
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" noImgZoomIn
noKatex />
<!-- 返回列表 -->
<el-button type="primary" @click="handleReturn">返回列表</el-button>
</div>
<!-- 返回列表 -->
<el-button class="return-btn" type="primary" @click="handleReturn">返回列表</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { onMounted, reactive, ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import { categoryService, categoryAttributeService, articleService } from '@/services';
@@ -61,9 +60,6 @@ import type { Article } from '@/types/index.ts';
import { ElMessage } from 'element-plus';
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 测试
// const id = 'preview-only';
// const scrollElement = document.documentElement;
// 路由
import router from '@/router/Router';
@@ -95,7 +91,6 @@ const statusoptions = ref([
value: '2'
}
]);
const categories = ref([]);
// 编辑文章
const editArticle = globalStore.getValue('updatearticle')
@@ -109,53 +104,21 @@ if (editArticle) {
// 初始化加载分类和属性构建级联选择器的options
const loadCategories = async () => {
try {
const response = await categoryService.getAllCategories();
const categories = ref([]);
const response = await categoryService.getCategoryTree();
if (response.code === 200) {
categories.value = response.data;
// 为每个分类加载对应的属性并构建options格式
const optionsData = await Promise.all(
categories.value.map(async (category) => {
try {
const attrResponse = await categoryAttributeService.getAttributesByCategory(category.typeid);
const children = attrResponse.code === 200 && attrResponse.data ?
attrResponse.data.map(attr => ({
label: attr.attributename,
value: attr.attributeid.toString()
})) : [];
return {
label: category.typename,
value: category.typeid.toString(),
children
};
} catch (error) {
console.error(`加载分类 ${category.typename} 的属性失败:`, error);
return {
label: category.typename,
value: category.typeid.toString(),
children: []
};
}
})
);
categorieoptions.value = optionsData;
// 如果是编辑模式且有attributeid设置级联选择器的默认值
if (Articleform.value.articleid !== 0 && Articleform.value.attributeid !== 0) {
// 查找属性所属的分类
for (const category of optionsData) {
const foundAttribute = category.children.find(attr => attr.value === Articleform.value.attributeid.toString());
if (foundAttribute) {
// 设置级联选择器的值:[分类ID, 属性ID]
selectedValues.value = [category.value, foundAttribute.value];
break;
}
}
}
// console.log('分类选项:', optionsData);
// console.log('选中的值:', selectedValues.value);
// 构建级联选择器的options
categorieoptions.value = categories.value.map(category => ({
label: category.name,
value: category.id,
children: category.children.map(child => ({
label: child.attributename,
value: child.attributeid
}))
}));
} else {
ElMessage.error('加载分类失败: ' + response.message);
}
} catch (error) {
console.error('加载分类失败:', error);
@@ -172,8 +135,6 @@ const handleCascaderChange = (values) => {
}
};
// 组件挂载时加载分类和属性
loadCategories();
const handleSave = (markdown) => {
Articleform.value.markdownscontent = markdown;
@@ -198,7 +159,6 @@ const handleSave = (markdown) => {
// console.log('发送文章数据:', articleData);
// console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
// 根据articleid决定调用创建还是更新接口
const savePromise = Articleform.value.articleid === 0
? articleService.createArticle(articleData)
@@ -267,6 +227,10 @@ const handleReturn = () => {
router.push('/home');
}
};
// 组件挂载时加载分类和属性
onMounted(() => {
loadCategories();
});
</script>
@@ -370,6 +334,9 @@ const handleReturn = () => {
border-radius: 8px;
}
.return-btn {
margin-top: 20px;
}
/* 响应式设计 - 平板 */
@media (max-width: 768px) {
.article-content-wrapper {
@@ -464,7 +431,6 @@ const handleReturn = () => {
color: #4a9eff;
}
}
/* 级联选择器ul元素自适应高度 */
:deep(.el-cascader-menu__list) {
height: auto !important;

View File

@@ -15,11 +15,11 @@
<div v-if="article.marked" class="article-special-tag">标记文章</div>
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
<div class="article-meta-info">
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span>
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
<span class="article-publish-date">{{ formatRelativeTime(article.createtime || article.createtime) }}</span>
<span v-if="article.attributename" class="article-category-badge">{{ article.attributename }} </span>
<span v-if="article.viewcount" class="article-views-count">{{ article.viewcount }} 阅读</span>
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
<span v-if="article.commentcount > 0" class="article-comments-count">{{ article.commentcount }} 评论</span>
<div class="article-status-badge-container" v-if="globalStore.Login">
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
@@ -28,7 +28,12 @@
</div>
</div>
<!-- 分页区域 -->
<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" />
<CustomPagination
v-model:pageNum="pageNum"
:pageSize="pageSize"
:totalPages="totalPages"
@pageChange="changePage"
/>
</transition-group>
<!-- 空状态 -->
<div v-if="!loading && articleList.length === 0" class="empty-state-container">
@@ -44,8 +49,8 @@ import { formatRelativeTime } from '@/utils/dateUtils'
import { formatContentPreview } from '@/utils/stringUtils'
import { ElMessage } from 'element-plus'
import { articleService, messageService, categoryAttributeService } from '@/services'
import PaginationComponent from '@/views/page.vue'
import { useGlobalStore } from '@/store/globalStore'
import CustomPagination from '@/components/CustomPagination.vue'
// ========== 组件初始化 ==========
@@ -59,7 +64,6 @@ const route = useRoute()
const pageNum = ref(1) // 当前页码
const pageSize = ref(10) // 每页数量
const totalPages = ref(0) // 总页数
const pageLayout = ref('pager, next')// 分页布局
// 响应式状态
const articleList = ref([])
@@ -84,7 +88,6 @@ const getArticlesByRoute = async () => {
// 检查URL参数确定获取文章的方式
const pathSegment = route.path.split('/')[2]
// console.log('当前路由分段:', pathSegment)
switch (pathSegment) {
case 'aericleattribute':
// 按属性类型获取文章
@@ -97,12 +100,13 @@ const getArticlesByRoute = async () => {
case 'aericletitle':
// 按标题搜索文章
const titleData = globalStore.getValue('articleserarch')
console.log('按标题搜索文章:', titleData.name)
// console.log('按标题搜索文章:', titleData.name)
return await articleService.getPagedArticles({title: titleData?.name}, pageNum.value, pageSize.value)
case 'aericlestatus':
// 按状态获取文章
const statusData = globalStore.getValue('articlestatus')
return await articleService.getPagedArticles({status: statusData?.status}, pageNum.value, pageSize.value)
// console.log('按状态获取文章:', statusData.status)
return await articleService.getPagedArticles({status: statusData.status}, pageNum.value, pageSize.value)
default:
// 默认获取所有文章
// console.log('获取所有文章列表')
@@ -110,50 +114,6 @@ const getArticlesByRoute = async () => {
}
}
/**
* 为单篇文章补充额外信息(留言数量、分类名称等)
* @param {Object} article - 文章对象
* @returns {Promise<Object>} 补充信息后的文章对象
*/
const enrichArticleWithExtraInfo = async (article) => {
try {
// 获取留言数量
const messageResponse = await messageService.getMessagesByArticleId(article.articleid)
// 获取分类名称
const categoryResponse = await categoryAttributeService.getAttributeById(article.attributeid)
// 设置分类名称
article.categoryName = categoryResponse?.data?.attributename || '未分类'
// 设置评论数量
article.commentCount = messageResponse?.data?.length || 0
// 标准化标记字段名
article.marked = article.mg
return article
} catch (err) {
console.error(`获取文章${article.articleid}额外信息失败:`, err)
// 错误情况下设置默认值
article.commentCount = 0
article.categoryName = '未分类'
return article
}
}
/**
* 批量为文章列表补充额外信息
* @param {Array} articles - 原始文章列表
* @returns {Promise<Array>} 补充信息后的文章列表
*/
const enrichArticlesWithExtraInfo = async (articles) => {
const enrichedArticles = []
for (const article of articles) {
const enrichedArticle = await enrichArticleWithExtraInfo(article)
enrichedArticles.push(enrichedArticle)
}
return enrichedArticles
}
/**
@@ -161,7 +121,6 @@ const enrichArticlesWithExtraInfo = async (articles) => {
*/
const fetchArticles = async () => {
let response = {}
try {
loading.value = true
@@ -170,16 +129,17 @@ const fetchArticles = async () => {
// console.log('更新后的文章列表:', response)
// 2. 确保数据存在
if (!response.data.content || !Array.isArray(response.data.content)) {
if (!response.data || !Array.isArray(response.data)) {
articleList.value = []
totalPages.value = 0
return
}
// 3. 为文章列表补充额外信息
const enrichedArticles = await enrichArticlesWithExtraInfo(response.data.content)
console.log('补充额外信息后的文章列表:', response.data)
// 4. 更新文章列表
articleList.value = enrichedArticles
articleList.value = response.data
// 5. 更新总页数
totalPages.value = response.totalPages
} catch (error) {
console.error('获取文章列表失败:', error)
ElMessage.error('获取文章列表失败,请稍后重试')
@@ -197,13 +157,10 @@ const fetchArticles = async () => {
const handleArticleClick = (article) => {
try {
// 增加文章浏览量(异步操作,不阻塞后续流程)
articleService.incrementArticleViews(article.articleId).catch(err => {
articleService.incrementArticleViews(article.articleid).catch(err => {
console.error('增加文章浏览量失败:', err)
})
// 清除之前的文章信息
globalStore.removeValue('articleInfo')
// 存储文章信息到全局状态
globalStore.setValue('articleInfo', article)
@@ -221,18 +178,8 @@ const handleArticleClick = (article) => {
* @param {number} newPage - 新的页码
*/
const changePage = (newPage) => {
pageNum.value = newPage
fetchArticles()
// 根据当前页码优化分页布局
if (page === 1) {
// 第一页只显示页码和下一页按钮
pageLayout.value = 'pager, next'
} else if (page === totalPages.value) {
// 最后一页只显示上一页按钮和页码
pageLayout.value = 'prev, pager'
} else {
// 中间页显示完整的上一页、页码、下一页
pageLayout.value = 'prev, pager, next'
}
}
// ========== 生命周期和监听器 ==========

View File

@@ -31,7 +31,7 @@
<div class="comment-actions-bar">
<span class="like-button" v-if="false" @click="handleLike(comment)">
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes
}}</span>
}}</span>
👍
</span>
<span class="reply-button" @click="handleReply(null, comment)">回复</span>
@@ -40,9 +40,9 @@
v-if="globalStore.Login">删除</span>
</div>
<!-- 回复列表 -->
<div v-if="comment.replies && comment.replies && comment.replies.length > 0"
<div v-if="comment.children && comment.children && comment.children.length > 0"
class="reply-list-container">
<div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper">
<div v-for="reply in comment.children" :key="reply.messageid" class="reply-item-wrapper">
<div class="reply-header-info">
<div class="avatar-container">
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)"
@@ -53,7 +53,7 @@
</div>
</div>
<div class="user-meta-info">
<div class="user-nickname">{{ reply.displayName || reply.nickname }}</div>
<div class="user-nickname">{{processMessageData(reply) }}</div>
<div class="comment-time">{{ formatDate(reply.createdAt) || '刚刚' }}</div>
</div>
</div>
@@ -62,7 +62,7 @@
<span class="like-button" v-if="false" @click="handleLike(reply)">
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes
}}</span>
}}</span>
👍
</span>
<span class="reply-button" @click="handleReply(comment, reply)">回复</span>
@@ -70,19 +70,23 @@
<span class="delete-button" @click="handleDelete(reply.messageid)"
v-if="globalStore.Login">删除</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无留言提示 -->
<div v-if="!loading && messageBoardData.length === 0" class="empty-message-state">
<div v-if="!messageBoardData" class="empty-message-state">
还没有留言快来抢沙发吧
</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>
<CustomPagination
v-model:pageNum="pageNum"
:pageSize="pageSize"
:totalPages="totalPages"
@pageChange="changePage"
/>
<!-- 留言输入区 -->
<div class="comment-form-section">
<h2 class="comment-form-title">发送评论请正确填写邮箱地址否则将会当成垃圾评论处理</h2>
@@ -138,6 +142,9 @@ import { messageService } from '@/services'
import { ElMessage, ElForm } from 'element-plus'
import { useGlobalStore } from '@/store/globalStore'
import { formatDate } from '@/utils/dateUtils'
import CustomPagination from '@/components/CustomPagination.vue'
// ============================== 组件初始化 ==============================
// 定义组件属性
@@ -168,7 +175,6 @@ const showCaptchaHint = ref(false) // 是否显示验证码提示
const pageNum = ref(1) // 当前页码
const pageSize = ref(5) // 每页数量
const totalPages = ref(0) // 总页数
const pageLayout = ref('pager, next')// 分页布局
// 表单数据
const form = reactive({
parentid: null, // 父留言ID
@@ -340,91 +346,6 @@ const getLetterAvatarStyle = (name) => {
// ============================== 数据处理模块 ==============================
/**
* 处理留言数据,构建留言与回复的层级结构
* @param {Array} messages - 原始留言数据
* @returns {Array} - 处理后的留言数据(包含回复数组)
*/
const processMessageData = (messages) => {
// 为主留言添加replies数组
const allMessagesWithReplies = messages.map(msg => ({
...msg,
replies: []
}))
// 分离主留言和回复
const mainMessages = []
const replies = []
allMessagesWithReplies.forEach(msg => {
if (msg.parentid && msg.parentid > 0) {
replies.push(msg)
} else {
mainMessages.push(msg)
}
})
// 将回复添加到对应的主留言中并处理@回复显示
processRepliesForMainMessages(mainMessages, replies)
return mainMessages
}
/**
* 处理回复数据,将其添加到对应的主留言中
* @param {Array} mainMessages - 主留言数组
* @param {Array} replies - 回复数组
*/
const processRepliesForMainMessages = (mainMessages, replies) => {
replies.forEach(reply => {
// 找到父留言
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
if (parentMsg) {
// 处理@回复的显示名称
processReplyDisplayName(reply, replies)
parentMsg.replies.push(reply)
}
})
}
/**
* 处理回复的显示名称
* 如果是回复其他回复,添加@标记
* @param {Object} reply - 回复对象
* @param {Array} allReplies - 所有回复数组
*/
const processReplyDisplayName = (reply, allReplies) => {
if (reply.replyid) {
const repliedMsg = allReplies.find(msg => msg.messageid === reply.replyid)
if (repliedMsg) {
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
} else {
reply.displayName = reply.nickname
}
} else {
reply.displayName = reply.nickname
}
}
// ============================== 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决定获取文章留言还是全局留言
@@ -437,33 +358,15 @@ const fetchMessages = async () => {
// 获取文章ID优先使用props其次使用全局状态
const articleid = getArticleId()
form.articleid = articleid
// 获取留言数量
messageService.getMessageCountByArticleId(articleid).then(res => {
if (res.code === 200) {
totalPages.value = Math.ceil(res.data / pageSize.value)
}
})
res = await (messageService.getMessagesByPage(articleid, pageNum.value - 1, pageSize.value))
// 验证响应结果
if (!res || !res.data) {
if (res.code != 200) {
handleEmptyResponse()
return
}
// 处理留言数据
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'
}
messageBoardData.value = res.data
totalPages.value = res.totalPages
} catch (error) {
handleFetchError(error)
} finally {
@@ -471,6 +374,30 @@ const fetchMessages = async () => {
}
}
/**
* 处理留言数据
* @param {Array} messages - 原始留言数据
* @returns {Array} - 处理后的留言数据(包含回复数组)
*/
const processMessageData = (messages) => {
if (messages.replyToNickname !== null) {
return `${messages.nickname} @ ${messages.replyToNickname}`
}
return messages.nickname
}
// ============================== API调用模块 ==============================
/**
* 切换分页
* @param {number} page - 目标页码
*/
const changePage = (page) => {
pageNum.value = page
fetchMessages()
}
/**
* 获取文章ID
* 优先级props.comments > globalStore中的articleInfo
@@ -478,7 +405,6 @@ const fetchMessages = async () => {
*/
const getArticleId = () => {
let articleid = props.comments || null
if (!articleid) {
// 安全获取文章ID如果globalStore中没有articleInfo则返回null
const articleData = globalStore.getValue('articleInfo')
@@ -486,24 +412,9 @@ const getArticleId = () => {
? articleData.articleid
: null
}
return articleid
}
/**
* 获取所有留言并过滤
* 只保留articleid为空或不存在的全局留言
* @returns {Promise} - API响应
*/
const fetchAllMessages = async () => {
const res = await messageService.getAllMessages()
// 过滤掉articleid不为空的留言只保留articleid为空或不存在的留言
if (res && res.data) {
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
}
return res
}
/**
* 处理空响应

View File

@@ -30,9 +30,15 @@
:style="getCharStyle(item.id, index)">{{ char }}</span>
</div>
</div>
// 分页区域
<PaginationComponent class="pagination-container" :list="nonsenseList" :pageSize="10"
@changePage="handleCurrentDataUpdate" />
<!-- 分页区域 -->
<div class="pagination-container">
<CustomPagination
v-model:page-num="pageNum"
:page-size="pageSize"
:total-pages="totalPages"
@page-change="handlePageChange"
/>
</div>
</div>
</div>
</template>
@@ -42,9 +48,9 @@ import { ref, onMounted, onBeforeUnmount } from 'vue'
import { nonsenseService } from '@/services'
import { ElMessage } from 'element-plus'
import { ElMessageBox } from 'element-plus'
import PaginationComponent from '@/views/page.vue'
import { formatRelativeTime } from '@/utils/dateUtils'
import { useGlobalStore } from '@/store/globalStore'
import CustomPagination from '@/components/CustomPagination.vue'
const globalStore = useGlobalStore()
/**
* 吐槽数据列表
@@ -61,15 +67,21 @@ const displayedNonsenseList = ref([])
const loading = ref(false)
// 错误状态
const error = ref(false)
// 分页相关
const pageNum = ref(1) // 当前页码
const pageSize = ref(5) // 每页数量
const totalPages = ref(0) // 总页数
// 处理分页数据更新
const handleCurrentDataUpdate = (data) => {
displayedNonsenseList.value = data
// console.log(data)
// 处理页码变化
const handlePageChange = (newPage) => {
pageNum.value = newPage
loadNonsenseList()
}
// 重试加载
const handleRetry = () => {
// 重置分页参数
pageNum.value = 1
totalPages.value = 0
error.value = false
loadNonsenseList()
}
@@ -85,9 +97,22 @@ const loadNonsenseList = async () => {
loading.value = true
error.value = false
try {
const response = await nonsenseService.getNonsenseByStatus(1)
const response = await nonsenseService.getNonsenseByStatus({
status: 1,
pageNum: pageNum.value - 1,
pageSize: pageSize.value
})
// console.log(response)
if (response.code === 200) {
nonsenseList.value = response.data
// 计算总页数
totalPages.value = response.totalPages
// 显示当前页的内容
displayedNonsenseList.value = response.data
// 初始化字符样式
// initializeCharStyles()
// 开始颜色变化定时器
console.log(displayedNonsenseList.value)
} else {
ElMessage.error('加载吐槽内容失败')
error.value = true

View File

@@ -1,199 +0,0 @@
<template>
<div class="pagination-container">
<!-- 数据渲染区域 -->
<!-- 分页按钮区域 - 只在需要分页时显示totalPages > 0 list.length <= pageSize -->
<div class="pagination-controls" v-if="totalPages > 1 ">
<button
v-for="page in totalPages"
:key="page"
:class="['pagination-btn', { active: currentPage === page }]"
@click="changePage(page)"
>
{{ page }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
// 定义组件名称
defineOptions({
name: 'PaginationComponent'
})
// 定义props
interface Props {
list: any[]
pageSize?: number
}
const props = withDefaults(defineProps<Props>(), {
list: () => [],
pageSize: 10
})
// 定义响应式数据
const currentPage = ref(1)
const groupedData = ref<any[]>([])
// 定义事件
const emit = defineEmits<{
changePage: [data: any[]]
}>()
// 计算属性 - 计算总页数
const totalPages = computed(() => {
// 如果列表为空返回0
if (!props.list || props.list.length === 0) return 0
// 如果列表长度小于pageSize不进行分组返回0表示不分页
if (props.list.length <= props.pageSize) return 0
// console.log(props.list.length, props.pageSize)
// 列表长度小于等于pageSize时正常计算页数
// 如果能整除,直接返回商
if (props.list.length % props.pageSize === 0) {
return props.list.length / props.pageSize
}
// 不能整除时,返回商+1用户需求将余数添加到最后一组
return Math.floor(props.list.length / props.pageSize) + 1
})
// 计算当前页数据
const currentPageData = computed(() => {
// 如果列表为空,返回空数组
if (!props.list || props.list.length === 0) {
return []
}
// 如果列表长度小于pageSize不分组直接返回完整列表
if (props.list.length <= props.pageSize) {
return [...props.list]
}
// 如果总页数为0列表为空的情况已处理返回空数组
if (totalPages.value === 0) {
return []
}
// 确保当前页码不超过总页数
if (currentPage.value > totalPages.value) {
currentPage.value = 1
}
// 正常分组逻辑
const startIndex = (currentPage.value - 1) * props.pageSize
let endIndex = startIndex + props.pageSize
// 对于最后一页,确保不会超出列表范围
if (currentPage.value === totalPages.value) {
endIndex = props.list.length
}
return props.list.slice(startIndex, endIndex)
})
// 方法 - 重新实现分组逻辑
const groupData = () => {
groupedData.value = []
// 如果列表为空或长度大于pageSize不进行分组
if (!props.list || props.list.length === 0 || props.list.length > props.pageSize) {
return
}
const listLength = props.list.length
// 正常分组逻辑,余数会自动添加到最后一组
for (let i = 0; i < listLength; i += props.pageSize) {
groupedData.value.push(props.list.slice(i, i + props.pageSize))
}
}
const changePage = (page: number) => {
// 只有在需要分页的情况下才处理页码变化
if (totalPages.value > 0 && page >= 1 && page <= totalPages.value) {
currentPage.value = page
// 发出事件,传递当前页数据
emit('changePage', currentPageData.value)
} else if (props.list && props.list.length > 0) {
// 当不分组时,直接传递完整列表
emit('changePage', [...props.list])
}
}
// 监听数据变化
watch(() => props.list, () => {
currentPage.value = 1
groupData()
// 数据变化时,立即发出当前数据
emit('changePage', currentPageData.value)
}, { deep: true })
// 生命周期钩子
onMounted(() => {
groupData()
// 组件挂载时,立即发出初始数据
emit('changePage', currentPageData.value)
})
</script>
<style scoped>
.pagination-container {
width: 100%;
padding: 20px;
box-sizing: border-box;
}
.data-content {
margin-bottom: 20px;
}
.data-item {
padding: 10px;
border: 1px solid #e0e0e0;
margin-bottom: 8px;
border-radius: 4px;
background-color: #f5f5f5;
}
.no-data {
text-align: center;
color: #999;
padding: 40px 0;
}
.pagination-controls {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
.pagination-btn {
padding: 8px 16px;
border: 1px solid #d0d0d0;
background-color: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.pagination-btn:hover {
background-color: #f0f0f0;
}
.pagination-btn.active {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.pagination-btn.active:hover {
background-color: #40a9ff;
}
</style>

View File

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