开发可视化项目而做的树形拖拽组件。需要拖入为子元素需要children并且为树形展开的情况。单击可选中,shift+单击可选中一片区域,ctrl+单击多选。

效果图

在这里插入图片描述
在这里插入图片描述

属性

  • itemStyle:单个item项的样式,更推荐使用类名drag-item去更改基本样式。被选中的会被添加active类名;
  • data:数据,注意需要有id;
  • iconSize:树形节点图标的大小;
  • iconColor:树形节点图标的颜色;
  • select:默认选中的数据;
  • dragCheck:拖拽进入为子组件的触控区域;
  • styleId:自定义样式的类名所在id,id匹配会增加类名self_style;
  • isdbCancel:是否按两下取消选中。

方法

  • dragEnd:拖拽完成之后的回调,第一个参数是事件对象e,第二个参数是改变后的数据;
  • selectItem:选中的回调,第一个参数是事件对象e,第二个参数是当前选中的数据;
  • selectItems:选中数据改变的回调;
  • hoverItem:悬浮的回调,第一个参数是事件对象e,第二个参数是当前悬浮的数据;
  • moveItem:鼠标离开的回调,第一个参数是事件对象e,第二个参数是当前离开元素的数据;
  • handleDbClick:双击的回调,第一个参数是事件对象e,第二个参数是当前双击元素的数据;
  • onRightClick:右键鼠标的回调,第一个参数是事件对象e,第二个参数是当前右键元素的数据;
  • getSelectedList:组件方法,获取所有选中的数据;
  • setCheckedKeys:设置当前组件哪些元素被选中;
  • setShowChild:设置当前组件哪些元素应该被展开子元素。
  • changeshowChild:改变当前组件某个节点的孩子是否展示。

组件目录

在这里插入图片描述

组件封装

index.vue

<template>
    <yhdrag ref="yhDrag" :data="data" :drag-check="dragCheck" :showchilds="showchilds" @dragEnd="dragEnd">
        <template #content="{ scope }">
            <div
                :class="getClass(scope.data)" :style="getStyle(scope.$level)"
                @mouseenter="hoverItem($event, scope.data)" @mousemove="moveItem($event, scope.data)"
                @dblclick="handleDbClick($event, scope.data)" @contextmenu.prevent="onRightClick($event, scope.data)"
                @click.exact="selectItem($event, scope.data)" @click.ctrl="selectItems($event, scope.data)"
                @click.shift="addItems($event, scope.data)">
                <span :style="getIconStyle()">
                    <template v-if="scope.data.children">
                        <CaretRight
                            v-if="!showchilds.includes(scope.data.id)" style="width: 1em; height: 1em;"
                            :color="iconColor" @click.stop="changeshowChild(scope.data)" />
                        <CaretBottom
                            v-else style="width: 1em; height: 1em;" :color="iconColor"
                            @click.stop="changeshowChild(scope.data)" />
                    </template>
                </span>
                <slot name="content" :data="scope.data"></slot>
            </div>
        </template>
    </yhdrag>
</template>

<script setup>
/**
 * ============================================info==================================================>
 * author:yihua                                        
 * date:2022/10/26                                          
 * describe:树形可拖拽组件,单击可选中,shift+单击可选中一片区域,ctrl+单击多选。
 * <=================================================================================================
 * 
 * ===========================================属性================================================>
 * itemStyle:单个item项的样式,更推荐使用类名drag-item去更改基本样式。被选中的会被添加active类名;
 * data:数据,注意需要有id;
 * iconSize:树形节点图标的大小;
 * iconColor:树形节点图标的颜色;
 * select:默认选中的数据;
 * dragCheck:拖拽进入为子组件的触控区域;
 * styleId:悬浮的id,悬浮的item会增加类名self_style;
 * isdbCancel:是否按两下取消选中。
 * <================================================================================================
 * 
 * ===========================================方法================================================>
 * dragEnd:拖拽完成之后的回调,第一个参数是事件对象e,第二个参数是改变后的数据;
 * selectItem:选中的回调,第一个参数是事件对象e,第二个参数是当前选中的数据;
 * selectItems:选中数据改变的回调;
 * hoverItem:悬浮的回调,第一个参数是事件对象e,第二个参数是当前悬浮的数据;
 * moveItem:鼠标离开的回调,第一个参数是事件对象e,第二个参数是当前离开元素的数据;
 * handleDbClick:双击的回调,第一个参数是事件对象e,第二个参数是当前双击元素的数据;
 * onRightClick:右键鼠标的回调,第一个参数是事件对象e,第二个参数是当前右键元素的数据;
 * getSelectedList:组件方法,获取所有选中的数据;
 * setCheckedKeys:设置当前组件哪些元素被选中;
 * setShowChild:设置当前组件哪些元素应该被展开子元素;
 * changeshowChild:改变当前组件某个节点的孩子是否展示;
 * goToItem:滑动到某一项。
 * <================================================================================================
 */
