Browse Source

Merge branch 'next' of https://github.com/jdf2e/nutui into next

Drjnigfubo 3 years ago
parent
commit
9344ea5259

+ 11 - 1
src/config.json

@@ -207,7 +207,6 @@
           "tarodoc": false,
           "author": "szg2008"
         }
-        
       ]
     },
     {
@@ -510,6 +509,17 @@
           "sort": 26,
           "show": false,
           "author": "yangxiaolu"
+        },
+        {
+          "version": "3.0.0",
+          "name": "List",
+          "cType": "操作反馈",
+          "cName": "虚拟列表",
+          "desc": "可在成千上万条数据渲染时只渲染用户最大可见条数,提升页面渲染性能",
+          "show": true,
+          "tarodoc": false,
+          "type": "component",
+          "author": "szg2008"
         }
       ]
     },

+ 34 - 0
src/packages/__VUE/list/__tests__/list.spec.ts

@@ -0,0 +1,34 @@
+import { mount } from '@vue/test-utils';
+import { mockScrollTop } from './../../../utils/unit';
+import List from '../index.vue';
+import { nextTick } from 'vue';
+
+test('should render height', async () => {
+  const wrapper = mount(List, {
+    props: {
+      height: 50,
+      listData: new Array(100).fill(0)
+    }
+  });
+
+  await nextTick();
+
+  const listItem = wrapper.findAll('.nut-list-item')[0];
+  expect((listItem.element as any).style.height).toEqual('50px');
+});
+
+test('should render height', async () => {
+  const visibleCount = Math.ceil(667 / 50);
+  const wrapper = mount(List, {
+    props: {
+      height: 50,
+      listData: new Array(100).fill(0)
+    }
+  });
+
+  await nextTick();
+  await mockScrollTop(100);
+
+  const listItem = wrapper.findAll('.nut-list-item');
+  expect(listItem.length).toBe(visibleCount);
+});

