feat: add customer service chat, optimize case and homepage, fix api proxy

这一提交完成了多项功能升级与优化:
1.  新增全局浮动客服组件,集成腾讯云TRTC聊天能力并添加错误格式化工具
2.  新增图片资源并调整部分静态文件结构
3.  优化案例页面:重构筛选逻辑、新增空状态处理、添加加载更多功能
4.  优化首页布局:修复统计数字动画、调整移动端适配样式、优化轮播逻辑
5.  重构官方内容获取工具:添加缓存机制、优化媒体资源处理
6.  优化API代理配置,移除默认本地代理并新增环境变量配置
7.  新增页面数据恢复刷新逻辑,优化页面交互体验
8.  修复按钮组件冗余代码,调整头部导航移动端样式
This commit is contained in:
2026-07-24 18:55:51 +08:00
parent 314cd6ccd7
commit 2ff1064de7
25 changed files with 4596 additions and 327 deletions
+69 -11
View File
@@ -2,14 +2,32 @@ const ALL_LABEL = '全部'
const LATEST_LABEL = '最新文章'
const FALLBACK_IMAGE = '/images/lanhu/slices/home-group95-image21@2x.png'
function isUsableUrl(value) {
const url = String(value || '').trim()
if (!url || url === '#') return false
return !/^javascript:/i.test(url)
}
function pickUrl(asset) {
return asset?.url || asset?.fileUrl || asset?.filePath || ''
return [asset?.url, asset?.fileUrl, asset?.filePath].find(isUsableUrl) || ''
}
function buildMediaMap(media = []) {
return new Map(media.map((asset) => [asset.id, asset]))
}
function noCacheOptions(query = {}) {
return {
query: {
...query,
_t: Date.now()
},
headers: {
'Cache-Control': 'no-cache'
}
}
}
function toTags(value) {
if (Array.isArray(value)) return value.filter(Boolean)
return String(value || '')
@@ -29,23 +47,57 @@ function bySortAndTime(left, right) {
export function useOfficialContent() {
const config = useRuntimeConfig()
const serverApiBase = `${String(config.officialApiProxyTarget || 'http://localhost:8080').replace(/\/$/, '')}/api`
const apiBase = import.meta.server ? serverApiBase : (config.public.officialApiBase || '/api')
const nuxtApp = useNuxtApp()
const apiBase = config.public.officialApiBase || '/api'
const siteConfigCache = useState('public-site-config-cache', () => null)
const siteConfigLoaded = useState('public-site-config-loaded', () => false)
function fetchPublicContent(type) {
console.log(apiBase)
return $fetch('/public/content', {
baseURL: apiBase,
query: { type }
...noCacheOptions({ type })
})
}
function fetchPublicCategories() {
return $fetch('/public/categories', {
baseURL: apiBase,
...noCacheOptions()
})
}
function fetchHomepageSections() {
return $fetch('/public/homepage/sections', { baseURL: apiBase })
return $fetch('/public/homepage/sections', {
baseURL: apiBase,
...noCacheOptions()
})
}
function fetchPublicSiteConfig() {
return $fetch('/public/site-config', { baseURL: apiBase })
function fetchPublicSiteConfig(options = {}) {
if (!options.fresh && siteConfigLoaded.value) return siteConfigCache.value || {}
nuxtApp._publicSiteConfigPromise ||= $fetch('/public/site-config', {
baseURL: apiBase,
...noCacheOptions()
})
.then((rows) => {
siteConfigCache.value = rows || {}
siteConfigLoaded.value = true
return siteConfigCache.value
})
.finally(() => {
nuxtApp._publicSiteConfigPromise = null
})
return nuxtApp._publicSiteConfigPromise
}
function fetchPublicMediaAsset(id) {
if (!id) return null
return $fetch(`/public/media/${encodeURIComponent(id)}`, {
baseURL: apiBase,
...noCacheOptions()
})
}
function assetUrl(assetId, mediaMap, fallback = FALLBACK_IMAGE) {
@@ -61,7 +113,8 @@ export function useOfficialContent() {
function normalizeCase(item, mediaMap) {
const metric = item.metricValue || ''
const metricLabel = item.metricLabel || ''
const clientName = item.isAnonymous && item.clientName ? `匿名客户:${item.clientName}` : item.clientName
const clientName =
item.isAnonymous && item.clientName ? `匿名客户:${item.clientName}` : item.clientName
return {
...item,
@@ -73,7 +126,10 @@ export function useOfficialContent() {
description: item.description || item.summary || '',
metric,
metricLabel,
metrics: [[metric, metricLabel], [item.secondary, '辅助成果']].filter((row) => row[0]),
metrics: [
[metric, metricLabel],
[item.secondary, '辅助成果']
].filter((row) => row[0]),
secondary: item.secondary || '',
contentHtml: item.contentHtml || ''
}
@@ -104,7 +160,7 @@ export function useOfficialContent() {
excerpt: item.summary || item.description || '',
cta: item.cta || '获取白皮书',
cover: item.coverUrl || assetUrl(item.coverAssetId, mediaMap),
documentUrl: item.documentUrl || pickUrl(documentAsset),
documentUrl: isUsableUrl(item.documentUrl) ? item.documentUrl : pickUrl(documentAsset),
documentName: item.documentName || documentAsset?.name || item.title,
contentHtml: item.contentHtml || ''
}
@@ -120,8 +176,10 @@ export function useOfficialContent() {
LATEST_LABEL,
bySortAndTime,
buildMediaMap,
fetchPublicCategories,
fetchPublicContent,
fetchHomepageSections,
fetchPublicMediaAsset,
fetchPublicSiteConfig,
normalizeCase,
normalizeArticle,