react+ts+antd动态组织树的增删改查

140 阅读6分钟

react+ts+antd动态组织树的增删改查

效果图

具体代码

axios封装

/*
 * @Author: PanZonghui
 * @Description: axios二次封装网络请求接口
 *
 */
import Qs from 'qs'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { getToken } from '@/utils/TokenUtil'

const baseURL = 'http://127.0.0.1:8080/api'
// 创建axiod实例
const service = axios.create({
  baseURL,
  timeout: 30000,
  withCredentials: false,
})
// service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
service.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'
service.defaults.headers.put['Content-Type'] = 'application/json;charset=utf-8'

// 全局声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
 * @description: 添加请求
 * @param {AxiosRequestConfig} config
 * @return {*}
 */
const addPending = (config: AxiosRequestConfig): void => {
  const url = [config.baseURL, config.method, config.url].join('')
  config.cancelToken = new axios.CancelToken(cancel => {
    if (!pending.has(url)) {
      // 如果 pending 中不存在当前请求,则添加进去
      pending.set(url, cancel)
    }
  })
}
/**
 * @description: 移除请求
 *   移除未响应完的相同请求,避免重复请求
 * @param {AxiosRequestConfig} config
 * @return {*}
 */
const removePending = (config: AxiosRequestConfig): void => {
  const url = [config.baseURL, config.method, config.url].join('')
  if (pending.has(url)) {
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}

/**
 * 请求拦截器
 */
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    removePending(config) // 在请求开始前,移除未响应完的相同请求,避免重复请求
    addPending(config) // 将当前请求添加到 pending 中
    if (getToken() !== null) {
      // @ts-expect-error
      config.headers.Authorization = getToken()
    }
    return config
  },
  error => {
    console.log('请求异常', error)
    // 错误抛到业务代码
    error.data = {}
    error.data.code = -1
    error.data.message = '发送请求出现异常!'
    return Promise.reject(error)
  },
)

/**
 * 响应拦截
 */
service.interceptors.response.use(
  (response: AxiosResponse) => {
    removePending(response) // 在请求结束后,移除本次请求
    if (response.status === 200) {
      // 请求结果正常
      const { code } = response.data
      if (code === 200) {
        // 请求成功
        return Promise.resolve(response.data)
      } else {
        // 处理系统自定义异常
        return Promise.reject(response.data)
      }
    } else {
      console.log('响应请求异常', response)
      return Promise.reject(response)
    }
  },
  error => {
    if (axios.isCancel(error)) {
      // 重复请求的错误
      // 中断promise
      return new Promise(() => {})
    }
    console.log('响应请求出现异常!', error)
    // 错误抛到业务代码
    error.data = {}
    error.data.code = -2
    error.data.message = '响应请求出现异常!'
    return Promise.reject(error.data)
  },
)

/**
 * @description: Http网络请求返回的数据类型接口
 */
interface HttpResponse<T> {
  code?: number
  data: T
  message?: string
}

interface HttpMethod {
  get: <T>(url: string, params?: unknown) => Promise<HttpResponse<T>>
  post: <T>(url: string, data?: unknown) => Promise<HttpResponse<T>>
  put: <T>(url: string, data?: unknown) => Promise<HttpResponse<T>>
  delete: <T>(url: string, params?: unknown) => Promise<HttpResponse<T>>
  upload: <T>(url: string, files: FormData) => Promise<HttpResponse<T>>
  download: (url: string, params?: unknown) => void
}

/**
 * @description: 网络请求接口类
 */
