Files
MyfronyProject/src/views/articlecontents.vue
qingfeng1121 f4263af343 refactor(前端): 重构前端代码结构并优化功能
重构路由配置和API调用逻辑,统一分页处理方式
优化分类和标签模块的交互,提取蒙版组件到主布局
调整样式和布局,增强响应式设计
更新接口字段名以保持前后端一致性
添加网站运行时间显示功能
2025-12-18 15:20:14 +08:00

545 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div id="article-detail-page">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state-container">
<el-skeleton :count="1" />
<el-skeleton :count="3" />
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state-container">
<el-alert :title="error" type="error" show-icon />
<el-button type="primary" @click="initializeArticleDetail">重新加载</el-button>
</div>
<!-- 文章详情 -->
<div v-else-if="article && Object.keys(article).length > 0" class="article-detail-wrapper">
<!-- 文章头部信息 -->
<div class="article-header-section">
<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 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>
<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>
<span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
</div>
</div>
</div>
<!-- 文章内容区域 -->
<div class="article-content-area">
<markdownViewer :markdownContent="article.markdownscontent" />
</div>
<!-- 文章底部信息 -->
<div class="article-footer-section">
<div class="article-tag-list">
</div>
<!-- 文章操作按钮 -->
<div class="article-actions-group">
<el-button type="primary" @click="navigateBack" plain>
返回
</el-button>
</div>
</div>
<!-- 相关文章推荐 -->
<div class="related-articles-section" v-if="relatedArticles.length > 0">
<h3>相关文章</h3>
<div class="related-articles-list">
<!-- <div v-for="item in relatedArticles" :key="item.articleid" class="related-article-card"
@click="handleRelatedArticleClick(item.articleid)">
<i class="el-icon-document"></i>
<span>{{ item.title }}</span>
</div> -->
</div>
</div>
</div>
<!-- 空状态 - 文章不存在 -->
<div v-else class="empty-state-container">
<el-empty description="文章不存在" />
</div>
<!-- 评论区组件 -->
<div>
<messageboard class="comment-section" />
</div>
</div>
</template>
<script setup lang="ts">
// =========================================================================
// 组件导入和初始化
// =========================================================================
/**
* 导入Vue核心功能
*/
import { ref, onMounted } from 'vue'
/**
* 导入路由相关功能
*/
import { useRouter } from 'vue-router'
/**
* 导入状态管理和UI组件
*/
import { useGlobalStore } from '@/store/globalStore'
import { ElMessage } from 'element-plus'
/**
* 导入类型和工具函数
*/
import type { Article } from '@/types'
import { formatRelativeTime } from '@/utils/dateUtils'
/**
* 导入子组件
*/
import messageboard from './messageboard.vue'
import markdownViewer from './markdown.vue'
// =========================================================================
// 路由和状态初始化
// =========================================================================
/**
* 初始化路由
*/
const router = useRouter()
/**
* 初始化全局状态管理
*/
const globalStore = useGlobalStore()
/**
* 响应式状态定义
*/
const article = ref<Article | null>(null) // 文章详情数据初始为null避免类型不匹配
const loading = ref(false) // 加载状态
const error = ref('') // 错误信息
const relatedArticles = ref<Article[]>([]) // 相关文章列表
// =========================================================================
// 文章数据处理模块
// =========================================================================
/**
* 获取文章ID
* @returns {number | null} 文章ID或null
*/
const getArticleId = (): number | null => {
return globalStore.getValue('articleInfo')?.articleid || null
}
/**
* 获取文章详情数据
* @returns {Promise<Article | null>} 文章数据或null
*/
const fetchArticleData = async (): Promise<Article | null> => {
try {
// 从全局状态获取文章信息
const response = await globalStore.getValue('articleInfo')
return response || null
} catch (err) {
console.error('获取文章数据失败:', err)
throw new Error('获取文章数据失败')
}
}
/**
* 增加文章浏览量
* @param {Article} articleData 文章数据
*/
const incrementViewCount = (articleData: Article): void => {
if (articleData.viewCount) {
articleData.viewCount++
} else {
articleData.viewCount = 1
}
}
/**
* 处理文章不存在的情况
*/
const handleArticleNotFound = (): void => {
error.value = '文章不存在或已被删除'
ElMessage.error(error.value)
}
/**
* 处理获取文章数据过程中的错误
* @param {unknown} err 错误对象
*/
const handleArticleFetchError = (err: unknown): void => {
error.value = err instanceof Error ? err.message : '获取文章详情失败,请稍后重试'
console.error('获取文章详情失败:', err)
ElMessage.error(error.value)
}
/**
* 初始化文章详情数据
*/
const initializeArticleDetail = async (): Promise<void> => {
try {
loading.value = true
error.value = ''
// 获取文章ID
const articleId = getArticleId()
if (!articleId) {
handleArticleNotFound()
return
}
// 获取文章数据
const articleData = await fetchArticleData()
if (articleData) {
article.value = articleData
// 增加浏览量
incrementViewCount(articleData)
} else {
handleArticleNotFound()
}
} catch (err) {
handleArticleFetchError(err)
} finally {
loading.value = false
}
}
// =========================================================================
// 导航和用户交互模块
// =========================================================================
/**
* 返回上一页
*/
const navigateBack = (): void => {
router.back()
}
/**
* 导航到相关文章
* @param {number} id - 相关文章ID
*/
const navigateToRelatedArticle = (id: number): void => {
router.push({
path: '/article/:url',
query: { url: id }
})
}
// =========================================================================
// 组件生命周期和初始化
// =========================================================================
/**
* 组件挂载时的初始化操作
*/
const setupComponent = (): void => {
initializeArticleDetail()
}
/**
* 组件挂载生命周期钩子
*/
onMounted(() => {
setupComponent()
})
</script>
<style scoped>
/* 文章详情容器 */
.article-detail-wrapper {
max-width: 900px;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* 加载状态容器 */
.loading-state-container {
max-width: 900px;
margin: 0 auto;
padding: 40px;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 错误状态容器 */
.error-state-container {
max-width: 900px;
margin: 0 auto;
padding: 60px 40px;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.error-state-container .el-button {
margin-top: 16px;
}
/* 空状态容器 */
.empty-state-container {
max-width: 900px;
margin: 0 auto;
padding: 80px 40px;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 文章头部区域 */
.article-header-section {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 2px solid #ecf0f1;
}
/* 文章标题 */
.article-main-title {
font-size: 2rem;
color: #2c3e50;
line-height: 1.4;
margin-bottom: 20px;
font-weight: 600;
}
/* 文章元信息 */
.article-meta-info {
display: flex;
flex-wrap: wrap;
gap: 20px;
color: #7f8c8d;
font-size: 0.95rem;
}
/* 元信息项 */
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
}
/* 文章内容区域 */
.article-content-area {
font-size: 1.05rem;
line-height: 1.8;
color: #34495e;
margin-bottom: 32px;
}
/* 文章内容中的段落 */
.article-content-area p {
margin-bottom: 16px;
text-align: justify;
}
/* 文章内容中的二级标题 */
.article-content-area h2 {
color: #2c3e50;
font-size: 1.5rem;
margin: 32px 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #ecf0f1;
font-weight: 600;
}
/* 文章内容中的三级标题 */
.article-content-area h3 {
color: #34495e;
font-size: 1.3rem;
margin: 24px 0 12px 0;
font-weight: 600;
}
/* 文章内容中的图片 */
.article-content-area img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 20px auto;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 文章内容中的引用 */
.article-content-area blockquote {
border-left: 4px solid #3498db;
padding-left: 16px;
color: #7f8c8d;
margin: 16px 0;
font-style: italic;
}
/* 文章底部区域 */
.article-footer-section {
padding-top: 24px;
border-top: 2px solid #ecf0f1;
margin-bottom: 32px;
}
/* 标签列表 */
.article-tag-list {
margin-bottom: 24px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* 文章操作按钮组 */
.article-actions-group {
display: flex;
justify-content: flex-start;
}
/* 相关文章区域 */
.related-articles-section {
padding-top: 32px;
border-top: 2px solid #ecf0f1;
}
/* 相关文章标题 */
.related-articles-section h3 {
font-size: 1.3rem;
color: #2c3e50;
margin-bottom: 16px;
font-weight: 600;
}
/* 相关文章列表容器 */
.related-articles-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 相关文章卡片 */
.related-article-card {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
/* 相关文章卡片悬停效果 */
.related-article-card:hover {
background-color: #e9ecef;
transform: translateX(5px);
border-color: #3498db;
}
.related-article-card i {
color: #3498db;
}
.related-article-card span {
font-size: 1rem;
color: #495057;
transition: color 0.3s ease;
}
.related-article-card:hover span {
color: #3498db;
}
/* 评论区样式 */
.comment-section {
margin-top: 32px;
}
/* 响应式设计 - 平板和手机 */
@media (max-width: 768px) {
#article-detail-page {
padding: 20px 0;
}
.article-detail-wrapper,
.loading-state-container,
.error-state-container,
.empty-state-container {
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.article-main-title {
font-size: 1.5rem;
line-height: 1.5;
}
.article-actions-group {
/* 文章操作按钮组 - 右对齐 */
justify-content: flex-end;
}
.article-meta-info {
gap: 8px;
font-size: 0.9rem;
}
.article-content-area {
font-size: 1rem;
line-height: 1.7;
}
.article-content-area h2 {
font-size: 1.3rem;
margin: 24px 0 12px 0;
}
.article-content-area h3 {
font-size: 1.2rem;
margin: 20px 0 10px 0;
}
.related-articles-section h3 {
font-size: 1.2rem;
}
.related-article-card {
padding: 10px;
gap: 8px;
}
.related-article-card span {
font-size: 0.95rem;
}
}
/* 响应式设计 - 小屏幕手机 */
@media (max-width: 480px) {
.article-detail-wrapper {
padding: 16px;
}
.article-main-title {
font-size: 1.35rem;
}
.article-meta-info {
font-size: 0.85rem;
}
}
</style>