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
+95
View File
@@ -0,0 +1,95 @@
import { $fetch } from 'ofetch'
import { useStorage } from 'nitropack/runtime'
import type { ApiEnvelope, CmsSiteInfo } from '~/app/types'
type RuntimeConfig = ReturnType<typeof useRuntimeConfig>
/**
* 直接调用 CMS 获取站点信息
* CMS 接口:/cms/cms-website/getSiteInfo
*
* 供 SSR 中间件(tenant.ts)预取使用:结果写入 event.context.siteInfo
* 再由 site-init.server.ts 注入 useState('site-info'),保证 SSR 首屏
* 渲染模板时 siteTemplateId 已可用(网站数据库优先于应用绑定/环境默认)。
*
* 与 /api/site/info handler 各自独立调用 CMS,互不依赖:
* handler 面向直接 API 调用方(返回原始响应),本函数面向 SSR 预取(返回解包数据)。
*
* @returns 解包后的 CmsSiteInfo;失败或 tenantId 为空返回 null(调用方回退默认模板)
*/
export async function getCmsSiteInfo(
tenantId: string,
config: RuntimeConfig,
query: Record<string, unknown> = {}
): Promise<CmsSiteInfo | null> {
if (!tenantId) return null
const modulesApiBase = config.public.modulesApiBase as string
try {
const res = await $fetch<ApiEnvelope<CmsSiteInfo> | CmsSiteInfo>(
'/cms/cms-website/getSiteInfo',
{
baseURL: modulesApiBase,
headers: { TenantId: tenantId },
query,
timeout: 6000,
retry: 1
}
)
const envelope = res as ApiEnvelope<CmsSiteInfo>
return envelope?.data ?? (res as CmsSiteInfo)
} catch {
return null
}
}
/**
* 在线留言是否要求滑块验证码。
* 来源优先级(从高到低):
* 1) CMS 后台「网站设置」setting.requireCaptcha(权威,后台配置即生效)
* 2) 运行时配置 public.contactCaptcha(本地调试/部署级覆盖,NUXT_PUBLIC_CONTACT_CAPTCHA=true|false
* 3) 默认 false(未配置则不显示滑块、不强制验证)
*
* 结果按租户缓存 5 分钟(仅缓存 CMS 解析出的 raw 值),避免每次提交都回源 CMS。
*/
const siteKv = useStorage('site-cache')
const CAPTCHA_CONFIG_TTL = 5 * 60 * 1000
type CaptchaReqCache = { raw: boolean | null; ts: number }
function resolveCaptchaRequired(raw: boolean | null, envOverride: boolean | null): boolean {
if (raw !== null) return raw
if (envOverride !== null) return envOverride
return false
}
export async function getContactCaptchaRequired(
tenantId: string,
config: RuntimeConfig
): Promise<boolean> {
const envOverride =
typeof config.public.contactCaptcha === 'boolean' ? config.public.contactCaptcha : null
const cacheKey = `captcha-req:${tenantId}`
if (tenantId) {
try {
const cached = await siteKv.getItem<CaptchaReqCache>(cacheKey)
if (cached && Date.now() - cached.ts < CAPTCHA_CONFIG_TTL) {
return resolveCaptchaRequired(cached.raw, envOverride)
}
} catch {
// 缓存读取失败则回源
}
}
try {
const info = await getCmsSiteInfo(tenantId, config)
const raw = (info?.setting?.requireCaptcha as boolean | undefined) ?? null
if (tenantId) {
await siteKv.setItem(cacheKey, { raw, ts: Date.now() })
}
return resolveCaptchaRequired(raw, envOverride)
} catch {
// CMS 不可达时,回退到环境变量覆盖(若也未设置则默认 false)
return envOverride ?? false
}
}