import { CaretRight, CaretBottom } from '@element-plus/icons-vue'
import { computed, defineEmits, defineProps, ref, defineExpose, nextTick } from 'vue'
import yhdrag from './yhdrag.vue';

const props = defineProps({
    itemStyle: {
        type: Object,
        default: () => { }
    },
    data: {
        type: Array,
        default: () => []
    },
    iconSize: {
        type: Number,
        default: () => 14
    },
    iconColor: {
        type: String,
        default: () => '#FFFFFF'
    },
    select: {
        type: Array,
        default: () => []
    },
    dragCheck: {
        type: Number,
        default: () => 20
    },
    styleId: {
        type: String,
        default: () => ''
    },
    isdbCancel: {
        type: Boolean,
        default: () => false
    }
})

// 获取样式
const getStyle = level => ({
    ...props.itemStyle,
    paddingLeft: `${(level + 1) * 20}px`
})

// icon样式
const getIconStyle = () => ({
    display: 'inline-block',
    fontSize: `${props.iconSize}px`,
    width: `${props.iconSize}px`,
    height: '10px',
    marginRight: '5px'
})

const showchilds = ref([])
// 改变子集节点的显隐
const changeshowChild = data => {
    const LEN = showchilds.value.length
    showchilds.value = showchilds.value.filter(item => item !== data.id)
    if (LEN === showchilds.value.length) {
        showchilds.value.push(data.id)
    }
}

// 选中的数组
const selectList = ref([])
// eslint-disable-next-line vue/no-setup-props-destructure
selectList.value = props.select

// 获取已选中的数据
const getSelectedList = () => selectList.value;
// 设置选中
const setCheckedKeys = arr => { selectList.value = arr }
// 设置展开子节点
const setShowChild = data => { showchilds.value = data.map(item => item.id) }

// 注册事件
const emits = defineEmits(['dragEnd', 'selectItem', 'hoverItem', 'handleDbClick', 'moveItem', 'onRightClick', 'selectItems'])

// 初始化类名
const getClass = computed(() => data => {
    const isHover = data.id === props.styleId ? 'self_style' : ''
    if (selectList.value.findIndex(i => i.id === data.id) > -1) {
        return `drag-item active ${isHover}`
    }
    return `drag-item ${isHover}`

})

/**
 * @param {Array} arr
 * @param {String} id1
 * @param {String} id2
 */
const findtwoIdsData = (arr, id1, id2) => {
    const rst = [];
    const hitId = (id) => id === id1 || id === id2;
    let hitTimes = 0;

    const addItem = (list) => {
        list.forEach((item) => {
            if (hitTimes === 0 && hitId(item.id)) {
                // 命中第一次
                rst.push(item);
                hitTimes++;
                if (item.children && item.children.length) {
                    addItem(item.children, hitTimes);
                }
            } else if (hitTimes === 1 && hitId(item.id)) {
                // 命中第二次
                rst.push(item);
                hitTimes++;
                // eslint-disable-next-line no-useless-return
                return;
            } else if (hitTimes === 1) {
                // 命中第一次中间的
                rst.push(item);
                if (item.children && item.children.length) {
                    addItem(item.children, hitTimes);
                }
            } else if (item.children && item.children.length) {
                addItem(item.children, hitTimes);
            }
        });
    };
    addItem(arr);
    return rst;
};

