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:
139
src/app/table-filters.tsx
Normal file
139
src/app/table-filters.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user