|
@@ -117,43 +117,54 @@ const out = { tag: TAG, at: new Date().toISOString(), one: null, two: null, thre
|
|
|
await mpage.context().close();
|
|
await mpage.context().close();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* ======================= 二、机会卡描述 ======================= */
|
|
|
|
|
|
|
+/* ======================= 二、报告卡描述(reportCard 的 .lead) =======================
|
|
|
|
|
+ 量 /reports 与 /stages 的每一张 .rc:
|
|
|
|
|
+ - 有无描述(.lead 元素存在且有文字)
|
|
|
|
|
+ - 标题 → 指标区(.m)之间的缝:有描述的卡把这条缝摊给四张卡(flex 平均分),
|
|
|
|
|
+ 无描述的卡应把同一条缝均分到卡内各条缝 —— 不再是 66px 一条空缝
|
|
|
|
|
+ - 内容总高 = .rc 内第一个可见子块顶 → 最后一个子块底
|
|
|
|
|
+ - 最大连续空白 = .rc 内叶子文字矩形按 top 排,相邻两块之间竖直间隙的最大值 */
|
|
|
{
|
|
{
|
|
|
- await gotoView('/pool', 'view-pool');
|
|
|
|
|
- out.two = await page.evaluate(`(async () => {
|
|
|
|
|
- ${DEEP}
|
|
|
|
|
- const v = appR().querySelector('view-pool').shadowRoot;
|
|
|
|
|
- const cards = [...v.querySelectorAll('kf-opportunity-card')];
|
|
|
|
|
- const cardsInfo = cards.map((c) => {
|
|
|
|
|
- const sr = c.shadowRoot, w = sr.querySelector('.wrap'), one = sr.querySelector('.one');
|
|
|
|
|
- const wrap = w.getBoundingClientRect();
|
|
|
|
|
- /* 叶子文字矩形:与 v12「卡内连续空白」同口径 */
|
|
|
|
|
- const leaves = [...sr.querySelectorAll('*')].filter((e) => {
|
|
|
|
|
- if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) return false;
|
|
|
|
|
- const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0;
|
|
|
|
|
- }).map((e) => e.getBoundingClientRect()).sort((a, b) => a.top - b.top || a.left - b.left);
|
|
|
|
|
- let maxGap = 0, gapAt = '';
|
|
|
|
|
- for (let i = 1; i < leaves.length; i++) {
|
|
|
|
|
- if (leaves[i].top < leaves[i - 1].bottom) continue; /* 同一行的兄弟 */
|
|
|
|
|
- const g = Math.round(leaves[i].top - leaves[i - 1].bottom);
|
|
|
|
|
- if (g > maxGap) { maxGap = g; gapAt = Math.round(leaves[i - 1].bottom) + '→' + Math.round(leaves[i].top); }
|
|
|
|
|
- }
|
|
|
|
|
- const nums = sr.querySelector('.nums').getBoundingClientRect();
|
|
|
|
|
- const title = sr.querySelector('.title').getBoundingClientRect();
|
|
|
|
|
- const acts = sr.querySelector('.acts').getBoundingClientRect();
|
|
|
|
|
- const hasOne = !!one && one.getBoundingClientRect().height > 0;
|
|
|
|
|
- /* 内容总高 = 首块顶到末块底(不含卡片内衬) */
|
|
|
|
|
- const first = leaves[0], last = leaves[leaves.length - 1];
|
|
|
|
|
- return { title: c.opp.title.slice(0, 14), desc: hasOne ? one.textContent.trim().slice(0, 10) : '',
|
|
|
|
|
- descLen: hasOne ? one.textContent.trim().length : 0, descH: hasOne ? Math.round(one.getBoundingClientRect().height) : 0,
|
|
|
|
|
- lineH: one ? getComputedStyle(one).lineHeight : null, gapAfterTitle_toNums: Math.round(nums.top - title.bottom),
|
|
|
|
|
- contentH: Math.round(last.bottom - first.top), cardH: Math.round(c.getBoundingClientRect().height),
|
|
|
|
|
- maxGap, gapAt, actsBottom: Math.round(acts.bottom - wrap.top), numsTop: Math.round(nums.top - wrap.top) };
|
|
|
|
|
- });
|
|
|
|
|
- /* 有没有空的描述占位:.one 存在但盒子高度为 0 / 只有 padding 的盒子 / aria-hidden 占位 */
|
|
|
|
|
- const empties = cardsInfo.filter((x) => !x.desc).length;
|
|
|
|
|
- return { n: cardsInfo.length, emptyCount: empties, cards: cardsInfo };
|
|
|
|
|
- })()`);
|
|
|
|
|
|
|
+ out.two = { pages: [] };
|
|
|
|
|
+ for (const [p, name] of [['/reports', 'view-reports'], ['/stages', 'view-stages']]) {
|
|
|
|
|
+ await gotoView(p, name);
|
|
|
|
|
+ const r = await page.evaluate(`(async () => {
|
|
|
|
|
+ ${DEEP}
|
|
|
|
|
+ const root = appR().querySelector('${name}').shadowRoot;
|
|
|
|
|
+ const cards = [...root.querySelectorAll(".rc")].filter((c) => c.getBoundingClientRect().height > 0 && c.getBoundingClientRect().width > 0);
|
|
|
|
|
+ const cardsInfo = cards.map((c) => {
|
|
|
|
|
+ const lead = c.querySelector('.lead');
|
|
|
|
|
+ const tk = c.querySelector('h3').getBoundingClientRect();
|
|
|
|
|
+ const m = c.querySelector('.m').getBoundingClientRect();
|
|
|
|
|
+ const kids = [...c.children].filter((e) => e.getBoundingClientRect().height > 0);
|
|
|
|
|
+ const first = kids[0].getBoundingClientRect(), last = kids[kids.length - 1].getBoundingClientRect();
|
|
|
|
|
+ /* 叶子文字块:先取最内层的文字元素(祖先容器不重复计,否则父盒扣掉 padding 后会造出假空白)。
|
|
|
|
|
+ display:contents(.src 在手机档里的 .l1/.l2)没有盒,跳过。 */
|
|
|
|
|
+ const textAll = [...c.querySelectorAll('*')].filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()));
|
|
|
|
|
+ const inner = textAll.filter((e) => !textAll.some((o) => o !== e && e.contains(o)));
|
|
|
|
|
+ const leaves = inner.map((e) => e.getBoundingClientRect()).filter((r) => r.width > 0 && r.height > 0).sort((a, b) => a.top - b.top || a.left - b.left);
|
|
|
|
|
+ let maxGap = 0, gapAt = '';
|
|
|
|
|
+ for (let i = 1; i < leaves.length; i++) {
|
|
|
|
|
+ if (leaves[i].top < leaves[i - 1].bottom - 1) continue;
|
|
|
|
|
+ /* 水平不相交(并排的列)不算卡内纵向空白 */
|
|
|
|
|
+ if (leaves[i].left >= leaves[i - 1].right - 1 || leaves[i - 1].left >= leaves[i].right - 1) continue;
|
|
|
|
|
+ const g = Math.round(leaves[i].top - leaves[i - 1].bottom);
|
|
|
|
|
+ if (g > maxGap) { maxGap = g; gapAt = Math.round(leaves[i - 1].bottom) + '→' + Math.round(leaves[i].top); }
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 卡内每条缝(相邻子块之间),以及每块子块的 stretch 情况 */
|
|
|
|
|
+ const gaps = [];
|
|
|
|
|
+ for (let i = 1; i < kids.length; i++) gaps.push(Math.round(kids[i].getBoundingClientRect().top - kids[i - 1].getBoundingClientRect().bottom));
|
|
|
|
|
+ return { title: (c.querySelector('h3').textContent || '').trim().slice(0, 14),
|
|
|
|
|
+ lead: lead ? lead.textContent.trim().slice(0, 12) : null, leadTxt: lead ? lead.textContent.trim() : '',
|
|
|
|
|
+ gapTitleToM: Math.round(m.top - tk.bottom), contentH: Math.round(last.bottom - first.top),
|
|
|
|
|
+ cardH: Math.round(c.closest('kf-glass-card').getBoundingClientRect().height),
|
|
|
|
|
+ maxGap, gapAt, rowIdx: Math.round(c.getBoundingClientRect().top) };
|
|
|
|
|
+ });
|
|
|
|
|
+ return { n: cardsInfo.length, cards: cardsInfo };
|
|
|
|
|
+ })()`);
|
|
|
|
|
+ /* 同排(top 相同)卡片的等高由 v12 脚本管;这里只记 */
|
|
|
|
|
+ out.two.pages.push({ page: p, ...r });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* ======================= 三、页脚类名命中 ======================= */
|
|
/* ======================= 三、页脚类名命中 ======================= */
|
|
@@ -263,19 +274,26 @@ console.log(`\n===== v13 三项验收 · tag=${TAG} =====`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 二 */
|
|
/* 二 */
|
|
|
|
|
+function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
|
|
|
{
|
|
{
|
|
|
- const t = out.two;
|
|
|
|
|
- const withD = t.cards.filter((c) => c.descLen), without = t.cards.filter((c) => !c.descLen);
|
|
|
|
|
- const maxGap = Math.max(...t.cards.map((c) => c.maxGap));
|
|
|
|
|
- const dh = withD.length && without.length ? Math.abs(avg(withD, 'contentH') - avg(without, 'contentH')) : -1;
|
|
|
|
|
- const heights = new Set(t.cards.map((c) => c.cardH));
|
|
|
|
|
- check('二 空描述:无描述卡不渲染描述区 + 内容高差 ≤ 40px + 卡内空白 ≤ 24px',
|
|
|
|
|
- without.length === 0 || (heights.size === 1 && without.every((c) => c.descH === 0 && c.gapAfterTitle_toNums === withD[0]?.gapAfterTitle_toNums && c.lineH == null) && dh <= 40 && maxGap <= 24),
|
|
|
|
|
- `共 ${t.n} 张,无描述 ${t.emptyCount} 张;有描述卡内容高 ${withD.map((c) => c.contentH).join('/')},无描述卡内容高 ${without.map((c) => c.contentH).join('/')},差 ${dh.toFixed(1)}px` +
|
|
|
|
|
- `;卡片高去重 [${[...heights].join(',')}];最大连续空白 ${maxGap}px(${t.cards.find((c) => c.maxGap === maxGap)?.title}:${t.cards.find((c) => c.maxGap === maxGap)?.gapAt})` +
|
|
|
|
|
- `;无描述卡:.one 存在=${without.some((c) => c.lineH != null)}(false=不渲染)`);
|
|
|
|
|
|
|
+ const parts = out.two.pages.map((pg) => {
|
|
|
|
|
+ const withD = pg.cards.filter((c) => c.lead), without = pg.cards.filter((c) => !c.lead);
|
|
|
|
|
+ const dh = withD.length && without.length ? Math.abs(avg(withD, 'contentH') - avg(without, 'contentH')) : null;
|
|
|
|
|
+ const maxGap = Math.max(...pg.cards.map((c) => c.maxGap));
|
|
|
|
|
+ const gapW = withD.length ? avg(withD, 'gapTitleToM') : 0;
|
|
|
|
|
+ const gapN = without.length ? avg(without, 'gapTitleToM') : 0;
|
|
|
|
|
+ const sameRow = new Set(pg.cards.filter((c) => !c.lead).map((c) => c.cardH));
|
|
|
|
|
+ return { pg, withD, without, dh, maxGap, gapW, gapN, sameRow,
|
|
|
|
|
+ ok: !without.length || (dh !== null && dh <= 40 && maxGap <= 24 && gapN < gapW * 0.8 && sameRow.size === 1) };
|
|
|
|
|
+ });
|
|
|
|
|
+ check('二 空描述卡:不渲染描述区 + 卡内重新均分 + 内容高差 ≤ 40px + 卡内空白 ≤ 24px',
|
|
|
|
|
+ parts.every((x) => x.ok),
|
|
|
|
|
+ parts.map(({ pg, withD, without, dh, maxGap, gapW, gapN, sameRow }) =>
|
|
|
|
|
+ `${pg.page}:共 ${pg.n} 张,无描述 ${without.length} 张(${without.map((c) => c.title).join('、') || '—'})` +
|
|
|
|
|
+ `;有描述卡 gap(标题→指标) 均值 ${gapW.toFixed(1)}px / 无描述卡 ${gapN.toFixed(1)}px` +
|
|
|
|
|
+ `;内容高 有描述 [${withD.map((c) => c.contentH).join(',')}] 无描述 [${without.map((c) => c.contentH).join(',')}] 差 ${dh == null ? '—' : dh.toFixed(1) + 'px'}` +
|
|
|
|
|
+ `;无描述卡高去重 [${[...sameRow].join(',')}];最大连续空白 ${maxGap}px`).join('\n '));
|
|
|
}
|
|
}
|
|
|
-function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
|
|
|
|
|
|
|
|
|
|
/* 三 */
|
|
/* 三 */
|
|
|
{
|
|
{
|