微信小程序 拖拽签章

效果

在这里插入图片描述

主要实现的功能点

  1. 文件按比例加载图片(宽高设定拖拽范围)
  2. 弹层展示印章模板
  3. 模板拖拽到文件图片上
  4. 实时获取拽拽位置

难点 弹层中的元素如何拖拽到文件图片上

实现历程

版本1.0

以前我们拖拽一个图层到另一个图层上,pc端使用的是mousedown mousemove mouseup事件

在这里插入图片描述

如上图:

  1. 給物料区域,图片A绑定点击事件,点击图片A,传输数据

  2. 生成一个拖拽dom元素,追加div元素到文件区域中心

  3. 然后绑定鼠标事件,鼠标事件实时获取坐标位置

为什么不直接使用 dragstart dragend drop, 当时文件时pdf,pdf展现的形式是canvas

  • 缺点:点击添加拖拽dom元素,然后再点击元素进行拖拽,多页文件得点击按钮翻页进行拖拽

版本2.0

现在文件的展示形式是图片的形式,然后使用的是dragstart dragend drop事件

在这里插入图片描述

如上图:

现在是多页滚动展示文件

  1. 給物料区域,图片A绑定drag事件,点击图片A,触发drag事件,按住鼠标左键,拖动图片A元素,图片A跟随鼠标到拖拽区域

  2. 蒙层也就是div图层绑定drop事件,用于接收数据(img只是用来展示,img元素和蒙层div是兄弟关系)

  3. 接收数据后,拖拽div数组添加元素,通过for循环展示拖拽div(这个拖拽div蒙层div的子元素),并设定当前文件页的拖拽范围

  4. 拖拽div绑定mouse事件,实时获取坐标位置

鼠标移动过程中,拖拽div跟随鼠标移动

鼠标松开,拖拽div固定在当前位置

  • 优点:文件多页是滚动展示,拖拽添加

  • 缺点:虽然文件多页是滚动展示,但是拖拽dom元素只能在当前文件页进行拖拽,上下分页的情况不能直接从上一页拖拽到下一页

版本3.0

h5版本,使用的是touchstart touchmove touchend事件

在这里插入图片描述

移动端不支持dragstart dragend dropmousedown mousemove mouseup事件,所以h5版本使用的是touchstart touchmove touchend事件,但是同理:

  1. 还是先给物料区域的图片A绑定点击事件,点击图片A,传输数据

  2. 在点击图片A事件中,生成一个拖拽dom元素,追加div元素到文件区域中心

  3. 然后给拖拽dom元素绑定touchstart touchmove touchend事件

touchstart事件中,获取拖拽dom元素的坐标位置

touchmove事件中,实时获取拖拽dom元素的坐标位置

touchend事件中,拖拽dom元素固定在当前位置

touchmove事件中,实时获取拖拽dom元素的坐标位置,计算拖拽dom元素的位置,实时更新拖拽dom元素的位置

  • 优点:增加了拖拽元素可以从上一页直接拖拽到下一页

  • 缺点:还是点击添加拖拽dom元素,然后再点击元素进行拖拽

版本4.0

微信小程序版本,使用的是touchstart touchmove touchend事件

在这里插入图片描述

项目目录

─src
    ├─components
    │  ├─z-drag-add  印章模板弹层-添加操作
    │  ├─z-drag-dom  拖拽dom组件
    │  ├─z-drag-files  拖拽区域(文件展示)
    │  ├─z-drag-pineapples  展示印模组件-编辑、删除操作
    ├─config
    ├─hooks
    ├─mock
    ├─pages
    │  ├─index
    ├─plugins
    ├─static
    │  └─images
    ├─store
    │  └─modules
    ├─types
    └─utils

准备阶段:印章模板弹层,拖拽区域(文件展示),拖拽dom组件,拖拽到文件区域展示印模组件

他们之间的关系

在这里插入图片描述
看不清可以看这个链接

如上图:

z-drag-files组件

只负责接收父组件index的数据,展示文件图片

<template>
  <view class="files-box">
    <view class="files" v-for="item in files" :key="item.id"
      :style="{ width: item.width + 'px', height: item.height + 'px' }">
      <image class="file-img" :src="item.url" :style="{ width: item.width + 'px', height: item.height + 'px' }"></image>
    </view>
  </view>
</template>
<script setup lang="ts">
import type { File } from '@/types/mock'
const props = defineProps<{
  files: File[]
}>();
</script>
<style>
.files-box {
  margin-top: 27px;
}

.files {
  margin: 0 auto 20rpx;
}

.file-img {
  box-shadow: 0 5rpx 10rpx rgba(0, 0, 0, 0.3);
  border-radius: 16rpx;
}
</style>

z-drag-add组件-添加操作

印章模板弹层,每一个印模添加dragStartdragMovedragEnd事件

实时传输拖拽dom元素的坐标位置,印章高宽及业务数据

父组件index负责接收数据,添加数据,z-drag-pineapples增加拖拽dom元素

<template>
  <view class="custom-collapse">
    <view class="content" :class="isOpen ? 'show-content' : ''">
      <view class="content-box">
        <view class="pineapple" v-for="(item, index) in pineapples" :key="index"
          @touchstart.stop="dragStart($event, item, index)" @touchmove.stop="dragMove($event, item)"
          @touchend.stop="dragEnd($event, item)">
          <view class="pineapple-t">
            {{ item.typeName }}
          </view>
          <view class="pineapple-b">
            <text class="name">{{ item.name }}</text>
          </view>
        </view>
      </view>
    </view>
    <view class="custom-collapse-header" v-show="isOpen">
      请添加签署区拖拽到需要签字盖章的位置
    </view>
    <view class="content-header" :class="isOpen ? 'is-open' : ''">
      <view class="content-title" @click.stop="handleVisible">
        <text class="arrow-icon" :class="isOpen ? 'is-open' : ''"></text>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, getCurrentInstance, watch, } from "vue";
import type { File, Pineapple } from '@/types/mock'
import { throttle } from 'lodash-es'
import { getRect } from '@/hooks/helper'

const instance = getCurrentInstance();
const props = defineProps({
  pineapples: {
    type: Array,
    default: () => [],
  },
  files: {
    type: Array as () => File[],
    default: () => [],
  },
  scrollTopHeight: {
    type: Number,
    default: 0,
  },
});

const emit = defineEmits(["dragStart", "dragMove", "dragEnd"]);
defineExpose({ open: () => isOpen.value = true, close: () => isOpen.value = false });
const handleVisible = () => {
  isOpen.value = !isOpen.value;
};

const isOpen = ref(true); // 控制折叠状态的变量
const isDown = ref(false); // 是否正在拖拽
const pineapplePos = ref({ divX: 0, divY: 0 });

const dragStart = async (e: TouchEvent, item: Pineapple) => {
  e.stopPropagation();
  isDown.value = true;
  isOpen.value = false;
  try {
    const rect = await getRect('.pineapple', instance);
    const touch = e.changedTouches[0];
    const { clientX: startX, clientY: startY } = touch;
    let { offsetLeft: left, offsetTop: top } = e.currentTarget as HTMLElement;
    pineapplePos.value = {
      divX: startX - left,
      divY: startY - top,
    };
    top = props.scrollTopHeight + top;
    emit("dragStart", 'add', item, rect, left, top);

  } catch (error) {
  }
};

const dragMove = async (e: TouchEvent, item: Pineapple) => {
  e.stopPropagation();
  if (!isDown.value) return;
  try {
    const rect = await getRect('.pineapple', instance);
    const touch = e.changedTouches[0];
    const { divX, divY } = pineapplePos.value;
    // 使用保存的偏移量计算新位置
    const newX = touch.clientX - divX;
    let newY = touch.clientY - divY;
    newY = newY + props.scrollTopHeight; // 添加滚动高度; 
    const { width, height } = rect;
    emit("dragMove", 'add', item, newX, newY, width, height);
  } catch (error) {
  }
}

