ソースを参照

feat(tabs): add props swipeable 支持手势滑动切换,优化小程序端切换动画 #1984 #1828

richard1015 3 年 前
コミット
0a5f67c8f6

+ 11 - 6
src/packages/__VUE/tabpane/index.vue

@@ -1,10 +1,10 @@
 <template>
-  <view class="nut-tab-pane" :class="{ inactive: paneKey != activeKey && autoHeight }">
+  <view class="nut-tab-pane" :style="paneStyle" :class="{ inactive: paneKey != activeKey && autoHeight }">
     <slot></slot>
   </view>
 </template>
 <script lang="ts">
-import { inject } from 'vue';
+import { computed, CSSProperties, inject } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { create } = createComponent('tab-pane');
 
@@ -25,11 +25,16 @@ export default create({
   },
   emits: ['click'],
   setup(props, { emit }) {
-    const parent = inject('activeKey') as any;
-    const parentOption = inject('autoHeight') as any;
+    const parentOption = inject('tabsOpiton') as any;
+    const paneStyle = computed(() => {
+      return {
+        display:
+          parentOption.animatedTime.value == 0 && props.paneKey != parentOption.activeKey.value ? 'none' : undefined
+      } as CSSProperties;
+    });
     return {
-      activeKey: parent.activeKey,
-      autoHeight: parentOption.autoHeight
+      ...parentOption,
+      paneStyle
     };
   }
 });

+ 10 - 2
src/packages/__VUE/tabs/demo.vue

@@ -6,6 +6,12 @@
       <nut-tab-pane title="Tab 2"> Tab 2 </nut-tab-pane>
       <nut-tab-pane title="Tab 3"> Tab 3 </nut-tab-pane>
     </nut-tabs>
+    <h2>{{ translate('swipeable') }}</h2>
+    <nut-tabs v-model="state.tab1value" swipeable>
+      <nut-tab-pane title="Tab 1"> Tab 1 </nut-tab-pane>
+      <nut-tab-pane title="Tab 2"> Tab 2 </nut-tab-pane>
+      <nut-tab-pane title="Tab 3"> Tab 3 </nut-tab-pane>
+    </nut-tabs>
     <h2>{{ translate('title1') }}</h2>
     <nut-tabs v-model="state.tab11value" type="smile">
       <nut-tab-pane title="Tab 1"> Tab 1 </nut-tab-pane>
@@ -40,11 +46,11 @@
     </nut-tabs>
     <h2>{{ translate('title5') }}</h2>
     <nut-tabs style="height: 300px" v-model="state.tab5value" title-scroll direction="vertical">
-      <nut-tab-pane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
+      <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>{{ translate('title6') }}</h2>
     <nut-tabs style="height: 300px" v-model="state.tab6value" type="smile" title-scroll direction="vertical">
-      <nut-tab-pane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
+      <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>{{ translate('title7') }}</h2>
     <nut-tabs v-model="state.tab8value" size="large">
