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,145 @@
|
||||
import { defineEventHandler, getQuery, setHeader } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../utils/tenant'
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
/**
|
||||
* 动态站点地图
|
||||
* GET /api/sitemap.xml
|
||||
* 根据当前租户的页面列表动态生成 sitemap.xml
|
||||
*
|
||||
* 覆盖范围:
|
||||
* - 静态页:首页 + 三大模块列表根页(/article /product /case)
|
||||
* - CMS 单页(/page slug 对应的顶层路由,如 /about /contact)
|
||||
* - 文章 / 产品 / 案例 详情页(仅已发布 status=0、未删除 deleted=0)
|
||||
*/
|
||||
interface SitemapUrl {
|
||||
loc: string
|
||||
priority: string
|
||||
changefreq: string
|
||||
lastmod: string
|
||||
}
|
||||
|
||||
function escapeXml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
/** 从列表返回中提取 list 数组(兼容 article 原始包装与 product/case 归一化结构) */
|
||||
function extractList(res: any): any[] {
|
||||
if (!res) return []
|
||||
const data = res.data !== undefined ? res.data : res
|
||||
if (data && Array.isArray(data.list)) return data.list
|
||||
if (Array.isArray(res?.data?.list)) return res.data.list
|
||||
if (Array.isArray(data)) return data
|
||||
return []
|
||||
}
|
||||
|
||||
/** 从列表项中提取唯一 id(各模块主键字段名不同) */
|
||||
function pickId(item: any, keys: string[]): string | number | null {
|
||||
for (const k of keys) {
|
||||
if (item?.[k] != null) return item[k]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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 origin = `https://${ctx.host || ''}`
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const urls: SitemapUrl[] = [
|
||||
{ loc: '/', priority: '1.0', changefreq: 'daily', lastmod: now },
|
||||
{ loc: '/article', priority: '0.9', changefreq: 'daily', lastmod: now },
|
||||
{ loc: '/product', priority: '0.9', changefreq: 'daily', lastmod: now },
|
||||
{ loc: '/case', priority: '0.9', changefreq: 'daily', lastmod: now }
|
||||
]
|
||||
|
||||
// ===== 单页 slug(/about /contact 等顶层路由) =====
|
||||
try {
|
||||
const res = await $fetch<{ data?: { list?: any[] } } | any[]>('/cms/cms-website/pageAll', {
|
||||
baseURL: modulesApiBase,
|
||||
headers: { TenantId: ctx.tenantId }
|
||||
})
|
||||
const list = Array.isArray(res) ? res : res?.data?.list || res?.data || []
|
||||
for (const p of list) {
|
||||
if (p?.slug && p.slug !== 'index') {
|
||||
urls.push({
|
||||
loc: `/${p.slug}`,
|
||||
priority: '0.8',
|
||||
changefreq: 'weekly',
|
||||
lastmod: p.updateTime || now
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 单页接口异常不影响详情页收录
|
||||
}
|
||||
|
||||
// ===== 文章 / 产品 / 案例详情(分页拉取,仅已发布) =====
|
||||
const fetchDetailUrls = async (
|
||||
apiPath: string,
|
||||
urlPrefix: string,
|
||||
idKeys: string[]
|
||||
) => {
|
||||
const limit = 500
|
||||
let page = 1
|
||||
// 安全上限:单 sitemap 文件 URL 上限 50,000
|
||||
while (urls.length < 50000) {
|
||||
let list: any[] = []
|
||||
try {
|
||||
const res = await $fetch(apiPath, {
|
||||
baseURL: modulesApiBase,
|
||||
headers: { TenantId: ctx.tenantId },
|
||||
query: { page, limit, status: 0, deleted: 0 }
|
||||
})
|
||||
list = extractList(res)
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
const idVal = pickId(item, idKeys)
|
||||
if (idVal == null) continue
|
||||
urls.push({
|
||||
loc: `/${urlPrefix}/${idVal}`,
|
||||
priority: '0.7',
|
||||
changefreq: 'weekly',
|
||||
lastmod: item.updateTime || item.publishTime || item.createTime || now
|
||||
})
|
||||
}
|
||||
|
||||
if (list.length < limit) break
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
await fetchDetailUrls('/cms/cms-article/page', 'article', ['id', 'articleId'])
|
||||
await fetchDetailUrls('/cms/cms-product/page', 'product', ['id', 'productId'])
|
||||
await fetchDetailUrls('/cms/cms-case/page', 'case', ['id', 'caseId', 'caseItemId'])
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls
|
||||
.map(
|
||||
(u) => ` <url>
|
||||
<loc>${escapeXml(origin + u.loc)}</loc>
|
||||
<lastmod>${u.lastmod}</lastmod>
|
||||
<changefreq>${u.changefreq}</changefreq>
|
||||
<priority>${u.priority}</priority>
|
||||
</url>`
|
||||
)
|
||||
.join('\n')}
|
||||
</urlset>`
|
||||
|
||||
setHeader(event, 'Content-Type', 'application/xml')
|
||||
return xml
|
||||
})
|
||||
Reference in New Issue
Block a user