fix(site): adaptive stat font scaling, marquee gap and inject race / 统计四宫格数字自适应缩放防串行、跑马灯拼接缝隙统一、双注入路径竞态防护

This commit is contained in:
liuwenxin 2026-09-18 11:35:08 +08:00
parent 90ba3016c0
commit 61609d3566
4 changed files with 114 additions and 32 deletions

View File

@ -1,4 +1,4 @@
/**
/**
* 鍥哄畾鏉垮潡鍐呭娉ㄥ叆閫昏緫
*/
@ -114,27 +114,102 @@ function splitSection2StatTitle(raw) {
}
/** 銆屽叧浜庡崕闃炽€嶅睆鍙充晶鍥涘鏍硷細.e_loop-39 .p_list + .cbox-39.p_loopitem */
/**
* 统计四宫格数字自适应卡片宽度:
* 基准取归档 CSS 当前断点计算字号(桌面 50px),强制单行不换行;
* 文字超宽时等比缩小(下限 14px),仍放不下则省略号兜底,避免串行或硬裁切。
*/
export function fitStatNumFontSize(numSpan) {
if (!numSpan) return
numSpan.style.whiteSpace = 'nowrap'
/* inline 元素 scrollWidth 不反映溢出宽度,改 inline-block 才能测出真实文本宽 */
numSpan.style.display = 'inline-block'
/* 极长文字缩到下限仍放不下时,省略号兜底(优于硬裁切) */
numSpan.style.maxWidth = '100%'
numSpan.style.overflow = 'hidden'
numSpan.style.textOverflow = 'ellipsis'
/* 断点下父级 line-height:1 小于字体实际渲染高,inline-block+overflow 会裁掉下缘(如字母 q 的下伸部),撑足行高(实测字体度量需 ≥1.34) */
numSpan.style.lineHeight = '1.35'
numSpan.style.fontSize = ''
const box = numSpan.parentElement
if (!box) return
const cs = getComputedStyle(box)
const avail = box.clientWidth - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0)
if (avail <= 0) return
const w = numSpan.scrollWidth
if (w > avail) {
const base = parseFloat(getComputedStyle(numSpan).fontSize) || 50
/* avail 再留 2px 余量,抵消 floor 取整与亚像素测量误差;下限 14px 保证可读 */
numSpan.style.fontSize = Math.max(14, Math.floor((base * (avail - 2)) / w)) + 'px'
}
}
function refitAllStatNums() {
document.querySelectorAll('.e_loop-39 .p_num').forEach(fitStatNumFontSize)
}
let statNumFitResizeTimer = null
/* 断点切换后归档字号变化(50/40/36/30px),防抖统一重算已注入数字 */
function scheduleStatNumFitOnResize() {
if (statNumFitResizeTimer) clearTimeout(statNumFitResizeTimer)
statNumFitResizeTimer = setTimeout(refitAllStatNums, 200)
}
if (typeof window !== 'undefined') window.addEventListener('resize', scheduleStatNumFitOnResize)
let statNumFitHooksInstalled = false
/*
* 注入发生在首页 swiper 非激活屏时 clientWidth=0,fit 会跳过缩放;
* 且注入可能早于 webfont 加载(fallback 字体测宽偏窄)。
* 故在多个时机补算:首次滚入视口、字体就绪、延迟兜底(flag 保证只挂一次)。
*/
export function installStatNumFitHooks() {
if (statNumFitHooksInstalled || typeof window === 'undefined') return
statNumFitHooksInstalled = true
if (document.fonts?.ready) document.fonts.ready.then(() => refitAllStatNums())
setTimeout(refitAllStatNums, 600)
if ('IntersectionObserver' in window) {
const list = document.querySelector('.e_loop-39 .p_list')
if (list) {
const io = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) {
refitAllStatNums()
io.disconnect()
}
})
io.observe(list)
}
}
}
/** 单张统计卡内容写入:标题按「主数字|单位」拆分,描述/图标兜底,供四宫格两条注入路径(section2 子项 / home features)复用 */
export function applyStatCardContent(cardEl, rawTitle, desc, iconUrl) {
const { num, unit } = splitSection2StatTitle(rawTitle)
const numSpan = cardEl.querySelector('.p_num')
if (numSpan) {
numSpan.textContent = num
if (num) numSpan.setAttribute('data-num', num)
}
const plus = cardEl.querySelector('.p_plus')
if (plus) plus.textContent = unit || ''
const label = cardEl.querySelector('.e_text-41')
if (label) label.textContent = desc || ''
const img = cardEl.querySelector('.e_image-42 img')
if (img && iconUrl) bindImageFallback(img, resolveImageUrl(iconUrl))
}
function injectSection2AboutStatGrid(listEl, templateEl, items) {
const clone = templateEl.cloneNode(true)
listEl.innerHTML = ''
items.forEach((item) => {
const node = clone.cloneNode(true)
node.classList.remove('swiper-slide-duplicate', 'swiper-slide-active', 'swiper-slide-next')
const { num, unit } = splitSection2StatTitle(item.itemTitle)
const numSpan = node.querySelector('.p_num')
if (numSpan) {
numSpan.textContent = num
if (num) numSpan.setAttribute('data-num', num)
}
const plus = node.querySelector('.p_plus')
if (plus) plus.textContent = unit || ''
const label = node.querySelector('.e_text-41')
if (label) label.textContent = item.itemDesc || ''
const img = node.querySelector('.e_image-42 img')
const imgUrl = firstImageUrl(item.itemImage)
if (img && imgUrl) bindImageFallback(img, resolveImageUrl(imgUrl))
applyStatCardContent(node, item.itemTitle, item.itemDesc, firstImageUrl(item.itemImage))
listEl.appendChild(node)
fitStatNumFontSize(node.querySelector('.p_num'))
})
/* section2 子项注入后打标:home features 路径见标记即跳过,避免并行竞态覆盖(子项优先) */
if (items.length > 0) listEl.setAttribute('data-hy-stat-injected', '1')
installStatNumFitHooks()
}
function applySection2AboutMainContent(slideEl, localizedData, code) {

View File

@ -1,7 +1,14 @@
import { resolveImageUrl } from '../utils/asset'
import { applyImageLoadPriority } from '../utils/imageLoading'
import { remember } from '../utils/asyncCache'
import { clearHomeAboutArchiveFallback, fetchFixedSection, injectAllFixedSections } from './fixedSection'
import {
clearHomeAboutArchiveFallback,
fetchFixedSection,
injectAllFixedSections,
applyStatCardContent,
fitStatNumFontSize,
installStatNumFitHooks
} from './fixedSection'
import { fetchProductPublic } from './product'
import { currentLang } from './language'
import i18n from '../i18n'
@ -319,25 +326,22 @@ export async function injectHomeContent(options = {}) {
}
}
// 2. 特色模块注入 (e_loop-39)
// 2. 特色模块注入 (e_loop-39):section2 子项优先(fixedSection 注入后带 data-hy-stat-injected 标记,此处跳过避免并行竞态覆盖)
const featureListContainer = document.querySelector('.e_loop-39 .p_list')
if (data && featureListContainer && data.features && data.features.length > 0) {
if (data && featureListContainer && data.features && data.features.length > 0 && !featureListContainer.hasAttribute('data-hy-stat-injected')) {
const originalItems = Array.from(featureListContainer.querySelectorAll('.p_loopitem'))
featureListContainer.innerHTML = ''
data.features.forEach((feature) => {
const item = (originalItems[0] || document.createElement('div')).cloneNode(true)
const titleEl = item.querySelector('.p_num')
if (titleEl) titleEl.textContent = feature.featureTitle
const descEl = item.querySelector('.e_text-41')
if (descEl) descEl.textContent = feature.featureDesc
item.classList.remove('swiper-slide-duplicate', 'swiper-slide-active', 'swiper-slide-next')
applyStatCardContent(item, feature.featureTitle, feature.featureDesc, feature.featureIcon)
const imgEl = item.querySelector('.e_image-42 img')
if (imgEl) {
applyImageLoadPriority(imgEl, 'lazy')
imgEl.src = resolveImageUrl(feature.featureIcon)
}
if (imgEl) applyImageLoadPriority(imgEl, 'lazy')
featureListContainer.appendChild(item)
fitStatNumFontSize(item.querySelector('.p_num'))
})
installStatNumFitHooks()
}
// 3. 首页产品 (#home-product-grid) — 仅 div + CSS:最多 4 条,双行轨道 translateX 向左无缝循环(无 Swiper、无轮播控件)

View File

@ -58,6 +58,8 @@
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px 20px;
/* 右侧留白=列间距:修复行尾卡与克隆行首卡之间 0px 间隙,保持无缝循环缝隙均匀 */
padding-right: 20px;
box-sizing: border-box;
align-items: stretch;
}

