204 lines
5.3 KiB
Vue
204 lines
5.3 KiB
Vue
<script lang="ts" setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { getArticleDetail, type Article } from '~/config/api'
|
|
|
|
// 获取路由参数
|
|
const route = useRoute()
|
|
const articleId = route.params.id as string
|
|
|
|
// 响应式数据
|
|
const article = ref<Article | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
|
|
// 格式化时间
|
|
const formatTime = (timeString: string): string => {
|
|
if (!timeString) return ''
|
|
const date = new Date(timeString)
|
|
if (isNaN(date.getTime())) return timeString
|
|
|
|
const Y = date.getFullYear()
|
|
const M = String(date.getMonth() + 1).padStart(2, '0')
|
|
const D = String(date.getDate()).padStart(2, '0')
|
|
const h = String(date.getHours()).padStart(2, '0')
|
|
const m = String(date.getMinutes()).padStart(2, '0')
|
|
return `${Y}-${M}-${D} ${h}:${m}`
|
|
}
|
|
|
|
// 获取文章详情
|
|
const fetchArticleDetail = async (): Promise<void> => {
|
|
if (!articleId) {
|
|
error.value = '文章ID不存在'
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const data = await getArticleDetail(articleId)
|
|
if (data) {
|
|
article.value = data
|
|
// 设置页面标题
|
|
useHead({
|
|
title: data.title || '文章详情',
|
|
meta: [
|
|
{ name: 'description', content: data.brief || data.title || '' }
|
|
]
|
|
})
|
|
} else {
|
|
error.value = '文章不存在'
|
|
}
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : '获取文章详情失败'
|
|
console.error('获取文章详情时发生错误:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchArticleDetail()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UPage>
|
|
<UPageBody>
|
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
<!-- 加载状态 -->
|
|
<div v-if="loading" class="text-center py-20">
|
|
<div class="text-gray-600 text-lg animate-pulse">正在加载文章...</div>
|
|
</div>
|
|
|
|
<!-- 错误状态 -->
|
|
<div v-else-if="error" class="text-center py-20">
|
|
<div class="text-red-600 text-xl mb-6">{{ error }}</div>
|
|
<button
|
|
class="inline-block px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white text-base font-normal rounded transition-all duration-150 ease-in-out cursor-pointer border border-transparent"
|
|
@click="fetchArticleDetail">
|
|
重新加载
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 文章内容 -->
|
|
<article v-else-if="article" class="bg-white rounded-lg shadow-sm p-6 md:p-8 lg:p-10">
|
|
<!-- 文章标题 -->
|
|
<header class="mb-8 pb-6 border-b border-gray-200">
|
|
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-4 leading-tight">
|
|
{{ article.title }}
|
|
</h1>
|
|
<div class="flex items-center text-gray-500 text-sm md:text-base">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
<time :datetime="article.createTime">{{ formatTime(article.createTime) }}</time>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 文章简介 -->
|
|
<div v-if="article.brief" class="mb-8 p-4 bg-gray-50 rounded-lg border-l-4 border-blue-500">
|
|
<p class="text-gray-700 text-base md:text-lg leading-relaxed">{{ article.brief }}</p>
|
|
</div>
|
|
|
|
<!-- 文章正文内容 -->
|
|
<div
|
|
v-if="article.content"
|
|
class="prose prose-lg max-w-none article-content"
|
|
v-html="article.content">
|
|
</div>
|
|
|
|
<!-- 文章为空提示 -->
|
|
<div v-else class="text-center py-12 text-gray-500">
|
|
暂无内容
|
|
</div>
|
|
</article>
|
|
|
|
|
|
</div>
|
|
</UPageBody>
|
|
</UPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@reference "~/assets/css/main.css";
|
|
|
|
/* 文章内容样式优化 */
|
|
.article-content :deep(h1),
|
|
.article-content :deep(h2),
|
|
.article-content :deep(h3),
|
|
.article-content :deep(h4) {
|
|
@apply font-bold text-gray-900 mt-8 mb-4;
|
|
}
|
|
|
|
.article-content :deep(h1) {
|
|
@apply text-3xl;
|
|
}
|
|
|
|
.article-content :deep(h2) {
|
|
@apply text-2xl;
|
|
}
|
|
|
|
.article-content :deep(h3) {
|
|
@apply text-xl;
|
|
}
|
|
|
|
.article-content :deep(h4) {
|
|
@apply text-lg;
|
|
}
|
|
|
|
.article-content :deep(p) {
|
|
@apply text-gray-700 leading-relaxed mb-4;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.article-content :deep(img) {
|
|
@apply max-w-full h-auto rounded-lg my-6 mx-auto block;
|
|
}
|
|
|
|
.article-content :deep(ul),
|
|
.article-content :deep(ol) {
|
|
@apply mb-4 pl-6;
|
|
}
|
|
|
|
.article-content :deep(li) {
|
|
@apply mb-2 text-gray-700 leading-relaxed;
|
|
}
|
|
|
|
.article-content :deep(strong) {
|
|
@apply font-semibold text-gray-900;
|
|
}
|
|
|
|
.article-content :deep(a) {
|
|
@apply text-blue-600 hover:text-blue-800 underline;
|
|
}
|
|
|
|
.article-content :deep(blockquote) {
|
|
@apply border-l-4 border-gray-300 pl-4 italic text-gray-600 my-4;
|
|
}
|
|
|
|
.article-content :deep(code) {
|
|
@apply bg-gray-100 px-2 py-1 rounded text-sm font-mono;
|
|
}
|
|
|
|
.article-content :deep(pre) {
|
|
@apply bg-gray-100 p-4 rounded-lg overflow-x-auto my-4;
|
|
}
|
|
|
|
.article-content :deep(pre code) {
|
|
@apply bg-transparent p-0;
|
|
}
|
|
|
|
/* 确保图片响应式 */
|
|
.article-content :deep(img) {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
/* 段落间距优化 */
|
|
.article-content :deep(p + p) {
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|
|
|