const preId = ref('')
// 按住ctrl点击选中
const selectItems = (e, data) => {
    const LEN = selectList.value.length
    selectList.value = selectList.value.filter(item => item.id !== data.id)
    if (selectList.value.length === LEN) {
        preId.value = ''
        selectList.value.push(data)
    }
    preId.value = data.id
    emits('selectItem', e, data)
    emits('selectItems', selectList.value)
}

// 按住shift点击选中
const addItems = (e, data) => {
    if (!preId.value || preId.value === data.id) {
        setCheckedKeys([data])
    } else {
        // 将两次点击中间的数据添加到数组中
        setCheckedKeys(findtwoIdsData(props.data, preId.value, data.id))
    }
    preId.value = data.id
    emits('selectItem', e, data)
    emits('selectItems', selectList.value)
}

// 直接点击选中
const selectItem = (e, data) => {
    preId.value = data.id
    if (props.isdbCancel) {
        const LEN = selectList.value.length
        selectList.value = selectList.value.filter(item => item.id !== data.id)
        if (selectList.value.length === LEN) {
            setCheckedKeys([data])
        } else {
            setCheckedKeys([])
            preId.value = ''
        }
    } else {
        setCheckedKeys([data])
    }
    emits('selectItem', e, data)
    emits('selectItems', selectList.value)
}

// 悬浮
const hoverItem = (e, data) => emits('hoverItem', e, data)
// 拖拽
const dragEnd = (e, data) => emits('dragEnd', e, data)
// 双击
const handleDbClick = (e, data) => emits('handleDbClick', e, data)
// 鼠标离开
const moveItem = (e, data) => emits('moveItem', e, data)
// 右键鼠标
const onRightClick = (e, data) => emits('onRightClick', e, data)
// 滚动到某项
const yhDrag = ref(null)
const goToItem = id => {
    let count = 0; let needChanges = []; let supCount = 0;
    // 1.首先找到父级节点,并且拿到上级已经展开了多少。
    const visit = (arr, pRoot, targetId, show, level = 0) => {
        let rst = null
        arr.some(root => {
            // 因为需要找第一级的父级,所以每次第一级需要重置
            if (level === 0) {
                needChanges = []
                pRoot = null
                supCount = 0
            }

            // 如果是显示在页面才需要计算高度
            if (show) {
                count++
            } else {
                supCount++
            }

            // eslint-disable-next-line no-unused-expressions
            !pRoot && (pRoot = root);
            // 找到target
            if (root.id === targetId) {
                rst = pRoot
                return true
            }

            const curShowChild = showchilds.value.includes(root.id)

            if (root.children && root.children.length) {
                rst = visit(root.children, pRoot, targetId, show && curShowChild, level + 1)
                if (!!rst && !curShowChild) {
                    needChanges.push(root)
                }
                if (!rst && !(show && curShowChild)) {
                    supCount -= root.children.length
                }
                return !!rst
            }
            return false;
        })
        return rst
    }

    visit(props.data, null, id, true)
    // 展开
    needChanges.forEach(item => changeshowChild(item))
    const willScrollTop = (count + supCount - 1) * 69;
    // 统一读,减少重排。
    const el = yhDrag.value.$el;
    const {offsetHeight,scrollTop} = el;
    // 当内容目前在可视区里面则不滚动。
    if(willScrollTop > offsetHeight + scrollTop || willScrollTop < scrollTop){
        // 滚动
        nextTick(()=>{
            el.scrollTo({
                top: willScrollTop,
                behavior: 'smooth'
            })
        })
    }
}
defineExpose({ getSelectedList, setCheckedKeys, changeshowChild, setShowChild, goToItem })
</script>

<style lang="scss" scoped>
.drag-item {
    display: flex;
    cursor: pointer;
    position: relative;
}
</style>



yhdrag.vue

