CustomPerm.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /* eslint-disable no-underscore-dangle */
  2. /* eslint-disable consistent-return */
  3. import { Button, message, Transfer, Tree } from 'antd'
  4. import React, { useMemo, useState, useEffect } from 'react'
  5. import styles from './ConnectCompany/index.less'
  6. import { DeleteOutlined } from '@ant-design/icons'
  7. import consts from '@/basic/consts'
  8. import { useDispatch } from 'umi'
  9. import { queryDepartmentStaffList } from '@/services/department'
  10. import { useWindowSizeFn } from '@/hooks/event/useWindomSizeFn'
  11. import { isEmpty, isNull, isNullOrUnDef } from '@/utils/is'
  12. import CustomModal from '@/components/Modal/src/components/CustomModal'
  13. const isChecked = (selectedKeys, eventKey) => selectedKeys.includes(eventKey)
  14. const itemIsChecked = onItemSelectAll => (selectedKeys, checkedKeys, checked, nodeList) => {
  15. if (!checked) {
  16. onItemSelectAll(nodeList, checked)
  17. } else {
  18. const keys = selectedKeys
  19. .filter(({ isDepartment }) => !isDepartment)
  20. .filter(({ key }) => !isChecked(checkedKeys, key))
  21. .map(({ key }) => key)
  22. onItemSelectAll(keys, checked)
  23. }
  24. }
  25. const generateTree = (treeNodes = [], checkedKeys = []) =>
  26. treeNodes.map(({ children, ...props }) => {
  27. const node = {
  28. ...props,
  29. disabled: checkedKeys.length && checkedKeys.includes(props.key),
  30. children: generateTree(children, checkedKeys)
  31. }
  32. if (props.isDepartment) {
  33. node.disabled = children?.every(
  34. item => checkedKeys.includes(item.key) || item.children?.every(u => !u.children?.length)
  35. )
  36. }
  37. return node
  38. })
  39. const CustomPerm = ({ onConfirm, defaultValues = [] }) => {
  40. const [state, setState] = useState({
  41. dataSource: [],
  42. leftTargetId: null,
  43. treeKeys: [],
  44. targetKeys: defaultValues,
  45. rightTargetId: null,
  46. transferDataSource: [],
  47. height: 0
  48. })
  49. function calcTreeHeight() {
  50. const node = document.querySelector('.ant-transfer-list-body')
  51. if (node) {
  52. return node.clientHeight - 56
  53. }
  54. return null
  55. }
  56. useEffect(() => {
  57. const transferDataSource = []
  58. function flatten(list = []) {
  59. list.forEach(item => {
  60. transferDataSource.push({
  61. key: item.key,
  62. title: item.title,
  63. parentId: item.parentId,
  64. children: item.children,
  65. isDepartment: item.isDepartment
  66. })
  67. if (item.children?.length) flatten(item.children)
  68. })
  69. }
  70. async function fetchList() {
  71. const { code = -1, data = [] } = await queryDepartmentStaffList()
  72. if (code === consts.RET_CODE.SUCCESS) {
  73. const height = calcTreeHeight()
  74. flatten(data)
  75. setState({ ...state, dataSource: data, transferDataSource, height })
  76. }
  77. }
  78. fetchList()
  79. }, [])
  80. useWindowSizeFn(() => {
  81. const height = calcTreeHeight()
  82. setState({ ...state, height })
  83. })
  84. const dispatch = useDispatch()
  85. const closeModal = () => {
  86. dispatch({
  87. type: 'single/changeModal'
  88. })
  89. }
  90. const handleOnSearch = (direction, value) => {
  91. const targetNode = state.transferDataSource.find(item => item.title.indexOf(value) > -1)
  92. if (direction === 'left') {
  93. if (!isEmpty(value) && !isNullOrUnDef(targetNode)) {
  94. setState({ ...state, leftTargetId: targetNode.key })
  95. return
  96. }
  97. if (isEmpty(value)) setState({ ...state, leftTargetId: null })
  98. } else {
  99. if (!isEmpty(value) && !isNullOrUnDef(targetNode)) {
  100. setState({ ...state, rightTargetId: targetNode.key })
  101. return
  102. }
  103. if (isEmpty(value)) setState({ ...state, rightTargetId: null })
  104. }
  105. }
  106. const onChange = (keys, direction, moveKeys) => {
  107. if (direction === 'left') {
  108. const currKeys = state.targetKeys.filter(key => !moveKeys.includes(key))
  109. setState({
  110. ...state,
  111. targetKeys: currKeys,
  112. treeKeys: state.treeKeys.filter(item => {
  113. const node = state.transferDataSource.find(i => i.key === item)
  114. if (node?.isDepartment) {
  115. const childrenKey = node?.children.map(c => c.key).filter(c => !currKeys.includes(c))
  116. if (childrenKey?.every(w => !currKeys.includes(w))) {
  117. return !moveKeys.includes(item) && item !== node.key
  118. }
  119. }
  120. return !moveKeys.includes(item)
  121. })
  122. })
  123. } else {
  124. setState({ ...state, targetKeys: [...state.targetKeys, ...keys] })
  125. }
  126. if (direction === 'right') {
  127. const newTargetKeys = moveKeys.filter(item => !state.targetKeys.includes(item))
  128. setState({ ...state, targetKeys: [...state.targetKeys, ...newTargetKeys] })
  129. }
  130. }
  131. const handleUnCheckItem = (checkedKeys, key, options = {}) => {
  132. // options-用于处理右侧自定义删除逻辑中,防止两个setState合并从而导致更新失败
  133. const node = state.transferDataSource.find(item => item.key === key)
  134. if (node?.isDepartment) {
  135. const filterKeys = state.transferDataSource
  136. .filter(item => item.parentId === node.key)
  137. .map(item => item.key)
  138. setState({
  139. ...state,
  140. ...options,
  141. treeKeys: state.treeKeys.filter(item => ![key, ...filterKeys].includes(item))
  142. })
  143. } else {
  144. const pNode = state.transferDataSource.find(item => item.key === node.parentId)
  145. if (checkedKeys.includes(pNode.key)) {
  146. // 说明 父节点已经被选中
  147. setState({
  148. ...state,
  149. ...options,
  150. treeKeys: state.treeKeys.filter(item => item !== pNode.key && item !== key)
  151. })
  152. } else {
  153. setState({
  154. ...state,
  155. ...options,
  156. treeKeys: state.treeKeys.filter(item => item !== key)
  157. })
  158. }
  159. }
  160. }
  161. // 根据计算返回tree选中的节点
  162. const checkedKeys = useMemo(() => {
  163. const targetId = state.leftTargetId
  164. if (isNull(targetId)) {
  165. return [...state.treeKeys]
  166. }
  167. const node = state.transferDataSource.find(item => item.key === targetId)
  168. if (!isNullOrUnDef(node)) {
  169. // 搜索的节点存在
  170. if (node.isDepartment) {
  171. // 节点是树节点
  172. if (state.treeKeys.findIndex(item => item === targetId) !== -1) {
  173. // treeKeys中有这个树节点
  174. return [
  175. ...state.transferDataSource
  176. .filter(item => item.parentId === targetId)
  177. .map(item => item.key),
  178. node.key
  179. ]
  180. }
  181. // 过滤出当前树节点的子元素中所有处于target状态的叶子结点
  182. return state.transferDataSource
  183. .filter(item => item.parentId === targetId)
  184. .filter(item => state.treeKeys.includes(item.key))
  185. .map(item => item.key)
  186. }
  187. // 直接返回当前节点
  188. return state.treeKeys.filter(item => item === targetId)
  189. }
  190. return []
  191. }, [state.leftTargetId, state.treeKeys.length, state.targetKeys.length])
  192. return (
  193. <CustomModal title="自定义展示数据">
  194. <div className={styles.modalContent}>
  195. <Transfer
  196. showSearch
  197. oneWay={true}
  198. titles={['部门栏', '目标栏']}
  199. operations={['点击添加']}
  200. className="h-full"
  201. render={item => item.title}
  202. onSearch={handleOnSearch}
  203. dataSource={state.transferDataSource}
  204. targetKeys={state.targetKeys}
  205. onChange={onChange}
  206. showSelectAll={false}>
  207. {({ direction, onItemSelectAll, onItemSelect }) => {
  208. if (direction === 'left') {
  209. return state.dataSource?.length ? (
  210. <Tree
  211. blockNode
  212. checkable
  213. defaultExpandAll
  214. height={state.height}
  215. checkedKeys={[...checkedKeys, ...state.targetKeys]}
  216. treeData={generateTree(
  217. state.leftTargetId
  218. ? state.transferDataSource.filter(item => item.key === state.leftTargetId)
  219. : state.dataSource,
  220. state.targetKeys
  221. )}
  222. onCheck={(_, { checked, checkedNodes, node: { isDepartment, key } }) => {
  223. if (!checked) {
  224. handleUnCheckItem(checkedKeys, key)
  225. } else {
  226. setState({
  227. ...state,
  228. treeKeys: checkedNodes.map(item => item.key)
  229. })
  230. }
  231. if (isDepartment) {
  232. itemIsChecked(onItemSelectAll)(
  233. checkedNodes,
  234. checkedKeys,
  235. checked,
  236. state.transferDataSource
  237. .filter(item => item.parentId === key)
  238. .map(item => item.key)
  239. )
  240. } else {
  241. onItemSelect(key, !isChecked(checkedKeys, key))
  242. }
  243. }}
  244. onSelect={(_, { node: { isDepartment, key, parentId } }) => {
  245. const checked = !state.treeKeys.includes(key)
  246. if (!checked) {
  247. handleUnCheckItem(checkedKeys, key)
  248. } else {
  249. const treeKeys = [...state.treeKeys]
  250. if (!isDepartment) {
  251. const samePidArr = state.transferDataSource
  252. .filter(item => item.parentId === parentId)
  253. .map(item => item.key)
  254. const count = checkedKeys.reduce((prev, curr) => {
  255. if (samePidArr.includes(curr)) {
  256. return prev + 1
  257. }
  258. return prev
  259. }, 0)
  260. if (samePidArr.length - count === 1) {
  261. treeKeys.push(parentId)
  262. }
  263. treeKeys.push(key)
  264. } else {
  265. const samePidArr = state.transferDataSource
  266. .filter(item => item.parentId === key)
  267. .filter(item => !checkedKeys.includes(item.key))
  268. .map(item => item.key)
  269. treeKeys.push(...samePidArr, key)
  270. }
  271. setState({
  272. ...state,
  273. treeKeys
  274. })
  275. }
  276. if (isDepartment) {
  277. itemIsChecked(onItemSelectAll)(
  278. state.transferDataSource.filter(item => item.parentId === key),
  279. checkedKeys,
  280. checked,
  281. state.transferDataSource
  282. .filter(item => item.parentId === key)
  283. .map(item => item.key)
  284. )
  285. } else {
  286. onItemSelect(key, !isChecked(checkedKeys, key))
  287. }
  288. }}
  289. />
  290. ) : null
  291. }
  292. if (direction === 'right') {
  293. return (
  294. <div className={styles.removeTreeSwitcher}>
  295. <Tree
  296. blockNode={true}
  297. height={state.height}
  298. treeData={generateTree(
  299. state.rightTargetId
  300. ? state.transferDataSource
  301. .filter(item => state.targetKeys?.includes(item.key))
  302. .filter(item => item === state.rightTargetId)
  303. : state.transferDataSource.filter(item =>
  304. state.targetKeys?.includes(item.key)
  305. )
  306. )}
  307. titleRender={item => (
  308. <li className="ant-transfer-list-content-item">
  309. <span className="ant-transfer-list-content-item-text">{item.title}</span>
  310. <span
  311. className="text-hex-d9d9d9 hover:text-hex-886ab5 transition-colors duration-300"
  312. onClick={() => {
  313. const currKeys = state.targetKeys.filter(i => i !== item.key)
  314. handleUnCheckItem(state.treeKeys, item.key, { targetKeys: currKeys })
  315. }}>
  316. <DeleteOutlined />
  317. </span>
  318. </li>
  319. )}
  320. />
  321. </div>
  322. )
  323. }
  324. }}
  325. </Transfer>
  326. </div>
  327. <div className={[styles.modalFooter, 'text-right'].join(' ')}>
  328. <Button className="mr-4" onClick={closeModal}>
  329. 取消
  330. </Button>
  331. <Button
  332. type="primary"
  333. onClick={() => {
  334. if (!state.targetKeys.length) {
  335. return message.warning('目标栏尚未有员工,操作无效')
  336. }
  337. if (onConfirm) {
  338. onConfirm({ staffIds: state.targetKeys, dataPermission: 'custom' })
  339. }
  340. closeModal()
  341. }}>
  342. 确定
  343. </Button>
  344. </div>
  345. </CustomModal>
  346. )
  347. }
  348. export default CustomPerm