const dragEnd = async (e: TouchEvent, item: Pineapple) => {
  e.stopPropagation();
  isDown.value = false;
  const rect = await getRect('.pineapple', instance);
  emit("dragEnd", 'add', item, rect,);
};
</script>
<style>
.custom-collapse {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  overflow: hidden;
  margin-bottom: 20rpx;
  background: #fff;
  z-index: 11000;
  box-shadow: 0 -4rpx 15rpx rgba(0, 0, 0, 0.3);
  border-bottom-left-radius: 32rpx;
  border-bottom-right-radius: 32rpx;
}

.content {
  width: 95vw;
  max-height: 0;
  overflow-y: hidden;
  transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  margin: 0 auto;
  /* 添加硬件加速 */
  transform: translateZ(0);
  will-change: max-height;
}

.content-header {
  padding: 0 24rpx 12rpx 24rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  transition: all 0.3s;
}

.is-open {
  border-bottom: 1px solid #eee;
}

.content-title {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  padding-top: 12rpx;
}

.arrow-icon {
  display: inline-block;
  width: 30rpx;
  height: 30rpx;
  transition: transform 0.3s ease;
}

.arrow-icon::after {
  content: "";
  position: absolute;
  top: 60%;
  left: 50%;
  width: 16rpx;
  height: 16rpx;
  border-left: 2rpx solid #666;
  border-bottom: 2rpx solid #666;
  transform: translate(-50%, -70%) rotate(-45deg);
  transition: transform 0.3s ease;
}

.arrow-icon.is-open::after {
  transform: translate(-50%, -30%) rotate(135deg);
}

.show-content {
  max-height: 560rpx;
  /* 添加以下属性改善动画性能 */
  transform: translateZ(0);
  backface-visibility: hidden;
  perspective: 1000;
  will-change: transform;
}

.content-box {
  display: flex;
  width: 100%;
  padding-top: 24rpx;
}

.custom-collapse-header {
  width: 90vw;
  margin: 0 auto;
  font-size: 24rpx;
  color: #999;
  padding: 20rpx 0;
}
</style>
<style>
.pineapple {
  display: flex;
  flex-direction: column;
  width: 160rpx;
  height: 200rpx;
  border-radius: 12rpx;
  background-color: #faf6f5;
  box-shadow: 0 15rpx 23rpx rgba(0, 0, 0, 0.3);
  overflow: hidden;
  color: #f66e5d;
  margin: 0 20rpx;
}

.pineapple-t {
  width: 100%;
  font-size: 24rpx;
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  background-size: cover;
  background-image: url('data:image/png;base64,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');
}

.pineapple-b {
  width: 100%;
  height: 50rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  line-clamp: 1;
  -webkit-box-orient: vertical;
  border-top: 1rpx solid #c8c7cc;
  text-align: center;
  background-color: #f66e5d;
  color: #fff;
}

.pineapple-b .name {
  font-size: 18rpx;
  padding: 0 10rpx;
}
</style>

z-drag-pineapples组件-编辑、删除操作

z-drag-add组件(印章模板弹层)拖拽一次印模,生成一个印章展示

然后z-drag-pineapples组件生成的每个印章绑定dragStartdragMovedragEnd事件,

实时传输拖拽dom元素的坐标位置,印章高宽及业务数据

拖拽过程中是相当于编辑操作,更新坐标位置数据

点击删除图标执行删除操作

<template>
  <view class="pineapple" v-for="item in pineapples" :key="item.uuid" :id="item.uuid"
    @touchstart.stop="dragStart($event, item)" @touchmove.stop="dragMove($event, item)"
    @touchend.stop="dragEnd($event, item)" :style="{ left: item.x + 'px', top: item.y + 'px' }" v-show="item.show">
    <view class="pineapple-del" @touchend.stop="handleDelete(item)">
      x
    </view>
    <view class="pineapple-t">
      <view class="">
        x:{{ item.x }}
      </view>
      <view class="">
        y:{{ item.y }}
      </view>
      <view>
        page:{{ item.page }}
      </view>
    </view>
    <view class="pineapple-b">
      <text class="name">{{ item.name }}</text>
    </view>
  </view>
</template>
<script setup lang="ts">
import { ref, computed, getCurrentInstance, watch, onMounted, } from "vue";
import type { Pineapple } from '@/types/mock'
import { PropType } from 'vue';
import { throttle } from 'lodash-es'
import { getRect } from '@/hooks/helper'

const instance = getCurrentInstance()
const props = defineProps({
  pineapples: {
    type: Array as PropType<Pineapple[]>,
    default: () => [],
  }
});
const emit = defineEmits(["dragStart", "dragMove", "dragEnd", "dragDelete"]);

const pineappleRef = ref({ width: 83, height: 104 })// 凤梨默认尺寸
const isDown = ref(false); // 是否正在拖拽
const pineapplePos = ref({ divX: 0, divY: 0 });

const dragStart = async (e: TouchEvent, item: Pineapple) => {
  console.log('dragStart');

  e.stopPropagation();
  isDown.value = true;
  try {
    const rect = await getRect(`#${item.uuid}`, instance);
    const touch = e.changedTouches[0];
    const { clientX: startX, clientY: startY } = touch;
    const { offsetLeft, offsetTop } = e.currentTarget as HTMLElement;
    pineapplePos.value = {
      divX: startX - offsetLeft,
      divY: startY - offsetTop,
    };
    emit("dragStart", 'update', item, rect, offsetLeft, offsetTop);
  } catch (error) {
  }
};

const dragMove = throttle(async (e: TouchEvent, item: Pineapple) => {
  e.stopPropagation();
  if (!isDown.value) return;
  try {
    const touch = e.changedTouches[0];
    const { divX, divY } = pineapplePos.value;
    // 使用保存的偏移量计算新位置
    const newX = touch.clientX - divX;
    const newY = touch.clientY - divY;
    emit("dragMove", 'update', item, newX, newY);
  } catch (error) {
  }
}, 50);

const dragEnd = async (e: TouchEvent, item: Pineapple) => {
  e.stopPropagation();
  isDown.value = false;
  const rect = await getRect(`#${item.uuid}`, instance);
  emit("dragEnd", 'update', item, rect);
};

const handleDelete = (item: Pineapple) => {
  const index = props.pineapples.findIndex((i: Pineapple) => item.uuid === i.uuid);
  emit("dragDelete", index);
};
</script>
<style>
.pineapple {
  position: absolute;
  display: flex;
  flex-direction: column;
  width: 160rpx;
  height: 200rpx;
  border-radius: 12rpx;
  background-color: #faf6f5;
  box-shadow: 0 15rpx 23rpx rgba(0, 0, 0, 0.3);
  color: #f66e5d;
  margin: 0 20rpx;
}

