feat(app): 添加多模板关于我们页面及相关路由和404页面

- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
2026-09-08 12:13:44 +08:00
commit 2b69686795
381 changed files with 59891 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
<template>
<div class="min-h-screen bg-white">
<div class="bg-[#1E2A47] relative overflow-hidden">
<div class="absolute -top-16 -right-16 w-72 h-72 rounded-full bg-[#CF1313]/20 blur-3xl" />
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16 relative z-10">
<nav class="text-xs text-white/60 mb-3" aria-label="面包屑">
<NuxtLink to="/" class="hover:text-white">首页</NuxtLink>
<span class="mx-2">/</span>
<span class="text-white">{{ pageTitle }}</span>
</nav>
<h1 class="text-3xl font-bold text-white">{{ pageTitle }}</h1>
</div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
<!-- 自身内容 -->
<div v-if="pageData && pageData.hasContent" class="max-w-4xl mx-auto">
<div v-if="pageData.updateTime" class="text-[#666666] text-sm mb-8">更新时间{{ formatDate(pageData.updateTime) }}</div>
<div class="cms-content">
<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-12">
<h2 class="text-2xl font-bold text-[#1E2A47] mb-8 text-center">子栏目</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<NuxtLink
v-for="p in childCards"
:key="p.navigationId"
:to="`/page/${p.navigationId}`"
class="block p-6 rounded-xl border border-gray-200 hover:shadow-lg hover:border-[#CF1313] transition"
>
<h3 class="text-lg font-semibold text-[#1E2A47] mb-2">{{ p.title }}</h3>
<p class="text-sm text-[#666666] leading-relaxed line-clamp-3">{{ excerpt(p.content) }}</p>
</NuxtLink>
</div>
</div>
<!-- 空状态 -->
<div v-if="!pageData?.hasContent && !childCards.length" class="max-w-4xl mx-auto text-center py-16">
<h1 class="text-2xl font-bold text-[#1E2A47] mb-4">{{ pageTitle }}</h1>
<p class="text-[#666666] mb-2">{{ emptyState.title }}</p>
<p class="text-[#9AA3B5] text-sm mb-6 leading-relaxed">{{ emptyState.desc }}<br>当前导航{{ currentNav?.title || pageTitle }}ID{{ navigationId }}</p>
<NuxtLink to="/" class="text-[#CF1313] hover:underline">返回首页</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模板 6 - 通用 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(/&nbsp;/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>
<style scoped>
.cms-content :deep(h2),
.cms-content :deep(h3),
.cms-content :deep(h4) { color: #1E2A47; margin-top: 1.5em; margin-bottom: .75em; }
.cms-content :deep(p) { color: #333333; line-height: 1.85; margin-bottom: 1em; }
.cms-content :deep(a) { color: #CF1313; }
.cms-content :deep(img) { border-radius: 12px; }
</style>