audit-picker-source-tree.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <template>
  2. <div
  3. :class="[
  4. $style.wrapper,
  5. {
  6. [$style.single]: single,
  7. },
  8. ]"
  9. >
  10. <a-spin ref="scroll" :spinning="!dataLoaded" :class="$style.spin">
  11. <a-dropdown
  12. v-if="dataLoaded"
  13. v-model="showMenu"
  14. :get-popup-container="() => this.$refs.scroll.$el"
  15. :trigger="['contextmenu']"
  16. :disabled="single"
  17. @visibleChange="menuVisibleChange"
  18. >
  19. <div>
  20. <a-tree
  21. v-if="treeData"
  22. :load-data="onLoadData"
  23. :tree-data="treeDataComputed"
  24. checkable
  25. check-strictly
  26. :checked-keys="[...selectedKeys, ...targetKeys]"
  27. :default-expand-parent="true"
  28. :default-expanded-keys="expandedKeys"
  29. :selected-keys="[selectedNode && selectedNode.eventKey]"
  30. @click="treeClick"
  31. @click.native="treeClickNative"
  32. @dblclick="treeDblClick"
  33. @rightClick="treeRightClick"
  34. @contextmenu.native.capture="treeRightClickNative"
  35. @check="
  36. (_, props) => {
  37. onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect)
  38. }
  39. "
  40. @select="onSelect"
  41. />
  42. </div>
  43. <a-menu slot="overlay" @click="menuClick">
  44. <a-menu-item key="1" :disabled="!selectedNodeHasSelectableChild">
  45. <a-icon type="check" />选中下一级节点
  46. </a-menu-item>
  47. <a-menu-item key="2" :disabled="!selectedNodeHasSelectableChild">
  48. <a-icon type="close" />取消下一级节点
  49. </a-menu-item>
  50. </a-menu>
  51. </a-dropdown>
  52. <!-- 空span撑开加载条 -->
  53. <span></span>
  54. </a-spin>
  55. <a-empty v-if="dataLoaded && treeData.length === 0" :image="simpleImage" />
  56. </div>
  57. </template>
  58. <script>
  59. import debounce from 'lodash.debounce'
  60. import cloneDeep from 'lodash.clonedeep'
  61. import auditPermissionTreeService from '../audit-permission-tree-service'
  62. import components from './_import-components/audit-picker-source-tree-import'
  63. import pickerSourceMixin from './picker-source-mixin'
  64. import AuditPickerLabel from './audit-picker-label'
  65. function addDisableProp(data, targetKeys = []) {
  66. data.forEach((item) => {
  67. item.disabled = targetKeys.includes(item.key)
  68. if (item.children) {
  69. addDisableProp(item.children, targetKeys)
  70. }
  71. })
  72. return data
  73. }
  74. function isChecked(selectedKeys, eventKey) {
  75. return selectedKeys.indexOf(eventKey) !== -1
  76. }
  77. /**
  78. * SdValuePicker的数据源:树结构
  79. * @displayName SdPickerSourceTree 数据源-树结构
  80. */
  81. export default {
  82. name: 'AuditPickerSourceTree',
  83. components,
  84. mixins: [pickerSourceMixin],
  85. props: {
  86. /**
  87. * 根节点{code:'200000',name:'西安分公司'}
  88. */
  89. rootNode: {
  90. type: [Object, Array],
  91. default: undefined,
  92. },
  93. /**
  94. * <p>获取数据的回调,获取根节点时,parentId为undefined</p>
  95. * <pre>(parentId,parent) => childrenItems</pre>
  96. */
  97. loadTreeData: {
  98. type: Function,
  99. required: true,
  100. },
  101. /**
  102. * <p>搜索数据的回调</p>
  103. * <pre>(searchText) => items</pre>
  104. * @since 8.0.2
  105. */
  106. searchTreeData: {
  107. type: Function,
  108. default: undefined,
  109. },
  110. /**
  111. * 默认展开指定的树节点
  112. * @since 8.0.7
  113. */
  114. defaultExpandedKeys: {
  115. type: Array,
  116. default: undefined,
  117. },
  118. moduleId: {
  119. type: String,
  120. default: undefined,
  121. },
  122. // 特殊树不需要获取节点
  123. selectchecked: {
  124. type: Boolean,
  125. default: false,
  126. },
  127. },
  128. data() {
  129. return {
  130. treeData: null,
  131. searchValueDebounce: '',
  132. searchedData: [],
  133. expandedKeys: [],
  134. showMenu: false,
  135. selectedNode: undefined,
  136. dataList: [], // 数组dataList,搜索要用
  137. }
  138. },
  139. computed: {
  140. listOrTreeData() {
  141. return this.searchValueDebounce ? this.searchedData : this.treeData
  142. },
  143. treeDataComputed() {
  144. // targetKeys和disabledKeys中的都变成禁用状态
  145. return addDisableProp(this.listOrTreeData, this.targetKeys.concat(this.disabledKeys))
  146. },
  147. selectedNodeHasSelectableChild() {
  148. return this.selectedNode?.dataRef.children?.some((item) => item.checkable && !item.disabled)
  149. },
  150. },
  151. watch: {
  152. listOrTreeData(data) {
  153. // 把treeData中所有selectable的节点,转成一个列表,给父组件作为dataSource
  154. const result = []
  155. function flatten(list, that) {
  156. list = list ?? []
  157. list.forEach((item) => {
  158. if (item.checkable) {
  159. result.push(item.originalValue)
  160. }
  161. flatten(item.children, that)
  162. })
  163. }
  164. flatten(data, this)
  165. this.updateDataSource(result)
  166. },
  167. searchValue(text) {
  168. this.doSearch(text)
  169. },
  170. },
  171. created() {
  172. // this.initTreeData()
  173. // 传了searchTreeData函数,就显示搜索框
  174. // this.updateShowSearch(!!this.searchTreeData)
  175. this.loadDataForRoot()
  176. // // 用户输入搜索条件时,延迟500ms
  177. this.doSearch = debounce(this.doSearch, 500, {
  178. leading: false,
  179. trailing: true,
  180. })
  181. },
  182. methods: {
  183. transformData(data) {
  184. return data.map((d) => {
  185. const { children, checkable, selectable, ...rest } = d
  186. let isLeaf
  187. if (d.isleaf !== undefined) {
  188. isLeaf = d.isleaf
  189. }
  190. if (d.isLeaf !== undefined) {
  191. isLeaf = d.isLeaf
  192. }
  193. return {
  194. ...rest,
  195. checkable: checkable ?? true,
  196. selectable: selectable ?? false,
  197. isLeaf: isLeaf ?? true,
  198. key: d[this.optionValue],
  199. title: <AuditPickerLabel vnodes={this.render(d, 'left')} />,
  200. children: children && this.transformData(children),
  201. originalValue: rest,
  202. code: rest.code,
  203. }
  204. })
  205. },
  206. loadDataForRoot() {
  207. Promise.resolve(this.loadTreeData())
  208. .then((data) => {
  209. data[0].isLeat = false
  210. this.treeData = this.transformData(data)
  211. if (this.defaultExpandedKeys) {
  212. this.expandedKeys = this.defaultExpandedKeys
  213. } else {
  214. // 没传展开的keys,根节点只有一个的话,展开它
  215. if (this.treeData.length === 1) {
  216. this.expandedKeys = [this.treeData[0].key]
  217. }
  218. }
  219. })
  220. .finally(() => (this.dataLoaded = true))
  221. },
  222. loadData(treeNode) {
  223. return new Promise((resolve) => {
  224. if (treeNode.dataRef.children) {
  225. // 有值了直接渲染
  226. resolve()
  227. return
  228. }
  229. Promise.resolve(
  230. this.loadTreeData(treeNode.dataRef.oldId, cloneDeep(treeNode.dataRef.originalValue))
  231. ).then((data) => {
  232. treeNode.dataRef.children = this.transformData(data)
  233. if (this.searchValueDebounce) {
  234. this.searchedData = [...this.searchedData]
  235. } else {
  236. this.treeData = [...this.treeData]
  237. }
  238. resolve()
  239. })
  240. })
  241. },
  242. doSearch(text) {
  243. this.searchValueDebounce = text
  244. if (!text) return
  245. this.dataLoaded = false
  246. this.searchedData = []
  247. Promise.resolve(this.searchTreeData(text))
  248. .then((data) => {
  249. // 返回一个列表,转换成树的格式
  250. this.searchedData = this.transformData(data)
  251. })
  252. .finally(() => (this.dataLoaded = true))
  253. },
  254. onChecked(_, e, checkedKeys, itemSelect) {
  255. const { eventKey } = e.node
  256. if (this.single) {
  257. // 单选模式,取消已经选中的,再选中当前的
  258. if (!isChecked(checkedKeys, eventKey)) {
  259. itemSelect(checkedKeys[0], false)
  260. itemSelect(eventKey, true)
  261. }
  262. } else {
  263. itemSelect(eventKey, !isChecked(checkedKeys, eventKey))
  264. }
  265. },
  266. treeClick(evt, treeNode) {
  267. const nodeData = treeNode.dataRef
  268. if (nodeData.disabled) return
  269. // 非叶子节点 + 不可选中,展开节点
  270. if (!nodeData.isLeaf && !nodeData.checkable && !nodeData.selectable)
  271. treeNode.vcTree.onNodeExpand(evt, treeNode)
  272. },
  273. treeDblClick(evt, treeNode) {
  274. const nodeData = treeNode.dataRef
  275. if (nodeData.disabled) return
  276. if (nodeData.checkable) this.itemDblClick(evt, nodeData.key)
  277. },
  278. treeClickNative() {
  279. // 点击树的其他区域,隐藏菜单
  280. if (this.single) return
  281. this.hideContextMemu()
  282. },
  283. treeRightClick({ node }) {
  284. // 记录当前点击的节点
  285. if (this.single) return
  286. this.selectedNode = node
  287. },
  288. hideContextMemu() {
  289. this.showMenu = false
  290. this.menuVisibleChange(false)
  291. },
  292. menuVisibleChange(visible) {
  293. if (!visible) this.selectedNode = undefined
  294. },
  295. treeRightClickNative(evt) {
  296. // 右键如果没有点击到树节点,不触发右键菜单
  297. if (this.single) return
  298. if (evt.target.nodeName !== 'SPAN') evt.stopPropagation()
  299. },
  300. menuClick(menu) {
  301. this.selectedNode.dataRef.children?.forEach((item) => {
  302. if (item.checkable && !item.disabled) this.itemSelect(item.key, menu.key === '1')
  303. })
  304. this.hideContextMemu()
  305. },
  306. onSelect(selectedKeys, evt) {
  307. /**
  308. * 树节点的选中事件
  309. * @property {Object} treeItem 选中的节点
  310. * @property {Object} evt 其他信息{selected: bool, selectedNodes, node, event}
  311. */
  312. this.$emit('treeItemSelect', { ...evt.node.dataRef.originalValue }, evt)
  313. },
  314. // 点击地址树展开时调用
  315. onLoadData(treeNode) {
  316. const params = {
  317. moduleId: this.moduleId,
  318. }
  319. return auditPermissionTreeService
  320. .getCategoryTree(treeNode.dataRef.oldId, params)
  321. .then((res) => {
  322. res.data.forEach((item) => {
  323. item.name = item.text
  324. item.oldId = item.id
  325. if (item.props.ORG_ID !== null) {
  326. item.code = item.props.ORG_ID.toString()
  327. item.id = item.props.ORG_ID
  328. } else {
  329. item.code = item.id.toString()
  330. }
  331. item.type = 'Group'
  332. item.checkable = true
  333. })
  334. treeNode.dataRef.children = this.transformData(res.data)
  335. this.treeData = [...this.treeData]
  336. })
  337. },
  338. // 初始化地址树
  339. initTreeData(depId) {
  340. const params = {
  341. moduleId: this.moduleId,
  342. }
  343. let topDepId = null
  344. auditPermissionTreeService.getParentOrgInfo(params).then((res) => {
  345. let id = res.data[0].id
  346. let text = res.data[0].text
  347. let code = this.defaultTopNodeId
  348. let oldId = res.data[0].id
  349. if (res.data[0].props.ORG_ID !== null) {
  350. code = res.data[0].props.ORG_ID
  351. id = res.data[0].props.ORG_ID
  352. }
  353. const obj = this.rootNode
  354. topDepId = !depId ? id : depId
  355. if (obj !== undefined) {
  356. text = this.rootNode.name
  357. code = this.rootNode.code
  358. id = this.rootNode.code
  359. topDepId = this.rootNode.id
  360. oldId = this.rootNode.id
  361. }
  362. this.defaultTopNodeId = oldId
  363. this.defaultTopNodeText = text
  364. this.defaultTreeExpandedKeys = [oldId]
  365. this.defaultSelectedKeys = [oldId]
  366. // this.selectedKeys = [id]
  367. this.expandedKeys = [oldId]
  368. auditPermissionTreeService.getCategoryTree(topDepId, params).then((res1) => {
  369. this.spinning = false
  370. res1.data.forEach((item) => {
  371. item.name = item.text
  372. item.oldId = item.id
  373. if (item.props.ORG_ID !== null) {
  374. item.code = item.props.ORG_ID
  375. item.id = item.props.ORG_ID
  376. } else {
  377. item.code = item.id
  378. }
  379. item.type = 'Group'
  380. })
  381. if (res1.data.length) {
  382. const treeNode = [
  383. {
  384. id: id,
  385. oldId: oldId,
  386. text: this.defaultTopNodeText,
  387. name: this.defaultTopNodeText,
  388. title: this.defaultTopNodeText,
  389. code: id,
  390. leaf: false,
  391. isleaf: false,
  392. props: {},
  393. children: res1.data,
  394. differentDisplay: false,
  395. expandable: true,
  396. type: 'Group',
  397. },
  398. ]
  399. this.treeData = this.transformData(treeNode)
  400. this.dataLoaded = true
  401. } else {
  402. const treeNode = [
  403. {
  404. id: id,
  405. oldId: oldId,
  406. text: this.defaultTopNodeText,
  407. name: this.defaultTopNodeText,
  408. code: id,
  409. leaf: false,
  410. props: res.data[0].props,
  411. children: [],
  412. key: id,
  413. type: 'Group',
  414. },
  415. ]
  416. this.treeData = this.transformData(treeNode)
  417. this.expandedKeys = this.defaultTreeExpandedKeys
  418. this.generateList(this.treeData)
  419. this.empty = false
  420. this.dataLoaded = true
  421. }
  422. })
  423. })
  424. },
  425. // 处理搜索用的dataList
  426. generateList(data) {
  427. for (let i = 0; i < data.length; i++) {
  428. const node = data[i]
  429. const key = node.id
  430. const title = node.text
  431. const props = node.props
  432. this.dataList.push({ key, id: key, title: title, props })
  433. if (node.children) {
  434. this.generateList(node.children)
  435. }
  436. }
  437. },
  438. },
  439. }
  440. </script>
  441. <style module lang="scss">
  442. @use '@/common/design' as *;
  443. @import './source.scss';
  444. .wrapper {
  445. :global .ant-tree-title {
  446. user-select: none;
  447. }
  448. }
  449. .single {
  450. :global .ant-tree-checkbox {
  451. .ant-tree-checkbox-inner {
  452. border-radius: 100px;
  453. &::after {
  454. position: absolute;
  455. top: 3px;
  456. left: 3px;
  457. display: table;
  458. width: 8px;
  459. height: 8px;
  460. content: ' ';
  461. background-color: $primary-color;
  462. border: 0;
  463. border-radius: 8px;
  464. opacity: 0;
  465. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  466. transform: scale(0);
  467. }
  468. }
  469. &.ant-tree-checkbox-checked {
  470. &::after {
  471. position: absolute;
  472. top: 16.67%;
  473. left: 0;
  474. width: 100%;
  475. height: 66.67%;
  476. content: '';
  477. border: 1px solid #1890ff;
  478. border-radius: 50%;
  479. animation: antRadioEffect 0.36s ease-in-out;
  480. animation-fill-mode: both;
  481. }
  482. .ant-tree-checkbox-inner {
  483. background-color: $white;
  484. &::after {
  485. opacity: 1;
  486. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  487. transform: scale(1);
  488. }
  489. }
  490. }
  491. }
  492. }
  493. </style>
  494. <docs>
  495. 需要作为 [SdValuePicker](#sdvaluepicker) 的子节点使用,样例请参考 [SdValuePicker](#sdvaluepicker)
  496. </docs>