+ 56 - 0
src/packages/__VUE/list/demo.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-list :height="50" :listData="count" @scroll="handleScroll">
+        <template v-slot="{ item }">
+          <div class="list-item">
+            {{ item }}
+          </div>
+        </template>
+      </nut-list>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { onMounted, reactive, toRefs } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('list');
+export default createDemo({
+  props: {},
+  setup() {
+    const state = reactive({
+      count: new Array(100).fill(0)
+    });
+
+    const handleScroll = () => {
+      let arr = new Array(100).fill(0);
+      const len = state.count.length;
+      state.count = state.count.concat(arr.map((item: number, index: number) => len + index + 1));
+    };
+
+    onMounted(() => {
+      state.count = state.count.map((item: number, index: number) => index + 1);
+    });
+
+    return { ...toRefs(state), handleScroll };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.demo {
+  .nut-cell {
+    height: 100%;
+  }
+  .list-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 50px;
+    margin-bottom: 10px;
+    background-color: #f4a8b6;
+    border-radius: 10px;
+  }
+}
+</style>

+ 87 - 0
src/packages/__VUE/list/doc.md

@@ -0,0 +1,87 @@
+# List 虚拟列表
+
+### 介绍
+在正常的列表展示以及上拉加载中,我们通常使用 `NutUI` 提供的 [滚动加载](#/infiniteloading) 组件,那如果我们加载的数据量非常大时,则可能会产生严重的性能问题,导致视图无法响应操作一段时间,这时候我们就用到了虚拟列表组件 `List`,它可以保证只渲染当前可视区域,其他部分在用户滚动到可视区域内之后再渲染。保证了页面流程度,提升性能。
+
+### 安装
+
+```javascript
+
+import { createApp } from 'vue';
+// vue
+import { List } from '@nutui/nutui';
+// taro
+import { List } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use();
+
+```
+
+### 基础用法
+
+:::demo
+
+```html
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-list :height="50" :listData="count" @scroll="handleScroll">
+        <template v-slot="{ item }">
+          <div class="list-item">
+            {{ item }}
+          </div>
+        </template>
+      </nut-list>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { onMounted, reactive, toRefs } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const state = reactive({
+      count: new Array(100).fill(0)
+    });
+
+    const handleScroll = () => {
+      let arr = new Array(100).fill(0);
+      const len = state.count.length;
+      state.count = state.count.concat(arr.map((item: number, index: number) => len + index + 1));
+    };
+
+    onMounted(() => {
+      state.count = state.count.map((item: number, index: number) => index + 1);
+    })
+
+    return { ...toRefs(state), handleScroll };
+  }
+};
+</script>
+```
+
+:::
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| height         | 列表项的高度               | Number | 50                |
+| listData         | 列表数据               | any[] | []                |
+
+### Slot
+
+| 参数         | 说明                             | 类型   |
+|--------------|----------------------------------|--------|
+| item         | 列表项数据               | Object |
+| index         | 索引               | Number |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| scroll  | 滚动时触发 | - |

+ 24 - 0
src/packages/__VUE/list/index.scss

@@ -0,0 +1,24 @@
+.nut-list {
+  width: 100%;
+  height: 100%;
+  overflow: scroll;
+  position: relative;
+  -webkit-overflow-scrolling: touch;
+  &-phantom {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    z-index: -1;
+  }
+  &-container {
+    left: 0;
+    right: 0;
+    top: 0;
+    position: absolute;
+  }
+  &-item {
+    overflow: hidden;
+    margin: $list-item-margin;
+  }
+}

+ 101 - 0
src/packages/__VUE/list/index.taro.vue

@@ -0,0 +1,101 @@
+<template>
+  <scroll-view
+    :class="classes"
+    :scroll-y="true"
+    :style="{ height: screenHeight + 'px' }"
+    scroll-top="0"
+    @scroll="handleScrollEvent"
+    ref="list"
+  >
+    <div class="nut-list-phantom" :style="{ height: listHeight + 'px' }"></div>
+    <div class="nut-list-container" :style="{ transform: getTransform }">
+      <div class="nut-list-item" :style="{ height: height + 'px' }" v-for="(item, index) in visibleData" :key="item">
+        <slot :item="item" :index="index"></slot>
+      </div>
+    </div>
+  </scroll-view>
+</template>
+<script lang="ts">
+import { reactive, toRefs, computed, ref, Ref, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+import Taro from '@tarojs/taro';
+const { componentName, create } = createComponent('list');
+export default create({
+  props: {
+    height: {
+      type: [Number],
+      default: 0
+    },
+    listData: {
+      type: Array,
+      default: () => {
+        return [];
+      }
+    }
+  },
+  emits: ['scroll'],
+
+  setup(props, { emit }) {
+    const list = ref(null) as Ref;
+    const state = reactive({
+      screenHeight: Taro.getSystemInfoSync().windowHeight,
+      startOffset: 0,
+      start: 0,
+      list: props.listData.slice()
+    });
+
+    const visibleCount = computed(() => {
+      return Math.ceil(state.screenHeight / props.height);
+    });
+
+    const end = computed(() => {
+      return state.start + visibleCount.value;
+    });
+
+    const getTransform = computed(() => {
+      return `translate3d(0, ${state.startOffset}px, 0)`;
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const listHeight = computed(() => {
+      return state.list.length * props.height;
+    });
+
+    const visibleData = computed(() => {
+      return state.list.slice(state.start, Math.min(end.value, state.list.length));
+    });
+
+    const handleScrollEvent = async (e: any) => {
+      const scrollTop = e.detail.scrollTop;
+      state.start = Math.floor(scrollTop / props.height);
+      if (end.value > state.list.length) {
+        emit('scroll');
+      }
+      state.startOffset = scrollTop - (scrollTop % props.height);
+    };
+
+    watch(
+      () => props.listData,
+      () => {
+        state.list = props.listData.slice();
+      }
+    );
+
+    return {
+      ...toRefs(state),
+      list,
+      getTransform,
+      listHeight,
+      visibleData,
+      classes,
+      handleScrollEvent
+    };
+  }
+});
+</script>

+ 93 - 0
src/packages/__VUE/list/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <view :class="classes" @scroll.passive="handleScrollEvent" ref="list">
+    <div class="nut-list-phantom" :style="{ height: listHeight + 'px' }"></div>
+    <div class="nut-list-container" :style="{ transform: getTransform }">
+      <div class="nut-list-item" :style="{ height: height + 'px' }" v-for="(item, index) in visibleData" :key="item">
+        <slot :item="item" :index="index"></slot>
+      </div>
+    </div>
+  </view>
+</template>
+<script lang="ts">
+import { reactive, toRefs, computed, ref, Ref, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('list');
+export default create({
+  props: {
+    height: {
+      type: [Number],
+      default: 50
+    },
+    listData: {
+      type: Array,
+      default: () => {
+        return [];
+      }
+    }
+  },
+  emits: ['scroll'],
+
+  setup(props, { emit }) {
+    const list = ref(null) as Ref;
+    const state = reactive({
+      screenHeight: document.documentElement.clientHeight || document.body.clientHeight || 667,
+      startOffset: 0,
+      start: 0,
+      list: props.listData.slice()
+    });
+
+    const visibleCount = computed(() => {
+      return Math.ceil(state.screenHeight / props.height);
+    });
+
+    const end = computed(() => {
+      return state.start + visibleCount.value;
+    });
+
+    const getTransform = computed(() => {
+      return `translate3d(0, ${state.startOffset}px, 0)`;
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const listHeight = computed(() => {
+      return state.list.length * props.height;
+    });
+
+    const visibleData = computed(() => {
+      return state.list.slice(state.start, Math.min(end.value, state.list.length));
+    });
+
+    const handleScrollEvent = () => {
+      const scrollTop = list.value?.scrollTop as number;
+      state.start = Math.floor(scrollTop / props.height);
+      if (end.value > state.list.length) {
+        emit('scroll');
+      }
+      state.startOffset = scrollTop - (scrollTop % props.height);
+    };
+
+    watch(
+      () => props.listData,
+      () => {
+        state.list = props.listData.slice();
+      }
+    );
+
+    return {
+      ...toRefs(state),
+      list,
+      getTransform,
+      listHeight,
+      visibleData,
+      classes,
+      handleScrollEvent
+    };
+  }
+});
+</script>

+ 3 - 0
src/packages/styles/variables-jdt.scss

@@ -717,5 +717,8 @@ $elevator-list-item-bars-z-index: 10 !default;
 $elevator-list-item-bars-inner-item-padding: 3px !default;
 $elevator-list-item-bars-inner-item-font-size: 10px !default;
 
+// list
+$list-item-margin: 0 0 10px 0;
+
 @import './mixins/index';
 @import './animation/index';

+ 3 - 0
src/packages/styles/variables.scss

@@ -742,5 +742,8 @@ $elevator-list-item-bars-z-index: 10 !default;
 $elevator-list-item-bars-inner-item-padding: 3px !default;
 $elevator-list-item-bars-inner-item-font-size: 10px !default;
 
+// list
+$list-item-margin: 0 0 10px 0;
+
 @import './mixins/index';
 @import './animation/index';

+ 3 - 6
src/sites/mobile-taro/vue/project.config.json

@@ -31,15 +31,12 @@
       "outputPath": ""
     },
     "enableEngineNative": false,
-    "useIsolateContext": false,
+    "useIsolateContext": true,
     "userConfirmedBundleSwitch": false,
     "packNpmManually": false,
     "packNpmRelationList": [],
     "minifyWXSS": true,
-    "disableUseStrict": false,
-    "minifyWXML": true,
-    "showES6CompileOption": false,
-    "useCompilerPlugins": false
+    "showES6CompileOption": false
   },
   "compileType": "miniprogram",
   "simulatorType": "wechat",
@@ -48,4 +45,4 @@
   "condition": {
     "miniprogram": {}
   }
-}
+}

+ 2 - 1
src/sites/mobile-taro/vue/src/app.config.ts

@@ -24,7 +24,8 @@ let subpackages = [
       'pages/infiniteloading/index',
       'pages/progress/index',
       'pages/circleprogress/index',
-      'pages/searchbar/index'
+      'pages/searchbar/index',
+      'pages/list/index'
     ]
   },
   {

+ 1 - 0
src/sites/mobile-taro/vue/src/feedback/pages/list/index.config.ts

@@ -0,0 +1 @@
+export default { navigationBarTitleText: 'List' };

+ 54 - 0
src/sites/mobile-taro/vue/src/feedback/pages/list/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="demo list-demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-list :height="50" :listData="count" @scroll="handleScroll">
+        <template v-slot="{ item }">
+          <div class="list-item">
+            {{ item }}
+          </div>
+        </template>
+      </nut-list>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent, onMounted, reactive, toRefs } from 'vue';
+export default defineComponent({
+  props: {},
+  setup() {
+    const state = reactive({
+      count: new Array(100).fill(0)
+    });
+
+    const handleScroll = () => {
+      let arr = new Array(100).fill(0);
+      const len = state.count.length;
+      state.count = state.count.concat(arr.map((item: number, index: number) => len + index + 1));
+    };
+
+    onMounted(() => {
+      state.count = state.count.map((item: number, index: number) => index + 1);
+    });
+
+    return { ...toRefs(state), handleScroll };
+  }
+});
+</script>
+<style lang="scss">
+.list-demo {
+  .nut-cell {
+    height: 100%;
+  }
+  .list-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 50px;
+    margin-bottom: 10px;
+    background-color: #f4a8b6;
+    border-radius: 10px;
+  }
+}
+</style>