View File

@ -834,13 +834,14 @@ body[data-pageid='about-honor-page'] .haiho-page #c_static_001_P_4578-1694141444
}
/*
* 首页「关于华阳」屏右侧四宫格统计数字(.p_num,红色):
* 归档 Home_*.min.css 按断点写了 50/40/36/30px,统一覆盖为 35px;
* 两个 slide id 均覆盖,避免不同断点字号跳变。
* 统计四宫格(.cbox-39 为 flex:0 0 48% 且不可收缩的 flex 项):
* fitSection2StatNumFontSize 给 .p_num 加了 nowrap,超长文字的 min-content
* 会沿 min-width:auto 链路把 48% 卡片撑大,触发 p_list 的 flex-wrap 串行;
* 解除卡片及其内部 flex 项的自动最小宽,让卡片保持 48%、长文字由 JS 缩放。
*/
.haiho-page #c_static_001_P_5212-1683353667670 .e_scrollNumber-40 .p_num,
.haiho-page #c_static_001_P_5212-17155923594210 .e_scrollNumber-40 .p_num {
font-size: 35px;
.haiho-page #c_static_001_P_5212-1683353667670 .e_loop-39 .cbox-39,
.haiho-page #c_static_001_P_5212-1683353667670 .e_loop-39 .e_container-47 > .p_item {
min-width: 0;
}
/*