<template>
    <div class="drag-box" :style="getStyle">
        <draggable :list="data" group="tree_drag" animation="300" item-key="id" @end="dragEnd">
            <template #item="{ element }">
                <yhdragItemVue :data="element" :level="level" :showchilds="showchilds" @dragEnd="dragEnd">
                    <template #content="{ scope }">
                        <slot name="content" :scope="scope"></slot>
                    </template>
                </yhdragItemVue>
            </template>
        </draggable>
    </div>
</template>

<script setup>
import draggable from 'vuedraggable';
import { defineProps, defineEmits } from 'vue';
import yhdragItemVue from './yhdragItem.vue';

const props = defineProps({
    data: {
        type: Array,
        default: () => []
    },
    level: {
        type: Number,
        default: () => 0
    },
    dragCheck: {
        type: Number,
        default: () => 20
    },
    showchilds: {
        type: Array,
        default: () => []
    }
})

const emits = defineEmits(['dragEnd'])
const dragEnd = e => emits('dragEnd', e, props.data)

const getStyle = () => {
    if (!props.data.length) {
        return {
            height: `${props.dragCheck}px`,
            top: `-${props.dragCheck}px`,
            display: 'inline-block'
        }
    }
    return {}
}
</script>

<style lang="less" scoped>
.drag-box {
    position: relative;
}
</style>

yhdragItem.vue

<template>
    <div>
        <template v-if="data.children">
            <slot name="content" :scope="{ data, $level: level }"></slot>
            <yhdragVue
                v-show="showchilds.includes(data.id)" :data="data.children" :level="level + 1"
                :showchilds="showchilds" @dragEnd="dragEnd">
                <template #content="{ scope }">
                    <slot name="content" :scope="scope"></slot>
                </template>
            </yhdragVue>
        </template>
        <slot v-else name="content" :scope="{ data, $level: level }"></slot>
    </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';
import yhdragVue from './yhdrag.vue';

const props = defineProps({
    data: {
        type: Object,
        default: () => { }
    },
    level: {
        type: Number,
        default: () => 0
    },
    showchilds: {
        type: Array,
        default: () => []
    }
})
const emits = defineEmits(['dragEnd'])

const dragEnd = (e, data) => emits('dragEnd', e, data)
</script>

组件使用

<template>
    <div class="content">
        <drag ref="dragRef" :itemStyle="itemStyle" :data="cpnList" @dragEnd="dragEnd" @selectItems="selectItem"
            @hoverItem="hoverItem" :select="[{ id: 1 }]">
            <template #content="{ data }">
                <div> id:{{ data.id }} ----------- name:{{ data.name }}</div>
            </template>
        </drag>
        <el-button @click="getSelect" type="primary">获取选中</el-button>
        <div>{{ selectJson }}</div>
    </div>
</template>
    
<script setup>
import drag from './dragCpn/index.vue'
import { ref } from 'vue';

const itemStyle = {
    height: '50px',
    background: '#1890FF',
    width: '400px',
    lineHeight: '50px',
    marginBottom: '10px',
    boxSizing: 'border-box'
}

const cpnList = ref([
    { id: 1, name: '组件A', children: [] },
    { id: 2, name: '组件B', children: [] },
    {
        id: 3, name: '组件C', children: [
            { id: 4, name: '组件A', children: [] },
            {
                id: 5, name: '组件C', children: [
                    { id: 6, name: '组件B', children: [] }
                ]
            },
        ]
    },
    {
        id:7,
        name:'组件A'
    }
])
const dragRef = ref(null)

const selectJson = ref('')

const getSelect = () => selectJson.value = JSON.stringify(dragRef.value.getSelectedList());

const dragEnd = (e, v) => {
    // console.log(e, v);
}

const selectItems = (v) => console.log(v);

const hoverItem = (e, v) => {
    // console.log(e, v)
}

</script>

<style lang="less" scoped>
.content /deep/ .drag-item:hover {
    background-color: aqua !important;
}

.content /deep/ .drag-item.active {
    background-color: rgb(255, 68, 0) !important;
}

