hc-tree.vue 14 KB

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