Add FilterDropdown component and column filter props factory

src/app/table-filters.tsx renders type-specific filter UIs (text search,
category checkbox list, date range) and exposes buildColumnFilterProps to
attach antd controlled filter props (filterDropdown/onFilter/filteredValue)
to a column. Verified against antd v6.4.3 type defs.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-06-18 09:44:52 +08:00
parent 4faa960591
commit b893fd558a

139
src/app/table-filters.tsx Normal file
View File

@@ -0,0 +1,139 @@
"use client";
import { Button, Input, Checkbox, DatePicker, Space } from "antd";
import dayjs from "dayjs";
import type { ColumnType } from "antd/es/table";
import type { FilterDropdownProps } from "antd/es/table/interface";
import { inferFilterType, distinctValues, matchesFilter } from "./table-filters";
import type { DataRow, FilterType } from "./table-filters";
const DATE_FMT = "YYYY-MM-DD";
/** 解析受控 selectedKeys 里的 "start|end" 编码为 RangePicker 值。 */
function decodeRange(keys: React.Key[]): [dayjs.Dayjs | null, dayjs.Dayjs | null] {
const raw = keys[0] != null ? String(keys[0]) : "";
const sep = raw.indexOf("|");
if (sep < 0) return [null, null];
const s = raw.slice(0, sep);
const e = raw.slice(sep + 1);
return [s ? dayjs(s, DATE_FMT) : null, e ? dayjs(e, DATE_FMT) : null];
}
export function FilterDropdown({
type,
distinct,
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: FilterDropdownProps & {
type: FilterType;
distinct: string[];
}): React.ReactElement {
if (type === "date") {
const [start, end] = decodeRange(selectedKeys);
return (
<div style={{ padding: 8 }}>
<DatePicker.RangePicker
value={[start, end]}
onChange={(dates) => {
if (!dates || dates[0] == null || dates[1] == null) {
setSelectedKeys([]);
} else {
const s = dates[0].format(DATE_FMT);
const e = dates[1].format(DATE_FMT);
setSelectedKeys([`${s}|${e}`]); // 单个编码键,供 onFilter 解码
}
}}
/>
<Space style={{ marginTop: 8, display: "flex", justifyContent: "flex-end" }}>
<Button
size="small"
onClick={() => {
clearFilters?.();
confirm();
}}
>
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
</Button>
</Space>
</div>
);
}
if (type === "category") {
return (
<div style={{ padding: 8, maxWidth: 260 }}>
<Checkbox.Group
style={{ display: "flex", flexDirection: "column", maxHeight: 240, overflow: "auto" }}
value={(selectedKeys as string[]) ?? []}
onChange={(vals) => setSelectedKeys(vals as React.Key[])}
options={distinct}
/>
<Space style={{ marginTop: 8, display: "flex", justifyContent: "flex-end" }}>
<Button
size="small"
onClick={() => {
clearFilters?.();
confirm();
}}
>
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
</Button>
</Space>
</div>
);
}
// text
const textValue = selectedKeys[0] != null ? String(selectedKeys[0]) : "";
return (
<div style={{ padding: 8 }}>
<Input
placeholder="输入关键字"
value={textValue}
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => confirm()}
style={{ marginBottom: 8, display: "block" }}
allowClear
/>
<Space style={{ display: "flex", justifyContent: "flex-end" }}>
<Button
size="small"
onClick={() => {
clearFilters?.();
confirm();
}}
>
</Button>
<Button type="primary" size="small" onClick={() => confirm()}>
</Button>
</Space>
</div>
);
}
/** 为某列构造 antd 受控筛选属性filterDropdown / onFilter / filteredValue。 */
export function buildColumnFilterProps(
key: string,
data: DataRow[],
filteredValue: React.Key[] | null
): Partial<ColumnType<DataRow>> {
const type = inferFilterType(key, data);
const distinct = type === "category" ? distinctValues(key, data) : [];
return {
filterDropdown: (props) => (
<FilterDropdown type={type} distinct={distinct} {...props} />
),
onFilter: (value, record) => matchesFilter(type, record[key], String(value)),
filteredValue: filteredValue ?? null,
};
}