dragSort.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <view>
  3. <view class="dragSortBox">
  4. <view class="dragSortBox-btns">
  5. <template v-if="isEdit">
  6. <text class="dragSortBox-btn" @click="toggleEdit('finish')">完成</text>
  7. <!-- <text class="dragSortBox-btn" @click="toggleEdit('cancel')">取消</text> -->
  8. </template>
  9. <text v-else class="dragSortBox-btn" @click="toggleEdit('edit')">编辑</text>
  10. </view>
  11. <movable-area id="dragSortArea" class="dragSort-area" :style="{height: boxHeight + 'px'}">
  12. <block v-for="(item,index) in cloneList" :key="item.id" >
  13. <movable-view class="dragSort-view" direction="all"
  14. :class="{'is-touched': item.isTouched}"
  15. :x="item.x" :y="item.y"
  16. :damping="40"
  17. :disabled="!isEdit"
  18. @change="onChange($event, item)"
  19. @touchstart="onTouchstart(item)"
  20. @touchend="onTouchend(item, index)"
  21. :style="{width: columnWidth + 'px', height: rpxTopx(rowHeight) + 'px', zIndex: item.zIndex}">
  22. <view class="dragSort-view__con" :class="isEdit?'shake':''" @click="handleDel(item)">
  23. <view class="dragSort-view__label is-ellipsis">{{item[label]}}</view>
  24. <text class="dragSort-view__btn-del" v-if="isEdit">X</text>
  25. </view>
  26. </movable-view>
  27. </block>
  28. </movable-area>
  29. </view>
  30. </view>
  31. </template>
  32. <script>
  33. export default {
  34. data() {
  35. return {
  36. cloneList: [], //用来展示的数据列表
  37. cacheList: [], //用来在点击“编辑”文字按钮的时候,将当前list的数据缓存,以便在取消的时候用到
  38. positionList: [], //用来存储xy坐标定位的列表
  39. columnWidth: 0, //列宽,单位px
  40. rowNum: 1, //行数
  41. boxHeight: 10, //可拖动区域的高度,单位px
  42. windowWidth: 750, //系统获取到的窗口宽度,单位px
  43. curTouchPostionIndex: 0, //当前操作的移动块在positionList队列里的索引
  44. xMoveUnit: 0, //沿x轴移动时的单位距离,单位px
  45. yMoveUnit: 0, //沿y轴移动时的单位距离,单位px
  46. clearT: '', //onChange事件中使用
  47. clearF: '', //点击“完成”文字按钮时使用
  48. isEdit: false, //是否在编辑状态
  49. type:null
  50. }
  51. },
  52. props: { //props里属性Number的单位都为rpx,在操作的时候需要用rpxTopx进行转换
  53. list: { //源数据列表
  54. type: Array,
  55. default () {
  56. return [{
  57. "name": "互联网",
  58. "id": 1, //id必传且唯一
  59. }, {
  60. "name": "古董",
  61. "id": 20
  62. }]
  63. }
  64. },
  65. label: { //list队列中的对象中要用来展示的key名
  66. type: String,
  67. default: 'name'
  68. },
  69. rowHeight: { //行高,单位rpx
  70. type: Number,
  71. default: 60
  72. },
  73. rowSpace: { //行间距,单位rpx
  74. type: Number,
  75. default: 15
  76. },
  77. columnSpace: { //列间距,单位rpx
  78. type: Number,
  79. default: 15
  80. },
  81. columnNum: { //列数
  82. type: Number,
  83. default: 4
  84. },
  85. zIndex: { //可移动项的默认z-index
  86. type: Number,
  87. default: 100
  88. }
  89. },
  90. computed: {
  91. btnText() {
  92. return this.isEdit ? '完成' : '编辑'
  93. }
  94. },
  95. created() {
  96. this.windowWidth = uni.getSystemInfoSync().windowWidth;
  97. },
  98. mounted() {
  99. const query = uni.createSelectorQuery().in(this);
  100. query.select('#dragSortArea').boundingClientRect(data => {
  101. // console.log("得到布局位置信息" + JSON.stringify(data));
  102. this.columnWidth = (data.width - (this.columnNum - 1) * this.rpxTopx(this.columnSpace)) / this.columnNum
  103. this.handleListData();
  104. }).exec();
  105. },
  106. methods: {
  107. /* 切换编辑状态
  108. * [type] String 参数状态
  109. */
  110. toggleEdit(type) {
  111. if(type == 'finish') { //点击“完成”
  112. this.isEdit = false;
  113. // console.log(this.getSortedIdArr(), 'sortedIdArr')
  114. }else if(type == 'cancel'){ //点击“取消”,将数据恢复到最近一次编辑时的状态
  115. this.isEdit = false;
  116. this.updateList(this.cacheList);
  117. }else if(type == 'edit'){ //点击“编辑”
  118. this.isEdit = true;
  119. this.cacheList = JSON.parse(JSON.stringify(this.list));
  120. }
  121. this.$emit("complete",type)
  122. },
  123. /* 更新父组件的list,并重新渲染布局
  124. * 有改变数组长度的操作内才需要调用此方法进行重新渲染布局进行更新,
  125. * 否则直接$emit('update:list')进行更新,无须调用此方法
  126. */
  127. updateList(arr) {
  128. this.$emit('update:list', arr);
  129. setTimeout(() => {
  130. this.handleListData()
  131. }, 100)
  132. },
  133. /* 删除某项 */
  134. handleDel(obj) {
  135. if(this.isEdit == true){
  136. uni.showLoading({
  137. title:"",
  138. mask:true
  139. })
  140. uni.$u.http.delete('/system/subscribed/remove/'+obj.id).then(res=>{
  141. for(var i = 0, len = this.list.length; i < len; i++) {
  142. var item = this.list[i];
  143. let qwe = {
  144. data:item,
  145. index:i
  146. }
  147. if(obj.id == item.id) {
  148. uni.hideLoading()
  149. var theList = JSON.parse(JSON.stringify(this.list));
  150. theList.splice(i, 1);
  151. this.updateList(theList);
  152. this.$emit('shanchu', qwe);
  153. break;
  154. }
  155. }
  156. for(let a =0;a<this.cacheList.length;a++){
  157. let qaz = this.cacheList[a];
  158. if(obj.id == qaz.id) {
  159. this.cacheList.splice(a, 1);
  160. break;
  161. }
  162. }
  163. }).catch(err=>{
  164. uni.showToast({
  165. title:'取消失败',
  166. icon:'none'
  167. })
  168. })
  169. }
  170. },
  171. /* 处理源数据列表,生成展示用的cloneList和positionList布局位置信息 */
  172. handleListData() {
  173. this.cloneList = JSON.parse(JSON.stringify(this.list));
  174. this.positionList = [];
  175. this.rowNum = Math.ceil(this.cloneList.length / this.columnNum);
  176. this.boxHeight = this.rowNum * this.rpxTopx(this.rowHeight) + (this.rowNum - 1) * this.rpxTopx(this.rowSpace);
  177. this.xMoveUnit = this.columnWidth + this.rpxTopx(this.columnSpace);
  178. this.yMoveUnit = this.rpxTopx(this.rowHeight) + this.rpxTopx(this.rowSpace);
  179. this.cloneList.forEach((item, index) => {
  180. item.sortNumber = index;
  181. item.zIndex = this.zIndex;
  182. item.x = this.xMoveUnit * (index % this.columnNum); //单位px
  183. item.y = Math.floor(index / this.columnNum) * this.yMoveUnit; //单位px
  184. this.positionList.push({
  185. x: item.x,
  186. y: item.y,
  187. id: item.id,
  188. })
  189. })
  190. },
  191. /* 找到id在位置队列positionList里对应的索引 */
  192. findPositionIndex(id) {
  193. var resultIndex = 0;
  194. for(var i = 0, len = this.positionList.length; i < len; i++) {
  195. var item = this.positionList[i];
  196. if(item.id == id) {
  197. resultIndex = i;
  198. break;
  199. }
  200. }
  201. return resultIndex
  202. },
  203. /* 触摸开始 */
  204. onTouchstart(obj) {
  205. if(!this.isEdit) {
  206. return false
  207. };
  208. this.type = obj.id
  209. this.curTouchPostionIndex = this.findPositionIndex(obj.id);
  210. // 将当前拖动的模块zindex调成当前队列里的最大
  211. this.cloneList.forEach((item, index) => {
  212. if(item.id == obj.id) {
  213. item.zIndex = item.zIndex + 100000;
  214. item.isTouched = true;
  215. }else {
  216. item.zIndex = this.zIndex + index + 1;
  217. item.isTouched = false;
  218. }
  219. })
  220. this.$set(this.cloneList, 0, this.cloneList[0])
  221. },
  222. /* 触摸结束 */
  223. onTouchend(obj) {
  224. if(!this.isEdit) {
  225. return false
  226. };
  227. this.type = null
  228. this.startSort(this.curTouchPostionIndex, 'onTouchend'); //再次调用并传参数‘onTouchend’,使拖动后且没有找到目标位置的滑块归位
  229. },
  230. /* 移动过程中触发的事件(所有移动块只要一有移动都会触发) */
  231. onChange(e, obj) {
  232. if(!this.isEdit) {
  233. return false
  234. };
  235. var theX = e.detail.x,
  236. theY = e.detail.y,
  237. curCenterX = theX + this.columnWidth / 2,
  238. curCenterY = theY + this.rpxTopx(this.rowHeight) / 2;
  239. if(e.detail.source === 'touch') { //表示由“拖动”触发
  240. var targetIndex = this.findTargetPostionIndex({
  241. curCenterX,
  242. curCenterY,
  243. })
  244. clearTimeout(this.clearT)
  245. this.clearT = setTimeout(() => {
  246. this.$nextTick(() => {
  247. this.startSort(targetIndex); //根据targetIndex将队列进行排序
  248. })
  249. }, 100)
  250. }
  251. },
  252. /* 根据targetIndex将cloneList进行排序
  253. * [targetIndex] Number 当前拖动的模块拖动到positionList队列里的目标位置的索引
  254. * [type] String 值为onTouchend时,再次调用set方法
  255. */
  256. startSort(targetIndex, type) {
  257. var curTouchId = this.positionList[this.curTouchPostionIndex].id;
  258. if(this.curTouchPostionIndex < targetIndex) {
  259. for(var i = 0, len = this.positionList.length; i < len; i++) {
  260. var curItem = this.positionList[i];
  261. var nextItem = this.positionList[i + 1] || this.positionList[this.positionList.length - 1];
  262. if(i >= this.curTouchPostionIndex && i <= targetIndex) { //找到要进行位移的索引集
  263. if(i == targetIndex) {
  264. curItem.id = curTouchId;
  265. }else {
  266. curItem.id = nextItem.id;
  267. }
  268. }
  269. }
  270. }else {
  271. var clonePostionList = JSON.parse(JSON.stringify(this.positionList));
  272. for(var i = 0, len = this.positionList.length; i < len; i++) {
  273. var curItem = this.positionList[i];
  274. var preItem = this.positionList[i - 1] || this.positionList[0];
  275. if(i >= targetIndex && i <= this.curTouchPostionIndex) { //找到要进行位移的索引集
  276. if(i == targetIndex) {
  277. curItem.id = curTouchId;
  278. }else{
  279. curItem.id = clonePostionList[i - 1].id;
  280. }
  281. }
  282. }
  283. }
  284. this.cloneList.forEach(item => {
  285. item.x += 0.001;
  286. item.y += 0.001;
  287. })
  288. if(type == 'onTouchend') {
  289. this.$set(this.cloneList, 0, this.cloneList[0])
  290. }
  291. this.$nextTick(() => {
  292. this.cloneList.forEach(item => {
  293. for(var i = 0, len = this.positionList.length; i < len; i++) {
  294. var item02 = this.positionList[i];
  295. if(item.id == item02.id) {
  296. item.x = item02.x;
  297. item.y = item02.y;
  298. }
  299. }
  300. })
  301. this.$set(this.cloneList, 0, this.cloneList[0])
  302. this.curTouchPostionIndex = targetIndex
  303. this.handleEmitData(); //需要在onChange事件里发射信息出去最稳妥,因为在快速拖动释放鼠标的时候该事件会再onTouchend后执行
  304. })
  305. },
  306. /* 处理要发射出去的数据队列,将排序后的结果同步到父组件的list */
  307. handleEmitData() {
  308. var idArr = this.getSortedIdArr();
  309. var emitList = [];
  310. idArr.forEach(id => {
  311. for(var i = 0, len = this.list.length; i < len; i++) {
  312. var item = this.list[i];
  313. if(id == item.id) {
  314. emitList.push(item);
  315. break;
  316. }
  317. }
  318. })
  319. this.$emit('update:list', emitList);
  320. },
  321. /* 获取最后的排序完的id队列集 */
  322. getSortedIdArr() {
  323. return this.positionList.map(item => item.id)
  324. },
  325. /* 找出拖动到positionList队列里的哪个目标索引
  326. * [curObj.curCenterX], Number 当前拖动的模块的中心点x轴坐标
  327. * [curObj.curCenterY], Number 当前拖动的模块的中心点y轴坐标
  328. * return 返回拖动到的目标索引
  329. */
  330. findTargetPostionIndex(curObj) {
  331. var resultIndex = this.curTouchPostionIndex;
  332. for(var i = 0, len = this.positionList.length; i < len; i++) {
  333. var item = this.positionList[i];
  334. if(curObj.curCenterX >= item.x && curObj.curCenterX <= item.x + this.columnWidth) {
  335. if(curObj.curCenterY >= item.y && curObj.curCenterY <= item.y + this.rpxTopx(this.rowHeight)) {
  336. resultIndex = i;
  337. break;
  338. }
  339. }
  340. }
  341. return resultIndex;
  342. },
  343. /* prx转换成px,返回值还是不带px单位的Number */
  344. rpxTopx(v) {
  345. return this.windowWidth * v / 750
  346. }
  347. },
  348. watch: {
  349. list() {
  350. this.handleListData();
  351. }
  352. }
  353. }
  354. </script>
  355. <style scoped>
  356. .is-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
  357. .dragSort-wrap {}
  358. .dragSortBox {
  359. padding: 0 20rpx;
  360. }
  361. .dragSortBox-btns { padding: 20rpx; margin-bottom: 20rpx; text-align: right; color: #409EFF;display: flex;justify-content: flex-end;}
  362. .dragSortBox-btn { margin-left: 20rpx;}
  363. .dragSort-area {
  364. width: 100%;
  365. /* background-color: skyblue; */
  366. }
  367. .big{
  368. width: 110% !important;
  369. height: 110%!important;
  370. }
  371. .dragSort-view {
  372. background-color: #eee;
  373. /* border: 1px solid #ccc; */
  374. border-radius: 20rpx;
  375. font-size: 14px;
  376. text-align: center;
  377. }
  378. .dragSort-view.is-touched { opacity: .9;}
  379. .dragSort-view__con { position: relative; height: 100%;}
  380. .dragSort-view__label { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); max-width: 100%;}
  381. .dragSort-view__btn-del { font-weight: bold;position: absolute; right: 0; top: 0;font-size: 8px;transform: translate(-50%, 10%)}
  382. /* transform: translate(50%, -50%); */
  383. @-webkit-keyframes shake {
  384. 0% { transform: rotate(0deg); } /* 初始状态不旋转 */
  385. 25% { transform: rotate(-1deg); } /* 左右摇晃 */
  386. 75% { transform: rotate(1deg); } /* 反向左右摇晃 */
  387. 100% { transform: rotate(0deg); } /* 结束时返回到初始状态 */
  388. }
  389. @keyframes shake {
  390. 0% { transform: rotate(0deg); } /* 初始状态不旋转 */
  391. 25% { transform: rotate(-1deg); } /* 左右摇晃 */
  392. 75% { transform: rotate(1deg); } /* 反向左右摇晃 */
  393. 100% { transform: rotate(0deg); } /* 结束时返回到初始状态 */
  394. }
  395. .shake {
  396. /* position: relative; */
  397. -webkit-animation: shake 0.2s infinite;
  398. animation: shake 0.2s infinite;
  399. }
  400. </style>