|
@@ -0,0 +1,402 @@
|
|
|
+<template>
|
|
|
+ <hc-new-dialog v-model="moveModal" is-table title="跨节点移动" widths="72rem" @close="closeModal">
|
|
|
+ <hc-page-split class="m-4" :options="{ sizes: [50, 50] }">
|
|
|
+ <!-- 左侧内容保持不变 -->
|
|
|
+ <template #left>
|
|
|
+ <hc-card scrollbar>
|
|
|
+ <div v-loading="cityLoading" class="checkbox-container">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="checkAll"
|
|
|
+ :indeterminate="isIndeterminate"
|
|
|
+ @change="handleCheckAllChange"
|
|
|
+ >
|
|
|
+ <span class="font-800">全选</span>
|
|
|
+ </el-checkbox>
|
|
|
+ <el-checkbox-group
|
|
|
+ v-model="checkedCities"
|
|
|
+ class="checkbox-group"
|
|
|
+ @change="handleCheckedCitiesChange"
|
|
|
+ >
|
|
|
+ <el-checkbox
|
|
|
+ v-for="city in cities"
|
|
|
+ :key="city.pkeyId"
|
|
|
+ :label="city.fullName "
|
|
|
+ :value="city.pkeyId "
|
|
|
+ class="checkbox-item"
|
|
|
+ >
|
|
|
+ {{ city.fullName }}
|
|
|
+ </el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </div>
|
|
|
+ </hc-card>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <hc-card class="tree-card">
|
|
|
+ <template #search>
|
|
|
+ <div class="flex-1">
|
|
|
+ <el-input v-model="searchInput" placeholder="请输入" clearable />
|
|
|
+ </div>
|
|
|
+ <div class="ml-2">
|
|
|
+ <el-button
|
|
|
+ hc-btn
|
|
|
+ type="primary"
|
|
|
+ @click="searchClick"
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 修改树的渲染方式 -->
|
|
|
+ <el-scrollbar class="tree-scrollbar mt-3" style="height: 100%;">
|
|
|
+ <!-- 普通树 -->
|
|
|
+ <el-tree
|
|
|
+ v-if="!isShowSearch"
|
|
|
+ ref="treeRef"
|
|
|
+ :key="treeKey"
|
|
|
+ node-key="id"
|
|
|
+ :props="treeProps"
|
|
|
+ :load="treeLoadNode"
|
|
|
+ lazy
|
|
|
+ :check-strictly="true"
|
|
|
+ highlight-current
|
|
|
+ @node-click="handleNodeClick"
|
|
|
+ >
|
|
|
+ <template #default="{ node, data }">
|
|
|
+ <span class="custom-tree-node">
|
|
|
+ <!-- 使用单选框替代复选框 -->
|
|
|
+ <el-radio
|
|
|
+ v-model="selectedNodeId"
|
|
|
+ :value="data.id"
|
|
|
+ :disabled="data.nodeType === 6"
|
|
|
+ class="mr-2"
|
|
|
+ />
|
|
|
+ <span>{{ node.label }}</span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+
|
|
|
+ <!-- 搜索结果树 -->
|
|
|
+ <el-tree
|
|
|
+ v-else
|
|
|
+ v-loading="treeLoading"
|
|
|
+ node-key="id"
|
|
|
+ default-expand-all
|
|
|
+ :props="treeProps"
|
|
|
+ :data="treeData"
|
|
|
+ :check-strictly="true"
|
|
|
+ highlight-current
|
|
|
+ @node-click="handleNodeClick"
|
|
|
+ >
|
|
|
+ <template #default="{ node, data }">
|
|
|
+ <span class="custom-tree-node">
|
|
|
+ <!-- 使用单选框替代复选框 -->
|
|
|
+ <el-radio
|
|
|
+ v-model="selectedNodeId"
|
|
|
+ :value="data.id"
|
|
|
+ :disabled="data.nodeType === 6"
|
|
|
+ class="mr-2"
|
|
|
+ />
|
|
|
+ <span>{{ node.label }}</span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </el-scrollbar>
|
|
|
+ </hc-card>
|
|
|
+ </hc-page-split>
|
|
|
+ <template #footer>
|
|
|
+ <el-button :loading="moveLoading" @click="submitMove(1)">保存并退出</el-button>
|
|
|
+ <el-button type="primary" :loading="moveLoading" @click="submitMove(2)">保存并继续</el-button>
|
|
|
+ </template>
|
|
|
+ </hc-new-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { nextTick, ref, watch } from 'vue'
|
|
|
+import { getArrValue, getObjValue } from 'js-fast-way'
|
|
|
+import queryApi from '~api/data-fill/query'
|
|
|
+
|
|
|
+// 接收父组件传入的属性
|
|
|
+const props = defineProps({
|
|
|
+ contractId: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ classType: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ authBtnTabKey: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ primaryKeyId: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+// 事件
|
|
|
+const emit = defineEmits(['close', 'save'])
|
|
|
+const contractId = ref(props.contractId)
|
|
|
+const classType = ref(props.classType)
|
|
|
+const authBtnTabKey = ref(props.authBtnTabKey)
|
|
|
+const primaryKeyId = ref(props.primaryKeyId)
|
|
|
+
|
|
|
+// 监听
|
|
|
+watch(() => [
|
|
|
+ props.contractId,
|
|
|
+ props.classType,
|
|
|
+ props.authBtnTabKey,
|
|
|
+ props.primaryKeyId,
|
|
|
+], ([cid, clas, tab, pkid]) => {
|
|
|
+ contractId.value = cid
|
|
|
+ classType.value = clas
|
|
|
+ authBtnTabKey.value = tab
|
|
|
+ primaryKeyId.value = pkid
|
|
|
+
|
|
|
+})
|
|
|
+
|
|
|
+const moveModal = defineModel('modelValue', {
|
|
|
+ default: false,
|
|
|
+})
|
|
|
+watch(() => moveModal.value, (val) => {
|
|
|
+ if (val && primaryKeyId.value) {
|
|
|
+ getSameLevelsTreeData()
|
|
|
+ }
|
|
|
+})
|
|
|
+const closeModal = ()=>{
|
|
|
+ moveModal.value = false
|
|
|
+ checkAll.value = false
|
|
|
+ checkedCities.value = []
|
|
|
+ cities.value = []
|
|
|
+ selectedNodeId.value = null // 重置选中状态
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+// 左侧复选框相关
|
|
|
+const checkAll = ref(false)
|
|
|
+const isIndeterminate = ref(false)
|
|
|
+const checkedCities = ref([])
|
|
|
+const cities = ref([])
|
|
|
+
|
|
|
+const handleCheckAllChange = (val) => {
|
|
|
+ checkedCities.value = val ? cities.value.map(city => city.pkeyId) : []
|
|
|
+ isIndeterminate.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleCheckedCitiesChange = (value) => {
|
|
|
+ const checkedCount = value.length
|
|
|
+ checkAll.value = checkedCount === cities.value.length
|
|
|
+ isIndeterminate.value = checkedCount > 0 && checkedCount < cities.value.length
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索相关
|
|
|
+const searchInput = ref('')
|
|
|
+
|
|
|
+// 树相关 - 修改为单选
|
|
|
+const treeRef = ref(null)
|
|
|
+const treeData = ref([])
|
|
|
+const isShowSearch = ref(false)
|
|
|
+const selectedNodeId = ref(null) // 存储当前选中的节点ID
|
|
|
+const currentNode = ref(null) // 存储当前选中的节点数据
|
|
|
+
|
|
|
+// 树配置 - 移除disabled配置,在单选框中处理
|
|
|
+const treeProps = {
|
|
|
+ label: 'title',
|
|
|
+ children: 'children',
|
|
|
+ isLeaf: 'notExsitChild',
|
|
|
+}
|
|
|
+const treeKey = ref(0) // 用于强制刷新树组件
|
|
|
+// 加载节点数据
|
|
|
+const treeLoadNode = async (node, resolve) => {
|
|
|
+ const { level, data: item } = node
|
|
|
+
|
|
|
+ let contractIdRelation = '',
|
|
|
+ parentId = '',
|
|
|
+ primaryKeyId = ''
|
|
|
+ if (level !== 0) {
|
|
|
+ const nodeData = getObjValue(item)
|
|
|
+ contractIdRelation = nodeData?.contractIdRelation || ''
|
|
|
+ parentId = contractIdRelation ? nodeData?.primaryKeyId : nodeData?.id
|
|
|
+ primaryKeyId = nodeData?.id || ''
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取数据
|
|
|
+ const { data } = await queryApi.queryWbsTreeData({
|
|
|
+ contractId: contractId.value || '',
|
|
|
+ contractIdRelation,
|
|
|
+ primaryKeyId,
|
|
|
+ parentId,
|
|
|
+ classifyType: classType.value,
|
|
|
+ tableOwner: authBtnTabKey.value,
|
|
|
+ dataTime: new Date(),
|
|
|
+ })
|
|
|
+
|
|
|
+ resolve(getArrValue(data))
|
|
|
+}
|
|
|
+
|
|
|
+// 处理节点点击和单选逻辑
|
|
|
+const handleNodeClick = (data) => {
|
|
|
+ // 如果节点被禁用则不处理
|
|
|
+ if (data.nodeType === 6) return
|
|
|
+
|
|
|
+ // 设置选中状态
|
|
|
+ selectedNodeId.value = data.id
|
|
|
+ currentNode.value = data
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索功能
|
|
|
+
|
|
|
+// 搜索功能
|
|
|
+const searchClick = () => {
|
|
|
+ if (!searchInput.value) {
|
|
|
+ isShowSearch.value = false
|
|
|
+ // 重新加载原始树
|
|
|
+ if (treeRef.value) {
|
|
|
+ refreshTree()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ isShowSearch.value = true
|
|
|
+ getSearchTreeData()
|
|
|
+ }
|
|
|
+}
|
|
|
+// 刷新树的方法 - 替代reload
|
|
|
+const refreshTree = () => {
|
|
|
+ // 通过修改key值强制树组件重新渲染
|
|
|
+ treeKey.value += 1
|
|
|
+ // 重置选中状态
|
|
|
+ selectedNodeId.value = null
|
|
|
+ currentNode.value = null
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const treeLoading = ref(false)
|
|
|
+const getSearchTreeData = async () => {
|
|
|
+ treeLoading.value = true
|
|
|
+ const { error, code, data } = await queryApi.getTreeNodeByQueryValueAndContractId({
|
|
|
+ contractId: contractId.value,
|
|
|
+ queryValue: searchInput.value,
|
|
|
+ tableOwner: authBtnTabKey.value,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!error && code === 200) {
|
|
|
+ treeData.value = getArrValue(data)
|
|
|
+ } else {
|
|
|
+ treeData.value = []
|
|
|
+ }
|
|
|
+ treeLoading.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 获取左侧数据
|
|
|
+const cityLoading = ref(false)
|
|
|
+const getSameLevelsTreeData = async () => {
|
|
|
+ cityLoading.value = true
|
|
|
+ const { error, code, data } = await queryApi.getSiblingWbsContract({
|
|
|
+ pKeyId: primaryKeyId.value,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!error && code === 200) {
|
|
|
+ cities.value = getArrValue(data)
|
|
|
+ } else {
|
|
|
+ cities.value = []
|
|
|
+ }
|
|
|
+ cityLoading.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 提交移动
|
|
|
+const moveLoading = ref(false)
|
|
|
+const submitMove = async (type)=>{
|
|
|
+ // 验证是否选择了目标节点
|
|
|
+ if (!selectedNodeId.value) {
|
|
|
+ window.$message?.warning('请选择目标节点')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ moveLoading.value = true
|
|
|
+ const { error, code, data, msg } = await queryApi.moveNode({
|
|
|
+ leftPkeyIds: checkedCities.value,
|
|
|
+ rightPkeyId: currentNode.value.pKeyId,
|
|
|
+ })
|
|
|
+ moveLoading.value = false
|
|
|
+
|
|
|
+ if (!error && code === 200) {
|
|
|
+ window.$message?.success(msg ?? '操作成功')
|
|
|
+
|
|
|
+ if (type === 1) {
|
|
|
+ emit('save')
|
|
|
+ closeModal()
|
|
|
+ } else {
|
|
|
+ // 重置表单但保持弹窗打开
|
|
|
+ checkAll.value = false
|
|
|
+ checkedCities.value = []
|
|
|
+ selectedNodeId.value = null
|
|
|
+ currentNode.value = null
|
|
|
+ getSameLevelsTreeData()
|
|
|
+ // 刷新树数据
|
|
|
+ if (isShowSearch.value && searchInput.value) {
|
|
|
+ getSearchTreeData() // 搜索状态下重新搜索
|
|
|
+ } else {
|
|
|
+ refreshTree() // 普通状态下刷新树
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.checkbox-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ .checkbox-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .checkbox-item {
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 树卡片样式
|
|
|
+.tree-card {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ :deep(.hc-card-body) {
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 滚动容器样式
|
|
|
+.tree-scrollbar {
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+ margin-top: 12px;
|
|
|
+
|
|
|
+ :deep(.el-scrollbar__view) {
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 调整单选框与文字的对齐方式
|
|
|
+:deep(.el-radio) {
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tree-node {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+</style>
|