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 qs from 'qs'; 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, XuiListPageProps, XuiListPageHandles, } from './xui-list-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 XuiListPage: ForwardRefRenderFunction = ( { className: wrapperClassName, roleId, aid, aidMap, rowKey, title, listType = 'table', tableHeight = document.body.offsetHeight - 324, extraHead, renderBeforeTable, extraData = {}, extraOptions = {}, keepDefaultValuesOnReset, searchNode = [], defaultSearchValues = {}, columns = [], operationColumnsConfig, extraContent, rowSelection, propDataSource, openLocalPagination, closePagination, paginationConfig, searchComponentType = 'searchHead', maxWidth, customNoData, mandatorySelection, onMandatorySelection, disableSelection, onDisableSelection, onSelectionChange, beforeInitRequest, processResponseData, operationColumns, request, processValues, paginationCallback, paginationChange, onRedirect, propLoading, hideResetButton = false, stopRequest, }, ref, ) => { const className = 'xui-ant__list-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([]); /** 分页字段 */ const pagingKeys = useMemo(() => ['pageNum', 'pageSize', '_pageNum', '_pageSize'], []); /** 需时间格式处理的字段关键字 */ const momentKeyword = useMemo(() => ['Time', 'Date'], []); 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, propDataSource, paginationConfig, triggerDispatch, current, pageSize, total, ]); /** 处理列表接口返回数据 */ 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 || {}; let requestParams = {}; if (isSearch) { requestParams = { ...params, }; } else { requestParams = { ...searchState, ...params, }; } /** 调用接口之前,判断筛选条件是否符合要求,searchParams为筛选条件, * 返回true表示筛选条件不符合要求,不进行列表接口调用,并且列表list置为空 */ if (stopRequest) { if (stopRequest(requestParams)) { triggerDispatch({ current: 1, pageSize: 30, dataSource: [], total: 0, }); return; } } if (!request) { return false; } triggerDispatch({ loading: true, }); 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 (onRedirect) { /** 组装url search值 */ let urlParams = qs.parse(location.search, { ignoreQueryPrefix: true }); urlParams = { ...urlParams, _pageNum: String(newPageNum), _pageSize: String(newPageSize), }; const searchStr = qs.stringify(urlParams, { addQueryPrefix: true }); onRedirect(`${location.pathname}${searchStr}`); } } else if (msg) { message.warn(msg); } triggerDispatch({ loading: false, }); return true; }, [ request, triggerDispatch, extraData, current, pageSize, roleId, aid, aidMap, extraOptions, searchState, disableSelection, mandatorySelection, onRedirect, selectedRowKeys, selectedRows, mandatoryRowKeys, onSelectionChange, rowKey, disabledRowKeys, processResponseData, stopRequest, ], ); /** 操作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); } } }, [ paginationChange, paginationConfig, openLocalPagination, paginationCallback, triggerDispatch, getTableData, searchState, ], ); /** 更新表格数据 */ const updateState = useCallback( (newState: Partial) => { triggerDispatch({ ...state, ...newState, }); }, [state, triggerDispatch], ); /** 筛选 */ const search = useCallback( (values: { [key: string]: any }, type: 'search' | 'reset') => { let params = { ...values, }; if (type === 'reset' && keepDefaultValuesOnReset) { triggerDispatch({ defaultValues: { ...defaultSearchValues }, }); params = { ...params, ...defaultSearchValues, }; } if (processValues && checkType(processValues) === 'Function') { params = processValues(params); } triggerDispatch({ searchState: params, }); const urlParams = qs.parse(location.search, { ignoreQueryPrefix: true }); const urlParamsEntries = Object.entries(urlParams); /** 获取除了分页和筛选外的参数 */ const searchRequestParams = {}; urlParamsEntries .filter( (entry: [string, unknown]) => !(pagingKeys.includes(entry[0]) || entry[0] === '_search'), ) .forEach((entry: [string, unknown]) => { const [key, value] = entry; searchRequestParams[key] = value; }); getTableData({ params: { ...params, pageNum: 1, ...searchRequestParams, }, isSearch: true, }); if (onRedirect) { /** 组装url search值 */ const entries = Object.entries(values); const searchNodeParams = {}; if (entries.length > 0) { entries.forEach((entry: [string, any]) => { const [key, value] = entry; searchNodeParams[key.replace('_', '')] = value; }); } let result = { ...searchRequestParams, }; urlParamsEntries .filter((entry: [string, unknown]) => pagingKeys.includes(entry[0])) .forEach((entry: [string, unknown]) => { const [key, value] = entry; result[key] = value; }); if (Object.keys(searchNodeParams).length > 0) { result = { ...result, _search: JSON.stringify(searchNodeParams), }; } const searchStr = qs.stringify(result, { addQueryPrefix: true }); onRedirect(`${location.pathname}${searchStr}`); } }, [ defaultSearchValues, pagingKeys, processValues, getTableData, onRedirect, triggerDispatch, keepDefaultValuesOnReset, ], ); const init = async (isAuto = false) => { /** 获取浏览器地址栏上的参数 */ const urlParams = qs.parse(location.search, { ignoreQueryPrefix: true }); const keys = Object.keys(urlParams); const entries = Object.entries(urlParams); /** 常规参数,value type is string */ let params = {}; /** _search, value type is object */ let originalSearchParams = {}; let searchParams = {}; if (onRedirect) { if (!keys.includes('_search')) { params = { ...params, ...(processValues && checkType(processValues) === 'Function' ? processValues({ ...defaultSearchValues }) : defaultSearchValues), }; originalSearchParams = { ...defaultSearchValues }; } if (entries.length > 0) { entries.forEach((entry: [string, unknown]) => { const [key, value] = entry; if (pagingKeys.includes(key)) { params[key.replace('_', '')] = Number(value); } else if (key === '_search') { const searchParse = JSON.parse(value as any); const searchParseEntries = Object.entries(searchParse); searchParseEntries.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, }); } else { params = { ...params, ...(processValues && checkType(processValues) === 'Function' ? processValues({ ...defaultSearchValues }) : defaultSearchValues), }; originalSearchParams = { ...defaultSearchValues }; 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); }, [dataSource, stateColumns, rowSelection, propDataSource, maxWidth, tableHeight, listType]); 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) => { 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 (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); }, }; } 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(XuiListPage);