[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¶
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 触发¶
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 后请验证)¶
- ✅ merchant tab 首次打开应 < 100ms(无肉眼可见卡顿)
- ✅ chip 筛选切换瞬完(不再等 jsstore round-trip)
- ✅ 排序质量分 / 邮箱 / 高评分 瞬完
- ✅ 翻页瞬完
- ✅ 关键词搜索瞬完
- ✅ Settings 改质量分权重 → 列表立即按新分排序
- ✅ 按任务筛选 client mode 内瞬完
- ⚠️ 数据 > 50k 时:banner 提示 + 切 server 后可见全部
- ⚠️ 添加高级筛选:自动切 server + banner 说明
- ✅ 导出 / 删除功能(走 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 复用