u-dragsort.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. <template>
  2. <view class="u-dragsort"
  3. :class="[direction == 'horizontal' ? 'u-dragsort--horizontal' : '', direction == 'all' ? 'u-dragsort--all' : '']">
  4. <movable-area class="u-dragsort-area" :style="movableAreaStyle">
  5. <movable-view v-for="(item, index) in list" :key="item.id" :id="`u-dragsort-item-${index}`"
  6. class="u-dragsort-item" :class="{ 'dragging': dragIndex === index }"
  7. :direction="direction === 'all' ? 'all' : direction" :x="item.x" :y="item.y" :inertia="false"
  8. :disabled="!draggable || (item.draggable === false)" @change="onChange(index, $event)"
  9. @touchstart="onTouchStart(index)" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
  10. <view class="u-dragsort-item-content">
  11. <slot :item="item" :index="index">
  12. {{ item.label }}
  13. </slot>
  14. </view>
  15. </movable-view>
  16. </movable-area>
  17. </view>
  18. </template>
  19. <script>
  20. import { mpMixin } from '../../libs/mixin/mpMixin';
  21. import { mixin } from '../../libs/mixin/mixin';
  22. import { addStyle, addUnit, sleep } from '../../libs/function/index';
  23. export default {
  24. name: 'u-dragsort',
  25. // #ifdef MP
  26. mixins: [mpMixin, mixin,],
  27. // #endif
  28. // #ifndef MP
  29. mixins: [mixin],
  30. // #endif
  31. props: {
  32. initialList: {
  33. type: Array,
  34. required: true,
  35. default: () => []
  36. },
  37. draggable: {
  38. type: Boolean,
  39. default: true
  40. },
  41. direction: {
  42. type: String,
  43. default: 'vertical',
  44. validator: value => ['vertical', 'horizontal', 'all'].includes(value)
  45. },
  46. // 新增列数属性,用于all模式
  47. columns: {
  48. type: Number,
  49. default: 3
  50. }
  51. },
  52. data() {
  53. return {
  54. list: [],
  55. dragIndex: -1,
  56. itemHeight: 40,
  57. itemWidth: 80,
  58. areaWidth: 0, // 可拖动区域宽度
  59. areaHeight: 0, // 可拖动区域高度
  60. originalPositions: [], // 保存原始位置
  61. currentPosition: {
  62. x: 0,
  63. y: 0
  64. }
  65. };
  66. },
  67. computed: {
  68. movableAreaStyle() {
  69. if (this.direction === 'vertical') {
  70. return {
  71. height: `${this.list.length * this.itemHeight}px`,
  72. width: '100%'
  73. };
  74. } else if (this.direction === 'horizontal') {
  75. return {
  76. height: '100%',
  77. width: `${this.list.length * this.itemWidth}px`
  78. };
  79. } else {
  80. // all模式,计算网格布局所需的高度
  81. const rows = Math.ceil(this.list.length / this.columns);
  82. return {
  83. height: `${rows * this.itemHeight}px`,
  84. width: '100%'
  85. };
  86. }
  87. }
  88. },
  89. emits: ['drag-end'],
  90. async mounted() {
  91. await this.$nextTick();
  92. this.initList();
  93. this.calculateItemSize();
  94. this.calculateAreaSize();
  95. },
  96. methods: {
  97. initList() {
  98. // 初始化列表项的位置
  99. this.list = this.initialList.map((item, index) => {
  100. let x = 0, y = 0;
  101. if (this.direction === 'horizontal') {
  102. x = index * this.itemWidth;
  103. y = 0;
  104. } else if (this.direction === 'vertical') {
  105. x = 0;
  106. y = index * this.itemHeight;
  107. } else {
  108. // all模式,网格布局
  109. const col = index % this.columns;
  110. const row = Math.floor(index / this.columns);
  111. x = col * this.itemWidth;
  112. y = row * this.itemHeight;
  113. }
  114. return {
  115. ...item,
  116. x,
  117. y
  118. };
  119. });
  120. // 保存初始位置
  121. this.saveOriginalPositions();
  122. },
  123. saveOriginalPositions() {
  124. // 保存当前位置作为原始位置
  125. this.originalPositions = this.list.map(item => ({
  126. x: item.x,
  127. y: item.y
  128. }));
  129. },
  130. async calculateItemSize() {
  131. // 计算项目尺寸
  132. await sleep(30);
  133. return new Promise((resolve) => {
  134. uni.createSelectorQuery()
  135. .in(this)
  136. .select('.u-dragsort-item-content')
  137. .boundingClientRect(res => {
  138. if (res) {
  139. this.itemHeight = res.height || 40;
  140. this.itemWidth = res.width || 80;
  141. // 更新所有项目的位置
  142. this.updatePositions();
  143. // 保存原始位置
  144. this.saveOriginalPositions();
  145. }
  146. resolve(res);
  147. })
  148. .exec();
  149. });
  150. },
  151. async calculateAreaSize() {
  152. // 计算可拖动区域尺寸
  153. await sleep(30);
  154. return new Promise((resolve) => {
  155. uni.createSelectorQuery()
  156. .in(this)
  157. .select('.u-dragsort-area')
  158. .boundingClientRect(res => {
  159. if (res) {
  160. this.areaWidth = res.width || 300;
  161. this.areaHeight = res.height || 300;
  162. }
  163. resolve(res);
  164. })
  165. .exec();
  166. });
  167. },
  168. updatePositions() {
  169. // 更新所有项目的位置
  170. this.list.forEach((item, index) => {
  171. if (this.direction === 'vertical') {
  172. item.y = index * this.itemHeight;
  173. item.x = 0;
  174. } else if (this.direction === 'horizontal') {
  175. item.x = index * this.itemWidth;
  176. item.y = 0;
  177. } else {
  178. // all模式,网格布局
  179. const col = index % this.columns;
  180. const row = Math.floor(index / this.columns);
  181. item.x = col * this.itemWidth;
  182. item.y = row * this.itemHeight;
  183. }
  184. });
  185. },
  186. onTouchStart(index) {
  187. this.dragIndex = index;
  188. // 保存当前位置作为原始位置
  189. this.saveOriginalPositions();
  190. },
  191. onChange(index, event) {
  192. if (!event.detail.source || event.detail.source !== 'touch') return;
  193. this.currentPosition.x = event.detail.x;
  194. this.currentPosition.y = event.detail.y;
  195. // all模式下使用更智能的位置计算
  196. if (this.direction === 'all') {
  197. this.handleAllModeChange(index);
  198. } else {
  199. // 原有的垂直和水平模式逻辑
  200. let itemSize = 0;
  201. let targetIndex = -1;
  202. if (this.direction === 'vertical') {
  203. itemSize = this.itemHeight;
  204. targetIndex = Math.max(0, Math.min(
  205. Math.round(this.currentPosition.y / itemSize),
  206. this.list.length - 1
  207. ));
  208. } else if (this.direction === 'horizontal') {
  209. itemSize = this.itemWidth;
  210. targetIndex = Math.max(0, Math.min(
  211. Math.round(this.currentPosition.x / itemSize),
  212. this.list.length - 1
  213. ));
  214. }
  215. // 如果位置发生变化,则重新排序
  216. if (targetIndex !== index) {
  217. this.reorderItems(index, targetIndex);
  218. }
  219. }
  220. },
  221. handleAllModeChange(index) {
  222. // 在all模式下,根据当前位置计算最近的网格位置
  223. const col = Math.max(0, Math.min(Math.round(this.currentPosition.x / this.itemWidth), this.columns - 1));
  224. const row = Math.max(0, Math.round(this.currentPosition.y / this.itemHeight));
  225. // 计算目标索引
  226. let targetIndex = row * this.columns + col;
  227. targetIndex = Math.max(0, Math.min(targetIndex, this.list.length - 1));
  228. // 如果位置发生变化,则重新排序
  229. if (targetIndex !== index) {
  230. this.reorderItems(index, targetIndex);
  231. }
  232. },
  233. reorderItems(fromIndex, toIndex) {
  234. const movedItem = this.list.splice(fromIndex, 1)[0];
  235. this.list.splice(toIndex, 0, movedItem);
  236. // 震动反馈
  237. if (uni.vibrateShort) {
  238. uni.vibrateShort();
  239. }
  240. // 更新当前拖拽项目的新索引
  241. this.dragIndex = toIndex;
  242. // 更新所有项目的位置
  243. this.updatePositions();
  244. // 保存当前位置作为原始位置
  245. this.saveOriginalPositions();
  246. },
  247. onTouchEnd() {
  248. // 0.001是为了解决拖动过快等某些极限场景下位置还原不生效问题
  249. if (this.direction === 'horizontal') {
  250. this.list[this.dragIndex].x = this.currentPosition.x + 0.001;
  251. } else if (this.direction === 'vertical' || this.direction === 'all') {
  252. this.list[this.dragIndex].y = this.currentPosition.y + 0.001;
  253. this.list[this.dragIndex].x = this.currentPosition.x + 0.001;
  254. }
  255. // 重置到位置,需要延迟触发动,否则无效。
  256. sleep(50).then(() => {
  257. this.list.forEach((item, index) => {
  258. item.x = this.originalPositions[index].x;
  259. item.y = this.originalPositions[index].y;
  260. });
  261. this.dragIndex = -1;
  262. this.$emit('drag-end', [...this.list]);
  263. });
  264. }
  265. },
  266. watch: {
  267. initialList: {
  268. handler() {
  269. this.$nextTick(() => {
  270. this.initList();
  271. });
  272. },
  273. deep: true
  274. },
  275. direction: {
  276. handler() {
  277. this.$nextTick(() => {
  278. this.initList();
  279. this.calculateItemSize();
  280. this.calculateAreaSize();
  281. });
  282. }
  283. },
  284. columns: {
  285. handler() {
  286. if (this.direction === 'all') {
  287. this.$nextTick(() => {
  288. this.initList();
  289. this.updatePositions();
  290. this.saveOriginalPositions();
  291. });
  292. }
  293. }
  294. }
  295. }
  296. };
  297. </script>
  298. <style scoped lang="scss">
  299. .u-dragsort {
  300. width: 100%;
  301. .u-dragsort-area {
  302. width: 100%;
  303. position: relative;
  304. }
  305. .u-dragsort-item {
  306. position: absolute;
  307. width: 100%;
  308. &.dragging {
  309. z-index: 1000;
  310. box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
  311. }
  312. .u-dragsort-item-content {
  313. padding: 0px;
  314. text-align: center;
  315. box-sizing: border-box;
  316. padding-bottom: 6px;
  317. border-radius: 8rpx;
  318. transition: all 0.3s ease;
  319. }
  320. }
  321. &.u-dragsort--horizontal {
  322. .u-dragsort-area {
  323. display: flex;
  324. white-space: nowrap;
  325. height: auto;
  326. }
  327. .u-dragsort-item {
  328. display: flex;
  329. width: auto;
  330. height: 100%;
  331. }
  332. }
  333. &.u-dragsort--all {
  334. .u-dragsort-area {
  335. height: auto;
  336. }
  337. .u-dragsort-item {
  338. width: auto;
  339. height: auto;
  340. }
  341. }
  342. }
  343. </style>