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
+145
View File
@@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
/** 从列表返回中提取 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
})