This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -0,0 +1,665 @@
<script lang="ts">
import { type PropType, ref, computed } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHILDREN_INFO, CHILDREN_SIZE } from "../x-drag/interface.uts"
import { vibrator } from "@/uni_modules/x-vibrate-s"
type POSITION = {
col : number,
row : number,
index : number
}
type POSITION_XY = {
x : number,
y : number
}
type XDRAG_DOMRECT = {
width : number,
height : number,
left : number,
top : number,
right : number,
bottom : number,
}
/**
* @name 拖拽排序 xDrag
* @description 自由布局拖拽排序组件,列或者宫格都支持。使用时,需要将需要拖拽排序的子元素设置为x-drag-item。
* 并且子元素和父元素不允许通过style来动态设置宽高,否则拖拽排序会失效。并且list及子元素中的order不允许动态设置,否则拖拽排序会失效。
* 想要动态修改数据可以通过vif切换重新渲染下。web支持响应式屏幕。本插件:引用了原生插件x-vibrate-s
* @page /pages/index/drag
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
domlist: [] as CHILDREN_INFO[],
backckList: [] as string[],
oldList: [] as UTSJSONObject[],
activeIndex: -1,
targetIndex: -1,
isApplongStartMove:false,
cellHeight: 0,
cellWidth: 0,
isMoveing: false,
_x: 0,
_y: 0,
tid: 0,
oragie_x: 0,
oragie_y: 0,
scrollDiffTopJuli: 0,
tid2: 12,
oldStartXy: { col: 0, row: 0, index: 0 } as POSITION,
xdragRect: {
width: 0,
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
} as XDRAG_DOMRECT,
// 拖动检测相关
hasMoved: false,
moveThreshold: 1, // 移动阈值,超过这个距离才认为是拖动
startTouchX: 0,
startTouchY: 0
}
},
emits: [
/**
* 排序变动时触发
* @param {UTSJSONObject[]} list - 当前变动后的数据列表
*/
"change",
/**
* 拖动的时候触发,可以根据参数进行对你超出页面屏幕高时进行一个滚动.
* @param {number} diff - 滚动的距离向上是-diff,向下是正
*/
"move",
"end",
"start",
],
props: {
/**
* 项目的高度,不要动态更改。
*/
itemHeight: {
type: String,
default: "50"
},
/**
* 列数,默认1即列表布局,不要动态更改。
* 如果是1以上就是宫格布局了。
*/
col: {
type: Number,
default: 1
},
/**
* 你的排序数据list,变动后,请通过change事件来取得
* 主要是用来骗编译器用的。
*/
list: {
type: Array as PropType<UTSJSONObject[]>,
default: () : UTSJSONObject[] => [] as UTSJSONObject[],
required: true
},
/**
* 当组件放置在scroll页面中,如果拖动时项目需要滚动时自动滚动的进步值
* 比如组件在屏幕被底部或者顶部遮挡一部分,当拖动靠近底部时,会向下滚动的值.
* 这个值是你外部自己通过设置滚动页面的scrollTop来达成的,具体见demo,已经为你写了示例.
*/
scrollDiff: {
type: Number,
default: 25
}
},
computed: {
_rows() : number {
let row = Math.ceil(this.domlist.length / this.col)
return row
},
_cols() : number {
return this.col
},
_totalHeight() : number {
return this._rows * this.cellHeight
}
},
provide() {
return {
XDRAGE_HEIGHT: checkIsCssUnit(this.itemHeight, xConfig.unit),
XDRAGE_COL: this.col,
XDRAGE_MAX_LEN: this.list.length,
}
},
mounted() {
this.oldList = this.list.slice(0)
this.getXdrageDomRect();
},
updated() {
this.getXdrageDomRect()
},
methods: {
updataResize(resize : CHILDREN_SIZE) {
this.cellHeight = resize.height
this.cellWidth = resize.width
},
addItem(item : CHILDREN_INFO) {
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == item.id)
if (index > -1) {
this.domlist.splice(index, 1, item)
} else {
this.domlist.push(item)
}
this.cellHeight = item.node.height!;
this.cellWidth = item.node.width!;
},
delItem(id : string) {
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == id)
if (index > -1) {
this.domlist.splice(index, 1)
}
},
indexTransformer(originalIndex : number) : number {
let detail = this.targetIndex - this.activeIndex;
if (detail > 0) {
return originalIndex - 1
}
return originalIndex + 1
},
getLastMaxCol() : number {
// 计算总行数,虽然这里主要关注最后一行,但计算总行数是理解过程的一部分
const totalRows = Math.ceil(this.domlist.length / this.col);
// 计算最后一行的列数,通过求余数得到
const lastRowColumns = this.domlist.length % this.col;
// 如果最后一行是满列的(即余数为0),直接返回列数;否则返回实际的列数(余数)
return lastRowColumns === 0 ? this.col : lastRowColumns;
},
coverXy(x : number, y : number) : POSITION {
let maxRow = Math.floor((this.domlist.length - 1) / this.col);
let lastMacCol = this.getLastMaxCol();
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
// #ifdef APP||WEB
bounce = this.getByAppDomRect(el)
// #endif
let row = Math.floor((y - bounce.top!) / this.cellHeight)
let col = Math.floor((x - bounce.left!) / this.cellWidth)
row = Math.min(Math.max(0, row), maxRow)
col = Math.min(Math.max(0, col), this.col - 1)
let index = (row + 1) * this.col - (this.col - col);
col = maxRow == row ? Math.min(lastMacCol - 1, col) : col;
index = Math.min(Math.max(0, index), this.domlist.length - 1)
return {
row,
col,
index
} as POSITION
},
eventTransformer_start(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0) return;
this.backckList = this.domlist.map((el) : string => el.id)
let x = evt.x;
let y = evt.y;
const startPos = this.coverXy(x, y);
this.activeIndex = startPos.index
this.targetIndex = this.activeIndex;
this.oldStartXy = startPos
let childrenIndex = this.domlist.findIndex((el) : boolean => el.id == this.backckList[this.activeIndex])
if (childrenIndex == -1) return;
let children = this.domlist[childrenIndex]!;
let childrenEle = children!.ele
let childrenTop = parseFloat(childrenEle!.getStylSetProperty('top') as string);
let childrenLeft = parseFloat(childrenEle!.getStylSetProperty('left') as string);
childrenEle!.setStylSetProperty('transition-duration', '0s')
childrenEle!.setStylSetProperty('z-index', '5')
this._x = x - childrenLeft;
this._y = y - childrenTop;
for (let i = 0; i < t.backckList.length; i++) {
let temdom = t.backckList[i]
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
let el = t.domlist[elIndex];
el.ele!.setActivdId(children.id)
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
},
eventTransformer_move(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0 || t.backckList.length == 0) return;
let x = evt.x;
let y = evt.y;
let el = this.$refs["xDrag"] as UniElement;
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
if (childrenIndex == -1) return
let children = t.domlist[childrenIndex]!;
let childrenEle = children!.ele
let offsetY = y - t._y
let offsetX = x - t._x
childrenEle!.setStylSetProperty('top', offsetY.toString() + 'px')
childrenEle!.setStylSetProperty('left', offsetX.toString() + 'px')
childrenEle!.setStylSetProperty('transition-duration', '0s')
childrenEle!.setStylSetProperty('z-index', '5')
let target = t.coverXy(x, y)
let targetIndex = target.index
let targetChildren = t.domlist[targetIndex]!;
// 如果目标位置是禁用项目,则不进行任何位置交换操作,直接返回
if (targetChildren.disabled) {
return;
}
// 如果目标位置与当前位置相同,也不需要进行交换
if (targetIndex == t.activeIndex) {
return;
}
this.oldStartXy = target
let backChildrent = t.backckList.slice(0)[t.activeIndex];
let backTargChildren = t.backckList.slice(0)[targetIndex];
t.targetIndex = targetIndex;
let backChildrent_model = t.oldList.slice(0)[t.activeIndex];
let backTargChildren_model = t.oldList.slice(0)[t.targetIndex];
t.backckList.splice(t.activeIndex, 1, backTargChildren)
t.backckList.splice(t.targetIndex, 1, backChildrent)
t.oldList.splice(t.activeIndex, 1, backTargChildren_model)
t.oldList.splice(t.targetIndex, 1, backChildrent_model)
for (let i = 0; i < t.backckList.length; i++) {
let temdom = t.backckList[i]
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
let el = t.domlist[elIndex];
el.oldindex = el.index;
el.index = i;
el.ele!.setOrderIndex(i)
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
t.activeIndex = t.targetIndex
},
eventTransformer_end(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0 || t.backckList.length == 0) return;
let x = evt.x;
let y = evt.y;
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
if (childrenIndex == -1) return
let children = t.domlist[childrenIndex]!;
let childrenEle = children!.ele
let result = t.coverXy(x, y);
let targetChildren = t.domlist[result.index]!;
if (targetChildren.disabled) {
result = this.oldStartXy
}
let col = result.col;
let row = result.row;
childrenEle!.setStylSetProperty('top', (this.cellHeight * row) + 'px')
childrenEle!.setStylSetProperty('left', (this.cellWidth * col) + 'px')
t.domlist.sort((ela, elb) : number => ela.index - elb.index)
for (let i = 0; i < t.domlist.length; i++) {
let temdom = t.domlist[i]
temdom.oldindex = temdom.index
temdom.index = i
temdom.ele!.setOrderIndex(i)
temdom.ele!.setActivdId("")
temdom.ele!.updateForce()
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
// this.activeIndex = -1;
// this.targetIndex = -1;
this.$emit('change', JSON.parseArray<UTSJSONObject>(JSON.stringify(this.oldList)!)!)
},
mLongStart(evt : UniTouchEvent) {
this.isApplongStartMove = true;
this.mStart(evt)
},
mStart(evt : UniTouchEvent) {
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
// 记录初始触摸位置和重置拖动状态
this.startTouchX = x;
this.startTouchY = y;
this.hasMoved = false;
this.oragie_x = x
this.oragie_y = y
let tempActiveIndex = this.coverXy(x, y).index
if (tempActiveIndex == -1) return;
let children = this.domlist[tempActiveIndex]!;
if (children.disabled) return;
this.isMoveing = true;
this.getXdrageDomRect()
this.scrollDiffTopJuli = 0
clearTimeout(this.tid2)
vibrator(100)
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
this.$emit('start')
// 暂时不阻止事件传播,等检测到真正拖动时再阻止
},
getXdrageDomRect() {
// #ifdef MP-WEIXIN
let t = this;
let el = this.$refs["xDrag"] as UniElement;
el.getBoundingClientRectAsync()
.then(res => {
t.xdragRect = {
width: res.width,
height: res.height,
left: res.left,
right: res.right,
top: res.top,
bottom: res.bottom,
} as XDRAG_DOMRECT
})
// #endif
},
getByAppDomRect(ele : UniElement) : XDRAG_DOMRECT {
let rect = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
} as XDRAG_DOMRECT
// #ifdef APP || WEB
let res = ele.getBoundingClientRect();
rect = {
width: res.width,
height: res.height,
left: res.left,
right: res.right,
top: res.top,
bottom: res.bottom,
} as XDRAG_DOMRECT
// #endif
return rect
},
mMove(evt : UniTouchEvent) {
if (!this.isMoveing || this.activeIndex == -1) return;
let x = evt.changedTouches[0].clientX + this.scrollDiffTopJuli;
let y = evt.changedTouches[0].clientY;
// 检测是否真正发生了拖动
if (!this.hasMoved) {
let deltaX = Math.abs(x - this.startTouchX);
let deltaY = Math.abs(y - this.startTouchY);
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > this.moveThreshold) {
this.hasMoved = true;
// 一旦检测到真正拖动,就开始阻止事件传播
evt.preventDefault();
evt.stopPropagation();
} else {
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
return;
}
} else if(this.isApplongStartMove||this.hasMoved) {
// 已经确认是拖动状态,继续阻止事件传播
evt.preventDefault();
evt.stopPropagation();
}
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
// #ifdef APP||WEB
bounce = this.getByAppDomRect(el)
// #endif
let maxheight = uni.getWindowInfo().windowHeight
let diffBottom = Math.abs(bounce.bottom - maxheight)
let _this = this;
// #ifdef APP||WEB
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(() => {
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
// #endif
// #ifdef MP-WEIXIN
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(async () => {
bounce = await el.getBoundingClientRectAsync();
diffBottom = Math.abs(bounce.bottom - maxheight)
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
// #endif
},
mEnd(evt : UniTouchEvent) {
this.isApplongStartMove = false;
this.scrollDiffTopJuli = 0
clearTimeout(this.tid2)
this.$emit('end')
if (!this.isMoveing) return;
this.isMoveing = false;
// 重置拖动状态
this.hasMoved = false;
this.startTouchX = 0;
this.startTouchY = 0;
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
this.eventTransformer_end({ x: x, y: y } as POSITION_XY)
},
// #ifdef WEB
mmStart(evt : UniMouseEvent) {
let x = evt.clientX;
let y = evt.clientY;
// 记录初始鼠标位置和重置拖动状态
this.startTouchX = x;
this.startTouchY = y;
this.hasMoved = false;
let tempActiveIndex = this.coverXy(x, y).index
if (tempActiveIndex == -1) return;
let children = this.domlist[tempActiveIndex]!;
if (children.disabled) return;
this.isMoveing = true;
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
this.$emit('start')
},
mmMove(evt : UniMouseEvent) {
if (!this.isMoveing || this.activeIndex == -1) return;
let x = evt.clientX;
let y = evt.clientY;
// 检测是否真正发生了拖动
if (!this.hasMoved) {
let deltaX = Math.abs(x - this.startTouchX);
let deltaY = Math.abs(y - this.startTouchY);
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > this.moveThreshold) {
this.hasMoved = true;
// 一旦检测到真正拖动,就开始阻止事件传播
evt.preventDefault();
evt.stopPropagation();
} else {
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
return;
}
} else {
// 已经确认是拖动状态,继续阻止事件传播
evt.preventDefault();
evt.stopPropagation();
console.log(777)
}
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
bounce = this.getByAppDomRect(el)
let maxheight = uni.getWindowInfo().windowHeight
let diffBottom = Math.abs(bounce.bottom - maxheight)
let _this = this;
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(() => {
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
},
mmEnd(evt : UniMouseEvent) {
this.$emit('end')
if (!this.isMoveing) return;
this.isMoveing = false;
// 重置拖动状态
this.hasMoved = false;
this.startTouchX = 0;
this.startTouchY = 0;
this.eventTransformer_end({ x: evt.clientX, y: evt.clientY } as POSITION_XY)
}
// #endif
}
}
</script>
<template>
<!-- @longpress="mStart" @longpress="mLongStart @touchstart="mStart"" -->
<view
class="xDrag"
ref="xDrag"
<!-- #ifdef APP || MP -->
@longpress="mLongStart"
<!-- #endif -->
<!-- #ifdef H5 -->
@touchstart="mStart"
<!-- #endif -->
@touchmove="mMove"
@touchend="mEnd"
@touchcancel="mEnd"
<!-- #ifdef WEB -->
@mousedown.parent="mmStart"
@mousemove.stop="mmMove"
@mouseup.parent="mmEnd"
@mouseleave="mmEnd"
<!-- #endif -->
:style="{height:_totalHeight+'px'}">
<!--
@slot 只可放置子组件x-drag-item
-->
<slot></slot>
</view>
</template>
<style scoped>
.xDrag {
position: relative;
}
</style>