Files
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

278 lines
9.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CmsSiteInfo, CmsNavigation, SiteConfig, SiteSetting, SocialLink, ApiEnvelope } from '~/types'
import { useApp } from './useApp'
import { useRuntimeConfig } from '#imports'
import { mapNavTitle } from '~/utils'
import { ensureFullUrl } from '~/utils/image'
/**
* 获取当前站点信息
* 调用 /api/site/info 代理接口
* 返回 CMS getSiteInfo 的完整数据,包括导航、配置等
*
* 站点名称 / Logo 优先使用应用信息(AppProduct)接口的数据,
* 回退到 CMS 站点信息(websiteName / websiteLogo)。
*/
export function useSite() {
// 应用产品信息(名称 / Logo 优先使用 AppProduct
const { appName, appLogo, appIcon } = useApp()
const siteInfo = useState<CmsSiteInfo | null>('site-info', () => null)
const loading = useState<boolean>('site-loading', () => false)
const error = useState<string | null>('site-error', () => null)
async function fetchSiteInfo() {
// 已加载(SSR 已注入)则直接返回,避免重复请求 CMS
if (siteInfo.value) return siteInfo.value
loading.value = true
error.value = null
try {
const res = await $fetch<ApiEnvelope<CmsSiteInfo> | CmsSiteInfo>('/api/site/info')
const envelope = res as ApiEnvelope<CmsSiteInfo>
const data = envelope?.data ?? (res as CmsSiteInfo)
siteInfo.value = data
return data
} catch (e: any) {
error.value = e?.message || '获取站点信息失败'
return null
} finally {
loading.value = false
}
}
/**
* 顶部导航
*
* 后端 getSiteInfo 的 setSafeWebsiteNavigation 把栏目分到 topNavs / bottomNavs 两个数组,
* 但该分组函数不可靠、且数组名与实际内容常常相反:
* - 典型租户(如汇吉采):真正置顶的菜单(首页/关于/核心业务…)被塞进 bottomNavs 且 top=1
* 而 topNavs 仅含一条底部链接(资料下载, top=0)。
* - 也有租户(如 cz-hro):topNavs 有数据但 top 标志不可靠(top=0),需直接信任后端分组。
*
* 因此以权威标志 `top === 1`(标记「应置顶」)合并两个数组来取顶部菜单,
* 不再依赖数组名 topNavs / bottomNavs
* 1) 合并 topNavs + bottomNavs,取 top===1 且未隐藏/未删除的项;
* 2) 若没有任何 top===1 项(top 标志整体不可靠,如 cz-hro)→ 兜底信任 topNavs。
*/
const navigations = computed<CmsNavigation[]>(() => {
const top = [
...(siteInfo.value?.topNavs || []),
...(siteInfo.value?.bottomNavs || [])
]
.filter((nav) => nav.top === 1 && !nav.hide && !nav.deleted)
.map(mapNavTitle)
if (top.length) return top
// 兜底:top 标志不可靠时,直接信任后端 topNavs 分组
return (siteInfo.value?.topNavs || [])
.filter((nav) => !nav.hide && !nav.deleted)
.map(mapNavTitle)
})
/**
* 底部导航(页脚链接):取 top!==1(含 top=0 或缺失)且未隐藏/未删除的项,
* 合并 topNavs + bottomNavs,与顶部菜单互斥、不重复。
*/
const bottomNavigations = computed<CmsNavigation[]>(() => {
return [
...(siteInfo.value?.topNavs || []),
...(siteInfo.value?.bottomNavs || [])
]
.filter((nav) => nav.top !== 1 && !nav.hide && !nav.deleted)
.map(mapNavTitle)
})
/**
* 完整导航树(合并 top + bottom,仅过滤 hide/deleted,不限 top/bottom)。
* 用于「栏目 navId 识别 / 栏目定位」等路由场景,覆盖 top!==1 的子栏目、页脚栏目。
* 注意:顶部菜单显示请仍用 `navigations`top===1 过滤),不要混用。
*/
const allNavigations = computed<CmsNavigation[]>(() => {
const all = [
...(siteInfo.value?.topNavs || []),
...(siteInfo.value?.bottomNavs || [])
]
return all.filter((nav) => !nav.hide && !nav.deleted).map(mapNavTitle)
})
/** 站点配置 */
const siteConfig = computed<SiteConfig | null>(() => {
return siteInfo.value?.config || null
})
/** 站点功能设置(后台「网站设置」下发的开关集合,含 searchBtn / search 等) */
const siteSetting = computed<SiteSetting | null>(() => {
return siteInfo.value?.setting || null
})
/**
* 是否显示头部搜索框
* 受后台 setting.searchBtn(搜索按钮开关)控制;兼容 setting.search。
* 未配置时默认 true(全站显示),避免存量站点突然丢失搜索能力。
*/
const showHeaderSearch = computed<boolean>(() => {
const s = siteInfo.value?.setting
if (typeof s?.searchBtn === 'boolean') return s.searchBtn
if (typeof s?.search === 'boolean') return s.search
return true
})
/** 站点名称(环境变量覆盖 > CMS 站点信息(企业名称) > 应用信息(产品名)) */
const siteName = computed(() => {
const override = (useRuntimeConfig().public.siteName as string) || ''
return override || siteInfo.value?.websiteName || appName.value || ''
})
/** 站点 Logo(优先应用信息,回退 CMS 站点信息) */
const siteLogo = computed(() => {
return appLogo.value || siteInfo.value?.websiteLogo || ''
})
/**
* 站点图标(无 Logo 时用于 Logo 区回退:图标 + 网站名称)
* 优先级:应用图标 > CMS websiteIcon;统一 ensureFullUrl 补全路径。
*/
const siteIcon = computed(() => {
const raw = appIcon.value || siteInfo.value?.websiteIcon || ''
return raw ? ensureFullUrl(raw) : ''
})
/** 站点关键词 */
const siteKeywords = computed(() => {
return siteInfo.value?.keywords || ''
})
/**
* 联系电话
* 优先级:1) siteInfo.phone(完整号码) 2) 环境变量兜底 NUXT_PUBLIC_PHONE 3) config.tel
* 说明:后端 CMS 的 phone 字段可能因敏感策略返回脱敏值(含 *),此时用环境变量兜底。
*/
const phone = computed(() => {
const rawPhone = siteInfo.value?.phone || ''
if (rawPhone && !rawPhone.includes('*')) return rawPhone
return (useRuntimeConfig().public.phone as string) || siteInfo.value?.config?.tel || rawPhone || ''
})
/** 邮箱 */
const email = computed(() => {
return siteInfo.value?.config?.email || siteInfo.value?.email || ''
})
/** 地址(优先 siteInfo.address,其次 config.address */
const address = computed(() => {
return siteInfo.value?.address || siteInfo.value?.config?.address || ''
})
/** ICP 备案号 */
const icpNo = computed(() => {
return siteInfo.value?.config?.icpNo || siteInfo.value?.icpNo || ''
})
/** 版权信息 */
const copyright = computed(() => {
return siteInfo.value?.config?.copyright || ''
})
/** 微信二维码(环境变量覆盖 > config.wxQrcode > 顶层 qrCode */
const wxQrcode = computed(() => {
const override = (useRuntimeConfig().public.wxQrcode as string) || ''
return override || siteInfo.value?.config?.wxQrcode || (siteInfo.value?.qrCode as string) || ''
})
/** 品牌标语 / Slogan(优先站点顶层 slogan,其次 config.slogan */
const slogan = computed(() => {
return siteInfo.value?.slogan || (siteInfo.value?.config as SiteConfig | undefined)?.slogan || ''
})
/** 官网域名(优先 siteInfo.domain,其次 config.Domain / config.SysDomain */
const officialWebsite = computed(() => {
const cfg = siteInfo.value?.config as SiteConfig | undefined
return siteInfo.value?.domain || cfg?.Domain || cfg?.SysDomain || ''
})
/** 官网域名(补全协议,用于 <a :href> */
const officialWebsiteUrl = computed(() => {
const raw = officialWebsite.value
if (!raw) return ''
return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`
})
/** 社交外部链接(后台「网站设置」录入,标准字段名 socialLinks;兼容旧 links 别名),URL 统一补全协议 */
const socialLinks = computed<SocialLink[]>(() => {
const cfg = siteInfo.value?.config as SiteConfig | undefined
const raw =
(siteInfo.value?.socialLinks as SocialLink[] | unknown | undefined) ??
(siteInfo.value?.links as SocialLink[] | unknown | undefined) ??
cfg?.socialLinks ??
cfg?.links
let list: SocialLink[] = []
if (Array.isArray(raw)) list = raw as SocialLink[]
else if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw)
list = Array.isArray(parsed) ? (parsed as SocialLink[]) : []
} catch {
list = []
}
}
return list.map((l) => ({
...l,
url: l.url && !/^https?:\/\//i.test(l.url) ? `https://${l.url}` : l.url
}))
})
/** 到期时间 */
const expirationTime = computed(() => {
return siteInfo.value?.expirationTime || ''
})
/**
* 模板目录名(template-XX):【code 优先,主键兜底】
*
* templateCodeapp_template.code)与前端 app/templates/ 目录一一对应,是权威标识;
* templateId 主键存在跳号风险(历史上 id=8 缺失导致整体错位一位),仅在 code 缺失时兜底。
* 为空返回 '' 由上层回退默认模板。
*/
const templateId = computed(() => {
return resolveTemplateKey(siteInfo.value?.templateCode, siteInfo.value?.templateId)
})
/**
* 按主键推导的模板目录名(兜底候选)
* 供 useTemplate.loadTemplate 在「code 指向的目录不存在」时二次尝试,
* 避免直接跌回 template-01 造成风格完全错乱。
*/
const templateIdFallback = computed(() => {
const byId = toTemplateKey(siteInfo.value?.templateId)
return byId === templateId.value ? '' : byId
})
return {
siteInfo,
navigations,
bottomNavigations,
allNavigations,
siteConfig,
siteSetting,
showHeaderSearch,
siteName,
siteLogo,
siteIcon,
siteKeywords,
phone,
email,
address,
icpNo,
copyright,
wxQrcode,
slogan,
officialWebsite,
officialWebsiteUrl,
socialLinks,
expirationTime,
templateId,
templateIdFallback,
loading,
error,
fetchSiteInfo
}
}