Browse Source

feat: tab页签组件初稿

zhenyulei 5 years ago
parent
commit
5779bf34d9

+ 88 - 61
package-lock.json

@@ -1684,6 +1684,11 @@
       "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
       "dev": true
     },
+    "@types/swiper": {
+      "version": "5.4.1",
+      "resolved": "https://r.cnpmjs.org/@types/swiper/download/@types/swiper-5.4.1.tgz",
+      "integrity": "sha1-TBnvV664U8JG62pzFIsbibf/Om8="
+    },
     "@types/tapable": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
@@ -2038,54 +2043,6 @@
         "tslint": "^5.20.1",
         "webpack": "^4.0.0",
         "yorkie": "^2.0.0"
-      },
-      "dependencies": {
-        "cosmiconfig": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
-          "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@types/parse-json": "^4.0.0",
-            "import-fresh": "^3.1.0",
-            "parse-json": "^5.0.0",
-            "path-type": "^4.0.0",
-            "yaml": "^1.7.2"
-          }
-        },
-        "fork-ts-checker-webpack-plugin-v5": {
-          "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
-          "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
-          "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@babel/code-frame": "^7.8.3",
-            "@types/json-schema": "^7.0.5",
-            "chalk": "^4.1.0",
-            "cosmiconfig": "^6.0.0",
-            "deepmerge": "^4.2.2",
-            "fs-extra": "^9.0.0",
-            "memfs": "^3.1.2",
-            "minimatch": "^3.0.4",
-            "schema-utils": "2.7.0",
-            "semver": "^7.3.2",
-            "tapable": "^1.0.0"
-          }
-        },
-        "schema-utils": {
-          "version": "2.7.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
-          "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@types/json-schema": "^7.0.4",
-            "ajv": "^6.12.2",
-            "ajv-keywords": "^3.4.1"
-          }
-        }
       }
     },
     "@vue/cli-plugin-vuex": {
@@ -2340,9 +2297,9 @@
           }
         },
         "vue-loader-v16": {
-          "version": "npm:vue-loader@16.0.0-rc.0",
-          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.0.tgz",
-          "integrity": "sha512-S4F7jhBQFuJbLtyjfrMYl4gBFhMACHtkZ+rJuH6/hvgxeAEAPBY5aVKn1+LuVE9+U1RGGQ9Nq/7DSR72spf0PQ==",
+          "version": "npm:vue-loader@16.0.0-rc.1",
+          "resolved": "https://r.cnpmjs.org/vue-loader/download/vue-loader-16.0.0-rc.1.tgz",
+          "integrity": "sha1-nB8WhOLQHIpyIW+dTbEZ4VxiJkU=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -2353,8 +2310,8 @@
           "dependencies": {
             "json5": {
               "version": "2.1.3",
-              "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
-              "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
+              "resolved": "http://r.cnpmjs.org/json5/download/json5-2.1.3.tgz",
+              "integrity": "sha1-ybD3+pIzv+WAf+ZvzzpWF+1ZfUM=",
               "dev": true,
               "optional": true,
               "requires": {
@@ -2363,8 +2320,8 @@
             },
             "loader-utils": {
               "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-              "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+              "resolved": "http://r.cnpmjs.org/loader-utils/download/loader-utils-2.0.0.tgz",
+              "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
               "dev": true,
               "optional": true,
               "requires": {
@@ -5125,8 +5082,8 @@
     },
     "deepmerge": {
       "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
-      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+      "resolved": "http://r.cnpmjs.org/deepmerge/download/deepmerge-4.2.2.tgz",
+      "integrity": "sha1-RNLqNnm49NT/ujPwPYZfwee/SVU=",
       "dev": true,
       "optional": true
     },
@@ -5494,6 +5451,14 @@
         }
       }
     },
+    "dom7": {
+      "version": "3.0.0",
+      "resolved": "https://r.cnpmjs.org/dom7/download/dom7-3.0.0.tgz",
+      "integrity": "sha1-uGHOXWemvs16qjrQKUL/FLEkAzE=",
+      "requires": {
+        "ssr-window": "^3.0.0-alpha.1"
+      }
+    },
     "domain-browser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -6716,6 +6681,54 @@
         }
       }
     },
