ic-picker-source-tree.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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. v-model="checkedKeys"
  23. :load-data="onLoadData"
  24. :tree-data="treeDataComputed"
  25. checkable
  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" />选中下一级节点1
  46. </a-menu-item>
  47. <a-menu-item key="2" :disabled="!selectedNodeHasSelectableChild">
  48. <a-icon type="close" />取消下一级节点2
  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 icTreeService from './ic-tree-service'
  63. import components from './_import-components/ic-picker-source-tree-import'
  64. import pickerSourceMixin from './picker-source-mixin'
  65. import IcPickerLabel from './ic-picker-label'
  66. function addDisableProp(data, targetKeys = []) {
  67. data.forEach((item) => {
  68. item.disabled = targetKeys.includes(item.key)
  69. if (item.children) {
  70. addDisableProp(item.children, targetKeys)
  71. }
  72. })
  73. return data
  74. }
  75. function isChecked(selectedKeys, eventKey) {
  76. return selectedKeys.indexOf(eventKey) !== -1
  77. }
  78. /**
  79. * SdValuePicker的数据源:树结构
  80. * @displayName SdPickerSourceTree 数据源-树结构
  81. */
  82. export default {
  83. name: 'IcPickerSourceTree',
  84. components,
  85. mixins: [pickerSourceMixin],
  86. props: {
  87. /**
  88. * 根节点{code:'200000',name:'西安分公司'}
  89. */
  90. rootNode: {
  91. type: [Object, Array],
  92. default: undefined,
  93. },
  94. /**
  95. * <p>获取数据的回调,获取根节点时,parentId为undefined</p>
  96. * <pre>(parentId,parent) => childrenItems</pre>
  97. */
  98. loadTreeData: {
  99. type: Function,
  100. required: true,
  101. },
  102. /**
  103. * <p>搜索数据的回调</p>
  104. * <pre>(searchText) => items</pre>
  105. * @since 8.0.2
  106. */
  107. searchTreeData: {
  108. type: Function,
  109. default: undefined,
  110. },
  111. /**
  112. * 默认展开指定的树节点
  113. * @since 8.0.7
  114. */
  115. defaultExpandedKeys: {
  116. type: Array,
  117. default: undefined,
  118. },
  119. versionId: {
  120. type: String,
  121. default: '',
  122. },
  123. orgId: {
  124. type: String,
  125. default: '',
  126. },
  127. selecttype: {
  128. type: String,
  129. default: 'control',
  130. },
  131. types: {
  132. type: String,
  133. default: 'NK',
  134. },
  135. isAbandonment: {
  136. type: String,
  137. default: 'NO',
  138. },
  139. // moduleId: {
  140. // type: String,
  141. // default: undefined,
  142. // },
  143. },
  144. data() {
  145. return {
  146. treeData: null,
  147. searchValueDebounce: '',
  148. searchedData: [],
  149. expandedKeys: [],
  150. showMenu: false,
  151. selectedNode: undefined,
  152. dataList: [], // 数组dataList,搜索要用
  153. checkedKeys: { checked: [], halfChecked: [] }, // 选中的节点数据
  154. halfCheckedKeys: [],
  155. isendids: [],
  156. }
  157. },
  158. computed: {
  159. listOrTreeData() {
  160. return this.searchValueDebounce ? this.searchedData : this.treeData
  161. },
  162. treeDataComputed() {
  163. // targetKeys和disabledKeys中的都变成禁用状态
  164. return addDisableProp(this.listOrTreeData, this.targetKeys.concat(this.disabledKeys))
  165. },
  166. selectedNodeHasSelectableChild() {
  167. return this.selectedNode?.dataRef.children?.some((item) => item.checkable && !item.disabled)
  168. },
  169. },
  170. watch: {
  171. listOrTreeData(data) {
  172. // 把treeData中所有selectable的节点,转成一个列表,给父组件作为dataSource
  173. const result = []
  174. function flatten(list, that) {
  175. list = list ?? []
  176. list.forEach((item) => {
  177. if (item.checkable) {
  178. result.push(item.originalValue)
  179. }
  180. flatten(item.children, that)
  181. })
  182. }
  183. flatten(data, this)
  184. this.updateDataSource(result)
  185. },
  186. searchValue(text) {
  187. this.doSearch(text)
  188. },
  189. },
  190. created() {
  191. this.loadDataForRoot()
  192. },
  193. methods: {
  194. transformData(data) {
  195. return data.map((d) => {
  196. const { children, checkable, selectable, isLeaf, ...rest } = d
  197. return {
  198. ...rest,
  199. checkable: checkable ?? true,
  200. selectable: selectable ?? false,
  201. isLeaf: isLeaf ?? true,
  202. key: d[this.optionValue],
  203. title: <IcPickerLabel vnodes={this.render(d, 'left')} />,
  204. children: children && this.transformData(children),
  205. originalValue: rest,
  206. code: rest.code,
  207. }
  208. })
  209. },
  210. loadDataForRoot() {
  211. Promise.resolve(this.loadTreeData())
  212. .then((data) => {
  213. if (this.selecttype === 'cross') {
  214. data.forEach((i) => {
  215. if (i.children) {
  216. i.children.forEach((c) => {
  217. if (c.props.isEnd === '0') {
  218. c.isLeaf = false
  219. c.cat = true
  220. } else {
  221. c.cat = false
  222. this.isendids.push(c.id.toString())
  223. }
  224. })
  225. }
  226. })
  227. }
  228. this.treeData = this.transformData(data)
  229. if (this.defaultExpandedKeys) {
  230. this.expandedKeys = this.defaultExpandedKeys
  231. } else {
  232. // 没传展开的keys,根节点只有一个的话,展开它
  233. if (this.treeData.length === 1) {
  234. this.expandedKeys = [this.treeData[0].key]
  235. }
  236. }
  237. })
  238. .finally(() => (this.dataLoaded = true))
  239. },
  240. loadData(treeNode) {
  241. return new Promise((resolve) => {
  242. if (treeNode.dataRef.children) {
  243. // 有值了直接渲染
  244. resolve()
  245. return
  246. }
  247. Promise.resolve(
  248. this.loadTreeData(treeNode.dataRef.oldId, cloneDeep(treeNode.dataRef.originalValue))
  249. ).then((data) => {
  250. treeNode.dataRef.children = this.transformData(data)
  251. if (this.searchValueDebounce) {
  252. this.searchedData = [...this.searchedData]
  253. } else {
  254. this.treeData = [...this.treeData]
  255. }
  256. resolve()
  257. })
  258. })
  259. },
  260. doSearch(text) {
  261. this.searchValueDebounce = text
  262. if (!text) return
  263. this.dataLoaded = false
  264. this.searchedData = []
  265. Promise.resolve(this.searchTreeData(text))
  266. .then((data) => {
  267. // 返回一个列表,转换成树的格式
  268. this.searchedData = this.transformData(data)
  269. })
  270. .finally(() => (this.dataLoaded = true))
  271. },
  272. onChecked(_, e, checkedKeys, itemSelect) {
  273. const { eventKey } = e.node
  274. if (this.single) {
  275. // 单选模式,取消已经选中的,再选中当前的
  276. if (!isChecked(checkedKeys, eventKey)) {
  277. itemSelect(checkedKeys[0], false)
  278. itemSelect(eventKey, true)
  279. }
  280. } else {
  281. itemSelect(eventKey, !isChecked(checkedKeys, eventKey))
  282. if (this.selecttype === 'cross') {
  283. this.halfCheckedKeys = _.filter((i) => this.isendids.indexOf(i) === -1)
  284. // this.halfCheckedKeys = _.filter((i) => i.indexOf('1000') === -1)
  285. } else {
  286. this.halfCheckedKeys = _.filter((i) => i.indexOf('1000') === -1)
  287. }
  288. }
  289. },
  290. treeClick(evt, treeNode) {
  291. const nodeData = treeNode.dataRef
  292. if (nodeData.disabled) return
  293. // 非叶子节点 + 不可选中,展开节点
  294. if (!nodeData.isLeaf && !nodeData.checkable && !nodeData.selectable)
  295. treeNode.vcTree.onNodeExpand(evt, treeNode)
  296. },
  297. treeDblClick(evt, treeNode) {
  298. const nodeData = treeNode.dataRef
  299. if (nodeData.disabled) return
  300. if (nodeData.checkable) this.itemDblClick(evt, nodeData.key)
  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. const params = {
  341. auditOrgId: parseInt(this.orgId),
  342. versionId: parseInt(this.versionId),
  343. isAbandonment: this.isAbandonment,
  344. }
  345. if (this.selecttype === 'cross') {
  346. return icTreeService
  347. .getCategoryTree(treeNode.dataRef.id, params, this.types)
  348. .then((res) => {
  349. if (treeNode.dataRef.children !== null && treeNode.dataRef.children.length > 0) {
  350. console.log('有子节点,不请求')
  351. } else {
  352. res.data.forEach((item) => {
  353. item.code = item.id.toString()
  354. if (item.props.isEnd === '0') {
  355. item.isLeaf = false
  356. item.cat = true
  357. } else {
  358. item.cat = false
  359. this.isendids.push(item.id.toString())
  360. }
  361. })
  362. treeNode.dataRef.children = this.transformData(res.data)
  363. this.treeData = [...this.treeData]
  364. }
  365. })
  366. } else {
  367. return icTreeService.getMeasureTree(treeNode.dataRef.id, params, this.types).then((res) => {
  368. if (treeNode.dataRef.children !== null && treeNode.dataRef.children.length > 0) {
  369. console.log('有子节点,不请求')
  370. } else {
  371. res.data.forEach((item) => {
  372. item.code = item.id.toString()
  373. if (item.props.disabled) {
  374. item.isLeaf = false
  375. item.cat = true
  376. } else {
  377. item.cat = false
  378. }
  379. })
  380. treeNode.dataRef.children = this.transformData(res.data)
  381. this.treeData = [...this.treeData]
  382. }
  383. })
  384. }
  385. },
  386. // 初始化地址树
  387. initTreeData(depId) {
  388. const topDepId = -1
  389. const params = {
  390. auditOrgId: 521,
  391. versionId: 341,
  392. }
  393. icTreeService.getCategoryTree(topDepId, params).then((res1) => {
  394. this.spinning = false
  395. const treeNode = res1.data
  396. this.treeData = this.transformData(treeNode)
  397. this.expandedKeys = this.defaultTreeExpandedKeys
  398. this.generateList(this.treeData)
  399. this.empty = false
  400. this.dataLoaded = true
  401. })
  402. },
  403. // 处理搜索用的dataList
  404. generateList(data) {
  405. for (let i = 0; i < data.length; i++) {
  406. const node = data[i]
  407. const key = node.id
  408. const title = node.text
  409. const props = node.props
  410. this.dataList.push({ key, id: key, title: title, props })
  411. if (node.children) {
  412. this.generateList(node.children)
  413. }
  414. }
  415. },
  416. },
  417. }
  418. </script>
  419. <style module lang="scss">
  420. @use '@/common/design' as *;
  421. @import './source.scss';
  422. .wrapper {
  423. :global .ant-tree-title {
  424. user-select: none;
  425. overflow: hidden;
  426. text-overflow: ellipsis;
  427. width: 200px;
  428. display: block;
  429. }
  430. }
  431. .single {
  432. :global .ant-tree-checkbox {
  433. .ant-tree-checkbox-inner {
  434. border-radius: 100px;
  435. &::after {
  436. position: absolute;
  437. top: 3px;
  438. left: 3px;
  439. display: table;
  440. width: 8px;
  441. height: 8px;
  442. content: ' ';
  443. background-color: $primary-color;
  444. border: 0;
  445. border-radius: 8px;
  446. opacity: 0;
  447. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  448. transform: scale(0);
  449. }
  450. }
  451. &.ant-tree-checkbox-checked {
  452. &::after {
  453. position: absolute;
  454. top: 16.67%;
  455. left: 0;
  456. width: 100%;
  457. height: 66.67%;
  458. content: '';
  459. border: 1px solid #1890ff;
  460. border-radius: 50%;
  461. animation: antRadioEffect 0.36s ease-in-out;
  462. animation-fill-mode: both;
  463. }
  464. .ant-tree-checkbox-inner {
  465. background-color: $white;
  466. &::after {
  467. opacity: 1;
  468. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  469. transform: scale(1);
  470. }
  471. }
  472. }
  473. }
  474. }
  475. </style>
  476. <docs>
  477. 需要作为 [SdValuePicker](#sdvaluepicker) 的子节点使用,样例请参考 [SdValuePicker](#sdvaluepicker)
  478. </docs>