goodsSort.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <template>
  2. <view class="app-content">
  3. <view class="list-wrap">
  4. <block v-if="!$util.isEmpty(list.data)">
  5. <block v-for="(item, index) in list.data" :key="item.id">
  6. <view
  7. class="list"
  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. <view class="list-img">
  18. <view v-if="item.status == 0" class="sold-out">
  19. <view class="sold-out-xj">已下架</view>
  20. </view>
  21. <image :src="item.cover" mode="aspectFit" ></image>
  22. </view>
  23. <view class="list-det" style="position:absolute;top:10upx;left:125upx;">
  24. <view class="app-size-28 app-color-0" style="font-size:28upx;font-weight:bold;">{{ item.name || item.goodsName}}</view>
  25. </view>
  26. <view class="list-det" style="position:absolute;bottom:10upx;left:122upx;">
  27. <view v-if="item.priceType == 1" class="app-size-28 app-color-0" style="font-size:24upx;">¥{{ item.price}}</view>
  28. <view v-else class="app-size-28 app-color-0" style="font-size:24upx;border:1px solid #ccc;border-radius:30upx;padding:3upx 15upx;">无价格</view>
  29. </view>
  30. <text v-if="index > 0" style="right:375upx;" class="move-btn move-btn-top" :class="{ 'disabled': isOperating }" @click.stop="topFn(item.id)" title="置顶">⇈</text>
  31. <text v-if="index < list.data.length - 1" style="right:290upx;" class="move-btn move-btn-bottom" :class="{ 'disabled': isOperating }" @click.stop="bottomFn(item.id)" title="置底">⇊</text>
  32. <text style="right:205upx;" class="move-btn move-btn-up" :class="{ 'disabled': isOperating }" @click.stop="upFn(item.id)" title="上移一位">↑</text>
  33. <text style="right:120upx;" class="move-btn move-btn-down" :class="{ 'disabled': isOperating }" @click.stop="downFn(item.id)" title="下移一位">↓</text>
  34. <view
  35. style="position:absolute;top:50upx;right:14upx;cursor:move;"
  36. class="drag-handle"
  37. :data-index="index"
  38. :data-id="item.id"
  39. @touchstart="dragStart"
  40. @touchmove="dragMove"
  41. @touchend="dragEnd">
  42. <text style="color:#666666;font-size:48upx;padding:8upx 6upx;line-height:1;">≡</text>
  43. </view>
  44. </view>
  45. </block>
  46. </block>
  47. <block v-else>
  48. <app-wrapper-empty title="暂无数据" :is-empty="$util.isEmpty(list.data)" />
  49. </block>
  50. </view>
  51. </view>
  52. </template>
  53. <script>
  54. import AppWrapperEmpty from '@/components/app-wrapper-empty'
  55. import { list } from '@/mixins'
  56. import { getListB, sort } from '@/api/goods'
  57. export default {
  58. name: 'goodsSort',
  59. components: {
  60. AppWrapperEmpty
  61. },
  62. mixins: [list],
  63. data() {
  64. return {
  65. draggedIndex: -1,
  66. draggedItem: null,
  67. dragOffsetY: 0,
  68. isDragging: false,
  69. itemHeight: 160, // 列表项高度,单位upx(与CSS中的height保持一致)
  70. startY: 0,
  71. scrollTop: 0, // 页面滚动高度
  72. originalOrder: [], // 原始顺序,用于拖拽失败时恢复
  73. targetIndex: -1, // 目标交换位置的索引
  74. isOperating: false, // 防止快速点击操作按钮
  75. totalNum: 0, // 总数量
  76. }
  77. },
  78. onPageScroll(e) {
  79. // 拖拽期间禁止更新滚动高度
  80. if (!this.isDragging) {
  81. this.scrollTop = e.scrollTop
  82. }
  83. },
  84. onPullDownRefresh() {
  85. this.resetList()
  86. this._list().then(res => {
  87. uni.stopPullDownRefresh()
  88. }).catch(err => {
  89. console.error('下拉刷新失败:', err)
  90. uni.stopPullDownRefresh()
  91. })
  92. },
  93. onReachBottom() {
  94. if (!this.list.finished) {
  95. this._list().then(res => {
  96. uni.stopPullDownRefresh()
  97. })
  98. } else {
  99. uni.stopPullDownRefresh()
  100. }
  101. },
  102. methods: {
  103. async init() {
  104. // 确保list.data是数组
  105. if (!Array.isArray(this.list.data)) {
  106. this.list.data = []
  107. }
  108. this._list()
  109. },
  110. _list() {
  111. // 传递分类ID参数和分页参数到API
  112. const params = {
  113. cId: this.option.id,
  114. page: this.list.page,
  115. pageSize: this.list.pageSize
  116. }
  117. return getListB(params).then(res => {
  118. // 使用mixin提供的completes方法处理分页数据
  119. this.completes(res)
  120. this.totalNum = Number(res.data.total)
  121. }).catch(err => {
  122. console.error('API请求失败:', err)
  123. this.list.loading = false
  124. })
  125. },
  126. // 拖拽相关方法
  127. transformStyle(index) {
  128. if (index === this.draggedIndex) {
  129. return `translateY(${this.dragOffsetY}upx)`
  130. }
  131. return 'translateY(0upx)'
  132. },
  133. dragStart(e) {
  134. // 防止重复触发
  135. if (this.isDragging) return
  136. this.isDragging = true
  137. const index = parseInt(e.currentTarget.dataset.index)
  138. if (isNaN(index) || index < 0 || index >= this.list.data.length) {
  139. this.isDragging = false
  140. return
  141. }
  142. this.draggedIndex = index
  143. this.draggedItem = this.list.data[index]
  144. this.startY = e.touches[0].clientY
  145. this.targetIndex = index // 初始化目标位置为当前位置
  146. // 保存原始顺序,用于拖拽失败时恢复
  147. this.originalOrder = [...this.list.data]
  148. // 添加拖拽模式样式,防止页面滚动
  149. this.toggleDragMode(true)
  150. // 阻止默认行为,防止页面滚动(兼容微信端与H5)
  151. if (e.preventDefault) {
  152. e.preventDefault()
  153. }
  154. if (e.stopPropagation) {
  155. e.stopPropagation()
  156. }
  157. return false
  158. },
  159. dragMove(e) {
  160. if (!this.isDragging || this.draggedIndex === -1) return
  161. // 验证触摸点是否存在
  162. if (!e.touches || !e.touches[0]) return
  163. // 阻止默认行为,防止页面滚动(兼容微信端与H5)
  164. if (e.preventDefault) {
  165. e.preventDefault()
  166. }
  167. if (e.stopPropagation) {
  168. e.stopPropagation()
  169. }
  170. const currentY = e.touches[0].clientY
  171. const deltaY = currentY - this.startY
  172. // 根据设备像素比例转换为upx单位,确保在不同设备上表现一致
  173. const systemInfo = uni.getSystemInfoSync()
  174. const pixelRatio = 750 / systemInfo.windowWidth
  175. const deltaYupx = deltaY * pixelRatio
  176. // 限制拖拽范围,防止超出列表边界
  177. const maxOffset = (this.list.data.length - 1 - this.draggedIndex) * this.itemHeight
  178. const minOffset = -this.draggedIndex * this.itemHeight
  179. const limitedOffset = Math.max(minOffset, Math.min(maxOffset, deltaYupx))
  180. this.dragOffsetY = limitedOffset
  181. // 实时计算目标位置,让用户看到将要交换的元素
  182. const moveItems = Math.round(limitedOffset / this.itemHeight)
  183. this.targetIndex = Math.max(0, Math.min(this.list.data.length - 1, this.draggedIndex + moveItems))
  184. },
  185. dragEnd() {
  186. if (!this.isDragging) return
  187. // 计算最终位置
  188. const moveItems = Math.round(this.dragOffsetY / this.itemHeight)
  189. const targetIndex = Math.max(0, Math.min(this.list.data.length - 1, this.draggedIndex + moveItems))
  190. // 如果位置有变化,则更新数组
  191. if (targetIndex !== this.draggedIndex) {
  192. const newData = [...this.list.data]
  193. const draggedItem = newData[this.draggedIndex]
  194. // 移除拖拽项
  195. newData.splice(this.draggedIndex, 1)
  196. // 插入到新位置
  197. newData.splice(targetIndex, 0, draggedItem)
  198. this.list.data = newData
  199. // 更新排序值并发送到后端
  200. this.updateSortOrder()
  201. }
  202. // 移除拖拽模式样式
  203. this.toggleDragMode(false)
  204. // 重置拖拽状态
  205. this.draggedIndex = -1
  206. this.draggedItem = null
  207. this.dragOffsetY = 0
  208. this.isDragging = false
  209. this.startY = 0
  210. this.originalOrder = []
  211. this.targetIndex = -1 // 重置目标位置
  212. },
  213. updateSortOrder() {
  214. // 更新每个项目的inTurn值,从大到小排序(值越大越靠前)
  215. const sortData = this.list.data.map((item, index) => ({
  216. id: item.id,
  217. inTurn: this.totalNum - index // 反向索引,使第一个项目有最大值
  218. }))
  219. // console.log('排序数据:', sortData)
  220. // 调用API更新排序
  221. sort({ cId: this.option.id, items: sortData }).then(res => {
  222. if (res.code === 1) {
  223. this.$msg('排序更新成功')
  224. } else {
  225. this.$msg('排序更新失败')
  226. // 恢复原始顺序
  227. this.list.data = this.originalOrder
  228. }
  229. }).catch(err => {
  230. console.error('更新排序失败:', err)
  231. this.$msg('排序更新失败')
  232. // 恢复原始顺序
  233. this.list.data = this.originalOrder
  234. })
  235. },
  236. // 切换拖拽模式,防止页面滚动
  237. toggleDragMode(isDragging) {
  238. // #ifdef H5
  239. // H5平台:直接操作body样式
  240. if (isDragging) {
  241. document.body.classList.add('dragging-mode')
  242. } else {
  243. document.body.classList.remove('dragging-mode')
  244. }
  245. // #endif
  246. // #ifndef H5
  247. // 微信小程序、App:使用页面样式和滚动API
  248. if (isDragging) {
  249. uni.setPageStyle({
  250. style: {
  251. 'overflow': 'hidden',
  252. 'position': 'fixed',
  253. 'width': '100%',
  254. 'top': `-${this.scrollTop}px`,
  255. 'left': '0',
  256. 'right': '0'
  257. }
  258. })
  259. } else {
  260. uni.setPageStyle({
  261. style: {
  262. 'overflow': '',
  263. 'position': '',
  264. 'width': '',
  265. 'top': '',
  266. 'left': '',
  267. 'right': ''
  268. }
  269. })
  270. uni.pageScrollTo({
  271. scrollTop: this.scrollTop,
  272. duration: 0
  273. })
  274. }
  275. // #endif
  276. },
  277. // 原有的上移下移方法
  278. upFn(id) {
  279. if (this.isOperating) return
  280. this.isOperating = true
  281. const index = this.list.data.findIndex(item => item.id === id)
  282. if (index > 0) {
  283. const newData = [...this.list.data]
  284. const temp = newData[index]
  285. newData[index] = newData[index - 1]
  286. newData[index - 1] = temp
  287. this.list.data = newData
  288. this.updateSortOrder()
  289. }
  290. // 防抖延迟
  291. setTimeout(() => {
  292. this.isOperating = false
  293. }, 300)
  294. },
  295. downFn(id) {
  296. if (this.isOperating) return
  297. this.isOperating = true
  298. const index = this.list.data.findIndex(item => item.id === id)
  299. if (index < this.list.data.length - 1) {
  300. const newData = [...this.list.data]
  301. const temp = newData[index]
  302. newData[index] = newData[index + 1]
  303. newData[index + 1] = temp
  304. this.list.data = newData
  305. this.updateSortOrder()
  306. }
  307. // 防抖延迟
  308. setTimeout(() => {
  309. this.isOperating = false
  310. }, 300)
  311. },
  312. // 置顶
  313. topFn(id) {
  314. if (this.isOperating) return
  315. this.isOperating = true
  316. const index = this.list.data.findIndex(item => item.id === id)
  317. if (index > 0) {
  318. const newData = [...this.list.data]
  319. const targetItem = newData[index]
  320. // 将目标项移到第一位
  321. newData.splice(index, 1)
  322. newData.unshift(targetItem)
  323. this.list.data = newData
  324. this.updateSortOrder()
  325. }
  326. // 防抖延迟
  327. setTimeout(() => {
  328. this.isOperating = false
  329. }, 300)
  330. },
  331. // 置底
  332. bottomFn(id) {
  333. if (this.isOperating) return
  334. this.isOperating = true
  335. const index = this.list.data.findIndex(item => item.id === id)
  336. if (index < this.list.data.length - 1) {
  337. const newData = [...this.list.data]
  338. const targetItem = newData[index]
  339. // 将目标项移到最后一位
  340. newData.splice(index, 1)
  341. newData.push(targetItem)
  342. this.list.data = newData
  343. this.updateSortOrder()
  344. }
  345. // 防抖延迟
  346. setTimeout(() => {
  347. this.isOperating = false
  348. }, 300)
  349. }
  350. }
  351. }
  352. </script>
  353. <style lang="scss" scoped>
  354. .app-content {
  355. min-height: calc(100vh - 100upx);
  356. padding-bottom: 100upx;
  357. }
  358. .list-wrap {
  359. position: relative;
  360. background-color: #fff;
  361. .list {
  362. height: 160upx;
  363. @include disFlex(center, flex-start);
  364. padding: 30upx 0;
  365. margin: 0 30upx;
  366. color: $fontColor3;
  367. border-bottom: 1px solid $borderColor;
  368. position:relative;
  369. .list-img {
  370. width: 130upx;
  371. height: 130upx;
  372. image {
  373. width:130upx;
  374. height:130upx;
  375. border-radius: 20upx;
  376. }
  377. .sold-out{
  378. z-index:10;
  379. width:130upx;
  380. height:52upx; /* 130 * (2/5) = 52upx */
  381. background-color:black;
  382. position:absolute;
  383. top:92upx;
  384. left:0;
  385. opacity: 0.48;
  386. color:white;
  387. text-align:center;
  388. border-radius: 0 0 20upx 20upx;
  389. .sold-out-xj{
  390. line-height:52upx;
  391. font-size:24upx;
  392. font-weight: bold;
  393. }
  394. }
  395. }
  396. .list-det {
  397. margin-left: 20upx;
  398. & > div {
  399. margin-top: 14upx;
  400. &:first-child {
  401. margin-top: 0;
  402. }
  403. }
  404. }
  405. }
  406. }
  407. .move-btn {
  408. position: absolute;
  409. top: 60upx;
  410. display: inline-flex;
  411. align-items: center;
  412. justify-content: center;
  413. width: 60upx;
  414. height: 60upx;
  415. color: #fff;
  416. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  417. border: 2px solid rgba(255, 255, 255, 0.3);
  418. border-radius: 50%;
  419. font-size: 28upx; /* 增大20% */
  420. font-weight: bold;
  421. box-shadow: 0 4upx 12upx rgba(102, 126, 234, 0.3);
  422. transition: all 0.2s ease;
  423. z-index: 9999;
  424. cursor: pointer;
  425. /* 触摸反馈 */
  426. &:active {
  427. transform: scale(0.95);
  428. box-shadow: 0 2upx 8upx rgba(102, 126, 234, 0.5);
  429. }
  430. /* 悬停效果 */
  431. &:hover {
  432. transform: translateY(-2upx);
  433. box-shadow: 0 6upx 20upx rgba(102, 126, 234, 0.4);
  434. }
  435. }
  436. /* 置顶按钮 - 红色渐变 */
  437. .move-btn-top {
  438. // background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
  439. // box-shadow: 0 4upx 12upx rgba(255, 107, 107, 0.3);
  440. background: linear-gradient(135deg, #feca57 0%, #ff9ff3 100%);
  441. box-shadow: 0 4upx 12upx rgba(254, 202, 87, 0.3);
  442. &:hover {
  443. box-shadow: 0 6upx 20upx rgba(255, 107, 107, 0.4);
  444. }
  445. &:active {
  446. box-shadow: 0 2upx 8upx rgba(255, 107, 107, 0.5);
  447. }
  448. }
  449. /* 置底按钮 - 橙色渐变 */
  450. .move-btn-bottom {
  451. background: linear-gradient(135deg, #feca57 0%, #ff9ff3 100%);
  452. box-shadow: 0 4upx 12upx rgba(254, 202, 87, 0.3);
  453. &:hover {
  454. box-shadow: 0 6upx 20upx rgba(254, 202, 87, 0.4);
  455. }
  456. &:active {
  457. box-shadow: 0 2upx 8upx rgba(254, 202, 87, 0.5);
  458. }
  459. }
  460. /* 上移按钮 - 绿色渐变 */
  461. .move-btn-up {
  462. background: linear-gradient(135deg, #48dbfb 0%, #0abde3 100%);
  463. box-shadow: 0 4upx 12upx rgba(72, 219, 251, 0.3);
  464. &:hover {
  465. box-shadow: 0 6upx 20upx rgba(72, 219, 251, 0.4);
  466. }
  467. &:active {
  468. box-shadow: 0 2upx 8upx rgba(72, 219, 251, 0.5);
  469. }
  470. }
  471. /* 下移按钮 - 紫色渐变 */
  472. .move-btn-down {
  473. // background: linear-gradient(135deg, #a55eea 0%, #8854d0 100%);
  474. // box-shadow: 0 4upx 12upx rgba(165, 94, 234, 0.3);
  475. background: linear-gradient(135deg, #48dbfb 0%, #0abde3 100%);
  476. box-shadow: 0 4upx 12upx rgba(72, 219, 251, 0.3);
  477. &:hover {
  478. box-shadow: 0 6upx 20upx rgba(165, 94, 234, 0.4);
  479. }
  480. &:active {
  481. box-shadow: 0 2upx 8upx rgba(165, 94, 234, 0.5);
  482. }
  483. }
  484. /* 按钮禁用状态 */
  485. .move-btn.disabled {
  486. opacity: 0.5;
  487. transform: none !important;
  488. box-shadow: 0 2upx 6upx rgba(0, 0, 0, 0.1) !important;
  489. pointer-events: none;
  490. cursor: not-allowed;
  491. &:hover, &:active {
  492. transform: none !important;
  493. box-shadow: 0 2upx 6upx rgba(0, 0, 0, 0.1) !important;
  494. }
  495. }
  496. // 拖拽相关样式
  497. .list.dragging {
  498. border: 2px dashed #007AFF !important;
  499. box-shadow: 0 8upx 40upx rgba(0, 122, 255, 0.25) !important;
  500. background-color: rgba(0, 122, 255, 0.08) !important;
  501. opacity: 0.9;
  502. border-radius: 16upx;
  503. transform: scale(1.02);
  504. transition: none !important; /* 拖拽时禁用过渡动画 */
  505. }
  506. .list.target-position {
  507. border: 3px solid #52C41A !important; /* 使用绿色边框区别于拖拽元素 */
  508. box-shadow: 0 6upx 30upx rgba(82, 196, 26, 0.3) !important;
  509. background-color: rgba(82, 196, 26, 0.1) !important;
  510. opacity: 0.95;
  511. border-radius: 16upx;
  512. transform: scale(1.01);
  513. transition: all 0.2s ease !important; /* 目标位置保持平滑过渡 */
  514. /* 添加一个内部指示器 */
  515. &::before {
  516. content: '移动到这儿';
  517. position: absolute;
  518. top: 50%;
  519. left: 50%;
  520. transform: translate(-50%, -50%);
  521. padding: 8upx 22upx;
  522. background: linear-gradient(90deg, #52C41A, #73D13D);
  523. border-radius: 8upx;
  524. color: #fff;
  525. font-size: 26upx;
  526. font-weight: bold;
  527. opacity: 0.9;
  528. z-index: 99990;
  529. }
  530. }
  531. .drag-handle {
  532. display: flex;
  533. align-items: center;
  534. justify-content: center;
  535. width: 80upx;
  536. height: 75upx;
  537. border-radius: 12upx;
  538. background-color: rgba(0, 0, 0, 0.03);
  539. transition: all 0.2s ease;
  540. -webkit-touch-callout: none; /* 禁用长按菜单 */
  541. -webkit-user-select: none; /* 禁用文本选择 */
  542. user-select: none;
  543. touch-action: none; /* 禁用浏览器默认触摸行为 */
  544. /* 增加触摸反馈 */
  545. &:active {
  546. transform: scale(1.1);
  547. background-color: rgba(0, 122, 255, 0.1);
  548. box-shadow: 0 4upx 16upx rgba(0, 122, 255, 0.2);
  549. }
  550. }
  551. /* 防止拖拽时页面滚动 */
  552. .dragging-mode {
  553. overflow: hidden !important;
  554. touch-action: none !important;
  555. -webkit-overflow-scrolling: auto !important;
  556. }
  557. </style>