feat:修复部分问题

This commit is contained in:
liulong 2026-06-05 12:54:11 +08:00
parent e724bc3326
commit 68bc750604
44 changed files with 962 additions and 126 deletions

64
tmp-active.mjs Normal file
View File

@ -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);
});

67
tmp-capability-debug.mjs Normal file
View File

@ -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);
});

93
tmp-capability-test.mjs Normal file
View File

@ -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);
});

70
tmp-debug2.mjs Normal file
View File

@ -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);
});

BIN
tmp-direct-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-direct-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

41
tmp-elementfrom.mjs Normal file
View File

@ -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);
});

BIN
tmp-final-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-final-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

46
tmp-final.mjs Normal file
View File

@ -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);
});

BIN
tmp-gxnlpt-content-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
tmp-gxnlpt-el.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
tmp-gxnlpt-more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
tmp-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

50
tmp-no-keepalive.mjs Normal file
View File

@ -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);
});

BIN
tmp-pix-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-pix-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-pix-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-pix-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
tmp-pix-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

57
tmp-pixel.mjs Normal file
View File

@ -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);
});

BIN
tmp-snap-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-snap-viewport.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

54
tmp-snap.mjs Normal file
View File

@ -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);
});

42
tmp-snap2.mjs Normal file
View File

@ -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);
});

BIN
tmp-zoom-1-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

BIN
tmp-zoom-2-after-click.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-zoom-3-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

39
tmp-zoom.mjs Normal file
View File

@ -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);
});

75
tmp-zoom2.mjs Normal file
View File

@ -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);
});

BIN
tmp-zoom2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
tmp-zoom3-no-eval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

32
tmp-zoom3.mjs Normal file
View File

@ -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);
});

49
tmp-zoom4.mjs Normal file
View File

@ -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);
});

BIN
tmp-zoom4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -314,7 +314,7 @@ export default [
title: '数据列表', title: '数据列表',
isShowSideBar: false, isShowSideBar: false,
hasHome: true, hasHome: true,
breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/fwscsjlbc' }], breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/tsjsc' }],
disableBack: true, disableBack: true,
}, },
}, },

View File

