WbsTree.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <template>
  2. <ElTree :key="treeKey" ref="ElTreeRef"
  3. :class="[ui,submitCounts?'tree-line1':'']" :default-expanded-keys="defaultExpandedCids" :indent="0"
  4. :load="ElTreeLoadNode"
  5. :props="ElTreeProps" accordion
  6. class="hc-tree-node tree-line el-radio-group" highlight-current
  7. lazy node-key="primaryKeyId" @node-click="ElTreeClick"
  8. @node-contextmenu="ElTreeLabelContextMenu">
  9. <template #default="{ node, data }">
  10. <div :id="`${idPrefix}${data['primaryKeyId']}`" class="data-custom-tree-node">
  11. <!--树组件,节点名称-->
  12. <div :class="node.level === 1?'level-name':''" class="label">
  13. <span
  14. v-if="isColor"
  15. :class="data?.colorStatus === 2?'text-blue':data?.colorStatus === 3?'text-orange':data?.colorStatus === 4?'text-green':''">{{
  16. node.label
  17. }}</span>
  18. <span v-else>{{ node.label }}</span>
  19. </div>
  20. <!--树组件,统计数量-->
  21. <div v-if="isSubmitCounts" class="text-blue submit-counts">
  22. 【{{ data.submitCounts ?? 0 }}】
  23. </div>
  24. <!--树组件,操作菜单-->
  25. <div v-if="node.level !== 1 && menusData.length > 0" :class="node.showTreeMenu?'show':''"
  26. class="menu-icon1">
  27. <div class="cu-tree-node-popover-menu-icon"
  28. @click.prevent.stop="ElTreeLabelContextMenu($event,data,node)">
  29. <HcIcon name="apps" ui="text-2xl"/>
  30. </div>
  31. </div>
  32. <!--树组件,操作菜单 END-->
  33. </div>
  34. </template>
  35. </ElTree>
  36. <!--右键菜单-->
  37. <HcContextMenu v-if="menusData.length > 0" ref="contextMenuRef" :datas="menusData" @closed="handleMenuClosed"
  38. @item-click="handleMenuSelect">
  39. <template #mark="{item}">
  40. <HcIcon :fill="treeRefData?.isFirst" :name="item.icon" class="menu-item-icon"/>
  41. <span class="menu-item-name">{{ treeRefData?.isFirst ? '取消标记为首件' : '标记为首件' }}</span>
  42. </template>
  43. <template #sort="{item}">
  44. <HcIcon :line="false" :name="item.icon" class="menu-item-icon"/>
  45. <span class="menu-item-name">{{ item.label }}</span>
  46. </template>
  47. </HcContextMenu>
  48. </template>
  49. <script setup>
  50. import {ref, nextTick, watch} from "vue";
  51. import dataFillQuery from '~api/data-fill/query';
  52. import {isItem, getArrValue, getObjValue, isValueNull} from "vue-utils-plus"
  53. //参数
  54. const props = defineProps({
  55. menus: {
  56. type: Array,
  57. default: () => ([])
  58. },
  59. projectId: {
  60. type: [String, Number],
  61. default: ''
  62. },
  63. contractId: {
  64. type: [String, Number],
  65. default: ''
  66. },
  67. autoExpandKeys: {
  68. type: Array,
  69. default: () => ([])
  70. },
  71. isMark: {
  72. type: Boolean,
  73. default: false
  74. },
  75. idPrefix: {
  76. type: String,
  77. default: 'wbs-tree-'
  78. },
  79. isAutoKeys: {
  80. type: Boolean,
  81. default: true
  82. },
  83. isAutoClick: {
  84. type: Boolean,
  85. default: true
  86. },
  87. isColor: {
  88. type: Boolean,
  89. default: false
  90. },
  91. ui: {
  92. type: String,
  93. default: ''
  94. },
  95. submitCounts: {
  96. type: Boolean,
  97. default: false
  98. },
  99. classifyType: {
  100. type: String,
  101. },
  102. treeKey: {
  103. type: [String, Number],
  104. }
  105. })
  106. //变量
  107. const ElTreeRef = ref(null)
  108. const treeRefNode = ref(null)
  109. const treeRefData = ref(null)
  110. const ElTreeProps = ref({
  111. label: 'title',
  112. children: 'children',
  113. isLeaf: 'notExsitChild'
  114. })
  115. const menusData = ref(props.menus)
  116. const menuMark = ref(props.isMark)
  117. const isAutoKeys = ref(props.isAutoKeys)
  118. const TreeExpandKey = ref(props.autoExpandKeys)
  119. const projectId = ref(props.projectId);
  120. const contractId = ref(props.contractId);
  121. const idPrefix = ref(props.idPrefix);
  122. const isSubmitCounts = ref(props.submitCounts);
  123. const classifyTypedata = ref(props.classifyType);
  124. const treeKeyData = ref(props.treeKey)
  125. //监听
  126. watch(() => [
  127. props.menus,
  128. props.isMark,
  129. props.isAutoKeys,
  130. props.autoExpandKeys,
  131. props.projectId,
  132. props.contractId,
  133. props.idPrefix,
  134. props.submitCounts,
  135. props.classifyType,
  136. props.treeKey,
  137. ], ([menus, isMark, AutoKeys, expandKeys, UserProjectId, UserContractId, UserIdPrefix, submitCounts, ClassifyType, TreeKey]) => {
  138. menusData.value = menus
  139. menuMark.value = isMark
  140. isAutoKeys.value = AutoKeys
  141. TreeExpandKey.value = expandKeys
  142. projectId.value = UserProjectId
  143. contractId.value = UserContractId
  144. idPrefix.value = UserIdPrefix
  145. isSubmitCounts.value = submitCounts
  146. classifyTypedata.value = ClassifyType
  147. treeKeyData.value = TreeKey
  148. })
  149. //事件
  150. const emit = defineEmits(['menuTap', 'nodeTap', 'nodeLoading'])
  151. //树形结构异步加载数据
  152. const defaultExpandedCids = ref([])
  153. const rootNode = ref({})
  154. const rootResolve = ref(null)
  155. const ElTreeLoadNode = async (node, resolve) => {
  156. let contractIdRelation = '', parentId = '', primaryKeyId = '';
  157. if (node.level !== 0) {
  158. rootNode.value = node
  159. rootResolve.value = resolve
  160. const nodeData = getObjValue(node?.data);
  161. contractIdRelation = nodeData?.contractIdRelation || ''
  162. parentId = contractIdRelation ? nodeData?.primaryKeyId : nodeData?.id
  163. primaryKeyId = nodeData?.id || ''
  164. }
  165. //获取数据
  166. const {error, code, data} = await dataFillQuery.queryWbsTreeData({
  167. contractId: contractId.value || '',
  168. contractIdRelation,
  169. primaryKeyId,
  170. parentId,
  171. classifyType: classifyTypedata.value
  172. })
  173. //处理数据
  174. if (!error && code === 200) {
  175. let clickKey = '', defaultExpandedArr = [];
  176. const keys = TreeExpandKey.value || []
  177. const resData = getArrValue(data)
  178. if (keys.length > 0) {
  179. let lastKey = keys[keys.length - 1];
  180. for (const item of resData) {
  181. //自动展开
  182. if (isItem(keys, item?.primaryKeyId)) {
  183. defaultExpandedArr.push(item?.primaryKeyId)
  184. }
  185. //最后一个,选中点击
  186. if (item?.primaryKeyId === lastKey) {
  187. clickKey = item?.primaryKeyId
  188. }
  189. }
  190. } else if (node.level === 0) {
  191. defaultExpandedArr.push(resData[0]?.primaryKeyId)
  192. }
  193. //自动展开
  194. defaultExpandedCids.value = defaultExpandedArr
  195. if (node.level === 0) {
  196. emit('nodeLoading')
  197. }
  198. resolve(resData)
  199. //最后一个,执行点击
  200. if (props.isAutoClick && clickKey) {
  201. await nextTick(() => {
  202. document.getElementById(`${idPrefix.value}${clickKey}`)?.click()
  203. })
  204. }
  205. } else {
  206. if (node.level === 0) {
  207. emit('nodeLoading')
  208. }
  209. resolve([])
  210. }
  211. }
  212. watch(classifyTypedata, (val) => {
  213. if (val) {
  214. classifyTypedata.value = val
  215. }
  216. },
  217. {immediate: true}
  218. )
  219. //节点被点击
  220. const ElTreeClick = async (data, node) => {
  221. if (isAutoKeys.value) {
  222. let autoKeysArr = []
  223. await getNodeExpandKeys(node, autoKeysArr)
  224. const autoKeys = autoKeysArr.reverse()
  225. emit('nodeTap', {node, data, keys: autoKeys})
  226. } else {
  227. emit('nodeTap', {node, data, keys: []})
  228. }
  229. }
  230. //处理自动展开的节点KEY
  231. const getNodeExpandKeys = async (node, newKeys) => {
  232. const parent = node?.parent ?? []
  233. const primaryKeyId = node?.data?.primaryKeyId ?? ''
  234. if (primaryKeyId) {
  235. newKeys.push(primaryKeyId)
  236. await getNodeExpandKeys(parent, newKeys)
  237. }
  238. }
  239. //鼠标右键事件
  240. const contextMenuRef = ref(null)
  241. const ElTreeLabelContextMenu = (e, data, node) => {
  242. const rows = menusData.value || [];
  243. if (node.level !== 1 && rows.length > 0) {
  244. e.preventDefault();
  245. treeRefNode.value = node;
  246. treeRefData.value = data;
  247. node.showTreeMenu = true
  248. //展开菜单
  249. contextMenuRef.value?.showMenu(e)
  250. }
  251. }
  252. //鼠标右键菜单被点击
  253. const handleMenuSelect = async ({key}) => {
  254. const node = treeRefNode.value;
  255. const data = treeRefData.value;
  256. //如果为标记菜单
  257. if (key === 'mark' && menuMark.value) {
  258. if (data.isFirst === true) {
  259. emit('menuTap', {key: 'cancel_mark', node, data})
  260. } else {
  261. emit('menuTap', {key: 'mark', node, data})
  262. }
  263. } else {
  264. if (isAutoKeys.value) {
  265. let autoKeysArr = []
  266. await getNodeExpandKeys(node, autoKeysArr)
  267. const autoKeys = autoKeysArr.reverse()
  268. emit('menuTap', {key, node, data, keys: autoKeys})
  269. }
  270. }
  271. }
  272. const handleMenuClosed = () => {
  273. const node = treeRefNode.value;
  274. if (!isValueNull(node)) {
  275. treeRefNode.value['showTreeMenu'] = false
  276. }
  277. }
  278. //设置树菜单的标记数据
  279. const setElTreeMenuMark = (keys, isFirst) => {
  280. keys.forEach(item => {
  281. //根据 data 或者 key 拿到 Tree 组件中的 node
  282. let node = ElTreeRef.value.getNode(item)
  283. if (!!node) node.data.isFirst = isFirst;
  284. })
  285. }
  286. //设置树菜单的标记数据
  287. const removeElTreeNode = (key) => {
  288. //根据 data 或者 key 拿到 Tree 组件中的 node
  289. let node = ElTreeRef.value.getNode(key)
  290. //删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
  291. ElTreeRef.value.remove(node)
  292. }
  293. // 暴露出去
  294. defineExpose({
  295. setElTreeMenuMark,
  296. removeElTreeNode,
  297. // resetNode
  298. })
  299. </script>
  300. <style lang="scss" scoped>
  301. @import "../../../styles/app/tree.scss";
  302. .el-radio-group {
  303. width: 100% !important;
  304. display: inline-grid;
  305. }
  306. .data-custom-tree-node {
  307. flex: 1;
  308. white-space: nowrap;
  309. overflow: hidden;
  310. text-overflow: ellipsis;
  311. .label {
  312. flex: 1;
  313. white-space: nowrap;
  314. overflow: hidden;
  315. text-overflow: ellipsis;
  316. }
  317. .submit-counts {
  318. position: unset;
  319. font-size: 14px;
  320. }
  321. .menu-icon1 {
  322. position: unset;
  323. vertical-align: bottom;
  324. display: inline-block;
  325. pointer-events: none;
  326. transition: opacity 0.2s;
  327. opacity: 0;
  328. right: 0;
  329. background: rgba(255, 255, 255, 0.25);
  330. border-radius: 2px;
  331. .cu-tree-node-popover-menu-icon {
  332. display: flex;
  333. align-items: center;
  334. justify-content: center;
  335. }
  336. }
  337. &:hover {
  338. .menu-icon1 {
  339. opacity: 1;
  340. pointer-events: all;
  341. cursor: context-menu;
  342. }
  343. }
  344. .menu-icon1.show {
  345. opacity: 1;
  346. pointer-events: all;
  347. cursor: context-menu;
  348. }
  349. }
  350. </style>
  351. <style lang="scss">
  352. .el-tree.hc-tree-node .el-tree-node {
  353. white-space: nowrap;
  354. overflow: hidden;
  355. text-overflow: ellipsis;
  356. .el-tree-node_content {
  357. white-space: nowrap;
  358. overflow: hidden;
  359. text-overflow: ellipsis;
  360. }
  361. }
  362. </style>