.pineapple-t {
  width: 100%;
  font-size: 24rpx;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border-radius: 12rpx;
  color: #f90511;
  background-size: cover;
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAAC4jAAAuIwF4pT92AAAgAElEQVR42pVd598UVbLmX7337q5rXHMOuMY1YUSSgAGMKGBCkKACgigYUJKCYeF9J+fQfU5P3XrqhK7TM7j3fpjfMPNOd5+urvDUU1WHFXmWkclyMjleUzIG7xlZw//G30wm7zm+N+5zzn/HcRn+jt9m/rfyb/eO81njz+uPwec8/p2Pl/epvwb/js/j1oPz53KMW1cmf8vC5/C73P024++wFpv532Lt8fqZuyc5X+7OjbXk5Tr0fcl69HX9/TsZGfkO57H+bytyfaJ4stz/UJ3MlBfN/SJwovL4XL0gcP/3cBN5+Z6r3+N38Rh/zvgbeWjl5+Sa+ryVa+TxWvqYcJ68PCY+4KlfS1AeJWz/7zweY/wr9wI0WXlif1K9ePe9iRcNQrH8nVUn0xew6vfyN1MK16qLG1Nqae6PFcHHc/trW1PebHzQan36AeJBW6O0v3xZte7Cule4jk2Eo4UUBG3Ke8M6c3eNFfopWgvV5BOHi+BAW140WQD/u7DuheNM/N4fF4/1L5sKOs/LvyXaoDTcmSpeE2VaWRR2VZjhVfiX9euL/5bvS4GFz2HN1oR7KX8f78lasoWloij4bzb+ZoXNS20IJ5v5g2fx5EYWMjPh+9w9QX+M9Z9n4Rzq6erjnYalpm4gnCgoFhIWx4vMZwUZIsr9y/Di86h1UzkGL6sEaK2JApP7CC+/1pkXSFCKmQ2/8cf4z0Gg+u8zW4jwggCDua/QF7f+IjN/sBaKnCjP44JmXsDxtyZPFlsoLbXKHHJ/4/J5NhMhhVfOn6e9Hk3rNZouL9H43Fkanz1Nkwu/0KRep2wyoXwydgINgoXAoZXTqQ9g/n5yE4USFSM+3FJLsX5SyjKb+7cXpIX2zZzwxOUY8Z2igcHnFfFJlBrlTlAKln9IvDq5KKmLz4LA1NMsrDLNoGXQJNw8C2Ly7z9ofPoUDQ9+ToO9u6m7ZRM1Hl5J9ZV3Uf2OG6l2/dVUu+kaWr7xaqrfcxs1/vUgNR5/mDpbX6H+nk9o8uMPlA0GlEOIECY/AIcQpon/i1ah1hnvLWhgcEcV0w73R9ZGDYw+UjTQC8/a0t71v4Naz2yp4rPcvVMUbmn6pTP28AVCwzW8xkxrS9Tft4eaqx6j2p030/I//k7L115BtVv+QY1/3kPN556kzssvUfvljdTduYM6296i1msvU2vNC9R85gmqP3Af//Z6Wrr6CmpcfyXV+ZjWS2tp8Nk+mvz2q4uauJZxWlkGxFQjoyVZU95D8H+JwL2QIbyZ08DgA+GrVzj/Z5Sql47dOVL3HYUnYvWTK/2bVVFOnv7UY0G5GUPj706I5tQevp9qK++mxlOPUWvtauq8v4OGJ47TkDUR5jo6e4aGP56k4fff0og1bPDNMX4/SZOL/6bRmdM0Zq0dnT1LQ/6+99H71HhuFdVuv4kuXfnfVLvtBuqsX0OjUycpGztTz4P2x8hvEl/vFMFEv25j4MijkhReA2cswNIHGgdjjMkivJAT2jzxYdGE9Us5XqveIwSaTkTj4LNGp36k9mubqHbzP2j57lupuW41jc//QuPffmOBnaHR6Z9ocOgL6n9+gAbfHhchDb5j4f30I41//pk/n6LR+XM0/uM3Gp76iSbLl6h/6CD1Duyj0YlvRLijU+4crdXP0RKbfZ01u8UPaHjsK8qghTBvieRZRAExGGplsKkLKu+V74YFN5sVIkTRwNwFyRUBA1rlOAvtPEX6Zi54hCgswcd6reNFBnOFf2tvfZWaa5+jxhOPUm/3LhpfvCgaNvjqCA2OHKb+sSMsyAs0bbflRkc/fMcCWkqib3hl/BJh8vEZR8QJHzM+/zP1j35JvYNf0JDPB0EOT56U6y7fdiMtsWtovfgcjX/5mc/B/tH6IBYitoZjVdShorH4eltEDSz9IGtgFgFuPhdZIThSKh6jbBCeBpssAKd1Y+rv/oj91VPU3LRebnDS69Loj9/Z/H4X4Y3ZV2VIzbxgkIaJr9u4gX3g03J8xovFuWB6eG+zH2yveZ5a61+k3oc7JWLnPorjXJM//qABhLn/U34Q37P2nqPW66/R0k3Xis/sv7+dpv2eO246jkGmUAgiYtggj6CFNpgw/GDAgUbWvSLLMnVAgCRl4NAReKZ8otUpFp4qLwxa11rzHDWffJSmrRZN+DXlKDk6d0ZMcjpxpg2/iLw2G49Eu7of7BChyPd8k20OCt23XnfC4c+t1c+yv/uAI3ghvgefh8e/ls/ZcJhgRghp8NVRCUCikT/8QO0tr9DyTddRmwMX3IdEbJ/mldoYXFEWgX+wRlImXAJpE3JhDzNsBVjaAFdM9AcRNPv0DAvIvL+Ds19mJ77M0RT+jI9gTNelcW2ZpnDofNHxhfM0OHaUxr//Lv5RTH3pInXe3CrnEX9lHMbqsPa01nLkfeEZ9nf7o3YDBsF/tjesc3iMvx9f+jeb7vfU3riOuu+9I+aKa/f376URr2vSalKXNXAZsOjOW2jA38ecOsvKwGHNXHAsrdEFEWfGNiKNFTovDf4gDRqm4htKbCfay4vtM46rP/YQtdevpeG3J0QQuCkRimdLpu0OTbs9Gl24QF3GcIiy8H2dLS+LVkCL5LdYmNemwddf0eiXXyLIhvCC4Lsf7BSo0//0E/Z5r7CGc6ReWqLBvn0Mkz51Pg8PiAF4/8gheXjD48epft+dDIH+St1333HnFIZlKgpkK5nWLHFd1kEZ0UCXC4sAI4hOUiBtsnkMHgGuyNODtvACesB0rCUQCPzWpN0U0xQTzQGcDU0bNb6RmghbbowFMe12RRsHLPwYKFhTp3zDI3b60KgRYA0LZnTyOxqxT5uKNk/cb0dD6u76kCHQCcpHoyh0nKP74Q6a8t+dRht5KIPPDkigwcOqM5SCNnZZy4Of1cmEjegiQDfgQKeBFgI01pMLns6SKGwVcFaqi/QtfB/Tsam7if7Rw9R+4zXqfb6fJnxz4usQIHxkFxCNGxj0RdtyPgcWnPnjHbBeZkEeoT5jus4bW6jz9hvU3fEeddgUu/g3a0pn62vU3rRBfGNnwxo2+S00Xbok54qCC5kOayiC1BRBCmuFn2Ttxm+GDHt6uz/mgPM7ZzSP0PJV/0M9Ppf4Vp+9WJ3DW6VQPpXTZAJy8hXWk4k2ZiABA5WkQtQ8FW2hccsrb6cOR8dJs0GTRkNMCJqX4+mP/ZPlC45/OSfOPfMQR45nfNg/+Bmb/Rr2VRw5f2KM16g74SICh1w3CIhhBLRucumSwB1oJgJP//BBNtHDMQNBMBudOy2YEpYg6SJjSLgUESLjzu7O98QPw+0s/+NKGjKulAcKf67TUZ//SxwQ83UCdPSaBtIhlQspnALUJYXl2WQ+eMpOufYIm8G9t/Niz7I5dpxm8ZOeNuuSUk26jNN+/02A9Jj9WPBJfXbq3U8+lheOhd+LeK/wT3bq0jAB5PLuGGgRktemjLUAmj46d078aPf1VyTqQoDQwNbG9ay9b1GXLaR/+AsB6P39e+R64zNnqP/xBzT59Veq3X+3wJwRg3cnxGlJzQWIA3kgCnsyQXOgK0puzihKR4FlGwhEz9MBRrDWXbr9Rhqy1ozZNEcIArih4UCiXy60lRHBQjtdpLzI0XSvPP3RL+dL7ZLMxQsoU/xehdmOzHTm82tP+wcNnTBIR+SG2QswZ1OGn83g8D3ZgIDT/3yfwCXAIInSjBlrjBWbD9zL7qQm7I6sQ3GCJRtTQphANK8IT9ZW6J0SLHvIgkXzwf0vPqPag/dSj01nzPkp8tbcCwLCEwHC1wmJ4G4Q5tRev1qiauT3RLOyhGKP2q5yUqOxWQW8Bxgia/PAesB4s8mAe8D+LoBsidysuQDXyGawPlkXB7ARr6336W5autL5Q2F0wtohhxADCkeolhHYeA3MS9o+2r3iAUVdBevNxByXWfOaG9fSmDUL2E9MFxGXfz/mNCzr92MdQRb55UFqv7VV4IQJJjJ1mhV5SOU6ish+a/ZE5ec2TfqN8s25z3unnQ5DnJc4EL3t/CnWD+HyWnoMcUDYSvRlhQA+BDnRXP081eAPDx+MphxYc7cOKxFYU/6SiZSMtEkS7ELVNnKfNbRfWkONp5+QDKPPi5mwygs2g+PnkyMITJtNZzZ8XPfD7dT9aKfzc4h03kkH8iKN/GaOV5zpdEplRyXfaOJDiCY+cZaSM9ToMkDvcEaDa0OAPfZ7IB2MZ2mwduTevY/fp/Hp01S7lZXjn/eIj5dz+LzZrc8z0rZkpKMGmkrNIPg/qyALqKVLN19L3d2f0AQ47cTxciEwE466o18viHBhLuD8Oju280IKBWnKGkth5un3QDGRtwDydBLpHF1T8JX6B17R3eCaLDRkJj32u1hrj3NsZDdjDiiAXXBf8IfIoZGjdxg+XWKQLUIX5rksj8KEZ0UllbOxrJkn6Vqk4f3f4Eca61bT8v13cUQ9T/0D+yRgIEVCpAXNhPRq2m45qMAL7O36iG8g5eNshfFOWG8f7QJJS9VMQJG3kRWyuoyQYlXj69YZ33D7lY0SMLLhiK3ifWGGWpz2tdhXyvcgM97fIXxk/cGVVL/tJvGXUnJAqcA6H1h4NiaYsI1A2puwVXyYgxNTF7EY6C6xf4BWDVkTkSmMGfN1OKplCBzs91xaB+H9xD7vdVe/ULVV6/1IUa03KBYoCk+ZKy0iNYReyiPVpB9KqBAa9eAyzlQaTz9Jo9M/OquBD+TvAKO627dJdgJCd8ABsr9nN1265m+Sn2tYU9hUAwNBu8JWc2FT8ntycfZ9zWefoMZD99GEwWcfLAj/Rlhk1rRgxtIdANpp66vC8QlDMp3GEqTm3GZVvjHWHkKdxadPSiNLM1UUfEV7Z7EkYWJ5MvcBBFCr+dwqKVpJ0PPKgTy54+kxaOf49BlqsKXV776V7/c35z9RbPdkghU4Y73CGVWVw8XzQBYY9/QQ+gE2V95Bne3viPbhBaGNf2VTZr+RB5YEvoQB6+D4Ny6LgO/UdVVTISi8mZISZuAeQ9GqrJjlsUxq9fcJJZ8SwiEIurzdExBspt0d7/o1Txzrw59HXhGQ6o04B+++8ybVrvoLDfbvc1qI8qkqKoXKnAmMdCiCFxq6ZC5l6+3Zxcn3fTT8+azj9DpdcbBTgTFfiykgUCBdanKuKilcBMQ6opokkpI2R5Mr8rb8PKuSuZEZCaY9T/QmNeughZnDm/DbreefljQOmgXA3WJkAa0Us2Z3hPsFawOXBUGaYuaBtUvnghZGAVpV3Q/+yUECV9FqvPC0OFxkFEPOP+VCk5G8C93EkReC7oAU/eqomIXx2pdEWV1fmBNcvvClNTApQ1Y0VLuAWQLFAvFrnC+DybJWIcpK9EXkfecNV48eD12qyfn56OQP1HniYQdpOJuSon7ufPicAI3qIYlqLwwz4zr2ZUt33EQ9ziHBtMCcpa4LUOlJ1AFnIsg+4EfElGNX03xpMLC7sVwQhBc/VzVRv1eO8YFkZk1Sm06hjsqmECyRx/d71GSlwHtv1wcuWAjXWMi9IVNBMarLgm2wFo6RX3MSge4sW0nnvAmX1HZsv/CMCBiP+mMPi98DEwJ6XjAhyMljRzmd+4JNus046gj1vjxY+gtd1bNmDhCXhXmtfcr/aQ1TdYn4MmV0Lo8pjy/8uUoBeqvKXOBov/u2g2KMHlqbNzLg3sLK4SwJxC+qfngtcTTuf/KxY5Cmru0kCDERYJKL2jz6P3BzjdVPS6IORwtGBWrf3PyS4L4MKRmfECSBZCWi2pmHLPlcBhG0purrFnU7LPyt1sTkNwpsV+CO7jgQM2bFAKXVWr+G/+2YbmC+5rNPUn/3x6IEPYZnw+++pSXGg63VzwgODmXMRAOTTEQB0dx3QDXWvcD+j/Pei7+7IML4r/vpHsp90RqUEjKP7mf7Yrg3KvLOrEnw2kybYdVUtQlbJdDqv5P2ksrxsfCvimJ+HQ6aOTYJ0Ky19nnhFnEPuS9eNZ550hWijh+jEaMJdE+gzpMx5oWwxUqjAK1K5VQ6J0JAjsg5bQ19KO9tc/7vwgUhPQE+JWEfj4XCHzJ6hy/EMcH3WRV9yZamvFBYiRaaeQ20Fe1c+JvK93Zx2hfMWKzrjS0iqFAeCDCnt3e3EMDDr49R57XNtHzr9dLoJH4wz2NrRx7IhDQCu8wBJ0M3QP2JRwWhD0+ddJW04ZB9xgZJ2aSeDIbjiwOs7id8qXCaAHLt2OeCgq0IYIFJJp/tgsYmm1ceiknzZpuasjbj3r7dQnTg35mn4lrPrxJyAS5r8OVhYWrA0IAHsLMQSHQD5lTxgR69C+aBZvFBDfYLSHMGLKBJ09FRA0Soj953DDOKNewTJ569cO2xxmcL7oYKu8h/mYVak0blihYm5l8Vmpkz8ZnKs5NoLBrID/7E19R5dbPL1/lz9/VXpS5jAvn7xX7BhOAJcY+OivPdr7E3JkdnQpa29ArFzgJk0Ly88i7p0QPeQ7QV1pmfYJtDPKKWZB+M/YTZgG8IASSab1mUmQ8SC+CKNfNmejmNnAs488Go8NqttVBycxbaiM20vXa1w37gLNe84LITKVk0JEojN76EjIStzCGMwBkoGJNXm7VDjnj0S1q6+zbn4yDAUBRC1ELWse5FdsYTanMyjqzElfvyhfnpQjO1Fedvqr7MLDDxywg4MeUAfRR8UtmJBBKUXy/+Id1hU7gldlUTX38WBpszk8HePQKq69f8lfrvOzpMOv0VoQrFc4RqXlLmUcXZwS7ddYvTQA4S00E/VtXAYADNw4nibzmnSCGsV33fbE7rLuPLLhdIjLkMfEl7dxZBnZAKhiJ5KIxJ5rF0Uegs1FWQsqF8aryWIeVDAar/+WfUuO4K6iGl8wI0sT+Qz1foBst4AV+2/PYbunjtFa74cuhzYZvl+yOHBENNR6DyDbU4mk0atQhjiqoWGjMPkm3FhG0+D0v03+YEvch/zpv5TPUzhk6y3FsRyp1NxngIKnBLzWdXiakKmO61qc9Bpv/Zflq+5gohZZ0AsxRIOwHmqiJvnI8ggMuz0ms3+PIQ+4hDotZSkH7sIaHtpbzogwicLlI/E31gWdEv5swvr2DBBb7xcqDamor5Vx6GCjBJd4UqkIX7G53/mTrrX3SYF8RCvU6tJ/8lJdkpNPDzA9RnM1666q9S6dMCDBpYFIGRViSC0NjwcwyaIUCUAnuHvhDHiswELRIAlcCB+J0UqS9ccK1m+VTVVqqpV355gc2lcmY+QNjL5McLtbVM5TThan1pAesf//A9dbds9u0grjVkcPiglEYRMLsogX70AdXQvfChgzsmT2dJZMyhBL/uAsb7QPSl1G64mno73uU893Csuw5Q1QoXBSPDFx2wucsFsmxBeXRBFjGXTZj59O1y2ND+ifAqebR2I5FlzyaC6dANgZ5ErBt9NIIoOKPqbn1FOsZ6aHxnE65df6UgDclElAnnfthmRZ7nSXkx4MDJ0r9p+Zbrqf3qJvaH37r2WsZ7zacfY6EdioXxIUfrLifnJrTRmrTbNQHTtmqK5rLpmHvPFuDH8O9sQcROCdrwCnR/KFPkPs8PjU3GF5Zajz7AWcey3GsfLcPb3qSlm6+j0ZkzDvdWMpGYyulm8tCpAGoKuWDzqX+5HuQjh10/HwcTdMwjCkN7oZmANCbLFowVlDWLqo9alEVodnmunJlkNPk8qI41FRPTRzKmwgs6GCJBY+0LEkhAAONemqseF/8nNB0HzREHTzSKtu+7gzLOvIwEyTzxgemYQwWtI0h0Nq2j+u03CcGIHsAMbRaFa+1C6Ee3FDS2tWENTUAHeTM2SV03rbjNBwyzGCzbP/n8H1idWaXaV1YZp2K+6BVEeVMwID/8zra3fbte4dilvZ8ImVBnHNx++nFX/oz9MjbtkZZZDlN2ZUnfm2/ZRfhGnVQawhmVg50VEwcrDWKSIzI0tffJR1JzLatYafEnYUfsZZx+SP8SjZsva875UBWJEwZG+b5w86E2gh5sjEiIJrKVZeihmTnqHvfVRQH+8Be0fN3fqbdze5KFJDURq0e9khqCw4KogSzxSQAyUf9AV7yosqg5+4hXNonKSzcUg9LMU/kmTzteNaWVgt8UAM8U9CmSBvCyIJUeo+h8XQJVwtM1bqxtig4LXiu0UFgZDiT9z/a6nLiYCSMN34hi0zICyLfHY0+1qfCB1noTzpN5D19TRd476FH9wfuowb4gzHOEZsbujm1C+0g3E5s1+gQBuo3Wwrl6SGVkrFJM0nNroYRZ2LxSA7mcyZuF3QtWNYU6MuQItTet9xR+IVRde/N6hwehnZ/vl1Y3KET9ntul3yf4v1CVcyYcGyy9xtgSANvAm7Fao2YAOAOGtsfAMpuOGVD/Rg0k36jI+c4ozG80kZz7FrRq93sajUtT1YIsbEi9qm226TgZzUX4RfSViRXGMKyNB9/G3MjpU84NSWN7Jp0LbvQio97HH0qJonbHjezj16Xtv74q58jU4AN994BR3VEmFpZmEkAwy4b8F+NV43PnJFo1OXAIHgyN33zyzpZX2Vce8FpY7fZcVFkrzbb8d76wjYNs2b2wiMUpKoIt9Ayxb0zvH9hL7Y3rI6sU5kxa61a72ZLff5XMCwRJ44F7aci/DxaFvuii0hsjhXXXOOkuFBZt1ZQ5BNnAYOB9d9Lk/Hlp74DmYaSgv+tDYWJCv92Y06Hm00/QlKEOvhP+LOlKqPTAmGp5Ml/gH83cZCjZfGFriO7sKjsTpsKWY53NJx4VnGf8nApgTHfnuzLNhJowggd40CZHXritSavt2nnzbK6wropKuj/QJP0lsZZ6YJ80ZA84Pxx+/ZXkyShGI7i0XnyWeh/skEgtnfeM2tsbXnSlwixLA4oudSq4EcuTVpl4teIWtLRSTJ8lY6zGD1XnqqvWtbsBtqCfGn083e3vsAKslSJ7b8d7UuMRHw8Ki7Wu+cDdnMbtTPqmbTInUq3KeVVPCtKeuXCjVhN+Iv+S0VJELzC1UiPAkxwMZChQ5uI2b/DVvDelhcJlJ1P/BE0E7GmZs4Q2c5F0QVNRSA2LCki2uiE+EKdeAbrvvMV+bpMz19c2U2vTBsGt09Eo9mZ3Ga6MfzopPUB1Bs8yswdNiz2CYVqz7JPOTaWoVNg86Q3UHVpIbS6B3t6/V/pfBl8f9fyZ+zvGUFsvb3RRWXpO3pFaSWj/DQDbaniSDDGm0+964K/shTELJ+YLNWEQWtui9ez5mDXtGefzICjOOiZ+3EsYaETm48cYYRyUBkzUgnvv7/DYb1Kmub4/0I172Tigs8KYtFt0Vm1x892lmMtocliv33uHmzZiICq0Fp4E4AA/vTZrnrTUehwpcx68qFw1YoZZjGS0tLJfwUz5sEI/WO3nbGotutXX+PZiAPz286tE0zJZpxV3ND51ysEY0PftJnXQ+P7jSardeZNMy4Pa0s3mRdIfaNNZubhHTGWhSW7snyae0tJ1V0oAAYUFNC/FdBSh+CkGdsP1TbvWOBRu0CYRmGCM+pfpYwngi8p+BuX8Whq5i8o0kdFN8H7wBsLqsEtpM1JAJTGzuStdstB6HDSEOEUE5s9dhi3I9eEja9f+TXx5bIJXk/xuzKFIBm2cBqpx10L7HKvafONEJgkMWLr6L1JwwVwcZjCEleHgArrLRTjXXhuYjt6eT9ikt0lLnJg9/CvMR02KWjXIGId+7KJWXpOO2cqGEz4DAtnBvg1RtbP93WRQJ/dTot1tbwptJW6J72Fw5EsOjvupxu4J01CiqZma5AzWUag9E4IAY4NlXjZBah6vUBvguIZrI3Cg/uiDtHzjtYIJsYARA1NAm+YzqwRQI0KD8e3zQoeMr6Tt99xZGVMFnpSOJ9nKpIjbl6TzIWpXIzX2EDf+CQ/UR1h5aIAhbIothiBD9tFT1qAucl7ktSwgNIT2D3wq7BK0EvVumDjw3/KN11ATVJbAL6serJExBwluRbEAxiQj/6ocqftKKumQBAxOf2p33ESNf97rsNS+T2ViCEn54JuvBMmjVNjft1eeMrpA3SxvX7pAYVow/4DJ4jRmmIaC+WR+Simblu++zy8v1HxwuyWCkLFY1jpAKdH6D3dIXwugSRczeC9vktEHNAigJQ+zIXivP/mIm1TyHRf5VDWWq2BFha3MC7sHGcddrW5Fs+lwdTRlMDce1aMWcolNufX4Q8IJYkHIUEJpMKB8occZL4aOUJkqajQE5nTWr2azf0Vy7Alm2TzZGUzPqvfwb0RT7Pwh8yeYD2aT7DOsmviHIe4BFBv8NAsoHAeiwPjOsvZbb0ibSmvLZqo9+k8JLKnw9KYUHmYV5bAhUrmwE4gac1i0n4pZkFf6yCy82R5avuEaaj/zpJu3YE0cnz8f29yEnZlMxcRRFkXynaMU4M1O2A++kS7fUOeltW4a89WNHIw4en+wXQSDzAdUGai1ztaXqc1BqfPqJgbA21zbndfGrDASKGTErLYsk/Oyp4xMKrngMvzhW/GNk6VlsQKhqz7ZpYSXqYlNnT0ZBWGK2N7haiIqZ5ypeeEiaY3I1eRS2JnD1Yh7+/fRpb/9F3V9ey/qC9I741t9oYkoUKNMGh06Jjh/PiNpVJw3YQ2dXLoofSgwfwSePpsmNtiRF8ayMFTNmpZ5VIA8HeP8mKDi1cVAgd7FzisbXTOobwhFhoFBb9S5sTcNdvbovrHVBbxM4dRkVyM1vV9YtemEjb54RR739psnI6vEZKG73/12S25xHMVuvV5oIox0YXakh/mLqaOIUNGTLU0wT4y5OWwgsf5FaV6soWRw9rTzgb7VLJgwBBp4u3Ls1bqpKI7ozccfZdC7U0oM2IwC/hbnF/rtp5+cC2H8itQNNQ80kSObwm4e3Tdfd+fKywBp1Wx0kimpjXdcJlJEf63mRNLp9HRnH70hWVZuexfmQAAJDh+i+mMPUv3+ux39PxpL7yAKNNCYSacj/cg9zKaxIB292I4AAAXBSURBVMOYwfinH6m17gVfq5jE7AC7bzSee4qazz4lo6m593/y4t/KlPw3X8eMAprceeM1cQODEyfkXD3GexjehgvB4A825llmrNd9fYsbUcsXb4OS1mTCvHBl4x09sW6SaU29W0VeSaNKfxjhT5ZFsnKCbAVMBkdn+CyQDiN+oV0Wcxp6PwTpQfGj+jA3JO8uCCAv3SZ9K7IjB2ZP1r4oYDzzOyFhpL+9eaPLkLC9gL9+ODe6abvb36PhV0dp9P131HluFS1d9RfOMu6WOrc0VIV8WY/6zgH3slF05jMRnQdHHBhKm3pPmFmFd6tug6LzZTHpiZvohDb0d++iJcZWy7dcJ7gPgh2c/F78GnYbQmTO/CZkcgzjwiZjRGQPAoIxq5b5DMO4IR5sLIH+vc7mDbJDUTYYCg2fhV3f2O8OOT8ffnlIWtdQjsDAdZ21DhlGF5P19ZraDiqLGyvqrZ608Eh3mS0Y9SoJ1cp2SDrvnJlFVS6T7BOYbG2HG4YWsNk0OYlfuvpvslOGmC4HAYBvZC0yj/HdcZk5BhM85WgNMhM3roNNHgVNvqxwkH/vBIvIDyCMXTow8zb4+ph01WMfmvpdt1Dt6r/y+21SIA85u2RAlTJG6KNO9okJNFueRw0MdJYeul5hKiYcCQVbJUBNfBrVWQydsZgwgU5uSxF0eoLdXWYTqt1xi8AQ4L7hD985QTLYBoYEXMGUELQVQQUYDhuOYbcjjFeMfzkrO3cMOUgMOKoDuwGoD44elcGYIWdEHY7IKMOCUWk+vFLg0XRpye+dlfmMR+3WWWF05kherTiVKBw10Ia9s7RWJRybmRsxmCXUUq5IADW3G8b4EQmRoXBgaT/zlGw3goYdsDrtdS/SYPcuSb2ksZv9FVgRDD3j90PssYVMBjnrN8ckaAwhMAiR/Ru6p9qvvswP6D5pxW2w1jUff4T6e3eXpcowp6ynp5SrKuYmqOZnUgpTwhgRoi1KIG3zSiqn9oYxegOu6niqTQeg5wJM3F/GZSCun2Yis7owZ/iz5qonpN6yfN2VVLv7NslTO9jmaed2MfEOyqmc+qGwJT18HGzQHdZg4aPQhe7RGufkbc7NO+zvYNISeEJGMp0mHRO2OrtXGXwkqxsAdH1mfvu7MpXL07pwkexZkM9thZkOQVcLRbqcWGFMJpNkI8aw1RMidPe9bbL9Sf3+e2j51huo/sgDskVT7dq/09J1LGB+r91wFTVvv5kaD62kFmc+vbffFFeAHm5p/vQ+0+1dOCm3ajbl3EqszWjB5JXG9Mo2qBEbKxM2et+YXNFJcxqVsMRmwUUWB5q4CW2ikSZuqB32IzCm3HUDA4uYHMKmZRiXlYDDfhCsCQLSiIEzdtXIfKdsCCzC6wkFNUl3/TBqW2Zr5ianyKip+LkJ+bTk6ixQaaCOwnmyDZxJ9wzUBKv+uy39SDrKVW4XkAJwk7LGav/nPGwWIbVpKyjfUmVzWhWV3aY3YV+ZsIO63lTbXGb3oXRX35naH3VW2bNhrq1kwbYnLgpPQy7sKWv/CuyrpC840LrIE6n/+F2RMLWzRa9ZyqOFa4Rpn7CpdR42u85NmZsCrmR55OdMliV7oibNjuH8anMIva7qemeR37Pq3cbPMy0Hr3mLfWBYhBqkK6rkYeiJC+y1/1y+qospynOpXR+LyqL19GNsG1NzaHpdYQJKa0DZalvuKhnOoddh9b8vc49lsdwu/Fv15SzXchSu7MRTPVGksPIFT13djEna//N4s2lPcTF3zNzNq3OE70JXgF7borWEdeq1Vteg15EKziT3Hd7BbC8xlqyx/22xf240GvLvyWQS9g80c4teJKxFQlp0s5cT9p+ds/oQ/7+vP7ven61pkXJUf4fPI87ZhwyPxthU1/879/8LxYr/tAD9VPM8jW7/l4W5xp7Ln/Nyi/6z9fzZ9ata/J+EM79efZ+qJmNN3KTRUXrub/8LCUM/rNeOGSoAAAAASUVORK5CYII=');
}