@ -930,7 +930,7 @@ export default {
.price-value { .price-value {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: #FF4D4F; color: #2E7D32;
} }
.price-unit { .price-unit {

View File

@ -320,7 +320,7 @@ export default {
.breadcrumb-box { .breadcrumb-box {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 83%;
max-width: 1400px; max-width: 1400px;
padding: 20px 20px 0; padding: 20px 20px 0;
margin: 0 auto; margin: 0 auto;
@ -354,7 +354,7 @@ export default {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 81%;
max-width: 1400px; max-width: 1400px;
padding: 24px; padding: 24px;
margin: 16px auto 0; margin: 16px auto 0;

View File

@ -36,11 +36,6 @@
<div class="card-title-text"> <div class="card-title-text">
<div class="card-title-row"> <div class="card-title-row">
<div class="card-title-main">{{ card.bt1 }}</div> <div class="card-title-main">{{ card.bt1 }}</div>
<!-- 收藏按钮 -->
<div class="card-collect" @click="handleCollect(card)">
<img v-if="card.scbz === 'Y'" src="../../assets/fwsc/ysc.svg" />
<img v-else src="../../assets/fwsc/wsc.svg" />
</div>
</div> </div>
<div class="card-title-sub"> <div class="card-title-sub">
<div class="card-company"> <div class="card-company">
@ -71,8 +66,8 @@
<div class="card-price-info"> <div class="card-price-info">
<span class="price-value">免费</span> <span class="price-value">免费</span>
</div> </div>
<div class="card-actions"> <div class="card-actions" @click="goToDataList(card.id)">
<span @click="handleContact(card)">立即咨询</span> <span>查看数据库</span>
</div> </div>
</div> </div>
</div> </div>
@ -293,7 +288,6 @@ export default {
fwnr: item.sjms, fwnr: item.sjms,
fwfw: item.fwfw || '', fwfw: item.fwfw || '',
fwlxbqList: item.sjlxMc ? [item.sjlxMc] : [], fwlxbqList: item.sjlxMc ? [item.sjlxMc] : [],
scbz: item.scbz || 'N',
lxr: item.lxr || '', lxr: item.lxr || '',
lxdh: item.lxdh || '', lxdh: item.lxdh || '',
email: item.email || '', email: item.email || '',
@ -362,15 +356,9 @@ export default {
this.rzVisible = true; this.rzVisible = true;
} }
}, },
// / //
async handleCollect(card) { goToDataList(id) {
try { this.$router.push({ path: '/tsjlbc', query: { id } });
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);
}
}, },
// //
handleContact(card) { handleContact(card) {
@ -476,7 +464,7 @@ export default {
color: #003B1A; color: #003B1A;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border-radius: 32px; border-radius: 6px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {

View File

@ -505,7 +505,7 @@ export default {
color: #003B1A; color: #003B1A;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border-radius: 32px; border-radius: 6px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {

View File

@ -672,12 +672,15 @@ export default {
}, },
activated() { activated() {
this.syncStackedNavLayout(); this.syncStackedNavLayout();
if (this.contentView === 'list' && !this.stackedNavLayout) { if (this.contentView !== 'list') return;
this.$nextTick(() => { this.$nextTick(() => {
// anchor,(keep-alive )
if (this.navigateToRouteAnchor()) return;
if (!this.stackedNavLayout) {
this.initScrollSpy(); this.initScrollSpy();
this.initSidebarSticky(); this.initSidebarSticky();
});
} }
});
}, },
beforeDestroy() { beforeDestroy() {
this.unbindStackedNavMedia(); this.unbindStackedNavMedia();
@ -702,23 +705,32 @@ export default {
this.applyDemoFallback(); this.applyDemoFallback();
} }
this.$nextTick(() => { this.$nextTick(() => {
const anchor = this.$route.query.anchor || this.$route.params.anchor; if (this.navigateToRouteAnchor()) return;
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;
}
this.initScrollSpy(); this.initScrollSpy();
this.initSidebarSticky(); 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) { getIconUrl(iconName) {
return SIDE_ICON_GRAY[iconName] || SIDE_ICON_GRAY['thspt.svg']; return SIDE_ICON_GRAY[iconName] || SIDE_ICON_GRAY['thspt.svg'];
}, },
@ -1276,11 +1288,11 @@ export default {
* 原因.portal-figma-scale-stage transform:scale() * 原因.portal-figma-scale-stage transform:scale()
* 会创建新的 containing block导致 CSS position:sticky 完全失效 * 会创建新的 containing block导致 CSS position:sticky 完全失效
* *
* 解决方案 * 方案:"测量""应用"解耦
* - 每帧滚动时实时测量侧边栏"自然视觉位置" transform 时的 getBoundingClientRect * - _recomputeStickyGeometry:仅在 resize / stage 尺寸变化时测量一次几何常量
* - 与导航栏上方 88dp (64dp 导航栏 + 24dp 间距) 比较 * (sticky 高度sidebar 高度可上推行程 travelLayout)
* - 若自然位置已被滚出视口上方 translateY 推回来 * - _applySidebarSticky:每帧 scroll 直接读 scrollTop translateY,
* - translateY 值不超过 sidebar 容器高度防止侧边栏脱出 * 不再调用 getBoundingClientRect,避免每帧 reflow 与子像素取整抖动
* *
* 坐标系 * 坐标系
* - getBoundingClientRect 视觉像素scale * - getBoundingClientRect 视觉像素scale
@ -1294,36 +1306,49 @@ export default {
return val > 0 ? val : (window.innerWidth >= 768 ? window.innerWidth / 1440 : 1); 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 stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
const sidebarEl = stickyEl?.parentElement; const sidebarEl = stickyEl?.parentElement;
const scrollRoot = this.getScrollRoot(); if (!stickyEl || !sidebarEl) return null;
const scale = this._readScale(); const scale = this._readScale();
if (!stickyEl || !sidebarEl || !scrollRoot || scale <= 0) return null; if (scale <= 0) return null;
// transform // transform, translateY
const saved = stickyEl.style.transform; const savedTransform = stickyEl.style.transform;
stickyEl.style.transform = ''; stickyEl.style.transform = '';
const rect = stickyEl.getBoundingClientRect(); const stickyRect = stickyEl.getBoundingClientRect();
const rootRect = scrollRoot.getBoundingClientRect();
const sidebarRect = sidebarEl.getBoundingClientRect(); const sidebarRect = sidebarEl.getBoundingClientRect();
stickyEl.style.transform = savedTransform;
// transform const stickyHeight = stickyRect.height;
stickyEl.style.transform = saved; 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 { return {
// top visual top stickyHeight,
naturalVisualTop: rect.top - rootRect.top, sidebarHeight,
// sticky innerOffsetVisual,
stickyHeight: rect.height, travelLayout,
//
sidebarHeight: sidebarRect.height,
// top visual top
sidebarVisualTop: sidebarRect.top - rootRect.top,
}; };
}, },
initSidebarSticky() { initSidebarSticky() {
@ -1338,7 +1363,7 @@ export default {
if (this._sidebarStickyRaf) return; if (this._sidebarStickyRaf) return;
this._sidebarStickyRaf = requestAnimationFrame(() => { this._sidebarStickyRaf = requestAnimationFrame(() => {
this._sidebarStickyRaf = null; this._sidebarStickyRaf = null;
this.updateSidebarSticky(); this._applySidebarSticky();
}); });
}; };
@ -1349,7 +1374,8 @@ export default {
this._onSidebarResize = () => { this._onSidebarResize = () => {
if (this._sidebarResizeTimer) clearTimeout(this._sidebarResizeTimer); if (this._sidebarResizeTimer) clearTimeout(this._sidebarResizeTimer);
this._sidebarResizeTimer = setTimeout(() => { this._sidebarResizeTimer = setTimeout(() => {
this.updateSidebarSticky(); this._recomputeStickyGeometry();
this._applySidebarSticky();
}, 60); }, 60);
}; };
window.addEventListener('resize', this._onSidebarResize); window.addEventListener('resize', this._onSidebarResize);
@ -1360,13 +1386,16 @@ export default {
this._sidebarStageObserver = new ResizeObserver(() => { this._sidebarStageObserver = new ResizeObserver(() => {
if (this._sidebarStageTimer) clearTimeout(this._sidebarStageTimer); if (this._sidebarStageTimer) clearTimeout(this._sidebarStageTimer);
this._sidebarStageTimer = setTimeout(() => { this._sidebarStageTimer = setTimeout(() => {
this.updateSidebarSticky(); this._recomputeStickyGeometry();
this._applySidebarSticky();
}, 80); }, 80);
}); });
this._sidebarStageObserver.observe(stage); this._sidebarStageObserver.observe(stage);
} }
this.$nextTick(() => this.updateSidebarSticky()); // +
this._recomputeStickyGeometry();
this.$nextTick(() => this._applySidebarSticky());
}, },
destroySidebarSticky() { destroySidebarSticky() {
if (this._sidebarStickyScrollEl && this._onSidebarStickyScroll) { if (this._sidebarStickyScrollEl && this._onSidebarStickyScroll) {
@ -1394,51 +1423,63 @@ export default {
clearTimeout(this._sidebarStageTimer); clearTimeout(this._sidebarStageTimer);
this._sidebarStageTimer = null; this._sidebarStageTimer = null;
} }
this._stickyGeometry = null;
this._lastStickyOffset = undefined;
const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
if (stickyEl) stickyEl.style.transform = ''; if (stickyEl) stickyEl.style.transform = '';
}, },
updateSidebarSticky() { /**
const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky'); * 缓存侧边栏的"几何常量"仅在布局可能变化时调用
if (!stickyEl) return; * (resizestage 尺寸变化初始化),滚动时不再测量
*/
const info = this._measureStickyNatural(); _recomputeStickyGeometry() {
const info = this._measureStickyGeometry();
if (!info) return; if (!info) return;
this._stickyGeometry = info;
const scale = this._readScale(); },
// CSS position:sticky top:24px 24dp /**
// naturalVisualTop 24 * 把当前 scrollTop 映射到 translateY,直接套用缓存的几何常量
const DESIRED_DP = 24; * 关键:不调用 getBoundingClientRect,避免每帧 reflow + 子像素取整抖动
const desiredVisualTop = DESIRED_DP * scale; */
_applySidebarSticky() {
const naturalVisualTop = info.naturalVisualTop; const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
const scrollRoot = this._sidebarStickyScrollEl;
// 线 if (!stickyEl || !scrollRoot) return;
if (naturalVisualTop >= desiredVisualTop) { if (!this._stickyGeometry) {
stickyEl.style.transform = ''; this._recomputeStickyGeometry();
return; if (!this._stickyGeometry) return;
} }
// // scrollTop 0 X ,sidebar X
const visualOffset = desiredVisualTop - naturalVisualTop; // sticky "",translateY() = scrollTop / scale
// translateY scale // [0, travelLayout] , sticky sidebar
const layoutOffset = visualOffset / scale; // DESIRED_DP (24dp) sidebar padding-top ,
// ( sticky 24dp ;
// 24dp 稿,sticky )
const scale = this._readScale();
if (scale <= 0) return;
// const scrollTop = scrollRoot.scrollTop;
// sticky sidebar padding sidebar sticky // translateY
// sidebarHeight - stickyHeight let layoutOffset = scrollTop / scale;
const sidebarBottom = info.sidebarVisualTop + info.sidebarHeight; // : sticky sidebar
const stickyBottom = info.naturalVisualTop + info.stickyHeight; const maxOffset = this._stickyGeometry.travelLayout;
// sidebar - sticky if (layoutOffset > maxOffset) layoutOffset = maxOffset;
const availableVisual = Math.max(0, sidebarBottom - stickyBottom); if (layoutOffset < 0) layoutOffset = 0;
// naturalVisualTop padding availableVisual
// sticky sidebar
const maxLayoutOffset = availableVisual / scale;
const clamped = Math.min(layoutOffset, maxLayoutOffset);
stickyEl.style.transform = clamped > 0 // :0.4 transform,
? `translateY(${clamped}px)` 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) { handleCardClick(card) {
// 使 wzLj lj // 使 wzLj lj
@ -1577,10 +1618,14 @@ html.portal-figma-scale-active .gxnlpt-page {
box-sizing: border-box; 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 { .gxnlpt-sidebar-sticky {
position: sticky; position: relative;
top: @gxnlpt-sidebar-sticky-top; top: 0;
z-index: 20; z-index: 20;
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
@ -2303,7 +2348,7 @@ html.portal-figma-scale-active .gxnlpt-page {
} }
.gxnlpt-layout--stacked .gxnlpt-sidebar-sticky { .gxnlpt-layout--stacked .gxnlpt-sidebar-sticky {
position: sticky; position: relative;
top: 0; top: 0;
z-index: 30; z-index: 30;
max-height: none; max-height: none;
@ -2326,7 +2371,7 @@ html.portal-figma-scale-active .gxnlpt-page {
} }
.gxnlpt-sidebar-sticky { .gxnlpt-sidebar-sticky {
position: sticky; position: relative;
top: 0; top: 0;
z-index: 30; z-index: 30;
background: @gxnlpt-page-bg; background: @gxnlpt-page-bg;

View File

@ -5,7 +5,7 @@
<!-- 主页面 --> <!-- 主页面 -->
<div class="container"> <div class="container">
<!-- 顶部背景轮播 --> <!-- 顶部背景轮播 -->
<div class="top-box snap-section" id="section-hero"> <div class="top-box snap-section" id="section-hero" :style="{ minHeight: topBannerHeight + 'px' }">
<t-swiper class="top-banner-swiper" animation="fade" :height="topBannerHeight" :interval="10000" :duration="500" <t-swiper class="top-banner-swiper" animation="fade" :height="topBannerHeight" :interval="10000" :duration="500"
:loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }"> :loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }">
<t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx"> <t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx">
@ -3227,9 +3227,9 @@ export default {
margin-top: var(--page-offset-top); margin-top: var(--page-offset-top);
} }
/* 顶部区域 */ /* :style minHeight(topBannerHeight)
这里不再强制 min-height:auto避免覆盖内联高度 */
.top-box { .top-box {
min-height: auto;
overflow: visible; overflow: visible;
} }

View File

@ -140,14 +140,22 @@ export default {
activeTab() { activeTab() {
this.page.pageNo = 1; this.page.pageNo = 1;
}, },
// / Figma .content-wrap
'$route.path'() {
this.unbindScrollListener();
this.$nextTick(() => this.bindScrollListener());
},
}, },
mounted() { mounted() {
this.syncTabFromRoute(this.$route.query.type); this.syncTabFromRoute(this.$route.query.type);
this.fetchNewsData(); this.fetchNewsData();
window.addEventListener('scroll', this.onScroll, { passive: true }); // main.vue .content-wrapwindow
this.$nextTick(() => {
this.bindScrollListener();
});
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('scroll', this.onScroll); this.unbindScrollListener();
}, },
methods: { methods: {
goHomeToNews() { goHomeToNews() {
@ -162,17 +170,11 @@ export default {
// tab no-op // tab no-op
if (this.activeTab === index) return; if (this.activeTab === index) return;
this.activeTab = index; this.activeTab = index;
this.bannerCollapsed = true; // banner banner
const type = this.newsTabs[index]?.type; // $router.replace main.vue $route.watch
if (this.$route.query.type !== type) { // .content-wrap.scrollTop 0""
this.$router.replace({ path: '/hydt', query: { type } });
}
//
this.$nextTick(() => { this.$nextTick(() => {
const body = this.$el.querySelector('.hydt-body'); this.bannerCollapsed = true;
if (body) {
body.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}); });
}, },
onPageChange() { onPageChange() {
@ -198,6 +200,28 @@ export default {
if (portalRoot) return portalRoot; if (portalRoot) return portalRoot;
return window; 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) { handleNewsClick(item) {
const link = item.yyLj || item.lj; const link = item.yyLj || item.lj;
if (link) { if (link) {

View File

@ -107,7 +107,7 @@ public class SearchServiceImpl implements SearchService {
@Override @Override
public List<String> getHotSearchKeywords() { public List<String> getHotSearchKeywords() {
return Arrays.asList("碳达峰", "碳核查", "ESG", "碳资产管理", "ISO 14067"); return Arrays.asList("碳达峰", "ESG", "碳资产管理", "ISO 14067");
} }
@Override @Override