Files
hjc-web/app/composables/useTemplate.ts
T
weicw1996 2828ba6718 feat(tender): 汇吉采标书购买 阶段3 - C端标书列表/详情/购买入口
- server/api/tender/{list.get,detail.get,order.post}.ts 代理到 mp-api /api/hjc/bid-project|order
- app/types/tender.ts 前端视图模型
- app/templates/template-07/pages/{TenderList,TenderDetail,BuyDocument}.vue(Tailwind,仿 Product 页)
- app/pages/tender/index.vue + tender/[id].vue + buy.vue 路由,经 useTemplate 加载
- useTemplate.ts 注册 TenderList/TenderDetail/BuyDocument(全部模板安全可选)
- 附 BUY_TENDER_MAP_REPORT.md(hjc-web 结构映射参考)
2026-09-08 21:45:34 +08:00

182 lines
6.4 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
/** 标书列表页 */
TenderList?: Component
/** 标书详情页 */
TenderDetail?: Component
/** 标书购买流程壳 */
BuyDocument?: 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' },
{ key: 'TenderList', file: 'TenderList' },
{ key: 'TenderDetail', file: 'TenderDetail' },
{ key: 'BuyDocument', file: 'BuyDocument' }
]
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
}
}