From eeaa36058af8e516921c0f71702273f4a6866ac6 Mon Sep 17 00:00:00 2001
From: neverland <chenjiahan@buaa.edu.cn>
Date: Thu, 12 Oct 2017 22:00:34 -0500
Subject: [PATCH] [new feature] add Tabbar component (#204)

* [Document] add english document of Checkbox

* [Document] add english document of Field

* [Document] add english document of NumberKeyboard

* [bugfix] NumberKeyboard should not dispaly title when title is empty

* [Document] add english document of PasswordInput

* [Document] add english document of Radio

* [document] add english document of Switch

* [bugfix] remove redundent styles in english document

* [Document] fix details

* fix Switch test cases

* [bugfix] Swipe shouid reinitialize when item changes

* [new feature] ImagePreview reconstruct

* [new feature] add Tabbar component
---
 docs/examples-docs/zh-CN/tab.md    |   2 +-
 docs/examples-docs/zh-CN/tabbar.md | 110 +++++++++++++++++++++++++++++
 docs/src/doc.config.js             |   6 +-
 packages/index.js                  |   6 ++
 packages/swipe-item/index.vue      |  13 ++--
 packages/tabbar-item/index.vue     |  49 +++++++++++++
 packages/tabbar/index.vue          |  46 ++++++++++++
 packages/vant-css/src/index.css    |   1 +
 packages/vant-css/src/tabbar.css   |  50 +++++++++++++
 test/unit/components/tabbar.vue    |  40 +++++++++++
 test/unit/specs/tabbar.spec.js     |  23 ++++++
 11 files changed, 337 insertions(+), 9 deletions(-)
 create mode 100644 docs/examples-docs/zh-CN/tabbar.md
 create mode 100644 packages/tabbar-item/index.vue
 create mode 100644 packages/tabbar/index.vue
 create mode 100644 packages/vant-css/src/tabbar.css
 create mode 100644 test/unit/components/tabbar.vue
 create mode 100644 test/unit/specs/tabbar.spec.js

diff --git a/docs/examples-docs/zh-CN/tab.md b/docs/examples-docs/zh-CN/tab.md
index 3ca6131af..f10d38f1e 100644
--- a/docs/examples-docs/zh-CN/tab.md
+++ b/docs/examples-docs/zh-CN/tab.md
@@ -47,7 +47,7 @@ export default {
 };
 </script>
 
-## Tab 标签
+## Tab 标签页
 
 ### 使用指南
 ``` javascript
diff --git a/docs/examples-docs/zh-CN/tabbar.md b/docs/examples-docs/zh-CN/tabbar.md
new file mode 100644
index 000000000..22a32b8ad
--- /dev/null
+++ b/docs/examples-docs/zh-CN/tabbar.md
@@ -0,0 +1,110 @@
+## Tabbar 标签栏
+
+<style>
+.demo-tabbar {
+  .van-tabbar {
+    position: relative;
+
+    &-item {
+      cursor: pointer;
+    }
+  }
+}
+</style>
+
+<script>
+export default {
+  data() {
+    return {
+      active: 0,
+      active2: 0,
+      icon: {
+        normal: 'https://img.yzcdn.cn/public_files/2017/10/13/c547715be149dd3faa817e4a948b40c4.png',
+        active: 'https://img.yzcdn.cn/public_files/2017/10/13/793c77793db8641c4c325b7f25bf130d.png'
+      }
+    }
+  }
+}
+</script>
+
+### 使用指南
+``` javascript
+import { Tabbar, TabbarItem } from 'vant';
+
+Vue.component(Tabbar.name, Tabbar);
+Vue.component(TabbarItem.name, TabbarItem);
+```
+
+### 代码演示
+
+#### 基础用法
+
+:::demo 基础用法
+```html
+<van-tabbar v-model="active">
+  <van-tabbar-item icon="shop">标签</van-tabbar-item>
+  <van-tabbar-item icon="chat" dot>标签</van-tabbar-item>
+  <van-tabbar-item icon="records">标签</van-tabbar-item>
+  <van-tabbar-item icon="gold-coin">标签</van-tabbar-item>
+</van-tabbar>
+```
+
+```javascript
+export default {
+  data() {
+    return {
+      active: 0
+    }
+  }
+}
+```
+:::
+
+#### 自定义图标
+通过 icon slot 自定义图标
+
+:::demo 自定义图标
+```html
+<van-tabbar v-model="active2">
+  <van-tabbar-item icon="shop">
+    <span>自定义</span>
+    <img slot="icon" :src="active2 === 0 ? icon.active : icon.normal" />
+  </van-tabbar-item>
+  <van-tabbar-item icon="chat">标签</van-tabbar-item>
+  <van-tabbar-item icon="records">标签</van-tabbar-item>
+</van-tabbar>
+```
+
+```javascript
+export default {
+  data() {
+    return {
+      active2: 0,
+      icon: {
+        normal: '//img.yzcdn.cn/1.png',
+        active: '//img.yzcdn.cn/2.png'
+      }
+    }
+  }
+}
+```
+:::
+
+### Tabbar API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| v-model | 当前选中标签的索引 | `Number`  | - | - |
+
+### Tabbar Event
+
+| 事件名 | 说明 | 参数 |
+|-----------|-----------|-----------|
+| change | 切换标签时触发 | active: 当前选中标签 |
+
+### TabbarItem API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| icon | 图标名称 | `String` | - | Icon 组件中可用的类型 |
+| dot | 是否显示小红点 | `Boolean` | - | - |
diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js
index ed205e30a..382792506 100644
--- a/docs/src/doc.config.js
+++ b/docs/src/doc.config.js
@@ -121,7 +121,11 @@ module.exports = {
               },
               {
                 "path": "/tab",
-                "title": "Tab - 标签"
+                "title": "Tab - 标签页"
+              },
+              {
+                "path": "/tabbar",
+                "title": "Tabbar - 标签栏"
               },
               {
                 "path": "/tag",
diff --git a/packages/index.js b/packages/index.js
index c6d551ad6..e0ec4d901 100644
--- a/packages/index.js
+++ b/packages/index.js
@@ -50,6 +50,8 @@ import SwipeItem from './swipe-item';
 import Switch from './switch';
 import SwitchCell from './switch-cell';
 import Tab from './tab';
+import Tabbar from './tabbar';
+import TabbarItem from './tabbar-item';
 import Tabs from './tabs';
 import Tag from './tag';
 import Toast from './toast';
@@ -108,6 +110,8 @@ const components = [
   Switch,
   SwitchCell,
   Tab,
+  Tabbar,
+  TabbarItem,
   Tabs,
   Tag,
   TreeSelect,
@@ -182,6 +186,8 @@ export {
   Switch,
   SwitchCell,
   Tab,
+  Tabbar,
+  TabbarItem,
   Tabs,
   Tag,
   Toast,
diff --git a/packages/swipe-item/index.vue b/packages/swipe-item/index.vue
index d960e7ece..3cc80f0bd 100644
--- a/packages/swipe-item/index.vue
+++ b/packages/swipe-item/index.vue
@@ -8,14 +8,9 @@
 export default {
   name: 'van-swipe-item',
 
-  beforeCreate() {
-    this.$parent.swipes.push(this);
-  },
-
   data() {
     return {
-      offset: 0,
-      index: this.$parent.swipes.indexOf(this)
+      offset: 0
     };
   },
 
@@ -28,8 +23,12 @@ export default {
     }
   },
 
+  beforeCreate() {
+    this.$parent.swipes.push(this);
+  },
+
   destroyed() {
-    this.$parent.swipes.splice(this.index, 1);
+    this.$parent.swipes.splice(this.$parent.swipes.indexOf(this), 1);
   }
 };
 </script>
diff --git a/packages/tabbar-item/index.vue b/packages/tabbar-item/index.vue
new file mode 100644
index 000000000..0e82852a9
--- /dev/null
+++ b/packages/tabbar-item/index.vue
@@ -0,0 +1,49 @@
+<template>
+  <div :class="['van-tabbar-item', { 'van-tabbar-item--active': active }]" @click="onClick">
+    <div :class="['van-tabbar-item__icon', { 'van-tabbar-item__icon-dot': dot }]">
+      <slot name="icon">
+        <van-icon v-if="icon" :name="icon" />
+      </slot>
+    </div>
+    <div class="van-tabbar-item__text">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+import Icon from '../icon';
+
+export default {
+  name: 'van-tabbar-item',
+
+  components: {
+    [Icon.name]: Icon
+  },
+
+  props: {
+    icon: String,
+    dot: Boolean
+  },
+
+  data() {
+    return {
+      active: false
+    };
+  },
+
+  beforeCreate() {
+    this.$parent.items.push(this);
+  },
+
+  destroyed() {
+    this.$parent.items.splice(this.$parent.items.indexOf(this), 1);
+  },
+
+  methods: {
+    onClick() {
+      this.$parent.onChange(this.$parent.items.indexOf(this));
+    }
+  }
+};
+</script>
diff --git a/packages/tabbar/index.vue b/packages/tabbar/index.vue
new file mode 100644
index 000000000..833d8d9fc
--- /dev/null
+++ b/packages/tabbar/index.vue
@@ -0,0 +1,46 @@
+<template>
+  <div :class="['van-tabbar', 'van-hairline--top-bottom', { 'van-tabbar--fixed': fixed }]">
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'van-tabbar',
+
+  data() {
+    return {
+      items: []
+    };
+  },
+
+  props: {
+    value: Number,
+    fixed: {
+      type: Boolean,
+      default: true
+    }
+  },
+
+  watch: {
+    items() {
+      this.setActiveItem();
+    },
+    value() {
+      this.setActiveItem();
+    }
+  },
+
+  methods: {
+    setActiveItem() {
+      this.items.forEach((item, index) => {
+        item.active = index === this.value;
+      });
+    },
+    onChange(active) {
+      this.$emit('input', active);
+      this.$emit('change', active);
+    }
+  }
+};
+</script>
diff --git a/packages/vant-css/src/index.css b/packages/vant-css/src/index.css
index 266d2779d..f9e94648f 100644
--- a/packages/vant-css/src/index.css
+++ b/packages/vant-css/src/index.css
@@ -22,6 +22,7 @@
 @import './steps.css';
 @import './tag.css';
 @import './tab.css';
+@import './tabbar.css';
 @import './image-preview.css';
 @import './stepper.css';
 @import './progress.css';
diff --git a/packages/vant-css/src/tabbar.css b/packages/vant-css/src/tabbar.css
new file mode 100644
index 000000000..cbfab2b49
--- /dev/null
+++ b/packages/vant-css/src/tabbar.css
@@ -0,0 +1,50 @@
+@import './common/var.css';
+
+.van-tabbar {
+  width: 100%;
+  height: 50px;
+  display: flex;
+  background-color: #fff;
+
+  &--fixed {
+    left: 0;
+    bottom: 0;
+    position: fixed;
+  }
+
+  &-item {
+    flex: 1;
+    color: #666;
+    display: flex;
+    line-height: 1;
+    font-size: 12px;
+    align-items: center;
+    flex-direction: column;
+    justify-content: center;
+
+    &__icon {
+      font-size: 18px;
+      margin-bottom: 5px;
+      position: relative;
+
+      &-dot {
+        &::after {
+          width: 8px;
+          height: 8px;
+          content: ' ';
+          position: absolute;
+          border-radius: 100%;
+          background-color: $red;
+        }
+      }
+
+      img {
+        height: 18px;
+      }
+    }
+
+    &--active {
+      color: $blue;
+    }
+  }
+}
diff --git a/test/unit/components/tabbar.vue b/test/unit/components/tabbar.vue
new file mode 100644
index 000000000..9b91bec79
--- /dev/null
+++ b/test/unit/components/tabbar.vue
@@ -0,0 +1,40 @@
+<template>
+  <van-tabbar v-model="active" @change="onChange">
+    <van-tabbar-item icon="shop">
+      <span>自定义</span>
+      <img slot="icon" :src="active === 0 ? icon.active : icon.normal" />
+    </van-tabbar-item>
+    <van-tabbar-item icon="chat">标签</van-tabbar-item>
+    <van-tabbar-item icon="chat">标签</van-tabbar-item>
+    <van-tabbar-item icon="records">标签</van-tabbar-item>
+  </van-tabbar>
+</template>
+
+<script>
+import Tabbar from 'packages/tabbar';
+import TabbarItem from 'packages/tabbar-item';
+
+export default {
+  components: {
+    [Tabbar.name]: Tabbar,
+    [TabbarItem.name]: TabbarItem
+  },
+
+  data() {
+    return {
+      active: 0,
+      changeRecord: 0,
+      icon: {
+        normal: 'https://img.yzcdn.cn/public_files/2017/10/13/c547715be149dd3faa817e4a948b40c4.png',
+        active: 'https://img.yzcdn.cn/public_files/2017/10/13/793c77793db8641c4c325b7f25bf130d.png'
+      }
+    };
+  },
+
+  methods: {
+    onChange(val) {
+      this.changeRecord = val;
+    }
+  }
+};
+</script>
diff --git a/test/unit/specs/tabbar.spec.js b/test/unit/specs/tabbar.spec.js
new file mode 100644
index 000000000..86add5d3f
--- /dev/null
+++ b/test/unit/specs/tabbar.spec.js
@@ -0,0 +1,23 @@
+import TabbarExample from '../components/tabbar';
+import { mount } from 'avoriaz';
+
+describe('Progress', () => {
+  let wrapper;
+
+  afterEach(() => {
+    wrapper && wrapper.destroy();
+  });
+
+  it('Tabbar with four items', (done) => {
+    wrapper = mount(TabbarExample);
+
+    wrapper.vm.$nextTick(() => {
+      expect(wrapper.find('.van-tabbar-item').length).to.equal(4);
+
+      wrapper.find('.van-tabbar-item')[3].element.click();
+      expect(wrapper.vm.active).to.equal(3);
+      expect(wrapper.vm.changeRecord).to.equal(3);
+      done();
+    });
+  });
+});