+ } else {
+ // text
+ const textValue = blankOnly
+ ? ""
+ : selectedKeys[0] != null
+ ? String(selectedKeys[0])
+ : "";
+ control = (
setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => confirm()}
- style={{ marginBottom: 8, display: "block" }}
allowClear
/>
-
-
-
-
+ );
+ }
+
+ return (
+
+ setSelectedKeys(e.target.checked ? [BLANK_SENTINEL] : [])}
+ style={{ marginBottom: 8 }}
+ >
+ 仅显示空白
+
+ {control}
+ {footer}
);
}
diff --git a/src/app/table-filters.test.ts b/src/app/table-filters.test.ts
index 2123d69..c6e726c 100644
--- a/src/app/table-filters.test.ts
+++ b/src/app/table-filters.test.ts
@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import {
+ BLANK_SENTINEL,
CATEGORY_THRESHOLD,
distinctValues,
inferFilterType,
+ isBlank,
matchesFilter,
nextLockedRow,
} from "./table-filters";
@@ -79,3 +81,30 @@ describe("nextLockedRow", () => {
expect(nextLockedRow("5", "7")).toBe("7");
});
});
+
+describe("isBlank", () => {
+ it("null/undefined/empty/whitespace are blank", () => {
+ expect(isBlank(null)).toBe(true);
+ expect(isBlank(undefined)).toBe(true);
+ expect(isBlank("")).toBe(true);
+ expect(isBlank(" ")).toBe(true);
+ });
+ it("non-empty values (including 0/false) are not blank", () => {
+ expect(isBlank("x")).toBe(false);
+ expect(isBlank(0)).toBe(false);
+ expect(isBlank(false)).toBe(false);
+ });
+});
+
+describe("matchesFilter blank sentinel", () => {
+ it("sentinel matches blank cells for any type", () => {
+ expect(matchesFilter("text", null, BLANK_SENTINEL)).toBe(true);
+ expect(matchesFilter("category", "", BLANK_SENTINEL)).toBe(true);
+ expect(matchesFilter("date", " ", BLANK_SENTINEL)).toBe(true);
+ });
+ it("sentinel does not match non-blank cells", () => {
+ expect(matchesFilter("text", "hello", BLANK_SENTINEL)).toBe(false);
+ expect(matchesFilter("category", "A", BLANK_SENTINEL)).toBe(false);
+ expect(matchesFilter("date", "2024-01-01", BLANK_SENTINEL)).toBe(false);
+ });
+});
diff --git a/src/app/table-filters.ts b/src/app/table-filters.ts
index f3cebcd..5d972da 100644
--- a/src/app/table-filters.ts
+++ b/src/app/table-filters.ts
@@ -35,8 +35,18 @@ export function inferFilterType(
return "text";
}
+/** Sentinel filter key meaning "match blank/empty cells". */
+export const BLANK_SENTINEL = "__blank__";
+
+/** A cell is blank when it is null/undefined or trims to empty. 0/false are NOT blank. */
+export function isBlank(value: unknown): boolean {
+ if (value === null || value === undefined) return true;
+ return String(value).trim() === "";
+}
+
/**
* Does a cell match a single filter key?
+ * - BLANK_SENTINEL: cell is blank (any type)
* - text: case-insensitive substring
* - category: exact equality
* - date: range encoded as "start|end" (either side may be ""), ISO string compare
@@ -46,6 +56,8 @@ export function matchesFilter(
cellValue: unknown,
filterValue: string
): boolean {
+ if (filterValue === BLANK_SENTINEL) return isBlank(cellValue);
+
const cell =
cellValue === null || cellValue === undefined ? "" : String(cellValue);