stellar-ui-v2 1.40.32 → 1.40.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ste-app-update/README.md +52 -5
- package/components/ste-app-update/method.js +188 -28
- package/components/ste-app-update/ste-app-update.vue +170 -4
- package/components/ste-checkbox/ste-checkbox.vue +57 -55
- package/components/ste-checkbox-group/ste-checkbox-group.vue +21 -6
- package/components/ste-drag-sort/README.md +124 -0
- package/components/ste-drag-sort/config.json +5 -0
- package/components/ste-drag-sort/ste-drag-sort.vue +706 -0
- package/components/ste-message-box/README.md +1 -1
- package/components/ste-message-box/ste-message-box.js +7 -5
- package/components/ste-message-box/ste-message-box.vue +364 -364
- package/components/ste-page-container/README.md +83 -83
- package/components/ste-page-container/WORKLOG-2026-03-12.md +229 -229
- package/components/ste-page-container/ste-page-container.vue +190 -190
- package/components/ste-popup/README.md +16 -16
- package/components/ste-popup/ste-popup.vue +392 -392
- package/components/ste-price/ste-price.vue +256 -256
- package/components/ste-radio/ste-radio.vue +53 -55
- package/components/ste-radio-group/ste-radio-group.vue +24 -9
- package/components/ste-search/ste-search.vue +576 -576
- package/components/ste-swipe-action/ste-swipe-action.vue +8 -8
- package/components/ste-swipe-action-group/ste-swipe-action-group.vue +5 -2
- package/components/ste-toast/README.md +1 -1
- package/components/ste-toast/ste-toast.js +67 -67
- package/components/ste-video/ste-video.vue +756 -756
- package/package.json +2 -2
- package/utils/System.js +19 -4
- package/utils/mixin.js +150 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="ste-drag-sort-root"
|
|
3
|
+
:class="{ 'ste-drag-sort-root-dragging': dragging ,'ste-drag-sort-columns': cmpColumns > 1}"
|
|
4
|
+
data-test="drag-sort">
|
|
5
|
+
<view v-for="(current, index) in list" :key="current.uid" class="ste-drag-sort-item" :class="{
|
|
6
|
+
'ste-drag-sort-item-disabled': disabled || !!(list[index] && list[index].raw && list[index].raw.disabled),
|
|
7
|
+
'ste-drag-sort-item-ready': dragging && dragIndex === index && !sortingStarted,
|
|
8
|
+
'ste-drag-sort-item-dragging': dragging && dragIndex === index,
|
|
9
|
+
'ste-drag-sort-item-animating': dragging && dragIndex !== index
|
|
10
|
+
}" :style="[itemStyles[index]]" @touchstart="onTouchStart($event, index, 'touch')"
|
|
11
|
+
@longpress="onTouchStart($event, index, 'longpress')" @touchmove="onTouchMove" @touchend="onTouchEnd"
|
|
12
|
+
@touchcancel="onTouchEnd" @mousedown.prevent="mouseDown($event, index)">
|
|
13
|
+
<slot name="item" :item="current.raw" :index="index" :dragging="dragging" :dragIndex="dragIndex"
|
|
14
|
+
:insertIndex="insertIndex"></slot>
|
|
15
|
+
</view>
|
|
16
|
+
</view>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import utils from '../../utils/utils.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ste-drag-sort 拖拽排序
|
|
24
|
+
* @description 支持纵向列表和网格的拖拽排序组件,支持长按触发和直接拖拽两种模式
|
|
25
|
+
* @property {Array} value 拖拽数据列表,支持 v-model 双向绑定
|
|
26
|
+
* @property {Boolean} disabled 是否禁用拖拽功能,默认 false
|
|
27
|
+
* @property {Number} columns 列数,1 为纵向列表,大于 1 为网格排序,默认 1
|
|
28
|
+
* @property {Boolean} longPress 拖拽启动方式,true 为长按后开始拖拽,false 为按下立即拖拽,默认 true
|
|
29
|
+
* @event {Function} start 开始拖拽时触发,参数:index(当前拖拽项索引)
|
|
30
|
+
* @event {Function} change 排序结果发生变化时触发,参数:list(最新排序结果)
|
|
31
|
+
* @event {Function} end 拖拽结束时触发,参数:index(最终落点索引)
|
|
32
|
+
*/
|
|
33
|
+
export default {
|
|
34
|
+
name: 'ste-drag-sort',
|
|
35
|
+
group: '基础组件',
|
|
36
|
+
title: 'DragSort 拖拽排序',
|
|
37
|
+
props: {
|
|
38
|
+
value: {
|
|
39
|
+
type: Array,
|
|
40
|
+
default: () => [],
|
|
41
|
+
},
|
|
42
|
+
disabled: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false,
|
|
45
|
+
},
|
|
46
|
+
columns: {
|
|
47
|
+
type: Number,
|
|
48
|
+
default: 1,
|
|
49
|
+
},
|
|
50
|
+
longPress: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: true,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
data() {
|
|
56
|
+
return {
|
|
57
|
+
list: [],
|
|
58
|
+
dragging: false,
|
|
59
|
+
dragIndex: -1,
|
|
60
|
+
insertIndex: -1,
|
|
61
|
+
startX: 0,
|
|
62
|
+
startY: 0,
|
|
63
|
+
offsetX: 0,
|
|
64
|
+
offsetY: 0,
|
|
65
|
+
/**
|
|
66
|
+
* 所有下标对应的位置信息
|
|
67
|
+
* 格式:{ [index: string]: { top: number, left: number, right: number, bottom: number } }
|
|
68
|
+
* 在长按/触摸触发拖拽前计算,用于 hit-test 判断手指进入哪个元素
|
|
69
|
+
*/
|
|
70
|
+
itemRects: {},
|
|
71
|
+
// 兼容旧逻辑保留,存储宽高用于样式偏移计算
|
|
72
|
+
itemPositions: [],
|
|
73
|
+
itemWidth: 0,
|
|
74
|
+
itemHeight: 0,
|
|
75
|
+
sortingStarted: false,
|
|
76
|
+
dragSessionId: 0,
|
|
77
|
+
// mouse 相关
|
|
78
|
+
mouseLongPressTimer: null,
|
|
79
|
+
mousePressing: false,
|
|
80
|
+
mousePendingIndex: -1,
|
|
81
|
+
mousePendingX: 0,
|
|
82
|
+
mousePendingY: 0,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
computed: {
|
|
86
|
+
cmpColumns() {
|
|
87
|
+
const value = Math.floor(Number(this.columns) || 1);
|
|
88
|
+
return value > 0 ? value : 1;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
itemStyles() {
|
|
92
|
+
// 强制追踪 sortingStarted,确保所有元素在 sortingStarted 变化时重新计算
|
|
93
|
+
const sortingStarted = this.sortingStarted;
|
|
94
|
+
|
|
95
|
+
return this.list.map((_, index) => {
|
|
96
|
+
const style = {};
|
|
97
|
+
|
|
98
|
+
if (this.cmpColumns > 1) {
|
|
99
|
+
const width = (100 / this.cmpColumns).toFixed(6) + '%';
|
|
100
|
+
style.flexBasis = width;
|
|
101
|
+
style.width = width;
|
|
102
|
+
style.boxSizing = 'border-box';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!this.dragging) return style;
|
|
106
|
+
|
|
107
|
+
if (index === this.dragIndex) {
|
|
108
|
+
const dragScale = sortingStarted ? 1.03 : 1.02;
|
|
109
|
+
style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${dragScale})`;
|
|
110
|
+
style.zIndex = 100;
|
|
111
|
+
return style;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const offset = this.getItemTranslateOffset(index);
|
|
115
|
+
if (offset.x || offset.y) {
|
|
116
|
+
style.transform = `translate(${offset.x}px, ${offset.y}px)`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return style;
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
watch: {
|
|
124
|
+
value: {
|
|
125
|
+
handler(val) {
|
|
126
|
+
this.syncList(val || []);
|
|
127
|
+
},
|
|
128
|
+
immediate: true,
|
|
129
|
+
deep: true,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
beforeDestroy() {
|
|
133
|
+
this.clearMouseLongPress();
|
|
134
|
+
this.removeMouseListeners();
|
|
135
|
+
this.enablePageScroll();
|
|
136
|
+
},
|
|
137
|
+
methods: {
|
|
138
|
+
// ---- 触摸事件兼容 ----
|
|
139
|
+
// 注意:rect.boundingClientRect 是视口坐标(clientX/clientY)
|
|
140
|
+
// hit-test 用 rect 边界做命中检测,此处优先取 clientX/clientY 与之对齐
|
|
141
|
+
getTouchX(touch) {
|
|
142
|
+
if (!touch) return 0;
|
|
143
|
+
if (typeof touch.clientX === 'number') return touch.clientX;
|
|
144
|
+
if (typeof touch.pageX === 'number') return touch.pageX;
|
|
145
|
+
if (typeof touch.x === 'number') return touch.x;
|
|
146
|
+
return 0;
|
|
147
|
+
},
|
|
148
|
+
getTouchY(touch) {
|
|
149
|
+
if (!touch) return 0;
|
|
150
|
+
if (typeof touch.clientY === 'number') return touch.clientY;
|
|
151
|
+
if (typeof touch.pageY === 'number') return touch.pageY;
|
|
152
|
+
if (typeof touch.y === 'number') return touch.y;
|
|
153
|
+
return 0;
|
|
154
|
+
},
|
|
155
|
+
toTouchArray(touches) {
|
|
156
|
+
if (!touches) return [];
|
|
157
|
+
if (Array.isArray(touches)) {
|
|
158
|
+
return touches.filter(Boolean);
|
|
159
|
+
} else if (touches && typeof touches === 'object') {
|
|
160
|
+
if (touches.length !== undefined) {
|
|
161
|
+
const result = [];
|
|
162
|
+
for (let i = 0; i < touches.length; i++) {
|
|
163
|
+
if (touches[i]) {
|
|
164
|
+
result.push(touches[i]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
} else {
|
|
169
|
+
return Object.keys(touches)
|
|
170
|
+
.filter(k => !isNaN(Number(k)))
|
|
171
|
+
.map(k => touches[k])
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// ---- uid 池管理 ----
|
|
179
|
+
getUidPools(items) {
|
|
180
|
+
const pools = new Map();
|
|
181
|
+
items.forEach((item) => {
|
|
182
|
+
const pool = pools.get(item.raw) || [];
|
|
183
|
+
pool.push(item.uid);
|
|
184
|
+
pools.set(item.raw, pool);
|
|
185
|
+
});
|
|
186
|
+
return pools;
|
|
187
|
+
},
|
|
188
|
+
syncList(value) {
|
|
189
|
+
const pools = this.getUidPools(this.list);
|
|
190
|
+
this.list = (value || []).map((item) => {
|
|
191
|
+
const pool = pools.get(item);
|
|
192
|
+
const uid = (pool && pool.shift()) || utils.guid(12);
|
|
193
|
+
return {
|
|
194
|
+
uid,
|
|
195
|
+
raw: item
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// ---- 状态重置 ----
|
|
201
|
+
resetDragState() {
|
|
202
|
+
this.dragSessionId += 1;
|
|
203
|
+
this.dragging = false;
|
|
204
|
+
this.dragIndex = -1;
|
|
205
|
+
this.insertIndex = -1;
|
|
206
|
+
this.startX = 0;
|
|
207
|
+
this.startY = 0;
|
|
208
|
+
this.offsetX = 0;
|
|
209
|
+
this.offsetY = 0;
|
|
210
|
+
this.itemRects = {};
|
|
211
|
+
this.itemPositions = [];
|
|
212
|
+
this.itemWidth = 0;
|
|
213
|
+
this.itemHeight = 0;
|
|
214
|
+
this.sortingStarted = false;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// ---- 禁用/恢复页面滚动 ----
|
|
218
|
+
disablePageScroll() {
|
|
219
|
+
try {
|
|
220
|
+
// #ifdef MP-WEIXIN
|
|
221
|
+
wx.setPageScrollEnabled && wx.setPageScrollEnabled({ enabled: false });
|
|
222
|
+
// #endif
|
|
223
|
+
// #ifdef WEB
|
|
224
|
+
if (typeof document !== 'undefined') {
|
|
225
|
+
document.body.style.overflow = 'hidden';
|
|
226
|
+
document.documentElement.style.overflow = 'hidden';
|
|
227
|
+
}
|
|
228
|
+
// #endif
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// 忽略不支持的平台
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
enablePageScroll() {
|
|
234
|
+
try {
|
|
235
|
+
// #ifdef MP-WEIXIN
|
|
236
|
+
wx.setPageScrollEnabled && wx.setPageScrollEnabled({ enabled: true });
|
|
237
|
+
// #endif
|
|
238
|
+
// #ifdef WEB
|
|
239
|
+
if (typeof document !== 'undefined') {
|
|
240
|
+
document.body.style.overflow = '';
|
|
241
|
+
document.documentElement.style.overflow = '';
|
|
242
|
+
}
|
|
243
|
+
// #endif
|
|
244
|
+
} catch (e) {
|
|
245
|
+
// 忽略不支持的平台
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// ---- 鼠标长按相关 ----
|
|
250
|
+
clearMouseLongPress() {
|
|
251
|
+
if (this.mouseLongPressTimer) {
|
|
252
|
+
clearTimeout(this.mouseLongPressTimer);
|
|
253
|
+
this.mouseLongPressTimer = null;
|
|
254
|
+
}
|
|
255
|
+
this.mousePressing = false;
|
|
256
|
+
this.mousePendingIndex = -1;
|
|
257
|
+
this.mousePendingX = 0;
|
|
258
|
+
this.mousePendingY = 0;
|
|
259
|
+
},
|
|
260
|
+
removeMouseListeners() {
|
|
261
|
+
// #ifdef WEB
|
|
262
|
+
if (typeof window === 'undefined') return;
|
|
263
|
+
window.removeEventListener('mousemove', this._onMouseMove);
|
|
264
|
+
window.removeEventListener('mouseup', this._onMouseUp);
|
|
265
|
+
// #endif
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// ---- 禁用判断 ----
|
|
269
|
+
getItemDisabled(index) {
|
|
270
|
+
return !!(this.list[index] && this.list[index].raw && this.list[index].raw.disabled);
|
|
271
|
+
},
|
|
272
|
+
getEnabledIndices() {
|
|
273
|
+
return this.list.reduce((indices, item, index) => {
|
|
274
|
+
if (!item.raw || !item.raw.disabled) {
|
|
275
|
+
indices.push(index);
|
|
276
|
+
}
|
|
277
|
+
return indices;
|
|
278
|
+
}, []);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// ---- 排序预览(基于新排序规则)----
|
|
282
|
+
/**
|
|
283
|
+
* 排序规则说明:
|
|
284
|
+
* 示例1:[0,1,2,3,4,5],拖拽元素1,手指移入4,结果 [0,2,3,4,1,5]
|
|
285
|
+
* 示例2:[0,1,2,3,4,5],拖拽元素5,手指移入1,结果 [0,5,1,2,3,4]
|
|
286
|
+
* 规则:将 dragIndex 元素从原位置移除,插入到 targetIndex 位置,其他元素顺序平移
|
|
287
|
+
*/
|
|
288
|
+
getReorderPreview(dragIdx, targetIdx) {
|
|
289
|
+
const enabledIndices = this.getEnabledIndices();
|
|
290
|
+
const dragEnabledPos = enabledIndices.indexOf(dragIdx);
|
|
291
|
+
const targetEnabledPos = enabledIndices.indexOf(targetIdx);
|
|
292
|
+
|
|
293
|
+
if (dragEnabledPos < 0 || targetEnabledPos < 0) return null;
|
|
294
|
+
|
|
295
|
+
// 将 dragIdx 从原位置移除,插入到 targetIdx 对应的启用位置
|
|
296
|
+
const nextEnabledSourceIndices = enabledIndices.slice();
|
|
297
|
+
const spliced = nextEnabledSourceIndices.splice(dragEnabledPos, 1);
|
|
298
|
+
nextEnabledSourceIndices.splice(targetEnabledPos, 0, spliced[0]);
|
|
299
|
+
|
|
300
|
+
// 构建原始下标 -> 最终槽位下标的映射
|
|
301
|
+
const finalIndexMap = new Map();
|
|
302
|
+
this.list.forEach((_, index) => finalIndexMap.set(index, index));
|
|
303
|
+
enabledIndices.forEach((slotIndex, orderIndex) => {
|
|
304
|
+
finalIndexMap.set(nextEnabledSourceIndices[orderIndex], slotIndex);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
enabledIndices,
|
|
309
|
+
nextEnabledSourceIndices,
|
|
310
|
+
finalIndexMap
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// ---- 位置测量(长按触发前调用,计算所有下标对应的 {top,left,right,bottom})----
|
|
315
|
+
measureItemPositions() {
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
utils.querySelector('.ste-drag-sort-root', this).then((rootRect) => {
|
|
319
|
+
utils.querySelector('.ste-drag-sort-item', this, true).then((rects) => {
|
|
320
|
+
let items = [];
|
|
321
|
+
if (Array.isArray(rects)) {
|
|
322
|
+
items = rects;
|
|
323
|
+
} else if (rects && typeof rects === 'object') {
|
|
324
|
+
if (rects.length !== undefined) {
|
|
325
|
+
for (let i = 0; i < rects.length; i++) {
|
|
326
|
+
if (rects[i]) items.push(rects[i]);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
Object.keys(rects).forEach(key => {
|
|
330
|
+
if (!isNaN(Number(key)) && rects[key]) {
|
|
331
|
+
items.push(rects[key]);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 建立 DOM 位置 → list 索引的映射
|
|
338
|
+
// querySelectorAll 返回 DOM 顺序的 rects,与 list 渲染顺序一致
|
|
339
|
+
// 用 uid 找到每个 rect 对应的 list 下标,确保 itemRects 的 key 是 list 索引
|
|
340
|
+
const uidToListIndex = {};
|
|
341
|
+
this.list.forEach((item, idx) => {
|
|
342
|
+
uidToListIndex[item.uid] = idx;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// DOM 顺序与 list 顺序一致,不需要按视觉排序
|
|
346
|
+
// items[i] 对应 list[uidToListIndex[uid]](即 list[i],因为 uid 顺序与 DOM 顺序一致)
|
|
347
|
+
// 直接用 list 索引 i 作为 itemRects 的 key,保持与 hitTestIndex / insertIndex 的索引一致
|
|
348
|
+
this.itemWidth = this.cmpColumns > 1
|
|
349
|
+
? (rootRect ? rootRect.width : 0) / this.cmpColumns
|
|
350
|
+
: (items[0] ? items[0].width : 0);
|
|
351
|
+
this.itemHeight = items[0] ? items[0].height : 0;
|
|
352
|
+
|
|
353
|
+
// 兼容旧逻辑的 itemPositions(用于样式偏移计算)
|
|
354
|
+
this.itemPositions = items.map((rect, index) => ({
|
|
355
|
+
top: rect.top || 0,
|
|
356
|
+
left: rect.left || 0,
|
|
357
|
+
width: rect.width || this.itemWidth,
|
|
358
|
+
height: rect.height || this.itemHeight,
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* itemRects 格式:{ [listIndex: string]: { top, left, right, bottom } }
|
|
363
|
+
* key 是 list 的下标索引,与 hitTestIndex / insertIndex 的索引体系完全一致
|
|
364
|
+
*/
|
|
365
|
+
const rects2 = {};
|
|
366
|
+
items.forEach((rect, index) => {
|
|
367
|
+
const top = rect.top || 0;
|
|
368
|
+
const left = rect.left || 0;
|
|
369
|
+
const width = rect.width || this.itemWidth;
|
|
370
|
+
const height = rect.height || this.itemHeight;
|
|
371
|
+
// 直接用数组下标 index 作为 list 索引(DOM 顺序 = list 顺序)
|
|
372
|
+
rects2[String(index)] = {
|
|
373
|
+
top,
|
|
374
|
+
left,
|
|
375
|
+
right: left + width,
|
|
376
|
+
bottom: top + height,
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
this.itemRects = rects2;
|
|
380
|
+
|
|
381
|
+
resolve();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}, 50);
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// ---- hit-test:根据手指坐标判断进入了哪个下标 ----
|
|
389
|
+
/**
|
|
390
|
+
* 命中策略(按优先级顺序):
|
|
391
|
+
* 1. 死区安全区:手指在 dragIndex rect ± 死区范围内 → 不触发排序
|
|
392
|
+
* 2. rect 命中:手指落在某个其他元素的 rect 内 → 返回该元素 index
|
|
393
|
+
* 3. 中心点距离:手指不在任何 rect 内 → 找中心点曼哈顿距离最近的元素
|
|
394
|
+
*
|
|
395
|
+
* @param {number} x 手指 clientX
|
|
396
|
+
* @param {number} y 手指 clientY
|
|
397
|
+
* @returns {number} 命中的下标
|
|
398
|
+
*/
|
|
399
|
+
hitTestIndex(x, y) {
|
|
400
|
+
const keys = Object.keys(this.itemRects);
|
|
401
|
+
if (!keys.length) return this.dragIndex;
|
|
402
|
+
|
|
403
|
+
const dragRect = this.itemRects[String(this.dragIndex)];
|
|
404
|
+
if (!dragRect) return this.dragIndex;
|
|
405
|
+
|
|
406
|
+
// 阶段一:安全区。手指在 dragIndex rect + 死区范围内,不触发排序
|
|
407
|
+
const DEAD_ZONE = 10;
|
|
408
|
+
const inDeadZone = x >= dragRect.left - DEAD_ZONE &&
|
|
409
|
+
x <= dragRect.right + DEAD_ZONE &&
|
|
410
|
+
y >= dragRect.top - DEAD_ZONE &&
|
|
411
|
+
y <= dragRect.bottom + DEAD_ZONE;
|
|
412
|
+
if (inDeadZone) return this.dragIndex;
|
|
413
|
+
|
|
414
|
+
// 阶段二:遍历 itemRects,手指落在哪个 rect 内则命中(排除 dragIndex)
|
|
415
|
+
let hit = null;
|
|
416
|
+
for (let i = 0; i < keys.length; i++) {
|
|
417
|
+
const idx = Number(keys[i]);
|
|
418
|
+
if (this.getItemDisabled(idx)) continue;
|
|
419
|
+
if (idx === this.dragIndex) continue;
|
|
420
|
+
const rect = this.itemRects[keys[i]];
|
|
421
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
422
|
+
hit = idx;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (hit !== null) return hit;
|
|
427
|
+
|
|
428
|
+
// 阶段三:手指不在任何 rect 内(越出列表边界等),找中心点最近距离(排除 dragIndex)
|
|
429
|
+
let closestIndex = this.dragIndex;
|
|
430
|
+
let minDist = Infinity;
|
|
431
|
+
for (let i = 0; i < keys.length; i++) {
|
|
432
|
+
const idx = Number(keys[i]);
|
|
433
|
+
if (this.getItemDisabled(idx)) continue;
|
|
434
|
+
if (idx === this.dragIndex) continue;
|
|
435
|
+
const rect = this.itemRects[keys[i]];
|
|
436
|
+
const cx = (rect.left + rect.right) / 2;
|
|
437
|
+
const cy = (rect.top + rect.bottom) / 2;
|
|
438
|
+
const dist = Math.abs(x - cx) + Math.abs(y - cy);
|
|
439
|
+
if (dist < minDist) {
|
|
440
|
+
minDist = dist;
|
|
441
|
+
closestIndex = idx;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return closestIndex;
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
// ---- 样式计算 ----
|
|
448
|
+
getItemTranslateOffset(index) {
|
|
449
|
+
if (!this.dragging || !this.sortingStarted) return { x: 0, y: 0 };
|
|
450
|
+
if (this.getItemDisabled(index)) return { x: 0, y: 0 };
|
|
451
|
+
if (index === this.dragIndex || this.dragIndex === this.insertIndex) return { x: 0, y: 0 };
|
|
452
|
+
|
|
453
|
+
// 复用 itemStyles 中捕获的 sortingStarted,确保依赖一致
|
|
454
|
+
const currentSortingStarted = this.sortingStarted;
|
|
455
|
+
if (!currentSortingStarted) return { x: 0, y: 0 };
|
|
456
|
+
|
|
457
|
+
const reorderPreview = this.getReorderPreview(this.dragIndex, this.insertIndex);
|
|
458
|
+
const nextIndex = reorderPreview && reorderPreview.finalIndexMap.get(index);
|
|
459
|
+
|
|
460
|
+
if (typeof nextIndex !== 'number' || nextIndex === index) return { x: 0, y: 0 };
|
|
461
|
+
|
|
462
|
+
if (this.cmpColumns > 1) {
|
|
463
|
+
const cols = this.cmpColumns;
|
|
464
|
+
const currentRow = Math.floor(index / cols);
|
|
465
|
+
const currentCol = index % cols;
|
|
466
|
+
const nextRow = Math.floor(nextIndex / cols);
|
|
467
|
+
const nextCol = nextIndex % cols;
|
|
468
|
+
return {
|
|
469
|
+
x: (nextCol - currentCol) * this.itemWidth,
|
|
470
|
+
y: (nextRow - currentRow) * this.itemHeight,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
x: 0,
|
|
476
|
+
y: (nextIndex - index) * this.itemHeight
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
// ---- 触觉反馈 ----
|
|
481
|
+
triggerReadyHaptic() {
|
|
482
|
+
if (!this.longPress) return;
|
|
483
|
+
try {
|
|
484
|
+
if (typeof uni === 'undefined' || typeof uni.vibrateShort !== 'function') return;
|
|
485
|
+
uni.vibrateShort({
|
|
486
|
+
type: 'light',
|
|
487
|
+
fail: () => {}
|
|
488
|
+
});
|
|
489
|
+
} catch (e) {
|
|
490
|
+
// 忽略不支持的平台
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
// ---- 拖拽核心流程 ----
|
|
495
|
+
startDrag(index, clientX, clientY) {
|
|
496
|
+
if (this.disabled) return Promise.resolve(false);
|
|
497
|
+
if (this.dragging) return Promise.resolve(false);
|
|
498
|
+
if (index < 0 || index >= this.list.length) return Promise.resolve(false);
|
|
499
|
+
if (this.getItemDisabled(index)) return Promise.resolve(false);
|
|
500
|
+
|
|
501
|
+
const sessionId = ++this.dragSessionId;
|
|
502
|
+
this.dragging = true;
|
|
503
|
+
this.dragIndex = index;
|
|
504
|
+
this.insertIndex = index;
|
|
505
|
+
this.startX = clientX;
|
|
506
|
+
this.startY = clientY;
|
|
507
|
+
this.offsetX = 0;
|
|
508
|
+
this.offsetY = 0;
|
|
509
|
+
this.sortingStarted = false;
|
|
510
|
+
|
|
511
|
+
this.triggerReadyHaptic();
|
|
512
|
+
this.$emit('start', index);
|
|
513
|
+
|
|
514
|
+
// 禁用页面滚动
|
|
515
|
+
this.disablePageScroll();
|
|
516
|
+
|
|
517
|
+
// 在开始拖拽时测量所有元素位置(计算 itemRects)
|
|
518
|
+
return this.measureItemPositions().then(() => {
|
|
519
|
+
return this.dragging && this.dragSessionId === sessionId && this.dragIndex === index;
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
moveDrag(clientX, clientY) {
|
|
524
|
+
if (!this.dragging) return;
|
|
525
|
+
this.offsetX = clientX - this.startX;
|
|
526
|
+
this.offsetY = clientY - this.startY;
|
|
527
|
+
|
|
528
|
+
// 移动超过阈值才开始排序
|
|
529
|
+
if (!this.sortingStarted && (Math.abs(this.offsetX) > 5 || Math.abs(this.offsetY) > 5)) {
|
|
530
|
+
this.sortingStarted = true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (this.sortingStarted) {
|
|
534
|
+
// 使用手指实际坐标进行 hit-test,获取当前进入的下标
|
|
535
|
+
const hitIdx = this.hitTestIndex(clientX, clientY);
|
|
536
|
+
if (hitIdx !== this.insertIndex) {
|
|
537
|
+
this.insertIndex = hitIdx;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
finishDrag() {
|
|
543
|
+
if (!this.dragging) return;
|
|
544
|
+
|
|
545
|
+
const oldIndex = this.dragIndex;
|
|
546
|
+
const targetInsert = this.insertIndex >= 0 ? this.insertIndex : oldIndex;
|
|
547
|
+
const reorderPreview = this.getReorderPreview(oldIndex, targetInsert);
|
|
548
|
+
const newIndex = (reorderPreview && reorderPreview.finalIndexMap.get(oldIndex)) !== undefined
|
|
549
|
+
? reorderPreview.finalIndexMap.get(oldIndex)
|
|
550
|
+
: oldIndex;
|
|
551
|
+
|
|
552
|
+
if (oldIndex !== newIndex && oldIndex >= 0) {
|
|
553
|
+
const nextList = new Array(this.list.length);
|
|
554
|
+
|
|
555
|
+
this.list.forEach((item, index) => {
|
|
556
|
+
if (this.getItemDisabled(index)) {
|
|
557
|
+
nextList[index] = item;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
reorderPreview.enabledIndices.forEach((slotIndex, orderIndex) => {
|
|
562
|
+
nextList[slotIndex] = this.list[reorderPreview.nextEnabledSourceIndices[orderIndex]];
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
this.list = nextList;
|
|
566
|
+
|
|
567
|
+
const result = nextList.map((item) => item.raw);
|
|
568
|
+
this.$emit('input', result);
|
|
569
|
+
this.$emit('change', result);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 恢复页面滚动
|
|
573
|
+
this.enablePageScroll();
|
|
574
|
+
|
|
575
|
+
this.resetDragState();
|
|
576
|
+
this.$emit('end', newIndex);
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
// ---- Touch 事件 ----
|
|
580
|
+
onTouchStart(event, index, type) {
|
|
581
|
+
if (type === 'longpress' && !this.longPress) return;
|
|
582
|
+
if (type === 'touch' && this.longPress) return;
|
|
583
|
+
|
|
584
|
+
const touch = this.getFirstTouch(event);
|
|
585
|
+
if (!touch) return;
|
|
586
|
+
|
|
587
|
+
this.startDrag(index, this.getTouchX(touch), this.getTouchY(touch)).then((started) => {
|
|
588
|
+
if (!started) return;
|
|
589
|
+
event.stopPropagation && event.stopPropagation();
|
|
590
|
+
event.preventDefault && event.preventDefault();
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
onTouchMove(event) {
|
|
594
|
+
if (!this.dragging) return;
|
|
595
|
+
const touch = this.getFirstTouch(event);
|
|
596
|
+
if (!touch) return;
|
|
597
|
+
this.moveDrag(this.getTouchX(touch), this.getTouchY(touch));
|
|
598
|
+
event.preventDefault && event.preventDefault();
|
|
599
|
+
},
|
|
600
|
+
onTouchEnd() {
|
|
601
|
+
this.finishDrag();
|
|
602
|
+
},
|
|
603
|
+
getFirstTouch(event) {
|
|
604
|
+
const touches = this.toTouchArray(event.touches);
|
|
605
|
+
if (touches.length > 0) return touches[0];
|
|
606
|
+
const changedTouches = this.toTouchArray(event.changedTouches);
|
|
607
|
+
if (changedTouches.length > 0) return changedTouches[0];
|
|
608
|
+
return null;
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
// ---- Mouse 事件(H5/PC) ----
|
|
612
|
+
mouseDown(event, index) {
|
|
613
|
+
// #ifdef WEB
|
|
614
|
+
if (typeof window === 'undefined') return;
|
|
615
|
+
if (this.disabled || this.getItemDisabled(index)) return;
|
|
616
|
+
|
|
617
|
+
this.mousePressing = true;
|
|
618
|
+
this.mousePendingIndex = index;
|
|
619
|
+
this.mousePendingX = event.clientX;
|
|
620
|
+
this.mousePendingY = event.clientY;
|
|
621
|
+
|
|
622
|
+
this._onMouseMove = this._onMouseMove || ((e) => this.onMouseMove(e));
|
|
623
|
+
this._onMouseUp = this._onMouseUp || (() => this.onMouseUp());
|
|
624
|
+
|
|
625
|
+
window.addEventListener('mousemove', this._onMouseMove);
|
|
626
|
+
window.addEventListener('mouseup', this._onMouseUp);
|
|
627
|
+
|
|
628
|
+
if (!this.longPress) {
|
|
629
|
+
this.startDrag(index, event.clientX, event.clientY);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this.mouseLongPressTimer = setTimeout(() => {
|
|
634
|
+
if (!this.mousePressing || this.mousePendingIndex < 0) return;
|
|
635
|
+
this.startDrag(this.mousePendingIndex, this.mousePendingX, this.mousePendingY);
|
|
636
|
+
}, 350);
|
|
637
|
+
// #endif
|
|
638
|
+
},
|
|
639
|
+
onMouseMove(event) {
|
|
640
|
+
if (this.dragging) {
|
|
641
|
+
this.moveDrag(event.clientX, event.clientY);
|
|
642
|
+
event.preventDefault && event.preventDefault();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!this.mousePressing || !this.mouseLongPressTimer) return;
|
|
646
|
+
const deltaX = Math.abs(event.clientX - this.mousePendingX);
|
|
647
|
+
const deltaY = Math.abs(event.clientY - this.mousePendingY);
|
|
648
|
+
if (deltaX > 5 || deltaY > 5) {
|
|
649
|
+
this.clearMouseLongPress();
|
|
650
|
+
this.removeMouseListeners();
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
onMouseUp() {
|
|
654
|
+
if (this.dragging) {
|
|
655
|
+
this.finishDrag();
|
|
656
|
+
}
|
|
657
|
+
this.clearMouseLongPress();
|
|
658
|
+
this.removeMouseListeners();
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
</script>
|
|
663
|
+
|
|
664
|
+
<style lang="scss" scoped>
|
|
665
|
+
.ste-drag-sort-root {
|
|
666
|
+
position: relative;
|
|
667
|
+
display: flex;
|
|
668
|
+
flex-direction: column;
|
|
669
|
+
overflow: visible;
|
|
670
|
+
user-select: none;
|
|
671
|
+
width: 100%;
|
|
672
|
+
|
|
673
|
+
&.ste-drag-sort-columns {
|
|
674
|
+
flex-direction: row;
|
|
675
|
+
flex-wrap: wrap;
|
|
676
|
+
box-sizing: border-box;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
&.ste-drag-sort-root-dragging {
|
|
680
|
+
touch-action: none;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.ste-drag-sort-item {
|
|
685
|
+
position: relative;
|
|
686
|
+
z-index: 10;
|
|
687
|
+
transition: opacity 0.12s ease;
|
|
688
|
+
|
|
689
|
+
&.ste-drag-sort-item-dragging {
|
|
690
|
+
opacity: 0.95;
|
|
691
|
+
z-index: 20;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
&.ste-drag-sort-item-ready {
|
|
695
|
+
opacity: 0.98;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
&.ste-drag-sort-item-disabled {
|
|
699
|
+
opacity: 0.6;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
&.ste-drag-sort-item-animating {
|
|
703
|
+
transition: transform 0.2s ease;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
</style>
|
|
@@ -12,7 +12,7 @@ npm install ste-vue-inset-loader --save-dev
|
|
|
12
12
|
```
|
|
13
13
|
"insetLoader": {
|
|
14
14
|
"config": {
|
|
15
|
-
"msgBox": "<
|
|
15
|
+
"msgBox": "<ste-message-box id='steMessageBox'></ste-message-box>"
|
|
16
16
|
},
|
|
17
17
|
// 全局配置
|
|
18
18
|
"label": ["msgBox"],
|