</style>


更新组件:index.vue改造增加多选拖动功能及可视区域不进行滚动。

<template>
  <yhdrag
    ref="yhDrag"
    :data="selfData"
    :drag-check="dragCheck"
    :showchilds="showchilds"
    @dragEnd="dragEnd"
  >
    <template #content="{ scope }">
      <div
        :class="getClass(scope.data)"
        :style="getStyle(scope.$level)"
        @mouseenter="hoverItem($event, scope.data)"
        @mousemove="moveItem($event, scope.data)"
        @dblclick="handleDbClick($event, scope.data)"
        @contextmenu.prevent="onRightClick($event, scope.data)"
        @click.exact="selectItem($event, scope.data)"
        @click.ctrl="selectItems($event, scope.data)"
        @click.shift="addItems($event, scope.data)"
      >
        <span :style="getIconStyle()">
          <template v-if="scope.data.children">
            <CaretRight
              v-if="!showchilds.includes(scope.data.id)"
              style="width: 1em; height: 1em"
              :color="iconColor"
              @click.stop="changeshowChild(scope.data)"
            />
            <CaretBottom
              v-else
              style="width: 1em; height: 1em"
              :color="iconColor"
              @click.stop="changeshowChild(scope.data)"
            />
          </template>
        </span>
        <slot
          name="content"
          :data="scope.data"
        ></slot>
      </div>
    </template>
  </yhdrag>
</template>

<script setup>
/**
 * ============================================info==================================================>
 * author:yihua
 * date:2022/10/26
 * describe:树形可拖拽组件,单击可选中,shift+单击可选中一片区域,ctrl+单击多选。
 * <=================================================================================================
 *
 * ===========================================属性================================================>
 * itemStyle:单个item项的样式,更推荐使用类名drag-item去更改基本样式。被选中的会被添加active类名;
 * data:数据,注意需要有id;
 * iconSize:树形节点图标的大小;
 * iconColor:树形节点图标的颜色;
 * select:默认选中的数据;
 * dragCheck:拖拽进入为子组件的触控区域;
 * styleId:悬浮的id,悬浮的item会增加类名self_style;
 * isdbCancel:是否按两下取消选中。
 * isMultDrag:是否多选拖动,多选拖动需要做到选子不选父,且还要增加是否可以拖到目标位置的逻辑
 * <================================================================================================
 *
 * ===========================================方法================================================>
 * dragEnd:拖拽完成之后的回调,第一个参数是事件对象e,第二个参数是改变后的数据,第三个是将要被移动的数据的数组,第四个参数是将要移动的位置的父级id;
 * selectItem:选中的回调,第一个参数是事件对象e,第二个参数是当前选中的数据;
 * selectItems:选中数据改变的回调;
 * hoverItem:悬浮的回调,第一个参数是事件对象e,第二个参数是当前悬浮的数据;
 * moveItem:鼠标离开的回调,第一个参数是事件对象e,第二个参数是当前离开元素的数据;
 * handleDbClick:双击的回调,第一个参数是事件对象e,第二个参数是当前双击元素的数据;
 * onRightClick:右键鼠标的回调,第一个参数是事件对象e,第二个参数是当前右键元素的数据;
 * getSelectedList:组件方法,获取所有选中的数据;
 * setCheckedKeys:设置当前组件哪些元素被选中;
 * setShowChild:设置当前组件哪些元素应该被展开子元素;
 * changeshowChild:改变当前组件某个节点的孩子是否展示;
 * goToItem:滑动到某一项。
 * <================================================================================================
 */
import { CaretRight, CaretBottom } from '@element-plus/icons-vue';
import { computed, defineEmits, defineProps, ref, defineExpose, watch, nextTick } from 'vue';
import yhdrag from './yhdrag.vue';

