feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 获取页面列表
|
||||
* GET /api/page/all
|
||||
* 代理到 SaaS 后端 CMS 页面列表接口
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const ctx = getTenantFromContext(event, config)
|
||||
const query = getQuery(event)
|
||||
const modulesApiBase = config.public.modulesApiBase as string
|
||||
|
||||
try {
|
||||
const res = await $fetch('/cms/cms-website/pageAll', {
|
||||
baseURL: modulesApiBase,
|
||||
headers: {
|
||||
TenantId: ctx.tenantId
|
||||
},
|
||||
query
|
||||
})
|
||||
|
||||
return res
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.statusMessage || 'Failed to fetch pages'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 父栏目聚合子栏目单页
|
||||
* GET /api/page/children?navigationIds=1,2,3
|
||||
*
|
||||
* 场景:父级 page 栏目(model=page)自身通常无内容,其下挂多个子 page 栏目。
|
||||
* 访问父栏目时,前端递归收集父栏目自身 + 所有后代 navigationId,以逗号串传入,
|
||||
* 后端按 navigation_id IN (...) 一次性取回该父栏目下所有单页。
|
||||
*
|
||||
* 代理到 SaaS 后端:/cms/cms-page/page?navigationIdsStr=...&status=1
|
||||
* 仅返回已发布(status=1)的单页,避免泄露草稿/已下线内容。
|
||||
*
|
||||
* 返回结构(与 /api/case/list 对齐,前端直接读 data.value?.list):
|
||||
* { list: CmsPage[], count: number }
|
||||
*/
|
||||
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 navigationIds = query.navigationIds as string | undefined
|
||||
if (!navigationIds) {
|
||||
return { list: [], count: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await $fetch('/cms/cms-page/page', {
|
||||
baseURL: modulesApiBase,
|
||||
headers: {
|
||||
TenantId: ctx.tenantId
|
||||
},
|
||||
query: {
|
||||
navigationIdsStr: navigationIds,
|
||||
// C 端只展示已发布单页:status 0草稿 1已发布 2已下线
|
||||
status: 1,
|
||||
page: 1,
|
||||
limit: 100,
|
||||
TenantId: ctx.tenantId
|
||||
}
|
||||
})
|
||||
|
||||
// 上游返回 {code, message, data: {list, count}} 包装层,
|
||||
// 去掉包装并把 list/count 透传给前端。
|
||||
const envelopeData = (res && res.data !== undefined ? res.data : res) || {}
|
||||
const rawList = Array.isArray(envelopeData.list) ? envelopeData.list : []
|
||||
const count = envelopeData.count ?? envelopeData.total ?? rawList.length
|
||||
|
||||
return { list: rawList, count }
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.statusMessage || 'Failed to fetch page children'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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: '内容接口暂时不可用,请稍后再试' }
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user