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('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>('templates-registry', () => ({})) /** 模板组件缓存:放在模块级 Map 中,避免 devalue 序列化组件对象 */ const templateComponentsCache = new Map() /** 注册模板 */ 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 { // [临时验证日志] 确认后台选用模板已通过 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 } }