feat(pages): 添加页面详情页和产品详情页路由

- 实现页面详情页功能,支持通过ID动态加载CMS导航内容
- 添加页面标题和内容渲染逻辑,支持多种内容字段候选
- 集成SEO优化,自动设置页面标题、描述和路径信息
- 实现错误处理机制,包括加载失败和页面不存在的情况
- 添加重试和返回首页的功能按钮
- 创建产品详情页基础路由结构(占位文件)
This commit is contained in:
2026-01-29 14:42:29 +08:00
parent 6070a8b9cd
commit 5333b5e42f
2 changed files with 135 additions and 0 deletions

124
app/pages/page/[id].vue Normal file
View File

@@ -0,0 +1,124 @@
<template>
<div class="mx-auto max-w-screen-md px-4 py-12">
<a-spin v-if="pending" />
<a-result
v-else-if="loadError"
status="error"
title="页面加载失败"
:sub-title="loadError.message"
>
<template #extra>
<a-space>
<a-button type="primary" @click="refresh()">重试</a-button>
<a-button @click="navigateTo('/')">返回首页</a-button>
</a-space>
</template>
</a-result>
<a-result
v-else-if="!navigation"
status="404"
title="页面不存在"
sub-title="未找到对应的页面内容"
>
<template #extra>
<a-space>
<a-button type="primary" @click="navigateTo('/')">返回首页</a-button>
</a-space>
</template>
</a-result>
<template v-else>
<a-typography-title :level="1" class="!mb-6">{{ pageTitle }}</a-typography-title>
<a-alert v-if="!pageContent" class="mb-6" type="info" show-icon message="暂无内容" />
<RichText v-else :content="pageContent" />
</template>
</div>
</template>
<script setup lang="ts">
import { getCmsNavigation } from '@/api/cms/cmsNavigation'
import type { CmsNavigation } from '@/api/cms/cmsNavigation/model'
import { usePageSeo } from '@/composables/usePageSeo'
const route = useRoute()
const id = computed(() => {
const raw = route.params.id
const text = Array.isArray(raw) ? raw[0] : raw
const n = Number(text)
return Number.isFinite(n) ? n : NaN
})
const {
data: navigation,
pending,
error: loadError,
refresh
} = await useAsyncData<CmsNavigation | null>(
() => `cms-navigation-${String(route.params.id)}`,
async () => {
if (!Number.isFinite(id.value)) return null
return await getCmsNavigation(id.value)
},
{ watch: [id] }
)
function pickString(obj: Record<string, unknown>, key: string) {
const v = obj[key]
return typeof v === 'string' ? v.trim() : ''
}
function coerceContent(value: unknown): string {
if (typeof value === 'string') return value
if (value && typeof value === 'object') {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
return ''
}
const pageTitle = computed(() => {
const nav = navigation.value as unknown as Record<string, unknown> | null
if (!nav) return '页面'
return pickString(nav, 'title') || pickString(nav, 'label') || `页面 ${String(route.params.id)}`
})
const pageContent = computed(() => {
const nav = navigation.value as unknown as Record<string, unknown> | null
if (!nav) return ''
// Different CMS deployments may store content under different keys.
const candidates = [
'content',
'html',
'body',
'text',
'pageContent',
'articleContent',
'suffix',
'comments',
'meta',
'style'
]
for (const k of candidates) {
const val = nav[k]
const text = coerceContent(val).trim()
if (text) return text
}
return ''
})
usePageSeo({
title: pageTitle.value,
description: pageContent.value ? pageContent.value.slice(0, 120) : `${pageTitle.value} - 页面内容`,
path: route.path
})
</script>