跳转至

[ISSUE-0053] A2:商家列表 client/server hybrid

v0.10.71 B+C 方案后 merchant tab 首次打开仍卡顿(22w 数据 jsstore server-pagination 耗时 ~200ms + DataGridPremium mount 重)。 A2 方案:merchant tab 也走 client-side(复用 DataView 顶层已 polling 30s 的 rows 50k), 跟 website/email/phone tab 架构对齐 → 瞬开;server 作 fallback 给老数据 / 高级筛选用。

架构对比

维度 v0.10.72 之前 v0.10.73 A2
merchant tab 数据源 每次 jsstore server-pagination 默认复用 rows 50k(client-side);显式切才 server
sort/filter/paginate jsstore where + order + limit JS in-memory(≤ 50k 数据时 ~5-20ms)
与 website/email/phone tab 一致
高级筛选(filters 数组) server 支持 自动切 server mode
老数据(> 50k)访问 翻页可达 fallback:banner 显示"切到完整模式"按钮

实施

1. DataView 顶层 rows 传给 LocalDataView

<LocalDataView
  ...
  rowsSnapshot={rows}   // 已有的 50k client cache
/>

2. LocalDataView 新增 mode state + client useMemo

const [mode, setMode] = useState<'client' | 'server'>('client');
const [qualityVersion, setQualityVersion] = useState(0);

const clientPaged = useMemo(() => {
  if (mode !== 'client') return null;
  let list = rowsSnapshot || [];

  // taskId filter
  if (filterTaskId) list = list.filter(r => r?.taskId === filterTaskId);

  // quickFilter (has-email / has-website / high-rating / scraped / pending)
  if (quickFilter === 'has-email') list = list.filter(...);
  ...

  // keyword (name / domain / category)
  if (keyword) list = list.filter(...);

  // sort
  const sortRules = sortModel.map(s => ({ by: s.field, type: s.sort }));
  if (sortRules.length === 0) {
    list = [...list].sort((a, b) => (b.id || 0) - (a.id || 0));  // id desc
  } else {
    list = applyClientSort(list, sortRules);  // 复用 search.ts
  }

  // paginate
  const start = (page - 1) * pageSize;
  return { list: list.slice(start, start + pageSize), total: list.length };
}, [mode, rowsSnapshot, filterTaskId, quickFilter, keyword, sortModel, page, pageSize, qualityVersion]);

// 展示层
const tableData = mode === 'client' ? (clientPaged?.list || []) : serverTableData;
const total = mode === 'client' ? (clientPaged?.total || 0) : serverTotal;

3. dataRun 仅 server mode 触发

useEffect(() => {
  if (mode === 'server') getTableData();
}, [page, pageSize, sort, mode]);

4. 自动切 server:filters 添加时

useEffect(() => {
  if (filters.length > 0 && mode === 'client') setMode('server');
}, [filters, mode]);

5. UI mode 提示 + 切换

  • client mode + 有老数据:蓝色 info banner → "快速模式,还有 N 条老数据 [切到完整模式]"
  • server mode:黄色 warning banner → "完整模式(较慢) [切回快速模式]"
  • 用户添加高级筛选时"切回快速模式"按钮 disabled

6. Quality config 即时刷新

qualityConfigItem.watch → client mode bump qualityVersion 触发 useMemo 重算;server mode 继续走 getTableData。

7. search.ts 导出 helper

COMPUTED_SORT_FIELDS / computeSortValue / applyClientSort 从 file-local 改 export,LocalDataView 复用。

性能预期

操作 v0.10.72 v0.10.73(client mode)
merchant tab 首次打开 ~200ms ~30-50ms(无 IndexedDB 等待)
quickFilter 切换 ~100ms(dataRun) <10ms(useMemo)
sort by quality ~80-150ms(hasEmailOnly path 全表+JS sort) <10ms(直接 in-memory)
翻页 ~80ms <5ms(slice)

已知限制(fallback 入口设计)

限制 处理
> 50k 老数据 UI 不显示 banner 提示 + 一键切 server
高级 filter(filters 数组) useEffect 自动切 server,banner disabled "切回" 按钮
导出 / 删除 仍走 server API(unaffected — 这些是显式 user action 不是 polling)

测试 checklist(v0.10.73 后请验证)

  1. ✅ merchant tab 首次打开应 < 100ms(无肉眼可见卡顿)
  2. ✅ chip 筛选切换瞬完(不再等 jsstore round-trip)
  3. ✅ 排序质量分 / 邮箱 / 高评分 瞬完
  4. ✅ 翻页瞬完
  5. ✅ 关键词搜索瞬完
  6. ✅ Settings 改质量分权重 → 列表立即按新分排序
  7. ✅ 按任务筛选 client mode 内瞬完
  8. ⚠️ 数据 > 50k 时:banner 提示 + 切 server 后可见全部
  9. ⚠️ 添加高级筛选:自动切 server + banner 说明
  10. ✅ 导出 / 删除功能(走 server API)正常

相关

  • [[0051-merchant-lag-rows-polling-default-sort|0051-商家列表卡顿-rows-polling-默认sort]] — B+C 方案(30s polling + id desc)
  • [[0050-merchant-chip-truncate-50k-slow|0050-商家列表chip截断50k-加载慢]] — 之前 stats 截断 + 加载慢
  • 修bug全字典扫描 — search.ts helper 导出 + LocalDataView 复用