feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
This commit is contained in:
125
app/pages/item/[id].vue
Normal file
125
app/pages/item/[id].vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user