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
+172
View File
@@ -0,0 +1,172 @@
import type { Component } from 'vue'
import { useApp } from './useApp'
import { useSite } from './useSite'
/** 模板配置 */
export interface TemplateConfig {
/** 模板 ID */
id: string
/** 模板名称 */
name: string
/** 模板描述 */
description: string
/** 预览图路径 */
preview: string
/** 支持的模块 */
supportedModules: string[]
/** 主题配置 */
themeConfig?: {
primaryColor?: string
secondaryColor?: string
fontFamily?: string
}
}
/** 模板组件映射 */
export interface TemplateComponents {
/** 首页布局 */
Home: Component
/** 通用 CMS 页面布局 */
Page?: Component
/** 文章列表页 */
NewsList?: Component
/** 文章详情页 */
NewsDetail?: Component
/** 产品列表页 */
ProductList?: Component
/** 产品详情页 */
ProductDetail?: Component
/** 案例列表页 */
CaseList?: Component
/** 案例详情页 */
CaseDetail?: Component
/** 联系我们页 */
Contact?: Component
/** 关于我们页 */
About?: Component
/** Header 组件 */
Header: Component
/** Footer 组件 */
Footer: Component
}
/**
* 模板加载器
* 根据模板 ID 动态加载对应模板组件
*/
export function useTemplate() {
const runtimePublic = useRuntimeConfig().public
const defaultTemplateId = runtimePublic.templateId as string
// 强制模板 ID(本地调试用,最高优先级,覆盖应用/站点绑定的模板)
const forceTemplateId = (runtimePublic.forceTemplateId as string) || ''
// 应用 / 站点绑定的模板 ID(应用库优先,站点库次之)
const { appTemplateId } = useApp()
const { templateId: siteTemplateId, templateIdFallback } = useSite()
/** 手动覆盖的模板 ID(保持可写,兼容原有行为;默认等于默认配置) */
const templateId = useState<string>('template-id', () => defaultTemplateId)
/** 解析后的实际模板 ID
* 强制(本地 env 调试,最高优先级) > 站点(cms_website,按租户隔离的真相源) > 应用(app_product) > 默认配置
* 站点优先于应用:同一产品被多租户共享时,应用级 template_id 不能盖掉租户各自在 cms_website 中的选择。
* 注:forceTemplateId 仅本地调试设置,生产环境为空,因此不影响线上解析。 */
const resolvedTemplateId = computed(() => {
return forceTemplateId || siteTemplateId.value || appTemplateId.value || defaultTemplateId
})
/** 所有已注册的模板 */
const templates = useState<Record<string, TemplateConfig>>('templates-registry', () => ({}))
/** 模板组件缓存:放在模块级 Map 中,避免 devalue 序列化组件对象 */
const templateComponentsCache = new Map<string, TemplateComponents>()
/** 注册模板 */
function registerTemplate(config: TemplateConfig) {
templates.value[config.id] = config
}
/** 当前模板配置 */
const currentTemplate = computed(() => {
return templates.value[resolvedTemplateId.value] || null
})
/**
* 动态导入模板组件
* Nuxt 自动导入 app/templates/[id]/ 下的组件(~ 别名指向 app/)
*/
async function loadTemplate(id?: string): Promise<TemplateComponents | null> {
// [临时验证日志] 确认后台选用模板已通过 getSiteInfo → siteTemplateId 生效;验证通过后删除本行
if (import.meta.dev) {
console.log('[useTemplate] loadTemplate id=', id, '| resolvedTemplateId=', resolvedTemplateId.value)
}
const targetId = id || resolvedTemplateId.value
templateId.value = targetId
// 命中缓存则直接返回(同一次页面生命周期内避免重复 import)
const cached = templateComponentsCache.get(targetId)
if (cached) {
return cached
}
try {
// 动态导入模板组件(~ 别名指向 app/,因此模板路径为 app/templates/[id]/
const header = (await import(`~/templates/${targetId}/components/Header.vue`)).default
const footer = (await import(`~/templates/${targetId}/components/Footer.vue`)).default
const home = (await import(`~/templates/${targetId}/pages/Home.vue`)).default
const components: TemplateComponents = { Header: header, Footer: footer, Home: home }
// 可选组件:尝试加载,不存在则忽略
// 注意:模板字符串必须直接写在 import() 内(与上方 Header/Footer/Home 一致),
// 否则 Vite 无法静态分析「变量路径」,运行时会 import 失败、
// 被 try/catch 静默吞掉,导致 Page 等可选组件永远 undefined → 页面卡在 SiteLoading。
const optionalComponents: { key: keyof TemplateComponents; file: string }[] = [
{ key: 'Page', file: 'Page' },
{ key: 'NewsList', file: 'NewsList' },
{ key: 'NewsDetail', file: 'NewsDetail' },
{ key: 'ProductList', file: 'ProductList' },
{ key: 'ProductDetail', file: 'ProductDetail' },
{ key: 'CaseList', file: 'CaseList' },
{ key: 'CaseDetail', file: 'CaseDetail' },
{ key: 'Contact', file: 'Contact' },
{ key: 'About', file: 'About' }
]
for (const { key, file } of optionalComponents) {
try {
const mod = await import(`~/templates/${targetId}/pages/${file}.vue`)
if (mod.default) {
components[key] = mod.default
}
} catch {
// 可选组件不存在时跳过
}
}
// 缓存并返回
templateComponentsCache.set(targetId, components)
return components
} catch {
// 目录不存在(如后台 code 指向前端尚未落地的模板):
// 先尝试「按主键推导」的兜底目录,仍失败才跌回默认模板,避免直接错乱成 template-01。
const fallbackById = templateIdFallback.value
if (fallbackById && targetId !== fallbackById) {
return loadTemplate(fallbackById)
}
if (targetId !== 'template-01') {
return loadTemplate('template-01')
}
return null
}
}
return {
templateId,
resolvedTemplateId,
templates,
currentTemplate,
registerTemplate,
loadTemplate
}
}