@@ -94,6 +100,7 @@ const initTranslate = () =>
   useTranslate({
     'zh-CN': {
       basic: '基础用法',
+      swipeable: '手势滑动切换',
       title1: '基础用法-微笑曲线',
       title2: '通过 pane-key 匹配',
       title3: '数据异步渲染 3s',
@@ -107,6 +114,7 @@ const initTranslate = () =>
     },
     'en-US': {
       basic: 'Basic Usage',
+      swipeable: 'Swipeable',
       title1: 'Basic Usage - Smile Curve',
       title2: 'Match by pane-key',
       title3: 'Data is rendered asynchronously for 3s',

+ 67 - 36
src/packages/__VUE/tabs/doc.en-US.md

@@ -43,6 +43,36 @@ export default {
 };
 </script>
 ```
+
+:::
+### Swipeable
+:::demo
+```html
+<template>
+<nut-tabs v-model="state.tab1value" swipeable>
+  <nut-tab-pane title="Tab 1">
+    Tab 1
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 2">
+    Tab 2
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 3">
+    Tab 3
+  </nut-tab-pane>
+</nut-tabs>
+</template>
+<script lang="ts">
+import { reactive } from 'vue';
+export default {
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+    });
+    return { state };
+  }
+};
+</script>
+```
 :::
 
 ### Basic Usage - Smile Curve
@@ -201,7 +231,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab5value" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -227,7 +257,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab6value" type="smile" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -333,22 +363,23 @@ export default {
 
 ### Tabs Props
 
-| Attribute     | Description                                                                                                                         | Type             | Default    |
-|---------------|-------------------------------------------------------------------------------------------------------------------------------------|------------------|------------|
-| v-model       | Index of active tab                                                                                                                 | number \| string | `0`          |
-| color         | Label selection color                                                                                                               | string           | `#1a1a1a`    |
-| background    | Tab bar background color                                                                                                            | string           | `#f5f5f5`    |
-| direction     | Use landscape orientation optional value `horizontal`、`vertical`                                                                   | string           | `horizontal` |
-| type          | Check the bottom display style optional value `line`、`smile`                                                                       | string           | `line`       |
-| title-scroll  | Is the tab bar scrollable                                                                                                           | boolean          | `false`      |
-| ellipsis      | Whether to omit too long title text                                                                                                 | boolean          | `true`       |
-| animated-time | Switch animation duration, unit ms 0 means no `animation`                                                                             | number \| string | `300`        |
-| title-gutter  | Label gap                                                                                                                           | number \| string | `0`          |
-| size          | Tab bar font size optional value  `large` `normal` `small`                                                                          | string           | `normal`     |
-| auto-height   | Automatic height. When set to `true`, `nut-tabs` and `nut-tabs__content` will change with the height of the current `nut-tab-pane`. | boolean          | `false`      |
-| sticky        | Whether to use sticky mode                                                                                                          | boolean          | `false`      |
-| top           | Sticky offset top                                                                                                                   | number           | `0`          |
-| name        | In the `taro` environment, `name` must be set to enable the automatic scrolling function of the title bar.                              | string | ''        |
+| Attribute        | Description                                                                                                                         | Type             | Default      |
+|------------------|-------------------------------------------------------------------------------------------------------------------------------------|------------------|--------------|
+| v-model          | Index of active tab                                                                                                                 | number \| string | `0`          |
+| color            | Label selection color                                                                                                               | string           | `#1a1a1a`    |
+| background       | Tab bar background color                                                                                                            | string           | `#f5f5f5`    |
+| direction        | Use landscape orientation optional value `horizontal`、`vertical`                                                                   | string           | `horizontal` |
+| type             | Check the bottom display style optional value `line`、`smile`                                                                       | string           | `line`       |
+| swipeable`4.0.3` | Whether to enable gestures to slide left and right                                                                                  | boolean          | `false`      |
+| title-scroll     | Is the tab bar scrollable                                                                                                           | boolean          | `false`      |
+| ellipsis         | Whether to omit too long title text                                                                                                 | boolean          | `true`       |
+| animated-time    | Switch animation duration, unit ms 0 means no `animation`                                                                           | number \| string | `300`        |
+| title-gutter     | Label gap                                                                                                                           | number \| string | `0`          |
+| size             | Tab bar font size optional value  `large` `normal` `small`                                                                          | string           | `normal`     |
+| auto-height      | Automatic height. When set to `true`, `nut-tabs` and `nut-tabs__content` will change with the height of the current `nut-tab-pane`. | boolean          | `false`      |
+| sticky           | Whether to use sticky mode                                                                                                          | boolean          | `false`      |
+| top              | Sticky offset top                                                                                                                   | number           | `0`          |
+| name             | In the `taro` environment, `name` must be set to enable the automatic scrolling function of the title bar.                          | string           | ''           |
 
 ### Tabs Slots
 
@@ -385,24 +416,24 @@ export default {
 
 The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/component/configprovider).
 
-| Name                                                | Default Value                                                                      |
-|-----------------------------------------------------|------------------------------------------------------------------------------------|
-| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                         |
-| --nut-tabs-titles-border-radius                     | _0_                                                                                |
-| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                           |
-| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                           |
-| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                           |
-| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                            |
+| Name                                                | Default Value                                                                  |
+|-----------------------------------------------------|--------------------------------------------------------------------------------|
+| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                     |
+| --nut-tabs-titles-border-radius                     | _0_                                                                            |
+| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                       |
+| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                       |
+| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                       |
+| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                        |
 | --nut-tabs-horizontal-tab-line-color                | _linear-gradient(90deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_  |
-| --nut-tabs-horizontal-titles-height                 | _46px_                                                                             |
-| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                             |
-| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                             |
+| --nut-tabs-horizontal-titles-height                 | _46px_                                                                         |
+| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                         |
+| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                         |
 | --nut-tabs-vertical-tab-line-color                  | _linear-gradient(180deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_ |
-| --nut-tabs-vertical-titles-item-height              | _40px_                                                                             |
-| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                             |
-| --nut-tabs-vertical-titles-width                    | _100px_                                                                            |
-| --nut-tabs-titles-item-line-border-radius           | _0_                                                                                |
-| --nut-tabs-titles-item-line-opacity                 | _1_                                                                                |
+| --nut-tabs-vertical-titles-item-height              | _40px_                                                                         |
+| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                         |
+| --nut-tabs-vertical-titles-width                    | _100px_                                                                        |
+| --nut-tabs-titles-item-line-border-radius           | _0_                                                                            |
+| --nut-tabs-titles-item-line-opacity                 | _1_                                                                            |
 

+ 67 - 36
src/packages/__VUE/tabs/doc.md

@@ -45,6 +45,36 @@ export default {
 ```
 :::
 
+### 手势滑动切换
+:::demo
+```html
+<template>
+<nut-tabs v-model="state.tab1value" swipeable>
+  <nut-tab-pane title="Tab 1">
+    Tab 1
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 2">
+    Tab 2
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 3">
+    Tab 3
+  </nut-tab-pane>
+</nut-tabs>
+</template>
+<script lang="ts">
+import { reactive } from 'vue';
+export default {
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+    });
+    return { state };
+  }
+};
+</script>
+```
+:::
+
 ### 基础用法-微笑曲线
 :::demo
 ```html