.pineapple-b {
  width: 100%;
  height: 50rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  line-clamp: 1;
  -webkit-box-orient: vertical;
  border-top: 1rpx solid #c8c7cc;
  text-align: center;
  background-color: #f66e5d;
  color: #fff;
  border-bottom-left-radius: 12rpx;
  border-bottom-right-radius: 12rpx;
}

.pineapple-b .name {
  font-size: 18rpx;
  padding: 0 10rpx;
}

.pineapple-del {
  position: absolute;
  top: -15rpx;
  right: -15rpx;
  z-index: 1;
  width: 30rpx;
  height: 30rpx;
  border-radius: 50%;
  background-color: #fff;
  display: flex;
  justify-content: center;
  border: 1rpx solid #c8c7cc;
  font-size: 20rpx;
  line-height: 25rpx;
}
</style>

index页面

子组件z-drag-add拖拽添加
通过这个index父组件通信传输,处理更新数据,并传给另一个子组件z-drag-pineapples

子组件z-drag-pineapples编辑、删除操作
通过这个index父组件通信传输,处理更新数据,并传给另一个子组件z-drag-pineapples

index父组件处理数据后,通过Pinia状态管理,更新数据

<template>
  <view class="content">
    <z-drag-files :files="files" />
    <z-drag-add :pineapples="pineapples" :files="files" :scrollTopHeight="scrollTopHeight" @dragStart="dragStart"
      @dragMove="dragMove" @dragEnd="dragEnd" />
    <z-drag-pineapples :pineapples="pineapplesPosition" @dragStart="dragStart" @dragMove="dragMove" @dragEnd="dragEnd"
      @dragDelete="dragDelete" />
    <z-drag-dom />
  </view>
