Browse Source

feat: tabs、tabpane

richard1015 4 years ago
parent
commit
0cb6528fc4

+ 26 - 2
src/config.json

@@ -527,8 +527,9 @@
           "sort": 1,
           "cName": "标签组件",
           "type": "component",
-          "show": true,
-          "taro": true,
+          "show": false,
+          "exportEmpty": true,
+          "taro": false,
           "desc": "标签组件",
           "author": "zhenyulei"
         },
@@ -609,6 +610,29 @@
           "show": true,
           "taro": true,
           "author": "yushuang24"
+        },
+        {
+          "version": "3.1.9",
+          "name": "Tabs",
+          "type": "component",
+          "cName": "选项卡切换",
+          "desc": "常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式",
+          "sort": 11,
+          "show": true,
+          "taro": true,
+          "author": "richard1015"
+        },
+        {
+          "version": "3.1.9",
+          "name": "TabPane",
+          "type": "component",
+          "cName": "选项卡子组件",
+          "desc": "",
+          "sort": 12,
+          "exportEmpty": true,
+          "taro": true,
+          "show": false,
+          "author": "richard1015"
         }
       ]
     },

+ 12 - 0
src/packages/__VUE/tabpane/index.scss

@@ -0,0 +1,12 @@
+.nut-tabpane {
+  width: 100%;
+  flex-shrink: 0;
+  display: block;
+  background-color: #fff;
+  padding: 24px 20px;
+  box-sizing: border-box;
+  overflow: auto;
+  &.active {
+    // overflow: visible;
+  }
+}

