Files
hjc-web/app/composables/useTemplate.ts
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

173 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}