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…