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
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="text-center px-6">
<p class="text-7xl font-bold text-gray-300 mb-4">404</p>
<h1 class="text-2xl font-semibold text-gray-800 mb-3">页面未找到</h1>
<p class="text-gray-500 mb-8">抱歉您访问的页面不存在或已被移除</p>
<NuxtLink
to="/"
class="inline-flex items-center justify-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
返回首页
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 404 页面(使用 blank 布局避免 Header/Footer 干扰)
*/
definePageMeta({
layout: 'blank'
})
// 错误页禁止被搜索引擎收录
useSeoMeta({
robots: 'noindex, nofollow'
})
</script>
+53
View File
@@ -0,0 +1,53 @@
<template>
<div>
<Component :is="templatePage" v-if="templatePage" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 动态 CMS 页面路由(兼容入口)
* CMS 导航主要使用 /page/:id, /article/:id, /product/:id 格式
* 此路由作为兼容入口,处理旧式路径如 /about, /services 等
*/
const route = useRoute()
const slug = route.params.slug as string
const { loadTemplate } = useTemplate()
const { siteInfo, siteName, navigations, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const templatePage = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
templatePage.value = components?.Page || null
// 尝试从导航中匹配 slug
const matchedNav = computed(() => {
const navs = navigations.value || []
const findNav = (items: any[]): any => {
for (const item of items) {
if (item.path === `/${slug}` || item.code === slug) return item
if (item.children?.length) {
const found = findNav(item.children)
if (found) return found
}
}
return null
}
return findNav(navs)
})
usePageSeo(
{
title: matchedNav.value?.title || slug,
path: `/${slug}`
},
siteInfo.value
)
</script>
+35
View File
@@ -0,0 +1,35 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 关于我们路由(独立页面类型,与 contact 同级)
*/
const { loadTemplate } = useTemplate()
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.About || null
usePageSeo(
{
title: '关于我们',
path: '/about'
},
siteInfo.value
)
useBreadcrumbSeo([
{ name: '首页', url: '/' },
{ name: '关于我们', url: '/about' }
])
</script>
+28
View File
@@ -0,0 +1,28 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" :key="route.fullPath" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 文章/新闻 路由(栏目列表 与 详情 二合一) /article/:id
* 按模块名单数命名:列表(栏目)/article/{navigationId},详情 /article/{id}。
* 靠「id 是否该模块已知栏目 navigationId」运行时判定。
* 旧链接 /news/*、/newss/* 由 server/middleware/z-news-detail-redirect.ts 301 到 /article/*。
*
* 10626 租户专属:/article/4722(专业人才团队)走写死 TalentTeam
* 不依赖 cms-article 模块拉列表,避免 cms-article 写入受 token 限制而落空。
*/
import TalentTeam from '~/templates/template-07/pages/TalentTeam.vue'
const route = useRoute()
const id = String(route.params.id)
let pageComponent = null as unknown
if (id === '4722') {
pageComponent = TalentTeam
} else {
;({ pageComponent } = await useModuleRoute('article'))
}
</script>
+39
View File
@@ -0,0 +1,39 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 新闻列表路由(根) /article
* 按模块名单数命名:列表根为 /article(旧规范为 /news)。
*/
const { loadTemplate } = useTemplate()
const { siteInfo, navigations, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.NewsList || null
// 从导航中查找新闻栏目标题
const newsNav = computed(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'article')
})
usePageSeo(
{
title: newsNav.value?.title || '新闻资讯',
path: '/article'
},
siteInfo.value
)
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" :key="route.fullPath" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 案例 路由(栏目列表 与 详情 二合一) /case/:id
* 按模块名单数命名:列表(栏目)/case/{navigationId},详情 /case/{id}。
* 靠「id 是否该模块已知栏目 navigationId」运行时判定。
* 旧链接 /cases/* 由 server/middleware/z-news-detail-redirect.ts 301 到 /case/*。
*/
const { route, pageComponent } = await useModuleRoute('case')
</script>
+33
View File
@@ -0,0 +1,33 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 案例列表路由(根) /case
* 按模块名单数命名:列表根为 /case(旧规范为 /cases)。
*/
const { loadTemplate } = useTemplate()
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.CaseList || null
usePageSeo(
{
title: '案例展示',
path: '/case'
},
siteInfo.value
)
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div></div>
</template>
<script setup lang="ts">
/**
* /cases(旧复数路径)→ /case(新单数规范)客户端重定向
*
* 服务端已有 server/middleware/z-news-detail-redirect.ts 处理 301
* 本文件覆盖客户端导航场景(SPA 路由切换不经过服务端中间件)。
*/
await navigateTo('/case', { redirectCode: 301 })
</script>
+32
View File
@@ -0,0 +1,32 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 联系我们路由
*/
const { loadTemplate } = useTemplate()
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.Contact || null
usePageSeo(
{
title: '联系我们',
path: '/contact'
},
siteInfo.value
)
</script>
+27
View File
@@ -0,0 +1,27 @@
<template>
<div>
<SiteHome />
</div>
</template>
<script setup lang="ts">
/**
* 首页路由
* 使用 SiteHome 组件动态加载当前模板的首页
*/
const { siteInfo, siteName, fetchSiteInfo } = useSite()
await fetchSiteInfo()
usePageSeo(
{
title: siteName.value || '首页',
description: siteInfo.value?.comments || undefined,
keywords: siteInfo.value?.keywords || undefined,
path: '/'
},
siteInfo.value
)
useOrganizationSeo(siteInfo.value)
</script>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div>
<Component :is="templatePage" v-if="templatePage" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* CMS 页面路由
* /page/:id → 既支持旧的导航节点 ID(数字),也支持新版「单页管理」的 path(slug)
* 数字 id → 按 navigationId 取 cms_navigation + cms_design 内容
* 非数字 slug → 按 path 取 cms_page 已发布单页(website-admin「单页管理」模块)
*/
const route = useRoute()
const id = route.params.id as string
const isNumeric = /^\d+$/.test(id)
const slug = isNumeric ? undefined : id
// 规范路由收敛:单页的规范地址是顶层 /about、/contact(独立富组件),
// /page/about、/page/contact 仅作兼容入口,统一 301 到规范地址,避免重复页与 SEO 分散。
const CANONICAL_PAGE_ROUTE: Record<string, string> = { about: '/about', contact: '/contact' }
if (slug && CANONICAL_PAGE_ROUTE[slug]) {
// 规范地址收敛:用 Nuxt 标准的 navigateTo 301 重定向(createError 仅渲染错误页,不会真正跳转)
throw navigateTo(CANONICAL_PAGE_ROUTE[slug], { redirectCode: 301 })
}
const { loadTemplate } = useTemplate()
const { siteInfo, navigations, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const templatePage = shallowRef<Component | null>(null)
const components = await loadTemplate()
// 语义化 slug 优先映射到独立页面组件(about→关于我们、contact→联系我们),
// 避免有专属组件的 slug 落入通用单页 Page 逻辑——CMS 未录入正文时会显示空状态。
// (这些页面已有结构化组件,内容更丰富,无需依赖 CMS 单页正文。)
const standaloneComp =
slug === 'about' ? components?.About
: slug === 'contact' ? components?.Contact
: null
templatePage.value = standaloneComp || components?.Page || null
// 从导航中查找当前页面信息(仅数字 id 时有效)
const currentNav = computed(() => {
if (isNumeric) {
const navs = navigations.value || []
const findNav = (items: any[]): any => {
for (const item of items) {
if (String(item.navigationId) === id) return item
if (item.children?.length) {
const found = findNav(item.children)
if (found) return found
}
}
return null
}
return findNav(navs)
}
return undefined
})
// 非数字 slug:拉取单页 SEO 信息(标题/关键词/描述/头图)
let seoData = ref<{
title?: string
keywords?: string
description?: string
photo?: string | null
} | null>(null)
if (!isNumeric && slug) {
const { data } = await useFetch<{
title?: string
keywords?: string
description?: string
photo?: string | null
}>('/api/page/detail', {
query: { path: slug }
})
seoData = data
}
usePageSeo(
{
title: seoData.value?.title || currentNav.value?.title || (slug === 'about' ? '关于我们' : slug === 'contact' ? '联系我们' : '页面'),
path: route.fullPath,
keywords: seoData.value?.keywords,
description: seoData.value?.description,
image: seoData.value?.photo || undefined
},
siteInfo.value
)
</script>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div>
<Component :is="templatePage" v-if="templatePage" />
</div>
</template>
<script setup lang="ts">
/**
* 模板预览路由
* 可通过 /preview?templateId=template-02 预览指定模板
*/
const route = useRoute()
const previewTemplateId = route.query.templateId as string
const { loadTemplate, registerTemplate } = useTemplate()
// 注册所有模板
const templates = await import('~/templates')
templates.default.forEach(registerTemplate)
const templatePage = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate(previewTemplateId)
templatePage.value = components?.Home || null
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" :key="route.fullPath" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 产品 路由(栏目列表 与 详情 二合一) /product/:id
* 按模块名单数命名:列表(栏目)/product/{navigationId},详情 /product/{id}。
* 靠「id 是否该模块已知栏目 navigationId」运行时判定。
* 旧链接 /products/* 由 server/middleware/z-news-detail-redirect.ts 301 到 /product/*。
*/
const { route, pageComponent } = await useModuleRoute('product')
</script>
+33
View File
@@ -0,0 +1,33 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 产品列表路由(根) /product
* 按模块名单数命名:列表根为 /product(旧规范为 /products)。
*/
const { loadTemplate } = useTemplate()
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.ProductList || null
usePageSeo(
{
title: '产品中心',
path: '/product'
},
siteInfo.value
)
</script>
+22
View File
@@ -0,0 +1,22 @@
<template>
<div>
<Component :is="pageComponent" v-if="pageComponent" />
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
/**
* 续费引导路由
* 使用空白布局
*/
const { loadTemplate } = useTemplate()
const pageComponent = shallowRef<Component | null>(null)
// 同步加载当前模板(SSR/客户端路由切换均直接 await,避免先闪 SiteLoading
const components = await loadTemplate()
pageComponent.value = components?.Renewal || null
</script>