Просмотр исходного кода

文物列表分批加载,模拟后端接口

任一存 2 лет назад
Родитель
Сommit
5f7c72497d
7 измененных файлов с 207 добавлено и 41 удалено
  1. 2 1
      package.json
  2. 3 3
      src/App.vue
  3. 9 0
      src/api.js
  4. 2 0
      src/main.js
  5. 54 0
      src/utils.js
  6. 132 37
      src/views/RelicsAppr.vue
  7. 5 0
      yarn.lock

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yuhuatai-mobile",
-  "version": "0.3.20221102.1454",
+  "version": "0.3.20221102.1546",
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve --mode dev",
@@ -17,6 +17,7 @@
     "swiper": "^8.4.4",
     "v-viewer": "^1.6.4",
     "vue": "^2.6.14",
+    "vue-infinite-scroll": "^2.0.2",
     "vue-lazyload": "^1.3.3",
     "vue-router": "^3.5.1",
     "vuex": "^3.6.2"

+ 3 - 3
src/App.vue

@@ -73,12 +73,12 @@ body {
 //   -webkit-touch-callout: none;
 // }
 
-.fade-out-leave-active {
-  transition: opacity 1s;
-}
 .fade-out-leave-to {
   opacity: 0;
 }
+.fade-out-leave-active {
+  transition: opacity 1s;
+}
 
 .viewer-backdrop {
   background-color: rgba(0, 0, 0, 90%) !important;

+ 9 - 0
src/api.js

@@ -1,4 +1,5 @@
 import axios from "axios"
+import { goodsData as rawData } from "@/assets/data/data.js"
 
 function _like() {
   return axios({
@@ -39,4 +40,12 @@ export default {
     console.log('fetch pano data: ', res.data)
     return res.data
   },
+  // type: straw(手稿)   book(书刊)   tool(用具)  clothing(服装) Italy(油画)
+  fetchRelicList(dimNum = 3, type = 'all', keyword = '', pageNum = 0, pageSize = 20) {
+    return rawData[`${dimNum}D`].filter((item) => {
+      return item.type === type || type === 'all'
+    }).filter((item) => {
+      return item.name.includes(keyword) || keyword === ''
+    }).slice(pageNum * pageSize, pageNum * pageSize + pageSize)
+  }
 }

+ 2 - 0
src/main.js

@@ -10,6 +10,7 @@ import 'viewerjs/dist/viewer.css'
 import Viewer from 'v-viewer'
 import VueLazyload from 'vue-lazyload'
 import { MessageCenter } from "@/utils.js"
+import infiniteScroll from 'vue-infinite-scroll'
 
 console.log(`version: ${process.env.VUE_APP_VERSION}`)
 
@@ -54,6 +55,7 @@ Vue.config.productionTip = false
 Vue.use(clickOutside)
 Vue.use(Viewer)
 Vue.use(VueLazyload)
+Vue.use(infiniteScroll)
 
 new Vue({
   router,

+ 54 - 0
src/utils.js

@@ -41,6 +41,60 @@ function deepClone(target, hash = new WeakMap()) {
 }
 
 export default {
+  /**
+ * 返回一个自带消抖效果的函数,用res表示。
+ *
+ * fn: 需要被消抖的函数
+ * delay: 消抖时长
+ * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
+ * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
+ */
+  debounce(fn, delay, isImmediateCall = true, isRememberLastCall = true) {
+    console.assert(isImmediateCall || isRememberLastCall, 'isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!')
+    let timer = null
+    // 上次调用的时刻
+    let lastCallTime = 0
+
+    if (isImmediateCall && !isRememberLastCall) {
+      return function (...args) {
+        const currentTime = Date.now()
+        if (currentTime - lastCallTime >= delay) {
+          fn.apply(this, args)
+        }
+        lastCallTime = currentTime
+      }
+    } else if (!isImmediateCall && isRememberLastCall) {
+      return function (...args) {
+        if (timer) {
+          clearTimeout(timer)
+        }
+        timer = setTimeout(() => {
+          fn.apply(this, args)
+        }, delay)
+      }
+    } else if (isImmediateCall && isRememberLastCall) {
+      return function (...args) {
+        const currentTime = Date.now()
+        if (currentTime - lastCallTime >= delay) { // 一组操作中的第一次
+          fn.apply(this, args)
+          lastCallTime = currentTime
+          return
+        } else { // 一组中的后续调用
+          if (timer) { // 在此之前存在中间调用
+            lastCallTime = currentTime
+            clearTimeout(timer)
+          }
+          timer = setTimeout(() => {
+            fn.apply(this, args)
+            lastCallTime = 0
+            timer = null
+          }, delay)
+        }
+      }
+    } else {
+      console.error('不应该执行到这里!')
+    }
+  },
   throttle(fn, interval) {
     let lastRunTime = 0
 

+ 132 - 37
src/views/RelicsAppr.vue

@@ -33,7 +33,7 @@
             @click="isShowSelections = !isShowSelections"
           >
             <div class="select">
-              {{ typeListForShow[currentTypeIdx].name }}
+              {{ currentDim === 3 ? typeList3d[currentTypeIdx].name : typeList2d[currentTypeIdx].name }}
             </div>
             <img
               src="@/assets/images/expand.png"
@@ -59,7 +59,7 @@
             class="selection-wrap"
           >
             <li
-              v-for="(item, index) in typeListForShow"
+              v-for="(item, index) in (currentDim === 3 ? typeList3d : typeList2d)"
               :key="item.id"
               :class="{
                 choosen: currentTypeIdx === index,
@@ -97,15 +97,17 @@
     </div>
 
     <div
-      v-if="currentDim === 3"
+      v-show="currentDim === 3"
+      v-infinite-scroll="fetchMore3d"
+      infinite-scroll-disabled="disableInfinite3d"
       class="relics-list-3d"
     >
       <RelicItem
-        v-for="(item, index) in list"
+        v-for="(item, index) in list3d"
         :key="index"
         :is-odd="!!(index % 2)"
         :is-first="index === 1"
-        :is-last="index === list.length - 1"
+        :is-last="index === list3d.length - 1"
         :relic-image="`${$cdnPath}3D/${item.bs}.png?x-oss-process=image/resize,p_4`"
         :relic-title="item.name"
         class="relic-item"
@@ -114,11 +116,13 @@
     </div>
 
     <ul
-      v-if="currentDim === 2"
+      v-show="currentDim === 2"
+      v-infinite-scroll="fetchMore2d"
+      infinite-scroll-disabled="disableInfinite2d"
       class="relics-list-2d"
     >
       <li
-        v-for="(item, index) in list"
+        v-for="(item, index) in list2d"
         :key="index"
         @click="onClick2dItem(item)"
       >
@@ -160,7 +164,33 @@ export default {
   data() {
     return {
       currentDim: 3,
-      typeListRaw: [
+      currentTypeIdx: 0,
+      isShowSelections: false,
+      isShowInput: false,
+      searchKeyword: '',
+      list3d: [],
+      list2d: [],
+      disableInfinite3d: false,
+      disableInfinite2d: false,
+      typeList3d: [
+        {
+          name: '全部',
+          id: 'all',
+        },
+        {
+          name: '书刊',
+          id: 'book',
+        },
+        {
+          name: '用具',
+          id: 'tool',
+        },
+        {
+          name: '服装',
+          id: 'clothign',
+        },
+      ],
+      typeList2d: [
         {
           name: '全部',
           id: 'all',
@@ -190,49 +220,55 @@ export default {
           id: 'photo',
         },
       ],
-      currentTypeIdx: 0,
-      isShowSelections: false,
-      isShowInput: false,
-      searchKeyword: '',
     }
   },
   computed: {
-    typeListForShow() {
-      if (this.currentDim === 3) {
-        return this.typeListRaw.slice(0, this.typeListRaw.length - 2)
-      } else if (this.currentDim === 2) {
-        return this.typeListRaw
-      } else {
-        return []
-      }
-    },
-    list() {
-      let temp = null
+    hasData() {
       if (this.currentDim === 3) {
-        temp = rawData['3D']
+        return this.list3d.length !== 0
       } else {
-        temp = rawData['2D']
+        return this.list2d.length !== 0
       }
-      temp = temp.filter((item) => {
-        return item.type === this.typeListForShow[this.currentTypeIdx].id || this.typeListForShow[this.currentTypeIdx].id === 'all'
-      }).filter((item) => {
-        return item.name.includes(this.searchKeyword)
-      })
-      return temp
-    },
-    hasData() {
-      return this.list.length !== 0
     },
   },
   watch: {
     currentDim: {
-      handler(vNew) {
-        if (vNew === 3 && this.currentTypeIdx > this.typeListForShow.length - 1) {
-          this.currentTypeIdx = 0
+      handler(vNew, vOld) {
+        let idxNew = 0
+        if (vOld === 3) {
+          const idOld = this.typeList3d[this.currentTypeIdx].id
+          const res = this.typeList2d.findIndex((item) => {
+            return item.id === idOld
+          })
+          if (res >= 0) {
+            idxNew = res
+          }
+        } else if (vOld === 2) {
+          const idOld = this.typeList2d[this.currentTypeIdx].id
+          const res = this.typeList3d.findIndex((item) => {
+            return item.id === idOld
+          })
+          if (res >= 0) {
+            idxNew = res
+          }
         }
+        this.currentTypeIdx = idxNew
+        this.refetchSmartly()
+      }
+    },
+    currentTypeIdx: {
+      handler(vNew, vOld) {
+        this.refetchSmartly()
+      }
+    },
+    searchKeyword: {
+      handler(vNew, vOld) {
+        this.refetchSmartly()
       }
     }
   },
+  mounted() {
+  },
   methods: {
     onClickFakeInput() {
       this.isShowInput = true
@@ -249,6 +285,51 @@ export default {
     },
     onClick2dItem(item) {
       this.$router.push({ name: 'RelicDetail', query: { ...item, ...{ dimNumber: 2 } } })
+    },
+    fetchMore3d: globalUtils.debounce(function() {
+      console.log('fetchMore3d')
+      if (this.list3d.length % 20 !== 0 ) {
+        return
+      }
+      const res = globalApi.fetchRelicList(3, this.typeList3d[this.currentTypeIdx].id, this.searchKeyword, this.list3d.length / 20)
+      this.list3d = this.list3d.concat(res)
+    }, 333),
+    fetchMore2d: globalUtils.debounce(function() {
+      console.log('fetchMore2d')
+      if (this.list2d.length % 20 !== 0 ) {
+        return
+      }
+      const res = globalApi.fetchRelicList(2, this.typeList2d[this.currentTypeIdx].id, this.searchKeyword, this.list2d.length / 20)
+      this.list2d = this.list2d.concat(res)
+    }, 333),
+    refetch3d: globalUtils.debounce(function() {
+      console.log('refetch3d')
+      this.disableInfinite3d = true
+      this.list3d = []
+      this.$nextTick(() => {
+        this.fetchMore3d()
+        this.$nextTick(() => {
+          this.disableInfinite3d = false
+        })
+      })
+    }, 333),
+    refetch2d: globalUtils.debounce(function() {
+      console.log('refetch2d')
+      this.disableInfinite2d = true
+      this.list2d = []
+      setTimeout(() => {
+        this.fetchMore2d()
+        setTimeout(() => {
+          this.disableInfinite2d = false
+        }, 0)
+      }, 0)
+    }, 333),
+    refetchSmartly() {
+      if (this.currentDim === 3) {
+        this.refetch3d()
+      } else {
+        this.refetch2d()
+      }
     }
   }
 }
@@ -438,6 +519,11 @@ export default {
     top: 50%;
     left: 50%;
     transform: translate(-50%, -50%);
+    animation-name: delay-show;
+    animation-duration: 0.3s;
+    animation-fill-mode: forwards;
+    animation-timing-function: step-end;
+
     > img {
       width: 22.92rem;
       height: 20.83rem;
@@ -451,4 +537,13 @@ export default {
     }
   }
 }
+
+@keyframes delay-show {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
 </style>

+ 5 - 0
yarn.lock

@@ -7901,6 +7901,11 @@ vue-hot-reload-api@^2.3.0:
   resolved "https://registry.npmmirror.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
   integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
 
+vue-infinite-scroll@^2.0.2:
+  version "2.0.2"
+  resolved "https://r2.cnpmjs.org/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
+  integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
+
 vue-lazyload@^1.3.3:
   version "1.3.4"
   resolved "https://r2.cnpmjs.org/vue-lazyload/-/vue-lazyload-1.3.4.tgz#2988998f6bc1a2027268f5b0cffa7a7e30e6ccb4"