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,12 @@
|
||||
/**
|
||||
* 应用启动初始化(客户端兜底)
|
||||
*
|
||||
* 正常情况下 SSR 已注入应用信息;若为空(如纯 CSR 场景),
|
||||
* 客户端启动时补查一次 /api/app/info。
|
||||
*/
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const { appInfo, fetchAppInfo } = useApp()
|
||||
if (!appInfo.value) {
|
||||
await fetchAppInfo()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 应用启动初始化(服务端)
|
||||
*
|
||||
* 在 SSR 首次渲染时,把中间件识别出的 AppProduct 注入到 useState('app-info'),
|
||||
* 从而随 SSR payload 序列化到客户端,客户端无需再次请求即可拿到应用信息。
|
||||
*/
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { appInfo } = useApp()
|
||||
const event = useRequestEvent()
|
||||
const ctx = event?.context?.tenant
|
||||
|
||||
if (ctx?.appProduct && !appInfo.value) {
|
||||
appInfo.value = ctx.appProduct
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 滚动渐显插件(客户端)
|
||||
*
|
||||
* 扫描页面中所有 [data-reveal] 元素,进入视口时为其添加 .is-visible,
|
||||
* 触发 animations.css 中定义的过渡。支持通过 data-reveal-delay(毫秒)做 stagger。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 初始隐藏状态完全由 CSS `.js [data-reveal]` 控制,<html class="js"> 由
|
||||
* nuxt.config 的 head 内联脚本在首屏绘制前注入,因此不会造成 SSR 闪烁,
|
||||
* 也无 JS 时内容正常显示。
|
||||
* - 兼容客户端路由切换:在 app:mounted 与 page:finish 时重新扫描。
|
||||
* - 【关键】使用 MutationObserver 持续监听 DOM 变更,兜住「挂载后才出现」的元素:
|
||||
* 例如组件在客户端异步取数后渲染、SSR 水合不一致导致 Vue 重建子树、
|
||||
* 或列表分页 / 条件区块延迟出现。这些元素若漏扫,会因 CSS 的 opacity:0
|
||||
* 永久不可见,表现为「整块区域样式丢失 / 空白」——这正是本插件必须防住的故障。
|
||||
* - 尊重 prefers-reduced-motion:此时直接跳过观察器,内容由 CSS 强制可见。
|
||||
*/
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const reduceMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
).matches
|
||||
if (reduceMotion) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue
|
||||
const el = entry.target as HTMLElement
|
||||
const delay = Number(el.dataset.revealDelay || 0)
|
||||
const reveal = () => el.classList.add('is-visible')
|
||||
if (delay > 0) {
|
||||
window.setTimeout(reveal, delay)
|
||||
} else {
|
||||
reveal()
|
||||
}
|
||||
observer.unobserve(el)
|
||||
}
|
||||
},
|
||||
{ threshold: 0, rootMargin: '0px 0px -10% 0px' }
|
||||
)
|
||||
|
||||
const scan = () => {
|
||||
const els = document.querySelectorAll<HTMLElement>(
|
||||
'[data-reveal]:not(.is-visible)'
|
||||
)
|
||||
els.forEach((el) => observer.observe(el))
|
||||
}
|
||||
|
||||
// 合并短时间内的多次 DOM 变更,避免频繁全量查询
|
||||
let rafId = 0
|
||||
const scheduleScan = () => {
|
||||
if (rafId) return
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = 0
|
||||
scan()
|
||||
})
|
||||
}
|
||||
|
||||
// 持续监听 DOM 新增节点:任何后续插入的 [data-reveal] 都会被补扫,
|
||||
// 从根本上避免「元素永久停留在 opacity:0」的问题。
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.addedNodes.length > 0) {
|
||||
scheduleScan()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const startMutationWatch = () => {
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
}
|
||||
|
||||
// 首屏挂载后扫描一次,并开始监听后续 DOM 变更
|
||||
nuxtApp.hook('app:mounted', () => {
|
||||
scheduleScan()
|
||||
startMutationWatch()
|
||||
// 兜底:水合完成后 Vue 可能仍在替换不一致的子树,稍后再补扫一次
|
||||
window.setTimeout(scheduleScan, 300)
|
||||
})
|
||||
|
||||
// 客户端路由切换后重新扫描(新页面可能有新的 [data-reveal])
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
scheduleScan()
|
||||
})
|
||||
|
||||
// 页面卸载时释放监听
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.dispose(() => {
|
||||
mutationObserver.disconnect()
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { CmsSiteInfo } from '~/types'
|
||||
|
||||
/**
|
||||
* 站点信息初始化(服务端)
|
||||
*
|
||||
* 在 SSR 首次渲染前,把中间件(tenant.ts)预取的 CMS 站点信息
|
||||
* 注入到 useState('site-info'),从而随 SSR payload 序列化到客户端。
|
||||
*
|
||||
* 为什么需要这个插件:
|
||||
* 模板解析 useTemplate().resolvedTemplateId 以「站点库(CMS site_info.templateId)
|
||||
* 优先」为准。而布局级组件 SiteHeader / SiteFooter 的 loadTemplate() 在页面
|
||||
* fetchSiteInfo() 之前就执行——若此时 siteInfo 为空,siteTemplateId 拿不到值,
|
||||
* 会回退到 app_product / 环境默认值,导致「改数据库模板 ID 不生效」,
|
||||
* 甚至布局与页面渲染出两套模板。
|
||||
* 本插件在渲染前注入 siteInfo,确保首屏 siteTemplateId 即可用。
|
||||
*/
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { siteInfo } = useSite()
|
||||
const event = useRequestEvent()
|
||||
const siteInfoFromContext = event?.context?.siteInfo as CmsSiteInfo | undefined
|
||||
|
||||
if (siteInfoFromContext && !siteInfo.value) {
|
||||
siteInfo.value = siteInfoFromContext
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user