</template>

<script setup lang="ts">
import { ref, computed, getCurrentInstance, watch, onMounted } from "vue";
import { onPageScroll, onLoad } from "@dcloudio/uni-app";
import type { PageScrollOption } from '@dcloudio/uni-app';
import { filesMock, pineapplesMock, } from '@/mock/index'
import type { File, Pineapple } from '@/types/mock'
import { spaceHeight, marginWidth, marginTop } from '@/config/index'
import { checkPineapplePage, checkPineappleBoundary } from '@/utils/tool'
import { buildShortUUID } from "@/utils/uuid";
import { useAppStore } from "@/store/modules/app";

const appStore = useAppStore();
const files = ref<File[]>([])
const pineapples = ref(pineapplesMock.pineapples)
const pineapplesPosition = ref([])
const windowInfo = ref()
const dragHeight = ref(0)
const scrollTopHeight = ref(0)

const { proxy } = getCurrentInstance() as any;

// 创建通用的拖动payload生成函数
const createDragPayload = (
  item: Pineapple,
  position: { x: number; y: number; page: number },
  rect: { width: number; height: number },
  isDown: boolean,
  uuid?: string
) => ({
  ...item,
  ...position,
  ...rect,
  uuid: uuid || item.uuid || buildShortUUID("pineapple"),
  isDown
});

