123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- <template>
- <view class="u-draggable" ref="uDraggable">
- <movable-area class="u-draggable__area" :style="[areaStyles]">
- <movable-view
- v-for="(item, index) in cloneList"
- direction="all"
- class="u-draggable__view"
- :key="index"
- :style="[{
- width: $u.addUnit(width),
- height: $u.addUnit(height)
- }]"
- :class="[{
- 'u-draggable__active': index == activeIndex,
- 'u-draggable__disabled': disabled || item.disabled,
- }]"
- :x="item.x"
- :y="item.y"
- :friction="friction"
- :damping="damping"
- :disabled="item.disabled || (longpress && !longPressStarted) || !isDrag"
- @change="handleTouchMove($event, index)"
- @longpress="handleLongPress($event, index)"
- @touchstart="handleTouchStart($event, index)"
- @touchend="handleTouchEnd"
- @touchcancel="handleTouchEnd"
- >
- <slot
- name="item"
- :index="index"
- :startIndex="item.startIndex"
- :item="item.item"
- :active="!item.disabled && index == activeIndex"
- :disabled="item.disabled"
- />
-
- <view class="u-draggable__close" v-if="closeable && !item.disabled" @click.stop="closeItem(index)">
- <slot name="close">
- <view class="u-draggable__close-inner">
- <u-icon name="close" size="12" color="#fff"></u-icon>
- </view>
- </slot>
- </view>
- </movable-view>
- </movable-area>
- </view>
- </template>
- <script>
- // #ifdef APP-NVUE
- const dom = weex.requireModule('dom');
- // #endif
- import props from './props.js';
- import mixin from '../../libs/mixin/mixin';
- import mpMixin from '../../libs/mixin/mpMixin';
- /**
- * u-draggable 拖拽排序
- * @description 拖拽排序组件,支持多列拖拽排序,可关闭,手柄拖拽,长按拖拽。
- * @tutorial https://uview.d3u.cn/components/draggable.html
- * @property {Array} list 数据列表 (默认 [] )
- * @property {Number} column 列数 (默认 2 )
- * @property {Number} aspectRatio 宽高比,填写这项时gridHeight失效 (默认 null )
- * @property {Number} itemHeight 项目高度 (默认 60 )
- * @property {Number} damping 阻尼系数 (默认 50 )
- * @property {Number} friction 摩擦系数 (默认 2 )
- * @property {Boolean} handle 是否使用手柄拖拽 (默认 false )
- * @property {Boolean} disabled 是否禁用 (默认 false )
- * @property {Boolean} longpress 是否长按拖拽 (默认 false )
- * @property {Boolean} closeable 是否显示关闭按钮 (默认 false )
- *
- * @event {Function} change 拖拽排序组件被点击时触发
- * @event {Function} close 拖拽排序组件被关闭时触发
- * @example <u-draggable :list="list" @change="change"></u-draggable>
- */
- export default {
- name: 'u-draggable',
- mixins: [mpMixin, mixin, props],
- data() {
- return {
- isDrag: false,
- isInit: false,
- maxIndex: -1,
- activeIndex: -1,
- isDisabled: false,
- positions: [],
- cloneList: [],
- containerWidth: 0,
- positionMap: [],
- longPressStarted: false,
- dragState: {
- item: null, // 当前拖拽的元素
- index: 0, // 当前索引位置
- oldIndex: -1, // 开始拖拽时的索引位置
- sortableIndex: -1 // 在可排序数组中的索引
- }
- };
- },
- computed: {
- rows() {
- return Math.ceil((this.isInit ? this.cloneList.length : this.list.length) / this.column);
- },
- height() {
- if (this.aspectRatio) {
- return this.width / this.aspectRatio;
- }
- return this.$u.getPx(this.itemHeight);
- },
- width() {
- return this.containerWidth / this.column;
- },
- areaStyles() {
- return {
- // #ifdef APP-NVUE
- width: this.$u.addUnit(this.containerWidth),
- // #endif
- height: this.$u.addUnit((this.rows) * this.height)
- };
- }
- },
- mounted() {
- this.init();
- },
- watch: {
- list: {
- handler(newVal) {
- this.init();
- },
- deep: true
- }
- },
- // #ifdef VUE3
- emits: ['change', 'close'],
- // #endif
- methods: {
- async init() {
- // 清除旧数据
- this.clear();
- await this.$nextTick();
- const res = await this.getContainerRect();
-
- if (res && res.width) {
- this.containerWidth = res.width;
- this.positions = [];
- this.positionMap = [];
-
- let sortableIndex = 0;
- this.cloneList = this.list.map((item, listIndex) => {
- this.maxIndex++;
-
- // 计算网格位置
- const row = Math.floor(listIndex / this.column);
- const col = listIndex % this.column;
- const x = col * this.width;
- const y = row * this.height;
-
- // 创建位置信息
- this.positions[listIndex] = {
- row,
- x,
- y,
- x1: x + this.width,
- y1: y + this.height,
- disabled: item.disabled,
- oldIndex: listIndex
- };
-
- // 处理禁用和可排序项的映射
- if (item.disabled) {
- this.positionMap.push({ index: listIndex, disabled: true, sortableIndex: -1 });
- } else {
- this.positionMap.push({ index: listIndex, disabled: false, sortableIndex: sortableIndex });
- sortableIndex++;
- }
-
- // 创建克隆项
- return {
- index: listIndex,
- item,
- x: x,
- y: y,
- startIndex: listIndex,
- disabled: item.disabled,
- oldIndex: listIndex
- };
- });
- this.isInit = true;
- }
- },
- getContainerRect() {
- return new Promise(resolve => {
- // #ifndef APP-NVUE
- this.$uGetRect('.u-draggable').then(res => {
- resolve(res);
- });
- // #endif
- // #ifdef APP-NVUE
- const ref = this.$refs['uDraggable'];
- if (ref) {
- dom.getComponentRect(ref, (res) => {
- resolve({
- height: res.size.height,
- width: res.size.width
- });
- });
- } else {
- resolve({ height: 0, width: 0 });
- }
- // #endif
- });
- },
- handleTouchStart(event, index) {
- const { handle } = event.target.dataset || {}
- if(this.handle && !handle) {
- return;
- }
-
- // 如果启用了长按模式且未长按,不允许拖拽
- if (this.longpress && !this.longPressStarted) {
- return;
- }
- // 获取目标元素并进行验证
- const targetItem = this.cloneList[index];
- if (!targetItem || targetItem.disabled || this.disabled) return;
-
- // 检查positionMap是否存在对应的数据
- const positionData = this.positionMap[targetItem.oldIndex];
- if (!positionData) return;
-
- // 设置拖拽状态
- this.isDrag = true;
- this.activeIndex = index;
-
- this.dragState.item = targetItem;
- this.dragState.index = targetItem.index;
- this.dragState.oldIndex = targetItem.index;
- this.dragState.sortableIndex = positionData.sortableIndex;
- },
- handleTouchMove(event, index) {
-
- // 如果不在拖拽状态,直接返回
- if (!this.isDrag) return;
- // 如果不是当前激活的元素,直接返回
- if (index !== this.activeIndex) return;
- // 获取当前位置
- const {x, y} = event.detail;
-
- // 计算中心点坐标
- const centerX = x + this.width / 2;
- const centerY = y + this.height / 2;
-
- this.detectCollision(centerX, centerY, index);
- },
- handleLongPress(event, index) {
- // 标记长按已开始
- this.longPressStarted = true;
- // 触发拖拽开始
- this.handleTouchStart(event, index);
- // 震动反馈(如果支持)
- if (uni.vibrateShort) {
- uni.vibrateShort();
- }
- },
- handleTouchEnd(event) {
- this.longPressStarted = false;
- // 如果没有激活的元素或全局禁用,直接返回
- if (this.activeIndex === -1 || this.disabled) return;
- // 结束拖拽状态
- this.isDrag = false;
- // 判断是否需要触发变更事件(只有当位置发生变化时)
- const hasPositionChanged = this.dragState.index !== this.dragState.oldIndex && this.dragState.oldIndex > -1;
-
- // 获取最后拖拽的元素和目标位置
- const lastDraggedItem = this.cloneList[this.activeIndex];
- const targetPosition = this.positions[this.dragState.index];
- // 验证元素和位置
- if (!lastDraggedItem || this.positionMap[this.dragState.index].disabled) {
- this.activeIndex = -1;
- return;
- }
- // 添加微小偏移以确保动画效果
- lastDraggedItem.x = targetPosition.x + 0.001;
- lastDraggedItem.y = targetPosition.y + 0.001;
- // 延迟设置最终位置并触发事件
- uni.$u.sleep(30).then(() => {
- // 设置最终位置
- lastDraggedItem.x = targetPosition.x;
- lastDraggedItem.y = targetPosition.y;
-
- // 如果位置发生变化,触发事件并重置状态
- if (hasPositionChanged) {
- this.dragState.oldIndex = -1;
- this.activeIndex = -1;
- this.triggerEmits();
- } else {
- // 即使位置没有变化,也需要重置激活状态
- this.activeIndex = -1;
- }
- });
- },
-
- detectCollision(centerX, centerY, activeIndex) {
- // 如果全局禁用,直接返回
- if (this.disabled) return;
-
- // 快速边界检查
- if (centerX < 0 || centerY < 0 ||
- centerX > this.containerWidth ||
- centerY > this.rows * this.height) {
- return;
- }
-
- // 获取当前行和列
- const currentRow = Math.floor(centerY / this.height);
- const currentCol = Math.floor(centerX / this.width);
-
- // 计算可能的目标索引
- const possibleIndex = currentRow * this.column + currentCol;
-
- // 验证索引是否有效且不是禁用位置
- if (possibleIndex >= 0 &&
- possibleIndex < this.positions.length &&
- possibleIndex !== this.dragState.index &&
- !this.positionMap[possibleIndex].disabled) {
-
- const targetPos = this.positions[possibleIndex];
- // 检查是否在目标区域内
- if (
- centerX >= targetPos.x &&
- centerX <= targetPos.x1 &&
- centerY >= targetPos.y &&
- centerY <= targetPos.y1
- ) {
- // 执行交换(只在可排序项之间)
- this.swapSortableItems(activeIndex, possibleIndex);
- }
- }
- },
-
- swapSortableItems(fromIndex, toIndex) {
- // 快速边界检查
- if (toIndex < 0 || toIndex >= this.positions.length) return;
-
- // 如果全局禁用或目标位置是禁用位置,直接返回
- if (this.disabled || this.positionMap[toIndex].disabled) return;
- // 获取拖拽元素
- const draggedItem = this.cloneList[fromIndex];
- if (!draggedItem || draggedItem.disabled) return;
-
- // 查找目标位置的元素
- const targetItem = this.cloneList.find(item => item.index === toIndex);
- if (!targetItem || targetItem.disabled) return;
- // 获取目标元素在cloneList中的索引
- const targetCloneIndex = this.cloneList.findIndex(item => item.index === toIndex);
- if (targetCloneIndex === -1) return;
- // 交换位置信息
- const draggedPosition = this.positions[this.dragState.index];
- const targetPosition = this.positions[toIndex];
- // 更新目标元素位置
- targetItem.x = draggedPosition.x;
- targetItem.y = draggedPosition.y;
- targetItem.index = this.dragState.index;
- // 更新拖拽元素索引
- draggedItem.index = toIndex;
-
- // 如果是拖拽状态,更新dragState的索引
- if (this.isDrag) {
- this.dragState.index = toIndex;
- }
- },
- triggerEmits() {
- // 按位置索引排序所有剩余项目
- const sortedItems = [...this.cloneList].sort((a, b) => a.index - b.index);
-
- // 创建最终结果数组
- const result = sortedItems.map(item => item.item);
-
- // 触发变更事件
- this.$emit('change', result);
- },
- closeItem(index) {
- // 如果全局禁用或项目禁用,直接返回
- if (this.disabled || (this.cloneList[index] && this.cloneList[index].disabled)) return;
-
- // 重置拖拽状态
- this.activeIndex = -1;
- this.isDrag = false;
- // 获取要删除的项目并验证
- const itemToRemove = this.cloneList[index];
- if (!itemToRemove) return;
- // 获取被删除项目的位置索引
- const removedPosition = itemToRemove.index;
-
- // 从列表中移除项目
- this.cloneList.splice(index, 1);
-
- // 重新计算所有项目的位置
- this.cloneList.forEach((item, idx) => {
- // 如果项目的位置索引大于被删除项目的位置,需要前移
- if (item.index > removedPosition) {
- item.index--;
-
- // 重新计算网格位置
- const row = Math.floor(item.index / this.column);
- const col = item.index % this.column;
- const x = col * this.width;
- const y = row * this.height;
-
- // 更新位置
- item.x = x;
- item.y = y;
-
- // 更新positions数组
- this.positions[item.index] = {
- row,
- x,
- y,
- x1: x + this.width,
- y1: y + this.height,
- disabled: item.disabled,
- oldIndex: item.index
- };
- }
- });
-
- // 重建位置映射表
- this.rebuildPositionMap();
-
- // 删除多余的位置信息
- if (this.positions.length > this.cloneList.length) {
- this.positions.splice(this.cloneList.length);
- }
-
- this.$emit('close', index);
- },
-
- rebuildPositionMap() {
- this.positionMap = [];
- let sortableIndex = 0;
-
- this.cloneList.forEach((item) => {
- if (item.disabled) {
- this.positionMap.push({
- index: item.index,
- disabled: true,
- sortableIndex: -1
- });
- } else {
- this.positionMap.push({
- index: item.index,
- disabled: false,
- sortableIndex: sortableIndex
- });
- sortableIndex++;
- }
- });
- },
- clear() {
- this.isInit = false;
- this.isDrag = false;
- this.maxIndex = -1;
- this.activeIndex = -1;
- this.cloneList = [];
- this.positions = [];
- this.positionMap = [];
- }
- },
- // #ifdef VUE2
- beforeDestroy() {
- this.clear();
- }
- // #endif
- // #ifdef VUE3
- beforeUnmount() {
- this.clear();
- }
- // #endif
- };
- </script>
- <style lang="scss" scoped>
- .u-draggable {
- overflow: hidden;
- /* #ifdef APP-NVUE */
- flex: 1;
- /* #endif */
- /* #ifndef APP-NVUE */
- width: 100%;
- /* #endif */
- &__area {
- /* #ifdef APP-NVUE */
- flex: 1;
- /* #endif */
- /* #ifndef APP-NVUE */
- width: 100%;
- /* #endif */
- }
- &__handle {
- position: absolute;
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- right: 0;
- top: 0;
-
- &-inner {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 60px;
- height: 60px;
- background-color: #000;
-
- }
- }
- &__close {
- position: absolute;
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- right: 0;
- top: 0;
- &-inner {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- background-color: #000;
- border-radius: 50%;
- opacity: 0.8;
- }
- }
- &__view {
- z-index: 2;
- transition: opacity 300ms ease;
- box-sizing: border-box;
-
- }
- &__active {
- z-index: 9999;
- transition: transform 100ms ease;
- }
-
- &__disabled {
- opacity: 0.5;
- cursor: not-allowed;
- /* #ifndef APP-NVUE */
- pointer-events: none;
- /* #endif */
- }
- }
- </style>
|