diff --git a/tmp-active.mjs b/tmp-active.mjs new file mode 100644 index 0000000..3298504 --- /dev/null +++ b/tmp-active.mjs @@ -0,0 +1,64 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 测试所有 6 个标签 + const labels = ['碳核算平台', '碳交易平台', '碳认证机构', '碳金融服务', '碳技术咨询', '更多能力']; + const expectedAnchors = ['content-1', 'content-3', 'content-2', 'content-4', 'content-5', null]; + + for (let i = 0; i < labels.length; i++) { + await page.locator('.capability-card').nth(i).click(); + await page.waitForFunction(() => location.href.includes('gxnlpt'), { timeout: 5000 }); + await page.waitForTimeout(2500); + + const data = await page.evaluate(() => { + const sideItems = document.querySelectorAll('.gxnlpt-side-item'); + const active = Array.from(sideItems).map((el, idx) => ({ + idx, text: el.textContent.trim(), active: el.classList.contains('is-active') + })).find(x => x.active); + const content1 = document.getElementById('content-1'); + const content2 = document.getElementById('content-2'); + const content3 = document.getElementById('content-3'); + const content4 = document.getElementById('content-4'); + const content5 = document.getElementById('content-5'); + const rects = {}; + for (const el of [content1, content2, content3, content4, content5]) { + if (el) rects[el.id] = Math.round(el.getBoundingClientRect().top); + } + return { + url: location.href, + scrollTop: document.querySelector('.content-wrap').scrollTop, + activeTab: active, + sectionTops: rects, + }; + }); + console.log(`[${labels[i]}]`, JSON.stringify(data)); + + // 回到首页 + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + } + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-capability-debug.mjs b/tmp-capability-debug.mjs new file mode 100644 index 0000000..5e5b026 --- /dev/null +++ b/tmp-capability-debug.mjs @@ -0,0 +1,67 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + page.on('pageerror', (e) => console.log('[pageerror]', e.message)); + page.on('console', (m) => { + const t = m.text(); + console.log(`[browser:${m.type()}]`, t); + }); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + console.log('home loaded'); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳核算平台" + const cardIndex = 0; + await page.locator('.capability-card').nth(cardIndex).click(); + // 多等几秒看是否跳转 + await page.waitForTimeout(4000); + console.log('url:', page.url()); + + // 检查 router-view 内的内容 + const data = await page.evaluate(() => { + const outlet = document.querySelector('.portal-route-outlet'); + if (!outlet) return { hasOutlet: false }; + const child = outlet.firstElementChild; + return { + hasOutlet: true, + hasChild: !!child, + childTag: child ? child.tagName : null, + childClass: child ? child.className : null, + hasGongXingNeng: !!document.querySelector('.gxnlpt-shell'), + hasCapabilitySection: !!document.querySelector('.capability-section'), + hasContent1: !!document.getElementById('content-1'), + bodyText: (document.body.innerText || '').slice(0, 300), + }; + }); + console.log('data:', data); + + await page.screenshot({ path: 'tmp-debug.png', fullPage: false }); + + // 等待 5s 后再次检查 + await page.waitForTimeout(5000); + const data2 = await page.evaluate(() => ({ + hasGongXingNeng: !!document.querySelector('.gxnlpt-shell'), + hasCapabilitySection: !!document.querySelector('.capability-section'), + hasContent1: !!document.getElementById('content-1'), + url: location.href, + })); + console.log('data2:', data2); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-capability-test.mjs b/tmp-capability-test.mjs new file mode 100644 index 0000000..c6ec66f --- /dev/null +++ b/tmp-capability-test.mjs @@ -0,0 +1,93 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + page.on('console', (m) => { + const t = m.text(); + if (t.includes('anchor') || t.includes('Capability') || t.includes('section')) { + console.log('[browser]', t); + } + }); + // 1) 打开首页 + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + // 等到 capability 区域可见 + await page.waitForSelector('.capability-section', { timeout: 20000 }); + console.log('home loaded'); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 截图 home + await page.screenshot({ path: 'tmp-home.png', fullPage: false }); + + // 取各 capability 卡的文本 + const cards = await page.$$eval('.capability-card .capability-name', (els) => els.map((e) => e.textContent.trim())); + console.log('capability cards:', cards); + + // 点击 "碳核算平台" + const cardIndex = cards.indexOf('碳核算平台'); + console.log('clicking index', cardIndex); + await page.locator('.capability-card').nth(cardIndex).click(); + await page.waitForTimeout(2500); + console.log('after click url:', page.url()); + + // 等 section 出现 + const hasContent1 = await page.locator('#content-1').count(); + console.log('content-1 exists:', hasContent1); + if (hasContent1) { + const inView = await page.locator('#content-1').evaluate((el) => { + const r = el.getBoundingClientRect(); + return { top: r.top, bottom: r.bottom, vh: window.innerHeight }; + }); + console.log('content-1 rect after click:', inView); + } + + await page.screenshot({ path: 'tmp-gxnlpt-content-1.png', fullPage: false }); + + // 回到首页 + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳认证机构" -> content-2 + const idx2 = cards.indexOf('碳认证机构'); + await page.locator('.capability-card').nth(idx2).click(); + await page.waitForTimeout(2500); + console.log('after click content-2 url:', page.url()); + const r2 = await page.locator('#content-2').evaluate((el) => { + const r = el.getBoundingClientRect(); + return { top: r.top, bottom: r.bottom }; + }); + console.log('content-2 rect after click:', r2); + + // 点击 "更多能力" + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + const moreIdx = cards.indexOf('更多能力'); + await page.locator('.capability-card').nth(moreIdx).click(); + await page.waitForTimeout(2000); + console.log('after click 更多能力 url:', page.url()); + await page.screenshot({ path: 'tmp-gxnlpt-more.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-debug2.mjs b/tmp-debug2.mjs new file mode 100644 index 0000000..fb9229d --- /dev/null +++ b/tmp-debug2.mjs @@ -0,0 +1,70 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3500); + + // 查看 DOM 状态 + const data = await page.evaluate(() => { + const out = {}; + const wrap = document.querySelector('.content-wrap'); + out.wrapScrollTop = wrap ? wrap.scrollTop : null; + out.wrapScrollHeight = wrap ? wrap.scrollHeight : null; + out.wrapClientHeight = wrap ? wrap.clientHeight : null; + out.wrapClasses = wrap ? wrap.className : null; + out.wrapStyleHeight = wrap ? getComputedStyle(wrap).height : null; + out.htmlClasses = document.documentElement.className; + out.htmlData = (function() { + const r = document.documentElement.getBoundingClientRect(); + return { width: r.width, height: r.height }; + })(); + const root = document.documentElement; + out.homeFigmaScale = getComputedStyle(root).getPropertyValue('--home-figma-scale'); + out.portalShellMinDesignHeight = getComputedStyle(root).getPropertyValue('--portal-shell-min-design-height'); + out.portalLandingFirstScreenHeight = getComputedStyle(root).getPropertyValue('--portal-landing-first-screen-height'); + out.pageNavHeight = getComputedStyle(root).getPropertyValue('--page-nav-height'); + + const outlet = document.querySelector('.portal-route-outlet'); + out.outletChildren = outlet ? outlet.children.length : null; + const firstChild = outlet && outlet.firstElementChild; + out.firstChildClass = firstChild ? firstChild.className : null; + out.firstChildBoundingRect = firstChild ? (function() { + const r = firstChild.getBoundingClientRect(); + return { top: r.top, left: r.left, width: r.width, height: r.height }; + })() : null; + out.firstChildStyle = firstChild ? firstChild.getAttribute('style') : null; + + // 找 content-1 + const c1 = document.getElementById('content-1'); + out.content1Rect = c1 ? (function() { + const r = c1.getBoundingClientRect(); + return { top: r.top, left: r.left, width: r.width, height: r.height }; + })() : null; + return out; + }); + console.log(JSON.stringify(data, null, 2)); + + // 不截图 + // await page.screenshot({ path: 'tmp-debug2.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-direct-1.png b/tmp-direct-1.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-direct-1.png differ diff --git a/tmp-direct-2.png b/tmp-direct-2.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-direct-2.png differ diff --git a/tmp-elementfrom.mjs b/tmp-elementfrom.mjs new file mode 100644 index 0000000..5d87a33 --- /dev/null +++ b/tmp-elementfrom.mjs @@ -0,0 +1,41 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3500); + + // 检查视口不同位置是什么元素 + const points = await page.evaluate(() => { + const results = []; + const pts = [[720, 100], [720, 300], [720, 500], [720, 700], [200, 200], [1200, 200], [720, 850]]; + for (const [x, y] of pts) { + const els = document.elementsFromPoint(x, y); + results.push({ x, y, els: els.slice(0, 5).map((el) => ({ tag: el.tagName, cls: (el.className || '').slice(0, 80), id: el.id })) }); + } + return results; + }); + console.log('points:'); + for (const p of points) { + console.log(` (${p.x},${p.y}):`, JSON.stringify(p.els)); + } + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-final-1.png b/tmp-final-1.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-final-1.png differ diff --git a/tmp-final-2.png b/tmp-final-2.png new file mode 100644 index 0000000..db1a147 Binary files /dev/null and b/tmp-final-2.png differ diff --git a/tmp-final.mjs b/tmp-final.mjs new file mode 100644 index 0000000..6ab6a3b --- /dev/null +++ b/tmp-final.mjs @@ -0,0 +1,46 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }); + const page = await ctx.newPage(); + + // 不使用 keep-alive + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 用 evaluate 在点击前修改 router 行为,禁用 keep-alive + // 不行;改用:点击后等久一点,确认 router 完成 + await page.locator('.capability-card').nth(0).click(); + // 等待 URL 变化 + await page.waitForFunction(() => location.href.includes('gxnlpt'), { timeout: 5000 }); + // 等待 gxnlpt-page 真正在视口里 + await page.waitForFunction(() => { + const el = document.querySelector('.gxnlpt-page'); + if (!el) return false; + const r = el.getBoundingClientRect(); + return r.top >= -10 && r.top < 200; + }, { timeout: 5000 }); + await page.waitForTimeout(2000); + + // 截 viewport + await page.screenshot({ path: 'tmp-final-1.png', fullPage: false }); + + // 截元素 + const el = await page.locator('.gxnlpt-page').first(); + await el.screenshot({ path: 'tmp-final-2.png' }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-gxnlpt-content-1.png b/tmp-gxnlpt-content-1.png new file mode 100644 index 0000000..9bb1fac Binary files /dev/null and b/tmp-gxnlpt-content-1.png differ diff --git a/tmp-gxnlpt-el.png b/tmp-gxnlpt-el.png new file mode 100644 index 0000000..db1a147 Binary files /dev/null and b/tmp-gxnlpt-el.png differ diff --git a/tmp-gxnlpt-more.png b/tmp-gxnlpt-more.png new file mode 100644 index 0000000..32a0d31 Binary files /dev/null and b/tmp-gxnlpt-more.png differ diff --git a/tmp-home.png b/tmp-home.png new file mode 100644 index 0000000..029ef40 Binary files /dev/null and b/tmp-home.png differ diff --git a/tmp-no-keepalive.mjs b/tmp-no-keepalive.mjs new file mode 100644 index 0000000..78e0451 --- /dev/null +++ b/tmp-no-keepalive.mjs @@ -0,0 +1,50 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + // 模拟 真实用户使用:刷新一次后不依赖 keep-alive + // 第一次访问 gxnlpt + await page.goto(`${BASE}/view/mhzc/gxnlpt`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.gxnlpt-page', { timeout: 20000 }); + await page.waitForTimeout(1500); + + // 直接访问首页 + 点击, 不等待 keep-alive 命中 + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳交易平台" + await page.locator('.capability-card').nth(1).click(); + await page.waitForFunction(() => location.href.includes('gxnlpt'), { timeout: 5000 }); + await page.waitForTimeout(2000); + + // 检查实际显示 + const data = await page.evaluate(() => { + const out = {}; + out.url = location.href; + out.scrollTop = document.querySelector('.content-wrap').scrollTop; + const sections = ['content-1', 'content-2', 'content-3', 'content-4', 'content-5'].map((id) => { + const el = document.getElementById(id); + return { id, top: el ? Math.round(el.getBoundingClientRect().top) : null }; + }); + out.sections = sections; + return out; + }); + console.log('after click 碳交易平台:', JSON.stringify(data, null, 2)); + + await page.screenshot({ path: 'tmp-no-keepalive.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-pix-1.png b/tmp-pix-1.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-pix-1.png differ diff --git a/tmp-pix-2.png b/tmp-pix-2.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-pix-2.png differ diff --git a/tmp-pix-3.png b/tmp-pix-3.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-pix-3.png differ diff --git a/tmp-pix-4.png b/tmp-pix-4.png new file mode 100644 index 0000000..db1a147 Binary files /dev/null and b/tmp-pix-4.png differ diff --git a/tmp-pix-5.png b/tmp-pix-5.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-pix-5.png differ diff --git a/tmp-pixel.mjs b/tmp-pixel.mjs new file mode 100644 index 0000000..1dd1e6b --- /dev/null +++ b/tmp-pixel.mjs @@ -0,0 +1,57 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3500); + + // 视口截图 + await page.screenshot({ path: 'tmp-pix-1.png', fullPage: false }); + + // 用 dom snapshot(用 canvas 截屏) + await page.evaluate(() => { + return new Promise((resolve) => { + // 强制重排 + document.body.offsetHeight; + resolve(); + }); + }); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp-pix-2.png', fullPage: false }); + + // 显式滚动到顶部 + await page.evaluate(() => { + const w = document.querySelector('.content-wrap'); + if (w) w.scrollTop = 0; + }); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp-pix-3.png', fullPage: false }); + + // 直接用 html2canvas 方式:通过截图 element 方式 + const gxnlptEl = await page.locator('.gxnlpt-page').first(); + await gxnlptEl.screenshot({ path: 'tmp-pix-4.png' }); + + // 强制重排 + 再次截图 + await page.evaluate(() => window.dispatchEvent(new Event('resize'))); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp-pix-5.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-snap-body.png b/tmp-snap-body.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-snap-body.png differ diff --git a/tmp-snap-viewport.png b/tmp-snap-viewport.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-snap-viewport.png differ diff --git a/tmp-snap.mjs b/tmp-snap.mjs new file mode 100644 index 0000000..10cade0 --- /dev/null +++ b/tmp-snap.mjs @@ -0,0 +1,54 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3500); + + // 给 gxnlpt-page 元素截图 + const gxnlptEl = await page.locator('.gxnlpt-page').first(); + await gxnlptEl.screenshot({ path: 'tmp-gxnlpt-el.png' }); + + // 给视口截图 + await page.screenshot({ path: 'tmp-snap-viewport.png', fullPage: false }); + + // 给 body 截图 + const body = await page.locator('body').first(); + await body.screenshot({ path: 'tmp-snap-body.png' }); + + // 检查 gxnlpt-page 的可见性 + const r = await gxnlptEl.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const style = getComputedStyle(el); + return { + rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height, bottom: rect.bottom, right: rect.right }, + display: style.display, + visibility: style.visibility, + opacity: style.opacity, + zIndex: style.zIndex, + transform: style.transform, + transformOrigin: style.transformOrigin, + position: style.position, + }; + }); + console.log('gxnlpt:', JSON.stringify(r, null, 2)); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-snap2.mjs b/tmp-snap2.mjs new file mode 100644 index 0000000..91da258 --- /dev/null +++ b/tmp-snap2.mjs @@ -0,0 +1,42 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + // 直接访问 gxnlpt?anchor=content-1(不要先访问 home),看看非 keep-alive 场景是否正常 + await page.goto(`${BASE}/view/mhzc/gxnlpt?anchor=content-1`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.gxnlpt-page', { timeout: 20000 }); + await page.waitForTimeout(3500); + + await page.screenshot({ path: 'tmp-direct-1.png', fullPage: false }); + + // 滚动到顶部 + await page.evaluate(() => { + const w = document.querySelector('.content-wrap'); + if (w) w.scrollTop = 0; + }); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp-direct-2.png', fullPage: false }); + + // 在视口顶部 evaluate 一下 + const data = await page.evaluate(() => { + const outlet = document.querySelector('.portal-route-outlet'); + const child = outlet && outlet.firstElementChild; + return { + outletChildCount: outlet ? outlet.children.length : 0, + childClass: child && child.className, + contentWrapScrollTop: document.querySelector('.content-wrap').scrollTop, + url: location.href, + }; + }); + console.log('data:', JSON.stringify(data)); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-zoom-1-home.png b/tmp-zoom-1-home.png new file mode 100644 index 0000000..44feda9 Binary files /dev/null and b/tmp-zoom-1-home.png differ diff --git a/tmp-zoom-2-after-click.png b/tmp-zoom-2-after-click.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-zoom-2-after-click.png differ diff --git a/tmp-zoom-3-top.png b/tmp-zoom-3-top.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-zoom-3-top.png differ diff --git a/tmp-zoom.mjs b/tmp-zoom.mjs new file mode 100644 index 0000000..0ac231a --- /dev/null +++ b/tmp-zoom.mjs @@ -0,0 +1,39 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 截图 home capability + await page.screenshot({ path: 'tmp-zoom-1-home.png', fullPage: false }); + + // 点击 "碳核算平台" + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3000); + console.log('url:', page.url()); + + await page.screenshot({ path: 'tmp-zoom-2-after-click.png', fullPage: false }); + + // 滚动到顶部再截图 + await page.evaluate(() => window.scrollTo(0, 0)); + await page.waitForTimeout(800); + await page.screenshot({ path: 'tmp-zoom-3-top.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-zoom2.mjs b/tmp-zoom2.mjs new file mode 100644 index 0000000..d5e0990 --- /dev/null +++ b/tmp-zoom2.mjs @@ -0,0 +1,75 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳核算平台" + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(3000); + console.log('url:', page.url()); + + // 检查实际 DOM + const data = await page.evaluate(() => { + const outlet = document.querySelector('.portal-route-outlet'); + const child = outlet && outlet.firstElementChild; + const sidebar = !!document.querySelector('.gxnlpt-sidebar'); + return { + hasOutlet: !!outlet, + hasChild: !!child, + childClass: child ? child.className : null, + hasGongXingNeng: !!document.querySelector('.gxnlpt-shell'), + hasGxnlptSidebar: sidebar, + hasContent1: !!document.getElementById('content-1'), + activeSection: (function() { + const els = document.querySelectorAll('[id^="content-"]'); + const inView = []; + els.forEach((el) => { + const r = el.getBoundingClientRect(); + if (r.top >= -50 && r.top < 800) { + inView.push({ id: el.id, top: Math.round(r.top) }); + } + }); + return inView; + })(), + visibleViewport: (function() { + const els = document.querySelectorAll('.gxnlpt-block, [id^="content-"], .gxnlpt-side-nav, .gxnlpt-sidebar'); + const all = []; + for (const el of els) { + const r = el.getBoundingClientRect(); + if (r.width === 0 || r.height === 0) continue; + all.push({ + tag: el.tagName, + cls: el.className && el.className.slice(0, 60), + id: el.id, + top: Math.round(r.top), + bottom: Math.round(r.bottom), + left: Math.round(r.left), + }); + } + return all; + })(), + }; + }); + console.log(JSON.stringify(data, null, 2)); + + await page.screenshot({ path: 'tmp-zoom2.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-zoom2.png b/tmp-zoom2.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-zoom2.png differ diff --git a/tmp-zoom3-no-eval.png b/tmp-zoom3-no-eval.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-zoom3-no-eval.png differ diff --git a/tmp-zoom3.mjs b/tmp-zoom3.mjs new file mode 100644 index 0000000..434f000 --- /dev/null +++ b/tmp-zoom3.mjs @@ -0,0 +1,32 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳核算平台" + await page.locator('.capability-card').nth(0).click(); + await page.waitForTimeout(5000); + console.log('url:', page.url()); + + // 不调用 evaluate,直接截图 + await page.screenshot({ path: 'tmp-zoom3-no-eval.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-zoom4.mjs b/tmp-zoom4.mjs new file mode 100644 index 0000000..55e1d4d --- /dev/null +++ b/tmp-zoom4.mjs @@ -0,0 +1,49 @@ +import { chromium } from 'playwright'; + +const BASE = 'http://localhost:9027'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const page = await ctx.newPage(); + + await page.goto(`${BASE}/view/mhzc/home`, { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForSelector('.capability-section', { timeout: 20000 }); + + // 滚动到 capability 区域 + await page.evaluate(() => { + const el = document.getElementById('section-capability'); + if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' }); + }); + await page.waitForTimeout(800); + + // 点击 "碳核算平台" + await page.locator('.capability-card').nth(0).click(); + + // 不停截图 + for (let i = 0; i < 10; i++) { + await page.waitForTimeout(1000); + const info = await page.evaluate(() => { + const contentWrap = document.querySelector('.content-wrap'); + const gxnlptSidebar = document.querySelector('.gxnlpt-sidebar'); + const home = document.querySelector('.capability-section'); + const content1 = document.getElementById('content-1'); + return { + url: location.href, + scrollTop: contentWrap ? contentWrap.scrollTop : null, + scrollHeight: contentWrap ? contentWrap.scrollHeight : null, + hasGxnlptSidebar: !!gxnlptSidebar, + hasHome: !!home, + content1Top: content1 ? Math.round(content1.getBoundingClientRect().top) : null, + }; + }); + console.log(`t=${i+1}s`, JSON.stringify(info)); + } + + await page.screenshot({ path: 'tmp-zoom4.png', fullPage: false }); + + await browser.close(); +})().catch((e) => { + console.error('TEST ERROR', e); + process.exit(1); +}); diff --git a/tmp-zoom4.png b/tmp-zoom4.png new file mode 100644 index 0000000..91d63f7 Binary files /dev/null and b/tmp-zoom4.png differ diff --git a/txw-mhzc-web/src/pages/index/router/routes.js b/txw-mhzc-web/src/pages/index/router/routes.js index c710b4d..0a185ed 100644 --- a/txw-mhzc-web/src/pages/index/router/routes.js +++ b/txw-mhzc-web/src/pages/index/router/routes.js @@ -314,7 +314,7 @@ export default [ title: '数据列表', isShowSideBar: false, hasHome: true, - breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/fwscsjlbc' }], + breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/tsjsc' }], disableBack: true, }, }, diff --git a/txw-mhzc-web/src/pages/index/views/fwsc/fwsc.vue b/txw-mhzc-web/src/pages/index/views/fwsc/fwsc.vue index 5d1984d..6f4fc38 100644 --- a/txw-mhzc-web/src/pages/index/views/fwsc/fwsc.vue +++ b/txw-mhzc-web/src/pages/index/views/fwsc/fwsc.vue @@ -930,7 +930,7 @@ export default { .price-value { font-size: 20px; font-weight: 600; - color: #FF4D4F; + color: #2E7D32; } .price-unit { diff --git a/txw-mhzc-web/src/pages/index/views/fwsc/sjlbc.vue b/txw-mhzc-web/src/pages/index/views/fwsc/sjlbc.vue index 8a3c924..94b02fb 100644 --- a/txw-mhzc-web/src/pages/index/views/fwsc/sjlbc.vue +++ b/txw-mhzc-web/src/pages/index/views/fwsc/sjlbc.vue @@ -320,7 +320,7 @@ export default { .breadcrumb-box { display: flex; align-items: center; - width: 100%; + width: 83%; max-width: 1400px; padding: 20px 20px 0; margin: 0 auto; @@ -354,7 +354,7 @@ export default { display: flex; align-items: flex-start; justify-content: space-between; - width: 100%; + width: 81%; max-width: 1400px; padding: 24px; margin: 16px auto 0; diff --git a/txw-mhzc-web/src/pages/index/views/fwsc/sjsc.vue b/txw-mhzc-web/src/pages/index/views/fwsc/sjsc.vue index af9001e..fdbc52e 100644 --- a/txw-mhzc-web/src/pages/index/views/fwsc/sjsc.vue +++ b/txw-mhzc-web/src/pages/index/views/fwsc/sjsc.vue @@ -36,11 +36,6 @@
{{ card.bt1 }}
- -
- - -
@@ -71,8 +66,8 @@
免费
-
- 立即咨询 +
+ 查看数据库
@@ -293,7 +288,6 @@ export default { fwnr: item.sjms, fwfw: item.fwfw || '', fwlxbqList: item.sjlxMc ? [item.sjlxMc] : [], - scbz: item.scbz || 'N', lxr: item.lxr || '', lxdh: item.lxdh || '', email: item.email || '', @@ -362,15 +356,9 @@ export default { this.rzVisible = true; } }, - // 收藏/取消收藏 - async handleCollect(card) { - try { - const type = card.scbz === 'Y' ? 'remove' : 'add'; - await gxzxApi.gxsc({ gxUuid: card.gxUuid, type }); - card.scbz = card.scbz === 'Y' ? 'N' : 'Y'; - } catch (error) { - console.error('收藏失败', error); - } + // 跳转到数据列表 + goToDataList(id) { + this.$router.push({ path: '/tsjlbc', query: { id } }); }, // 联系数据提供方 handleContact(card) { @@ -476,7 +464,7 @@ export default { color: #003B1A; cursor: pointer; background: transparent; - border-radius: 32px; + border-radius: 6px; transition: all 0.3s; &:hover { diff --git a/txw-mhzc-web/src/pages/index/views/fwsc/xqsc.vue b/txw-mhzc-web/src/pages/index/views/fwsc/xqsc.vue index bdca7bb..f081461 100644 --- a/txw-mhzc-web/src/pages/index/views/fwsc/xqsc.vue +++ b/txw-mhzc-web/src/pages/index/views/fwsc/xqsc.vue @@ -505,7 +505,7 @@ export default { color: #003B1A; cursor: pointer; background: transparent; - border-radius: 32px; + border-radius: 6px; transition: all 0.3s; &:hover { diff --git a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue index 281585f..3ba032c 100644 --- a/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue +++ b/txw-mhzc-web/src/pages/index/views/gxnlpt/index.vue @@ -672,12 +672,15 @@ export default { }, activated() { this.syncStackedNavLayout(); - if (this.contentView === 'list' && !this.stackedNavLayout) { - this.$nextTick(() => { + if (this.contentView !== 'list') return; + this.$nextTick(() => { + // 路由进入时携带 anchor,需要跳转到对应分类(keep-alive 缓存场景) + if (this.navigateToRouteAnchor()) return; + if (!this.stackedNavLayout) { this.initScrollSpy(); this.initSidebarSticky(); - }); - } + } + }); }, beforeDestroy() { this.unbindStackedNavMedia(); @@ -702,23 +705,32 @@ export default { this.applyDemoFallback(); } this.$nextTick(() => { - const anchor = this.$route.query.anchor || this.$route.params.anchor; - if (anchor) { - const index = this.categoryList.findIndex((item) => item.id === anchor); - const tabIndex = index >= 0 ? index : 0; - if (this.isStackedNavMode) { - this.activeTabIndex = tabIndex; - } else { - this.initScrollSpy(); - this.initSidebarSticky(); - this.scrollToSection(anchor, tabIndex); - } - return; - } + if (this.navigateToRouteAnchor()) return; this.initScrollSpy(); this.initSidebarSticky(); }); }, + /** + * 根据当前路由的 anchor 参数,跳转到对应的分类区块。 + * 返回 true 表示有 anchor 并已处理;返回 false 表示没有 anchor,调用方应继续后续初始化。 + * 同时供 bootstrap(mounted 首次进入)与 activated(keep-alive 缓存命中)复用。 + */ + navigateToRouteAnchor() { + const anchor = this.$route.query.anchor || this.$route.params.anchor; + if (!anchor) return false; + const index = this.categoryList.findIndex((item) => item.id === anchor); + const tabIndex = index >= 0 ? index : 0; + if (this.isStackedNavMode) { + // 窄屏:切到对应 tab,并把内容滚到顶部,与 handleCategoryNav 行为一致 + this.activeTabIndex = tabIndex; + this.$nextTick(() => this.scrollListContentToTop()); + return true; + } + this.initScrollSpy(); + this.initSidebarSticky(); + this.scrollToSection(anchor, tabIndex); + return true; + }, getIconUrl(iconName) { return SIDE_ICON_GRAY[iconName] || SIDE_ICON_GRAY['thspt.svg']; }, @@ -1276,11 +1288,11 @@ export default { * 原因:.portal-figma-scale-stage 有 transform:scale(), * 会创建新的 containing block,导致 CSS position:sticky 完全失效。 * - * 解决方案: - * - 每帧滚动时,实时测量侧边栏"自然视觉位置"(无 transform 时的 getBoundingClientRect) - * - 与导航栏上方 88dp (64dp 导航栏 + 24dp 间距) 比较 - * - 若自然位置已被滚出视口上方,用 translateY 推回来 - * - translateY 值不超过 sidebar 容器高度,防止侧边栏脱出 + * 方案:把"测量"和"应用"解耦。 + * - _recomputeStickyGeometry:仅在 resize / stage 尺寸变化时测量一次几何常量 + * (sticky 高度、sidebar 高度、可上推行程 travelLayout)。 + * - _applySidebarSticky:每帧 scroll 直接读 scrollTop 算 translateY, + * 不再调用 getBoundingClientRect,避免每帧 reflow 与子像素取整抖动。 * * 坐标系: * - getBoundingClientRect → 视觉像素(scale 后) @@ -1294,36 +1306,49 @@ export default { return val > 0 ? val : (window.innerWidth >= 768 ? window.innerWidth / 1440 : 1); }, /** - * 测量 sticky 元素当前的自然视觉 top(清除 transform 后测量,再恢复)。 - * 返回 { naturalVisualTop, stickyHeight, sidebarHeight } 三个视觉像素值。 - * 若元素不存在或 scale 无效则返回 null。 + * 测量侧边栏的"几何常量"。 + * + * 设计原则:**不在每帧滚动时测量** DOM 位置,那样会反复触发 reflow, + * 且浮点坐标的子像素精度在 `translateY` 像素取整时会引起视觉抖动。 + * + * 这里只测量**滚动无关的尺寸/偏移**: + * - sticky 元素视觉高度(布局不变则不变) + * - sidebar 容器视觉高度(异步数据填充时才会变) + * - sticky 顶部到 sidebar 顶部的"内边距"偏移(用于算 maxOffset) + * + * 滚动位置由 scrollTop 实时推算,见 _applySidebarSticky。 */ - _measureStickyNatural() { + _measureStickyGeometry() { const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); const sidebarEl = stickyEl?.parentElement; - const scrollRoot = this.getScrollRoot(); + if (!stickyEl || !sidebarEl) return null; + const scale = this._readScale(); - if (!stickyEl || !sidebarEl || !scrollRoot || scale <= 0) return null; + if (scale <= 0) return null; - // 保存当前 transform,清除后测量自然位置 - const saved = stickyEl.style.transform; + // 临时清掉内联 transform,避免上一次的 translateY 污染测量 + const savedTransform = stickyEl.style.transform; stickyEl.style.transform = ''; - const rect = stickyEl.getBoundingClientRect(); - const rootRect = scrollRoot.getBoundingClientRect(); + const stickyRect = stickyEl.getBoundingClientRect(); const sidebarRect = sidebarEl.getBoundingClientRect(); + stickyEl.style.transform = savedTransform; - // 恢复 transform - stickyEl.style.transform = saved; + const stickyHeight = stickyRect.height; + const sidebarHeight = sidebarRect.height; + // sticky 顶部到 sidebar 顶部的距离(视觉像素), + // 与 scrollTop 无关 —— 只取决于 sidebar 内部 padding 与子元素布局 + const innerOffsetVisual = stickyRect.top - sidebarRect.top; + + // 可上推的视觉像素余量:sidebar 底部不能被 sticky 底部超出 + const travelVisual = Math.max(0, sidebarHeight - stickyHeight - innerOffsetVisual); + // 转为布局像素(translateY 在 scale 容器内以布局像素为单位) + const travelLayout = travelVisual / scale; return { - // 自然视觉 top(相对于滚动容器 visual top) - naturalVisualTop: rect.top - rootRect.top, - // sticky 元素视觉高度 - stickyHeight: rect.height, - // 父容器视觉高度 - sidebarHeight: sidebarRect.height, - // 父容器视觉 top(相对于滚动容器 visual top) - sidebarVisualTop: sidebarRect.top - rootRect.top, + stickyHeight, + sidebarHeight, + innerOffsetVisual, + travelLayout, }; }, initSidebarSticky() { @@ -1338,7 +1363,7 @@ export default { if (this._sidebarStickyRaf) return; this._sidebarStickyRaf = requestAnimationFrame(() => { this._sidebarStickyRaf = null; - this.updateSidebarSticky(); + this._applySidebarSticky(); }); }; @@ -1349,7 +1374,8 @@ export default { this._onSidebarResize = () => { if (this._sidebarResizeTimer) clearTimeout(this._sidebarResizeTimer); this._sidebarResizeTimer = setTimeout(() => { - this.updateSidebarSticky(); + this._recomputeStickyGeometry(); + this._applySidebarSticky(); }, 60); }; window.addEventListener('resize', this._onSidebarResize); @@ -1360,13 +1386,16 @@ export default { this._sidebarStageObserver = new ResizeObserver(() => { if (this._sidebarStageTimer) clearTimeout(this._sidebarStageTimer); this._sidebarStageTimer = setTimeout(() => { - this.updateSidebarSticky(); + this._recomputeStickyGeometry(); + this._applySidebarSticky(); }, 80); }); this._sidebarStageObserver.observe(stage); } - this.$nextTick(() => this.updateSidebarSticky()); + // 初始化几何 + 首次应用 + this._recomputeStickyGeometry(); + this.$nextTick(() => this._applySidebarSticky()); }, destroySidebarSticky() { if (this._sidebarStickyScrollEl && this._onSidebarStickyScroll) { @@ -1394,51 +1423,63 @@ export default { clearTimeout(this._sidebarStageTimer); this._sidebarStageTimer = null; } + this._stickyGeometry = null; + this._lastStickyOffset = undefined; const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); if (stickyEl) stickyEl.style.transform = ''; }, - updateSidebarSticky() { - const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); - if (!stickyEl) return; - - const info = this._measureStickyNatural(); + /** + * 缓存侧边栏的"几何常量"。仅在布局可能变化时调用 + * (resize、stage 尺寸变化、初始化),滚动时不再测量。 + */ + _recomputeStickyGeometry() { + const info = this._measureStickyGeometry(); if (!info) return; - - const scale = this._readScale(); - // CSS position:sticky 的 top:24px → 相对于滚动容器顶部 24dp - // naturalVisualTop 也是相对于滚动容器顶部,所以用 24 直接对比 - const DESIRED_DP = 24; - const desiredVisualTop = DESIRED_DP * scale; - - const naturalVisualTop = info.naturalVisualTop; - - // 自然位置已在目标线之下 → 不需要补偿 - if (naturalVisualTop >= desiredVisualTop) { - stickyEl.style.transform = ''; - return; + this._stickyGeometry = info; + }, + /** + * 把当前 scrollTop 映射到 translateY,直接套用缓存的几何常量。 + * 关键:不调用 getBoundingClientRect,避免每帧 reflow + 子像素取整抖动。 + */ + _applySidebarSticky() { + const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); + const scrollRoot = this._sidebarStickyScrollEl; + if (!stickyEl || !scrollRoot) return; + if (!this._stickyGeometry) { + this._recomputeStickyGeometry(); + if (!this._stickyGeometry) return; } - // 需要推回去的视觉像素距离 - const visualOffset = desiredVisualTop - naturalVisualTop; - // 转为布局像素(translateY 在 scale 容器内以布局像素为单位) - const layoutOffset = visualOffset / scale; + // 当 scrollTop 从 0 增加到 X 时,sidebar 在视口里向上平移了 X 视觉像素。 + // sticky 想保持"看起来固定",translateY(布局像素) = scrollTop / scale。 + // 但需限制在 [0, travelLayout] 区间,防止 sticky 底部超出 sidebar 底部。 + // DESIRED_DP 视觉偏移 (24dp) 实际由 sidebar 的 padding-top 自然实现, + // 这里不主动补偿(因为我们没有把 sticky 钉到 24dp 处的硬需求; + // 原设计的目标 24dp 是设计稿装饰间距,sticky 顶部到达滚动容器顶部即可)。 + const scale = this._readScale(); + if (scale <= 0) return; - // 不能推出父容器底部 - // sticky 元素在 sidebar 内部有 padding 偏移,需要核算 sidebar 顶部到 sticky 顶部 - // 的真实可用距离,而不能简单地 sidebarHeight - stickyHeight - const sidebarBottom = info.sidebarVisualTop + info.sidebarHeight; - const stickyBottom = info.naturalVisualTop + info.stickyHeight; - // 允许的视觉余量:sidebar 底部 - sticky 自然底部 - const availableVisual = Math.max(0, sidebarBottom - stickyBottom); - // 由于 naturalVisualTop 已经包含了 padding 偏移,所以 availableVisual - // 就是 sticky 底部到 sidebar 底部的真实视觉距离 - const maxLayoutOffset = availableVisual / scale; - const clamped = Math.min(layoutOffset, maxLayoutOffset); + const scrollTop = scrollRoot.scrollTop; + // 把滚动距离转为布局像素的 translateY + let layoutOffset = scrollTop / scale; + // 上限:不让 sticky 底部超出 sidebar 底部 + const maxOffset = this._stickyGeometry.travelLayout; + if (layoutOffset > maxOffset) layoutOffset = maxOffset; + if (layoutOffset < 0) layoutOffset = 0; - stickyEl.style.transform = clamped > 0 - ? `translateY(${clamped}px)` - : ''; + // 像素级死区:0.4 像素以内的微小变化不写 transform,避免浮点抖动 + if (this._lastStickyOffset !== undefined) { + if (Math.abs(layoutOffset - this._lastStickyOffset) < 0.4) return; + } + + this._lastStickyOffset = layoutOffset; + if (layoutOffset > 0) { + // 用 translate3d 强制 GPU 合成层,顺便保留子像素精度 + stickyEl.style.transform = `translate3d(0, ${layoutOffset}px, 0)`; + } else { + stickyEl.style.transform = ''; + } }, handleCardClick(card) { // 优先使用后端返回的 wzLj,否则用兜底数据的 lj @@ -1577,10 +1618,14 @@ html.portal-figma-scale-active .gxnlpt-page { box-sizing: border-box; } -/* 分类 + 收录/收藏:滚动时钉在视口 */ +/* 分类 + 收录/收藏:滚动时由 JS 模拟 sticky 钉在视口。 + 不能用 CSS position:sticky:外层 .portal-figma-scale-stage 上的 + transform:scale() 会创建新的 containing block,导致原生 sticky 失效, + 且 JS 模拟与 CSS sticky 同时启用时会互相冲突引起上下抖动。 + 故此处关闭 CSS sticky,完全交给 _applySidebarSticky() 通过 transform 实现。 */ .gxnlpt-sidebar-sticky { - position: sticky; - top: @gxnlpt-sidebar-sticky-top; + position: relative; + top: 0; z-index: 20; flex: 0 0 auto; display: flex; @@ -2303,7 +2348,7 @@ html.portal-figma-scale-active .gxnlpt-page { } .gxnlpt-layout--stacked .gxnlpt-sidebar-sticky { - position: sticky; + position: relative; top: 0; z-index: 30; max-height: none; @@ -2326,7 +2371,7 @@ html.portal-figma-scale-active .gxnlpt-page { } .gxnlpt-sidebar-sticky { - position: sticky; + position: relative; top: 0; z-index: 30; background: @gxnlpt-page-bg; diff --git a/txw-mhzc-web/src/pages/index/views/home2/index.vue b/txw-mhzc-web/src/pages/index/views/home2/index.vue index b56a108..7b043ff 100644 --- a/txw-mhzc-web/src/pages/index/views/home2/index.vue +++ b/txw-mhzc-web/src/pages/index/views/home2/index.vue @@ -5,7 +5,7 @@
-
+
@@ -3227,9 +3227,9 @@ export default { margin-top: var(--page-offset-top); } - /* 顶部区域 */ + /* 顶部区域:移动端由 :style 绑定 minHeight(topBannerHeight) 撑满一屏; + 这里不再强制 min-height:auto,避免覆盖内联高度 */ .top-box { - min-height: auto; overflow: visible; } diff --git a/txw-mhzc-web/src/pages/index/views/hydt/index.vue b/txw-mhzc-web/src/pages/index/views/hydt/index.vue index f743c8a..88efb16 100644 --- a/txw-mhzc-web/src/pages/index/views/hydt/index.vue +++ b/txw-mhzc-web/src/pages/index/views/hydt/index.vue @@ -140,14 +140,22 @@ export default { activeTab() { this.page.pageNo = 1; }, + // 兜底:移动端断点切换 / Figma 缩放等导致 .content-wrap 重新挂载时,重绑滚动 + '$route.path'() { + this.unbindScrollListener(); + this.$nextTick(() => this.bindScrollListener()); + }, }, mounted() { this.syncTabFromRoute(this.$route.query.type); this.fetchNewsData(); - window.addEventListener('scroll', this.onScroll, { passive: true }); + // 注意:滚动容器是 main.vue 中的 .content-wrap,window 不会冒泡到滚动事件 + this.$nextTick(() => { + this.bindScrollListener(); + }); }, beforeDestroy() { - window.removeEventListener('scroll', this.onScroll); + this.unbindScrollListener(); }, methods: { goHomeToNews() { @@ -162,17 +170,11 @@ export default { // 同 tab 直接 no-op(避免无意义的状态抖动) if (this.activeTab === index) return; this.activeTab = index; - this.bannerCollapsed = true; - const type = this.newsTabs[index]?.type; - if (this.$route.query.type !== type) { - this.$router.replace({ path: '/hydt', query: { type } }); - } - // 平滑滚动到列表区,让用户看到收起后的内容 + // 收起 banner:列表区本来就在 banner 紧邻下方,收起后用户自然能看到更多列表。 + // 注意:这里不再调用 $router.replace,否则会触发 main.vue 中 $route.watch + // 把 .content-wrap.scrollTop 瞬间置 0,导致用户感觉到"弹一下"。 this.$nextTick(() => { - const body = this.$el.querySelector('.hydt-body'); - if (body) { - body.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } + this.bannerCollapsed = true; }); }, onPageChange() { @@ -198,6 +200,28 @@ export default { if (portalRoot) return portalRoot; return window; }, + bindScrollListener() { + // 必须把滚动监听挂到真正的滚动容器 .content-wrap 上; + // 之前挂在 window 上,window 不会冒泡来自内部容器的滚动事件,导致 onScroll 永不触发 + this.unbindScrollListener(); + const root = this.getScrollRoot(); + if (!root || root === window) { + this._scrollTarget = window; + window.addEventListener('scroll', this.onScroll, { passive: true }); + } else { + this._scrollTarget = root; + root.addEventListener('scroll', this.onScroll, { passive: true }); + } + }, + unbindScrollListener() { + if (!this._scrollTarget) { + // 兜底:组件销毁时即使 _scrollTarget 未初始化也要清掉 window 上的残留监听 + window.removeEventListener('scroll', this.onScroll); + return; + } + this._scrollTarget.removeEventListener('scroll', this.onScroll); + this._scrollTarget = null; + }, handleNewsClick(item) { const link = item.yyLj || item.lj; if (link) { diff --git a/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java b/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java index 845d955..fa74378 100644 --- a/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java +++ b/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java @@ -107,7 +107,7 @@ public class SearchServiceImpl implements SearchService { @Override public List getHotSearchKeywords() { - return Arrays.asList("碳达峰", "碳核查", "ESG", "碳资产管理", "ISO 14067"); + return Arrays.asList("碳达峰", "ESG", "碳资产管理", "ISO 14067"); } @Override