// 开始拖动
const dragStart = (type: string, item: Pineapple, rect: DOMRect, left: number, top: number) => {
  // 校验拖动位置是否超出页面范围(两页之间)
  const position = checkPineapplePage(
    { x: left, y: Math.max(top, marginTop), width: rect.width, height: rect.height },
    files.value
  );
  const payload = createDragPayload(item, position, rect, true);
  if (type === 'add') {
    pineapplesPosition.value.push({ ...payload, show: false });
  } else {
    pineapplesPosition.value = pineapplesPosition.value.map(v =>
      v.uuid === payload.uuid ? { ...v, ...position, show: false } : v
    );
  }
  // 实时更新拖动层坐标
  appStore.setPineapple(payload);
};

// 拖动中
const dragMove = (type: string, item: Pineapple, newX: number, newY: number, width: number, height: number) => {
  const effectiveWidth = width || item.width || 0;
  const effectiveHeight = height || item.height || 0;
  // 校验拖动位置是否超出页面范围(4个边)
  const position = checkPineappleBoundary(
    newX, newY,
    effectiveWidth, effectiveHeight,
    windowInfo.value.windowWidth,
    dragHeight.value,
    files.value
  );
  const payload = createDragPayload(
    item,
    position,
    { width: effectiveWidth, height: effectiveHeight },
    true,
    item.uuid || appStore.pineapple.uuid
  );
  // 实时更新拖动层坐标
  appStore.setPineapple(payload);
};