const props = defineProps({
  itemStyle: {
    type: Object,
    default: () => {}
  },
  data: {
    type: Array,
    default: () => []
  },
  iconSize: {
    type: Number,
    default: () => 14
  },
  iconColor: {
    type: String,
    default: () => '#FFFFFF'
  },
  select: {
    type: Array,
    default: () => []
  },
  dragCheck: {
    type: Number,
    default: () => 20
  },
  styleId: {
    type: String,
    default: () => ''
  },
  isdbCancel: {
    type: Boolean,
    default: () => false
  },
  isMultDrag: {
    type: Boolean,
    default: () => true
  }
});

/**
 * 深拷贝
 * @param {*} obj
 * @param {Map} map
 * @returns
 */
const deepClone = (obj, map = new WeakMap()) => {
  if (typeof obj !== 'object' || obj == null) return obj;

  // 最开始map是空的map,所以objFromMap是undefined
  const objFromMap = map.get(obj);
  if (objFromMap) return objFromMap;

  let target = {};
  map.set(obj, target);

  // Map
  if (obj instanceof Map) {
    target = new Map();
    obj.forEach((v, k) => {
      const v1 = deepClone(v, map);
      const k1 = deepClone(k, map);
      target.set(k1, v1);
    });
  } else if (obj instanceof Set) {
    target = new Set();
    obj.forEach(v => target.add(deepClone(v, map)));
  } else if (obj instanceof Array) {
    target = [];
    obj.forEach(item => target.push(deepClone(item, map)));
  } else {
    // eslint-disable-next-line no-restricted-syntax, guard-for-in
    for (const key in obj) {
      const value = obj[key];
      target[key] = deepClone(value, map);
    }
  }
  return target;
};

// 维护一份自己的数据便于更改
const selfData = ref([]);
watch(
  () => props.data,
  val => {
    // 后续可以直接使用props.data,否则拖动会直接改变props.data
    selfData.value = deepClone(val);
  },
  {
    immediate: true
  }
);

// 获取样式
const getStyle = level => ({
  ...props.itemStyle,
  paddingLeft: `${(level + 1) * 20}px`
});

// icon样式
const getIconStyle = () => ({
  display: 'inline-block',
  fontSize: `${props.iconSize}px`,
  width: `${props.iconSize}px`,
  height: '10px',
  marginRight: '5px'
});

const showchilds = ref([]);
// 改变子集节点的显隐
const changeshowChild = data => {
  const LEN = showchilds.value.length;
  showchilds.value = showchilds.value.filter(item => item !== data.id);
  if (LEN === showchilds.value.length) {
    showchilds.value.push(data.id);
  }
};

// 选中的数组
const selectList = ref([]);
// eslint-disable-next-line vue/no-setup-props-destructure
selectList.value = props.select;

// 获取已选中的数据
const getSelectedList = () => selectList.value;
// 设置选中
const setCheckedKeys = arr => {
  selectList.value = arr;
};
// 设置展开子节点
const setShowChild = data => {
  showchilds.value = data.map(item => item.id);
};

// 注册事件
const emits = defineEmits([
  'dragEnd',
  'selectItem',
  'hoverItem',
  'handleDbClick',
  'moveItem',
  'onRightClick',
  'selectItems'
]);

// 初始化类名
const getClass = computed(() => data => {
  const isHover = data.id === props.styleId ? 'self_style' : '';
  if (selectList.value.findIndex(i => i.id === data.id) > -1) {
    return `drag-item active ${isHover}`;
  }
  return `drag-item ${isHover}`;
});

/**
 * 根据id找父级
 * @param {*} id
 * @param {*} data
 */
const getParentIdPathById = (id, data) => {
  const path = [];
  data.some(item => {
    if (item.id == id) {
      path.push(item.id);
      return true;
    }
    if (item.children && item.children.length) {
      const childPath = getParentIdPathById(id, item.children);
      if (childPath.length) {
        path.push(item.id, ...childPath);
      }
      return childPath.length > 0;
    }
    return false;
  });
  return path;
};

// 获取选中的字典
const getSelectDict = () => {
  const dict = {};
  selectList.value.forEach(item => {
    dict[item.id] = true;
  });
  return dict;
};

/**
 * 找子级所有的id
 * @param {Array} data
 */