const HttpRequest: HttpMethod = {
  post(url, data = {}) {
    return new Promise((resolve, reject) => {
      service
        .post(url, data)
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          reject(err)
        })
    })
  },
  put(url, data = {}) {
    return new Promise((resolve, reject) => {
      service
        .put(url, data)
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          reject(err)
        })
    })
  },
  get(url, params = {}) {
    return new Promise((resolve, reject) => {
      service
        .get(url, {
          params,
          paramsSerializer: params => Qs.stringify(params, { indices: false }),
        })
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          reject(err)
        })
    })
  },
  delete(url, params = {}) {
    return new Promise((resolve, reject) => {
      service
        .delete(url, {
          params,
        })
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          reject(err)
        })
    })
  },
  upload(url, files: FormData) {
    return new Promise((resolve, reject) => {
      service
        .post(url, files, {
          headers: { 'Content-Type': 'multipart/form-data' },
        })
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          reject(err)
        })
    })
  },
  download(url, params = {}) {
    axios
      .get(`${baseURL}${url}`, {
        params,
        responseType: 'blob',
        paramsSerializer: params => Qs.stringify(params, { indices: false }),
        headers: {
          Authorization: 'xxx',
        },
      })
      .then(res => {
        // 创建一个类文件对象:Blob对象表示一个不可变的、原始数据的类文件对象
        let blob = new Blob([res.data], {
          type: res.data.type,
        })
        // 创建新的URL并指向File对象或者Blob对象的地址
        const blobURL = window.URL.createObjectURL(blob)
        // 创建a标签,用于跳转至下载链接
        const tempLink = document.createElement('a')
        tempLink.style.display = 'none'
        tempLink.href = blobURL
        // 设置指示浏览器下载url,
        let fileName = decodeURI(
          res.headers['content-disposition'].split(';')[1].split('=')[1],
        )
        tempLink.setAttribute('download', fileName)
        // 兼容:某些浏览器不支持HTML5的download属性
        if (typeof tempLink.download === 'undefined') {
          tempLink.setAttribute('target', '_blank')
        }
        // 挂载a标签
        document.body.appendChild(tempLink)
        tempLink.click()
        // 移除a标签
        document.body.removeChild(tempLink)
        // 释放blob URL地址
        window.URL.revokeObjectURL(blobURL)
      })
      .catch((err: any) => {
        console.log(`文件下载失败${err}`)
      })
  },
}

export default HttpRequest

数据类型封装

//TreeType.ts

/**
 * 组织机构树接口属性
 */
export interface TreeProps {
  id: string
  name: string
  children?: TreeProps[]
  isAdd?: boolean // 自己扩展 是否是添加状态
  isEdit?: boolean // 自己扩展 是否是编辑状态
}

api接口封装

// TreeApi.ts
/**
 * 组织机构模块下 dom树 api接口
 */

import http from '@/utils/AxiosRequest'
import type { TreeProps } from '@/types/Organization/TreeType'

const treeUrls = {
  queryList: '/organization/queryList',
  add: '/organization/add',
  updateById: '/organization/updateById',
  deleteById: '/organization/deleteById',
}
/**
 * @description:查询组织机构树列表
 * @return
 */
export const queryOrganizationList = () =>
  http.get<TreeProps[]>(treeUrls.queryList)

/**
 * @description: 添加一个组织机构节点
 * @param {string} parentId 父节点id
 * @param {string} name 节点名称
 */
export const addOrganization = (parentId: string, name: string) =>
  http.post(treeUrls.add, { parentId, name })

/**
 * @description: 根据节点id修改一个组织机构节点名称
 * @param {string} id 节点id
 * @param {string} name 节点名称
 */
export const updateOrganizationById = (id: string, name: string) =>
  http.put(treeUrls.updateById, { id, name })

/**
 * @description: 根据id删除组织机构节点
 * @param {string} id 节点id
 */
export const deleteOrganizationById = (id: string) =>
  http.delete(treeUrls.deleteById, { id })

OrganizationTree组件

import React, { useState, useRef, useLayoutEffect } from 'react'
import { Input, Tree } from 'antd'
import { nanoid } from 'nanoid'
import {
  EditOutlined,
  PlusCircleOutlined,
  MinusCircleOutlined,
  CheckOutlined,
  CloseOutlined,
} from '@ant-design/icons'
import { Scrollbars } from 'react-custom-scrollbars'
import type { TreeProps } from '@/types/Organization/TreeType'
import {
  queryOrganizationList,
  addOrganization,
  updateOrganizationById,
  deleteOrganizationById,
} from '@/api/organization/TreeApi'

/**
 * @description: 动态组织机构树组件
 */