// 拖动结束
const dragEnd = (type: string, item: Pineapple, rect: DOMRect) => {
  const pineapple = appStore.pineapple;
  const effectiveWidth = rect?.width || item.width || 0;
  const effectiveHeight = rect?.height || item.height || 0;
  // 校验拖动位置是否超出页面范围(两页之间)
  const position = checkPineapplePage(
    { x: pineapple.x, y: pineapple.y, width: effectiveWidth, height: effectiveHeight },
    files.value
  );
  const payload = createDragPayload(
    item,
    position,
    { width: effectiveWidth, height: effectiveHeight },
    false,
    item.uuid || appStore.pineapple.uuid
  );

  pineapplesPosition.value = pineapplesPosition.value.map(v =>
    v.uuid === payload.uuid ? { ...v, ...position, show: true } : v
  );
  pineapplesPosition.value.forEach(v => v.show = true);
  // 实时更新拖动层坐标
  appStore.setPineapple(payload);
};
// 删除
const dragDelete = (index: number) => {
  pineapplesPosition.value.splice(index, 1);
  appStore.setPineapple({ x: 0, y: 0, page: 0, uuid: '', isDown: false, })
};

watch(
  () => files.value,
  (newVal) => {
    // 当文件列表变化时,重新计算凤梨的位置
    if (newVal && newVal.length > 0) {
      let currentTop = 0;
      for (let i = 0; i < newVal.length; i++) {
        const rect = newVal[i];
        currentTop = currentTop + rect.height + spaceHeight;
      }
      dragHeight.value = currentTop;
    }
  },
  { immediate: true, deep: true }
);
// 页面滚动
onPageScroll((e: PageScrollOption) => {
  scrollTopHeight.value = e.scrollTop;
});
// 页面加载
onMounted(() => {
  windowInfo.value = uni.getWindowInfo();
  let { windowWidth } = windowInfo.value
  files.value = filesMock.files.map((v) => {
    let width = windowWidth
    let scale = width / v.width
    let height = v.height * scale
    return {
      ...v,
      width: width - marginWidth,
      height: height,
    }
  })
})
// 页面加载
onLoad(() => {
  wx.showShareMenu({
    menus: ['shareAppMessage', 'shareTimeline'],
  });
}) 
</script>

<style>
.content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
</style>

z-drag-dom拖拽dom组件

通过pinia,获取数据,computed计算属性实时更新拖动层坐标,并跟随手指移动

<template>
  <view class="drag-dom pineapple" :style="{
    left: pineapple.x + 'px',
    top: pineapple.y + 'px',
    opacity: pineapple.isDown ? 1 : 0
  }">
    <view class="pineapple-t">
      {{ pineapple.typeName }}
    </view>
    <view class="pineapple-b">
      <text class="name">{{ pineapple.name }}</text>
    </view>
  </view>
</template>
<script setup lang="ts">
import { ref, computed, watch, } from "vue";
import { useAppStore } from "@/store/modules/app";
const appStore = useAppStore();
const pineapple = computed(() => appStore.pineapple)
</script>
<style>
.drag-dom {
  position: absolute;
  pointer-events: none;
  z-index: 999;
}

.pineapple {
  display: flex;
  flex-direction: column;
  width: 160rpx;
  height: 200rpx;
  border-radius: 12rpx;
  background-color: #faf6f5;
  box-shadow: 0 15rpx 23rpx rgba(0, 0, 0, 0.3);
  overflow: hidden;
  color: #f66e5d;
  margin: 0 20rpx;
}

.pineapple-t {
  width: 100%;
  font-size: 24rpx;
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  background-size: cover;
  background-image: url('data:image/png;base64,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');
}

.pineapple-b {
  width: 100%;
  height: 50rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  line-clamp: 1;
  -webkit-box-orient: vertical;
  border-top: 1rpx solid #c8c7cc;
  text-align: center;
  background-color: #f66e5d;
  color: #fff;
}

.pineapple-b .name {
  font-size: 18rpx;
  padding: 0 10rpx;
}
</style>

store/modules/app.ts 状态库

import { defineStore } from 'pinia';
import { store } from '@/store';

export interface Pineapple {
  x: number;
  y: number;
  page: number; // 页码
  uuid: string; // 唯一标识符
  isDown: boolean; // 是否按下
  typeName?: string; // 类型名称
  name?: string;
}
export const useAppStore = defineStore('app', {
  state: () => ({
    pineapple: {
      x: 0,
      y: 0,
      page: 1, // 默认页码为1
      uuid: '', // 默认唯一标识符为空
      isDown: false, // 默认未按下
      typeName: '', // 默认类型名称为空
      name: '', // 默认名称为空
    } as Pineapple
  }),
  getters: {
    getPineapple(state) {
      return state.pineapple
    },
  },
  actions: {
    setPineapple(pineapple: Pineapple) {
      this.pineapple = pineapple;
    },
  }
});