+ 32 - 0
src/packages/__VUE/tabpane/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <view class="nut-tabpane" :class="{ active: paneKey == activeKey }">
+    <slot></slot>
+  </view>
+</template>
+<script lang="ts">
+import { inject } from 'vue';
+import { createComponent } from '../../utils/create';
+const { create } = createComponent('tabpane');
+
+export default create({
+  props: {
+    title: {
+      type: [String, Number],
+      default: ''
+    },
+    paneKey: {
+      type: [String, Number],
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['click'],
+  setup(props, { emit }) {
+    const parent = inject('activeKey') as any;
+    return { activeKey: parent.activeKey };
+  }
+});
+</script>

+ 100 - 0
src/packages/__VUE/tabs/demo.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-tabs v-model="state.tab1value">
+      <nut-tabpane title="Tab 1"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>基础用法-微笑曲线</h2>
+    <nut-tabs v-model="state.tab11value" type="smile">
+      <nut-tabpane title="Tab 1"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>通过 pane-key 匹配</h2>
+    <nut-tabs v-model="state.tab2value">
+      <nut-tabpane title="Tab 1" pane-key="0"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2" pane-key="1" :disabled="true"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3" pane-key="2"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>数据异步渲染 3s</h2>
+    <nut-tabs v-model="state.tab3value">
+      <nut-tabpane v-for="item in state.list3" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+
+    <h2>数量多,滚动操作</h2>
+    <nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
+      <nut-tabpane v-for="item in state.list4" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+    <h2>左右布局</h2>
+    <nut-tabs style="height: 300px" v-model="state.tab5value" title-scroll direction="vertical">
+      <nut-tabpane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+    <h2>自定义标签栏</h2>
+    <nut-tabs v-model="state.tab6value">
+      <template v-slot:titles>
+        <div
+          class="nut-tabs__titles-item"
+          @click="state.tab6value = item.paneKey"
+          :class="{ active: state.tab6value == item.paneKey }"
+          :key="item.paneKey"
+          v-for="item in state.list6"
+        >
+          <nut-icon v-if="item.icon" :name="item.icon" />
+          <span class="nut-tabs__titles-item__text">{{ item.title }}</span>
+        </div>
+      </template>
+      <nut-tabpane v-for="item in state.list6" :pane-key="item.paneKey">
+        {{ item.title }}
+      </nut-tabpane>
+    </nut-tabs>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('tabs');
+export default createDemo({
+  props: {},
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+      tab11value: '0',
+      tab2value: '0',
+      tab3value: '0',
+      tab4value: '0',
+      tab5value: '0',
+      tab6value: 'c1',
+      list3: Array.from(new Array(2).keys()),
+      list4: Array.from(new Array(10).keys()),
+      list5: Array.from(new Array(2).keys()),
+      list6: [
+        {
+          title: '自定义 1',
+          paneKey: 'c1',
+          icon: 'dongdong'
+        },
+        {
+          title: '自定义 2',
+          paneKey: 'c2',
+          icon: 'JD'
+        },
+        {
+          title: '自定义 3',
+          paneKey: 'c3'
+        }
+      ]
+    });
+    setTimeout(() => {
+      state.list3.push(999);
+      state.tab3value = '2';
+    }, 3000);
+
+    return { state };
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 202 - 0
src/packages/__VUE/tabs/doc.md

@@ -0,0 +1,202 @@
+# Tabs 组件
+
+### 介绍
+
+常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+// vue
+import { Tabs, TabPane } from '@nutui/nutui';
+// taro
+import { Tabs, TabPane } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Tabs);
+app.use(TabPane);
+```
+
+### 基础用法
+
+``` html
+<nut-tabs v-model="state.tab1value">
+  <nut-tabpane title="Tab 1">
+    Tab 1
+  </nut-tabpane>
+  <nut-tabpane title="Tab 2">
+    Tab 2
+  </nut-tabpane>
+  <nut-tabpane title="Tab 3">
+    Tab 3
+  </nut-tabpane>
+</nut-tabs>
+```
+
+``` javascript
+import { reactive } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+      tab11value: '0',
+      tab2value: '0',
+      tab3value: '0',
+      tab4value: '0',
+      tab5value: '0',
+      tab6value: 'c1',
+      list3: Array.from(new Array(2).keys()),
+      list4: Array.from(new Array(10).keys()),
+      list5: Array.from(new Array(2).keys()),
+      list6: [
+        {
+          title: '自定义 1',
+          paneKey: 'c1',
+          icon: 'dongdong'
+        },
+        {
+          title: '自定义 2',
+          paneKey: 'c2',
+          icon: 'JD'
+        },
+        {
+          title: '自定义 3',
+          paneKey: 'c3'
+        }
+      ]
+    });
+    setTimeout(() => {
+      state.list3.push(999);
+      state.tab3value = '2';
+    }, 3000);
+
+    return { state };
+  }
+};
+```
+
+### 基础用法-微笑曲线
+
+``` html
+<nut-tabs v-model="state.tab11value" type="smile">
+  <nut-tabpane title="Tab 1">
+    Tab 1
+  </nut-tabpane>
+  <nut-tabpane title="Tab 2">
+    Tab 2
+  </nut-tabpane>
+  <nut-tabpane title="Tab 3">
+    Tab 3
+  </nut-tabpane>
+</nut-tabs>
+```
+
+### 通过 pane-key 匹配
+
+``` html
+<nut-tabs v-model="state.tab2value">
+  <nut-tabpane title="Tab 1" pane-key="0">
+    Tab 1
+  </nut-tabpane>
+  <nut-tabpane title="Tab 2" pane-key="1" :disabled="true">
+    Tab 2
+  </nut-tabpane>
+  <nut-tabpane title="Tab 3" pane-key="2">
+    Tab 3
+  </nut-tabpane>
+</nut-tabs>
+```
+
+### 数据异步渲染 3s
+
+``` html
+<nut-tabs v-model="state.tab3value">
+  <nut-tabpane v-for="item in state.list3" :title="'Tab '+ item">
+    Tab {{item}}
+  </nut-tabpane>
+</nut-tabs>
+```
+
+### 数量多,滚动操作
+
+``` html
+<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
+  <nut-tabpane v-for="item in state.list4" :title="'Tab '+ item">
+    Tab {{item}}
+  </nut-tabpane>
+</nut-tabs>
+```
+
+### 左右布局
+
+``` html
+<nut-tabs style="height:300px" v-model="state.tab5value" title-scroll direction="vertical">
+  <nut-tabpane v-for="item in state.list5" :title="'Tab '+ item">
+    Tab {{item}}
+  </nut-tabpane>
+</nut-tabs>
+```
+
+### 自定义标签栏
+``` html
+<nut-tabs v-model="state.tab6value">
+  <template v-slot:titles>
+    <div class="nut-tabs__titles-item" @click="state.tab6value=item.paneKey"
+      :class="{active:state.tab6value==item.paneKey}" :key="item.paneKey" v-for="item in state.list6">
+      <nut-icon v-if="item.icon" :name="item.icon" />
+      <span class="nut-tabs__titles-item__text">{{item.title}}</span>
+    </div>
+  </template>
+  <nut-tabpane v-for="item in state.list6" :pane-key="item.paneKey">
+    {{item.title}}
+  </nut-tabpane>
+</nut-tabs>
+```
+
+## API
+
+### 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          |
+
+## Tabs Slots
+
+| 名称    | 说明           |
+|---------|----------------|
+| titles  | 自定义导航区域 |
+| default | 自定义内容     |
+
+### Tabpane Props
+
+| 参数     | 说明                    | 类型    | 默认值           |
+|----------|-------------------------|---------|------------------|
+| title    | 标题                    | string  |                  |
+| pane-key | 标签 Key , 匹配的标识符 | string  | 默认索引0,1,2... |
+| disabled | 是否禁用标签            | boolean | false            |
+
+
+## Tabpane Slots
+
+| 名称    | 说明       |
+|---------|------------|
+| default | 自定义内容 |
+
+### Tabs Events
+
+| 事件名 | 说明                     | 回调参数                 |
+|--------|--------------------------|--------------------------|
+| click  | 点击标签时触发           | {title,paneKey,disabled} |
+| change | 当前激活的标签改变时触发 | {title,paneKey,disabled} |
+

+ 121 - 0
src/packages/__VUE/tabs/index.scss

@@ -0,0 +1,121 @@
+.nut-tabs {
+  display: flex;
+  overflow: hidden;
+  &.horizontal {
+    flex-direction: column;
+  }
+  &.vertical {
+    flex-direction: row;
+    width: 100%;
+    .nut-tabs__titles {
+      flex-direction: column;
+      height: 100%;
+      padding: 10px 0 !important;
+      width: $tabs-vertical-titles-width;
+      flex-shrink: 0;
+      &.scrollable {
+        overflow-x: hidden;
+        overflow-y: auto;
+      }
+      &-item {
+        height: $tabs-vertical-titles-item-height;
+        flex: none;
+        &.active {
+          &::before {
+            right: 0;
+            bottom: 0;
+            left: auto;
+            width: 11px;
+            height: 10px;
+            background-size: 100% 100%;
+            background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/197875/38/105/620/60ffcd30E34877e77/54c3dd9fe0a5ab76.png');
+          }
+        }
+      }
+    }
+    .nut-tabs__content {
+      flex: 1;
+      flex-direction: column;
+      .nut-tabpane {
+        height: 100%;
+      }
+    }
+  }
+
+  &__titles {
+    height: $tabs-horizontal-titles-height;
+    padding: 0 10px;
+    display: flex;
+    user-select: none;
+    background: $tabs-titles-background-color;
+    border-radius: $tabs-titles-border-radius;
+    &::-webkit-scrollbar {
+      display: none;
+      width: 0;
+      background: transparent;
+    }
+
+    &.smile {
+      .nut-tabs__titles-item {
+        &.active {
+          &::before {
+            width: 12px;
+            height: 4px;
+            background-size: 100% 100%;
+            background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/127200/40/18747/536/5fb36b5aE61cac2d8/638032e8da9b93f4.png');
+          }
+        }
+      }
+    }
+    &.scrollable {
+      overflow-x: auto;
+      overflow-y: hidden;
+      .nut-tabs__titles-item {
+        width: auto;
+      }
+    }
+    &-item {
+      position: relative;
+      font-size: $tabs-titles-item-font-size;
+      flex: 1 0 auto;
+      min-width: $tabs-horizontal-titles-item-min-width;
+      width: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: $tabs-titles-item-color;
+      &__text {
+        &.ellipsis {
+          @include oneline-ellipsis();
+        }
+        text-align: center;
+      }
+      &::before {
+        position: absolute;
+        transition: width 0.3s ease;
+        width: 0;
+        content: ' ';
+      }
+      &.disabled {
+        color: $disable-color;
+      }
+      &.active {
+        font-weight: 600;
+        &::before {
+          position: absolute;
+          bottom: 15%;
+          left: 50%;
+          transform: translateX(-50%);
+          content: ' ';
+          width: $tabs-horizontal-titles-item-active-line-width;
+          height: 3px;
+          background: $tabs-tab-line;
+        }
+      }
+    }
+  }
+  &__content {
+    display: flex;
+    box-sizing: border-box;
+  }
+}

+ 176 - 0
src/packages/__VUE/tabs/index.taro.vue

@@ -0,0 +1,176 @@
+<template>
+  <view class="nut-tabs" :class="[direction]">
+    <view class="nut-tabs__titles" :class="{ [type]: type, scrollable: titleScroll }" :style="tabsNavStyle">
+      <slot v-if="$slots.titles" name="titles"></slot>
+      <template v-else>
+        <view
+          class="nut-tabs__titles-item"
+          :style="titleStyle"
+          @click="tabChange(item, index)"
+          :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
+          v-for="(item, index) in titles"
+          :key="item.paneKey"
+        >
+          <view
+            class="nut-tabs__titles-item__text"
+            :class="{ ellipsis: ellipsis && !titleScroll && direction == 'horizontal' }"
+            >{{ item.title }}
+          </view>
+        </view>
+      </template>
+    </view>
+    <view class="nut-tabs__content" :style="contentStyle">
+      <slot name="default"></slot>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { pxCheck } from '../../utils/pxCheck';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+const { create } = createComponent('tabs');
+class Title {
+  title: string = '';
+  titleSlot?: VNode[];
+  paneKey: string = '';
+  disabled: boolean = false;
+  constructor() {}
+}
+export default create({
+  props: {
+    modelValue: {
+      type: [String, Number],
+      default: 0
+    },
+    color: {
+      type: String,
+      default: ''
+    },
+    direction: {
+      type: String,
+      default: 'horizontal' //vertical
+    },
+    type: {
+      type: String,
+      default: 'line' //card、line、smile
+    },
+    lineWidth: {
+      type: [String, Number],
+      default: ''
+    },
+    titleScroll: {
+      type: Boolean,
+      default: false
+    },
+    ellipsis: {
+      type: Boolean,
+      default: true
+    },
+    background: {
+      type: String,
+      default: ''
+    },
+    animatedTime: {
+      type: [Number, String],
+      default: 300
+    },
+    titleGutter: {
+      type: [Number, String],
+      default: 0
+    }
+  },
+
+  components: {},
+  emits: ['update:modelValue', 'click', 'change'],
+
+  setup(props, { emit, slots }) {
+    provide('activeKey', { activeKey: computed(() => props.modelValue) });
+    const titles: Ref<Title[]> = ref([]);
+    const currentIndex = ref(0);
+
+    const renderTitles = (vnodes: VNode[]) => {
+      vnodes.forEach((vnode: VNode, index: number) => {
+        let type = vnode.type;
+        type = (type as any).name || type;
+        if (type == 'nut-tabpane') {
+          let title = new Title();
+          if (vnode.props?.title || vnode.props?.['pane-key']) {
+            title.title = vnode.props?.title;
+            title.paneKey = vnode.props?.['pane-key'] || index;
+            title.disabled = vnode.props?.disabled;
+          } else {
+            // title.titleSlot = vnode.children?.title() as VNode[];
+          }
+          titles.value.push(title);
+        } else {
+          renderTitles(vnode.children as VNode[]);
+        }
+      });
+    };
+    const init = (vnodes: VNode[] = slots.default()) => {
+      titles.value = [];
+      if (vnodes.length) {
+        renderTitles(vnodes);
+      }
+    };
+    watch(
+      () => slots.default(),
+      (vnodes: VNode[]) => {
+        init(vnodes);
+      }
+    );
+    watch(
+      () => props.modelValue,
+      (value: string) => {
+        let index = titles.value.findIndex((item) => item.paneKey == value);
+        if (index == -1) {
+          console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+        } else {
+          currentIndex.value = index;
+        }
+      }
+    );
+    onMounted(init);
+    onActivated(init);
+    const contentStyle = computed(() => {
+      return {
+        transform:
+          props.direction == 'horizontal'
+            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
+            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
+        transitionDuration: `${props.animatedTime}ms`
+      };
+    });
+    const tabsNavStyle = computed(() => {
+      return {
+        background: props.background
+      };
+    });
+    const titleStyle = computed(() => {
+      return {
+        marginLeft: pxCheck(props.titleGutter),
+        marginRight: pxCheck(props.titleGutter)
+      };
+    });
+    const methods = {
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled) {
+          return;
+        }
+        currentIndex.value = index;
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      }
+    };
+
+    return {
+      titles,
+      contentStyle,
+      tabsNavStyle,
+      titleStyle,
+      ...methods
+    };
+  }
+});
+</script>

+ 168 - 0
src/packages/__VUE/tabs/index.vue

@@ -0,0 +1,168 @@
+<template>
+  <view class="nut-tabs" :class="[direction]">
+    <view class="nut-tabs__titles" :class="{ [type]: type, scrollable: titleScroll }" :style="tabsNavStyle">
+      <slot v-if="$slots.titles" name="titles"></slot>
+      <template v-else>
+        <view
+          class="nut-tabs__titles-item"
+          :style="titleStyle"
+          @click="tabChange(item, index)"
+          :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
+          v-for="(item, index) in titles"
+          :key="item.paneKey"
+        >
+          <view
+            class="nut-tabs__titles-item__text"
+            :class="{ ellipsis: ellipsis && !titleScroll && direction == 'horizontal' }"
+            >{{ item.title }}
+          </view>
+        </view>
+      </template>
+    </view>
+    <view class="nut-tabs__content" :style="contentStyle">
+      <slot name="default"></slot>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { pxCheck } from '@/packages/utils/pxCheck';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+const { create } = createComponent('tabs');
+class Title {
+  title: string = '';
+  titleSlot?: VNode[];
+  paneKey: string = '';
+  disabled: boolean = false;
+  constructor() {}
+}
+export default create({
+  props: {
+    modelValue: {
+      type: [String, Number],
+      default: 0
+    },
+    direction: {
+      type: String,
+      default: 'horizontal' //vertical
+    },
+    type: {
+      type: String,
+      default: 'line' //card、line、smile
+    },
+    titleScroll: {
+      type: Boolean,
+      default: false
+    },
+    ellipsis: {
+      type: Boolean,
+      default: true
+    },
+    background: {
+      type: String,
+      default: ''
+    },
+    animatedTime: {
+      type: [Number, String],
+      default: 300
+    },
+    titleGutter: {
+      type: [Number, String],
+      default: 0
+    }
+  },
+
+  components: {},
+  emits: ['update:modelValue', 'click', 'change'],
+
+  setup(props, { emit, slots }) {
+    provide('activeKey', { activeKey: computed(() => props.modelValue) });
+    const titles: Ref<Title[]> = ref([]);
+    const currentIndex = ref(0);
+
+    const renderTitles = (vnodes: VNode[]) => {
+      vnodes.forEach((vnode: VNode, index: number) => {
+        let type = vnode.type;
+        type = (type as any).name || type;
+        if (type == 'nut-tabpane') {
+          let title = new Title();
+          if (vnode.props?.title || vnode.props?.['pane-key']) {
+            title.title = vnode.props?.title;
+            title.paneKey = vnode.props?.['pane-key'] || index;
+            title.disabled = vnode.props?.disabled;
+          } else {
+            // title.titleSlot = vnode.children?.title() as VNode[];
+          }
+          titles.value.push(title);
+        } else {
+          renderTitles(vnode.children as VNode[]);
+        }
+      });
+    };
+    const init = (vnodes: VNode[] = slots.default()) => {
+      titles.value = [];
+      if (vnodes.length) {
+        renderTitles(vnodes);
+      }
+    };
+    watch(
+      () => slots.default(),
+      (vnodes: VNode[]) => {
+        init(vnodes);
+      }
+    );
+    watch(
+      () => props.modelValue,
+      (value: string) => {
+        let index = titles.value.findIndex((item) => item.paneKey == value);
+        if (index == -1) {
+          console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+        } else {
+          currentIndex.value = index;
+        }
+      }
+    );
+    onMounted(init);
+    onActivated(init);
+    const contentStyle = computed(() => {
+      return {
+        transform:
+          props.direction == 'horizontal'
+            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
+            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
+        transitionDuration: `${props.animatedTime}ms`
+      };
+    });
+    const tabsNavStyle = computed(() => {
+      return {
+        background: props.background
+      };
+    });
+    const titleStyle = computed(() => {
+      return {
+        marginLeft: pxCheck(props.titleGutter),
+        marginRight: pxCheck(props.titleGutter)
+      };
+    });
+    const methods = {
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled) {
+          return;
+        }
+        currentIndex.value = index;
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      }
+    };
+
+    return {
+      titles,
+      contentStyle,
+      tabsNavStyle,
+      titleStyle,
+      ...methods
+    };
+  }
+});
+</script>

+ 13 - 1
src/packages/styles/variables.scss

@@ -232,7 +232,7 @@ $rang-bg-color: rgba($primary-color, 0.5) !default;
 $rang-bar-bg-color: linear-gradient(135deg, $primary-color 0%, $primary-color-end 100%) !default;
 
 //address
-$address-region-tab-line: linear-gradient(90deg, $primary-color 0%, $primary-color-end 100%) !default;
+$address-region-tab-line: linear-gradient(90deg, #f5503a 0%, #fad1cb 100%) !default;
 
 //steps
 $step-wait-bg-color: #959fb1 !default;
@@ -329,5 +329,17 @@ $pagination-item-border-width: 1px !default;
 $pagination-item-border-radius: 2px !default;
 $pagination-prev-next-padding: 0 11px !default;
 
+// tabs
+$tabs-tab-line: linear-gradient(90deg, #f5503a 0%, #fad1cb 100%) !default;
+$tabs-titles-border-radius: 0 !default;
+$tabs-titles-item-font-size: $font-size-2 !default;
+$tabs-titles-item-color: $title-color !default;
+$tabs-titles-background-color: $help-color !default;
+$tabs-horizontal-titles-height: 46px !default;
+$tabs-horizontal-titles-item-min-width: 50px !default;
+$tabs-horizontal-titles-item-active-line-width: 40px !default;
+$tabs-vertical-titles-item-height: 40px !default;
+$tabs-vertical-titles-width: 100px !default;
+
 @import './mixins/index';
 @import './animation/index';

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

@@ -44,7 +44,7 @@ export default {
       pages: [
         'pages/navbar/index',
         'pages/tabbar/index',
-        'pages/tab/index',
+        'pages/tabs/index',
         'pages/fixednav/index',
         'pages/elevator/index',
         'pages/pagination/index'

+ 0 - 3
src/sites/mobile-taro/vue/src/nav/pages/tab/index.config.ts

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

+ 0 - 189
src/sites/mobile-taro/vue/src/nav/pages/tab/index.vue

@@ -1,189 +0,0 @@
-<template>
-  <div class="demo full">
-    <h2>基础用法,默认tab-title宽度均分相等</h2>
-    <nut-tab>
-      <nut-tab-panel tab-title="全部"
-        ><p class="content">这里是页签全部内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待付款"
-        ><p class="content">这里是页签待付款内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待收获"
-        ><p class="content">这里是页签待收获内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已完成"
-        ><p class="content">这里是页签已完成内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2>defaultIndex设置默认显示tab,iconType为half时切换选中icon样式</h2>
-    <h2>switchTab监听切换tab返回事件</h2>
-    <nut-tab
-      :default-index="currIndex"
-      @switch-tab="switchTab"
-      icon-type="half"
-    >
-      <nut-tab-panel tab-title="全部"
-        ><p class="content">这里是页签全部内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待付款"
-        ><p class="content">这里是页签待付款内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待收获"
-        ><p class="content">这里是页签待收获内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已完成"
-        ><p class="content">这里是页签已完成内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2> animatedTime 开启切换标签内容时的转场动画时间</h2>
-    <nut-tab :animated-time="500">
-      <nut-tab-panel tab-title="全部"
-        ><p class="content">这里是页签全部内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待付款"
-        ><p class="content">这里是页签待付款内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待收获"
-        ><p class="content">这里是页签待收获内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已完成"
-        ><p class="content">这里是页签已完成内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2>
-      设置scrollType="scroll",标签栏可以在水平方向上滚动,切换时会自动将当前标签居中。</h2
-    >
-    <nut-tab :animated-time="500" scrollType="scroll">
-      <nut-tab-panel tab-title="全部"
-        ><p class="content">这里是页签全部内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待付款"
-        ><p class="content">这里是页签待付款内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待收获"
-        ><p class="content">这里是页签待收获内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已完成"
-        ><p class="content">这里是页签已完成内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已取消"
-        ><p class="content">这里是页签已取消内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待评价"
-        ><p class="content">这里是页签待评价内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2>设置slot:header可以自定义标签</h2>
-    <nut-tab scrollType="scroll">
-      <nut-tab-panel tab-title="全部">
-        <template v-slot:header><nut-icon name="dongdong"></nut-icon></template>
-        <p class="content">这里是页签全部内容</p>
-      </nut-tab-panel>
-      <nut-tab-panel tab-title="待付款"
-        ><p class="content">这里是页签待付款内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待收获"
-        ><p class="content">这里是页签待收获内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已完成"
-        ><p class="content">这里是页签已完成内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="已取消"
-        ><p class="content">这里是页签已取消内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="待评价"
-        ><p class="content">这里是页签待评价内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2>左右tab布局</h2>
-    <nut-tab
-      direction="vertical"
-      :animated-time="500"
-      :default-index="2"
-      scrollType="scroll"
-    >
-      <nut-tab-panel tab-title="页签一"
-        ><p class="content">这里是页签一内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签二"
-        ><p class="content">这里是页签二内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签三"
-        ><p class="content">这里是页签三内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签四"
-        ><p class="content">这里是页签四内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签五"
-        ><p class="content">这里是页签五内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签六"
-        ><p class="content">这里是页签六内容</p></nut-tab-panel
-      >
-      <nut-tab-panel tab-title="页签七"
-        ><p class="content">这里是页签七内容</p></nut-tab-panel
-      >
-    </nut-tab>
-
-    <h2>异步操作</h2>
-    <nut-tab :animated-time="500" v-if="editList.length > 0">
-      <nut-tab-panel
-        :tab-title="item.title"
-        v-for="(item, index) in editList"
-        :key="index"
-      >
-        <p class="content">这里是页签{{ index }}内容</p>
-      </nut-tab-panel>
-    </nut-tab>
-    <nut-button type="primary" @click="changeList">改变数据</nut-button>
-  </div>
-</template>
-
-<script lang="ts">
-import { ref, reactive, toRefs } from 'vue';
-export default {
-  props: {},
-  setup() {
-    const resData = reactive({
-      editList: [
-        {
-          title: '标签一'
-        },
-        {
-          title: '标签二'
-        }
-      ]
-    });
-    const currIndex = ref(0);
-    function changeList() {
-      resData.editList.push({
-        title: '标签' + resData.editList.length
-      });
-    }
-    function switchTab(activeInddex: number) {
-      console.log(activeInddex);
-    }
-    function changeIndex() {
-      let aa = currIndex.value;
-      currIndex.value = aa + 1;
-    }
-    return {
-      ...toRefs(resData),
-      changeList,
-      currIndex,
-      switchTab
-    };
-  }
-};
-</script>
-
-<style lang="scss">
-.content {
-  padding: 10px;
-}
-</style>

+ 3 - 0
src/sites/mobile-taro/vue/src/nav/pages/tabs/index.config.ts

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

+ 98 - 0
src/sites/mobile-taro/vue/src/nav/pages/tabs/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-tabs v-model="state.tab1value">
+      <nut-tabpane title="Tab 1"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>基础用法-微笑曲线</h2>
+    <nut-tabs v-model="state.tab11value" type="smile">
+      <nut-tabpane title="Tab 1"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>通过 pane-key 匹配</h2>
+    <nut-tabs v-model="state.tab2value">
+      <nut-tabpane title="Tab 1" pane-key="0"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2" pane-key="1" :disabled="true"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3" pane-key="2"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    <h2>数据异步渲染 3s</h2>
+    <nut-tabs v-model="state.tab3value">
+      <nut-tabpane v-for="item in state.list3" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+
+    <h2>数量多,滚动操作</h2>
+    <nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
+      <nut-tabpane v-for="item in state.list4" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+    <h2>左右布局</h2>
+    <nut-tabs style="height: 300px" v-model="state.tab5value" title-scroll direction="vertical">
+      <nut-tabpane v-for="item in state.list5" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
+    </nut-tabs>
+    <h2>自定义标签栏</h2>
+    <nut-tabs v-model="state.tab6value">
+      <template v-slot:titles>
+        <div
+          class="nut-tabs__titles-item"
+          @click="state.tab6value = item.paneKey"
+          :class="{ active: state.tab6value == item.paneKey }"
+          :key="item.paneKey"
+          v-for="item in state.list6"
+        >
+          <nut-icon v-if="item.icon" :name="item.icon" />
+          <span class="nut-tabs__titles-item__text">{{ item.title }}</span>
+        </div>
+      </template>
+      <nut-tabpane v-for="item in state.list6" :pane-key="item.paneKey">
+        {{ item.title }}
+      </nut-tabpane>
+    </nut-tabs>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const state = reactive({
+      tab1value: '0',
+      tab11value: '0',
+      tab2value: '0',
+      tab3value: '0',
+      tab4value: '0',
+      tab5value: '0',
+      tab6value: 'c1',
+      list3: Array.from(new Array(2).keys()),
+      list4: Array.from(new Array(10).keys()),
+      list5: Array.from(new Array(2).keys()),
+      list6: [
+        {
+          title: '自定义 1',
+          paneKey: 'c1',
+          icon: 'dongdong'
+        },
+        {
+          title: '自定义 2',
+          paneKey: 'c2',
+          icon: 'JD'
+        },
+        {
+          title: '自定义 3',
+          paneKey: 'c3'
+        }
+      ]
+    });
+    setTimeout(() => {
+      state.list3.push(999);
+      state.tab3value = '2';
+    }, 3000);
+
+    return { state };
+  }
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 1 - 0
vite.config.ts

@@ -11,6 +11,7 @@ export default defineConfig({
   base: '/3x/',
   server: {
     port: 2021,
+    host: '0.0.0.0',
     proxy: {
       '/devServer': {
         target: 'https://nutui.jd.com',