跳转至

[ISSUE-0045] 质量分真正能排序 — 撤回 v0.10.64 阉割做法

v0.10.64 修 ISSUE-0044 Bug 5 时把 quality 加进 unsortable,让表头不可点 — 功能阉割。用户截图(v0.10.52)仍然能看到 ↑ 箭头(旧版可点击但没效果), 升级后又变成"完全点不动" — 体验更差。

本次(v0.10.65)改成真排序:服务端检测 computed-sort,走 hasEmailOnly 同款"全表 + JS sort + slice" 路径。

病灶(v0.10.64 之前)

链路 问题
用户点 quality 表头 DataGrid 显示 ↑ 箭头 ✓
formatSortParams { quality: -1 }
apiLocalDataListsort 给 jsstore jsstore DB 无 quality 列 → 静默忽略
返回行 仍是原 DB 顺序(默认 create_time desc)
用户视觉 "排序坏了"

病灶(v0.10.64 之后 — 反而更差)

const unsortable = ['profile', 'contact', 'location', 'social', 'logo', 'quality'];

sortable: false → 表头完全不可点,连箭头都没有。看似"诚实"但用户的核心痛点 (按高质量找客户)反而无解。

修(v0.10.65)

1. 撤回 unsortable 添加 'quality'

const unsortable = ['profile', 'contact', 'location', 'logo'];
// social 也撤了 — 它的 valueGetter 是 count,可以 JS sort

quality / email / phone / social 全部 sortable: true。

2. apiLocalDataList 加 needsClientSort 检测

const COMPUTED_SORT_FIELDS = new Set(['quality', 'email', 'phone', 'social']);
const needsClientSort = hasComputedSort(sort);

// 拆 sort:dbSort 给 jsstore(仅真实列),完整 sort 给 JS post-sort
const dbSort = sort.filter((s) => !COMPUTED_SORT_FIELDS.has(s?.by));

3. 合并到 hasEmailOnly 路径

if (hasEmailOnly || needsClientSort) {
  const big = await getListByQuery(baseQuery, 1, HAS_EMAIL_SCAN_LIMIT, dbSort);
  let scanned = big?.list || [];
  const truncated = scanned.length >= HAS_EMAIL_SCAN_LIMIT;
  if (hasEmailOnly) {
    scanned = scanned.filter((r) => Array.isArray(r.emails) && r.emails.length > 0);
  }
  if (needsClientSort) {
    scanned = applyClientSort(scanned, sort);
  }
  const start = (current - 1) * pageSize;
  return { success: true, data: { list: scanned.slice(start, start + pageSize), total: scanned.length, ..., truncated, scanLimit: HAS_EMAIL_SCAN_LIMIT } };
}

4. computeSortValue + applyClientSort 实现

function computeSortValue(row, field) {
  switch (field) {
    case 'quality': return computeQualityScore(row);
    case 'email':   return (row.emails || []).length;
    case 'phone':   return row.phone ? 1 : 0;
    case 'social':  return ['website', 'facebook', 'instagram', ...].filter((k) => row[k]).length;
    default:        return row[field] ?? 0;
  }
}

function applyClientSort(list, sort) {
  return [...list].sort((a, b) => {
    for (const s of sort.filter((x) => x?.by)) {
      const va = computeSortValue(a, s.by);
      const vb = computeSortValue(b, s.by);
      const diff = typeof va === 'number' && typeof vb === 'number'
        ? va - vb
        : String(va).localeCompare(String(vb));
      if (diff !== 0) return s.type === 'desc' ? -diff : diff;
    }
    return 0;
  });
}

支持多列 sort:先 by:quality 再 by:rating 等。

性能

  • HAS_EMAIL_SCAN_LIMIT = 50,000 行
  • 测算:50k 行 quality sort ≈ 50-100ms(同 hasEmailOnly path 性能)
  • 50k 时 truncated: true → UI banner 提示用户「仅显示前 5w,请缩小筛选」

audit_grep 闸

audit_grep:
  - pattern: "unsortable\\s*=\\s*\\[(?:[^\\]])*['\"]quality['\"]"
    description: "v0.10.65 起不应再有 quality in unsortable"

元洞察

"做不到"和"做得到但需要客户端"是不同等级的方案

v0.10.64 我跳过了"做得到但需要客户端",直接选了"做不到"。这是工程偷懒, 反映在 UX 上就是阉割功能。

用户给的 v0.10.52 截图本身就证明:当年的代码"假装支持排序"也比"明确不支持" 强 — 至少给了用户希望(虽然没生效)。正确方案永远是真实现, 不是删功能。

相关

  • [[0044-screenshot-6-ux-feedback-batch|0044-用户截图6项UX反馈-缩放-按钮色-排序-分页-顶部CTA]] — 上一轮(v0.10.64 阉割做法)
  • [[0018-hasemailonly-50k-silent-truncate|0018-hasEmailOnly-50k上限静默截断]] — 同 path 同 LIMIT 同模板