// Need to be used outside the setup
export function useAppStoreWithOut() {
  return useAppStore(store);
}

tool.ts工具函数

checkPineappleBoundary 检查元素是否超出边界

checkPineapplePage 校验凤梨页码是否在文件上下页中间

import { marginTop, marginWidth, spaceHeight } from '@/config/index';
import type { File } from '@/mock/index'
// 边界计算器类,用于处理元素拖拽时的边界计算
class BoundaryCalculator {
  private readonly maxX: number;// X轴最大允许拖拽距离
  private readonly maxY: number;// Y轴最大允许拖拽距离

  constructor(
    private readonly options: {
      dragWidth: number// 拖拽区域宽度
      dragHeight: number// 拖拽区域高度
      marginWidth: number// 元素左侧间距
      marginTop: number// 元素顶部间距
      spaceHeight: number// 元素之间间距
    }
  ) {
    // 计算X轴最大允许拖拽距离
    this.maxX = options.dragWidth - options.marginWidth
    // 计算Y轴最大允许拖拽距离
    this.maxY = options.dragHeight + options.marginTop - options.spaceHeight
  }

  // 数值范围限制工具
  private clamp(val: number, min: number, max: number) {
    return Math.max(min, Math.min(val, max))
  }

  // 核心计算方法
  calculate(
    moveX: number, // 元素X轴移动距离
    moveY: number, // 元素Y轴移动距离
    width: number, // 元素宽度
    height: number, // 元素高度
    files: File[] // 所有文件
  ) {
    // 限制X轴移动距离在合理范围内
    const clampedX = this.clamp(moveX, 0, this.maxX - width)
    // 限制Y轴移动距离在合理范围内
    const clampedY = this.clamp(moveY, this.options.marginTop, this.maxY - height)

    return {
      x: clampedX,
      y: clampedY,
      page: this.getPage(clampedY, files) // 当前所在页码
    }
  }

  // 分页计算逻辑(根据垂直位置确定页码)
  private getPage(y: number, files: File[]) {
    let currentTop = 0  // 当前累计高度
    for (const [index, file] of files.entries()) {
      currentTop += file.height + this.options.spaceHeight
      if (y < currentTop) return index + 1 // 返回第一个超过当前高度的页码
    }
    return files.length  // 默认返回最后一页
  }
}

// 边界检查入口函数
export const checkPineappleBoundary = (
  moveX: number,
  moveY: number,
  width: number,
  height: number,
  dragWidth: number, // 拖拽区域实际宽度
  dragHeight: number, // 拖拽区域实际高度
  files: File[]
) => {
  // 创建边界计算器实例
  const calculator = new BoundaryCalculator({
    dragWidth,
    dragHeight,
    marginWidth,
    marginTop,
    spaceHeight
  })
  return calculator.calculate(moveX, moveY, width, height, files)
}
// 页面边界定义
interface PageBoundary {
  top: number; // 页面上边界
  bottom: number; // 页面下边界
}

// 生成页面边界
const generatePageBoundaries = (files: File[]) => {
  return files.reduce<PageBoundary[]>((acc, file, index) => {
    const prev = acc[acc.length - 1]?.bottom || marginTop // 获取前一页底部位置
    const spacing = index < files.length - 1 ? spaceHeight : 0 // 最后一页不加间距
    return [...acc,
    {
      top: prev,
      bottom: prev + file.height + spacing  // 当前页底部 = 前一页底部 + 元素高度 + 间距
    }]
  }, [])
}
// 二分查找当前页(根据元素Y轴位置)
const findCurrentPage = (pineappleY: number) => (pages: PageBoundary[]) => {
  let low = 0, high = pages.length - 1
  while (low <= high) {
    const mid = (low + high) >> 1 // 位运算取中间索引
    pineappleY >= pages[mid].top - 1e-6 ? (low = mid + 1) : (high = mid - 1) // 比较元素Y轴位置与中间页边界
  }
  return Math.min(high + 1, pages.length) // 返回合法页码
}
// 页码有效性验证
const validatePageRange = (currentPage: number) => (pages: PageBoundary[]) => {
  if (currentPage <= 0) return 1 // 不能小于第一页
  if (currentPage > pages.length) return pages.length // 不能超过最后一页
  return currentPage
}
// 吸附位置计算(处理跨页吸附逻辑)
const calculateSnapPosition = (pineapple: Pineapple) =>
  (ctx: { pages: PageBoundary[]; currentPage: number }) => {
    const { pages, currentPage } = ctx
    const currentPageInfo = pages[currentPage - 1]
    const nextPageInfo = pages[currentPage]
    const pineappleBottom = pineapple.y + pineapple.height
    // 计算与下一页的重叠阈值
    const overlapThreshold = currentPageInfo.bottom - spaceHeight
    const overlap = pineappleBottom - overlapThreshold

    if (overlap > 0 && nextPageInfo) {
      // 当重叠超过元素高度的50%时吸附到下一页
      const shouldSnapNext = overlap > pineapple.height * 0.5
      return {
        x: pineapple.x,
        y: Math.max(
          shouldSnapNext ? nextPageInfo.top : overlapThreshold - pineapple.height,
          marginTop
        ),
        page: currentPage + (shouldSnapNext ? 1 : 0)
      }
    }
    return { ...pineapple, page: currentPage }
  }


// 校验凤梨页码是否在文件上下页中间
export const checkPineapplePage = (pineapple: Pineapple, files: File[]) => {
  // 1. 生成页面边界
  const pages = generatePageBoundaries(files);

  // 2. 查找当前页
  let currentPage = findCurrentPage(pineapple.y)(pages);

  // 3. 验证页码有效性
  currentPage = validatePageRange(currentPage)(pages);

  // 4. 计算吸附位置
  const ctx = { pages, currentPage };
  const result = calculateSnapPosition(pineapple)(ctx);

  // 5. 最终位置修正
  return {
    ...result,
    y: Math.max(result.y, marginTop).toFixed(2),
  };
}

uuid.ts 生成唯一标识符

const hexList: string[] = [];
for (let i = 0; i <= 15; i++) {
  hexList[i] = i.toString(16);
}

export function buildUUID(): string {
  let uuid = '';
  for (let i = 1; i <= 36; i++) {
    if (i === 9 || i === 14 || i === 19 || i === 24) {
      uuid += '-';
    } else if (i === 15) {
      uuid += 4;
    } else if (i === 20) {
      uuid += hexList[(Math.random() * 4) | 8];
    } else {
      uuid += hexList[(Math.random() * 16) | 0];
    }
  }
  return uuid.replace(/-/g, '');
}

let unique = 0;
export function buildShortUUID(prefix = ''): string {
  const time = Date.now();
  const random = Math.floor(Math.random() * 1000000000);
  unique++;
  return prefix + '_' + random + unique + String(time);
}

这次版本的升级

  1. 突破弹层元素拖拽交互瓶颈:实现了弹层内元素向文件图片的直接拖拽,解决了此前操作链路不畅的问题。
  2. 简化印模拖拽流程:升级物料区印模交互逻辑,支持印模直接跟随手指移动完成添加,省去了 “点击添加后再拖拽调整” 的两步操作,提升操作效率。
  3. 优化多页文件展示体验:针对多页文件滚动场景,封装并升级印章拖拽至上下页时的吸顶算法,确保印章定位精准、交互流畅。
  4. 提升代码可维护性:对组件进行精细化拆分,明确业务组件与通用组件的职责边界,增强代码可读性与后续迭代的便捷性。

还需要优化的点

  1. 通用印模css
  2. 通用拖拽事件
  3. 业务数据处理的逻辑
    gitee地址https://gitee.com/sleepkele/uniapp-wechat-drag
Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