- 问题:Cascader因为要回显所以要渲染全量数据,结果导致组件渲染全量数据特别卡
- 方案:把全量数据中暂时不需要的数据去掉,需要的时候再加上 具体做法分四步
- 根据回显的值,从树中把包含值的节点及其祖先节点全拿出来
- 把第一级的节点全拿出来,去掉二级及以下的全部节点
- 求1和2的并集 作为Cascader的数据源
- 点击某个菜单时,再获取这个菜单的子菜单,并把后代菜单全去掉,再与所点菜单的已有子菜单求并集 封装的组件主要包括两个文件
- CascaderTree.js 显示级联菜单的组件
- treeUtil.js 处理数据的工具
CascaderTree.js
import { Cascader, Tooltip, message } from 'antd'
import { connect } from 'react-redux'
import { cloneDeep } from 'lodash'
import styles from './style/index.module.less'
import {
findValuedTree,
unionSet,
removeSubTree,
findNodesWithoutSubTree,
delEmptyTreeNode,
initTree,
} from '@/utils/treeUtil'
/**
* 处理默认数据
* @param {*} value
* @param {*} multiple 是否多选
* @returns 根据multiple返回一维或二维数组 例如:["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
*/
const formatDefaultValue = (value, multiple) => {
let result
const defaultValue = cloneDeep(value)
if (multiple) {
if (defaultValue) {
if (Array.isArray(defaultValue)) {
if (Array.isArray(defaultValue[0])) {
// 二维数组
result = defaultValue
} else {
// 一维数组
if (defaultValue.length) {
result = defaultValue?.map(item => item?.split?.('-'))
} else {
//及联多选只支持一维空数组
result = defaultValue
}
}
} else {
result = [defaultValue?.split?.('-')]
}
} else {
result = undefined
}
} else {
if (defaultValue) {
if (Array.isArray(defaultValue)) {
if (Array.isArray(defaultValue[0])) {
;[result] = defaultValue
} else {
result = defaultValue
}
} else {
result = defaultValue?.split?.('-')
}
} else {
result = undefined
}
}
return result
}
/**
* 处理要提交的数据
* @param {*} value
* @param {*} multiple 是否多选
*/
const formatSubmitValue = (value, multiple) => {
if (multiple) {
return value?.map?.(item => item?.join?.('-'))
} else {
return value?.join?.('-')
}
}
const CascaderTree = props => {
const { getInterfaceTypeData, dataItem } = props
const renderFields = dataItem?.props?.response
const children = renderFields?.children ?? 'children'
const { multiple, requireFull = true } = dataItem?.props
const defalutValues = getJSONParseData(dataItem?.defalutValues)
const cityDefaultValue = formatDefaultValue(defalutValues, multiple)
/**
* 获取过数据
*/
const [manualChange, setManualChange] = useState(false)
const [treeData, setTreeData] = useState([])
/**
* 映射字段对应的key
*/
const fieldNames = {
label: renderFields?.label ?? 'label',
value: renderFields?.value ?? 'value',
children: children,
}
const [options, setOptions] = useState([])
const [selectValue, setSelectValue] = useState(cityDefaultValue)
/**
* 获取数据
*/
useEffect(() => {
if (!manualChange) {
const { config } = dataItem?.props?.request
const { structure } = dataItem?.props?.response
getInterfaceTypeData(config).then(data => {
setManualChange(true)
// 接口返回的树数据(多根树)
let tree = data
initTree(tree, children)
// 把树的空子树移除
const allTree = delEmptyTreeNode(tree, children)
setTreeData(allTree)
/**
* 包含选中节点的树 例如:
* [
* {
* "id":"8","name":"北京市",
* "children":[
* {"id":"10000","name":"北京市",
* "children":[
* {"id":"35001","name":"西城区"},
* {"id":"35002","name":"朝阳区"}
* ]
* }
* ]
* }
* ]
*/
const valueTree = findValuedTree(allTree, cityDefaultValue, renderFields?.label)
// 只包含第一级节点的树
const nodes = removeSubTree(allTree)
// 求只包含第一级节点的树与包含选中节点的树 的并集
const tree = unionSet(valueTree, nodes)
setOptions(tree)
})
}
}, [
getInterfaceTypeData,
dataItem,
children,
manualChange,
cityDefaultValue,
renderFields?.label,
])
const loadData = selectedOptions => {
const targetOption = selectedOptions[selectedOptions.length - 1]
const ids = selectedOptions.map(item => item[renderFields?.id])
targetOption.loading = true
// 只包含第一级子节点的树
const nodes = findNodesWithoutSubTree(treeData, ids, renderFields?.id, children)
// 求只包含第一级子节点的树与包含选中节点的树 的并集
targetOption[children] = unionSet(targetOption[children], nodes)
setOptions([...options])
}
//提交数据接口
const putCustomDetailItemFn = (item, value) => {
const flatValue = formatSubmitValue(value, multiple)
let dealValue = flatValue
if (multiple) {
dealValue = flatValue?.length ? JSON.stringify(flatValue) : ''
}
const params = {
id: props.customerInfo?.id,
[item.name]: dealValue,
}
props.putCustomDetailItem(params).then(res => {
if (res?.status === 1) {
message.success('修改成功')
} else {
message.error('修改失败')
}
})
}
const onChange = (value, selectedOptions) => {
if (!value?.length && selectValue?.length) {
message.warn('不可清空已设置信息!')
} else {
setSelectValue(value)
putCustomDetailItemFn(dataItem, value)
}
}
const titleLength = dataItem.title.split('')
const TooltipInfo =
titleLength.length > 6 ? (
<span style={{ color: '#777', fontSize: '14px' }}>{dataItem.title}</span>
) : null
return (
<div className={styles.intptName}>
<Tooltip placement="bottom" title={TooltipInfo} color={'#eee'}>
<span className={styles.lableName}>{dataItem.title}:</span>
</Tooltip>
<Cascader
allowClear={false}
options={options}
onChange={onChange}
className={styles.inpt}
placeholder={!dataItem.readonly ? `请选择${dataItem.title}` : `暂无`}
// showSearch={{ filter }}
fieldNames={fieldNames}
value={selectValue}
getPopupContainer={triggerNode => triggerNode.parentElement}
changeOnSelect={!requireFull}
multiple={multiple}
loadData={loadData}
/>
</div>
)
}
export default connect(
({ customer }) => ({ ...customer }),
({ customer }) => ({ ...customer }),
)(CascaderTree)
treeUtil.js
/**
* 广度优先查找树
* @param {*} treeArr 多根树
* @param {*} subKey 子树的key
* @param {*} func 回调函数,参数是 树节点 返回是否找到
* @returns 返回找到的节点
*/
export const wideSearchTree = (treeArr, subKey = 'children', func) => {
let node,
queue = treeArr?.slice() ?? []
while ((node = queue.shift())) {
if (func(node)) {
return node
}
let { [subKey]: children } = node
children?.length && queue.push(...children)
}
return null
}
/**
* 广度优先遍历树
* @param {*} tree 多根树
* @param {*} subKey 子树的key
* @param {*} func 回调函数,参数是 树节点
* @returns
*/
export function bfsTreeEach(tree, subKey = 'children', func) {
let node,
nodes = tree?.slice() ?? []
while ((node = nodes.shift())) {
func(node)
let { [subKey]: children } = node
if (children?.length) {
nodes.push(...children)
}
}
}
/**
* 添加或修改节点的属性
* @param {*} node
* @param {*} key
* @param {*} value
* @param {*} predicate
*/
function updateNode(node, key, value, predicate) {
if (predicate(node)) {
node[key] = value
}
}
/**
* 是否是叶子节点
* @param {*} node
* @param {*} subKey
* @returns
*/
function isLeaf(node, subKey = 'children') {
return !node?.[subKey]?.length
}
/**
* 初始化树
* @param {*} tree
* @param {*} subKey
*/
export const initTree = (tree, subKey = 'children') => {
bfsTreeEach(tree, subKey, node => {
// 标记节点是否是叶子节点
updateNode(node, 'isLeaf', false, node => !isLeaf(node, subKey))
})
}
/**
* @param tree 要过滤的树(多根)
* @param predicate 过滤条件,返回 `true` (node, parent, deep) => boolean
* @returns 过滤后的树节点集
*/
export const filterTree = (tree, subKey = 'children', predicate) => {
let copyTree = cloneDeep(tree)
return filter(copyTree) ?? []
// deep 层深
function filter(copyTree, parent, deep = 0) {
if (!copyTree?.length) {
return copyTree
}
return copyTree.filter(node => {
if (!predicate(node, parent, deep)) {
return false
}
// 递归调用不需要再传入 predicate
node[subKey] = filter(node[subKey], node, deep + 1)
return true
})
}
}
const isTwoDimension = arr => Array.isArray(arr?.[0])
/**
* 判断节点是否在值数组内
* @param {*} node
* @param {*} deep
* @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
*/
const nodeInValues = (node, deep, key = 'id', values = []) => {
const value = node[key]
return isTwoDimension(values) ? values.find(item => item[deep] === value) : values[deep] === value
}
/**
* 判断节点是否需要留在树内(结果是1和2的并集:1、节点值在values内且是最后一级 2、节点值不在values内但父节点值在values内)
* @param {*} node
* @param {*} parent 父节点
* @param {*} deep
* @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
* @returns true false
*/
const nodeIsRetainInTree = (node, parent, deep, key = 'id', values = []) => {
const value = node[key]
const pvalue = parent?.[key] ?? '$&123&$'
let valuePath, pValuePath
if (isTwoDimension(values)) {
valuePath = values.find(item => item[deep] === value)
pValuePath = values.find(item => item[deep - 1] === pvalue)
} else {
valuePath = values[deep] === value ? values : null
pValuePath = values[deep - 1] === pvalue ? values : null
}
// 节点在值数组内 返回是否是最后一级
if (valuePath) {
return deep === valuePath.length
} else if (pValuePath) {
return true
}
// 节点不在值数组内,也算作不是最后一级
return false
}
/**
* 根据给定的值获取树,树中只保留给定值的节点,去掉其他节点
* @param {*} treeArr
* @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
* @param {*} key 字段名
* @returns
*/
export const findValuedTree = (treeArr, values, key = 'id', subKey = 'children') => {
return filterTree(
treeArr,
subKey,
(node, parent, deep) =>
nodeIsRetainInTree(node, parent, deep, key, values) || nodeInValues(node, deep, key, values),
)
}
/**
* 差集 a - b
* @param {*} a
* @param {*} b
* @param {*} key 字段名
* @returns
*/
export const differenceSet = (a, b, key = 'id') => {
return a.filter(item => !b.find(e => e[key] === item[key]))
}
/**
* 并集 a + b
* @param {*} a
* @param {*} b
* @param {*} key
* @returns
*/
export function unionSet(a = [], b = [], key = 'id') {
const dset = differenceSet(b, a, key)
return [...a, ...dset]
}
/**
* 移除子树
* @param {*} treeArr
* @param {*} subKey 子树名
* @returns
*/
export const removeSubTree = (treeArr, subKey = 'children') => {
return treeArr?.map?.(item => ({ ...item, [subKey]: [] })) ?? []
}
/**
* 根据父节点值获取子节点列表,不包含子节点一下的子树
* @param {*} treeArr 要查找的树
* @param {*} valuePath 值的路径 ['北京市', '北京市', '海淀区']
* @param {*} key 搜索的字段
* @param {*} subKey 子树的key
* @returns
*/
export const findNodesWithoutSubTree = (treeArr, valuePath, key = 'id', subKey = 'children') => {
let tempNode,
tree = treeArr
valuePath.forEach(value => {
tempNode = wideSearchTree(tree, subKey, node => {
if (node[key] === value) {
return true
}
return false
})
if (tempNode) {
tree = tempNode[subKey]
} else {
tree = []
}
})
const children = removeSubTree(tempNode[subKey], subKey)
return children
}
/**
* 移除空子树
* @param {*} tree
* @param {*} key
* @returns
*/
export const delEmptyTreeNode = (tree, key = 'children') => {
if (!key && String(key) !== '0') {
return tree
}
return tree?.map?.(node => {
if (node?.[key]?.length) {
delEmptyTreeNode(node?.[key], key)
} else {
Reflect.deleteProperty(node, key)
}
return node
})
}