hc-tree.vue 19 KB

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