+    "fork-ts-checker-webpack-plugin-v5": {
+      "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
+      "resolved": "https://r.cnpmjs.org/fork-ts-checker-webpack-plugin/download/fork-ts-checker-webpack-plugin-5.2.1.tgz",
+      "integrity": "sha1-eTJthpeXkG+osk4qvPlCH8gFRQ0=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@types/json-schema": "^7.0.5",
+        "chalk": "^4.1.0",
+        "cosmiconfig": "^6.0.0",
+        "deepmerge": "^4.2.2",
+        "fs-extra": "^9.0.0",
+        "memfs": "^3.1.2",
+        "minimatch": "^3.0.4",
+        "schema-utils": "2.7.0",
+        "semver": "^7.3.2",
+        "tapable": "^1.0.0"
+      },
+      "dependencies": {
+        "cosmiconfig": {
+          "version": "6.0.0",
+          "resolved": "https://r.cnpmjs.org/cosmiconfig/download/cosmiconfig-6.0.0.tgz",
+          "integrity": "sha1-2k/uhTxS9rHmk19BwaL8UL1KmYI=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/parse-json": "^4.0.0",
+            "import-fresh": "^3.1.0",
+            "parse-json": "^5.0.0",
+            "path-type": "^4.0.0",
+            "yaml": "^1.7.2"
+          }
+        },
+        "schema-utils": {
+          "version": "2.7.0",
+          "resolved": "https://r.cnpmjs.org/schema-utils/download/schema-utils-2.7.0.tgz",
+          "integrity": "sha1-FxUfdtjq5n+793lgwzxnatn078c=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/json-schema": "^7.0.4",
+            "ajv": "^6.12.2",
+            "ajv-keywords": "^3.4.1"
+          }
+        }
+      }
+    },
     "form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -6790,8 +6803,8 @@
     },
     "fs-monkey": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.1.tgz",
-      "integrity": "sha512-fcSa+wyTqZa46iWweI7/ZiUfegOZl0SG8+dltIwFXo7+zYU9J9kpS3NB6pZcSlJdhvIwp81Adx2XhZorncxiaA==",
+      "resolved": "https://r.cnpmjs.org/fs-monkey/download/fs-monkey-1.0.1.tgz",
+      "integrity": "sha1-SoLzaUQ2XmGfRFTZ//EGVTBnt4E=",
       "dev": true,
       "optional": true
     },
@@ -8926,8 +8939,8 @@
     },
     "memfs": {
       "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.0.tgz",
-      "integrity": "sha512-f/xxz2TpdKv6uDn6GtHee8ivFyxwxmPuXatBb1FBwxYNuVpbM3k/Y1Z+vC0mH/dIXXrukYfe3qe5J32Dfjg93A==",
+      "resolved": "https://r.cnpmjs.org/memfs/download/memfs-3.2.0.tgz",
+      "integrity": "sha1-+UOOYitazR2qikrhYMSW/dEyWyY=",
       "dev": true,
       "optional": true,
       "requires": {
@@ -12326,6 +12339,11 @@
         "tweetnacl": "~0.14.0"
       }
     },
+    "ssr-window": {
+      "version": "3.0.0",
+      "resolved": "https://r.cnpmjs.org/ssr-window/download/ssr-window-3.0.0.tgz",
+      "integrity": "sha1-/VuCgBY4lD4MxwTEaRgBQ1r3rDc="
+    },
     "ssri": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
@@ -12682,6 +12700,15 @@
         }
       }
     },
+    "swiper": {
+      "version": "6.3.5",
+      "resolved": "https://r.cnpmjs.org/swiper/download/swiper-6.3.5.tgz",
+      "integrity": "sha1-dES2gND9r+hIWYJaw1WbWwG9GRY=",
+      "requires": {
+        "dom7": "^3.0.0-alpha.7",
+        "ssr-window": "^3.0.0-alpha.4"
+      }
+    },
     "table": {
       "version": "5.4.6",
       "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",

+ 3 - 1
package.json

@@ -36,12 +36,14 @@
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
     "upload": "vue-cli-service build && node ./jd/upload.js",
-    "add":"node jd/createComponentMode.js"
+    "add": "node jd/createComponentMode.js"
   },
   "dependencies": {
+    "@types/swiper": "^5.4.1",
     "core-js": "^3.6.5",
     "sass": "^1.27.0",
     "sass-loader": "^10.0.4",
+    "swiper": "^6.3.5",
     "vue": "^3.0.0",
     "vue-router": "^4.0.0-rc.1"
   },

