|
|
@@ -0,0 +1,165 @@
|
|
|
+import React, { useState, useCallback, useEffect } from 'react';
|
|
|
+import { TreeSelect } from 'antd';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import { areaClient } from '@/client/api';
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
+
|
|
|
+// 定义区域数据类型
|
|
|
+type AreaItem = {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+ parentId?: number;
|
|
|
+ children?: AreaItem[];
|
|
|
+ isLeaf?: boolean;
|
|
|
+};
|
|
|
+
|
|
|
+// 定义组件属性类型
|
|
|
+interface LazyAreaTreeSelectProps {
|
|
|
+ value?: number;
|
|
|
+ onChange?: (value?: number) => void;
|
|
|
+ placeholder?: string;
|
|
|
+ disabled?: boolean;
|
|
|
+ pageSize?: number; // 每页加载数量,默认为50
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 懒加载区域树形选择组件
|
|
|
+ * 支持按需加载子节点,避免大数据量一次性加载
|
|
|
+ */
|
|
|
+const LazyAreaTreeSelect: React.FC<LazyAreaTreeSelectProps> = ({
|
|
|
+ value,
|
|
|
+ onChange,
|
|
|
+ placeholder = '请选择区域',
|
|
|
+ disabled = false,
|
|
|
+ pageSize = 50
|
|
|
+}) => {
|
|
|
+ const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
|
|
|
+ const [treeData, setTreeData] = useState<any[]>([]);
|
|
|
+
|
|
|
+ // 获取顶层区域数据(省级)
|
|
|
+ const { data: rootAreas, isLoading: rootLoading } = useQuery({
|
|
|
+ queryKey: ['areas', 'root'],
|
|
|
+ queryFn: async (): Promise<InferResponseType<typeof areaClient.$get, 200>> => {
|
|
|
+ const filters = {
|
|
|
+ parentId: 0 // 顶级节点
|
|
|
+ };
|
|
|
+
|
|
|
+ const res = await areaClient.$get({
|
|
|
+ query: {
|
|
|
+ page: 1,
|
|
|
+ pageSize: pageSize,
|
|
|
+ filters: JSON.stringify(filters)
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('获取区域列表失败');
|
|
|
+ }
|
|
|
+ return res.json();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 加载子区域数据
|
|
|
+ const loadChildren = useCallback(async (parentId: number) => {
|
|
|
+ const filters = {
|
|
|
+ parentId: parentId
|
|
|
+ };
|
|
|
+
|
|
|
+ const res = await areaClient.$get({
|
|
|
+ query: {
|
|
|
+ page: 1,
|
|
|
+ pageSize: pageSize,
|
|
|
+ filters: JSON.stringify(filters)
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('获取子区域列表失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await res.json();
|
|
|
+ return result.data || [];
|
|
|
+ }, [pageSize]);
|
|
|
+
|
|
|
+ // 处理树节点加载
|
|
|
+ const onLoadData = async ({ key, children }: any) => {
|
|
|
+ if (children && children.length > 0) {
|
|
|
+ return; // 已经加载过
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const childAreas = await loadChildren(key as number);
|
|
|
+
|
|
|
+ // 检查是否为叶子节点
|
|
|
+ const hasChildren = childAreas.length > 0;
|
|
|
+
|
|
|
+ // 更新树数据
|
|
|
+ const updateTreeData = (list: any[], key: React.Key, children: any[]): any[] => {
|
|
|
+ return list.map(node => {
|
|
|
+ if (node.key === key) {
|
|
|
+ return {
|
|
|
+ ...node,
|
|
|
+ children: children.map(item => ({
|
|
|
+ title: item.name,
|
|
|
+ value: item.id,
|
|
|
+ key: item.id,
|
|
|
+ isLeaf: !hasChildren
|
|
|
+ })),
|
|
|
+ isLeaf: !hasChildren
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (node.children) {
|
|
|
+ return {
|
|
|
+ ...node,
|
|
|
+ children: updateTreeData(node.children, key, children)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return node;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ setTreeData(origin => updateTreeData(origin, key, childAreas));
|
|
|
+ setLoadedKeys(prev => [...prev, key]);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载子区域失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 转换顶层数据
|
|
|
+ useEffect(() => {
|
|
|
+ if (rootAreas?.data) {
|
|
|
+ const formatted = rootAreas.data.map(item => ({
|
|
|
+ title: item.name,
|
|
|
+ value: item.id,
|
|
|
+ key: item.id,
|
|
|
+ isLeaf: false // 顶层节点默认可以展开
|
|
|
+ }));
|
|
|
+ setTreeData(formatted);
|
|
|
+ }
|
|
|
+ }, [rootAreas]);
|
|
|
+
|
|
|
+ // 处理值变化
|
|
|
+ const handleChange = (newValue: any) => {
|
|
|
+ onChange?.(newValue);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <TreeSelect
|
|
|
+ value={value}
|
|
|
+ onChange={handleChange}
|
|
|
+ treeData={treeData}
|
|
|
+ placeholder={placeholder}
|
|
|
+ disabled={disabled}
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ showSearch
|
|
|
+ treeNodeFilterProp="title"
|
|
|
+ loadData={onLoadData}
|
|
|
+ loading={rootLoading}
|
|
|
+ treeLoadedKeys={loadedKeys}
|
|
|
+ allowClear
|
|
|
+ dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default LazyAreaTreeSelect;
|