背景

在重构中遇到了一个比较复杂的排序功能,里面有同组项,拖动时要考虑同组项拖动其中一个时其他同组项的排序处理问题,且有同组项拖动到另外一个同组项之间等问题的处理,整个排序处理逻辑比较复杂,因此研究了一下小清的写法后,值得分享

访问地址

http://192.168.3.103:8099/#/login
线上访问地址为:crm.mytijian.com -> 维护后台 -> 单项管理 -> 排序

页面展示

核心功能概述

  1. 当拖动非同组项到同组项A中间时,采用置换方式将此非同组项依次向上或向下置换,直至挪出此同组项A之间
  2. 当拖动同组项A的其中一个小项A1移动到非同组项位置时,首先把同组项A全部找到并放入一个数组,然后以A1为分界线,将A1上下两层中除去同组项A后剩下的数据分别放入start和end两个数组,然后以start + A + end方式重新组装并排序
  3. 当拖动同组项A的其中一个小项A1移动到同组项B中间时,首先把同组项A和同组项B分别放入A、B两个数组,然后以A1为分界线,将A1上下两层中除去同组项A和同组项B的数据分别放入start和end两个数组,然后以start + A + B + end 或者start + B + A + end方式重新组装并排序

代码

  1. <template>
  2. <div class="p10">
  3. <div class="lh30">
  4. <div class="dib">
  5. 项目名称:<i-input v-model.trim="filterWord" icon="ios-search-strong" placeholder="请输入项目名称" style="width: 260px" class="fr" @on-enter="filterItem" @on-click="filterItem" @on-change="filterItem">
  6. </i-input>
  7. </div>
  8. <span class="c-rd ml10">鼠标拖动可排序,双击单项可单个排序(注:当输入搜索条件时不可拖动)</span>
  9. </div>
  10. <div>
  11. <div class="df">
  12. <div>
  13. <div class="mt10 vertical-table overflow" v-bind:style="{'max-height': height, 'min-height': '300px', 'max-width': '900px'}">
  14. <table class="mt-table">
  15. <tr>
  16. <th class="w60">序号</th>
  17. <th class="w300">项目名称</th>
  18. <th class="w150">所有类别</th>
  19. </tr>
  20. <draggable element="tbody" id="dragList" v-model="itemList" @end="end" @change="change" :options="{disabled: !!filterWord}">
  21. <tr v-for="(item, index) in itemList" :key="item.id" :class="{'c-fail': !item.show}" v-show="!item.hide" @dblclick="openSingleSort(item, index)">
  22. <td>{{item.sequence}}</td>
  23. <td><span v-if="item.groupId"> {{item.groupId}} <i class="iconfont icon-fenzu c-primary"></i></span> {{item.name}}</td>
  24. <td>{{item.speciesName}}</td>
  25. </tr>
  26. </draggable>
  27. </table>
  28. <mt-none class="pt20" :show="itemList.length == 0" />
  29. </div>
  30. <div class="mt10">
  31. <Button type="ghost" class="fr" @click="cancel">取消</Button>
  32. <Button type="primary" class="fr mr10" @click="ok">保存</Button>
  33. </div>
  34. </div>
  35. <div class="mt10 ml10">
  36. <table class="mt-table w200 table-radius">
  37. <tr>
  38. <th>类别顺序</th>
  39. </tr>
  40. <tr class="pointer" v-for="(species, index) in speciesList" :key="index">
  41. <td>{{species.name}}</td>
  42. </tr>
  43. </table>
  44. <Button type="primary" class="mt15" @click="sortBySpecies">按以上类别排序</Button>
  45. </div>
  46. </div>
  47. </div>
  48. <!-- 弹窗 -->
  49. <Modal v-model="isShowModal" width="400px" title="修改" :mask-closable="false">
  50. <div v-if="isShowModal">
  51. <Form :label-width="100">
  52. <FormItem label="修改">
  53. <div class="c-or">{{selectItem.name}}</div>
  54. </FormItem>
  55. <FormItem label="序号为:">
  56. <div class="rel">
  57. <InputNumber class="w150" v-model.trim="newSequence" @on-change="filterInput" :precision="0" :step="1"></InputNumber>
  58. </div>
  59. </FormItem>
  60. </Form>
  61. </div>
  62. <div slot="footer">
  63. <span class="c-gr" v-if="successMsg">{{successMsg}}</span>
  64. <Button type="text" @click="clearModalData">取消</Button>
  65. <Button type="primary" @click="changeSequence">确定</Button>
  66. </div>
  67. </Modal>
  68. </div>
  69. </template>
  70. <script>
  71. import { mapState } from 'vuex'
  72. import api from '@/api/m-itemmanage'
  73. import _cloneDeep from 'lodash/cloneDeep'
  74. import Draggable from 'vuedraggable'
  75. import { fullTohalf, arrayToHash, contains } from '@/common/utils'
  76. export default {
  77. name: 'manage-itemmanage-sort',
  78. data() {
  79. return {
  80. filterWord: '',
  81. itemList: [],
  82. itemListBak: [],
  83. itemListBak2: [],
  84. oldItemList: [],
  85. speciesList: [],
  86. height: '',
  87. startGroupItem: [],
  88. endGroupItem: [],
  89. isShowModal: false,
  90. selectItem: [],
  91. selectIndex: '',
  92. newSequence: null,
  93. successMsg: ''
  94. }
  95. },
  96. computed: {
  97. ...mapState({
  98. hospitalId: state => state.user.hospital.id
  99. })
  100. },
  101. components: {
  102. draggable: Draggable
  103. },
  104. methods: {
  105. filterInput() {
  106. this.newSequence = parseInt(this.newSequence)
  107. },
  108. changeSequence() {
  109. if (this.newSequence || this.newSequence === 0) {
  110. let newIndex = parseInt(this.newSequence)
  111. if (newIndex < 0) {
  112. newIndex = 0
  113. }
  114. if (newIndex >= this.itemList.length - 1) {
  115. newIndex = this.itemList.length - 1
  116. }
  117. console.log(this.selectIndex, newIndex)
  118. if (newIndex != this.selectIndex) {
  119. this.singleSort(newIndex, this.selectIndex)
  120. } else {
  121. this.clearModalData()
  122. }
  123. } else {
  124. this.clearModalData()
  125. }
  126. },
  127. clearModalData() {
  128. this.isShowModal = false
  129. this.newSequence = null
  130. this.successMsg = ''
  131. },
  132. singleSort(newIndex, index) {
  133. let sorted = []
  134. let startGroupItem = this.getWholeGroup(index)
  135. let endGroupItem = this.getWholeGroup(newIndex)
  136. let insertArray = []
  137. let selectItem = this.itemList[index]
  138. let dropItem = this.itemList[newIndex]
  139. if (selectItem.groupId && dropItem.groupId && selectItem.groupId == dropItem.groupId) {
  140. //如果是组内移动,不做处理
  141. return
  142. }
  143. if (selectItem.groupId) {
  144. insertArray = insertArray.concat(startGroupItem)
  145. } else {
  146. insertArray.push(selectItem)
  147. }
  148. if (dropItem.groupId) {
  149. insertArray = insertArray.concat(endGroupItem)
  150. } else {
  151. insertArray.push(dropItem)
  152. }
  153. let beforeGroupItems = []
  154. let afterGroupItems = []
  155. for (let i = 0; i < newIndex; i++) {
  156. if (!this.containsItem(insertArray, this.itemList[i])) {
  157. beforeGroupItems.push(this.itemList[i])
  158. }
  159. }
  160. for (let i = newIndex; i < this.itemList.length; i++) {
  161. if (!this.containsItem(insertArray, this.itemList[i])) {
  162. afterGroupItems.push(this.itemList[i])
  163. }
  164. }
  165. sorted = sorted
  166. .concat(beforeGroupItems)
  167. .concat(insertArray)
  168. .concat(afterGroupItems)
  169. this.itemList = sorted
  170. for (let i = 0; i < this.itemList.length; i++) {
  171. this.itemList[i].sequence = i
  172. }
  173. this.ok('single')
  174. },
  175. containsItem(array, item) {
  176. for (let i = 0; i < array.length; i++) {
  177. if (array[i].id == item.id) {
  178. return true
  179. }
  180. }
  181. return false
  182. },
  183. ok(type) {
  184. this.newItemList = arrayToHash(this.itemList)
  185. let changedItem = [] //排序后,sequence发生变化的项目
  186. for (let index in this.newItemList) {
  187. if (this.oldItemList[index].sequence != this.newItemList[index].sequence) {
  188. changedItem.push({ id: this.oldItemList[index].id, sequence: this.newItemList[index].sequence, hisItemId: this.newItemList[index].hisItemId })
  189. }
  190. }
  191. console.log(5, changedItem)
  192. if (changedItem.length > 0) {
  193. api.updateSequence({ data: changedItem }).then(data => {
  194. if (type === 'single') {
  195. this.successMsg = '保存成功'
  196. let timer = setTimeout(() => {
  197. this.clearModalData()
  198. clearTimeout(timer)
  199. }, 1000)
  200. } else {
  201. this.$Message.success('保存成功!')
  202. }
  203. this.getItem()
  204. })
  205. } else {
  206. this.clearModalData()
  207. }
  208. },
  209. cancel() {
  210. this.itemList = _cloneDeep(this.itemListBak2)
  211. },
  212. openSingleSort(item, index) {
  213. this.selectItem = item
  214. this.selectIndex = index
  215. this.isShowModal = true
  216. },
  217. // step1
  218. change(evt) {
  219. // 对应原来老版crm中的sort方法的 update 方法
  220. this.startGroupItem = this.getWholeGroup(evt.moved.oldIndex)
  221. this.endGroupItem = this.getWholeGroup(evt.moved.newIndex)
  222. },
  223. end(evt) {
  224. evt.oldIndex // 可以知道拖动前的位置
  225. evt.newIndex // 可以知道拖动后的位置
  226. this.sort(evt.newIndex, evt.oldIndex)
  227. },
  228. getWholeGroup(index) {
  229. let len = this.itemList.length
  230. let groupItems = []
  231. if (this.itemListBak[index].groupId) {
  232. for (let i = -index; i < len - index; i++) {
  233. // console.log(i, len, len-index, index+i, index)
  234. /*
  235. *-73 267 194 0 73
  236. *-72 267 194 1 73
  237. *...
  238. *192 267 194 265 73
  239. *193 267 194 266 73
  240. */
  241. // 整个遍历一遍数组并将同组项全部挑选出来
  242. if (this.itemListBak[index + i] && this.itemListBak[index + i].groupId == this.itemListBak[index].groupId) {
  243. groupItems.push(this.itemListBak[index + i])
  244. }
  245. }
  246. }
  247. return groupItems
  248. },
  249. sort(currIndex, oldIndex) {
  250. if (currIndex == undefined) {
  251. return
  252. }
  253. if (this.inOneGroup(currIndex) || !this.itemList[currIndex].groupId) {
  254. // 1:同组项组内拖动 || 不是同组项拖动
  255. console.log(-1)
  256. this.isSortMiddleOfGroup(currIndex, oldIndex)
  257. } else {
  258. // 同组项拖动且非组内拖动
  259. console.log(-2)
  260. this.sortGroupItem(currIndex, oldIndex)
  261. }
  262. for (let i = 0; i < this.itemList.length; i++) {
  263. this.itemList[i].sequence = i
  264. }
  265. // 重新排序后保存当前快照,为了之后在拖动时准确找出牵连的同组项
  266. this.itemListBak = _cloneDeep(this.itemList)
  267. },
  268. sortGroupItem(currIndex, oldIndex) {
  269. console.log(this.startGroupItem, this.endGroupItem)
  270. let currItem = this.itemList[currIndex]
  271. let beforeGroupItems = []
  272. let afterGroupItems = []
  273. let endGroupId
  274. if (this.endGroupItem.length > 0) {
  275. endGroupId = this.endGroupItem[0].groupId
  276. }
  277. let sorted = []
  278. // 以被拖动的项目为分界线,从上下两层中分别剔除与拖动相关的同组项并分为上下两个大集合
  279. for (let i = 0; i < currIndex; i++) {
  280. if (this.itemList[i].groupId != currItem.groupId) {
  281. if (endGroupId) {
  282. if (this.itemList[i].groupId != endGroupId) {
  283. beforeGroupItems.push(this.itemList[i])
  284. }
  285. } else {
  286. beforeGroupItems.push(this.itemList[i])
  287. }
  288. }
  289. }
  290. for (let i = currIndex + 1; i < this.itemList.length; i++) {
  291. if (this.itemList[i].groupId != currItem.groupId) {
  292. if (endGroupId) {
  293. if (this.itemList[i].groupId != endGroupId) {
  294. afterGroupItems.push(this.itemList[i])
  295. }
  296. } else {
  297. afterGroupItems.push(this.itemList[i])
  298. }
  299. }
  300. }
  301. // 若有A同组项向下拖动到B同组项之间,则在重新排序时B在A之上
  302. // 若有A同组项向上拖动到B同组项之间,则在重新排序时A在B之上
  303. if (currIndex > oldIndex) {
  304. sorted = sorted
  305. .concat(beforeGroupItems)
  306. .concat(this.endGroupItem)
  307. .concat(this.startGroupItem)
  308. .concat(afterGroupItems)
  309. } else {
  310. sorted = sorted
  311. .concat(beforeGroupItems)
  312. .concat(this.startGroupItem)
  313. .concat(this.endGroupItem)
  314. .concat(afterGroupItems)
  315. }
  316. this.itemList = sorted
  317. },
  318. inOneGroup(dropIndex) {
  319. let maxInd = this.itemList.length - 1
  320. if (dropIndex == 0 && this.itemList[0].groupId && this.itemList[1].groupId && this.itemList[0].groupId == this.itemList[1].groupId) {
  321. console.log(1, true, '被拖动到第一个,整个组被拖动到最上方且是组内拖动')
  322. return true
  323. }
  324. if (dropIndex == maxInd && this.itemList[maxInd].groupId && this.itemList[maxInd - 1].groupId && this.itemList[maxInd].groupId == this.itemList[maxInd - 1].groupId) {
  325. console.log(2, true, '被拖动到最后一个,整个组被拖动到最后一个且是组内拖动')
  326. return true
  327. }
  328. if (dropIndex == maxInd) {
  329. console.log(3, false, '拖动到最后一个')
  330. return false
  331. }
  332. if (dropIndex == 0) {
  333. console.log(4, false, '拖动到第一个')
  334. return false
  335. }
  336. if (this.itemList[dropIndex].groupId && (this.itemList[dropIndex].groupId == this.itemList[dropIndex + 1].groupId || this.itemList[dropIndex].groupId == this.itemList[dropIndex - 1].groupId)) {
  337. console.log(5, true, '被拖动项目是同组项,且拖动后的位置紧挨着一个id相同的同组项,简称组内拖动')
  338. } else {
  339. console.log(6, '非被拖动到第一个且最后一个,且非组内拖动')
  340. }
  341. return this.itemList[dropIndex].groupId && (this.itemList[dropIndex].groupId == this.itemList[dropIndex + 1].groupId || this.itemList[dropIndex].groupId == this.itemList[dropIndex - 1].groupId)
  342. },
  343. isSortMiddleOfGroup(dropIndex, oldIndex) {
  344. if (this.inOneGroup(dropIndex)) {
  345. //组内拖动除外(组内拖动不需要再次重新排序)
  346. console.log(7, '组内拖动')
  347. return
  348. }
  349. if (!this.itemList[dropIndex - 1] || !this.itemList[dropIndex + 1]) {
  350. console.log(8, '非同组项 且 被拖动到第一个或最后一个')
  351. return
  352. }
  353. if (!(this.itemList[dropIndex - 1].groupId && this.itemList[dropIndex + 1].groupId && this.itemList[dropIndex - 1].groupId == this.itemList[dropIndex + 1].groupId)) {
  354. console.log(9, '非同组项之间拖动')
  355. return
  356. }
  357. console.log(10, '非同组项被拖动到同组项之间')
  358. if (oldIndex < dropIndex) {
  359. //向下拖
  360. // 当前被拖动项目 与 下方第一个元素互换位置 并继续循环执行此步骤
  361. this.exchangeIndex(dropIndex, dropIndex + 1)
  362. dropIndex = dropIndex + 1
  363. this.isSortMiddleOfGroup(dropIndex, dropIndex - 1)
  364. } else if (oldIndex > dropIndex) {
  365. //向上拖
  366. this.exchangeIndex(dropIndex, dropIndex - 1)
  367. dropIndex = dropIndex - 1
  368. this.isSortMiddleOfGroup(dropIndex, dropIndex + 1)
  369. }
  370. },
  371. exchangeIndex(index1, index2) {
  372. let temp = this.itemList[index1]
  373. this.itemList[index1] = this.itemList[index2]
  374. this.itemList[index2] = temp
  375. },
  376. getItem() {
  377. api.getExamItemInSort({ data: { hospitalId: this.hospitalId } }).then(res => {
  378. this.itemList = res
  379. this.itemListBak = _cloneDeep(this.itemList)
  380. this.itemListBak2 = _cloneDeep(this.itemList)
  381. this.oldItemList = arrayToHash(_cloneDeep(this.itemList))
  382. this.filterItem()
  383. })
  384. },
  385. filterItem() {
  386. this.filterWord = this.filterWord.trim()
  387. this.filterItemByQianDuan()
  388. this.srollTo()
  389. },
  390. filterItemByQianDuan() {
  391. let word = fullTohalf(this.filterWord)
  392. this.itemList.forEach((item, index) => {
  393. if (contains(item.name, word) || contains(item.pinyin, word) || contains(item.hisItemId, word)) {
  394. item.hide = false
  395. } else {
  396. item.hide = true
  397. }
  398. })
  399. },
  400. srollTo() {
  401. for (let i = 0; i < this.itemList.length; i++) {
  402. if (this.itemList[i].foc) {
  403. document.getElementById('dragList').scrollTop = 40 * i - 350
  404. return
  405. }
  406. }
  407. document.getElementById('dragList').scrollTop = 0
  408. },
  409. sortBySpecies() {
  410. this.$Modal.confirm({
  411. title: '提示',
  412. content: '你确定按类别重新排序?排序后不可撤销!',
  413. onOk: () => {
  414. this.speciesSort()
  415. }
  416. })
  417. },
  418. speciesSort() {
  419. let noSpeciesList = []
  420. let perSpeciesItems = {}
  421. for (let j = 0; j < this.speciesList.length; j++) {
  422. perSpeciesItems[this.speciesList[j].id] = []
  423. }
  424. for (let i = 0; i < this.itemList.length; i++) {
  425. let hasSpecies = false
  426. for (let j = 0; j < this.speciesList.length; j++) {
  427. let speId = this.speciesList[j].id
  428. if (speId == this.itemList[i].speciesId) {
  429. perSpeciesItems[speId].push(this.itemList[i])
  430. hasSpecies = true
  431. break
  432. }
  433. }
  434. if (!hasSpecies) {
  435. noSpeciesList.push(this.itemList[i])
  436. }
  437. }
  438. let sortedItems = []
  439. for (let i in perSpeciesItems) {
  440. sortedItems = sortedItems.concat(perSpeciesItems[i])
  441. }
  442. sortedItems = sortedItems.concat(noSpeciesList)
  443. this.itemList = sortedItems
  444. for (let i = 0; i < this.itemList.length; i++) {
  445. this.itemList[i].sequence = i
  446. }
  447. this.ok()
  448. }
  449. },
  450. mounted() {
  451. this.height = window.innerHeight - 350 + 'px'
  452. this.getItem()
  453. api.getSpeciesList({ data: { hospitalId: this.hospitalId, type: 1 } }).then(res => {
  454. this.speciesList = res
  455. })
  456. }
  457. }
  458. </script>
  459. <style lang="less" scoped src="../style/index.less">
  460. </style>
文档更新时间: 2019-05-10 14:29