Преглед изворни кода

fix(v13-一): 排序段选中改浅底主文字色、整块 1px 分隔、字级一致;新增 verify-visual-v13 脚本

FmodeCode пре 5 дана
родитељ
комит
3c27490782
2 измењених фајлова са 332 додато и 8 уклоњено
  1. 315 0
      scripts/verify-visual-v13.mjs
  2. 17 8
      src/pages/view-pool.js

+ 315 - 0
scripts/verify-visual-v13.mjs

@@ -0,0 +1,315 @@
+#!/usr/bin/env node
+/**
+ * scripts/verify-visual-v13.mjs —— v13 收尾三项验收(每项打印实测原值,不看「通过」两个字)。
+ *
+ *   node scripts/verify-visual-v13.mjs --tag before   只打印、不判分(改前基线)
+ *   node scripts/verify-visual-v13.mjs                判分:任一项不达标 exit 1
+ *   --json out.json                                   结果落盘(docs 里的数字从这里抄)
+ *
+ * 量什么:
+ *   一、排序段(/pool .seg,桌面;手机上打开 .pbar-more 里的 .pbar-sorts)
+ *       - 选中项 computed background 与文字色、aria-pressed 计数
+ *       - 选中 / 未选中 font-size、line-height、按钮框高
+ *       - 外层:border-radius / border-width;按钮:每项各自 borderRadius = 0、相邻之间 1px 分隔线
+ *   二、机会卡描述(/pool 每张 kf-opportunity-card)
+ *       - .one 是否有文字、一行字高(line-height)、卡内布局方式
+ *       - 无描述卡是否「没有描述区」而不是空 div;指标区上提了多少
+ *       - 有描述卡 / 无描述卡「内容总高」差;每张卡最大连续空白(叶子文字的竖直间隙)
+ *   三、/pool 页脚类名命中:拿 cardFooter() 的每个类名,在 /pool 页脚里查规则命中(CSSOM 遍历 shadow root 的 adoptedStyleSheets),
+ *       再与 /reports 同名元素逐项比对 computed padding / border / borderRadius / 字号 / 颜色。
+ *
+ * 坑(见 memory kf-verify-browser-quirks):视图异步挂载;跨 shadow 走 host;hover 要真实指针;滚动位置先归零。
+ */
+import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const argv = process.argv.slice(2);
+const arg = (k, d) => (argv.includes(k) ? argv[argv.indexOf(k) + 1] : d);
+const TAG = arg('--tag', 'after');
+const JUDGE = TAG !== 'before';
+const JSON_OUT = arg('--json', '');
+
+if (!process.env.CHROME_PATH) {
+  const pinned = '/opt/hermes/.playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell';
+  if (existsSync(pinned)) process.env.CHROME_PATH = pinned;
+}
+
+const { startServer, launch, DEEP } = await import('./v11b-lib.mjs');
+const srv = await startServer(Number(process.env.VERIFY_PORT || 4194));
+const browser = await launch();
+const errors = [];
+
+const newPage = async (width = 1440, height = 900) => {
+  const ctx = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: 1 });
+  const page = await ctx.newPage();
+  page.on('pageerror', (e) => errors.push(e.message));
+  page.on('console', (m) => { if (m.type() === 'error' && !/favicon/.test(m.text())) errors.push(m.text()); });
+  return page;
+};
+const page = await newPage();
+
+const gotoView = async (p, name) => {
+  await page.goto(new URL(p, srv.base).href, { waitUntil: 'networkidle' });
+  await page.waitForFunction((n) => {
+    const v = document.querySelector('kf-app')?.shadowRoot?.querySelector(n);
+    return !!v && v.shadowRoot && v.shadowRoot.childElementCount > 0 && ('db' in v ? v.db != null : true);
+  }, name, { timeout: 20000 });
+  await page.waitForTimeout(700);
+};
+
+const rgb = (s) => (s.match(/\d+(\.\d+)?/g) || []).map(Number);
+const hex = (rgba, under = [255, 255, 255]) => {
+  /* rgba → 叠在不透明底上的实际颜色(选中态是半透明底,亮度要算合成后的) */
+  const [r, g, b, a = 1] = rgba;
+  return [0, 1, 2].map((i) => Math.round(rgba[i] * a + under[i] * (1 - a)));
+};
+const lum = ([r, g, b]) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
+const out = { tag: TAG, at: new Date().toISOString(), one: null, two: null, three: null };
+
+/* ======================= 一、排序段 ======================= */
+{
+  await gotoView('/pool', 'view-pool');
+  out.one = await page.evaluate(`(async () => {
+    ${DEEP}
+    const v = appR().querySelector('view-pool').shadowRoot;
+    const seg = v.querySelector('.filters .seg');
+    if (!seg) return { missing: true };
+    const btns = [...seg.querySelectorAll('button')];
+    const read = (b) => { const s = getComputedStyle(b); const r = b.getBoundingClientRect();
+      return { text: b.textContent.trim(), pressed: b.getAttribute('aria-pressed'), bg: s.backgroundColor, color: s.color, fontSize: s.fontSize, lineHeight: s.lineHeight,
+        h: Math.round(r.height * 10) / 10, radius: s.borderTopLeftRadius + ' ' + s.borderTopRightRadius, borderLeft: s.borderLeftWidth }; };
+    const ss = getComputedStyle(seg);
+    /* 分隔线:相邻按钮「左 1px」+ 第一个「无左边框」 */
+    const seps = btns.map((b, i) => ({ i, left: getComputedStyle(b).borderLeftWidth, leftStyle: getComputedStyle(b).borderLeftStyle, leftColor: getComputedStyle(b).borderLeftColor }));
+    /* 实际命中规则里的 background(不是 computed 的 rgba(0,0,0,0) 兜底) */
+    const sel = btns.find((b) => b.getAttribute('aria-pressed') === 'true');
+    const selRule = sel ? getComputedStyle(sel) : null;
+    const wrap = seg.parentElement.getBoundingClientRect();
+    const r = seg.getBoundingClientRect();
+    return { btns: btns.map(read), seps, outer: { radius: ss.borderTopLeftRadius, border: ss.borderTopWidth + ' ' + ss.borderTopStyle + ' ' + ss.borderTopColor, h: Math.round(r.height * 10) / 10, bg: ss.backgroundColor },
+      selected: sel ? read(sel) : null, unselected: btns.filter((b) => b.getAttribute('aria-pressed') !== 'true').map(read),
+      selColorRule: selRule ? selRule.color : null, wrapTop: wrap.top };
+  })()`);
+
+  /* 手机档:把排序抽屉打开,量里面的段选 */
+  const mpage = await newPage(393, 852);
+  await mpage.goto(new URL('/pool', srv.base).href, { waitUntil: 'networkidle' });
+  await mpage.waitForFunction(() => {
+    const v = document.querySelector('kf-app')?.shadowRoot?.querySelector('view-pool');
+    return !!v && v.shadowRoot && v.shadowRoot.childElementCount > 0 && v.db != null;
+  }, null, { timeout: 20000 });
+  await mpage.waitForTimeout(600);
+  await mpage.evaluate(() => {
+    const v = document.querySelector('kf-app').shadowRoot.querySelector('view-pool');
+    v.shadowRoot.querySelector('.pbar-more').setAttribute('open', '');
+  });
+  await mpage.waitForTimeout(200);
+  out.one.mobile = await mpage.evaluate(() => {
+    const v = document.querySelector('kf-app').shadowRoot.querySelector('view-pool').shadowRoot;
+    const btns = [...v.querySelectorAll('.pbar-sorts button')];
+    const sel = btns.find((b) => b.getAttribute('aria-pressed') === 'true');
+    const s = sel ? getComputedStyle(sel) : null;
+    return sel ? { text: sel.textContent.trim(), bg: s.backgroundColor, color: s.color, fontSize: s.fontSize, lineHeight: s.lineHeight } : { missing: true };
+  });
+  await mpage.context().close();
+}
+
+/* ======================= 二、机会卡描述 ======================= */
+{
+  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 };
+  })()`);
+}
+
+/* ======================= 三、页脚类名命中 ======================= */
+const FOOT_PROBE = `(async () => {
+  ${DEEP}
+  const host = appR().querySelector(location.pathname.includes('reports') ? 'view-reports' : 'view-pool');
+  const sr = host.shadowRoot;
+  const foot = sr.querySelector('.cfoot[data-foot]');
+  if (!foot) return { missing: true, host: host.localName };
+  /* 1. 页脚里出现的每个类名,逐项查:在宿主 shadow root 的 adoptedStyleSheets 里,是否有规则的选择器能命中它 */
+  const sheets = [...sr.adoptedStyleSheets, ...[...sr.querySelectorAll('style')].map((s) => s.sheet)];
+  const rules = [];
+  const walkRules = (list, media) => { for (const r of list || []) {
+    if (r.cssRules && !r.selectorText) { walkRules(r.cssRules, r.conditionText || media); continue; }
+    if (r.selectorText) rules.push({ sel: r.selectorText, media: media || '', css: r.style.cssText.slice(0, 90) });
+  } };
+  for (const s of sheets) if (s) { try { walkRules(s.cssRules, ''); } catch { /* 跨源 */ } }
+  const classes = new Set();
+  for (const el of [foot, ...foot.querySelectorAll('*')]) {
+    const cn = typeof el.className === 'string' ? el.className : '';   /* svg 的 className 是 SVGAnimatedString */
+    for (const c of cn.split(/\\s+/).filter(Boolean)) classes.add((el.localName === 'footer' || !/^(svg|path|circle|line|polyline|rect|g)$/.test(el.localName) ? '' : 'svg:') + c);
+  }
+  const hits = [];
+  for (const c of classes) {
+    const name = c;
+    const matching = rules.filter((r) => {
+      /* 规则选择器里是否以 .name 作为「完整类名」出现(避免 .cfl 命中 .cfleft) */
+      return new RegExp('\\\\.' + name.replace(/[-]/g, '\\\\-') + '(?![\\\\w-])').test(r.sel);
+    });
+    /* 选择器是否真的能作用到页脚里的某个元素 */
+    const el = [foot, ...foot.querySelectorAll('*')].find((e) => {
+      const cn = typeof e.className === 'string' ? e.className : '';
+      return cn.split(/\\s+/).includes(name);
+    });
+    const applies = matching.filter((r) => {
+      if (!el) return false;
+      try { return el.matches(r.sel.replace(/::?[a-z-]+(\\([^)]*\\))?/g, '')); } catch { return false; }
+    });
+    hits.push({ cls: name, tag: el ? elLabel(el).slice(0, 30) : '', rules: matching.length, applies: applies.length, el: el ? elLabel(el).slice(0, 30) : '' });
+  }
+  /* 2. 逐元素 computed 快照:footer 本身 + 里面每个元素,按「结构路径」记 */
+  const snap = (f) => {
+    const one = (el) => { const s = getComputedStyle(el);
+      return { padding: [s.paddingTop, s.paddingRight, s.paddingBottom, s.paddingLeft].join(' '),
+        border: [s.borderTopWidth, s.borderRightWidth, s.borderBottomWidth, s.borderLeftWidth].join(' ') + ' / ' + s.borderTopStyle + ' / ' + s.borderTopColor,
+        radius: [s.borderTopLeftRadius, s.borderTopRightRadius, s.borderBottomRightRadius, s.borderBottomLeftRadius].join(' '),
+        font: s.fontSize + '/' + s.lineHeight + ' ' + s.fontWeight, color: s.color, bg: s.backgroundColor,
+        display: s.display, gap: s.gap, justify: s.justifyContent, marginTop: s.marginTop };
+    };
+    const list = [[f, f.parentElement && f.parentElement.classList.contains('cfoot') ? '' : '']];
+    const path = (el) => { const p = []; for (let q = el; q && q !== f; q = q.parentElement) p.unshift(q.localName + (typeof q.className === 'string' && q.className ? '.' + q.className.trim().split(/\\s+/).join('.') : '')); return p.join('>'); };
+    const res = { 'cfoot': one(f) };
+    for (const el of f.querySelectorAll('*')) {
+      if (['svg', 'path', 'circle', 'line', 'polyline', 'rect', 'g'].includes(el.localName)) continue;
+      res[path(el)] = one(el);
+    }
+    return res;
+  };
+  /* 两页页脚根节点不同(/reports 是 <footer>,/pool 是 <span>):键用 cfoot,跨页可比 */
+  return { host: host.localName, footTag: foot.localName, classes: [...classes], hits, snap: snap(foot), mediaHits: rules.filter((r) => /cfoot|kf-btn/.test(r.sel)).map((r) => ({ sel: r.sel.slice(0, 70), media: r.media, css: r.css.slice(0, 70) })).slice(0, 14) };
+})()`;
+
+{
+  await gotoView('/pool', 'view-pool');
+  const poolFoot = await page.evaluate(FOOT_PROBE);
+  await gotoView('/reports', 'view-reports');
+  const repFoot = await page.evaluate(FOOT_PROBE);
+  /* 同路径元素逐项比对 */
+  const paths = Object.keys(poolFoot.snap || {}).filter((k) => k in (repFoot.snap || {}));
+  const diff = [];
+  for (const k of paths) {
+    for (const prop of ['padding', 'border', 'radius', 'font', 'color', 'bg', 'display', 'gap', 'justify', 'marginTop']) {
+      const a = poolFoot.snap[k][prop], b = repFoot.snap[k][prop];
+      if (a !== b) diff.push({ path: k, prop, pool: a, reports: b });
+    }
+  }
+  out.three = { pool: poolFoot, reports: repFoot, compared: paths, diff };
+}
+
+/* ======================= 汇总判分 ======================= */
+const checks = [];
+const check = (name, ok, detail) => { checks.push({ name, ok, detail }); console.log(`\n${JUDGE ? (ok ? '✓' : '✗') : '·'} ${name}\n    ${detail}`); };
+console.log(`\n===== v13 三项验收 · tag=${TAG} =====`);
+
+/* 一 */
+{
+  const o = out.one;
+  const sel = o.selected, un = o.unselected?.[0];
+  const composed = sel ? hex(rgb(sel.bg)) : [0, 0, 0];
+  const selLum = lum(composed), oldLum = lum([24, 24, 27]);
+  const brighter = composed.some((v) => v > 24);
+  const fontSame = un && sel.fontSize === un.fontSize && sel.lineHeight === un.lineHeight && sel.h === un.h;
+  const white = sel && rgb(sel.color).slice(0, 3).every((v) => v === 255);
+  const radiiZero = o.btns?.every((b) => b.radius === '0px 0px');
+  const seps1 = o.seps?.slice(1).every((s) => s.left === '1px' && s.leftStyle === 'solid') && o.seps?.[0].left === '0px';
+  const outerOk = o.outer?.radius === '8px' && o.outer?.border.startsWith('1px solid');
+  const h28 = o.outer?.h === 28 && o.btns?.every((b) => b.h === 28);
+  const m = o.mobile || {};
+  const mComposed = rgb(m.bg || 'rgb(0,0,0)');
+  const mOk = m.bg && mComposed.slice(0, 3).some((v) => v > 24) && !(rgb(m.color || '').slice(0, 3).every((v) => v === 255));
+  check('一 排序段:选中浅底不白字 / 字级一致 / 一整块 1px 分隔 / 高 28', brighter && !white && fontSame && radiiZero && seps1 && outerOk && h28 && mOk,
+    `选中「${sel?.text}」bg=${sel?.bg} → 叠白底后 ${composed}(亮度 ${selLum.toFixed(1)},旧 rgb(24,24,27) 亮度 ${oldLum.toFixed(1)}),字色 ${sel?.color}` +
+    `;未选中 ${un?.bg} / ${un?.color};font-size 选中 ${sel?.fontSize} vs 未选中 ${un?.fontSize},line-height ${sel?.lineHeight} vs ${un?.lineHeight},框高 ${sel?.h} vs ${un?.h}` +
+    `;按钮圆角 [${o.btns?.map((b) => b.radius).join(' | ')}],分隔线 [${o.seps?.map((s) => s.left).join(',')}]` +
+    `;外层 radius ${o.outer?.radius} border ${o.outer?.border} 高 ${o.outer?.h}` +
+    `;手机选中 bg=${m.bg} 字色 ${m.color} 字号 ${m.fontSize}`);
+}
+
+/* 二 */
+{
+  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=不渲染)`);
+}
+function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
+
+/* 三 */
+{
+  const t = out.three;
+  const poolMiss = (t.pool?.hits || []).filter((h) => h.applies === 0);
+  const comp = t.compared || [];
+  const same = comp.filter((p) => !t.diff.some((d) => d.path === p));
+  check('三 /pool 页脚类名逐项命中(每个类名 ≥1 条生效规则)+ 与 /reports 同名元素 computed 逐项相等',
+    !t.pool?.missing && poolMiss.length === 0 && comp.length >= 3 && (t.pool.classes || []).every((c) => (t.pool.hits || []).some((h) => h.cls === c && h.applies > 0)) && t.diff.length === 0,
+    `类名 ${t.pool?.classes?.length} 个:${(t.pool?.hits || []).map((h) => `${h.cls}(${h.applies} 条生效/${h.rules} 条含)${h.applies ? '' : '✗'}`).join('、')}` +
+    `;与 /reports 比对 ${comp.length} 个元素 × ${['padding', 'border', 'radius', 'font', 'color', 'bg', 'display', 'gap', 'justify', 'marginTop'].length} 项,一致 ${same.length},不一致 ${t.diff.length}` +
+    (t.diff.length ? '\n    ' + t.diff.slice(0, 8).map((d) => `${d.path}.${d.prop}: pool=${d.pool} reports=${d.reports}`).join('\n    ') : ''));
+}
+
+/* ======================= v12 回归:同原脚本判分(不能退步) ======================= */
+console.log('\n----- v12 回归(子进程跑 verify-visual-v12.mjs) -----');
+const { spawnSync } = await import('node:child_process');
+const reg = spawnSync(process.execPath, [join(ROOT, 'scripts/verify-visual-v12.mjs'), '--json', 'tmp/v13-v12-after.json'], { encoding: 'utf8', env: { ...process.env, VERIFY_PORT: '4195' } });
+const regLines = (reg.stdout || '').split('\n');
+console.log(regLines.filter((l) => /^(✓|✗) |判分:/.test(l) || /^    /.test(l) && false).join('\n'));
+const regLine = regLines.find((l) => /^判分:/.test(l)) || '(v12 脚本无输出)';
+const [, pass, total] = regLine.match(/(\d+)\/(\d+)/) || [];
+out.v12 = { pass: Number(pass), total: Number(total), line: regLine, fails: regLines.filter((l) => l.startsWith('✗')).map((l) => l.slice(2)) };
+check('v12 回归不退步', out.v12.pass >= 11, `${regLine};未达标:${out.v12.fails.join('、') || '无'}`);
+
+if (errors.length) console.log(`\n页面错误 ${errors.length}:\n    ${[...new Set(errors)].slice(0, 10).join('\n    ')}`);
+const fail = checks.filter((c) => !c.ok);
+console.log(`\n${JUDGE ? '判分' : '基线(不判分)'}:${checks.length - fail.length}/${checks.length} 达标${errors.length ? `,页面错误 ${errors.length}` : ''}`);
+
+if (JSON_OUT) {
+  mkdirSync(dirname(join(ROOT, JSON_OUT)), { recursive: true });
+  writeFileSync(join(ROOT, JSON_OUT), JSON.stringify({ ...out, checks, errors }, null, 2));
+}
+
+await browser.close();
+srv.kill();
+process.exit(JUDGE && (fail.length || errors.length) ? 1 : 0);

