table-reuse
Version:
A reusable table built on top of Antd ProTable
325 lines (315 loc) • 12.9 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import React, { useRef, useState, useMemo, useEffect } from 'react';
import { ProFormSelect, ProFormRadio, ProFormDatePicker, ProFormDateTimeRangePicker, ProFormItem, ProFormGroup, ProFormText, QueryFilter, ProTable } from '@ant-design/pro-components';
import { Input, Form } from 'antd';
import { ActionView, createButtons } from 'action-view';
import { ArrowRightOutlined, SearchOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { generatePath } from 'react-router-dom';
const SELECT_BOX_PLACEHOLDER = "Select...";
const getSelectFilter = (filter) => {
const { label, fieldName, options, filterProps } = filter;
return (jsx(ProFormSelect, { name: fieldName, label: label, placeholder: SELECT_BOX_PLACEHOLDER, fieldProps: {
showSearch: true,
allowClear: true,
options,
optionFilterProp: "label",
...filterProps,
} }, fieldName));
};
const getRadioFilter = (filter) => {
const { label, fieldName, options } = filter;
return jsx(ProFormRadio.Group, { name: fieldName, label: label, options: options }, fieldName);
};
const getDatePickerFilter = (filter) => {
const { label, fieldName } = filter;
return jsx(ProFormDatePicker, { name: fieldName, label: label }, fieldName);
};
const getDateRangePickerFilter = (filter) => {
const { label, fieldName } = filter;
return (jsx(ProFormDateTimeRangePicker, { name: fieldName, label: label, transform: (value, namePath, allValues) => {
if (Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string") {
return {
[namePath]: [dayjs(value[0]), dayjs(value[1])],
};
}
return allValues;
}, fieldProps: {
placeholder: ["Start date", "End date"],
separator: jsx(ArrowRightOutlined, {}),
} }, fieldName));
};
const getSearchAreaFilter = (config, onSearch) => {
const { searchType = "options", searchField = "searchValue" } = config;
if (searchType === "single" && onSearch) {
return (jsx(ProFormItem, { name: searchField, children: jsx(Input.Search, {}) }));
}
if (searchType === "options") {
return (jsxs(ProFormGroup, { children: [jsx(ProFormSelect, { name: "searchField", placeholder: SELECT_BOX_PLACEHOLDER, fieldProps: {
showSearch: true,
allowClear: true,
options: config.searchOptions,
} }), jsx(ProFormText, { name: "searchValue", placeholder: "Enter here", fieldProps: {
allowClear: true,
suffix: jsx(SearchOutlined, {}),
} })] }));
}
return (jsx(ProFormText, { name: searchField, placeholder: config.inputPlaceholder, fieldProps: {
allowClear: true,
suffix: jsx(SearchOutlined, {}),
} }));
};
function SearchForm({ form, filters = [], onSearch, onReset, searchConfig, }) {
const renderFilter = (filter) => {
switch (filter.filterType) {
case "SELECT":
return getSelectFilter(filter);
case "RADIO":
return getRadioFilter(filter);
case "DATE":
return getDatePickerFilter(filter);
case "DATE_RANGE":
return getDateRangePickerFilter(filter);
default:
return null;
}
};
const onFinish = async () => {
const values = form.getFieldsValue();
console.log("onFinish", values);
return onSearch(values);
};
return (jsxs(QueryFilter, { form: form, onFinish: onFinish, onReset: onReset, labelWidth: "auto", defaultCollapsed: false, span: 8, submitter: {
resetButtonProps: {
children: "Reset",
},
submitButtonProps: {
children: "Search",
type: "primary",
},
}, children: [searchConfig && (jsx(React.Fragment, { children: getSearchAreaFilter(searchConfig) }, "search-field")), filters.map((filter) => (jsx(React.Fragment, { children: renderFilter(filter) }, filter.fieldName)))] }));
}
function createActionColumn(buildRowActions, maxVisibleRowActions = 1) {
if (!buildRowActions)
return undefined;
return {
title: "Actions",
valueType: "option",
// fixed: "right",
render: (_, record) => {
const actions = buildRowActions(record);
return jsx(ActionView, { actions: actions, maxVisible: maxVisibleRowActions });
},
};
}
/**
* 创建 ProTable 的 request 方法
* 在标准化 filter 后合并 tabFilter
*/
function createRequestFunction(onList, searchParams, tabFilter, buildApiPayload) {
const requestFunc = (params, sort) => {
const normalizedFilter = buildApiPayload
? buildApiPayload(searchParams)
: searchParams;
const mergedFilter = { ...normalizedFilter, ...tabFilter };
const apiPayload = {
...params,
sort: sort ?? {},
...mergedFilter,
};
console.log("normalizedFilter", normalizedFilter);
console.log("mergedFilter", mergedFilter);
console.log("apiPayload", apiPayload);
return onList(apiPayload);
};
return requestFunc;
}
// Used as sample
function createProTableTabs({ tabs, activeKey, onTabChange, }) {
return {
menu: {
type: "tab",
activeKey: String(activeKey),
items: tabs.map((tab) => ({
key: String(tab.key),
label: tab.label,
})),
onChange: (key) => {
// TS now knows key is string | undefined
if (key !== undefined) {
console.log("切换到", key);
onTabChange(key);
}
},
},
};
}
function SearchableTable({ columns, onList, tableId, tableTitle, pageActions, buildRowActions, searchConfig, filters, tabs = [], defaultTabKey, buildApiPayload, rowKey = "id", pullInterval = 0, maxVisibleRowActions = 2, reloadOnDataChange = false, }) {
const [form] = Form.useForm();
const actionRef = useRef();
const [activeTabKey, setActiveTabKey] = useState(defaultTabKey);
const [searchParams, setSearchParams] = useState();
const tabFilter = useMemo(() => {
const current = tabs.find((tab) => tab.key === activeTabKey);
return (current?.filter ?? {});
}, [activeTabKey, tabs]);
const request = useMemo(() => createRequestFunction(onList, searchParams, tabFilter, buildApiPayload), [onList, searchParams, tabFilter, buildApiPayload]);
useEffect(() => {
if (pullInterval > 0) {
const timer = setInterval(() => {
actionRef.current?.reload();
}, pullInterval);
return () => clearInterval(timer);
}
}, [pullInterval]);
useEffect(() => {
if (reloadOnDataChange) {
actionRef.current?.reload();
}
}, [searchParams, activeTabKey, reloadOnDataChange]);
const onSearch = async () => {
const values = await form.validateFields();
console.log("onSearch", values);
setSearchParams(values);
actionRef.current?.reload();
};
const handleReset = () => {
form.resetFields();
setSearchParams({});
actionRef.current?.reload();
};
const allColumns = useMemo(() => {
const actionColumn = createActionColumn(buildRowActions, maxVisibleRowActions);
return actionColumn ? [...columns, actionColumn] : columns;
}, [columns, buildRowActions, maxVisibleRowActions]);
const onTabChange = (newKey) => {
console.log("onTabChange:", newKey);
setActiveTabKey(newKey);
onSearch();
};
const toolbar = createProTableTabs({
tabs,
activeKey: activeTabKey,
onTabChange,
});
return (jsxs("div", { children: [tableTitle && jsx("h3", { style: { marginBottom: 16 }, children: tableTitle }), filters && (jsx(SearchForm, { form: form, filters: filters, onSearch: onSearch, onReset: handleReset, searchConfig: searchConfig })), jsx(ProTable, { request: request, columns: allColumns, rowKey: rowKey, actionRef: actionRef, toolBarRender: () => createButtons(pageActions || []), toolbar: toolbar, columnsState: {
persistenceKey: `${tableId || tableTitle}-column-state`,
persistenceType: "localStorage",
}, search: false, pagination: { showSizeChanger: true }, scroll: { x: "max-content" } })] }));
}
/** ------------------ getCrudPath.ts ------------------ */
/**
* 获取所有 CRUD 和页面级操作路径(扁平化)
* @param basePath 页面或模块的基础路径,例如 "pages/p100"
* @param id 行/项 ID,默认使用 ":id" 占位符
*/
function getCrudPath(basePath, id = ":id") {
return {
edit: `/${basePath}/edit/${id}`,
view: `/${basePath}/view/${id}`,
clone: `/${basePath}/clone/${id}`,
delete: `/${basePath}/delete/${id}`,
list: `/${basePath}`,
create: `/${basePath}/create`,
settings: `/${basePath}/settings`,
dashboard: `/${basePath}/dashboard`,
search: `/${basePath}/search`,
import: `/${basePath}/import`,
export: `/${basePath}/export`,
bulkEdit: `/${basePath}/bulk-edit`,
};
}
/**
* 根据 row 和 labels 生成页面级与行级操作
* 使用 getCrudPath 统一路径
*/
function getCrudActions({ basePath, navigate, labels, rowKey, apiDelete, onImport, onExport, }) {
const paths = getCrudPath(basePath);
// 顶部页面操作(例如创建、导入、导出)
const pageActions = [];
if (labels.createLabel) {
pageActions.push({
label: labels.createLabel,
type: "primary",
onClick: () => navigate(paths.create),
});
}
if (labels.importLabel && onImport) {
pageActions.push({
label: labels.importLabel,
type: "default",
onClick: async () => {
try {
await onImport();
}
catch (err) {
console.error("导入失败", err);
}
},
});
}
if (labels.exportLabel && onExport) {
pageActions.push({
label: labels.exportLabel,
type: "default",
onClick: async () => {
try {
await onExport();
}
catch (err) {
console.error("导出失败", err);
}
},
});
}
// 行级操作(针对某一条数据)
const buildRowActions = (row) => {
const id = row[rowKey];
if (id == null)
return [];
const actions = [];
if (labels.viewLabel) {
actions.push({
label: labels.viewLabel,
type: "text",
onClick: () => navigate(generatePath(paths.view, { id: String(id) })),
});
}
if (labels.editLabel) {
actions.push({
label: labels.editLabel,
type: "text",
onClick: () => navigate(generatePath(paths.edit, { id: String(id) })),
});
}
if (labels.cloneLabel) {
actions.push({
label: labels.cloneLabel,
type: "text",
onClick: () => navigate(generatePath(paths.clone, { id: String(id) }), {
state: { cloneId: id },
}),
});
}
if (labels.deleteLabel && apiDelete) {
actions.push({
label: labels.deleteLabel,
type: "text",
danger: true,
onClick: async () => {
try {
await apiDelete(String(id));
}
catch (err) {
console.error("删除失败", err);
}
},
});
}
return actions;
};
// 保持原返回结构
return { pageActions, buildRowActions };
}
export { SearchableTable, getCrudActions, getCrudPath, getDatePickerFilter, getDateRangePickerFilter, getRadioFilter, getSearchAreaFilter, getSelectFilter };
//# sourceMappingURL=index.esm.js.map