jumpTreeDialog.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <template>
  2. <hc-new-dialog v-model="moveModal" is-table title="跨节点移动" widths="72rem" @close="closeModal">
  3. <hc-page-split class="m-4" :options="{ sizes: [50, 50] }">
  4. <!-- 左侧内容保持不变 -->
  5. <template #left>
  6. <hc-card scrollbar>
  7. <div v-loading="cityLoading" class="checkbox-container">
  8. <el-checkbox
  9. v-model="checkAll"
  10. :indeterminate="isIndeterminate"
  11. @change="handleCheckAllChange"
  12. >
  13. <span class="font-800">全选</span>
  14. </el-checkbox>
  15. <el-checkbox-group
  16. v-model="checkedCities"
  17. class="checkbox-group"
  18. @change="handleCheckedCitiesChange"
  19. >
  20. <el-checkbox
  21. v-for="city in cities"
  22. :key="city.pkeyId"
  23. :label="city.fullName "
  24. :value="city.pkeyId "
  25. class="checkbox-item"
  26. >
  27. {{ city.fullName }}
  28. </el-checkbox>
  29. </el-checkbox-group>
  30. </div>
  31. </hc-card>
  32. </template>
  33. <hc-card class="tree-card">
  34. <template #search>
  35. <div class="flex-1">
  36. <el-input v-model="searchInput" placeholder="请输入" clearable />
  37. </div>
  38. <div class="ml-2">
  39. <el-button
  40. hc-btn
  41. type="primary"
  42. @click="searchClick"
  43. >
  44. 搜索
  45. </el-button>
  46. </div>
  47. </template>
  48. <!-- 修改树的渲染方式 -->
  49. <el-scrollbar class="tree-scrollbar mt-3" style="height: 100%;">
  50. <!-- 普通树 -->
  51. <el-tree
  52. v-if="!isShowSearch"
  53. ref="treeRef"
  54. :key="treeKey"
  55. node-key="id"
  56. :props="treeProps"
  57. :load="treeLoadNode"
  58. lazy
  59. :check-strictly="true"
  60. highlight-current
  61. @node-click="handleNodeClick"
  62. >
  63. <template #default="{ node, data }">
  64. <span class="custom-tree-node">
  65. <!-- 使用单选框替代复选框 -->
  66. <el-radio
  67. v-model="selectedNodeId"
  68. :value="data.id"
  69. :disabled="data.nodeType === 6"
  70. class="mr-2"
  71. />
  72. <span>{{ node.label }}</span>
  73. </span>
  74. </template>
  75. </el-tree>
  76. <!-- 搜索结果树 -->
  77. <el-tree
  78. v-else
  79. v-loading="treeLoading"
  80. node-key="id"
  81. default-expand-all
  82. :props="treeProps"
  83. :data="treeData"
  84. :check-strictly="true"
  85. highlight-current
  86. @node-click="handleNodeClick"
  87. >
  88. <template #default="{ node, data }">
  89. <span class="custom-tree-node">
  90. <!-- 使用单选框替代复选框 -->
  91. <el-radio
  92. v-model="selectedNodeId"
  93. :value="data.id"
  94. :disabled="data.nodeType === 6"
  95. class="mr-2"
  96. />
  97. <span>{{ node.label }}</span>
  98. </span>
  99. </template>
  100. </el-tree>
  101. </el-scrollbar>
  102. </hc-card>
  103. </hc-page-split>
  104. <template #footer>
  105. <el-button :loading="moveLoading" @click="submitMove(1)">保存并退出</el-button>
  106. <el-button type="primary" :loading="moveLoading" @click="submitMove(2)">保存并继续</el-button>
  107. </template>
  108. </hc-new-dialog>
  109. </template>
  110. <script setup>
  111. import { nextTick, ref, watch } from 'vue'
  112. import { getArrValue, getObjValue } from 'js-fast-way'
  113. import queryApi from '~api/data-fill/query'
  114. // 接收父组件传入的属性
  115. const props = defineProps({
  116. contractId: {
  117. type: String,
  118. default: '',
  119. },
  120. classType: {
  121. type: String,
  122. default: '',
  123. },
  124. authBtnTabKey: {
  125. type: String,
  126. default: '',
  127. },
  128. primaryKeyId: {
  129. type: String,
  130. default: '',
  131. },
  132. })
  133. // 事件
  134. const emit = defineEmits(['close', 'save'])
  135. const contractId = ref(props.contractId)
  136. const classType = ref(props.classType)
  137. const authBtnTabKey = ref(props.authBtnTabKey)
  138. const primaryKeyId = ref(props.primaryKeyId)
  139. // 监听
  140. watch(() => [
  141. props.contractId,
  142. props.classType,
  143. props.authBtnTabKey,
  144. props.primaryKeyId,
  145. ], ([cid, clas, tab, pkid]) => {
  146. contractId.value = cid
  147. classType.value = clas
  148. authBtnTabKey.value = tab
  149. primaryKeyId.value = pkid
  150. })
  151. const moveModal = defineModel('modelValue', {
  152. default: false,
  153. })
  154. watch(() => moveModal.value, (val) => {
  155. if (val && primaryKeyId.value) {
  156. getSameLevelsTreeData()
  157. }
  158. })
  159. const closeModal = ()=>{
  160. moveModal.value = false
  161. checkAll.value = false
  162. checkedCities.value = []
  163. cities.value = []
  164. selectedNodeId.value = null // 重置选中状态
  165. emit('close')
  166. }
  167. // 左侧复选框相关
  168. const checkAll = ref(false)
  169. const isIndeterminate = ref(false)
  170. const checkedCities = ref([])
  171. const cities = ref([])
  172. const handleCheckAllChange = (val) => {
  173. checkedCities.value = val ? cities.value.map(city => city.pkeyId) : []
  174. isIndeterminate.value = false
  175. }
  176. const handleCheckedCitiesChange = (value) => {
  177. const checkedCount = value.length
  178. checkAll.value = checkedCount === cities.value.length
  179. isIndeterminate.value = checkedCount > 0 && checkedCount < cities.value.length
  180. }
  181. // 搜索相关
  182. const searchInput = ref('')
  183. // 树相关 - 修改为单选
  184. const treeRef = ref(null)
  185. const treeData = ref([])
  186. const isShowSearch = ref(false)
  187. const selectedNodeId = ref(null) // 存储当前选中的节点ID
  188. const currentNode = ref(null) // 存储当前选中的节点数据
  189. // 树配置 - 移除disabled配置,在单选框中处理
  190. const treeProps = {
  191. label: 'title',
  192. children: 'children',
  193. isLeaf: 'notExsitChild',
  194. }
  195. const treeKey = ref(0) // 用于强制刷新树组件
  196. // 加载节点数据
  197. const treeLoadNode = async (node, resolve) => {
  198. const { level, data: item } = node
  199. let contractIdRelation = '',
  200. parentId = '',
  201. primaryKeyId = ''
  202. if (level !== 0) {
  203. const nodeData = getObjValue(item)
  204. contractIdRelation = nodeData?.contractIdRelation || ''
  205. parentId = contractIdRelation ? nodeData?.primaryKeyId : nodeData?.id
  206. primaryKeyId = nodeData?.id || ''
  207. }
  208. // 获取数据
  209. const { data } = await queryApi.queryWbsTreeData({
  210. contractId: contractId.value || '',
  211. contractIdRelation,
  212. primaryKeyId,
  213. parentId,
  214. classifyType: classType.value,
  215. tableOwner: authBtnTabKey.value,
  216. dataTime: new Date(),
  217. })
  218. resolve(getArrValue(data))
  219. }
  220. // 处理节点点击和单选逻辑
  221. const handleNodeClick = (data) => {
  222. // 如果节点被禁用则不处理
  223. if (data.nodeType === 6) return
  224. // 设置选中状态
  225. selectedNodeId.value = data.id
  226. currentNode.value = data
  227. }
  228. // 搜索功能
  229. // 搜索功能
  230. const searchClick = () => {
  231. if (!searchInput.value) {
  232. isShowSearch.value = false
  233. // 重新加载原始树
  234. if (treeRef.value) {
  235. refreshTree()
  236. }
  237. } else {
  238. isShowSearch.value = true
  239. getSearchTreeData()
  240. }
  241. }
  242. // 刷新树的方法 - 替代reload
  243. const refreshTree = () => {
  244. // 通过修改key值强制树组件重新渲染
  245. treeKey.value += 1
  246. // 重置选中状态
  247. selectedNodeId.value = null
  248. currentNode.value = null
  249. }
  250. const treeLoading = ref(false)
  251. const getSearchTreeData = async () => {
  252. treeLoading.value = true
  253. const { error, code, data } = await queryApi.getTreeNodeByQueryValueAndContractId({
  254. contractId: contractId.value,
  255. queryValue: searchInput.value,
  256. tableOwner: authBtnTabKey.value,
  257. })
  258. if (!error && code === 200) {
  259. treeData.value = getArrValue(data)
  260. } else {
  261. treeData.value = []
  262. }
  263. treeLoading.value = false
  264. }
  265. // 获取左侧数据
  266. const cityLoading = ref(false)
  267. const getSameLevelsTreeData = async () => {
  268. cityLoading.value = true
  269. const { error, code, data } = await queryApi.getSiblingWbsContract({
  270. pKeyId: primaryKeyId.value,
  271. })
  272. if (!error && code === 200) {
  273. cities.value = getArrValue(data)
  274. } else {
  275. cities.value = []
  276. }
  277. cityLoading.value = false
  278. }
  279. // 提交移动
  280. const moveLoading = ref(false)
  281. const submitMove = async (type)=>{
  282. // 验证是否选择了目标节点
  283. if (!selectedNodeId.value) {
  284. window.$message?.warning('请选择目标节点')
  285. return
  286. }
  287. moveLoading.value = true
  288. const { error, code, data, msg } = await queryApi.moveNode({
  289. leftPkeyIds: checkedCities.value,
  290. rightPkeyId: currentNode.value.pKeyId,
  291. })
  292. moveLoading.value = false
  293. if (!error && code === 200) {
  294. window.$message?.success(msg ?? '操作成功')
  295. if (type === 1) {
  296. emit('save')
  297. closeModal()
  298. } else {
  299. // 重置表单但保持弹窗打开
  300. checkAll.value = false
  301. checkedCities.value = []
  302. selectedNodeId.value = null
  303. currentNode.value = null
  304. getSameLevelsTreeData()
  305. // 刷新树数据
  306. if (isShowSearch.value && searchInput.value) {
  307. getSearchTreeData() // 搜索状态下重新搜索
  308. } else {
  309. refreshTree() // 普通状态下刷新树
  310. }
  311. }
  312. }
  313. }
  314. </script>
  315. <style lang="scss" scoped>
  316. .checkbox-container {
  317. display: flex;
  318. flex-direction: column;
  319. gap: 10px;
  320. .checkbox-group {
  321. display: flex;
  322. flex-direction: column;
  323. gap: 10px;
  324. }
  325. .checkbox-item {
  326. height: 32px;
  327. display: flex;
  328. align-items: center;
  329. }
  330. }
  331. // 树卡片样式
  332. .tree-card {
  333. height: 100%;
  334. display: flex;
  335. flex-direction: column;
  336. :deep(.hc-card-body) {
  337. flex: 1;
  338. height: 0;
  339. display: flex;
  340. flex-direction: column;
  341. padding: 0;
  342. }
  343. }
  344. // 滚动容器样式
  345. .tree-scrollbar {
  346. flex: 1;
  347. height: 0;
  348. margin-top: 12px;
  349. :deep(.el-scrollbar__view) {
  350. height: 100%;
  351. }
  352. }
  353. // 调整单选框与文字的对齐方式
  354. :deep(.el-radio) {
  355. vertical-align: middle;
  356. }
  357. .custom-tree-node {
  358. display: flex;
  359. align-items: center;
  360. }
  361. </style>