@@ -201,7 +231,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab5value" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -227,7 +257,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab6value" type="smile" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -333,22 +363,23 @@ export default {
 
 ### Tabs Props
 
-| 参数          | 说明                                                                                               | 类型             | 默认值     |
-|---------------|----------------------------------------------------------------------------------------------------|------------------|------------|
-| v-model       | 绑定当前选中标签的标识符                                                                           | number \| string | `0`          |
-| color         | 标签选中色                                                                                         | string           | `#1a1a1a`    |
-| background    | 标签栏背景颜色                                                                                     | string           | `#f5f5f5`    |
-| direction     | 使用横纵方向 可选值 horizontal、vertical                                                           | string           | `horizontal` |
-| type          | 选中底部展示样式 可选值 line、smile                                                                | string           | `line`       |
-| title-scroll  | 标签栏是否可以滚动                                                                                 | boolean          | `false`      |
-| ellipsis      | 是否省略过长的标题文字                                                                             | boolean          | `true`       |
-| animated-time | 切换动画时长,单位 ms 0 代表无动画                                                                  | number \| string | `300`        |
-| title-gutter  | 标签间隙                                                                                           | number \| string | `0`          |
-| size          | 标签栏字体尺寸大小 可选值  large normal small                                                      | string           | `normal`     |
-| auto-height   | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
-| sticky        | 是否使用粘性布局                                                                                   | boolean          | `false`      |
-| top           | 粘性布局下的吸顶距离                                                                               | number           | `0`          |
-| name        | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                              | string | ''        |
+| 参数             | 说明                                                                                               | 类型             | 默认值       |
+|------------------|----------------------------------------------------------------------------------------------------|------------------|--------------|
+| v-model          | 绑定当前选中标签的标识符                                                                           | number \| string | `0`          |
+| color            | 标签选中色                                                                                         | string           | `#1a1a1a`    |
+| background       | 标签栏背景颜色                                                                                     | string           | `#f5f5f5`    |
+| direction        | 使用横纵方向 可选值 horizontal、vertical                                                           | string           | `horizontal` |
+| type             | 选中底部展示样式 可选值 line、smile                                                                | string           | `line`       |
+| swipeable`4.0.3` | 是否开启手势左右滑动切换                                                                           | boolean          | `false`      |
+| title-scroll     | 标签栏是否可以滚动                                                                                 | boolean          | `false`      |
+| ellipsis         | 是否省略过长的标题文字                                                                             | boolean          | `true`       |
+| animated-time    | 切换动画时长,单位 ms 0 代表无动画                                                                  | number \| string | `300`        |
+| title-gutter     | 标签间隙                                                                                           | number \| string | `0`          |
+| size             | 标签栏字体尺寸大小 可选值  large normal small                                                      | string           | `normal`     |
+| auto-height      | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
+| sticky           | 是否使用粘性布局                                                                                   | boolean          | `false`      |
+| top              | 粘性布局下的吸顶距离                                                                               | number           | `0`          |
+| name             | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                                           | string           | ''           |
 
 ### Tabs Slots
 
@@ -385,24 +416,24 @@ export default {
 
 组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。
 
-| 名称                                                | 默认值                                                                             |
-|-----------------------------------------------------|------------------------------------------------------------------------------------|
-| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                         |
-| --nut-tabs-titles-border-radius                     | _0_                                                                                |
-| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                           |
-| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                           |
-| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                           |
-| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                            |
+| 名称                                                | 默认值                                                                         |
+|-----------------------------------------------------|--------------------------------------------------------------------------------|
+| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                     |
+| --nut-tabs-titles-border-radius                     | _0_                                                                            |
+| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                       |
+| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                       |
+| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                       |
+| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                        |
 | --nut-tabs-horizontal-tab-line-color                | _linear-gradient(90deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_  |
-| --nut-tabs-horizontal-titles-height                 | _46px_                                                                             |
-| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                             |
-| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                             |
+| --nut-tabs-horizontal-titles-height                 | _46px_                                                                         |
+| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                         |
+| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                         |
 | --nut-tabs-vertical-tab-line-color                  | _linear-gradient(180deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_ |
-| --nut-tabs-vertical-titles-item-height              | _40px_                                                                             |
-| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                             |
-| --nut-tabs-vertical-titles-width                    | _100px_                                                                            |
-| --nut-tabs-titles-item-line-border-radius           | _0_                                                                                |
-| --nut-tabs-titles-item-line-opacity                 | _1_                                                                                |
+| --nut-tabs-vertical-titles-item-height              | _40px_                                                                         |
+| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                         |
+| --nut-tabs-vertical-titles-width                    | _100px_                                                                        |
+| --nut-tabs-titles-item-line-border-radius           | _0_                                                                            |
+| --nut-tabs-titles-item-line-opacity                 | _1_                                                                            |
 

+ 64 - 34
src/packages/__VUE/tabs/doc.taro.md

@@ -44,6 +44,35 @@ export default {
 </script>
 ```
 :::
+### 手势滑动切换
+:::demo
+```html
+<template>
+<nut-tabs v-model="state.tab1value" swipeable>
+  <nut-tab-pane title="Tab 1">
+    Tab 1
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 2">
+    Tab 2
+  </nut-tab-pane>
+  <nut-tab-pane title="Tab 3">
+    Tab 3
+  </nut-tab-pane>
+</nut-tabs>
+</template>
+<script lang="ts">
+import { reactive } from 'vue';
+export default {
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+    });
+    return { state };
+  }
+};
+</script>
+```
+:::
 
 ### 基础用法-微笑曲线
 :::demo
@@ -201,7 +230,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab5value" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -227,7 +256,7 @@ export default {
 ```html
 <template>
 <nut-tabs style="height:300px" v-model="state.tab6value" type="smile" title-scroll direction="vertical">
-  <nut-tab-pane v-for="item in state.list5" :title="'Tab '+ item">
+  <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
 </nut-tabs>
@@ -333,20 +362,21 @@ export default {
 
 ### Tabs Props
 
-| 参数          | 说明                                                                                               | 类型             | 默认值     |
-|---------------|----------------------------------------------------------------------------------------------------|------------------|------------|
-| v-model       | 绑定当前选中标签的标识符                                                                           | number \| string | `0`          |
-| color         | 标签选中色                                                                                         | string           | `#1a1a1a `   |
-| background    | 标签栏背景颜色                                                                                     | string           | `#f5f5f5`    |
-| direction     | 使用横纵方向 可选值 horizontal、vertical                                                           | string           | `horizontal` |
-| type          | 选中底部展示样式 可选值 line、smile                                                                | string           | `line`       |
-| title-scroll  | 标签栏是否可以滚动                                                                                 | boolean          | `false`      |
-| ellipsis      | 是否省略过长的标题文字                                                                             | boolean          | `true`       |
-| animated-time | 切换动画时长,单位 ms 0 代表无动画                                                                  | number \| string | `300`        |
-| title-gutter  | 标签间隙                                                                                           | number \| string | `0`          |
-| size          | 标签栏字体尺寸大小 可选值  large normal small                                                      | string           | `normal`     |
-| auto-height   | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
-| name        | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                              | string | ''        |
+| 参数             | 说明                                                                                               | 类型             | 默认值       |
+|------------------|----------------------------------------------------------------------------------------------------|------------------|--------------|
+| v-model          | 绑定当前选中标签的标识符                                                                           | number \| string | `0`          |
+| color            | 标签选中色                                                                                         | string           | `#1a1a1a `   |
+| background       | 标签栏背景颜色                                                                                     | string           | `#f5f5f5`    |
+| direction        | 使用横纵方向 可选值 horizontal、vertical                                                           | string           | `horizontal` |
+| type             | 选中底部展示样式 可选值 line、smile                                                                | string           | `line`       |
+| swipeable`4.0.3` | 是否开启手势左右滑动切换                                                                           | boolean          | `false`      |
+| title-scroll     | 标签栏是否可以滚动                                                                                 | boolean          | `false`      |
+| ellipsis         | 是否省略过长的标题文字                                                                             | boolean          | `true`       |
+| animated-time    | 切换动画时长,单位 ms 0 代表无动画(_小程序场景数据过大建议设置0,解决切换卡顿问题_)                 | number \| string | `300`        |
+| title-gutter     | 标签间隙                                                                                           | number \| string | `0`          |
+| size             | 标签栏字体尺寸大小 可选值  large normal small                                                      | string           | `normal`     |
+| auto-height      | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
+| name             | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                                           | string           | ''           |
 
 ### Tabs Slots
 
@@ -383,24 +413,24 @@ export default {
 
 组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。
 
-| 名称                                                | 默认值                                                                             |
-|-----------------------------------------------------|------------------------------------------------------------------------------------|
-| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                         |
-| --nut-tabs-titles-border-radius                     | _0_                                                                                |
-| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                           |
-| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                           |
-| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                           |
-| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                           |
-| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                            |
+| 名称                                                | 默认值                                                                         |
+|-----------------------------------------------------|--------------------------------------------------------------------------------|
+| --nut-tabs-tab-smile-color                          | _var(--nut-primary-color)_                                                     |
+| --nut-tabs-titles-border-radius                     | _0_                                                                            |
+| --nut-tabs-titles-item-large-font-size              | _var(--nut-font-size-3)_                                                       |
+| --nut-tabs-titles-item-font-size                    | _var(--nut-font-size-2)_                                                       |
+| --nut-tabs-titles-item-small-font-size              | _var(--nut-font-size-1)_                                                       |
+| --nut-tabs-titles-item-color                        | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-item-active-color                 | _var(--nut-title-color)_                                                       |
+| --nut-tabs-titles-background-color                  | _var(--nut-help-color)_                                                        |
 | --nut-tabs-horizontal-tab-line-color                | _linear-gradient(90deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_  |
-| --nut-tabs-horizontal-titles-height                 | _46px_                                                                             |
-| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                             |
-| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                             |
+| --nut-tabs-horizontal-titles-height                 | _46px_                                                                         |
+| --nut-tabs-horizontal-titles-item-min-width         | _50px_                                                                         |
+| --nut-tabs-horizontal-titles-item-active-line-width | _40px_                                                                         |
 | --nut-tabs-vertical-tab-line-color                  | _linear-gradient(180deg,var(--nut-primary-color) 0%,rgba(#fa2c19, 0.15) 100%)_ |
-| --nut-tabs-vertical-titles-item-height              | _40px_                                                                             |
-| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                             |
-| --nut-tabs-vertical-titles-width                    | _100px_                                                                            |
-| --nut-tabs-titles-item-line-border-radius           | _0_                                                                                |
-| --nut-tabs-titles-item-line-opacity                 | _1_                                                                                |
+| --nut-tabs-vertical-titles-item-height              | _40px_                                                                         |
+| --nut-tabs-vertical-titles-item-active-line-height  | _14px_                                                                         |
+| --nut-tabs-vertical-titles-width                    | _100px_                                                                        |
+| --nut-tabs-titles-item-line-border-radius           | _0_                                                                            |
+| --nut-tabs-titles-item-line-opacity                 | _1_                                                                            |
 

+ 114 - 0
src/packages/__VUE/tabs/hooks.ts

@@ -0,0 +1,114 @@
+import { onMounted, reactive, ref } from 'vue';
+import { useTouch } from '@/packages/utils/useTouch';
+import { useTaroRect } from '@/packages/utils/useTaroRect';
+import requestAniFrame from '@/packages/utils/raf';
+import Taro from '@tarojs/taro';
+type TouchPosition = 'left' | 'right' | 'top' | 'bottom' | '';
+
+export const useTabContentTouch = (props: any, tabMethods: any) => {
+  const tabsContentRef = ref<HTMLElement>();
+
+  const tabsContentRefRect = ref({ width: 0, height: 0 });
+  const initTaroWidth = async () => {
+    if (Taro.getEnv() !== Taro.ENV_TYPE.WEB) {
+      let rect = await useTaroRect(tabsContentRef, Taro);
+      tabsContentRefRect.value.width = rect.width || 0;
+      tabsContentRefRect.value.height = rect.height || 0;
+    } else {
+      tabsContentRefRect.value.width = tabsContentRef.value?.clientWidth || 0;
+      tabsContentRefRect.value.height = tabsContentRef.value?.clientHeight || 0;
+    }
+  };
+
+  onMounted(() => {
+    setTimeout(() => {
+      initTaroWidth();
+    }, 100);
+  });
+  const touchState = reactive({
+    offset: 0,
+    moving: false
+  });
+  const touch = useTouch();
+  let position: TouchPosition = '';
+  const setoffset = (deltaX: number, deltaY: number) => {
+    let offset = deltaX;
+    console.log(tabsContentRefRect.value.width);
+    if (props.direction == 'horizontal') {
+      position = deltaX > 0 ? 'right' : 'left';
+      // 计算拖拽 百分比
+      offset = (Math.abs(offset) / tabsContentRefRect.value.width) * 100;
+    } else {
+      position = deltaY > 0 ? 'bottom' : 'top';
+      offset = deltaY;
+      // 计算拖拽 百分比
+      offset = (Math.abs(offset) / tabsContentRefRect.value?.height) * 100;
+    }
+    // 拖拽阈值 85%
+    if (offset > 85) {
+      offset = 85;
+    }
+    switch (position) {
+      case 'left':
+      case 'top':
+        // 起始tab拖拽拦截
+        if (tabMethods.isEnd()) {
+          offset = 0;
+          touchState.moving = false;
+        }
+        break;
+      case 'right':
+      case 'bottom':
+        offset = -offset;
+        // 末位tab拖拽拦截
+        if (tabMethods.isBegin()) {
+          offset = 0;
+          touchState.moving = false;
+        }
+        break;
+    }
+    touchState.offset = offset;
+  };
+  const touchMethods = {
+    onTouchStart(event: Event) {
+      if (!props.swipeable) return;
+      touch.start(event);
+    },
+    onTouchMove(event: Event) {
+      if (!props.swipeable) return;
+      touch.move(event);
+      touchState.moving = true;
+      setoffset(touch.deltaX.value, touch.deltaY.value);
+
+      if (props.direction == 'horizontal' && touch.isHorizontal()) {
+        event.preventDefault();
+        event.stopPropagation();
+      }
+      if (props.direction == 'vertical' && touch.isVertical()) {
+        event.preventDefault();
+        event.stopPropagation();
+      }
+    },
+    onTouchEnd() {
+      if (touchState.moving) {
+        touchState.moving = false;
+        switch (position) {
+          case 'left':
+          case 'top':
+            // 大于 35%阈值 切换至下一 Tab
+            if (touchState.offset > 35) {
+              tabMethods.next();
+            }
+            break;
+          case 'right':
+          case 'bottom':
+            if (touchState.offset < -35) {
+              tabMethods.prev();
+            }
+            break;
+        }
+      }
+    }
+  };
+  return { touchMethods, touchState, tabsContentRef };
+};

+ 72 - 25
src/packages/__VUE/tabs/index.taro.vue

@@ -29,7 +29,16 @@
         <view v-if="canShowLabel" class="nut-tabs__titles-item nut-tabs__titles-placeholder"></view>
       </template>
     </Nut-Scroll-View>
-    <view class="nut-tabs__content" :style="contentStyle">
+    <view
+      class="nut-tabs__content"
+      ref="tabsContentRef"
+      :id="'tabsContentRef-' + refRandomId"
+      :style="contentStyle"
+      @touchstart="onTouchStart"
+      @touchmove="onTouchMove"
+      @touchend="onTouchEnd"
+      @touchcancel="onTouchEnd"
+    >
       <slot name="default"></slot>
     </view>
   </view>
@@ -40,10 +49,11 @@ import { JoySmile } from '@nutui/icons-vue-taro';
 import { pxCheck } from '@/packages/utils/pxCheck';
 import { TypeOfFun } from '@/packages/utils/util';
 import NutScrollView from '../scrollView/index.taro.vue';
-import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick } from 'vue';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick, CSSProperties } from 'vue';
 import raf from '@/packages/utils/raf';
 import Taro from '@tarojs/taro';
 import type { RectItem } from './types';
+import { useTabContentTouch } from './hooks';
 export class Title {
   title: string = '';
   titleSlot?: VNode[];
@@ -87,6 +97,10 @@ export default create({
       type: Boolean,
       default: true
     },
+    swipeable: {
+      type: Boolean,
+      default: false
+    },
     autoHeight: {
       type: Boolean,
       default: false
@@ -120,8 +134,11 @@ export default create({
 
   setup(props: any, { emit, slots }: any) {
     const container = ref(null);
-    provide('activeKey', { activeKey: computed(() => props.modelValue) });
-    provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
+    provide('tabsOpiton', {
+      activeKey: computed(() => props.modelValue || '0'),
+      autoHeight: computed(() => props.autoHeight),
+      animatedTime: computed(() => props.animatedTime)
+    });
     const titles: Ref<Title[]> = ref([]);
     const renderTitles = (vnodes: VNode[]) => {
       vnodes.forEach((vnode: VNode, index: number) => {
@@ -156,9 +173,9 @@ export default create({
     const findTabsIndex = (value: string | number) => {
       let index = titles.value.findIndex((item) => item.paneKey == value);
       if (titles.value.length == 0) {
-        console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
+        console.warn('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
       } else if (index == -1) {
-        // console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+        // console.warn('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
       } else {
         currentIndex.value = index;
       }
@@ -268,14 +285,54 @@ export default create({
     );
     onMounted(init);
     onActivated(init);
+    const tabMethods = {
+      isBegin: () => {
+        return currentIndex.value == 0;
+      },
+      isEnd: () => {
+        return currentIndex.value == titles.value.length - 1;
+      },
+      next: () => {
+        currentIndex.value += 1;
+        tabMethods.updateValue(titles.value[currentIndex.value]);
+      },
+      prev: () => {
+        currentIndex.value -= 1;
+        tabMethods.updateValue(titles.value[currentIndex.value]);
+      },
+      updateValue: (item: Title) => {
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      },
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled || currentIndex.value == index) {
+          return;
+        }
+        currentIndex.value = index;
+        tabMethods.updateValue(item);
+      },
+      setTabItemRef: (el: HTMLElement, index: number) => {
+        titleRef.value[index] = el;
+      }
+    };
+    const { tabsContentRef, touchState, touchMethods } = useTabContentTouch(props, tabMethods);
     const contentStyle = computed(() => {
-      return {
+      let offsetPercent = currentIndex.value * 100;
+      if (touchState.moving) {
+        offsetPercent += touchState.offset;
+      }
+      let style: CSSProperties = {
         transform:
           props.direction == 'horizontal'
-            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
-            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
-        transitionDuration: `${props.animatedTime}ms`
+            ? `translate3d(-${offsetPercent}%, 0, 0)`
+            : `translate3d( 0,-${offsetPercent}%, 0)`,
+        transitionDuration: touchState.moving ? undefined : `${props.animatedTime}ms`
       };
+      if (props.animatedTime == 0) {
+        style = {};
+      }
+      return style;
     });
     const tabsNavStyle = computed(() => {
       return {
@@ -294,22 +351,10 @@ export default create({
         marginRight: pxCheck(props.titleGutter)
       };
     });
-    const methods = {
-      tabChange: (item: Title, index: number) => {
-        emit('click', item);
-        if (item.disabled || currentIndex.value == index) {
-          return;
-        }
-        currentIndex.value = index;
-        emit('update:modelValue', item.paneKey);
-        emit('change', item);
-      },
-      setTabItemRef: (el: HTMLElement, index: number) => {
-        titleRef.value[index] = el;
-      }
-    };
+    const refRandomId = Math.random().toString(36).slice(-8);
     return {
       titles,
+      tabsContentRef,
       contentStyle,
       tabsNavStyle,
       titleStyle,
@@ -318,7 +363,9 @@ export default create({
       scrollLeft,
       scrollWithAnimation,
       canShowLabel,
-      ...methods
+      refRandomId,
+      ...tabMethods,
+      ...touchMethods
     };
   }
 });

+ 71 - 25
src/packages/__VUE/tabs/index.vue

@@ -55,7 +55,15 @@
         </template>
       </view>
     </template>
-    <view class="nut-tabs__content" :style="contentStyle">
+    <view
+      class="nut-tabs__content"
+      ref="tabsContentRef"
+      :style="contentStyle"
+      @touchstart="onTouchStart"
+      @touchmove="onTouchMove"
+      @touchend="onTouchEnd"
+      @touchcancel="onTouchEnd"
+    >
       <slot name="default"></slot>
     </view>
   </view>
@@ -65,7 +73,7 @@ import { createComponent } from '@/packages/utils/create';
 import { pxCheck } from '@/packages/utils/pxCheck';
 import { TypeOfFun } from '@/packages/utils/util';
 import { useRect } from '@/packages/utils/useRect';
-import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick } from 'vue';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick, CSSProperties } from 'vue';
 import raf from '@/packages/utils/raf';
 export class Title {
   title: string = '';
@@ -78,6 +86,7 @@ export type TabsSize = 'large' | 'normal' | 'small';
 import Sticky from '../sticky/index.vue';
 const { create } = createComponent('tabs');
 import { JoySmile } from '@nutui/icons-vue';
+import { useTabContentTouch } from './hooks';
 export default create({
   components: { [Sticky.name]: Sticky, JoySmile },
   props: {
@@ -109,6 +118,10 @@ export default create({
       type: Boolean,
       default: true
     },
+    swipeable: {
+      type: Boolean,
+      default: false
+    },
     autoHeight: {
       type: Boolean,
       default: false
@@ -139,8 +152,12 @@ export default create({
   setup(props: any, { emit, slots }: any) {
     const container = ref(null);
     let stickyFixed: boolean;
-    provide('activeKey', { activeKey: computed(() => props.modelValue) });
-    provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
+    provide('tabsOpiton', {
+      activeKey: computed(() => props.modelValue || '0'),
+      autoHeight: computed(() => props.autoHeight),
+      animatedTime: computed(() => props.animatedTime)
+    });
+
     const titles: Ref<Title[]> = ref([]);
     const renderTitles = (vnodes: VNode[]) => {
       vnodes.forEach((vnode: VNode, index: number) => {
@@ -175,9 +192,9 @@ export default create({
     const findTabsIndex = (value: string | number) => {
       let index = titles.value.findIndex((item) => item.paneKey == value);
       if (titles.value.length == 0) {
-        console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
+        console.warn('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
       } else if (index == -1) {
-        // console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+        // console.warn('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
       } else {
         currentIndex.value = index;
       }
@@ -253,14 +270,54 @@ export default create({
     );
     onMounted(init);
     onActivated(init);
+    const tabMethods = {
+      isBegin: () => {
+        return currentIndex.value == 0;
+      },
+      isEnd: () => {
+        return currentIndex.value == titles.value.length - 1;
+      },
+      next: () => {
+        currentIndex.value += 1;
+        tabMethods.updateValue(titles.value[currentIndex.value]);
+      },
+      prev: () => {
+        currentIndex.value -= 1;
+        tabMethods.updateValue(titles.value[currentIndex.value]);
+      },
+      updateValue: (item: Title) => {
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      },
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled || currentIndex.value == index) {
+          return;
+        }
+        currentIndex.value = index;
+        tabMethods.updateValue(item);
+      },
+      setTabItemRef: (el: HTMLElement, index: number) => {
+        titleRef.value[index] = el;
+      }
+    };
+    const { tabsContentRef, touchState, touchMethods } = useTabContentTouch(props, tabMethods);
     const contentStyle = computed(() => {
-      return {
+      let offsetPercent = currentIndex.value * 100;
+      if (touchState.moving) {
+        offsetPercent += touchState.offset;
+      }
+      let style: CSSProperties = {
         transform:
           props.direction == 'horizontal'
-            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
-            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
-        transitionDuration: `${props.animatedTime}ms`
+            ? `translate3d(-${offsetPercent}%, 0, 0)`
+            : `translate3d( 0,-${offsetPercent}%, 0)`,
+        transitionDuration: touchState.moving ? undefined : `${props.animatedTime}ms`
       };
+      if (props.animatedTime == 0) {
+        style = {};
+      }
+      return style;
     });
     const tabsNavStyle = computed(() => {
       return {
@@ -279,22 +336,10 @@ export default create({
         marginRight: pxCheck(props.titleGutter)
       };
     });
-    const methods = {
-      tabChange: (item: Title, index: number) => {
-        emit('click', item);
-        if (item.disabled || currentIndex.value == index) {
-          return;
-        }
-        currentIndex.value = index;
-        emit('update:modelValue', item.paneKey);
-        emit('change', item);
-      },
-      setTabItemRef: (el: HTMLElement, index: number) => {
-        titleRef.value[index] = el;
-      }
-    };
+
     return {
       navRef,
+      tabsContentRef,
       titles,
       contentStyle,
       tabsNavStyle,
@@ -302,7 +347,8 @@ export default create({
       tabsActiveStyle,
       container,
       onStickyScroll,
-      ...methods
+      ...tabMethods,
+      ...touchMethods
     };
   }
 });

+ 10 - 4
src/sites/mobile-taro/vue/src/nav/pages/tabs/index.vue

@@ -1,12 +1,18 @@
 <template>
   <div class="demo full" :class="{ web: env === 'WEB' }">
-    <Header v-if="env === 'WEB'" />
+    <Header v-if="env === 'WEB'"></Header>
     <h2>基础用法</h2>
     <nut-tabs v-model="state.tab1value">
       <nut-tab-pane title="Tab 1"> Tab 1 </nut-tab-pane>
       <nut-tab-pane title="Tab 2"> Tab 2 </nut-tab-pane>
       <nut-tab-pane title="Tab 3"> Tab 3 </nut-tab-pane>
     </nut-tabs>
+    <h2>手势滑动切换</h2>
+    <nut-tabs v-model="state.tab1value" swipeable>
+      <nut-tab-pane title="Tab 1"> Tab 1 </nut-tab-pane>
+      <nut-tab-pane title="Tab 2"> Tab 2 </nut-tab-pane>
+      <nut-tab-pane title="Tab 3"> Tab 3 </nut-tab-pane>
+    </nut-tabs>
     <h2>基础用法-微笑曲线</h2>
     <nut-tabs v-model="state.tab11value" type="smile">
       <nut-tab-pane title="Tab 1"> Tab 1 </nut-tab-pane>
@@ -37,15 +43,15 @@
 
     <h2>数量多,滚动操作</h2>
     <nut-tabs v-model="state.tab4value" title-scroll title-gutter="10" name="tab4value">
-      <nut-tab-pane v-for="item in state.list4" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
+      <nut-tab-pane v-for="item in state.list4" :pane-key="item" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>左右布局</h2>
     <nut-tabs style="height: 300px" v-model="state.tab5value" title-scroll direction="vertical">
-      <nut-tab-pane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
+      <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>左右布局-微笑曲线</h2>
     <nut-tabs style="height: 300px" v-model="state.tab6value" type="smile" title-scroll direction="vertical">
-      <nut-tab-pane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
+      <nut-tab-pane v-for="item in state.list5" :pane-key="item" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>标签栏字体尺寸 large normal small </h2>
     <nut-tabs v-model="state.tab1value" size="large">