hc-tree.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <template>
  2. <el-radio-group v-model="radioKeys" @change="treeRadioChange">
  3. <ElTree
  4. ref="ElTreeRef" class="tree-line el-radio-group hc-tree-node" :class="ui" :props="ElTreeProps" :load="ElTreeLoadNode" lazy accordion highlight-current node-key="id"
  5. :default-expanded-keys="defaultExpandedCids" :indent="0" :show-checkbox="isCheck" @node-click="ElTreeClick" @node-contextmenu="ElTreeLabelContextMenu2"
  6. @check-change="handleCheckChange"
  7. >
  8. <template #default="{ node, data }">
  9. <div :id="`${idPrefix}${data.id}`" class="data-custom-tree-node">
  10. <!-- 树组件,节点名称 -->
  11. <div v-if="node.level === 1" class="label level-name">
  12. <span> {{ node.label }}</span>
  13. </div>
  14. <div v-else class="label">
  15. <el-radio v-if="isRadio && showRadioFun(data)" class="size-xl" :value="data.id" @click.stop="clickRadio(data)">
  16. {{ node.label }}
  17. </el-radio>
  18. <span v-else>{{ node.label }}</span>
  19. </div>
  20. <div v-if="isShowNumber" class="submit-counts text-blue">
  21. 【{{ data.treeNumber ?? 0 }}】
  22. </div>
  23. <!-- 没有传入菜单使用默认的 -->
  24. <div v-if="isShowMenu" class="menu-icon1" :class="node.showTreeMenu ? 'show' : ''">
  25. <div class="cu-tree-node-popover-menu-icon" @click.prevent.stop="ElTreeLabelContextMenu2($event, data, node)">
  26. <HcIcon name="apps" ui="text-2xl" />
  27. </div>
  28. </div>
  29. </div>
  30. </template>
  31. </ElTree>
  32. </el-radio-group>
  33. <!-- 右键菜单 -->
  34. <HcContextMenu v-if="menusData.length > 0" ref="contextMenuRef2" :datas="menusData" @item-click="ElTreeMenuClick" @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. <!-- 没有传入菜单使用默认的 -->
  41. <HcContextMenu v-if="menusData.length < 1" ref="contextMenuRef2" :datas="ElTreeMenu" @item-click="ElTreeMenuClick" @closed="handleMenuClosed">
  42. <template #sort="{ item }">
  43. <HcIcon :name="item.icon" :line="false" class="menu-item-icon" />
  44. <span class="menu-item-name">{{ item.label }}</span>
  45. </template>
  46. </HcContextMenu>
  47. <EditNodeDialog :project-id="projectId" :node="nodeItemInfo" :show="editDialogShow" :type="editDialogType" @hide="dialogHide" />
  48. <SortNodeDialog :node="nodeItemInfo" :show="sortDialogShow" @hide="sortDialogHide" />
  49. </template>
  50. <script setup>
  51. import { nextTick, ref, watch } from 'vue'
  52. import { remove, syncFileTree, syncProjectTree, syncSoundImageData } from '~api/other'
  53. import { getArchiveTreeLazyTree, initTree } from '~api/other'
  54. import { getArrValue, getObjValue, isArrItem, isNullES } from 'js-fast-way'
  55. import EditNodeDialog from '~src/components/dialog/EditNodeDialog.vue'
  56. import SortNodeDialog from '~src/components/dialog/SortNodeDialog.vue'
  57. import { HcDelMsg } from 'hc-vue3-ui'
  58. import initialgApi from '~api/initial/initial'
  59. import { ElLoadingService } from 'element-plus'
  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. isShowMenu:{
  109. type:Boolean,
  110. default:true, //是否显示默认菜单
  111. },
  112. isOwn:{
  113. type:Boolean,
  114. default:false,
  115. },
  116. isShowNumber:{
  117. type:Boolean,
  118. default:false,
  119. },
  120. isCheck:{
  121. type:Boolean,
  122. default:false,
  123. },
  124. })
  125. //事件
  126. const emit = defineEmits(['menuTap', 'nodeTap', 'nodeLoading', 'radioChange', 'noderadio', 'nodeCheckChange'])
  127. //变量
  128. const ElTreeRef = ref(null)
  129. const treeRefNode = ref(null)
  130. const treeRefData = ref(null)
  131. const ElTreeProps = ref({
  132. label: 'title',
  133. children: 'children',
  134. isLeaf: 'isLeaf',
  135. })
  136. const menusData = ref(props.menus)
  137. const isAutoKeys = ref(props.isAutoKeys)
  138. const TreeExpandKey = ref(props.autoExpandKeys)
  139. const projectId = ref(props.projectId)
  140. const contractId = ref(props.contractId)
  141. const idPrefix = ref(props.idPrefix)
  142. const radioKeys = ref(Number(props.radioKey))
  143. const isOwn = ref(props.isOwn)
  144. const isShowNumber = ref(props.isShowNumber)
  145. const isShowMenu = ref(props.isShowMenu)
  146. const isCheck = ref(props.isCheck)
  147. //监听
  148. watch(() => [
  149. props.menus,
  150. props.isAutoKeys,
  151. props.autoExpandKeys,
  152. props.projectId,
  153. props.contractId,
  154. props.idPrefix,
  155. props.radioKey,
  156. props.isOwn,
  157. props.isShowNumber,
  158. props.isShowMenu,
  159. props.isCheck,
  160. ], ([menus, AutoKeys, expandKeys, UserProjectId, UserContractId, UserIdPrefix, radioKey, own, num, ishowmenu, isshowCheck]) => {
  161. menusData.value = menus
  162. isAutoKeys.value = AutoKeys
  163. TreeExpandKey.value = expandKeys
  164. projectId.value = UserProjectId
  165. contractId.value = UserContractId
  166. idPrefix.value = UserIdPrefix
  167. radioKeys.value = Number(radioKey)
  168. isOwn.value = own
  169. isShowNumber.value = num
  170. isShowMenu.value = ishowmenu
  171. isCheck.value = isshowCheck
  172. })
  173. //单选框事件
  174. const treeRadioChange = (val) => {
  175. emit('radioChange', val)
  176. }
  177. //树形结构异步加载数据
  178. const defaultExpandedCids = ref([])
  179. const ElTreeLoadNode = async (node, resolve) => {
  180. if (isOwn.value) {
  181. let parentId = 0
  182. if (node.level !== 0) {
  183. const nodeData = getObjValue(node?.data)
  184. parentId = nodeData?.id || ''
  185. }
  186. //获取数据
  187. const { error, code, data } = await initialgApi.getlazyTree({
  188. parentId,
  189. projectId:projectId.value,
  190. // contractId:contractId.value,
  191. })
  192. //处理数据
  193. if (!error && code === 200) {
  194. let clickKey = '', defaultExpandedArr = []
  195. const keys = TreeExpandKey.value || []
  196. let resData = getArrValue(data)
  197. //如果是加载第一层,而且返回为空,先初始化树
  198. // if (parentId === 0 && resData.length === 0) {
  199. // const { error:error2, code:code2, data:data2 } = await initTree({
  200. // projectId:projectId.value,
  201. // })
  202. // if (!error2 && code2 === 200) {
  203. // resData = getArrValue(data2)
  204. // }
  205. // }
  206. for (let i = 0; i < resData.length; i++) {
  207. const item = resData[i]
  208. resData[i].isLeaf = !item.hasChildren
  209. }
  210. if (keys.length > 0) {
  211. let lastKey = keys[keys.length - 1]
  212. for (const item of resData) {
  213. //自动展开
  214. if (isArrItem(keys, item?.id)) {
  215. defaultExpandedArr.push(item?.id)
  216. }
  217. //最后一个,选中点击
  218. if (item?.id === lastKey) {
  219. clickKey = item?.id
  220. }
  221. }
  222. } else if (node.level === 0) {
  223. defaultExpandedArr.push(resData[0]?.id)
  224. }
  225. //自动展开
  226. defaultExpandedCids.value = defaultExpandedArr
  227. if (node.level === 0) {
  228. emit('nodeLoading')
  229. }
  230. resolve(resData)
  231. //最后一个,执行点击
  232. if (props.isAutoClick && clickKey) {
  233. await nextTick(() => {
  234. document.getElementById(`${idPrefix.value}${clickKey}`)?.click()
  235. })
  236. }
  237. } else {
  238. if (node.level === 0) {
  239. emit('nodeLoading')
  240. }
  241. resolve([])
  242. }
  243. } else {
  244. let parentId = 0
  245. if (node.level !== 0) {
  246. const nodeData = getObjValue(node?.data)
  247. parentId = nodeData?.id || ''
  248. }
  249. //获取数据
  250. const { error, code, data } = await getArchiveTreeLazyTree({
  251. parentId,
  252. projectId:projectId.value,
  253. contractId:contractId.value,
  254. type:isShowNumber.value ? 1 : null,
  255. })
  256. //处理数据
  257. if (!error && code === 200) {
  258. let clickKey = '', defaultExpandedArr = []
  259. const keys = TreeExpandKey.value || []
  260. let resData = getArrValue(data)
  261. //如果是加载第一层,而且返回为空,先初始化树
  262. if (parentId === 0 && resData.length === 0) {
  263. const { error:error2, code:code2, data:data2 } = await initTree({
  264. projectId:projectId.value,
  265. })
  266. if (!error2 && code2 === 200) {
  267. resData = getArrValue(data2)
  268. }
  269. }
  270. for (let i = 0; i < resData.length; i++) {
  271. const item = resData[i]
  272. resData[i].isLeaf = !item.hasChildren
  273. }
  274. if (keys.length > 0) {
  275. let lastKey = keys[keys.length - 1]
  276. for (const item of resData) {
  277. //自动展开
  278. if (isArrItem(keys, item?.id)) {
  279. defaultExpandedArr.push(item?.id)
  280. }
  281. //最后一个,选中点击
  282. if (item?.id === lastKey) {
  283. clickKey = item?.id
  284. }
  285. }
  286. } else if (node.level === 0) {
  287. defaultExpandedArr.push(resData[0]?.id)
  288. }
  289. //自动展开
  290. defaultExpandedCids.value = defaultExpandedArr
  291. if (node.level === 0) {
  292. emit('nodeLoading')
  293. }
  294. resolve(resData)
  295. //最后一个,执行点击
  296. if (props.isAutoClick && clickKey) {
  297. await nextTick(() => {
  298. document.getElementById(`${idPrefix.value}${clickKey}`)?.click()
  299. })
  300. }
  301. } else {
  302. if (node.level === 0) {
  303. emit('nodeLoading')
  304. }
  305. resolve([])
  306. }
  307. }
  308. }
  309. //节点被点击
  310. const ElTreeClick = async (data, node) => {
  311. if (isAutoKeys.value) {
  312. let autoKeysArr = []
  313. await getNodeExpandKeys(node, autoKeysArr)
  314. const autoKeys = autoKeysArr.reverse()
  315. emit('nodeTap', { node, data, keys: autoKeys })
  316. } else {
  317. emit('nodeTap', { node, data, keys: [] })
  318. }
  319. }
  320. const handleCheckChange = (data, checked, indeterminate) => {
  321. const checkedNodes = ElTreeRef.value.getCheckedNodes()
  322. const checkedKeys = ElTreeRef.value.getCheckedKeys()
  323. emit('nodeCheckChange', { checkedNodes, checkedKeys })
  324. }
  325. //处理自动展开的节点KEY
  326. const getNodeExpandKeys = async (node, newKeys) => {
  327. const parent = node?.parent ?? []
  328. const keyId = node?.data?.id ?? ''
  329. if (keyId) {
  330. newKeys.push(keyId)
  331. await getNodeExpandKeys(parent, newKeys)
  332. }
  333. }
  334. //鼠标右键事件
  335. const contextMenuRef = ref(null)
  336. const ElTreeLabelContextMenu = (e, data, node) => {
  337. const rows = menusData.value || []
  338. if (node.level !== 1 && rows.length > 0) {
  339. e.preventDefault()
  340. treeRefNode.value = node
  341. treeRefData.value = data
  342. node.showTreeMenu = true
  343. //展开菜单
  344. contextMenuRef.value?.showMenu(e)
  345. }
  346. }
  347. //单选
  348. const clickRadio = (data)=>{
  349. console.log(data, 'data111')
  350. emit('noderadio', { data })
  351. }
  352. //鼠标右键菜单被点击
  353. const handleMenuSelect = ({ key }) => {
  354. const node = treeRefNode.value
  355. const data = treeRefData.value
  356. emit('menuTap', { key, node, data })
  357. }
  358. const handleMenuClosed = () => {
  359. const node = treeRefNode.value
  360. if (!isNullES(node)) {
  361. treeRefNode.value['showTreeMenu'] = false
  362. }
  363. }
  364. //设置树菜单的标记数据
  365. const removeElTreeNode = (key) => {
  366. //根据 data 或者 key 拿到 Tree 组件中的 node
  367. let node = ElTreeRef.value.getNode(key)
  368. //删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
  369. ElTreeRef.value.remove(node)
  370. }
  371. //鼠标右键事件2
  372. const contextMenuRef2 = ref(null)
  373. const ElTreeLabelContextMenu2 = (e, data, node) => {
  374. emit('menuTap', { node, data })
  375. let rows = ElTreeMenu.value || []
  376. if (node.level == 1) {
  377. rows = ElTreeMenu.value.filter((item)=>{
  378. if (item.key !== 'sort') {
  379. return rows
  380. }
  381. })
  382. ElTreeMenu.value = rows
  383. console.log(rows, 'row')
  384. } else {
  385. ElTreeMenu.value = [
  386. { icon: 'add-circle', label: '新增', key: 'add' },
  387. { icon: 'draft', label: '编辑', key: 'edit' },
  388. { icon: 'delete-bin', label: '删除', key: 'del' },
  389. { icon: 'refresh', label: '目录同步', key: 'sync' },
  390. { icon: 'sort-asc', label: '排序', key: 'sort' },
  391. ]
  392. }
  393. if (rows.length > 0) {
  394. e.preventDefault()
  395. treeRefNode.value = node
  396. treeRefData.value = data
  397. node.showTreeMenu = true
  398. //展开菜单
  399. contextMenuRef2.value?.showMenu(e)
  400. }
  401. }
  402. //设置树菜单数据
  403. const ElTreeMenu = ref([
  404. { icon: 'add-circle', label: '新增', key: 'add' },
  405. { icon: 'draft', label: '编辑', key: 'edit' },
  406. { icon: 'delete-bin', label: '删除', key: 'del' },
  407. { icon: 'refresh', label: '目录同步', key: 'sync' },
  408. { icon: 'sort-asc', label: '排序', key: 'sort' },
  409. ])
  410. //树菜单被点击
  411. const nodeItemInfo = ref()
  412. const ElTreeMenuClick = async ({ key }) => {
  413. const node = treeRefNode.value
  414. const data = treeRefData.value
  415. nodeItemInfo.value = node
  416. // nodeDataInfo.value = data
  417. setTreeMenuDataClick({ key, node, data })
  418. }
  419. //处理菜单被点击数据
  420. const setTreeMenuDataClick = ({ key, node, data }) => {
  421. //console.log(node)
  422. switch (key) {
  423. case 'add':
  424. addNode(node)
  425. break
  426. case 'edit':
  427. editNodeModal(node)
  428. break
  429. case 'del':
  430. delNodeMoadl(node)
  431. break
  432. case 'sync':
  433. syncNodeMoadl(node)
  434. break
  435. case 'sort':
  436. sortNodeMoadl(node, data)
  437. break
  438. case 'fileSync':
  439. filesyncNodeMoadl(node, data)
  440. break
  441. case 'syncVocie':
  442. syncVocieMoadl(node, data)
  443. break
  444. }
  445. }
  446. //新增编辑弹窗
  447. const editDialogShow = ref(false)
  448. const editDialogType = ref('add')
  449. const addNode = ()=>{
  450. editDialogType.value = 'add'
  451. editDialogShow.value = true
  452. }
  453. const editNodeModal = ()=>{
  454. editDialogType.value = 'edit'
  455. editDialogShow.value = true
  456. }
  457. const dialogHide = ()=>{
  458. editDialogShow.value = false
  459. }
  460. //排序弹窗
  461. const sortDialogShow = ref(false)
  462. const sortNodeMoadl = (node, data)=>{
  463. // if(data?.isStorageNode===1){
  464. // sortDialogShow.value = true;
  465. // }else{
  466. // window.$message.warning('非存储节点不允许排序')
  467. // }
  468. sortDialogShow.value = true
  469. }
  470. const sortDialogHide = ()=>{
  471. sortDialogShow.value = false
  472. }
  473. //删除节点
  474. const delNodeMoadl = (node) => {
  475. HcDelMsg(async (resolve) => {
  476. const { code } = await remove({ id: node.data.id })
  477. if (code === 200) {
  478. window.$message?.success('删除成功')
  479. ElTreeRef.value.remove(node)
  480. }
  481. resolve() //关闭弹窗的回调
  482. })
  483. }
  484. //同步节点
  485. const syncNodeMoadl = async (node) => {
  486. // 显示加载状态
  487. const loadingInstance = ElLoadingService({
  488. lock: true,
  489. text: '加载中...',
  490. })
  491. window?.$messageBox?.alert('是否同步该节点?', '提示', {
  492. showCancelButton: true,
  493. confirmButtonText: '确认同步',
  494. cancelButtonText: '取消',
  495. callback: async (action) => {
  496. if (action === 'confirm') {
  497. const { code } = await syncProjectTree({
  498. id: node.data.id,
  499. })
  500. // 隐藏加载状态
  501. loadingInstance.close()
  502. if (code === 200) {
  503. window.$message?.success('同步成功')
  504. // 引入延迟,确保用户能看到提示信息
  505. setTimeout(() => {
  506. window?.location?.reload() // 刷新页面
  507. }, 1000) // 延迟2秒
  508. }
  509. } else {
  510. loadingInstance.close()
  511. }
  512. },
  513. })
  514. }
  515. //文件同步
  516. const filesyncNodeMoadl = async (node, data)=>{
  517. const loadingInstance = ElLoadingService({
  518. lock: true,
  519. text: '加载中...',
  520. })
  521. window?.$messageBox?.alert('是否同步该节点?', '提示', {
  522. showCancelButton: true,
  523. confirmButtonText: '确认同步',
  524. cancelButtonText: '取消',
  525. callback: async (action) => {
  526. if (action === 'confirm') {
  527. const { code } = await syncFileTree({
  528. projectId:projectId.value,
  529. contractId:data.contractId,
  530. associationType: data.associationType,
  531. })
  532. // 隐藏加载状态
  533. loadingInstance.close()
  534. if (code === 200) {
  535. await window.$message?.success('同步成功')
  536. setTimeout(() => {
  537. window?.location?.reload() // 刷新页面
  538. }, 1000) // 延迟2秒
  539. }
  540. } else {
  541. loadingInstance.close()
  542. }
  543. },
  544. })
  545. }
  546. //声像文件同步syncVocieMoadl
  547. const syncVocieMoadl = async (node, data)=>{
  548. window?.$messageBox?.alert('是否同步该节点?', '提示', {
  549. showCancelButton: true,
  550. confirmButtonText: '确认同步',
  551. cancelButtonText: '取消',
  552. callback: async (action) => {
  553. if (action === 'confirm') {
  554. const { code } = await syncSoundImageData({
  555. projectId:projectId.value,
  556. contractId:data.contractId,
  557. nodeId:data.id,
  558. associationType: data.associationType,
  559. })
  560. if (code == 200) {
  561. await window.$message?.success('同步成功')
  562. window?.location?.reload() //刷新页面
  563. }
  564. }
  565. },
  566. })
  567. }
  568. // 暴露出去
  569. defineExpose({
  570. removeElTreeNode,
  571. ElTreeRef,
  572. })
  573. </script>
  574. <style lang="scss" scoped>
  575. @import "../../styles/app/tree.scss";
  576. .el-radio-group {
  577. width: 100%;
  578. }
  579. .data-custom-tree-node {
  580. flex: 1;
  581. white-space: nowrap;
  582. overflow: hidden;
  583. text-overflow: ellipsis;
  584. .label {
  585. flex: 1;
  586. white-space: nowrap;
  587. overflow: hidden;
  588. text-overflow: ellipsis;
  589. }
  590. .submit-counts {
  591. position: unset;
  592. font-size: 14px;
  593. }
  594. .menu-icon1 {
  595. background: hsla(0,0%,100%,.25);
  596. border-radius: 2px;
  597. display: inline-block;
  598. pointer-events: none;
  599. position: relative;
  600. right: -45px;
  601. transition: width .2s;
  602. vertical-align: bottom;
  603. width: 0;
  604. .cu-tree-node-popover-menu-icon {
  605. display: flex;
  606. align-items: center;
  607. justify-content: center;
  608. }
  609. }
  610. &:hover {
  611. .menu-icon1 {
  612. cursor: context-menu;
  613. pointer-events: all;
  614. right: 0;
  615. width: 24px;
  616. }
  617. }
  618. .menu-icon1.show {
  619. cursor: context-menu;
  620. pointer-events: all;
  621. right: 0;
  622. width: 24px;
  623. }
  624. }
  625. </style>
  626. <style lang="scss">
  627. .el-tree.hc-tree-node .el-tree-node {
  628. white-space: nowrap;
  629. overflow: hidden;
  630. text-overflow: ellipsis;
  631. .el-tree-node_content {
  632. white-space: nowrap;
  633. overflow: hidden;
  634. text-overflow: ellipsis;
  635. }
  636. }
  637. </style>