import React, { useCallback, useReducer, useMemo, useEffect, useImperativeHandle, ForwardRefRenderFunction, useState, useRef, } from 'react'; import Table from 'antd/es/table'; import 'antd/es/table/style/index'; import Pagination from 'antd/es/pagination'; import 'antd/es/pagination/style/index'; import Spin from 'antd/es/spin'; import 'antd/es/spin/style/index'; import message from 'antd/es/message'; import 'antd/es/message/style/index'; import 'antd/es/checkbox/style/index'; import Loading3QuartersOutlined from '@ant-design/icons/Loading3QuartersOutlined'; import dayjs from 'dayjs'; import classNames from 'classnames'; import { getRoleKeyFromUrl } from '@jy-fe/business'; import { pagination, tableScroll, checkType, resizableColumns } from '@jy-fe/utils/es/joyowoUmi'; import XuiHeadLine from '../xui-head-line'; import XuiOperationList from '../xui-operation-list'; import XuiSearchHead from '../xui-search-head'; import XuiSearchBar from '../xui-search-bar'; import XuiSearchFrame from '../xui-search-frame'; import XuiResizableTh from '../xui-resizable-th'; import XuiResizableTd from '../xui-resizable-td'; import { SearchHeadHandles, NodeType as SearchHeadNodeType, } from '../xui-search-head/xui-search-head.d'; import { SearchBarHandles, NodeType as SearchBarNodeType, } from '../xui-search-bar/xui-search-bar.d'; import { SearchFrameHandles, NodeType as SearchFrameNodeType, } from '../xui-search-frame/xui-search-frame.d'; import { GetTableDataType, InitialStateType, XuiTablePageProps, XuiTablePageHandles, } from './xui-table-page.d'; import tableNoDataPNG from '../assets/table-no-data.png'; Spin.setDefaultIndicator(); const initialState: InitialStateType = { searchState: {}, loading: false, current: 1, pageSize: 30, total: 0, dataSource: [], selectedRowKeys: [], selectedRows: [], defaultValues: {}, disabledRowKeys: [], mandatoryRowKeys: [], }; const reducer = ( state: InitialStateType, action: { type: string; payload?: { [key: string]: any }; }, ) => { switch (action.type) { case 'update': return { ...state, ...action.payload }; default: throw new Error(); } }; const XuiTablePage: ForwardRefRenderFunction = ( { aid, aidMap, className: wrapperClassName, closePagination, columns = [], customNoData, defaultSearchValues = {}, extraContent, extraData = {}, extraHead, extraOptions = {}, id, isSearchCache = true, keepDefaultValuesOnReset, listType = 'table', maxWidth, openLocalPagination, operationColumnsConfig, paginationConfig, propDataSource, propLoading, renderBeforeTable, roleId, rowKey, rowSelection, searchComponentType = 'searchHead', searchNode = [], tableHeight = document.body.offsetHeight - 324, title, beforeInitRequest, disableSelection, mandatorySelection, onDisableSelection, onMandatorySelection, onSelectionChange, operationColumns, paginationCallback, paginationChange, processResponseData, processValues, request, }, ref, ) => { const className = 'xui-ant__table-page'; const searchHeadRef = useRef(null); const searchBarRef = useRef(null); const searchFrameRef = useRef(null); const curSearchComponentRaf = useMemo(() => { if (searchComponentType === 'searchBar') { return searchBarRef; } if (searchComponentType === 'searchHead') { return searchHeadRef; } return searchFrameRef; }, [searchComponentType]); const [stateColumns, setStateColumns] = useState([]); /** sessionStorage唯一标识符 */ const sessionStorageKey = useMemo(() => `${location.pathname}${id ? `:${id}` : ''}`, [id]); /** 需时间格式处理的字段关键字 */ const momentKeyword = useMemo(() => ['Time', 'Date', 'Month'], []); const [state, hookDispatch] = useReducer(reducer, { ...initialState, pageSize: paginationConfig?.pageSizeOptions?.[0] ? Number(paginationConfig?.pageSizeOptions?.[0]) : initialState.pageSize, }); const { searchState, loading, current, pageSize, total, dataSource, selectedRowKeys, selectedRows, defaultValues, disabledRowKeys, mandatoryRowKeys, } = state; const triggerDispatch = useCallback((payload: Partial) => { hookDispatch({ type: 'update', payload, }); }, []); /** 更新分页数据 */ useEffect(() => { const currentPageConfig = { current: paginationConfig?.current ?? current, pageSize: paginationConfig?.pageSize ?? pageSize, total: openLocalPagination ? propDataSource?.length : paginationConfig?.total ?? total, }; triggerDispatch(currentPageConfig); }, [ openLocalPagination, paginationConfig, propDataSource, current, pageSize, total, triggerDispatch, ]); /** 处理列表接口返回数据 */ const discopeResponseData = (data: any) => { const { pageNum, pageSize, list, total } = data || {}; return { list, pageNum, pageSize, total, }; }; /** * 获取列表数据 * params 接口调用额外传参 * holdSelected 保持勾选状态 */ const getTableData = useCallback( async extraPropsParams => { const { params = {}, isSearch = false, holdSelected = false, callback } = extraPropsParams || {}; if (!request) { return false; } triggerDispatch({ loading: true, }); let requestParams = {}; if (isSearch) { requestParams = { ...params, }; } else { requestParams = { ...searchState, ...params, }; } const extraParams = { ...extraData, }; Object.keys(extraParams).forEach(key => { const valueInRequestParams = requestParams[key]; let cover = false; if (checkType(valueInRequestParams) === 'Array' && valueInRequestParams.length > 0) { cover = true; } else if ( checkType(valueInRequestParams) === 'Object' && Object.keys(valueInRequestParams).length > 0 ) { cover = true; } else if ( checkType(valueInRequestParams) !== 'Array' && (valueInRequestParams || valueInRequestParams === 0) ) { cover = true; } if (cover) { extraParams[key] = valueInRequestParams; } }); const aidVal = (() => { if (aid) return aid; if (aidMap) return aidMap[getRoleKeyFromUrl()]; return ''; })(); const res: { status: number; data: { pageNum: number; pageSize: number; total: number; list: any[]; }; msg: string; } = await request( { pageNum: current, pageSize, ...requestParams, ...extraParams, }, { header: { rid: roleId || '', aid: aidVal, }, ...extraOptions, }, ); const { status, data, msg } = res; if (!status) { const { pageNum: newPageNum, pageSize: newPageSize, list = [], total: newTotal, } = processResponseData ? processResponseData(data) : discopeResponseData(data); const newList = list.map((item, index) => ({ ...item, serialNumer: (newPageNum - 1) * newPageSize + index + 1, })); triggerDispatch({ current: newPageNum, pageSize: newPageSize, dataSource: newList, total: newTotal, }); if (!holdSelected) { triggerDispatch({ selectedRowKeys: [], selectedRows: [], }); } if (disableSelection) { const currentDisabledRowKeys = newList .filter(l => disableSelection(l)) .map(item => item[rowKey]); triggerDispatch({ disabledRowKeys: currentDisabledRowKeys, }); callback && callback({ selectedRowKeys: !holdSelected ? [] : selectedRowKeys, selectedRows: !holdSelected ? [] : selectedRows, disabledRowKeys: currentDisabledRowKeys, mandatoryRowKeys, dataSource: newList || [], }); onSelectionChange && onSelectionChange( !holdSelected ? [] : selectedRowKeys, !holdSelected ? [] : selectedRows, ); } else if (mandatorySelection) { const newMandatoryRowKeys: React.ReactText[] = []; const newMandatoryRows: { [key: string]: any }[] = []; newList.forEach(l => { if (mandatorySelection(l)) { newMandatoryRowKeys.push(l[rowKey]); newMandatoryRows.push(l); } }); triggerDispatch({ mandatoryRowKeys: newMandatoryRowKeys, selectedRowKeys: newMandatoryRowKeys, selectedRows: newMandatoryRows, }); callback && callback({ selectedRowKeys: !holdSelected ? [] : newMandatoryRowKeys, selectedRows: !holdSelected ? [] : newMandatoryRows, disabledRowKeys, mandatoryRowKeys: newMandatoryRowKeys, dataSource: newList || [], }); onSelectionChange && onSelectionChange( !holdSelected ? [] : newMandatoryRowKeys, !holdSelected ? [] : newMandatoryRows, ); } else { callback && callback({ selectedRowKeys: !holdSelected ? [] : selectedRowKeys, selectedRows: !holdSelected ? [] : selectedRows, disabledRowKeys, mandatoryRowKeys, dataSource: newList || [], }); onSelectionChange && onSelectionChange( !holdSelected ? [] : selectedRowKeys, !holdSelected ? [] : selectedRows, ); } if (isSearchCache) { const cacheJSONStr = sessionStorage.getItem(sessionStorageKey); const cache = { ...(cacheJSONStr ? JSON.parse(cacheJSONStr) : {}), pageNum: newPageNum, pageSize: newPageSize, }; sessionStorage.setItem(sessionStorageKey, JSON.stringify(cache)); } } else if (msg) { message.warn(msg); } triggerDispatch({ loading: false, }); return true; }, [ aid, aidMap, extraData, extraOptions, isSearchCache, roleId, rowKey, current, disabledRowKeys, mandatoryRowKeys, pageSize, searchState, selectedRowKeys, selectedRows, sessionStorageKey, disableSelection, mandatorySelection, onSelectionChange, processResponseData, request, triggerDispatch, ], ); /** 操作table的分页器 */ const changeTablePagination = useCallback( (newCurrent, newPageSize) => { if (paginationChange) { paginationChange(newCurrent, newPageSize); } else if (paginationConfig?.onChange) { paginationConfig.onChange(newCurrent, newPageSize); } else { if (openLocalPagination) { triggerDispatch({ current: newCurrent, pageSize: newPageSize, }); } else { getTableData({ params: { pageNum: newCurrent, pageSize: newPageSize, }, isSearch: false, holdSelected: true, }); } if (paginationCallback) { paginationCallback(newCurrent, newPageSize); } } }, [ openLocalPagination, paginationConfig, getTableData, paginationCallback, paginationChange, triggerDispatch, ], ); /** 更新表格数据 */ const updateState = useCallback( (newState: Partial) => { triggerDispatch({ ...state, ...newState, }); }, [state, triggerDispatch], ); /** 筛选 */ const search = useCallback( (values: { [key: string]: any }, type: 'search' | 'reset') => { let searchValues = { ...values, }; if (type === 'reset' && keepDefaultValuesOnReset) { triggerDispatch({ defaultValues: { ...defaultSearchValues }, }); searchValues = { ...searchValues, ...defaultSearchValues, }; } if (processValues && checkType(processValues) === 'Function') { searchValues = processValues({ ...searchValues }); } triggerDispatch({ searchState: searchValues, }); getTableData({ params: { pageNum: 1, ...searchValues, }, isSearch: true, }); if (isSearchCache) { /** 组装sessionStorage */ sessionStorage.setItem( sessionStorageKey, JSON.stringify({ ...values, }), ); } }, [ defaultSearchValues, isSearchCache, keepDefaultValuesOnReset, sessionStorageKey, getTableData, processValues, triggerDispatch, ], ); const init = async (isAuto = false) => { /** 获取sessionStorage里的参数 */ const cacheJSONStr = sessionStorage.getItem(sessionStorageKey); const cacheParams = cacheJSONStr ? JSON.parse(cacheJSONStr) : {}; const entries = isSearchCache ? Object.entries(cacheParams) : []; /** 常规参数,value type is string */ let params = { ...(processValues && checkType(processValues) === 'Function' ? processValues({ ...defaultSearchValues }) : defaultSearchValues), }; /** _search, value type is object */ const originalSearchParams = { ...defaultSearchValues }; let searchParams = {}; if (entries.length > 0) { entries.forEach(searchParseEntry => { const [searchParseKey, searchParseValue] = searchParseEntry; let isDate = false; momentKeyword.forEach(element => { if (searchParseKey.includes(element)) { isDate = true; } }); if (isDate) { if (checkType(searchParseValue) === 'Array') { originalSearchParams[searchParseKey] = (searchParseValue as string[]).map(child => child ? dayjs(child) : undefined, ); } else { originalSearchParams[searchParseKey] = dayjs(searchParseValue as string); } } else { originalSearchParams[searchParseKey] = searchParseValue; } }); if (processValues && checkType(processValues) === 'Function') { /** 自定义处理数据 */ searchParams = processValues({ ...originalSearchParams }); } else { searchParams = { ...originalSearchParams }; } } triggerDispatch({ searchState: processValues && checkType(processValues) === 'Function' ? processValues({ ...originalSearchParams }) : originalSearchParams, defaultValues: originalSearchParams, }); let beforeInitRequestParams: any = {}; let ableInitRequest: any = true; if (beforeInitRequest) { const resultParams = await beforeInitRequest(); if (checkType(resultParams) === 'Object') { beforeInitRequestParams = resultParams; } if (checkType(resultParams) === 'Boolean') { ableInitRequest = resultParams; } } // 判断初始化是否调用接口 if ((isAuto && ableInitRequest) || !isAuto) { params = { ...params, ...searchParams, ...beforeInitRequestParams, }; /** 获取表格数据 */ await getTableData({ params }); } }; const clearSelectedRows = () => { triggerDispatch({ selectedRowKeys: [], selectedRows: [], }); }; const tableDataSource = useMemo(() => { if (propDataSource) { if (openLocalPagination) { return propDataSource.slice().splice((current - 1) * pageSize, pageSize); } return propDataSource; } return dataSource; }, [openLocalPagination, dataSource, propDataSource, current, pageSize]); useImperativeHandle(ref, () => ({ selectedRowKeys, selectedRows, searchState, dataSource, pageNum: current, pageSize, total, init, clearSelectedRows, updateState, getTableData, clearSearchParams: () => { if (curSearchComponentRaf.current) { curSearchComponentRaf.current.clear(); } }, resetSearchParams: (paramNameList: string[]) => { if (curSearchComponentRaf.current) { curSearchComponentRaf.current.resetState(paramNameList); } }, })); useEffect(() => { if (!propDataSource) { init(true); } }, []); useEffect(() => { const currentColumns = [...columns]; if (operationColumns) { const operateWidth = XuiOperationList.getWidth( () => operationColumns(searchState), searchState, operationColumnsConfig, ); if (operateWidth) { currentColumns.push({ title: '操作', dataIndex: '操作', fixed: 'right', width: operateWidth, render: (_, record: any) => ( operationColumns(record)} data={record} /> ), }); } } setStateColumns(currentColumns); }, [columns, operationColumnsConfig, searchState, operationColumns]); const { columns: newColumns, scroll } = useMemo(() => { const props = { oldColumns: resizableColumns(stateColumns, nextColumns => { setStateColumns(nextColumns); }), dataSource: propDataSource || dataSource, rowSelectionWidth: rowSelection ? 40 : 0, maxHeight: tableHeight, maxWidth, }; if (listType === 'drawer900') { props.maxWidth = 900 - 24 * 2; } if (listType === 'drawer600') { props.maxWidth = 600 - 24 * 2; } return tableScroll(props); }, [listType, maxWidth, propDataSource, rowSelection, tableHeight, dataSource, stateColumns]); const tableProps: { [key: string]: any } = { pagination: false, onRow: record => { if (mandatoryRowKeys.includes(record[rowKey])) { return { className: `${className}--tr-mandatory` }; } if (disabledRowKeys.includes(record[rowKey])) { return { className: `${className}--tr-disabled` }; } return {}; }, }; if (rowSelection) { tableProps.rowSelection = { columnWidth: 40, selectedRowKeys, fixed: true, hideSelectAll: disabledRowKeys.length > 0, onSelect: (record: any, selected: boolean, selectedRows: any) => { if (disabledRowKeys.includes(record[rowKey])) { onDisableSelection && onDisableSelection(record); return; } if (mandatoryRowKeys.includes(record[rowKey])) { onMandatorySelection && onMandatorySelection(record); return; } let newSelectedRowKeys: React.ReactText[] = []; let newSelectedRows: { [key: string]: any }[] = []; if (rowSelection?.type === 'radio') { newSelectedRowKeys = selectedRows.map(item => item[rowKey]).slice(); newSelectedRows = selectedRows.slice(); } else if (selected) { newSelectedRowKeys = selectedRowKeys.slice(); newSelectedRows = selectedRows.slice(); newSelectedRowKeys.push(record[rowKey]); newSelectedRows.push(record); triggerDispatch({ selectedRowKeys: newSelectedRowKeys, selectedRows: newSelectedRows, }); } else { selectedRows.forEach(row => { if (row[rowKey] !== record[rowKey]) { newSelectedRowKeys.push(row[rowKey]); newSelectedRows.push(row); } }); } triggerDispatch({ selectedRowKeys: newSelectedRowKeys, selectedRows: newSelectedRows, }); onSelectionChange && onSelectionChange(newSelectedRowKeys, newSelectedRows); }, onSelectAll: (selected: boolean, _: any, changeRows: { [key: string]: any }[]) => { const newSelectedRowKeys: React.ReactText[] = []; const newSelectedRows: { [key: string]: any }[] = []; if (selected) { const mergeRows = selectedRows.concat(changeRows); const mergeRowKeys = Array.from(new Set(mergeRows.map(row => row[rowKey]))); mergeRows.forEach(row => { const currentKey = row[rowKey]; if (mergeRowKeys.includes(currentKey) && !disabledRowKeys.includes(currentKey)) { newSelectedRowKeys.push(currentKey); newSelectedRows.push(row); mergeRowKeys.splice(mergeRowKeys.indexOf(currentKey), 1); } }); } else { const removeRowKeys = changeRows.map(row => row[rowKey]); selectedRows.forEach(row => { if (!removeRowKeys.includes(row[rowKey]) || mandatoryRowKeys.includes(row[rowKey])) { newSelectedRowKeys.push(row[rowKey]); newSelectedRows.push(row); } }); } triggerDispatch({ selectedRowKeys: newSelectedRowKeys, selectedRows: newSelectedRows, }); onSelectionChange && onSelectionChange(newSelectedRowKeys, newSelectedRows); }, ...(typeof rowSelection === 'boolean' ? {} : rowSelection), }; } const renderSearchComponent = useCallback(() => { const hasSearchNode = searchNode && searchNode.length > 0; const component = (() => { if (searchComponentType === 'searchBar') { return ( ); } if (searchComponentType === 'searchHead') { return ( ); } return ( ); })(); return hasSearchNode ? component : null; }, [curSearchComponentRaf, defaultValues, searchComponentType, searchNode, search]); const isCustomNoData = useMemo(() => customNoData && tableDataSource.length === 0, [ customNoData, tableDataSource, ]); return (
{title && } {extraHead} {renderSearchComponent()}
{renderBeforeTable}
{(loading || propLoading) && (
)} record[rowKey]} bordered loading={false} columns={newColumns} dataSource={tableDataSource} scroll={scroll} {...tableProps} components={{ header: { cell: XuiResizableTh, }, body: { cell: XuiResizableTd, }, }} />
{!closePagination && !!total && ( )}
{isCustomNoData && (
{(customNoData as { img?: JSX.Element }).img || }
{(customNoData as { tip?: string }).tip || '暂无数据'}
)} ); }; export default React.forwardRef(XuiTablePage);