- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
126 lines
4.1 KiB
Vue
126 lines
4.1 KiB
Vue
<template>
|
||
<main class="min-h-screen bg-gray-50 px-4 py-8">
|
||
<div class="mx-auto max-w-screen-lg space-y-6">
|
||
<a-breadcrumb>
|
||
<a-breadcrumb-item>
|
||
<NuxtLink to="/">首页</NuxtLink>
|
||
</a-breadcrumb-item>
|
||
<a-breadcrumb-item>
|
||
<NuxtLink to="/articles">文章</NuxtLink>
|
||
</a-breadcrumb-item>
|
||
<a-breadcrumb-item v-if="article?.categoryId">
|
||
<NuxtLink :to="`/article/${article.categoryId}`">
|
||
{{ article.categoryName || '栏目' }}
|
||
</NuxtLink>
|
||
</a-breadcrumb-item>
|
||
<a-breadcrumb-item>{{ articleTitle }}</a-breadcrumb-item>
|
||
</a-breadcrumb>
|
||
|
||
<a-skeleton v-if="pending" active :paragraph="{ rows: 10 }" />
|
||
|
||
<a-result
|
||
v-else-if="loadError"
|
||
status="error"
|
||
title="文章加载失败"
|
||
:sub-title="String(loadError)"
|
||
/>
|
||
|
||
<template v-else>
|
||
<a-card class="shadow-sm">
|
||
<template #title>
|
||
<div class="space-y-2">
|
||
<div class="text-2xl font-semibold text-gray-900">
|
||
{{ articleTitle }}
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500">
|
||
<span v-if="article?.categoryName">栏目:{{ article.categoryName }}</span>
|
||
<span v-if="article?.author">作者:{{ article.author }}</span>
|
||
<span v-if="article?.createTime">发布时间:{{ article.createTime }}</span>
|
||
<span v-if="article?.source">来源:{{ article.source }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<a-alert
|
||
v-if="articleOverview"
|
||
type="info"
|
||
show-icon
|
||
class="mb-5"
|
||
:message="articleOverview"
|
||
/>
|
||
|
||
<a-image
|
||
v-if="articleCover"
|
||
:src="articleCover"
|
||
:preview="false"
|
||
class="mb-5 w-full"
|
||
/>
|
||
|
||
<RichText :content="articleContent" />
|
||
</a-card>
|
||
</template>
|
||
</div>
|
||
</main>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { getCmsArticle } from '@/api/cms/cmsArticle'
|
||
import { listCmsArticleContent } from '@/api/cms/cmsArticleContent'
|
||
import RichText from '@/components/RichText.vue'
|
||
import { usePageSeo } from '@/composables/usePageSeo'
|
||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||
|
||
const route = useRoute()
|
||
|
||
const articleId = computed(() => {
|
||
const raw = route.params.id
|
||
const val = Array.isArray(raw) ? raw[0] : raw
|
||
const n = Number(val)
|
||
return Number.isFinite(n) ? n : NaN
|
||
})
|
||
|
||
async function loadArticleDetail(id: number): Promise<{ article: CmsArticle; content: string }> {
|
||
const article = await getCmsArticle(id)
|
||
|
||
// Prefer content field, fallback to detail/overview if API differs by model.
|
||
let content = String(article.content || article.detail || '').trim()
|
||
|
||
if (!content) {
|
||
try {
|
||
const list = await listCmsArticleContent({ articleId: id, limit: 1 })
|
||
const hit = Array.isArray(list) ? list[0] : null
|
||
if (hit?.content) content = String(hit.content).trim()
|
||
} catch {
|
||
// ignore content enrichment failures; still render metadata
|
||
}
|
||
}
|
||
|
||
return { article, content }
|
||
}
|
||
|
||
const { data, pending, error: loadError } = useAsyncData(
|
||
() => `cms-article-item-${articleId.value}`,
|
||
async () => {
|
||
if (!Number.isFinite(articleId.value)) throw new Error('无效的文章ID')
|
||
return loadArticleDetail(articleId.value)
|
||
},
|
||
{ watch: [articleId] }
|
||
)
|
||
|
||
const article = computed(() => data.value?.article)
|
||
const articleTitle = computed(() => article.value?.title?.trim() || `文章 ${articleId.value}`)
|
||
const articleOverview = computed(() => article.value?.overview?.trim() || '')
|
||
const articleCover = computed(() => (typeof article.value?.image === 'string' ? article.value?.image.trim() : ''))
|
||
const articleContent = computed(() => data.value?.content || '')
|
||
|
||
watchEffect(() => {
|
||
if (!Number.isFinite(articleId.value)) return
|
||
usePageSeo({
|
||
title: articleTitle.value,
|
||
description: articleOverview.value || articleTitle.value,
|
||
path: route.fullPath
|
||
})
|
||
})
|
||
</script>
|
||
|