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: '数据列表',
isShowSideBar: false,
hasHome: true,
breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/fwscsjlbc' }],
breadCrumbs: [{ title: '首页', to: '/home' }, { title: '服务中心', to: '' }, { title: '碳数据市场', to: '/tsjsc' }],
disableBack: true,
},
},

View File

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

View File

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

View File

@ -36,11 +36,6 @@
<div class="card-title-text">
<div class="card-title-row">
<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 class="card-title-sub">
<div class="card-company">
@ -71,8 +66,8 @@
<div class="card-price-info">
<span class="price-value">免费</span>
</div>
<div class="card-actions">
<span @click="handleContact(card)">立即咨询</span>
<div class="card-actions" @click="goToDataList(card.id)">
<span>查看数据库</span>
</div>
</div>
</div>
@ -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 {

View File

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

View File

@ -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();
/**
* 缓存侧边栏的"几何常量"仅在布局可能变化时调用
* (resizestage 尺寸变化初始化),滚动时不再测量
*/
_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;

View File

@ -5,7 +5,7 @@
<!-- 主页面 -->
<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"
:loop="true" :autoplay="true" theme="dark" :navigation="{ showSlideBtn: 'never' }">
<t-swiper-item v-for="(video, idx) in topBannerVideos" :key="idx">
@ -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;
}

View File

@ -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-wrapwindow
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) {

View File

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