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
+200
View File
@@ -0,0 +1,200 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getQuery } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 页面详情
*
* 支持两种入口:
* 1. GET /api/page/detail?navigationId=4296 → CMS 导航节点(model=page+ cms_design 内容(旧机制)
* 2. GET /api/page/detail?path=about → 新版「单页管理」cms_page(按 slug 取已发布单页)
*
* 与 website-admin 的「单页管理」模块对应:后端 mp-java CmsPageController.getByPath。
* cms-api 侧已按 status=1(已发布)过滤,未发布/不存在统一返回 data=null。
*
* 返回结构(始终为对象,前端用 status 区分):
* {
* status: 'ok' | 'empty' | 'error',
* pageId, title, path, model,
* content, layout, photo,
* keywords, description,
* hasContent, updateTime, message?
* }
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const query = getQuery(event)
const modulesApiBase = config.public.modulesApiBase as string
const navigationId = query.navigationId || query.id
const path = query.path
const base = {
status: 'empty' as const,
pageId: 0,
title: '',
path: (typeof path === 'string' ? path : '') || '',
model: 'page',
parentId: 0,
content: '',
layout: null as any,
photo: null as string | null,
keywords: '',
description: '',
hasContent: false,
updateTime: null as string | null,
message: '该页面不存在或未发布'
}
// ===== 分支2:按 path 取新版单页(cms_page =====
if (typeof path === 'string' && path.trim()) {
try {
const res = await $fetch<{
code?: number
message?: string
data?: {
pageId?: number
title?: string
path?: string
content?: string | null
keywords?: string | null
description?: string | null
image?: string | null
status?: number
updateTime?: string | null
/** 附件列表:后端存 JSON 字符串(cms_page.attachments),也可能是已解析的数组 */
attachments?: unknown
} | null
}>(`/cms/cms-page/getByPath/${encodeURIComponent(path.trim())}`, {
baseURL: modulesApiBase,
headers: { TenantId: ctx.tenantId },
timeout: 10000,
retry: 1
})
const node = res?.data
if (!node) {
const code = res?.code
const isBizError = typeof code === 'number' && code !== 0 && code !== 200
if (isBizError) {
return { ...base, status: 'error' as const, message: res?.message || '上游接口返回异常' }
}
return { ...base, status: 'empty' as const, message: '该单页不存在或未发布' }
}
const content = typeof node.content === 'string' ? node.content : ''
const hasContent = content.trim().length > 0
// 解析附件列表(cms_page.attachments):后端存 JSON 字符串,容错解析
let attachments: Array<{ name: string; url: string; size?: number; ext?: string; sort?: number }> | undefined
try {
const raw = node.attachments
if (raw) {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
if (Array.isArray(parsed)) {
attachments = parsed
.filter((it: any) => it && typeof it.url === 'string' && typeof it.name === 'string')
.map((it: any) => ({
name: String(it.name),
url: String(it.url),
size: typeof it.size === 'number' ? it.size : undefined,
ext: typeof it.ext === 'string' ? it.ext : undefined,
sort: typeof it.sort === 'number' ? it.sort : undefined
}))
}
}
} catch (e) {
console.error(`[page/detail] parse attachments failed: path=${path}`, (e as any)?.message || e)
}
return {
status: hasContent ? ('ok' as const) : ('empty' as const),
pageId: node.pageId ?? 0,
title: node.title || '',
path: node.path || path,
model: 'page',
parentId: 0,
content,
layout: null,
photo: node.image ?? null,
keywords: node.keywords || '',
description: node.description || '',
hasContent,
updateTime: node.updateTime || null,
attachments,
message: hasContent ? undefined : '该页面正文尚未在 CMS 管理后台录入'
}
} catch (error: any) {
console.error(`[page/detail] upstream(cms-page) failed: path=${path} tenant=${ctx.tenantId}`, error?.message || error)
return { ...base, status: 'error' as const, message: '内容接口暂时不可用,请稍后再试' }
}
}
// ===== 分支1:按 navigationId 取导航节点(旧机制,保持原逻辑) =====
if (!navigationId) {
throw createError({ statusCode: 400, statusMessage: 'Missing navigationId or path parameter' })
}
const navId = Number(navigationId)
try {
const res = await $fetch<{
code?: number
message?: string
data?: {
navigationId?: number
title?: string
model?: string
parentId?: number
updateTime?: string | null
design?: {
content?: string | null
layout?: any
photo?: string | null
updateTime?: string | null
createTime?: string | null
} | null
} | null
}>(`/cms/cms-navigation/${navId}`, {
baseURL: modulesApiBase,
headers: { TenantId: ctx.tenantId },
timeout: 10000,
retry: 1
})
const node = res?.data
if (!node) {
const code = res?.code
const isBizError = typeof code === 'number' && code !== 0 && code !== 200
if (isBizError) {
return { ...base, navigationId: navId, message: res?.message || '上游接口返回异常' }
}
return { ...base, status: 'empty' as const, navigationId: navId, message: '该导航节点在 CMS 中不存在或未发布' }
}
const design = node.design || {}
const content = typeof design.content === 'string' ? design.content : ''
const hasContent = content.trim().length > 0
const updateTime = design.updateTime || design.createTime || node.updateTime || null
return {
status: hasContent ? ('ok' as const) : ('empty' as const),
navigationId: node.navigationId ?? navId,
title: node.title || '',
model: node.model || 'page',
parentId: node.parentId || 0,
content,
layout: design.layout ?? null,
photo: design.photo ?? null,
keywords: '',
description: '',
hasContent,
updateTime,
message: hasContent ? undefined : '该页面正文尚未在 CMS 管理后台录入'
}
} catch (error: any) {
console.error(`[page/detail] upstream(navigation) failed: navigationId=${navigationId} tenant=${ctx.tenantId}`, error?.message || error)
return { ...base, status: 'error' as const, navigationId: navId, message: '内容接口暂时不可用,请稍后再试' }
}
})