2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
162 lines
5.8 KiB
Vue
162 lines
5.8 KiB
Vue
<template>
|
||
<div class="min-h-[60vh] bg-white pb-16">
|
||
<div class="t9-container">
|
||
<!-- 顶部:栏目标题 + 自身内容 -->
|
||
<div v-if="pageTitle" class="max-w-4xl mx-auto">
|
||
<h1 class="t9-section-title">{{ pageTitle }}</h1>
|
||
<div v-if="pageData && pageData.hasContent">
|
||
<div v-if="pageData.updateTime" class="text-sm text-center mb-8" style="color: var(--t9-muted)">
|
||
更新时间:{{ formatDate(pageData.updateTime) }}
|
||
</div>
|
||
<RichText :content="pageData.content" />
|
||
</div>
|
||
<PageAttachments
|
||
v-if="pageData?.attachments?.length"
|
||
:attachments="pageData.attachments"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 子栏目聚合列表 -->
|
||
<div v-if="childCards.length" class="max-w-5xl mx-auto mt-14">
|
||
<h2 class="t9-section-title">子栏目</h2>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||
<NuxtLink
|
||
v-for="p in childCards"
|
||
:key="p.navigationId"
|
||
:to="`/page/${p.navigationId}`"
|
||
class="block p-6 border transition-colors hover:border-[var(--t9-primary)]"
|
||
style="border-color: var(--t9-border)"
|
||
>
|
||
<h3 class="text-lg font-semibold mb-2" style="color: var(--t9-text-strong)">{{ p.title }}</h3>
|
||
<p class="text-sm leading-7 t9-clamp-3" style="color: var(--t9-muted)">{{ excerpt(p.content) }}</p>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="!pageData?.hasContent && !childCards.length" class="text-center py-20">
|
||
<p class="mb-2" style="color: var(--t9-text-secondary)">{{ emptyState.title }}</p>
|
||
<p class="text-sm mb-6 leading-relaxed" style="color: var(--t9-muted-2)">
|
||
{{ emptyState.desc }}<br />
|
||
当前导航:{{ currentNav?.title || pageTitle }}(ID:{{ navigationId }})
|
||
</p>
|
||
<NuxtLink to="/" class="hover:underline" style="color: var(--t9-primary)">返回首页</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
/**
|
||
* 模板 9 - 通用 CMS 页面(墨绿文化出版风)
|
||
* 支持两种入口:
|
||
* 1. /page/:navigationId → CMS 导航路径(优先)
|
||
* 2. /:slug → 传统路径
|
||
*
|
||
* 父栏目聚合:当当前 page 栏目拥有子栏目时,递归收集其下所有子栏目的单页,
|
||
* 以卡片列表形式聚合展示(每个子页卡片链接到 /page/{childNavId} 详情页)。
|
||
*/
|
||
import dayjs from 'dayjs'
|
||
import type { CmsNavigation, PageDetail } from '~/types'
|
||
import { collectDescendantNavIds, isParentNavigation } from '~/utils/nav-tree'
|
||
|
||
interface ChildPage {
|
||
pageId?: number
|
||
title?: string
|
||
path?: string
|
||
content?: string
|
||
navigationId?: number
|
||
image?: string | null
|
||
}
|
||
|
||
const route = useRoute()
|
||
const { allNavigations, fetchSiteInfo } = useSite()
|
||
|
||
await fetchSiteInfo()
|
||
|
||
/** 从路由获取 navigationId */
|
||
const navigationId = computed(() => {
|
||
const id = route.params.id as string
|
||
if (id) {
|
||
const num = Number(id)
|
||
if (!Number.isNaN(num)) return num
|
||
}
|
||
return undefined
|
||
})
|
||
|
||
/** 当前栏目是否为父栏目(拥有子栏目) */
|
||
const hasChildren = computed(() => isParentNavigation(navigationId.value, allNavigations.value || []))
|
||
|
||
/** 递归收集当前栏目自身 + 所有后代 navigationId */
|
||
const descendantNavIds = computed(() =>
|
||
collectDescendantNavIds(navigationId.value, allNavigations.value || [])
|
||
)
|
||
|
||
/** 从导航中查找当前页面信息(使用 allNavigations,已做标题映射) */
|
||
const currentNav = computed<CmsNavigation | undefined>(() => {
|
||
if (!navigationId.value) return undefined
|
||
const findNav = (items: CmsNavigation[]): CmsNavigation | undefined => {
|
||
for (const item of items) {
|
||
if (item.navigationId === navigationId.value) return item
|
||
if (item.children?.length) {
|
||
const found = findNav(item.children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
return findNav(allNavigations.value)
|
||
})
|
||
|
||
/** 获取页面内容 */
|
||
const navId = navigationId.value
|
||
const pagePath = (() => {
|
||
const num = Number(route.params.id)
|
||
return Number.isNaN(num) ? (route.params.id as string) : undefined
|
||
})()
|
||
const { data: pageData } = await useFetch<PageDetail>('/api/page/detail', {
|
||
key: `page-${navId ?? pagePath ?? 'default'}`,
|
||
query: { navigationId: navId, path: pagePath }
|
||
})
|
||
|
||
/** 父栏目聚合:取其下所有子栏目的单页(不含父栏目自身,避免与顶部内容重复) */
|
||
const childPages = ref<ChildPage[]>([])
|
||
if (hasChildren.value && descendantNavIds.value.length) {
|
||
const { data: childrenData } = await useFetch<{ list: ChildPage[]; count: number }>('/api/page/children', {
|
||
key: `page-children-${navId}`,
|
||
query: { navigationIds: descendantNavIds.value.join(',') }
|
||
})
|
||
childPages.value = childrenData.value?.list || []
|
||
}
|
||
const childCards = computed(() => childPages.value.filter(p => p.navigationId !== navId))
|
||
|
||
const pageTitle = computed(() => {
|
||
return pageData.value?.title || currentNav.value?.title || '页面'
|
||
})
|
||
|
||
/** 空内容兜底文案,按状态区分「未录入」与「接口异常」 */
|
||
const emptyState = computed(() => {
|
||
const status = pageData.value?.status
|
||
if (status === 'error') {
|
||
return { title: '内容暂时无法加载', desc: '内容接口暂时不可用,请稍后再试。' }
|
||
}
|
||
return { title: '内容尚未录入', desc: '该页面正文尚未在 CMS 管理后台配置,请前往 CMS 后台为该导航补充内容。' }
|
||
})
|
||
|
||
/** 富文本正文截取纯文本摘要 */
|
||
function excerpt(html?: string, len = 80) {
|
||
if (!html) return ''
|
||
const text = (html || '')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/ /g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
return text.length > len ? text.slice(0, len) + '…' : text
|
||
}
|
||
|
||
function formatDate(date?: string) {
|
||
if (!date) return ''
|
||
return dayjs(date).format('YYYY-MM-DD')
|
||
}
|
||
</script>
|