audit-tree.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <template>
  2. <a-card
  3. :class="[
  4. $style.treewrap,
  5. $style.ywlxtree,
  6. { [$style.collapse]: fold, [$style.single]: single },
  7. ]"
  8. >
  9. <!-- 选择部门 -->
  10. <a-select v-if="isSelectDep" v-model="depvalue" :options="depOptions" @change="changedep">
  11. </a-select>
  12. <!-- 搜索框 -->
  13. <a-input-search
  14. v-if="isSearch"
  15. v-model="searchStr"
  16. placeholder=""
  17. allow-clear
  18. @change="onSearchChange"
  19. />
  20. <a-spin :spinning="spinning" :class="$style.spin" />
  21. <a-empty v-if="empty" />
  22. <a-tree
  23. v-else
  24. :key="key"
  25. ref="auditTree"
  26. v-model="checkedKeys"
  27. :block-node="false"
  28. :show-icon="true"
  29. :show-line="showLine"
  30. :check-strictly="true"
  31. :checkable="checkable"
  32. :draggable="draggable"
  33. :tree-data="treeData"
  34. :replace-fields="replaceFields"
  35. :selected-keys="defaultSelectedKeys"
  36. :expanded-keys="expandedKeys"
  37. :default-expanded-keys="defaultTreeExpandedKeys"
  38. :load-data="onLoadData"
  39. @select="treeSelect"
  40. @check="treeCheck"
  41. @expand="onExpand"
  42. @dragenter="onDragEnter"
  43. @drop="onDrop"
  44. >
  45. <template v-slot:title="{ text, props }">
  46. <span v-if="text.indexOf(searchValue) > -1">
  47. {{ text.substr(0, text.indexOf(searchValue))
  48. }}<span :class="$style.active">{{ searchValue }}</span
  49. >{{ text.substr(text.indexOf(searchValue) + searchValue.length) }}</span
  50. ><span v-else>{{ text }}</span>
  51. <!-- 后台返回数量,如果有数量的话就显示 -->
  52. <span v-if="props.count > 0">({{ props.count }})</span>
  53. </template>
  54. <!-- 如果已配置负责人,则展示用户图标 -->
  55. <template slot="hasuser" slot-scope="item">
  56. <a-icon type="team" @click="showUser(item)" />
  57. </template>
  58. </a-tree>
  59. <a-button v-if="showPrimary" type="primary" :class="$style.fold" @click="foldClick">
  60. <a-icon :type="icontype" />
  61. </a-button>
  62. </a-card>
  63. </template>
  64. <script>
  65. import { message } from '@/common/one-ui'
  66. import { getUserInfo } from '@/common/store-mixin'
  67. import iamAuditTreeMixins from './iam-audit-tree-mixins'
  68. import auditTreeService from './audit-tree-service'
  69. import components from './_import-components/audit-tree-import'
  70. export default {
  71. name: 'AuditTree',
  72. metaInfo: {
  73. title: 'AuditTree',
  74. },
  75. components,
  76. mixins: [iamAuditTreeMixins],
  77. props: {
  78. // 默认选中节点
  79. selectedKeys: {
  80. type: Array,
  81. default: function() {
  82. return []
  83. },
  84. },
  85. // 是否开启搜索
  86. isSearch: {
  87. type: Boolean,
  88. default: false,
  89. },
  90. // 是否显示复选框
  91. checkable: {
  92. type: Boolean,
  93. default: false,
  94. },
  95. // 是否可以拖拽
  96. draggable: {
  97. type: Boolean,
  98. default: false,
  99. },
  100. // 是否显示连线
  101. showLine: {
  102. type: Boolean,
  103. default: false,
  104. },
  105. // 根节点名称
  106. topNodeText: {
  107. type: String,
  108. default: '根节点',
  109. },
  110. // 根节点ID
  111. topNodeId: {
  112. type: String,
  113. default: '0',
  114. },
  115. // 地址树接口数据源
  116. treeparams: {
  117. type: Object,
  118. default: () => {
  119. return {}
  120. },
  121. },
  122. // 默认展开节点id
  123. defaultExpandedKeys: {
  124. type: Array,
  125. default: () => {
  126. return ['0']
  127. },
  128. },
  129. // 是否显示部门选择下拉框
  130. isSelectDep: {
  131. type: Boolean,
  132. default: false,
  133. },
  134. // 查看模式 manager维护权限 read查看权限
  135. managerType: {
  136. type: String,
  137. default: 'manager',
  138. },
  139. // 部门下拉框默认值
  140. defaultDepValue: {
  141. type: Array,
  142. default: () => {
  143. return []
  144. },
  145. },
  146. // 是否显示隐藏按钮
  147. showPrimary: {
  148. type: Boolean,
  149. default: false,
  150. },
  151. // 地址树是否单选
  152. single: {
  153. type: Boolean,
  154. default: false,
  155. },
  156. // 是否检查单个节点权限
  157. checkNodeEdit: {
  158. type: Boolean,
  159. default: false,
  160. },
  161. },
  162. data() {
  163. return {
  164. defaultTreeExpandedKeys: ['0'],
  165. defaultSelectedKeys: ['0'],
  166. depvalue: [], // 选择的组织数据
  167. checkedKeys: [], // 选中的节点数据
  168. treeData: [],
  169. dataList: [], // 数组dataList,搜索要用
  170. spinning: true,
  171. empty: false,
  172. icontype: 'left',
  173. fold: false,
  174. replaceFields: {
  175. title: 'text',
  176. key: 'id',
  177. },
  178. expandedKeys: ['0'],
  179. backupsExpandedKeys: [],
  180. autoExpandParent: false,
  181. searchValue: '',
  182. searchStr: '',
  183. defaultTopNodeText: '根节点',
  184. defaultTopNodeId: '0',
  185. key: 0,
  186. depOptions: [], // 部门下拉框列表值
  187. edit: true, // 当前节点是否可编辑
  188. parentId: null, // 当前选中的节点
  189. userRoles: getUserInfo().roles.find((item) => {
  190. return item.code === 'ADMINISTRATOR'
  191. }),
  192. }
  193. },
  194. methods: {
  195. // 刷新树节点
  196. refreshNode(nodeId) {
  197. this.key++
  198. this.parentId = nodeId
  199. this.refreshData(this.treeData[0])
  200. },
  201. // 刷新树节点
  202. refreshData(treeData, isedit) {
  203. if (treeData.id + '' === this.parentId + '') {
  204. const params = {
  205. type: this.treeparams.type,
  206. }
  207. return auditTreeService
  208. .getCategoryTree(treeData.id, this.treeparams, params)
  209. .then((res) => {
  210. // 继承disabled属性
  211. if (this.checkNodeEdit && !this.userRoles) {
  212. res.data.forEach((item) => {
  213. const node = treeData.children.find((nodeinfo) => {
  214. return nodeinfo.id + '' === item.id + ''
  215. })
  216. // 如果是旧节点,则直接赋值
  217. if (node) {
  218. item.disabled = node.disabled
  219. } else {
  220. // 新增的节点信息,需要重新判断层级资源是否可编辑
  221. const depnode = this.depOptions.find((depinfo) => {
  222. return depinfo.value + '' === item.id + ''
  223. })
  224. // 找到层级资源对应的部门
  225. if (depnode) {
  226. item.props.edit = depnode.edit
  227. item.disabled = !depnode.edit
  228. } else {
  229. item.props.edit = false
  230. item.disabled = true
  231. }
  232. // 如果有传是否可以编辑,则说明是从上一节点找到的下节点,直接继承上节点的编辑状态
  233. if (isedit !== undefined) {
  234. item.props.edit = isedit
  235. item.disabled = !isedit
  236. }
  237. }
  238. })
  239. }
  240. treeData.children = res.data
  241. if (res.data.length > 0) {
  242. treeData.isLeaf = false
  243. if (this.expandedKeys.indexOf(treeData.id) === -1) {
  244. this.expandedKeys = new Set(this.expandedKeys.push(treeData.id))
  245. }
  246. } else {
  247. treeData.isLeaf = true
  248. }
  249. return false
  250. })
  251. }
  252. if (treeData.children) {
  253. treeData.children.forEach((item) => {
  254. this.refreshData(item, item.edit)
  255. })
  256. }
  257. },
  258. onDragEnter(info) {
  259. // expandedKeys 需要受控时设置
  260. this.expandedKeys = info.expandedKeys
  261. },
  262. // 拖拽节点方法
  263. onDrop(info) {
  264. // 被插入节点信息
  265. const dropKey = info.node.eventKey
  266. // 拖拽节点信息
  267. const dragKey = info.dragNode.eventKey
  268. const dropPos = info.node.pos.split('-')
  269. // -1表示之前 0表示合并 1表示之后
  270. const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1])
  271. const loop = (data, key, callback) => {
  272. data.forEach((item, index, arr) => {
  273. if (item.id === key) {
  274. return callback(item, index, arr)
  275. }
  276. if (item.children) {
  277. return loop(item.children, key, callback)
  278. }
  279. })
  280. }
  281. const data = this.treeData
  282. // Find dragObject
  283. let dragObj
  284. loop(data, dragKey, (item, index, arr) => {
  285. arr.splice(index, 1)
  286. dragObj = item
  287. })
  288. if (!info.dropToGap) {
  289. // Drop on the content
  290. loop(data, dropKey, (item) => {
  291. item.children = item.children || []
  292. // where to insert 示例添加到尾部,可以是随意位置
  293. item.children.push(dragObj)
  294. })
  295. } else if (
  296. (info.node.children || []).length > 0 && // Has children
  297. info.node.expanded && // Is expanded
  298. dropPosition === 1 // On the bottom gap
  299. ) {
  300. loop(data, dropKey, (item) => {
  301. item.children = item.children || []
  302. // where to insert 示例添加到尾部,可以是随意位置
  303. item.children.unshift(dragObj)
  304. })
  305. } else {
  306. let ar
  307. let i
  308. loop(data, dropKey, (item, index, arr) => {
  309. ar = arr
  310. i = index
  311. })
  312. if (dropPosition === -1) {
  313. ar.splice(i, 0, dragObj)
  314. } else {
  315. ar.splice(i + 1, 0, dragObj)
  316. }
  317. }
  318. this.treeData = data
  319. // 拖拽完成,调用接口
  320. const dragParams = {
  321. configId: this.treeparams.configId,
  322. id: dragKey,
  323. targetId: dropKey,
  324. position: dropPosition,
  325. }
  326. auditTreeService.dragNode(dragParams).then((res) => {
  327. if (res) {
  328. this.$emit('dragNodeOk')
  329. } else {
  330. message.error('节点拖拽失败,目标节点下编号冲突')
  331. }
  332. })
  333. },
  334. // 点击地址树展开时调用
  335. onLoadData(treeNode) {
  336. const params = {
  337. type: this.treeparams.type,
  338. }
  339. return auditTreeService
  340. .getCategoryTree(treeNode.dataRef.id, this.treeparams, params)
  341. .then((res) => {
  342. treeNode.dataRef.children = res.data
  343. this.treeData = this.transformData([...this.treeData])
  344. this.generateList(this.treeData)
  345. })
  346. },
  347. // 初始化地址树
  348. initTreeData(depId) {
  349. const params = {
  350. type: this.treeparams.type,
  351. }
  352. const topDepId = !depId ? 0 : depId
  353. auditTreeService.getCategoryTree(topDepId, this.treeparams, params).then((res) => {
  354. this.spinning = false
  355. const treeNode = [
  356. {
  357. id: this.defaultTopNodeId,
  358. text: this.defaultTopNodeText,
  359. isLeaf: false,
  360. props: {
  361. isroot: true,
  362. },
  363. children: [],
  364. key: this.defaultTopNodeId,
  365. },
  366. ]
  367. if (res.data.length) {
  368. treeNode.children = res.data
  369. }
  370. this.treeData = this.transformData(treeNode)
  371. this.expandedKeys = this.defaultTreeExpandedKeys
  372. this.generateList(this.treeData)
  373. this.empty = false
  374. })
  375. },
  376. beforeInitTreeData(formId) {
  377. auditTreeService.getManagedHierarchyOrg(formId).then((res) => {
  378. res.data.editNodes.forEach((item) => {
  379. this.depOptions.push({
  380. label: item.text,
  381. value: item.id,
  382. text: item.text,
  383. edit: true,
  384. })
  385. })
  386. if (this.managerType !== 'manager') {
  387. // 取编辑和查看节点并集
  388. res.data.viewNodes.forEach((item, index) => {
  389. const node = this.depOptions.find((nodeinfo) => {
  390. return nodeinfo.value === item.id
  391. })
  392. if (!node) {
  393. this.depOptions.push({
  394. label: item.text,
  395. value: item.id,
  396. text: item.text,
  397. edit: false,
  398. })
  399. }
  400. })
  401. }
  402. // 部门下拉框列表值赋值
  403. this.initTreeData()
  404. })
  405. },
  406. },
  407. }
  408. </script>
  409. <style module lang="scss">
  410. @use '@/common/design' as *;
  411. .spin {
  412. width: 100%;
  413. line-height: 30;
  414. }
  415. .ywlxtree {
  416. :global(.ant-tree-title) {
  417. display: inline-block;
  418. width: 100%;
  419. overflow: hidden;
  420. text-overflow: ellipsis;
  421. white-space: nowrap;
  422. }
  423. :global(.ant-input-search) {
  424. margin: 8px 0;
  425. overflow: hidden;
  426. }
  427. .active {
  428. color: $primary-color;
  429. }
  430. :global(.ant-select) {
  431. width: 100%;
  432. overflow: hidden;
  433. }
  434. }
  435. .treewrap {
  436. position: relative;
  437. display: flex;
  438. flex-direction: column;
  439. width: 20%;
  440. min-height: 100%;
  441. margin-right: $padding-lg;
  442. transition: width 0.2s;
  443. .fold {
  444. position: absolute;
  445. top: calc(50% - 30px);
  446. right: -15px;
  447. z-index: 2;
  448. width: 15px;
  449. height: 75px;
  450. padding: 0;
  451. border-radius: 0 10px 10px 0;
  452. }
  453. :global(.ant-tree) {
  454. overflow: hidden;
  455. text-overflow: ellipsis;
  456. white-space: nowrap;
  457. }
  458. :global(.ant-card-body) {
  459. overflow: auto;
  460. background: $white;
  461. }
  462. }
  463. .collapse {
  464. width: 0;
  465. :global(.ant-card-body) {
  466. background: transparent;
  467. :global(.ant-empty) {
  468. display: none;
  469. }
  470. }
  471. }
  472. // 单选样式
  473. .single {
  474. :global .ant-tree-checkbox {
  475. .ant-tree-checkbox-inner {
  476. border-radius: 100px;
  477. &::after {
  478. position: absolute;
  479. top: 3px;
  480. left: 3px;
  481. display: table;
  482. width: 8px;
  483. height: 8px;
  484. content: ' ';
  485. background-color: $primary-color;
  486. border: 0;
  487. border-radius: 8px;
  488. opacity: 0;
  489. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  490. transform: scale(0);
  491. }
  492. }
  493. &.ant-tree-checkbox-checked {
  494. &::after {
  495. position: absolute;
  496. top: 16.67%;
  497. left: 0;
  498. width: 100%;
  499. height: 66.67%;
  500. content: '';
  501. border: 1px solid #1890ff;
  502. border-radius: 50%;
  503. animation: antRadioEffect 0.36s ease-in-out;
  504. animation-fill-mode: both;
  505. }
  506. .ant-tree-checkbox-inner {
  507. background-color: $white;
  508. &::after {
  509. opacity: 1;
  510. transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  511. transform: scale(1);
  512. }
  513. }
  514. }
  515. }
  516. }
  517. </style>