const OrganizationTree: React.FC = () => {
  const currentTree = useRef<TreeProps>() // 当前添加/编辑的树节点信息
  const treeData = useRef<TreeProps[]>([]) // dom树数据
  const [treeDataList, setTreeDataList] = useState<any[]>([]) // 加工后树的数据,用于渲染
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]) // 选中树的key
  const [expandedKeys, setExpandedKeys] = useState<string[]>([]) // 展开树的key
  const [autoExpandParent, setAutoExpandParent] = useState<boolean>(false) // 自动展开父节点

  useLayoutEffect(() => {
    queryTreeData()
  }, [])

  // todo加工生成树结构数据
  const generateTreeList = (list: TreeProps[]): any[] => {
    return list.map(item => {
      if (item.isAdd === true) {
        return {
          key: item.id,
          title: (
            <div>
              <Input
                defaultValue={item.name}
                size="small"
                style={{ width: '100px' }}
                onChange={e => handleInputChange(item.id, e.target.value)}
              />
              <span>
                <CheckOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleConfirmAddTree(item.id)}
                />
                <CloseOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleCancelAddOrEditTree()}
                />
              </span>
            </div>
          ),
          children: item.children,
        }
      } else if (item.isEdit === true) {
        return {
          key: item.id,
          title: (
            <div>
              <Input
                defaultValue={item.name}
                size="small"
                style={{ width: '100px' }}
                onChange={e => handleInputChange(item.id, e.target.value)}
              />
              <span>
                <CheckOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleConfirmUpdateTree(item.id)}
                />
                <CloseOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleCancelAddOrEditTree()}
                />
              </span>
            </div>
          ),
          children:
            item.children != null ? generateTreeList(item.children) : null,
        }
      } else {
        return {
          key: item.id,
          title: (
            <div>
              <span onClick={() => handleSelectTree(item.id)}>{item.name}</span>
              <span>
                <EditOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleEditTree(item.id)}
                />
                <PlusCircleOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleAddTree(item.id)}
                />
                <MinusCircleOutlined
                  style={{ marginLeft: 5 }}
                  onClick={() => handleDelectNodeByKey(item.id)}
                />
              </span>
            </div>
          ),
          children:
            item.children != null ? generateTreeList(item.children) : null,
        }
      }
    })
  }

  // !发送网络查询树数据
  const queryTreeData = async () => {
    queryOrganizationList().then(res => {
      const organizationList = res.data
      // 生成需要的dom
      setTreeDataList(generateTreeList(organizationList))
      // 保存数据
      treeData.current = organizationList
      // 清空当前节点信息
      currentTree.current = undefined
    })
  }

  // ?循环过滤树节点
  const loopFilterNode: any = (searchValue: string, data: TreeProps[]) =>
    data
      .map(item => {
        const index = item.name.indexOf(searchValue)
        const beforeStr = item.name.substring(0, index)
        const afterStr = item.name.substring(
          index + searchValue.length,
          item.name.length,
        )
        const name =
          index > -1 ? (
            <>
              {beforeStr}
              <span style={{ color: 'red' }}>{searchValue}</span>
              {afterStr}
            </>
          ) : (
            item.name
          )

        if (item.children && item.children.length > 0) {
          const children = loopFilterNode(searchValue, item.children)
          return {
            ...item,
            name: index > -1 || children.length ? name : null,
            children: children.length ? children : undefined,
          }
        }
        return index > -1
          ? {
              ...item,
              name,
            }
          : {
              ...item,
              name: null,
            }
      })
      .filter(item => item.name)

  // ?获取要展开节点的key
  const getExpandedKeys = (data: any[], expandedKeys: string[]) => {
    data.forEach(item => {
      expandedKeys.push(item.id)
      if (item.children) {
        getExpandedKeys(item.children, expandedKeys)
      }
    })
  }

  // ? 处理过滤树节点
  const handleFilterTree = (searchValue: string) => {
    // 获取过滤后的数据
    const filterArr = loopFilterNode(searchValue, treeData.current)
    // 要展开的key数组
    const expandedKeys: string[] = []
    // 获取要展开节点的key
    getExpandedKeys(filterArr, expandedKeys)
    // 更新渲染的节点数据
    setTreeDataList(generateTreeList(filterArr))
    // 更新展开节点数据
    setExpandedKeys(expandedKeys)
  }

  // todo处理树节点被选中
  const handleSelectTree = (selectedKey: string) => {
    setSelectedKeys([selectedKey])
  }

  // todo点击树展开事件
  const handleTreeExpand = (keys: any[]) => {
    setExpandedKeys([...keys])
    setAutoExpandParent(false)
  }

  // ? 改变node节点的数据
  const changeNode = (key: string, value: string, data: TreeProps[]) =>
    data.forEach(item => {
      if (item.id === key) {
        item.name = value
      } else if (item.children != null) {
        changeNode(key, value, item.children)
      }
    })

  // todo输入框数据改变
  const handleInputChange = (key: string, value: string) => {
    changeNode(key, value, treeData.current)
    setTreeDataList(generateTreeList(treeData.current))
  }

  // ?动态添加一个节点
  const addNode = (treeList: TreeProps[], parentKey: string) => {
    treeList.forEach(item => {
      if (item.id === parentKey) {
        setExpandedKeys([parentKey])
        setAutoExpandParent(true)
        const newNode = {
          id: nanoid(),
          name: '新增标签',
          isAdd: true,
        }
        if (item.children != null) {
          item.children.push(newNode)
        } else {
          item.children = []
          item.children.push(newNode)
        }
      } else if (item.children != null) {
        addNode(item.children, parentKey)
      }
    })
  }

  // todo添加节点事件
  const handleAddTree = (parentKey: string) => {
    addNode(treeData.current, parentKey)
    setTreeDataList(generateTreeList(treeData.current))
  }

  // ?查找到添加节点信息
  const findAddNodeInfo = (nodeKey: string, treeData: TreeProps[]) => {
    treeData.forEach(item => {
      if (item.children) {
        let isFind = false
        item.children.forEach(node => {
          if (node.id === nodeKey) {
            isFind = true
            node.isAdd = false
            node.isEdit = false
            currentTree.current = { id: item.id, name: node.name }
          }
        })
        if (!isFind) {
          findAddNodeInfo(nodeKey, item.children)
        }
      }
    })
  }

  // !确认添加树节点信息
  const handleConfirmAddTree = async (nodeKey: string) => {
    // 查询当前添加节点的信息
    findAddNodeInfo(nodeKey, treeData.current)
    if (!currentTree.current) return
    const parentId = currentTree.current.id
    const name = currentTree.current.name
    console.log('添加节点信息为')
    console.log({ parentId, name })
    try {
      // 添加节点
      await addOrganization(parentId, name)
      // 更新节点信息
      await queryTreeData()
    } catch (error) {
      console.log(error, '添加节点失败')
    }
  }

  // todo取消(添加/编辑)事件
  const handleCancelAddOrEditTree = () => {
    // 更新节点信息
    queryTreeData()
  }

  // ?动态修改一个节点
  const editNode = (treeList: TreeProps[], nodeKey: string) => {
    treeList.forEach(item => {
      item.isEdit = false
      if (item.id === nodeKey) {
        setExpandedKeys([item.id])
        setAutoExpandParent(true)
        item.isEdit = true
      }
      if (item.children != null) {
        editNode(item.children, nodeKey)
      }
    })
  }

  // todo处理点击编辑事件
  const handleEditTree = (nodeKey: string) => {
    editNode(treeData.current, nodeKey)
    setTreeDataList(generateTreeList(treeData.current))
  }

  // ?查找到修改节点信息
  const findEditNodeInfo = (nodeKey: string, treeData: TreeProps[]) => {
    treeData.forEach(item => {
      let isFind = false
      if (item.id === nodeKey) {
        isFind = true
        item.isAdd = false
        item.isEdit = false
        currentTree.current = { id: item.id, name: item.name }
      }
      if (!isFind) {
        item.children && findEditNodeInfo(nodeKey, item.children)
      }
    })
  }

  // !确认修改树节点信息
  const handleConfirmUpdateTree = async (nodeKey: string) => {
    // 查询当前修改节点的信息
    findEditNodeInfo(nodeKey, treeData.current)

    if (!currentTree.current) return
    const { id, name } = currentTree.current
    console.log('修改节点信息为', { id, name })
    try {
      // 发送网络修改节点信息
      await updateOrganizationById(id, name)
      // 更新节点信息
      await queryTreeData()
    } catch (error) {
      console.log('修改节点信息失败', error)
    }
  }

  // !根据key删除节点
  const handleDelectNodeByKey = async (nodeKey: string) => {
    console.log('根据key删除节点', nodeKey)
    try {
      // 发送网络删除节点
      await deleteOrganizationById(nodeKey)
      // 发送网络查询最新节点
      await queryTreeData()
    } catch (error) {
      console.log('删除节点失败', error)
    }
  }

  return (
    <>
      <div style={{ padding: '5px', boxSizing: 'border-box', height: '42px' }}>
        <Input
          allowClear
          placeholder="Search"
          onChange={e => handleFilterTree(e.target.value)}
        />
      </div>
      <Scrollbars style={{ height: 'calc(100% - 42px)' }}>
        <Tree
          showLine={{ showLeafIcon: false }}
          autoExpandParent={autoExpandParent}
          expandedKeys={expandedKeys}
          selectedKeys={selectedKeys}
          treeData={treeDataList}
          onExpand={handleTreeExpand}
        />
      </Scrollbars>
    </>
  )
}

export default OrganizationTree

后端接口

后端接口代码见:github.com/smalllhui/n…