直接上代码
ini
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { http, useDebounce } from '../../utils';
import { maxRequestNum } from '../../global';
import { TreeSelect, message } from 'antd';
import type { TreeSelectProps } from 'antd';
// 节点类型
export interface RegionTreeNode {
id: string;
value: string;
label: string;
children?: RegionTreeNode[];
isLeaf?: boolean;
regionLevel?: number;
}
// 组件 Props
export interface AsyncRegionSelectProps extends TreeSelectProps {
/**
* 自定义加载顶层节点的参数
*/
topLevelParams?: Record<string, any>;
disabled?: boolean;
}
/**
* 异步懒加载 区域选择组件
* 省/市/区/街道 四级联动异步加载 + 搜索功能
*/
const AsyncRegionSelect: React.FC<AsyncRegionSelectProps> = (props) => {
const { topLevelParams, disabled, ...restProps } = props;
// 区域树数据
const [regionTreeData, setRegionTreeData] = useState<RegionTreeNode[]>([]);
// 搜索后临时展示的树数据
const [searchTreeData, setSearchTreeData] = useState<RegionTreeNode[]>([]);
// 加载状态
const [loading, setLoading] = useState<boolean>(false);
// 展开的节点 keys
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// 记录是否处于搜索状态
const isSearchRef = useRef(false);
// 格式化树数据
const formatTreeData = (data: any[] = []): RegionTreeNode[] => {
return data.map((item) => ({
id: item?.id || '',
value: item?.id || '',
label: item?.regionName || '',
children: item?.children ? formatTreeData(item.children) : [],
isLeaf: false, // 强制显示展开箭头
regionLevel: item?.regionLevel || 0,
}));
};
// 加载顶层节点(省)
const loadTopLevelNodes = () => {
const param = {
pageNum: 1,
pageSize: maxRequestNum,
param: {
parentRegionId: '',
regionLevel: 0,
...topLevelParams,
},
};
setLoading(true);
http
.post('/service/webapp/listRegionManagement', param)
.then((res: any) => {
if (res?.code === 200) {
const treeData = formatTreeData(res?.rows || []);
setRegionTreeData(treeData);
} else {
message.error(res?.msg || '加载区域数据失败');
}
})
.catch(() => message.error('加载区域数据失败'))
.finally(() => setLoading(false));
};
// 加载子节点
const loadChildNodes = async (
parentId: string,
parentLevel: number,
): Promise<any[]> => {
try {
const param = {
pageNum: 1,
pageSize: maxRequestNum,
param: {
parentRegionId: parentId,
regionLevel: parentLevel + 1,
},
};
const res = await http.post(
'/service/webapp/listRegionManagement',
param,
);
if (res?.code !== 200) return [];
return res?.rows || [];
} catch (err) {
return [];
}
};
// 更新树子节点
const updateTreeChildren = (
tree: RegionTreeNode[],
id: string,
newChildren: RegionTreeNode[],
): RegionTreeNode[] => {
return tree?.map((node) => {
if (node?.id === id) {
return {
...node,
children: newChildren,
isLeaf: newChildren?.length === 0,
};
}
if (node?.children) {
return {
...node,
children: updateTreeChildren(node?.children, id, newChildren),
};
}
return node;
});
};
// 异步加载子节点
const handleLoadData = async (node: any) => {
// 搜索状态下不执行懒加载
if (isSearchRef.current) return;
const { id, children } = node;
if (children && children.length > 0) return;
const childList = await loadChildNodes(id, node?.regionLevel || 0);
const newChildTreeData = formatTreeData(childList);
setRegionTreeData((prev) => updateTreeChildren(prev, id, newChildTreeData));
};
// 递归收集节点所有父级 ID(用于展开)
// const getExpandedKeys = (
// node: RegionTreeNode,
// keys: string[] = [],
// ): string[] => {
// if (node.id) keys.push(node.id);
// if (node.children && node.children.length > 0) {
// getExpandedKeys(node.children[0], keys);
// }
// return keys;
// };
// 递归收集节点路径(用于搜索后展开整条链)
const collectPathKeys = (node: RegionTreeNode): string[] => {
const keys: string[] = [];
const traverse = (n: RegionTreeNode) => {
keys.push(n?.id);
if (n?.children && n?.children?.length > 0) {
traverse(n?.children?.[0]);
}
};
traverse(node);
return keys;
};
//根据名称搜索
const handleSearch = useDebounce((value: string) => {
if (!value) {
// 清空搜索 → 恢复原始树
isSearchRef.current = false;
setSearchTreeData([]);
setExpandedKeys([]);
loadTopLevelNodes();
return;
}
isSearchRef.current = true;
setLoading(true);
http
.get(`/service/webapp/getRegionChainByName?regionName=${value}`)
.then((res: any) => {
if (res?.code === 200) {
const newTreeData = res?.rows || [];
if (newTreeData?.length === 0) {
message.info('未搜索到匹配区域');
setSearchTreeData([]);
return;
}
// 格式化搜索返回的完整树
const treeData = formatTreeData(newTreeData);
setSearchTreeData(treeData);
// 自动展开到最末级
const expandKeys = collectPathKeys(treeData[0]);
setExpandedKeys(expandKeys);
} else {
message.error(res?.msg || '加载区域数据失败');
}
})
.catch(() => {
message.error('加载区域数据失败');
})
.finally(() => setLoading(false));
}, 500);
// 清除后重新加载顶层
const handleChange = useCallback(
(value: any) => {
if (value === undefined || value === null) {
isSearchRef.current = false;
setSearchTreeData([]);
setExpandedKeys([]);
loadTopLevelNodes();
}
props.onChange?.(value);
},
[props.onChange],
);
// 初始化加载顶层
useEffect(() => {
loadTopLevelNodes();
}, []);
// 最终渲染的 treeData:搜索优先,否则用原始树
const renderTreeData =
searchTreeData.length > 0 ? searchTreeData : regionTreeData;
return (
<TreeSelect
disabled={disabled}
style={{ width: '100%' }}
placeholder="请选择区域"
treeData={renderTreeData}
loadData={handleLoadData}
loading={loading}
treeNodeFilterProp="label"
allowClear
showSearch
onSearch={handleSearch}
treeExpandedKeys={expandedKeys}
onTreeExpand={(keys) => setExpandedKeys(keys as string[])}
{...restProps}
onChange={handleChange}
/>
);
};
export default AsyncRegionSelect;
效果