const getChildId = (childrenData, childIds = []) => {
  if (childrenData && childrenData.length) {
    childrenData.forEach(child => {
      childIds.push(child.id);
      if (child.children && child.children.length) {
        getChildId(child.children, childIds);
      }
    });
  }
  return childIds;
};

/**
 * @param {Array} arr
 * @param {String} id1
 * @param {String} id2
 */
const findtwoIdsData = (arr, id1, id2) => {
  const rst = [];
  const hitId = id => id === id1 || id === id2;
  let hitTimes = 0;

  const addItem = list => {
    list.forEach(item => {
      if (hitTimes === 0 && hitId(item.id)) {
        // 命中第一次
        rst.push(item);
        hitTimes++;
        if (!props.isMultDrag && item.children && item.children.length) {
          addItem(item.children, hitTimes);
        }
      } else if (hitTimes === 1 && hitId(item.id)) {
        // 命中第二次
        rst.push(item);
        hitTimes++;
        // eslint-disable-next-line no-useless-return
        return;
      } else if (hitTimes === 1) {
        // 命中第一次中间的
        rst.push(item);
        if (!props.isMultDrag && item.children && item.children.length) {
          addItem(item.children, hitTimes);
        }
      } else if (item.children && item.children.length) {
        addItem(item.children, hitTimes);
      }
    });
  };
  addItem(arr);
  return rst;
};

const preId = ref('');
// 按住ctrl点击选中
const isSelect = data => {
  const pIds = getParentIdPathById(data.id, selfData.value).slice(0, -1);
  const cIds = getChildId(data.children);
  const selectDict = getSelectDict();
  let canSelect = true;
  // eslint-disable-next-line array-callback-return
  pIds.concat(cIds).some(id => {
    if (selectDict[id]) {
      canSelect = false;
      return true;
    }
  });
  return canSelect;
};
const selectItems = (e, data) => {
  // 多选拖动需要做到父子级不能同时选中
  if (props.isMultDrag && !isSelect(data)) {
    return;
  }
  const LEN = selectList.value.length;
  selectList.value = selectList.value.filter(item => item.id !== data.id);
  if (selectList.value.length === LEN) {
    preId.value = '';
    selectList.value.push(data);
  }
  preId.value = data.id;
  emits('selectItem', e, data);
  emits('selectItems', selectList.value);
};

// 按住shift点击选中
const addItems = (e, data) => {
  if (!preId.value || preId.value === data.id) {
    setCheckedKeys([data]);
  } else {
    // 将两次点击中间的数据添加到数组中
    setCheckedKeys(findtwoIdsData(selfData.value, preId.value, data.id));
  }
  preId.value = data.id;
  emits('selectItem', e, data);
  emits('selectItems', selectList.value);
};

// 直接点击选中
const selectItem = (e, data) => {
  preId.value = data.id;
  if (props.isdbCancel) {
    const LEN = selectList.value.length;
    selectList.value = selectList.value.filter(item => item.id !== data.id);
    if (selectList.value.length === LEN) {
      setCheckedKeys([data]);
    } else {
      setCheckedKeys([]);
      preId.value = '';
    }
  } else {
    setCheckedKeys([data]);
  }
  emits('selectItem', e, data);
  emits('selectItems', selectList.value);
};

// 悬浮
const hoverItem = (e, data) => emits('hoverItem', e, data);

/**
 * 移动数据
 * @param {Array} data  已经移动了最后拖动的数据
 * @param {Object} selectDict  已选中的id映射
 * @param {*} targetId 需要替换的位置id
 */
