law-case-picker-source-tree.vue 12 KB

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