+ 11 - 1
src/config.ts

@@ -107,7 +107,17 @@ export const nav = [
   },
   {
     name: '导航组件',
-    packages: []
+    packages: [
+      {
+        name: 'tab',
+        sort: 1,
+        cName: '标签组件',
+        type: 'component',
+        show: true,
+        desc: '标签组件',
+        author: 'zhenyulei'
+      }
+    ]
   },
   {
     name: '业务组件',

+ 110 - 0
src/packages/tab/demo.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</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</h2>
+    <h2>switchTab监听切换tab返回事件</h2>
+    <nut-tab :default-index="1" @switchTab="switchTab">
+      <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 :animatedTime="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>标签数量超过 5 个时,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中。</h2>
+    <nut-tab :animatedTime="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-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>
+      <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" :animatedTime="500" :defaultIndex="2">
+      <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 :animatedTime="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';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  props: {},
+  setup() {
+    const resData = reactive({
+      editList: [
+        {
+          title: '标签一'
+        },
+        {
+          title: '标签二'
+        }
+      ]
+    });
+    function changeList() {
+      resData.editList.push({
+        title: '标签' + resData.editList.length
+      });
+    }
+    function switchTab(activeInddex: number, event: MouseEvent) {
+      console.log(activeInddex, event);
+    }
+    return {
+      ...toRefs(resData),
+      changeList,
+      switchTab
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.content {
+  padding: 10px;
+}
+</style>

+ 159 - 0
src/packages/tab/doc.md

@@ -0,0 +1,159 @@
+# Tab 选项卡
+
+常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式。
+
+
+## 基础样式
+
+```html
+<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>
+```
+
+## 默认显示tab:
+defaultIndex设置默认显示tab
+switchTab监听切换tab返回事件
+
+```html
+<nut-tab  :defaultIndex="1" @switchTab="switchTab">
+  <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>
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  setup() {
+    function switchTab(activeInddex:number,event:MouseEvent){
+      console.log(activeInddex,event);
+    }
+    return {
+      switchTab
+    };
+  }
+});
+</script>
+```
+
+## animatedTime 开启切换标签内容时的转场动画时间
+```html
+<nut-tab :animatedTime="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>
+```
+
+## 标签数量超过 5 个时,标签栏可以在水平方向上滚动
+切换时会自动将当前标签居中。
+
+```html
+<nut-tab :animatedTime="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-panel tab-title="已取消"><p class="content">这里是页签已取消内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待评价"><p class="content">这里是页签待评价内容</p></nut-tab-panel>
+</nut-tab>
+```
+
+## 设置slot:header可以自定义标签
+```html
+<nut-tab>
+  <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>
+```
+## 左右tab布局
+```html
+<nut-tab direction="vertical" :animatedTime="500" :defaultIndex="2">
+  <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>
+```
+## 异步操作
+
+```html
+<nut-tab :animatedTime="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>
+<script lang="ts">
+import { ref , reactive,toRefs } from "vue";
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  setup() {
+    const resData = reactive({
+      editList:[
+        {
+          title:'标签一'
+        },
+        {
+          title:'标签二'
+        }
+      ]
+    });
+    function changeList(){
+      resData.editList.push({
+        title:'标签'+resData.editList.length
+      })
+    }
+    return {
+      ...toRefs(resData),
+      changeList
+    };
+  }
+});
+</script>
+```
+
+
+### Prop
+
+### nut-tab
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| direction | 页签栏的分布,可选值 horizontal/vertical | String | horizontal
+| default-index | 默认选中的页签栏 | Number | 0
+| animated-time | 开启切换标签内容时的转场动画时间 | Number | 0
+
+
+
+### nut-tab-panel
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| tab-title| 页签的标题 | String | ''
+| slot:header | 页签title的自定义slot | vnode | --
+
+### Event
+
+| 事件名称 | 说明 | 回调参数 
+|----- | ----- | ----- 
+| switch-tab | 切换页签时触发事件 | 点击的索引值和触发元素
+

+ 102 - 0
src/packages/tab/index.scss

@@ -0,0 +1,102 @@
+.nutui-tab {
+  .tab-title {
+    width: 100%;
+    height: 46px;
+    overflow-x: scroll;
+    display: flex;
+    flex-wrap: nowrap;
+    scroll-behavior: smooth;
+    background: #f5f5f5;
+    position: relative;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+    .underline {
+      position: absolute;
+    }
+    .tab-title-box {
+      min-width: 75px;
+      height: 100%;
+      display: flex;
+      flex: 1;
+      justify-content: center;
+      align-items: center;
+      box-sizing: border-box;
+      text-align: center;
+      font-size: 14px;
+    }
+    .nut-tab-active {
+      color: #1a1a1a;
+      font-weight: bold;
+      font-size: 16px;
+      position: relative;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 5px;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 12px;
+        height: 4px;
+        background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/127200/40/18747/536/5fb36b5aE61cac2d8/638032e8da9b93f4.png');
+        background-size: 100% 100%;
+      }
+    }
+  }
+  .nutui-tab-swiper {
+    overflow: hidden;
+    display: block;
+    width: 100%;
+    height: 200px;
+    background: #fff;
+    box-sizing: border-box;
+  }
+}
+.vertical-tab {
+  display: flex;
+  height: 150px;
+  .tab-title {
+    width: 70px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow-y: scroll;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+    .tab-title-box {
+      width: 100%;
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      min-height: 35px;
+    }
+    .nut-tab-active {
+      color: #1a1a1a;
+      font-weight: bold;
+      font-size: 16px;
+      position: relative;
+      &::after {
+        content: '';
+        position: absolute;
+        right: 5px;
+        top: 50%;
+        left: auto;
+        transform: translateY(-50%) rotate(270deg);
+        width: 12px;
+        height: 4px;
+        background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/127200/40/18747/536/5fb36b5aE61cac2d8/638032e8da9b93f4.png');
+        background-size: 100% 100%;
+      }
+    }
+  }
+  .nutui-tab-swiper {
+    overflow: hidden;
+    display: block;
+    flex: 1;
+    height: 100%;
+    background: #fff;
+    box-sizing: border-box;
+  }
+}

+ 145 - 0
src/packages/tab/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div :class="[direction === 'vertical' ? 'vertical-tab' : 'nutui-tab']">
+    <div class="tab-title" ref="navlist">
+      <div
+        :class="['tab-title-box', { 'nut-tab-active': activeIndex == index }]"
+        v-for="(item, index) in titles"
+        :key="index"
+        @click="switchTitle(index, $event)"
+      >
+        {{ item.title }}
+        <TabTitle v-bind:slots="item.content" v-if="item.content"></TabTitle>
+      </div>
+      <div class="underline"></div>
+    </div>
+    <div :class="['nutui-tab-swiper', swiperClassName]">
+      <div class="swiper-wrapper">
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+//ts-nochecked
+import { PropType, h, toRefs, reactive, computed, ref, onMounted, nextTick, watch, watchEffect } from 'vue';
+import { createComponent } from '@/utils/create';
+const { create } = createComponent('tab');
+import TabTitle from './tabTitle';
+import Swiper from 'swiper';
+import 'swiper/swiper-bundle.css';
+type TabDirection = 'horizontal' | 'vertical';
+
+export default create({
+  props: {
+    defaultIndex: {
+      type: Number,
+      default: 0
+    },
+    animatedTime: {
+      type: Number,
+      default: 0
+    },
+    direction: {
+      type: String as PropType<TabDirection>,
+      default: 'horizontal'
+    }
+  },
+  components: {
+    TabTitle
+  },
+  setup(props, ctx) {
+    const titles: any = reactive([]);
+    let mySwiper: any = reactive({});
+    const isLock = ref(false);
+    const activeIndex = ref(props.defaultIndex);
+    const navlist: any = ref(null);
+    // 生成随机的id
+    function createHash() {
+      return Array.from(Array(10), () => Math.floor(Math.random() * 36).toString(36)).join('');
+    }
+
+    const swiperClassName = ref('swiper-' + createHash());
+    //title点击后居中显示
+    function centerTitle(index: number) {
+      const currEle = navlist.value.querySelectorAll('.tab-title-box')[index];
+      if (props.direction === 'vertical') {
+        const currTitleTop = navlist.value.offsetTop;
+        const currTop = currEle.offsetTop;
+        const currHeight = currEle.offsetHeight;
+        const tapHeight = navlist.value.offsetHeight;
+        navlist.value.scroll(0, currTop - currTitleTop - tapHeight / 2 + currHeight / 2);
+      } else {
+        const currLeft = currEle.offsetLeft;
+        const currWidth = currEle.offsetWidth;
+        const tapWidth = navlist.value.offsetWidth;
+        navlist.value.scroll(currLeft - tapWidth / 2 + currWidth / 2, 0);
+      }
+    }
+
+    //切换tab
+    function switchTitle(index: number) {
+      activeIndex.value = index;
+      centerTitle(index);
+      mySwiper.slideToLoop(index, props.animatedTime, false);
+    }
+    function initSwiper(currIndex) {
+      mySwiper = new Swiper('.' + swiperClassName.value, {
+        loop: true /** 循环模式选项 */,
+        observer: true, //修改swiper自己或子元素时,自动初始化swiper
+        observeParents: true, //修改swiper的父元素时,自动初始化swiper
+        setWrapperSize: true,
+        direction: props.direction,
+        initialSlide: currIndex,
+        on: {
+          touchStart: function() {
+            isLock.value = true;
+          },
+          transitionEnd: function(swiper: Swiper): void {
+            ctx.emit('switchTab', swiper.realIndex, swiper);
+            if (isLock.value) {
+              activeIndex.value = swiper.realIndex;
+              centerTitle(swiper.realIndex);
+            }
+          }
+        }
+      });
+    }
+    function initTitle() {
+      titles.length = 0;
+      if (ctx.slots.default) {
+        const slots: any[] = ctx.slots.default().length === 1 ? ctx.slots.default()[0].children : ctx.slots.default();
+        slots &&
+          slots.forEach((item, index) => {
+            titles.push({
+              title: item.props['tab-title'],
+              content: item.children && item.children.header ? item.children.header() : null
+            });
+          });
+      }
+      setTimeout(() => {
+        initSwiper(activeIndex.value);
+      }, 0);
+    }
+    onMounted(() => {
+      initTitle();
+    });
+    watch(
+      () => ctx.slots.default(),
+      () => {
+        initTitle();
+      }
+    );
+    return {
+      swiperClassName,
+      titles,
+      navlist,
+      activeIndex,
+      switchTitle
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 9 - 0
src/packages/tab/tabTitle.ts

@@ -0,0 +1,9 @@
+import { h } from 'vue';
+export default {
+  setup(props: any) {
+    return () => h(`view`, {}, props.slots);
+  },
+  props: {
+    slots: Object
+  }
+};

+ 2 - 0
src/packages/tabpanel/index.scss

@@ -0,0 +1,2 @@
+.nut-temp {
+}

+ 26 - 0
src/packages/tabpanel/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="swiper-slide">
+    <slot></slot>
+  </div>
+</template>
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { create } = createComponent('tab-panel');
+
+export default create({
+  props: {
+    tabTitle: {
+      type: String,
+      default: ''
+    }
+  },
+  components: {},
+  setup(props, ctx) {
+    //
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 1 - 1
src/sites/doc/components/Header.vue

@@ -19,7 +19,7 @@
         <ul class="nav-list">
           <li class="nav-item nav-item-actie">指南</li>
           <li class="nav-item">组件</li>
-          <li class="nav-item">示例</li>
+          <li class="nav-item"><a href="http://localhost:8080/demo.html#/" style="color:#fff">示例</a></li>
           <li class="nav-item">资源</li>
           <li class="nav-item">
             <div

+ 1 - 0
src/sites/mobile/App.vue

@@ -68,6 +68,7 @@ export default defineComponent({
   .demo {
     height: 100%;
     background: #f7f8fa;
+    overflow-x: hidden;
     overflow-y: auto;
     padding: 0 25px;
     padding-top: 57px;