WbsTree.vue 11 KB

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