pull.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. <template>
  2. <view class="app-content">
  3. <view v-if="!$util.isEmpty(list.data)" style="padding-bottom:120upx;">
  4. <view class="sort-list-wrap">
  5. <block v-for="(item, index) in list.data" :key="item.id">
  6. <view
  7. class="sort-item"
  8. :class="{
  9. dragging: draggedIndex === index,
  10. 'target-position': targetIndex === index && isDragging && targetIndex !== draggedIndex
  11. }"
  12. :style="{
  13. transform: transformStyle(index),
  14. transition: isDragging ? 'none' : 'transform 0.3s ease',
  15. zIndex: draggedIndex === index ? 999 : targetIndex === index && isDragging ? 888 : 1
  16. }"
  17. >
  18. <!-- 可拖拽的内容区域 -->
  19. <view
  20. class="item-content"
  21. :data-index="index"
  22. :data-id="item.id"
  23. @touchstart="dragStart"
  24. @touchmove="dragMove"
  25. @touchend="dragEnd"
  26. style="width: fit-content;"
  27. >
  28. <view class="item-name">{{ item.name }}</view>
  29. <view class="item-index">第{{ index + 1 }}个,按住名字可以拖拽</view>
  30. </view>
  31. <!-- 操作按钮区域 -->
  32. <view class="buttons-area">
  33. <!-- 置顶 -->
  34. <view
  35. v-if="index > 0"
  36. class="move-btn move-btn-top"
  37. :class="{ disabled: isOperating }"
  38. @click.stop="topFn(item.id)"
  39. title="置顶"
  40. >
  41. <zui-svg-icon icon="general-top-line" color="#fff" :width="20" :height="20" />
  42. </view>
  43. <!-- 置底 -->
  44. <view
  45. v-if="index < list.data.length - 1"
  46. class="move-btn move-btn-bottom"
  47. :class="{ disabled: isOperating }"
  48. @click.stop="bottomFn(item.id)"
  49. title="置底"
  50. >
  51. <zui-svg-icon icon="general-bottom-line" color="#fff" :width="20" :height="20" />
  52. </view>
  53. <!-- 上移 -->
  54. <text
  55. v-if="index > 0"
  56. class="move-btn move-btn-up"
  57. :class="{ disabled: isOperating }"
  58. @click.stop="upFn(item.id)"
  59. title="上移一位"
  60. >↑</text>
  61. <!-- 下移 -->
  62. <text
  63. v-if="index < list.data.length - 1"
  64. class="move-btn move-btn-down"
  65. :class="{ disabled: isOperating }"
  66. @click.stop="downFn(item.id)"
  67. title="下移一位"
  68. >↓</text>
  69. </view>
  70. </view>
  71. </block>
  72. </view>
  73. </view>
  74. <view v-else>
  75. <AppWrapperEmpty title="暂无数据" :is-empty="$util.isEmpty(list.data)" />
  76. </view>
  77. </view>
  78. </template>
  79. <script>
  80. import AppWrapperEmpty from "@/components/app-wrapper-empty";
  81. import { productList, batchChangeMore } from '@/api/product'
  82. import list from '@/mixins/list'
  83. export default {
  84. name: "more",
  85. components: {
  86. AppWrapperEmpty
  87. },
  88. mixins: [list],
  89. data() {
  90. return {
  91. // 拖拽相关状态
  92. draggedIndex: -1,
  93. draggedItem: null,
  94. dragOffsetY: 0,
  95. isDragging: false,
  96. itemHeight: 160, // 列表项高度,单位upx(实际高度=160upx)
  97. startY: 0,
  98. scrollTop: 0, // 页面滚动高度
  99. originalOrder: [], // 原始顺序,用于拖拽失败时恢复
  100. targetIndex: -1, // 目标交换位置的索引
  101. isOperating: false, // 防止快速点击操作按钮
  102. totalNum: 0 // 总数量
  103. };
  104. },
  105. onPageScroll(e) {
  106. // 拖拽期间禁止更新滚动高度
  107. if (!this.isDragging) {
  108. this.scrollTop = e.scrollTop;
  109. }
  110. },
  111. methods: {
  112. goBack() {
  113. uni.navigateBack()
  114. },
  115. // 响应 transform 样式变化
  116. transformStyle(index) {
  117. if (index === this.draggedIndex) {
  118. return 'translateY(0upx)';
  119. }
  120. return 'translateY(0upx)';
  121. },
  122. // 拖拽开始
  123. dragStart(e) {
  124. if (this.isDragging) return;
  125. this.isDragging = true;
  126. const index = parseInt(e.currentTarget.dataset.index);
  127. if (isNaN(index) || index < 0 || index >= this.list.data.length) {
  128. this.isDragging = false;
  129. return;
  130. }
  131. this.draggedIndex = index;
  132. this.draggedItem = this.list.data[index];
  133. this.startY = e.touches[0].clientY;
  134. this.targetIndex = index;
  135. // 保存原始顺序
  136. this.originalOrder = [...this.list.data];
  137. // 添加拖拽模式样式
  138. this.toggleDragMode(true);
  139. if (e.preventDefault) {
  140. e.preventDefault();
  141. }
  142. if (e.stopPropagation) {
  143. e.stopPropagation();
  144. }
  145. return false;
  146. },
  147. // 拖拽移动
  148. dragMove(e) {
  149. if (!this.isDragging || this.draggedIndex === -1) return;
  150. if (!e.touches || !e.touches[0]) return;
  151. if (e.preventDefault) {
  152. e.preventDefault();
  153. }
  154. if (e.stopPropagation) {
  155. e.stopPropagation();
  156. }
  157. const currentY = e.touches[0].clientY;
  158. const deltaY = currentY - this.startY;
  159. // 根据设备像素比例转换为upx单位
  160. const systemInfo = uni.getSystemInfoSync();
  161. const pixelRatio = 750 / systemInfo.windowWidth;
  162. const deltaYupx = deltaY * pixelRatio;
  163. // 限制拖拽范围
  164. const maxOffset = (this.list.data.length - 1 - this.draggedIndex) * this.itemHeight;
  165. const minOffset = -this.draggedIndex * this.itemHeight;
  166. const limitedOffset = Math.max(minOffset, Math.min(maxOffset, deltaYupx));
  167. this.dragOffsetY = limitedOffset;
  168. // 实时计算目标位置
  169. const moveItems = Math.round(limitedOffset / this.itemHeight);
  170. this.targetIndex = Math.max(0, Math.min(this.list.data.length - 1, this.draggedIndex + moveItems));
  171. },
  172. // 拖拽结束
  173. dragEnd() {
  174. if (!this.isDragging) return;
  175. // 计算最终位置
  176. const moveItems = Math.round(this.dragOffsetY / this.itemHeight);
  177. const targetIndex = Math.max(0, Math.min(this.list.data.length - 1, this.draggedIndex + moveItems));
  178. // 如果位置有变化,则更新数组
  179. if (targetIndex !== this.draggedIndex) {
  180. const newData = [...this.list.data];
  181. const draggedItem = newData[this.draggedIndex];
  182. newData.splice(this.draggedIndex, 1);
  183. newData.splice(targetIndex, 0, draggedItem);
  184. this.list.data = newData;
  185. // 拖拽结束后立即保存
  186. this.saveOrder();
  187. }
  188. // 移除拖拽模式样式
  189. this.toggleDragMode(false);
  190. // 重置拖拽状态
  191. this.draggedIndex = -1;
  192. this.draggedItem = null;
  193. this.dragOffsetY = 0;
  194. this.isDragging = false;
  195. this.startY = 0;
  196. this.originalOrder = [];
  197. this.targetIndex = -1;
  198. },
  199. // 切换拖拽模式
  200. toggleDragMode(isDragging) {
  201. // #ifdef H5
  202. if (isDragging) {
  203. document.body.classList.add('dragging-mode');
  204. } else {
  205. document.body.classList.remove('dragging-mode');
  206. }
  207. // #endif
  208. // #ifdef MP-WEIXIN
  209. if (typeof uni.setPageStyle !== 'function') {
  210. console.warn('uni.setPageStyle is not available on this platform.');
  211. return;
  212. }
  213. if (isDragging) {
  214. uni.setPageStyle({
  215. style: {
  216. overflow: 'hidden',
  217. position: 'fixed',
  218. width: '100%',
  219. top: `-${this.scrollTop}px`,
  220. left: '0',
  221. right: '0'
  222. }
  223. });
  224. } else {
  225. uni.setPageStyle({
  226. style: {
  227. overflow: '',
  228. position: '',
  229. width: '',
  230. top: '',
  231. left: '',
  232. right: ''
  233. }
  234. });
  235. uni.pageScrollTo({
  236. scrollTop: this.scrollTop,
  237. duration: 0
  238. });
  239. }
  240. // #endif
  241. // #ifdef APP-PLUS
  242. const webview = this.$scope.$getAppWebview();
  243. if (!webview) {
  244. console.warn('无法获取 webview。');
  245. return;
  246. }
  247. if (isDragging) {
  248. webview.evalJS(`
  249. var body = document.body;
  250. body.style.overflow = 'hidden';
  251. body.style.position = 'fixed';
  252. body.style.width = '100%';
  253. body.style.left = '0';
  254. body.style.right = '0';
  255. body.style.top = '-${this.scrollTop}px';
  256. `);
  257. } else {
  258. webview.evalJS(`
  259. var body = document.body;
  260. body.style.overflow = '';
  261. body.style.position = '';
  262. body.style.width = '';
  263. body.style.left = '';
  264. body.style.right = '';
  265. body.style.top = '';
  266. `);
  267. uni.pageScrollTo({
  268. scrollTop: this.scrollTop,
  269. duration: 0
  270. });
  271. }
  272. // #endif
  273. },
  274. // 上移
  275. upFn(id) {
  276. if (this.isOperating) return;
  277. this.isOperating = true;
  278. const index = this.list.data.findIndex((item) => item.id === id);
  279. if (index > 0) {
  280. const newData = [...this.list.data];
  281. const temp = newData[index];
  282. newData[index] = newData[index - 1];
  283. newData[index - 1] = temp;
  284. this.list.data = newData;
  285. // 操作后立即保存
  286. this.saveOrder();
  287. }
  288. setTimeout(() => {
  289. this.isOperating = false;
  290. }, 300);
  291. },
  292. // 下移
  293. downFn(id) {
  294. if (this.isOperating) return;
  295. this.isOperating = true;
  296. const index = this.list.data.findIndex((item) => item.id === id);
  297. if (index < this.list.data.length - 1) {
  298. const newData = [...this.list.data];
  299. const temp = newData[index];
  300. newData[index] = newData[index + 1];
  301. newData[index + 1] = temp;
  302. this.list.data = newData;
  303. // 操作后立即保存
  304. this.saveOrder();
  305. }
  306. setTimeout(() => {
  307. this.isOperating = false;
  308. }, 300);
  309. },
  310. // 置顶
  311. topFn(id) {
  312. if (this.isOperating) return;
  313. this.isOperating = true;
  314. const index = this.list.data.findIndex((item) => item.id === id);
  315. if (index > 0) {
  316. const newData = [...this.list.data];
  317. const targetItem = newData[index];
  318. newData.splice(index, 1);
  319. newData.unshift(targetItem);
  320. this.list.data = newData;
  321. // 操作后立即保存
  322. this.saveOrder();
  323. }
  324. setTimeout(() => {
  325. this.isOperating = false;
  326. }, 300);
  327. },
  328. // 置底
  329. bottomFn(id) {
  330. if (this.isOperating) return;
  331. this.isOperating = true;
  332. const index = this.list.data.findIndex((item) => item.id === id);
  333. if (index < this.list.data.length - 1) {
  334. const newData = [...this.list.data];
  335. const targetItem = newData[index];
  336. newData.splice(index, 1);
  337. newData.push(targetItem);
  338. this.list.data = newData;
  339. // 操作后立即保存
  340. this.saveOrder();
  341. }
  342. setTimeout(() => {
  343. this.isOperating = false;
  344. }, 300);
  345. },
  346. // 保存排序
  347. saveOrder() {
  348. let that = this;
  349. const sortData = this.list.data.map((item, index) => ({
  350. id: item.id,
  351. inTurn: this.list.data.length - index // 反向索引,第一个有最大值
  352. }));
  353. batchChangeMore({ addData: JSON.stringify(sortData) }).then(res => {
  354. if (res.code == 1) {
  355. //that.$msg('已保存')
  356. }
  357. })
  358. },
  359. async init() {
  360. const res = await productList({
  361. page: this.list.page,
  362. showPage: 1,
  363. pageSize: 2000,
  364. classId: this.option.classId
  365. });
  366. this.completes(res);
  367. this.totalNum = res.data && res.data.total ? Number(res.data.total) : this.list.data.length;
  368. }
  369. },
  370. async onPullDownRefresh() {
  371. this.resetList();
  372. await this.init();
  373. uni.stopPullDownRefresh();
  374. },
  375. async onReachBottom() {
  376. if (!this.list.finished) {
  377. await this.init();
  378. uni.stopPullDownRefresh();
  379. } else {
  380. uni.stopPullDownRefresh();
  381. }
  382. }
  383. };
  384. </script>
  385. <style lang="scss" scoped>
  386. .app-content {
  387. min-height: 100vh;
  388. padding-bottom: 20upx;
  389. }
  390. .sort-list-wrap {
  391. position: relative;
  392. background-color: #fff;
  393. .sort-item {
  394. height: 100upx;
  395. display: flex;
  396. align-items: center;
  397. padding: 12upx 0;
  398. margin: 0 30upx;
  399. color: #333;
  400. border-bottom: 1px solid #f0f0f0;
  401. position: relative;
  402. transition: all 0.3s ease;
  403. .item-content {
  404. display: flex;
  405. flex-direction: column;
  406. justify-content: center;
  407. width: auto;
  408. user-select: none;
  409. -webkit-user-select: none;
  410. touch-action: none;
  411. -webkit-touch-callout: none;
  412. .item-name {
  413. font-size: 28upx;
  414. font-weight: bold;
  415. color: #000;
  416. margin-bottom: 4upx;
  417. white-space: nowrap;
  418. }
  419. .item-index {
  420. font-size: 22upx;
  421. color: #999;
  422. white-space: nowrap;
  423. }
  424. }
  425. .buttons-area {
  426. display: flex;
  427. gap: 24upx;
  428. align-items: center;
  429. padding-left: 20upx;
  430. pointer-events: auto;
  431. margin-left: auto;
  432. }
  433. }
  434. }
  435. .move-btn {
  436. position: relative;
  437. display: inline-flex;
  438. align-items: center;
  439. justify-content: center;
  440. width: 50upx;
  441. height: 50upx;
  442. color: #fff;
  443. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  444. border: 2px solid rgba(255, 255, 255, 0.3);
  445. border-radius: 50%;
  446. font-size: 24upx;
  447. font-weight: bold;
  448. box-shadow: 0 3upx 10upx rgba(102, 126, 234, 0.25);
  449. transition: all 0.2s ease;
  450. z-index: 10;
  451. cursor: pointer;
  452. flex-shrink: 0;
  453. /* #ifdef MP-WEIXIN || APP-PLUS || MP-ALIPAY */
  454. user-select: none;
  455. -webkit-user-select: none;
  456. -webkit-touch-callout: none;
  457. /* #endif */
  458. /* 触摸反馈 */
  459. &:active {
  460. transform: scale(0.92);
  461. box-shadow: 0 2upx 6upx rgba(102, 126, 234, 0.4);
  462. }
  463. /* 悬停效果 - 仅在 H5 显示 */
  464. /* #ifdef H5 */
  465. &:hover:not(.disabled) {
  466. transform: translateY(-2upx);
  467. box-shadow: 0 4upx 15upx rgba(102, 126, 234, 0.35);
  468. }
  469. /* #endif */
  470. }
  471. /* 置顶按钮 - 黄色渐变 */
  472. .move-btn-top {
  473. background: linear-gradient(135deg, #feca57 0%, #ff9ff3 100%);
  474. box-shadow: 0 3upx 10upx rgba(254, 202, 87, 0.25);
  475. &:active {
  476. box-shadow: 0 2upx 6upx rgba(254, 202, 87, 0.4);
  477. }
  478. /* #ifdef H5 */
  479. &:hover:not(.disabled) {
  480. box-shadow: 0 4upx 15upx rgba(254, 202, 87, 0.35);
  481. }
  482. /* #endif */
  483. }
  484. /* 置底按钮 - 黄色渐变 */
  485. .move-btn-bottom {
  486. background: linear-gradient(135deg, #feca57 0%, #ff9ff3 100%);
  487. box-shadow: 0 3upx 10upx rgba(254, 202, 87, 0.25);
  488. &:active {
  489. box-shadow: 0 2upx 6upx rgba(254, 202, 87, 0.4);
  490. }
  491. /* #ifdef H5 */
  492. &:hover:not(.disabled) {
  493. box-shadow: 0 4upx 15upx rgba(254, 202, 87, 0.35);
  494. }
  495. /* #endif */
  496. }
  497. /* 上移按钮 - 青色渐变 */
  498. .move-btn-up {
  499. background: linear-gradient(135deg, #48dbfb 0%, #0abde3 100%);
  500. box-shadow: 0 3upx 10upx rgba(72, 219, 251, 0.25);
  501. &:active {
  502. box-shadow: 0 2upx 6upx rgba(72, 219, 251, 0.4);
  503. }
  504. /* #ifdef H5 */
  505. &:hover:not(.disabled) {
  506. box-shadow: 0 4upx 15upx rgba(72, 219, 251, 0.35);
  507. }
  508. /* #endif */
  509. }
  510. /* 下移按钮 - 青色渐变 */
  511. .move-btn-down {
  512. background: linear-gradient(135deg, #48dbfb 0%, #0abde3 100%);
  513. box-shadow: 0 3upx 10upx rgba(72, 219, 251, 0.25);
  514. &:active {
  515. box-shadow: 0 2upx 6upx rgba(72, 219, 251, 0.4);
  516. }
  517. /* #ifdef H5 */
  518. &:hover:not(.disabled) {
  519. box-shadow: 0 4upx 15upx rgba(72, 219, 251, 0.35);
  520. }
  521. /* #endif */
  522. }
  523. /* 按钮禁用状态 */
  524. .move-btn.disabled {
  525. opacity: 0.5;
  526. transform: none !important;
  527. box-shadow: 0 2upx 6upx rgba(0, 0, 0, 0.08) !important;
  528. pointer-events: none;
  529. cursor: not-allowed;
  530. &:hover,
  531. &:active {
  532. transform: none !important;
  533. box-shadow: 0 2upx 6upx rgba(0, 0, 0, 0.08) !important;
  534. }
  535. }
  536. // 拖拽相关样式
  537. .sort-list-wrap .sort-item.dragging {
  538. border: 2px dashed #007aff !important;
  539. box-shadow: 0 8upx 40upx rgba(0, 122, 255, 0.25) !important;
  540. background-color: rgba(0, 122, 255, 0.08) !important;
  541. opacity: 0.9;
  542. border-radius: 16upx;
  543. transform: scale(1.02);
  544. transition: none !important; /* 拖拽时禁用过渡动画 */
  545. }
  546. .sort-list-wrap .sort-item.target-position {
  547. border: 3px solid #52c41a !important; /* 使用绿色边框区别于拖拽元素 */
  548. box-shadow: 0 6upx 30upx rgba(82, 196, 26, 0.3) !important;
  549. background-color: rgba(82, 196, 26, 0.1) !important;
  550. opacity: 0.95;
  551. border-radius: 16upx;
  552. transform: scale(1.01);
  553. transition: all 0.2s ease !important; /* 目标位置保持平滑过渡 */
  554. /* 添加一个内部指示器 */
  555. &::before {
  556. content: '移动到这儿';
  557. position: absolute;
  558. top: 50%;
  559. left: 50%;
  560. transform: translate(-50%, -50%);
  561. padding: 8upx 22upx;
  562. background: linear-gradient(90deg, #52c41a, #73d13d);
  563. border-radius: 8upx;
  564. color: #fff;
  565. font-size: 26upx;
  566. font-weight: bold;
  567. opacity: 0.9;
  568. z-index: 99990;
  569. }
  570. }
  571. /* 防止拖拽时页面滚动 */
  572. .dragging-mode {
  573. overflow: hidden !important;
  574. touch-action: none !important;
  575. -webkit-overflow-scrolling: auto !important;
  576. }
  577. </style>