import React, { Component } from "react"; import { Card, Tree, Input, Checkbox, Icon } from "antd"; import { cloneDeep } from "lodash"; import { throttle } from 'jad-tool' import { is, List, Map } from 'immutable'; import { RenderToolTip } from '../Tips/Tips'; import { OptionType, PropsKeyValue, transform2TreeData, transform2FlatData, initOptions, getCheckedTreeOptions, updateOption } from './utils' const { TreeNode } = Tree; const { Search } = Input; type State = { options: OptionType[], optionsFlat: OptionType[], checkedOptions: OptionType[], // 根据 checkedKeys 从 options 过滤到的数据,用于 selected-tree 的展示 checkedKeys: any[], expandedKeys: any[], expandedKeysChecked: any[], searchValue: string, autoExpandParent: boolean, autoExpandParentChecked: boolean, checkedAllStatus: boolean }; type TreeSelectSearchProps = { options: OptionType[], // options 与 options二选一 optionsType: 'tree' | 'flat', // 数据源的数据格式类型:tree 树状有 children;flat 二维数组,有pid需要拿到后转换成;默认 flat value?: any[], disabled?: boolean, keyValue?: PropsKeyValue, onChange?: (values: any[]) => void, rootPid?: string | number, onChangeCheckedStrategy?: 'CHILD' | 'PARENT' | 'ALL', // 默认 CHILD [propName: string]: any, } export class TreeSelectSearch extends Component{ state: State; keyValue = { propPValue: 'pid', propValue: 'id', propName: 'name', } initOptions = (options: OptionType[], checkedKeys: any[]) => { return initOptions.bind(this)(options, checkedKeys) } initPropOptions = []; constructor(props: TreeSelectSearchProps) { super(props); if (props['keyValue']) { this.keyValue = { propPValue: props['keyValue']['propPValue'] || this.keyValue.propPValue, propValue: props['keyValue']['propValue'] || this.keyValue.propValue, propName: props['keyValue']['propName'] || this.keyValue.propName, } } const clonedOptions = cloneDeep(props['options']); this.initPropOptions = props['optionsType'] === 'tree' ? clonedOptions : transform2TreeData(clonedOptions, this.keyValue, props['rootPid']); // 默认 flat const initCheckedKeys = props.value || []; // 跟 onChangeCheckedStrategy 有关,默认是 "ALL",接受"PARENT"/"CHILD" const { options, optionsFlat, checkedOptions, checkedAllStatus } = this.initOptions(cloneDeep(this.initPropOptions), initCheckedKeys); this.state = { options: options, optionsFlat: optionsFlat, checkedOptions: checkedOptions, checkedKeys: initCheckedKeys, checkedAllStatus: checkedAllStatus, expandedKeys: [], expandedKeysChecked: initCheckedKeys, searchValue: "", autoExpandParent: true, autoExpandParentChecked: true, }; this.onSearch = this.onSearch.bind(this) this.onChange = this.onChange.bind(this) } componentDidMount() { this.updateRenderLoopTreeNodeCache(this.state.searchValue); } shouldComponentUpdate(nextProps, nextState) { if ((is(Map(nextProps), Map(this.props)) && is(Map(nextState), Map(this.state)))) return false else return true } /** * 这些场景需要有效触发 UNSAFE_componentWillReceiveProps 更新 * 1. options * 2. value */ UNSAFE_componentWillReceiveProps(nextProps) { const { options: nextPropsOptions, value: nextPropsValue, optionsType, rootPid } = nextProps; const { options: thisPropsOptions } = this.props; const { checkedKeys } = this.state; switch (optionsType) { // 默认 flat case 'tree': if (!is(List(nextPropsOptions), List(thisPropsOptions))) { // props.options 是否一致,不一致时需要更新this.initPropOptions 及 state 中的值 this.initPropOptions = cloneDeep(nextPropsOptions); // 默认 flat this.propsUpdateStateOptionsAndRenderLoopTreeNodeCache(cloneDeep(this.initPropOptions), nextPropsValue) } else if (!is(List(nextPropsValue), List(checkedKeys))) { // props.value 与 state.checkedKeys 是否一致 this.propsUpdateStateOptionsAndRenderLoopTreeNodeCache(cloneDeep(this.initPropOptions), nextPropsValue) } break; default: if (!is(List(nextPropsOptions), List(thisPropsOptions))) { // props.options 是否一致,不一致时需要更新this.initPropOptions 及 state 中的值 this.initPropOptions = transform2TreeData(cloneDeep(nextPropsOptions), this.keyValue, rootPid); // 默认 flat this.propsUpdateStateOptionsAndRenderLoopTreeNodeCache(cloneDeep(this.initPropOptions), nextPropsValue) } else if (!is(List(nextPropsValue), List(checkedKeys))) { // props.value 与 state.checkedKeys 是否一致 this.propsUpdateStateOptionsAndRenderLoopTreeNodeCache(cloneDeep(this.initPropOptions), nextPropsValue) } break; } } updateStateOptions = (options: OptionType[], checkedKeys: any[]) => { const { options: newOptions, optionsFlat: newOptionsFlat, checkedOptions: newCheckedOptions, checkedAllStatus: newCheckedAllStatus } = this.initOptions(options, checkedKeys) this.setState({ checkedKeys: checkedKeys, options: newOptions, optionsFlat: newOptionsFlat, checkedOptions: newCheckedOptions, checkedAllStatus: newCheckedAllStatus, expandedKeysChecked: checkedKeys, autoExpandParentChecked: true }, this.onChange) } propsUpdateStateOptionsAndRenderLoopTreeNodeCache = (options: OptionType[], checkedKeys: any[]) => { this.updateStateOptions(options, checkedKeys) this.updateRenderLoopTreeNodeCache(this.state.searchValue); } // onCheck 方法中需要 // checkedKeys 与 optionsFlat过滤 checked === true 的一致(如果不一致说明前面有问题) onFilterCheckedKeysCheckedStrategy = (checkedKeys: any[], optionsFlat: OptionType[]) => { const { propValue } = this.keyValue; const { onChangeCheckedStrategy } = this.props; if (!checkedKeys || !checkedKeys.length) return [] else { switch (onChangeCheckedStrategy) { case 'ALL': return checkedKeys; case 'PARENT': const parentCheckedKeys = optionsFlat.filter(item => item['checked'] && (!item['parent'] || (item['parent'] && !item['parent'].checked))).map(e => e[propValue]) return parentCheckedKeys; case 'CHILD': default: const childCheckedKeys = optionsFlat.filter(item => item['checked'] && (!(item['children'] && item['children'].length))).map(e => e[propValue]) return childCheckedKeys; } } } // onCheckAll、onClear需要 onFindCheckedKeysCheckedStrategy = (optionsFlat: OptionType[], isALLChecked?: boolean) => { const { propValue } = this.keyValue; const { onChangeCheckedStrategy } = this.props; let checkedKeys = [] if (isALLChecked) { // 全选中 switch (onChangeCheckedStrategy) { case 'ALL': // 所有节点 checkedKeys = optionsFlat.map(item => item[propValue]); break; case 'PARENT': // 跟节点 checkedKeys = optionsFlat.filter(item => !item['parent']).map(item => item[propValue]) break; case 'CHILD': // 底层节点 default: checkedKeys = optionsFlat.filter(item => !(item['children'] && item['children'].length)).map(item => item[propValue]) break; } } else { // 非全选中,要过滤 checked 属性 switch (onChangeCheckedStrategy) { case 'ALL': // 所有节点 checkedKeys = optionsFlat.filter(item => item['checked']).map(item => item[propValue]); break; case 'PARENT': // 父节点:checked === true && (没有父节点 || (有父节点 && 但父节点 checked === false)) checkedKeys = optionsFlat.filter(item => item['checked'] && (!item['parent'] || (item['parent'] && !item['parent'].checked))).map(item => item[propValue]); break; case 'CHILD': // 底层节点:checked === true && 没有子节点 default: checkedKeys = optionsFlat.filter(item => item['checked'] && !(item['children'] && item['children'].length)).map(item => item[propValue]); break; } } return checkedKeys } onExpand = (expandedKeys) => { this.setState({ expandedKeys, autoExpandParent: false }); }; onExpandChecked = (expandedKeys) => { this.setState({ expandedKeysChecked: expandedKeys, autoExpandParentChecked: false }); }; // 搜索模块 @throttle(300) onSearch(value) { const { optionsFlat } = this.state; const { propValue, propName } = this.keyValue; if (!value || !value.trim()) return this.setState({ expandedKeys: [], searchValue: value, autoExpandParent: true }) const expandedKeys = optionsFlat .map((item) => { if (item[propName].indexOf(value) > -1) { return item[propValue] } return null; }) .filter((item, i, self) => item && self.indexOf(item) === i); this.updateRenderLoopTreeNodeCache(value) this.setState({ expandedKeys, searchValue: value, autoExpandParent: true }); } onChangeSearch = (e) => { const { value } = e.target; this.onSearch(value); }; @throttle(300) onChange() { const { checkedKeys } = this.state; const { onChange } = this.props; onChange(checkedKeys) } onUnCheckExpandedKeysChange = (expandedKeysChecked: any[], uncheckKey) => { return expandedKeysChecked.filter(key => key !== uncheckKey) } // 选中/取消选中模块 onCheck = (checkedKeys, info) => { // checkedKeys:所有 checked === true 的 const checkedKey = info.node.props.eventKey || undefined; const checkStatus = info.checked; if (checkedKey === undefined) return console.error(`Please check: TreeSelectSearch onCheck checkedKey which is ${checkedKey}`); const { options, optionsFlat, expandedKeysChecked } = this.state; const { propValue } = this.keyValue; const matchCheckedRecord = optionsFlat.find(item => item[propValue] === checkedKey) // O(N) —— ok updateOption(checkStatus, matchCheckedRecord) // O(f(n)) —— ok const newCheckedOptions = getCheckedTreeOptions(options); const newCheckedKeys = this.onFilterCheckedKeysCheckedStrategy(checkedKeys, optionsFlat); const newCheckedAllStatus = options.every(item => item['checked']); const newExpandedKeysChecked = checkStatus ? [...expandedKeysChecked, checkedKey] : this.onUnCheckExpandedKeysChange(expandedKeysChecked, checkedKey) this.setState({ checkedKeys: newCheckedKeys, options: options, optionsFlat: optionsFlat, checkedOptions: newCheckedOptions, checkedAllStatus: newCheckedAllStatus, expandedKeysChecked: newExpandedKeysChecked, autoExpandParentChecked: true, }, () => { this.onChange() }) }; // 删除/清空模块 onClear = (record: OptionType) => { const { propValue } = this.keyValue; const { options, optionsFlat, expandedKeysChecked } = this.state; const checkedKey = record[propValue]; const checkStatus = false; const matchCheckedRecord = optionsFlat.find(item => item[propValue] === checkedKey); updateOption(checkStatus, matchCheckedRecord) // 更新了 options、optionsFlat(注意:要传 matchCheckedRecord,不传 record,因为 record !== matchCheckedRecord,修改了 record 不会改到 options) const newCheckedOptions = getCheckedTreeOptions(options) const newCheckedKeys = this.onFindCheckedKeysCheckedStrategy(optionsFlat) const newCheckedAllStatus = options.every(item => item['checked']); const newExpandedKeysChecked = checkStatus ? [...expandedKeysChecked, checkedKey] : this.onUnCheckExpandedKeysChange(expandedKeysChecked, checkedKey) this.setState({ checkedKeys: newCheckedKeys, options: options, optionsFlat: optionsFlat, checkedOptions: newCheckedOptions, checkedAllStatus: newCheckedAllStatus, expandedKeysChecked: newExpandedKeysChecked, autoExpandParentChecked: true, }, this.onChange) }; onCheckAll = (e) => { const { checked } = e.target; if (checked) { const { options, optionsFlat } = this.state; const newCheckedKeys = this.onFindCheckedKeysCheckedStrategy(optionsFlat, true) this.updateStateOptions(options, newCheckedKeys) } else { this.onClearAll() } }; onClearAll = () => { const resetStateOptions = cloneDeep(this.initPropOptions); const resetStateOptionsFlat = transform2FlatData(resetStateOptions, this.keyValue, this.props['rootPid']); this.setState({ checkedKeys: [], checkedOptions: [], options: resetStateOptions, optionsFlat: resetStateOptionsFlat, checkedAllStatus: false, expandedKeysChecked: [], autoExpandParentChecked: true, }, this.onChange) }; // render 缓存(初始、search时更新) renderLoopTreeNodeCache; updateRenderLoopTreeNodeCache = (searchValue?: string) => { this.renderLoopTreeNodeCache = this.renderLoopTreeNode(this.initPropOptions, searchValue) } renderLoopTreeNode = (data, searchValue?: string) => { const { propValue, propName } = this.keyValue; return data.map((item) => { const index = item[propName].indexOf(searchValue); const beforeStr = item[propName].substr(0, index); const afterStr = item[propName].substr(index + searchValue.length); const title = index > -1 ? ( {beforeStr} {searchValue} {afterStr} ) : ( {RenderToolTip(item[propName], 18, {placement: 'topLeft'})} ); if (item.children) { return ( {this.renderLoopTreeNode(item.children, searchValue)} ); } return ; }); }; selectedTitleNode = (record: OptionType) => { const { propName } = this.keyValue; return <> {RenderToolTip(record[propName], 18, {placement: 'topLeft'})} this.onClear(record)} /> } renderLoopSelectedTreeNode = (data) => { const { propValue } = this.keyValue; return data.map((item) => { if (item.children) { return ( {this.renderLoopSelectedTreeNode(item.children)} ); } return ; }); }; render() { const { checkedOptions, checkedKeys, checkedAllStatus, expandedKeys, expandedKeysChecked, autoExpandParent, autoExpandParentChecked } = this.state; return (
全选} className="card-style" > {this.renderLoopTreeNodeCache} 清空 } className="card-style" > {this.renderLoopSelectedTreeNode(checkedOptions)}
); } }