diff --git a/frontend/src/api/fixedSection.js b/frontend/src/api/fixedSection.js index d0769f1..6ae7cf7 100644 --- a/frontend/src/api/fixedSection.js +++ b/frontend/src/api/fixedSection.js @@ -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) { diff --git a/frontend/src/api/home.js b/frontend/src/api/home.js index a02b139..7865c0e 100644 --- a/frontend/src/api/home.js +++ b/frontend/src/api/home.js @@ -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、无轮播控件) diff --git a/frontend/src/generated/page-body.html b/frontend/src/generated/page-body.html index 559f162..f064a9b 100644 --- a/frontend/src/generated/page-body.html +++ b/frontend/src/generated/page-body.html @@ -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; } diff --git a/frontend/src/style.css b/frontend/src/style.css index 90726bf..a886732 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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; } /*