Pattern 一句話

當 filter 必然有層錯位、用「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」按鈕讓使用者看見掃描範圍、自己決定要不要續抓。

對應 #59 Filter × Source 合成策略 的策略 D。


何時用、何時不用

  • Source 不支援 server-side filter(A 不可行)
  • 不能或不值得重 index(C 不可行)
  • Match 稀疏或不可預期、自動續抓(B)會拉爆
  • 工程量限制、原型期 / MVP

不用

  • Filter 是主要互動模式(使用者預期「自動全找完」)
  • 三數字會讓 UI 太複雜
  • 使用者完全不在意「掃描範圍」

三數字的語意

數字意思來源
已掃 N已從 source 載入並 filter 過的筆數client 累計
命中 K已掃 N 筆中、符合 filter 的筆數client 累計
共 MSource 總筆數(如果 source 知道)source meta(可選)

最少要顯示「已掃 N / 命中 K」 — 沒有 N 使用者不知道掃描範圍、沒有 K 使用者不知道有沒有命中。

「共 M」可選 — 有的 source(pagefind)會給 total count、有的(streaming)不會。


UI 模板

基本版

1<div class="filter-status">
2  已掃 <strong>24</strong> 筆 / 命中 <strong>3</strong>3  <button>再掃一批</button>
4</div>

含總數

1<div class="filter-status">
2  已掃 <strong>24</strong> / <strong>~150</strong> 筆 — 命中 <strong>3</strong>
3  <button>再掃一批</button>
4</div>

含結束狀態(呼應 #57 三狀態)

 1<!-- Loading -->
 2<div class="filter-status">掃描中... 已掃 <strong>24</strong> / 命中 <strong>3</strong></div>
 3
 4<!-- Partial(還可續) -->
 5<div class="filter-status">已掃 <strong>24</strong> / 命中 <strong>3</strong>
 6  <button>再掃一批</button>
 7</div>
 8
 9<!-- End(掃完) -->
10<div class="filter-status">已全部掃完、共命中 <strong>12</strong></div>
11
12<!-- Empty (filter) -->
13<div class="filter-status">已掃 <strong>24</strong>、沒有命中
14  <button>再掃一批</button><a>清除 filter</a>
15</div>

進度更新時機

即時更新(每筆)

1for (const item of stream) {
2  scanned++;
3  if (matches(item)) {
4    matched++;
5    appendResult(item);
6  }
7  updateUI(scanned, matched);  // 每筆更新
8}

UX 順、但 DOM 操作頻繁、可能 jank。

批次更新(每批)

1const batch = await fetchNext();
2scanned += batch.length;
3const m = batch.filter(matches);
4matched += m.length;
5appendResults(m);
6updateUI(scanned, matched);  // 每批一次

DOM 操作少、但 UX 不夠順(一段時間沒動)。

推薦:每批 + 載入中 spinner

批次後更新數字、批次間顯示 spinner。最平衡。


跟自動續抓(B)的混合

可以做成「初始自動續抓 N 批、之後切誠實 UX」:

1async function searchWithFilter(query) {
2  // 初始自動續抓 3 批(湊一些結果)
3  await fetchUntilQuota(3, autoBatches: 3);
4
5  // 之後使用者手動點「再掃一批」
6  showHonestProgressUI();
7}

混合的好處:使用者一進來就有結果(不是空畫面)、之後續抓由使用者決定。


反例

反例 1:只顯示「命中 K」、不顯示「已掃 N」

1<div>找到 3 筆結果</div>

使用者不知道是從多少筆裡找的、不知道「再掃會不會有」。

反例 2:只顯示「共 M / N」進度條、沒分「已掃」「命中」

1<progress value="24" max="150"></progress>

進度條告訴使用者「load 進度」、但「load 進度 ≠ filter 進度」。沒命中時使用者不知道為什麼進度走了 24% 但畫面沒結果。

反例 3:「再掃一批」沒做

只顯示三數字、沒提供續抓 button — 使用者看到「已掃 24 沒命中」、不知道下一步。


跟 #57 三狀態的關係

誠實進度 UX 是 #57 Loading / Empty / End 三狀態的區分 在「filter + 分批」情境下的具體實作。三數字提供區分三狀態的訊號:

#57 狀態對應的三數字組合
Loading已掃增加中、N 還在跑
Empty (filter)已掃 = 24、命中 = 0、還有 → 「再掃」
End已掃 = M、命中 = K(K 可能 0)
Partial已掃 < M、命中 ≥ 1、還有 → 「再掃」

判讀徵兆

訊號該做的事
Filter 後可能 0 筆、source 還有未載入用本 pattern
UI 上只有「找到 K 筆」、沒有「已掃 N」補 N — 否則使用者無法判斷
沒有「再掃一批」按鈕補 — 給使用者下一步行動
工程量允許做策略 A / C用 A / C、誠實 UX 是退路
Match 密集、自動續抓不會爆用策略 B、誠實 UX 太顯眼

核心原則:誠實 UX 不是「lazy 解法」、是「sourcing 限制下的合理透明度」。給使用者三數字 + 行動選項、比假裝完美但 silent 失敗好。

#19 覆寫深度的成本告知 同源:兩者都是「把實作的限制 / 代價攤給使用者、讓使用者參與決策」。差別在 #19 是「實作前告知工程成本」、本卡是「runtime 持續顯示掃描成本」 — 攤出來的位置不同、原則一致:silent 累積負擔是反模式。