hc-tree.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. <template>
  2. <el-radio-group v-model="radioKeys" @change="treeRadioChange">
  3. <ElTree class="hc-tree-node tree-line" :class="ui" ref="ElTreeRef" :props="ElTreeProps" :load="ElTreeLoadNode" lazy highlight-current accordion node-key="id"
  4. :default-expanded-keys="defaultExpandedCids" @node-click="ElTreeClick" @node-contextmenu="ElTreeLabelContextMenu" :indent="0" >
  5. <template #default="{ node, data }">
  6. <div class="data-custom-tree-node" :id="`${idPrefix}${data['id']}`">
  7. <!--树组件,节点名称-->
  8. <div class="label level-name" v-if="node.level === 1" >{{ node.label }}</div>
  9. <div class="label" v-else>
  10. <el-radio class="size-xl" :label="data['id']" @click.stop v-if="isRadio && showRadioFun(data)">{{ node.label }}</el-radio>
  11. <span v-else>{{ node.label }}</span>
  12. <div class="menu-icon1" :class="node.showTreeMenu?'show':''" v-if="node.level !== 1 && menusData.length > 0">
  13. <div class="cu-tree-node-popover-menu-icon" @click.prevent.stop="ElTreeLabelContextMenu($event,data,node)">
  14. <HcIcon name="apps" ui="text-2xl"/>
  15. </div>
  16. </div>
  17. <!--没有传入菜单使用默认的-->
  18. <div class="menu-icon1" :class="node.showTreeMenu?'show':''" v-if="node.level !== 1 && menusData.length < 1">
  19. <div class="cu-tree-node-popover-menu-icon" @click.prevent.stop="ElTreeLabelContextMenu2($event,data,node)">
  20. <HcIcon name="apps" ui="text-2xl"/>
  21. </div>
  22. </div>
  23. </div>
  24. <!--树组件,操作菜单-->
  25. <!-- <div class="menu-icon" :class="node.showTreeMenu?'show':''" v-if="node.level !== 1 && menusData.length > 0">
  26. <div class="cu-tree-node-popover-menu-icon" @click.prevent.stop="ElTreeLabelContextMenu($event,data,node)">
  27. <HcIcon name="apps" ui="text-2xl"/>
  28. </div>
  29. </div> -->
  30. <!--树组件,操作菜单 END-->
  31. </div>
  32. </template>
  33. </ElTree>
  34. </el-radio-group>
  35. <!--右键菜单-->
  36. <HcContextMenu ref="contextMenuRef" :datas="menusData" @item-click="handleMenuSelect" v-if="menusData.length > 0" @closed="handleMenuClosed">
  37. <template #sort="{item}">
  38. <HcIcon :name="item.icon" :line="false" class="menu-item-icon"/>
  39. <span class="menu-item-name">{{item.label}}</span>
  40. </template>
  41. </HcContextMenu>
  42. <!--没有传入菜单使用默认的-->
  43. <HcContextMenu ref="contextMenuRef2" :datas="ElTreeMenu" @item-click="ElTreeMenuClick" v-if="menusData.length < 1" @closed="handleMenuClosed">
  44. <template #sort="{item}">
  45. <HcIcon :name="item.icon" :line="false" class="menu-item-icon"/>
  46. <span class="menu-item-name">{{item.label}}</span>
  47. </template>
  48. </HcContextMenu>
  49. <EditNodeDialog :projectId="projectId" :node="nodeItemInfo" :show="editDialogShow" :type="editDialogType" @hide="dialogHide"></EditNodeDialog>
  50. <SortNodeDialog :node="nodeItemInfo" :show="sortDialogShow" @hide="sortDialogHide"></SortNodeDialog>
  51. </template>
  52. <script setup>
  53. import {ref,nextTick,watch} from "vue";
  54. import { getArchiveTreeLazyTree,initTree } from '~api/other';
  55. import {isItem,getArrValue,getObjValue,isValueNull} from "vue-utils-plus"
  56. import {remove,syncProjectTree} from "~api/other";
  57. import EditNodeDialog from "~src/components/dialog/EditNodeDialog.vue"
  58. import SortNodeDialog from "~src/components/dialog/SortNodeDialog.vue"
  59. import {delMessage} from "~uti/tools";
  60. //参数
  61. const props = defineProps({
  62. ui: {
  63. type: String,
  64. default: ''
  65. },
  66. menus: {
  67. type: Array,
  68. default: () => ([])
  69. },
  70. projectId: {
  71. type: [String,Number],
  72. default: ''
  73. },
  74. contractId: {
  75. type: [String,Number],
  76. default: ''
  77. },
  78. autoExpandKeys: {
  79. type: Array,
  80. default: () => ([])
  81. },
  82. idPrefix: {
  83. type: String,
  84. default: 'hc-tree-'
  85. },
  86. isAutoKeys: {
  87. type: Boolean,
  88. default: true
  89. },
  90. isAutoClick: {
  91. type: Boolean,
  92. default: true
  93. },
  94. isRadio: {
  95. type: Boolean,
  96. default: false
  97. },
  98. radioKey: {
  99. type: String,
  100. default: ''
  101. },
  102. showRadioFun:{
  103. type:Function,
  104. default() {
  105. return true;
  106. }
  107. }
  108. })
  109. //变量
  110. const ElTreeRef = ref(null)
  111. const treeRefNode = ref(null)
  112. const treeRefData = ref(null)
  113. const ElTreeProps = ref({
  114. label: 'title',
  115. children: 'children',
  116. isLeaf: 'isLeaf'
  117. })
  118. const menusData = ref(props.menus)
  119. const isAutoKeys = ref(props.isAutoKeys)
  120. const TreeExpandKey = ref(props.autoExpandKeys)
  121. const projectId = ref(props.projectId);
  122. const contractId = ref(props.contractId);
  123. const idPrefix = ref(props.idPrefix);
  124. const radioKeys = ref(Number(props.radioKey));
  125. //监听
  126. watch(() => [
  127. props.menus,
  128. props.isAutoKeys,
  129. props.autoExpandKeys,
  130. props.projectId,
  131. props.contractId,
  132. props.idPrefix,
  133. props.radioKey,
  134. ], ([menus, AutoKeys, expandKeys, UserProjectId, UserContractId, UserIdPrefix, radioKey]) => {
  135. menusData.value = menus
  136. isAutoKeys.value = AutoKeys
  137. TreeExpandKey.value = expandKeys
  138. projectId.value = UserProjectId
  139. contractId.value = UserContractId
  140. idPrefix.value = UserIdPrefix
  141. radioKeys.value = Number(radioKey)
  142. })
  143. //事件
  144. const emit = defineEmits(['menuTap','nodeTap', 'nodeLoading', 'radioChange'])
  145. //单选框事件
  146. const treeRadioChange = (val) => {
  147. emit('radioChange', val)
  148. }
  149. //树形结构异步加载数据
  150. const defaultExpandedCids = ref([])
  151. const ElTreeLoadNode = async (node, resolve) => {
  152. let parentId = 0;
  153. if (node.level !== 0) {
  154. const nodeData = getObjValue(node?.data);
  155. parentId = nodeData?.id || ''
  156. }
  157. //获取数据
  158. const {error, code, data} = await getArchiveTreeLazyTree({
  159. parentId,
  160. projectId:projectId.value,
  161. contractId:contractId.value
  162. })
  163. //处理数据
  164. if (!error && code === 200) {
  165. let clickKey = '', defaultExpandedArr = [];
  166. const keys = TreeExpandKey.value || []
  167. let resData = getArrValue(data)
  168. //如果是加载第一层,而且返回为空,先初始化树
  169. if(parentId === 0 && resData.length === 0){
  170. const {error:error2, code:code2, data:data2} = await initTree({
  171. projectId:projectId.value,
  172. })
  173. if (!error2 && code2 === 200) {
  174. resData = getArrValue(data2)
  175. }
  176. }
  177. for (let i = 0; i < resData.length; i++) {
  178. const item = resData[i];
  179. resData[i].isLeaf = !item.hasChildren
  180. }
  181. if (keys.length > 0) {
  182. let lastKey = keys[keys.length-1];
  183. for (const item of resData) {
  184. //自动展开
  185. if (isItem(keys,item?.id)) {
  186. defaultExpandedArr.push(item?.id)
  187. }
  188. //最后一个,选中点击
  189. if (item?.id === lastKey) {
  190. clickKey = item?.id
  191. }
  192. }
  193. } else if (node.level === 0) {
  194. defaultExpandedArr.push(resData[0]?.id)
  195. }
  196. //自动展开
  197. defaultExpandedCids.value = defaultExpandedArr
  198. if (node.level === 0) {
  199. emit('nodeLoading')
  200. }
  201. resolve(resData)
  202. //最后一个,执行点击
  203. if (props.isAutoClick && clickKey) {
  204. await nextTick(() => {
  205. document.getElementById(`${idPrefix.value}${clickKey}`)?.click()
  206. })
  207. }
  208. } else {
  209. if (node.level === 0) {
  210. emit('nodeLoading')
  211. }
  212. resolve([])
  213. }
  214. }
  215. //节点被点击
  216. const ElTreeClick = async (data,node) => {
  217. if (isAutoKeys.value) {
  218. let autoKeysArr = []
  219. await getNodeExpandKeys(node, autoKeysArr)
  220. const autoKeys = autoKeysArr.reverse()
  221. emit('nodeTap', {node, data, keys: autoKeys})
  222. } else {
  223. emit('nodeTap', {node, data, keys: []})
  224. }
  225. }
  226. //处理自动展开的节点KEY
  227. const getNodeExpandKeys = async (node, newKeys) => {
  228. const parent = node?.parent ?? []
  229. const keyId = node?.data?.id ?? ''
  230. if (keyId) {
  231. newKeys.push(keyId)
  232. await getNodeExpandKeys(parent, newKeys)
  233. }
  234. }
  235. //鼠标右键事件
  236. const contextMenuRef = ref(null)
  237. const ElTreeLabelContextMenu = (e,data,node) => {
  238. const rows = menusData.value || [];
  239. if (node.level !== 1 && rows.length > 0) {
  240. e.preventDefault();
  241. treeRefNode.value = node;
  242. treeRefData.value = data;
  243. node.showTreeMenu = true
  244. //展开菜单
  245. contextMenuRef.value?.showMenu(e)
  246. }
  247. }
  248. //鼠标右键菜单被点击
  249. const handleMenuSelect = ({key}) => {
  250. const node = treeRefNode.value;
  251. const data = treeRefData.value;
  252. emit('menuTap', {key, node, data})
  253. }
  254. const handleMenuClosed = () => {
  255. const node = treeRefNode.value;
  256. if (!isValueNull(node)) {
  257. treeRefNode.value['showTreeMenu'] = false
  258. }
  259. }
  260. //设置树菜单的标记数据
  261. const removeElTreeNode = (key) => {
  262. //根据 data 或者 key 拿到 Tree 组件中的 node
  263. let node = ElTreeRef.value.getNode(key)
  264. //删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
  265. ElTreeRef.value.remove(node)
  266. }
  267. //鼠标右键事件2
  268. const contextMenuRef2 = ref(null)
  269. const ElTreeLabelContextMenu2 = (e,data,node) => {
  270. const rows = ElTreeMenu.value || [];
  271. if (node.level !== 1 && rows.length > 0) {
  272. e.preventDefault();
  273. treeRefNode.value = node;
  274. treeRefData.value = data;
  275. node.showTreeMenu = true
  276. //展开菜单
  277. contextMenuRef2.value?.showMenu(e)
  278. }
  279. }
  280. //设置树菜单数据
  281. const ElTreeMenu = ref([
  282. {icon: 'add-circle', label: '新增', key: "add"},
  283. {icon: 'draft', label: '编辑', key: "edit"},
  284. {icon: 'delete-bin', label: '删除', key: "del"},
  285. {icon: 'refresh', label: '同步', key: "sync"},
  286. {icon: 'sort-asc', label: '排序', key: "sort"}
  287. ])
  288. //树菜单被点击
  289. const nodeItemInfo = ref();
  290. const ElTreeMenuClick = async ({key}) => {
  291. const node = treeRefNode.value;
  292. const data = treeRefData.value;
  293. nodeItemInfo.value = node
  294. // nodeDataInfo.value = data
  295. setTreeMenuDataClick({key,node,data})
  296. }
  297. //处理菜单被点击数据
  298. const setTreeMenuDataClick = ({key,node,data}) => {
  299. //console.log(node)
  300. switch (key) {
  301. case 'add':
  302. addNode(node);
  303. break;
  304. case 'edit':
  305. editNodeModal(node);
  306. break;
  307. case 'del':
  308. delNodeMoadl(node);
  309. break;
  310. case 'sync':
  311. syncNodeMoadl(node);
  312. break;
  313. case 'sort':
  314. sortNodeMoadl(node);
  315. break;
  316. }
  317. }
  318. //新增编辑弹窗
  319. const editDialogShow = ref(false);
  320. const editDialogType = ref('add');
  321. const addNode = ()=>{
  322. editDialogType.value = 'add';
  323. editDialogShow.value = true;
  324. }
  325. const editNodeModal = ()=>{
  326. editDialogType.value = 'edit';
  327. editDialogShow.value = true;
  328. }
  329. const dialogHide = ()=>{
  330. editDialogShow.value = false;
  331. }
  332. //排序弹窗
  333. const sortDialogShow = ref(false);
  334. const sortNodeMoadl = ()=>{
  335. sortDialogShow.value = true;
  336. }
  337. const sortDialogHide = ()=>{
  338. sortDialogShow.value = false;
  339. }
  340. //删除节点
  341. const delNodeMoadl = (node)=>{
  342. delMessage(async() => {
  343. const {code } = await remove({
  344. id:node.data.id
  345. })
  346. if (code == 200) {
  347. window.$message?.success('删除成功')
  348. ElTreeRef.value.remove(node)
  349. }
  350. })
  351. }
  352. //同步节点
  353. const syncNodeMoadl = (node)=>{
  354. window?.$messageBox?.alert('是否同步该节点?', '提示', {
  355. showCancelButton: true,
  356. confirmButtonText: '确认同步',
  357. cancelButtonText: '取消',
  358. callback: async(action) => {
  359. if (action === 'confirm') {
  360. const {code } = await syncProjectTree({
  361. id:node.data.id
  362. })
  363. if (code == 200) {
  364. window.$message?.success('同步成功')
  365. window?.location?.reload() //刷新页面
  366. }
  367. }
  368. }
  369. })
  370. }
  371. // 暴露出去
  372. defineExpose({
  373. removeElTreeNode,
  374. ElTreeRef
  375. })
  376. </script>
  377. <style lang="scss" scoped>
  378. @import "../../styles/app/tree.scss";
  379. .el-radio-group {
  380. width: auto;
  381. }
  382. .data-custom-tree-node {
  383. .label{
  384. line-height: 30px;
  385. }
  386. .menu-icon {
  387. position: absolute;
  388. pointer-events: none;
  389. transition: opacity 0.2s;
  390. opacity: 0;
  391. right: 0;
  392. background: rgba(255, 255, 255, 0.25);
  393. border-radius: 2px;
  394. .cu-tree-node-popover-menu-icon {
  395. display: flex;
  396. align-items: center;
  397. justify-content: center;
  398. }
  399. }
  400. &:hover {
  401. .menu-icon {
  402. opacity: 1;
  403. pointer-events: all;
  404. cursor: context-menu;
  405. }
  406. }
  407. .menu-icon.show {
  408. opacity: 1;
  409. pointer-events: all;
  410. cursor: context-menu;
  411. }
  412. .menu-icon1 {
  413. // position: absolute;
  414. vertical-align: bottom;
  415. display:inline-block;
  416. pointer-events: none;
  417. transition: opacity 0.2s;
  418. opacity: 0;
  419. // right: 0;
  420. background: rgba(255, 255, 255, 0.25);
  421. border-radius: 2px;
  422. .cu-tree-node-popover-menu-icon {
  423. display: flex;
  424. align-items: center;
  425. justify-content: center;
  426. }
  427. }
  428. &:hover {
  429. .menu-icon1 {
  430. opacity: 1;
  431. pointer-events: all;
  432. cursor: context-menu;
  433. }
  434. }
  435. .menu-icon1.show {
  436. opacity: 1;
  437. pointer-events: all;
  438. cursor: context-menu;
  439. }
  440. }
  441. </style>