const moveData = (data, selectDict, targetId) => {
  const rst = [];
  // TODO,用新数组来收集,类似深拷贝
  data.forEach(item => {
    if (item.id === targetId) {
      rst.push(...selectList.value);
    } else if (!selectDict[item.id]) {
      if (item.children && item.children.length) {
        // 不在选中里面
        rst.push({
          ...item,
          children: moveData(item.children, selectDict, targetId)
        });
      } else {
        rst.push(item);
      }
    }
  });
  return rst;
};
// 拖拽
const dragEnd = (e, data) => {
  // eslint-disable-next-line no-underscore-dangle
  const targetPostionItemId = e.item.__draggable_context.element.id;
  // 判断当前移动元素是否在选中的元素里面,如果不在可以移动
  const selectDict = getSelectDict();
  // 找到父级路径
  const targetParentIdPath = getParentIdPathById(targetPostionItemId, data).slice(0, -1);
  // 不进行多选拖动或者没有选中直接拖动或者被拖动元素不在选中直接执行原有的逻辑
  if (!props.isMultDrag || selectList.value.length === 0 || !selectDict[targetPostionItemId]) {
    // eslint-disable-next-line no-underscore-dangle
    emits('dragEnd', e, data, [e.item.__draggable_context.element], targetParentIdPath[targetParentIdPath.length - 1]);
    return;
  }
  let canMove = true;
  // 判断选中元素是否移动到自己的所在路径,由于路径是移动位置最后父级,所以可以直接使用路径来判断。
  // eslint-disable-next-line array-callback-return
  targetParentIdPath.some(cid => {
    if (selectDict[cid]) {
      canMove = false;
      return true;
    }
  });
  // 不能移动将数据恢复为原样
  if (!canMove) {
    selfData.value = deepClone(props.data);
    return;
  }
  // 可以移动判断,将选中元素填充到目标位置,并且清除原来选中的子节点位置。
  selfData.value = moveData(selfData.value, selectDict, targetPostionItemId);
  emits('dragEnd', e, selfData.value, selectList.value, targetParentIdPath[targetParentIdPath.length - 1]);
};
// 双击
const handleDbClick = (e, data) => emits('handleDbClick', e, data);
// 鼠标离开
const moveItem = (e, data) => emits('moveItem', e, data);
// 右键鼠标
const onRightClick = (e, data) => emits('onRightClick', e, data);
// 滚动到某项
const yhDrag = ref(null);
const goToItem = id => {
  let count = 0;
  let needChanges = [];
  let supCount = 0;
  // 1.首先找到父级节点,并且拿到上级已经展开了多少。
  const visit = (arr, pRoot, targetId, show, level = 0) => {
    let rst = null;
    // eslint-disable-next-line array-callback-return
    arr.some((root, index) => {
      // 因为需要找第一级的父级,所以每次第一级需要重置
      if (level === 0) {
        needChanges = [];
        pRoot = null;
        supCount = 0;
      }

      // 如果是显示在页面才需要计算高度
      if (show) {
        count++;
      } else {
        supCount++;
      }

      // eslint-disable-next-line no-unused-expressions
      !pRoot && (pRoot = root);
      // 找到target
      if (root.id === targetId) {
        rst = pRoot;
        return true;
      }

      const curShowChild = showchilds.value.includes(root.id);

      if (root.children && root.children.length) {
        rst = visit(root.children, pRoot, targetId, show && curShowChild, level + 1);
        if (!!rst && !curShowChild) {
          needChanges.push(root);
        }
        if (!rst && !(show && curShowChild)) {
          supCount -= root.children.length;
        }
        return !!rst;
      }
    });
    return rst;
  };

  visit(selfData.value, null, id, true);
  // 展开
  needChanges.forEach(item => changeshowChild(item));
  const willScrollTop = (count + supCount - 1) * 69;
  // 统一读,减少重排。
  const el = yhDrag.value.$el;
  const { offsetHeight, scrollTop } = el;
  // 当内容目前在可视区里面则不滚动。
  if (willScrollTop > offsetHeight + scrollTop || willScrollTop < scrollTop) {
    // 滚动
    nextTick(() => {
      el.scrollTo({
        top: willScrollTop,
        behavior: 'smooth'
      });
    });
  }
};
defineExpose({
  getSelectedList,
  setCheckedKeys,
  changeshowChild,
  setShowChild,
  goToItem
});
</script>

<style lang="less" scoped>
.drag-item {
  display: flex;
  cursor: pointer;
  position: relative;
}
</style>



Logo

前往低代码交流专区

更多推荐