2828ba6718
- 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 结构映射参考)
182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
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
|
||
}
|
||
}
|