+ 17 - 8
src/pages/view-pool.js

@@ -64,14 +64,23 @@ export class ViewPool extends LitElement {
     .filters .kf-chip .n { font-size: 11px; color: var(--kf-ink-3); }
     .filters .kf-chip[aria-pressed="true"] .n { color: rgba(255, 255, 255, 0.72); }
     .kf-chip[data-zero="1"] { opacity: 0.42; }
-    /* 排序 = 单选段:一整块被 1px 分隔线切开,不是五个独立按钮 */
-    .seg { display: inline-flex; align-items: stretch; height: 28px; border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--r-sm); overflow: hidden; background: transparent; }
-    .seg button { appearance: none; -webkit-appearance: none; border: 0; border-left: 1px solid rgba(255, 255, 255, 0.14); border-radius: 0; margin: 0;
-      background: transparent; padding: 0 12px; height: 100%; font: 500 12px/1 var(--kf-font); color: var(--kf-ink-2); cursor: pointer;
+    /* 排序 = 单选段(v13 一):一整块(外层 1px 描边 + r-sm),内部 1px 分隔线,按钮各自不带圆角;
+       选中 = 浅色实底 + 主文字色(不再是深色实底白字),未选中 = 透明底 + 次级色;两者字号 / 行高完全一致。 */
+    .seg { --seg-line: rgba(0, 0, 0, 0.10); --seg-on: rgba(0, 0, 0, 0.06);
+      display: inline-flex; align-items: stretch; height: 28px; box-sizing: border-box; border: 1px solid var(--seg-line); border-radius: var(--r-sm); overflow: hidden; background: transparent; }
+    /* 字号 / 行高写在段上,按钮继承:避免选中态切字体时行盒变化(height 100% 只少 2px 边框,用 line-height 定,不跳) */
+    .seg > button { appearance: none; -webkit-appearance: none; border: 0; border-left: 1px solid var(--seg-line); border-radius: 0; margin: 0;
+      display: inline-flex; align-items: center; background: transparent; padding: 0 12px; height: calc(100% + 2px); box-sizing: border-box;
+      font-family: var(--kf-font); font-size: 12px; font-weight: 500; line-height: 1; color: var(--kf-ink-2); cursor: pointer;
       white-space: nowrap; transition: background var(--kf-dur) var(--kf-ease), color var(--kf-dur) var(--kf-ease); }
-    .seg button:first-child { border-left: 0; }
-    .seg button:hover { background: rgba(255, 255, 255, 0.55); color: var(--kf-ink); }
-    .seg button[aria-pressed="true"], .seg button[aria-pressed="true"]:hover { background: var(--kf-ink); color: #fff; }
+    .seg > button:first-child { border-left: 0; }
+    .seg > button:hover { background: rgba(0, 0, 0, 0.035); color: var(--kf-ink); }
+    .seg > button[aria-pressed="true"], .seg > button[aria-pressed="true"]:hover { background: var(--seg-on); color: var(--kf-ink); }
+    /* 暗色:底改白色半透明,描边同步提亮(与下拉 / 输入框同一套) */
+    @media (prefers-color-scheme: dark) {
+      .seg { --seg-line: rgba(255, 255, 255, 0.14); --seg-on: rgba(255, 255, 255, 0.10); }
+      .seg > button:hover { background: rgba(255, 255, 255, 0.06); }
+    }
     /* 搜索框:与排序段同高(28px)、同圆角;聚焦时 2px 焦点环由共享表给 */
     .frow.toolbar input[type="search"] { appearance: none; -webkit-appearance: none; height: 28px; box-sizing: border-box; padding: 0 10px;
       border-radius: var(--r-sm); border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.5); color: var(--kf-ink);
@@ -105,7 +114,7 @@ export class ViewPool extends LitElement {
       .pbar-sorts button { appearance: none; -webkit-appearance: none; border: 1px solid rgba(255, 255, 255, 0.14); border-left-width: 0; margin: 0; box-sizing: border-box; flex: 1; height: 44px; display: grid; place-items: center; border-radius: 0; font: 500 13px var(--kf-font); color: var(--kf-ink-2); background: rgba(255, 255, 255, 0.45); cursor: pointer; }
       .pbar-sorts button:first-child { border-left-width: 1px; border-top-left-radius: var(--r-sm); border-bottom-left-radius: var(--r-sm); }
       .pbar-sorts button:last-child { border-top-right-radius: var(--r-sm); border-bottom-right-radius: var(--r-sm); }
-      .pbar-sorts button[aria-pressed="true"] { background: var(--kf-ink); color: #fff; border-color: var(--kf-ink); }
+      .pbar-sorts button[aria-pressed="true"] { background: rgba(0, 0, 0, 0.06); color: var(--kf-ink); border-color: rgba(0, 0, 0, 0.10); }
       /* 手机上原生筛选卡收起(桌面档里已隐藏 .pbar 不在此列) */
       .filters { display: none; }
     }