import React, { useMemo, useCallback, useState, useEffect } from 'react' import moment from 'moment' import _, { isNumber } from 'lodash' import { Tooltip, Checkbox } from 'antd' // import { measureScrollbar } from 'rc-table/es/utils'// 新版本的rc-table没有这个方法 import measureScrollbar from './_rc-table-utils' import CSS from 'csstype' import { RowSelection, Record, RowKey } from './index' function transcode(value, codeData) { const index = _.findIndex(codeData, { value }); if (index < 0) return "-"; return codeData[index].label } export function renderColumnItem(item, text, record, index) { const wrappedCilItem = { showTip: false, placement: 'rightBottom', ...item } let _text = text; _text = wrappedCilItem.codeData && Array.isArray(wrappedCilItem.codeData) ? transcode(_text, wrappedCilItem.codeData) : _text; _text = wrappedCilItem.isDate ? (wrappedCilItem.format ? moment(_text).format(wrappedCilItem.format) : moment(_text).format('YYYY-MM-DD')) : _text if (wrappedCilItem.render && typeof wrappedCilItem.render === 'function') { _text = wrappedCilItem.render(_text, record, index) } if (wrappedCilItem.showTip) return ( // showTip开启才显示tip {_text} ) return _text } // columnWidth 自定义列表选择框宽度 string|number 60px // columnTitle 自定义列表选择框标题 string|React.ReactNode - // fixed 把选择框列固定在左边 boolean - // getCheckboxProps 选择框的默认属性配置 Function(record) - // hideDefaultSelections 自定义选择项时去掉『全选』『反选』两个默认选项 boolean false // selectedRowKeys 指定选中项的 key 数组,需要和 onChange 进行配合 string[] [] // selections 自定义选择项 配置项, 设为 true 时使用默认选择项 object[]|boolean true // type 多选/单选,checkbox or radio string checkbox // onChange 选中项发生变化时的回调 Function(selectedRowKeys, selectedRows) - // onSelect 用户手动选择/取消选择某行的回调 Function(record, selected, selectedRows, nativeEvent) - // onSelectAll 用户手动选择/取消选择所有行的回调 Function(selected, selectedRows, changeRows) - // onSelectInvert 用户手动选择反选的回调 Function(selectedRows) - // 获取级联选择的key值 function getSubKeys(record) { const keys = [] if (record.children && record.children.length) { record.children.reduce((keys, item, index) => { const key = item["g-row-key"] const subKeys = getSubKeys(item) keys.push(key, ...subKeys) return keys }, keys) } return keys } const defaultColumnWidth = 35 // 勾选列的宽度 const useRowSelectionProps = function useRowSelectionProps(rowSelection: RowSelection) { const { // 用户传递的key,所有计算以此为准 selectedRowKeys: originSelectedKeys = [], columnWidth = defaultColumnWidth, showFooterSelection = true, type = "checkbox", preventDefault = true, getCheckboxProps } = rowSelection || {} const isMultiple = type === 'checkbox' return { originSelectedKeys, columnWidth, showFooterSelection, preventDefault, getCheckboxProps, isMultiple } } export const useRowSelection = (rowSelection: RowSelection, dataSource: Array, bordered: boolean): [RowSelection, (record: T) => void, null | React.ReactElement] => { const getFlatRecords = useCallback( (list) => { return list.reduce( (records: T[], record: T) => { records.push(record) if (record.children && record.children.length) { records.push.apply(records, getFlatRecords(record.children)) } return records }, [] ) }, [] ) const [flatRecords, setFlatRecords] = useState([] as T[]) useEffect( () => { setFlatRecords(getFlatRecords(dataSource)) }, [dataSource] ) const { originSelectedKeys, columnWidth, showFooterSelection, preventDefault, getCheckboxProps: originGetCheckoutProps, isMultiple } = useRowSelectionProps(rowSelection) const [selectedRowKeys, setselectedRowKeys] = useState(originSelectedKeys) // 计算选中行 useEffect( () => { // 去掉g-row-key const rows = flatRecords.filter(record => selectedRowKeys.includes(record["g-row-key"])) if (typeof _.get(rowSelection, 'onChange') === 'function') { const pureRows = getPureList(rows) rowSelection.onChange(selectedRowKeys, pureRows) } }, [selectedRowKeys] ) // 计算最终选中的key、每次计算都用rowSelection.selectedRowKeys来获得最新的keys const getSelectedKeys = useCallback( (keys: string[], selected: boolean): string[] => { if (selected) { return _.union(originSelectedKeys, keys) } return _.difference(originSelectedKeys, keys) }, [originSelectedKeys] ) // 勾选或者点击行选中 const onSelectRow = useCallback( (record: T, selected: boolean) => { if (!rowSelection) return const key = record["g-row-key"] // 当前节点的key let subKeys = [] if (isMultiple) { // preventDefault表示不选中子节点 if (!preventDefault && record.children && record.children.length) { // 也可以不用判断,只是对于没有子节点的节点来说不用执行下面的代码 subKeys = getSubKeys(record) // 子节点的key } const computedKeys = getSelectedKeys([key, ...subKeys], selected) setselectedRowKeys(computedKeys) } else { setselectedRowKeys([key]) } if (typeof rowSelection.onSelect === 'function') { const rows: T[] = isMultiple ? getFlatRecords([record]) : record // 获取纯净数据 const pureRows = getPureList(rows) rowSelection.onSelect(pureRows, selected) } }, [rowSelection], ) // 设置选中的行,供外部使用 // 获取到的实际时行数据,会带有children,但是不会有非enumerable属性 // 为了统一,需要omit掉 const setKeys = useCallback( (record: T): void => { if (typeof rowSelection !== 'undefined') { const selected = !originSelectedKeys.includes(record["g-row-key"]) onSelectRow(record, selected) } }, [originSelectedKeys, rowSelection] ) // 点击选择框 // 默认的选择功能 // 不会带有children, 但有非enumerable属性 const onSelect = useCallback( (...args) => { onSelectRow(...args as [T, boolean]); }, [rowSelection] ) const onChange = useCallback( // 防止onChange多次调用,所以加一个拦截 (...args) => { const [rowKeys] = args setselectedRowKeys(rowKeys) }, [], ) const getCheckboxProps = useCallback( (record) => { let props = { // className: '123' } if (typeof originGetCheckoutProps === 'function') { const originProps = originGetCheckoutProps(record) if (_.isPlainObject(originProps)) { props = { ...props, ...originProps } } } return props }, [originGetCheckoutProps], ) const footerselectionstyle = useMemo(() => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 8px 0 4px', marginLeft: '-4px', width: parseInt(columnWidth as string) + 4, visibility: (isMultiple ? 'show' : 'hidden') as any, borderRight: bordered ? '1px solid transparent' : undefined }), [isMultiple, columnWidth, bordered]) const [indeterminate, footerselectionchecked] = useMemo(() => ([originSelectedKeys.length > 0 && originSelectedKeys.length < flatRecords.length, originSelectedKeys.length > 0]), [originSelectedKeys, flatRecords]) const onFooterSelectionChange = useCallback((e) => { if (indeterminate === footerselectionchecked) { //全选 const keys = _.map(flatRecords, "g-row-key") setselectedRowKeys(keys) } else { // 取消全选 setselectedRowKeys([]) } }, [indeterminate, footerselectionchecked, flatRecords]) const footerselection = useMemo(() => { if (!showFooterSelection || !isMultiple) return null return (
) }, [showFooterSelection, indeterminate, originSelectedKeys, onFooterSelectionChange, isMultiple]) if (!rowSelection) return [null, setKeys, null] rowSelection.columnWidth = columnWidth rowSelection.getCheckboxProps = getCheckboxProps return [ { ...rowSelection, onSelect, onChange }, setKeys, footerselection ] } export const usePagination = (pagination, rowselection, length = 0) => { const showTotal = useCallback((total, range) => total > 0 ? `第${range[0]} - ${range[1]}条,共${total}条` : '', []) const [defaultPagination] = useState({ //分页信息 defaultPageSize: 50, defaultCurrent: 1, pageSizeOptions: ["20", "50", "80", "120"], showSizeChanger: true, //是否可以改变 pageSize showQuickJumper: true, //是否可以快速跳转至某页 showTotal, total: length }) const computedPagination = useMemo( () => { if (pagination) { if (_.isPlainObject(pagination)) return { ...defaultPagination, ...pagination } return defaultPagination } return false }, [pagination] ) const onChange = useCallback((...args) => { const onChange = _.get(computedPagination, 'onChange') onChange && onChange(...args) }, [computedPagination]) if (pagination) { return { ...computedPagination, onShowSizeChange: onChange, onChange } } } export const setStorageWidth = (ele, tableKey, key) => { const { width } = ele.getBoundingClientRect() if (tableKey) { const storage = window.localStorage.getItem(tableKey) const obj = (storage && /{.*}/.test(storage)) ? JSON.parse(storage) : {} let storageWidth: object = _.get(obj, 'width', {}) storageWidth[key] = width obj.width = storageWidth window.localStorage.setItem(tableKey, JSON.stringify(obj)) } } export const getStorageWidth = (key) => { if (key) { const obj = window.localStorage.getItem(key) if (obj && /{.*}/.test(obj)) { return _.get(JSON.parse(obj), 'width', {}) } return {} // if (!_.isNil(width) && Number.isFinite(Number(width))) { // 从缓存中取得数据 // return Number(width) // } } } /** * * @param {HTMLTbaleElement} table 主table */ export const toggleFixedTable = (tableParent: HTMLTableElement, ...args): void => { // 找打content区域 const tablecontent = _.get(tableParent, 'parentElement.parentElement') if (!tablecontent) return const left = tablecontent.querySelector('.ant-table-fixed-left') const right = tablecontent.querySelector('.ant-table-fixed-right') toggleFixedTableScroll(left, ...args) toggleFixedTableScroll(right, ...args) } export const setStyle = (dom, text) => { if (!dom) return requestAnimationFrame(() => { const { cssText } = dom.style dom.style.cssText = cssText ? `${cssText};${text}` : text }) } export function getStyleText(p) { return isNumber(p) ? p + "px" : p } // 在有固定列的情况下,如果主table的宽度小于容器宽度要隐藏滚动条的占位 /** * * @param {*} fix 固定列容器 * @param {*} hide 隐藏还是显示 */ export const toggleFixedTableScroll = (fix, ...args) => { const [scrollY, scrollX, hasScrollY] = args const scrollbarmeasure = measureScrollbar({ direction: 'horizontal', prefixCls: 'antd' }) if (fix) { const outer = fix.querySelector('.ant-table-body-outer') const inner = fix.querySelector('.ant-table-body-inner') // setStyle(inner, "overflow-y: hidden") // scrollX --- 设置outer的margin-bottom属性为负值 // scrollY --- 设置inner的overflow-x属性为scroll if (hasScrollY) { if (scrollY) { setStyle(inner, `overflow-y: scroll`) if (scrollX) { // 负值保证主table在拖拽的时候不会卡住 setStyle(outer, `margin-bottom: -${scrollbarmeasure}px`) setStyle(inner, `overflow-x: scroll`) } else { setStyle(outer, `margin-bottom: 0`) setStyle(inner, `overflow-x: hidden`) } } else { setStyle(inner, `overflow-y: hidden`) if (scrollX) { setStyle(outer, `margin-bottom: -${scrollbarmeasure}px`) setStyle(inner, `overflow-x: hidden`) } else { setStyle(outer, `margin-bottom: 0`) setStyle(inner, `overflow-x: hidden`) } } } else { // scrollY一定时false setStyle(inner, `overflow: hidden`) // 这时候不需要设置负margin给滚动条让位 setStyle(outer, `margin-bottom: 0`) } const table = inner.querySelector('table') setTableBorderBottom(table, !scrollY || scrollY && scrollX) } } export const switchIndex = ([...list], from, to) => { list.splice(to < 0 ? list.length + to : to, 0, list.splice(from, 1)[0]); return list } // 根据内容高度计算是否显示border-bottom export const setMainTableBorder = (table, headerFixed, dataLength) => { let showBorderBottom = false // 非固定头部的table,并且没有数据的时候,设置一个border-bottom // 防止被placeholder if (!headerFixed) { if (!dataLength) showBorderBottom = true } else { const parent = _.get(table, 'parentElement') if (!parent) return const scrollHeight = parent.offsetWidth > parent.clientWidth; // 出现纵向滚动 const scrollWidth = parent.offsetHeight > parent.clientHeight // 出现横向滚动 showBorderBottom = !scrollHeight || scrollHeight && scrollWidth } setTableBorderBottom(table, showBorderBottom) } const setTableBorderBottom = (table, show) => { setStyle(table, show ? 'border-bottom: 1px solid rgba(126,126,126,0.3)' : 'border-bottom: 0'); } // 定义不可枚举属性 const defineProperty = function defineProperty(obj: Object, key: string, value: any) { Object.defineProperty(obj, key, { writable: false, value }) } const defineProperties = function defineProperties(obj: Object, data: any) { for (const key of Object.keys(data)) { data[key] = { writable: false, value: data[key] } } Object.defineProperties(obj, data) } export const originKey = "__origin" /** * 计算序号和可展开rowKey */ export const computeIndexAndRowKey = function computeIndexAndRowKey(list: T[], computedRowKey): [T[], string[]] { const rowKey: string[] = []; let index = 0 const items: any[] = [ { root: true, children: list } ] while (items.length) { const item = items.shift() if (!item.root) { item["g-index"] = ++index item["g-row-key"] = computedRowKey(item, index) } if (!_.isUndefined(item.children)) { if (!item.root) rowKey.push(item["g-row-key"]) if (item.children.length) { items.unshift(...item.children) } } } return [list, rowKey] } export const cloneDatasource = function cloneDatasource(dataSource: T[]): T[] { return dataSource.map((item: T) => { const freezeObj = _.cloneDeep(item); Object.freeze(freezeObj) const copyItem = _.cloneDeep(item); Object.defineProperty(copyItem, originKey, { value: freezeObj, writable: false, configurable: false, enumerable: true }) if (_.get(copyItem, "children.length")) { copyItem.children = cloneDatasource(copyItem.children) } return copyItem }) } /** * 计算diff数据 * @param {Array} oldList 编辑之前的原始数据 * @param {Array} newList 编辑之后的列表 * @param {function} computedRowKey 计算数据的key * @param {boolean} isTree 是否是树状结构 */ export const diffList = (oldList: T[], newList: T[], isTree = false, addList = [], delList = [], modifyList = []) => { // console.log('整个列表', newList) // 2、计算新增节点 // console.time('计算新增节点') newList.reduce((result, newItem) => { const isAdd = !newItem.hasOwnProperty(originKey) if (isAdd) { pushChildrenToList(result[0], [newItem]) } else { const oldItem = newItem[originKey] let [oi, ni] = [oldItem, newItem]; if (isTree) { oi = _.omit(oldItem, 'children') as T; ni = _.omit(newItem, 'children') as T // 比较子树 const oldChildLength = _.get(oldItem, 'children.length') const newChildLength = _.get(newItem, 'children.length') if (oldChildLength || newChildLength) { // 进入子级比较 const [subAddList, subDelList, subModifyList] = diffList( _.get(oldItem, 'children', []), _.get(newItem, 'children', []), isTree ) addList.push.apply(addList, subAddList) result[0].push.apply(result[0], subDelList) result[1].push.apply(result[1], subModifyList) } } // // 比较数据本身 oi = getPureRecord(oi) ni = getPureRecord(ni) if (!_.isEqual(oi, ni)) { result[1].push(ni) } } return result }, [addList, modifyList]) // console.timeEnd('计算新增节点') // 3、计算删除节点 // 4、计算修改节点 // console.time('计算删改节点') oldList.reduce((list, oldItem) => { const pureOld = getPureRecord(oldItem) const isDelete = newList.every(newItem => !_.isEqual(getPureRecord(newItem[originKey]), pureOld)) if (isDelete) { // 删除的数据 pushChildrenToList(list, [oldItem]) } return list }, delList) // console.timeEnd('计算删改节点') return [addList, delList, modifyList] } const pushChildrenToList = (list, items) => { items.forEach(item => { list.push(getPureRecord(item)) if (_.get(item, "children.length")) { pushChildrenToList(list, item.children) } }) } /** * 获取dataindex对应的index * 用于列缩放是找到对应的col元素,来设置实际的宽度 * @param {*} cols 列数据 */ export const getComputedColIndex = (cols): string[] => { // console.time('计算columnIndex') const colIndexs = [] const inner = (subCols) => { subCols.forEach((subCol) => { const childrenLength = _.get(subCol, "children.length") if (childrenLength) { inner(_.get(subCol, "children", [])) } else { colIndexs.push(subCol.dataIndex) } }) } inner(cols) return colIndexs } /** * @param {Array} list * @param {number} withIndex * @param {number} parent */ export const computeIndex = function computeIndex(list: Array, expandRowKeys = [], virtualScroll: boolean): [T[], string[], T[], string[]] { // console.time('计算序号') // 因为需要对数据的children进行操作 const copyList = _.cloneDeep(list); // 所有数据的key const renderRowKeys: string[] = [] // 所有数据平铺结构 const tilingList: T[] = []; // 所有可以展开的数据key const expandableRowKeys: string[] = [] const items = [] items.push({ nestLevel: 0, node: { 'g-root': true, children: copyList } }) while (items.length) { const { node, nestLevel, "g-parent-row-key": gprk, "g-parent": gp } = items.shift(); let rowKey = node["g-row-key"] if (!node['g-root']) { defineProperty(node, "g-level", nestLevel) // 关联父级节点 if (gprk !== undefined) { defineProperties(node, { ["g-parent-row-key"]: gprk, ["g-parent"]: gp, }) } else { // 根节点 defineProperty(node, "g-parent-row-key", 'root') } renderRowKeys.push(rowKey) tilingList.push(node) } if (node.children !== undefined) { if (_.get(node, "children.length")) { if ( !virtualScroll || node['g-root'] || expandRowKeys.includes(rowKey) ) { items.unshift(...node.children.map(child => ({ node: child, nestLevel: nestLevel + 1, "g-parent-row-key": rowKey, "g-parent": node }))); } else if (virtualScroll) { // 虚拟滚动下删除掉children node.children = [] } } if (!node['g-root']) { expandableRowKeys.push(rowKey) } } } // console.timeEnd('计算序号') return [list, renderRowKeys, tilingList, expandableRowKeys] } /** * 计算虚拟滚动有效数据、防止出现末页数据不足以全部占据table内容区域,而出现底部空白 * @param list * @param start * @param length */ export const getListRange = function getListRange(list: T[], start: number, length: number): T[] { // list.slice(start, start + length); const count = list.length; return list.slice( Math.min(start, count - length), start + length ) } export const getVirtualList = function getVirtualList(start: number, length: number, renderRowKeys: string[], list: Array): Array { // const keys = renderRowKeys.slice(start, start+length); // 拟定要去掉的属性 let result = getListRange(list, start, length); if (!result.length) return result do { const { root = [], ...groups } = _.groupBy(result, 'g-parent-row-key') const entries = Object.entries>(groups).reverse() for (const [parentRowKey, nodes] of entries) { let seted = false; for (const item of root) { if (String(item['g-row-key']) === parentRowKey) { if (!item.children || !item.children.length) { item.children = nodes } else { const idsGroup = _.groupBy(nodes, 'g-row-key') item.children = item.children.map(subItem => _.get(idsGroup[subItem["g-row-key"]], '0', subItem)) } seted = true; break; } } // 在root中没有找到父级节点 if (!seted) { const gp = nodes[0]["g-parent"] gp.children = nodes; root.unshift(gp) } } result = root } while (!result.every(item => item["g-parent-row-key"] === 'root')) return result } // 获取纯净数据 export const getPureRecord = function getPureRecord(record: T, exclude: string[] = []): T { let omitPropo = ["g-parent", "g-parent-row-key", "g-level", "children", "g-row-key", "g-index", originKey] if (exclude.length) { omitPropo = _.difference(omitPropo, exclude) } return _.omit(record, omitPropo) } export const getPureList = function getPureList(list: T[]): T[] { return list.map(item => getPureRecord(item)) }