UNPKG

table-reuse

Version:

A reusable table built on top of Antd ProTable

325 lines (315 loc) 12.9 kB
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