提交 9cc72d2f 编写于 作者: J JiZhi 提交者: doly mood

Feat add tabs (#227)

* feat(slide): support listenScroll

* feat(tabs): add tabs feature

* docs(tabs): tabs's docs

* test(tabs): tabs' unit case

* chore(tabs): the examples of tabs
上级 4463c191
......@@ -21,7 +21,8 @@
"button": "Button",
"loading": "Loading",
"tip": "Tip",
"toolbar": "Toolbar"
"toolbar": "Toolbar",
"tab-bar": "TabBar"
}
},
"form": {
......@@ -99,7 +100,8 @@
"button": "Button",
"loading": "Loading",
"tip": "Tip",
"toolbar": "Toolbar"
"toolbar": "Toolbar",
"tab-bar": "TabBar"
}
},
"form": {
......
......@@ -84,7 +84,7 @@ Rate component. You can customize the rating star numbers, and disable the inter
| Attribute | Description | Type | Accepted Values | Default |
| - | - | - | - | - |
| v-model | bound rate value | Number | - | 0 |
| max | max star number | Number | - | false |
| max | max star number | Number | - | 5 |
| disabled | disabled status | Boolean | true/false | false |
| justify | auto justify container's width | Boolean | true/false | false |
......
......@@ -167,6 +167,29 @@
The scoped slots provide two parameters: current active index `current` and slide items length `dots`.
- Dispatch scroll position in real time<sup>1.10.0</sup>
```html
<cube-slide :options="options" @scroll="scroll"></cube-slide>
```
```javascript
export default {
data() {
return {
options: {
listenScroll: true
}
}
},
methods: {
scroll ({x, y}) {
console.log(x, y)
}
}
}
```
### Props configuration
| Attribute | Description | Type | Accepted Values | Default |
......@@ -194,8 +217,9 @@
| Event Name | Description | Parameters |
| - | - | - |
| change | triggers when current slide changes | index of current slide |
| scroll-end<sup>1.9.0</sup> | triggers when scroll end. | index of current slide |
| change | triggered when current slide changes | index of current slide |
| scroll<sup>1.10.0</sup> | triggered when slide is scrolling | Object {x, y} - scroll position |
| scroll-end<sup>1.9.0</sup> | triggered when scroll end. | index of current slide |
### Instance methods
......
## TabBar
> New in 1.10.0+
Implementing the function of tab switching.
### Example
The following demo code is [here](https://github.com/didi/cube-ui/tree/master/example/pages/tab-bar).
### CubeTabBar
`cube-tab-bar` supports click highlighting, underscore follow-up effects, and custom slots for icon-label-like app-navigation styles.
- Basic usage
You can initialize `cube-tab-bar` by passing in the data structure of `tabs` as follows. You must use the `v-model` directive to select the corresponding tab. The value of the v-model argument must correspond to the label attribute of a tab in `cube-tab-bar`. The icon attribute is used as a class selector, which is generally used with icon-font class. It will dispatch `click` and `change` event at the proper time. The parameter is the label value corresponding to each selected tab.
```html
<template>
<cube-tab-bar
v-model="selectedLabelDefault"
:data="tabs"
@click="clickHandler"
@change="changeHandler">
</cube-tab-bar>
</template>
```
```js
export default {
data() {
return {
selectedLabelDefault: 'Vip'
tabs: [{
label: 'Home'
}, {
label: 'Like'
}, {
label: 'Vip'
}, {
label: 'Me'
}]
}
},
methods: {
clickHandler (label) {
// if you clicked home tab, then print 'Home'
console.log(label)
},
changeHandler (label) {
// if you clicked different tab, this methods can be emitted
}
}
}
```
- Custom slot
In fact, we always want to display icons and text effects which looks like app navigation styles, so the `cube-tab-bar` component also supports the use of slot. Note that you must use the `cube-tab` component as a sub-component of the first level to wrap your custom slot.
```html
<template>
<cube-tab-bar
v-model="selectedLabelSlots"
showSlider
inline
@click="clickHandler">
<cube-tab v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<!-- icon slot -->
<i slot="icon" :class="item.icon"></i>
<!-- default slot -->
{{item.label}}
</cube-tab>
</cube-tab-bar>
</template>
```
```js
export default {
data () {
return {
selectedLabelSlots: 'Like'
tabs: [{
label: 'Home',
icon: 'cubeic-home'
}, {
label: 'Like',
icon: 'cubeic-like'
}, {
label: 'Vip',
icon: 'cubeic-vip'
}, {
label: 'Me',
icon: 'cubeic-person'
}]
}
},
methods: {
clickHandler (label) {
// if you clicked home tab, then print 'Home'
console.log(label)
}
}
}
```
At the same time, `cube-tab-bar` also supports other configurations, `showSlider` controls whether to turn on the effect of underscore, `inline` to determine whether the icon and label are in a line, `useTransition` controls whether the underscore use transition, as shown in the sample code.
### CubeTabBar & CubeTabPanels
Usually, our requirement is to display different panel as tabs are switched, so we need to use the `cube-tab-panels` component. `cube-tab-panels` must be nested with `cube-tab-panel`. The label values passed to `cube-tab` and `cube-tab-panel` must be the same, because it is necessary to create the relationship between tab with panel. They are linked by the same `v-model`.To see the effect, click on the `tab-basic` demo on the right.
```html
<template>
<cube-tab-bar v-model="selectedLabel" showSlider>
<cube-tab v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<i slot="icon" :icon="item.icon"></i>
{{item.label}}
</cube-tab>
</cube-tab-bar>
<cube-tab-panels v-model="selectedLabel">
<cube-tab-panel v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<ul>
<li class="tab-panel-li" v-for="(hero, index) in item.heroes" :key="index">
{{hero}}
</li>
</ul>
</cube-tab-panel>
</cube-tab-panels>
</template>
```
```js
export default {
data() {
return {
selectedLabel: '天辉',
tabs: [{
label: '天辉',
icon: 'cubeic-like',
heroes: ['敌法师', '卓尔游侠', '主宰', '米拉娜', '变体精灵', '幻影长矛手', '复仇之魂', '力丸', '矮人狙击手', '圣堂刺客', '露娜', '赏金猎人', '熊战士']
}, {
label: '夜魇',
icon: 'cubeic-star',
heroes: ['血魔', '影魔', '剃刀', '剧毒术士', '虚空假面', '幻影刺客', '冥界亚龙', '克林克兹', '育母蜘蛛', '编织者', '幽鬼', '司夜刺客', '米波']
}]
}
}
}
```
In fact, `cube-tab-bar` can be combined with many other cube-ui's components (such as: `cube-scroll`, `cube-slide`) to make a similar effect to the layout of native apps. Click on the `ScrollTab Demo` and `tab-composite` example on the right to see the effect.
### Props
- CubeTabBar
| Attribute | Description | Type | Demo | Default |
| - | - | - | - | - |
| value | Use v-model to select the corresponding tab when initializing. | String/Number | - | - |
| data | For data rendered with `cube-tab-bar`, when using the built-in default slot, this parameter must be passed. Each item of the array is an Object type, including `label`. If a custom slot is used, this value may not be passed | Array | [{label: 1}, {label: 2}] | [] |
| showSlider | Whether to turn on the underscore follow effect | Boolean | true/false | false |
| inline | Whether text and icons are displayed on one line | Boolean | true/false | false |
| useTransition | Whether to use transition | Boolean | true/false | true |
- CubeTab
| Attribute | Description | Type | Needed | Default |
| - | - | - | - | - |
| label | Use it to determine which tab is clicked | String/Number | yes | - |
- CubeTabPanels
| Attribute | Description | Type | Demo | Default |
| - | - | - | - | - |
| value | Use v-model to display the corresponding panels at initialization | String/Number | - | - |
| data | For data rendered with `cube-tab-panels`, when using the built-in default slot, this parameter must be passed. Each item of the array is an Object type, including `label`. If a custom slot is used, this value may not be passed | Array | [{label: 1}, {label: 2}] | [] |
- CubeTabPanel
| Attribute | Description | Type | Needed | Default |
| - | - | - | - | - |
| label | determine that the panels is displayed | String/Number | yes | - |
### Slot
- CubeTab
| Attribute | Description |
| - | - |
| default | `cube-tab`'s text |
| icon | Generally used to display icon |
### Events
- CubeTabBar
| Event Name | Description | parameter |
| - | - | - | - |
| click | Dispatched when the tab is clicked | The label value of the tab which is selected |
| change | Dispatched when tab changed | The label value of the tab which is selected |
### Instance methods
- CubeTabBar
This method works when the instance's `showSlider` property is set to true.
| Method name | Description | Parameter Type |
| - | - | - |
| setSliderTransform | Change the underscore's transformX of the `cube-tab-bar` component. If you pass Number, it will be converted into a pixel, or you can pass a String with units | Number/String |
......@@ -213,6 +213,11 @@ $drawer-title-bdc := $color-light-grey-ss
$drawer-title-bgc := $color-white
$drawer-panel-bgc := $color-white
$drawer-item-active-bgc := $color-light-grey-opacity
// tab-bar & tab-panel
$tab-color := $color-grey
$tab-active-color := $color-dark-orange
$tab-slider-bgc := $color-dark-orange
```
### webpack config
......
......@@ -84,7 +84,7 @@
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| - | - | - | - | - |
| v-model | 绑定的评分值 | Number | - | 0 |
| max | 星星个数 | Number | - | false |
| max | 星星个数 | Number | - | 5 |
| disabled | 禁用状态 | Boolean | true/false | false |
| justify | 是否自适应容器宽度(通过在星星之间增加空隙) | Boolean | true/false | false |
......
......@@ -150,6 +150,29 @@
作用域插槽提供了所需的当前索引值 `current` 以及长度 `dots`
- 实时派发滚动的距离<sup>1.10.0</sup>
```html
<cube-slide :options="options" @scroll="scroll"></cube-slide>
```
```javascript
export default {
data() {
return {
options: {
listenScroll: true
}
}
},
methods: {
scroll ({x, y}) {
console.log(x, y)
}
}
}
```
### Props 配置
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
......@@ -179,6 +202,7 @@
| 事件名 | 说明 | 参数 |
| - | - | - |
| change | Slide 页面切换时触发 | 当前页面的索引值 |
| scroll<sup>1.10.0</sup> | 滚动中实时派发 | Object {x, y} -滚动位置的坐标值 |
| scroll-end<sup>1.9.0</sup> | 在滚动结束时触发 | 当前页面的索引值 |
### 实例方法
......
## TabBar
> 1.10.0 新增
选项卡。
### 示例
如下示例相关代码在[这里](https://github.com/didi/cube-ui/tree/master/example/pages/tab-bar)
### CubeTabBar
支持默认的点击高亮效果,又支持下划线跟随的效果,并且支持自定义的插槽,实现icon与label搭配的类似于app底部选项卡的样式。
- 默认样式
传入如下 `tabs` 的数据结构便能初始化 `cube-tab-bar`,必须使用 `v-model` 指令来选中对应的 tab, v-model 的参数的值必须与某一项 tab 的 label 属性对应,icon 属性是用做于 class 选择器,一般是用字体图标样式,`cube-tab-bar` 在不同的时机派发 `click``change` 事件,参数则是每次选中的 tab 对应的 label 值。
```html
<template>
<cube-tab-bar
v-model="selectedLabelDefault"
:data="tabs"
@click="clickHandler"
@change="changeHandler">
</cube-tab-bar>
</template>
```
```js
export default {
data () {
return {
selectedLabelDefault: 'Vip'
tabs: [{
label: 'Home',
icon: 'cubeic-home'
} {
label: 'Like',
icon: 'cubeic-like'
} {
label: 'Vip',
icon: 'cubeic-vip'
} {
label: 'Me',
icon: 'cubeic-person'
}]
}
}
methods: {
clickHandler (label) {
// if you clicked home tab, then print 'Home'
console.log(label)
}
changeHandler (label) {
// if you clicked different tab, this methods can be emitted
}
}
}
```
- 自定义插槽
实际上我们更常见的需求是图标搭配文字效果,因此 `cube-tab-bar` 组件也支持了插槽的使用方式,
注意必须搭配 `cube-tab` 组件作为第一层级的子组件,来包裹你自定义插槽。
```html
<template>
<cube-tab-bar
v-model="selectedLabelSlots"
showSlider
inline
@click="clickHandler">
<cube-tab v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<!-- name为icon的插槽 -->
<i slot="icon" :class="item.icon"></i>
<!-- 默认插槽 -->
{{item.label}}
</cube-tab>
</cube-tab-bar>
</template>
```
```js
export default {
data () {
return {
selectedLabelSlots: 'Like'
tabs: [{
label: 'Home'
icon: 'cubeic-home'
} {
label: 'Like'
icon: 'cubeic-like'
} {
label: 'Vip'
icon: 'cubeic-vip'
} {
label: 'Me'
icon: 'cubeic-person'
}]
}
}
methods: {
clickHandler (label) {
// if you clicked home tab, then print 'Home'
console.log(label)
}
}
}
```
同时还支持一些配置项, `showSlider` 控制是否开启下划线跟随的效果,`inline` 来决定icon与label是否处于一行,`useTransition` 控制下划线是否使用transition过渡,如示例代码所示。
### CubeTabBar & CubeTabPanels
往往我们的需求是随着 tab 的切换显示不同的容器,这个时候需要搭配 `cube-tab-panels` 组件。`cube-tab-panels` 必须嵌套 `cube-tab-panel`,传入 `cube-tab``cube-tab-panel` 的label值必须一致,因为需要建立一个 tab 对应一个 panel 的关系。他们通过相同的 `v-model` 联动。查看效果可点击右边的 `tab-basic` 示例。
```html
<template>
<cube-tab-bar v-model="selectedLabel" showSlider>
<cube-tab v-for="(item, index) in tabs" :icon="item.icon" :label="item.label" :key="item.label">
</cube-tab>
</cube-tab-bar>
<cube-tab-panels v-model="selectedLabel">
<cube-tab-panel v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<ul>
<li class="tab-panel-li" v-for="(hero, index) in item.heroes">
{{hero}}
</li>
</ul>
</cube-tab-panel>
</cube-tab-panels>
</template>
```
```js
export default {
data () {
return {
selectedLabel: '天辉'
tabs: [{
label: '天辉'
icon: 'cubeic-like'
heroes: ['敌法师' '卓尔游侠' '主宰' '米拉娜' '变体精灵' '幻影长矛手' '复仇之魂' '力丸' '矮人狙击手' '圣堂刺客' '露娜' '赏金猎人' '熊战士']
} {
label: '夜魇'
icon: 'cubeic-star'
heroes: ['血魔' '影魔' '剃刀' '剧毒术士' '虚空假面' '幻影刺客' '冥界亚龙' '克林克兹' '育母蜘蛛' '编织者' '幽鬼' '司夜刺客' '米波']
}]
}
}
}
```
事实上,`cube-tab-bar` 还能搭配许多其他cube-ui的组件(cube-slide ,cube-scroll)来做出类似原生 app 布局的效果。可点击右边的 `ScrollTab Demo``tab-composite` 示例来查看效果
### Props 配置
- CubeTabBar
| 参数 | 说明 | 类型 | 示例 | 默认值 |
| - | - | - | - | - |
| value | 使用 v-model,初始化时选中对应的 tab | String/Number | - | - |
| data | 用于 `cube-tab-bar` 渲染的数据,当需要使用内置的默认插槽,此参数必传,数组的每一项是一个 Object 类型,包括 `label`,如果使用自定义插槽,可不传此值 | Array | [{label: 1}, {label: 2}] | [] |
| showSlider | 是否开启下划线跟随效果 | Boolean | true/false | false |
| inline | 文字与图标是否显示在一行 | Boolean | true/false | false |
| useTransition | 是否开启 transition 过渡 | Boolean | true/false | true |
- CubeTab
| 参数 | 说明 | 类型 | 是否必传 | 默认值 |
| - | - | - | - | - |
| label | 用于判断哪个 tab 点击从而高亮 | String/Number | 是 | - |
- CubeTabPanels
| 参数 | 说明 | 类型 | 示例 | 默认值 |
| - | - | - | - | - |
| value | 使用 v-model,初始化时显示对应的 panels | String/Number | - | - |
| data | 用于 `cube-tab-panels` 渲染的数据,当需要使用内置的默认插槽,此参数必传,数组的每一项是一个 Object 类型,包括 `label`,如果使用自定义插槽,可不传此值 | Array | [{label: 1}, {label: 2}] | [] |
- CubeTabBar
| 参数 | 说明 | 类型 | 是否必传 | 默认值 |
| - | - | - | - | - |
| label | 用于显示 panel | String/Number | 是 | - |
### 插槽
- CubeTab
| 名称 | 说明 |
| - | - |
| default | `cube-tab` 渲染的文案 |
| icon | 一般是用来添加 icon 图标 |
### 事件
- CubeTabBar
| 事件名 | 说明 | 参数1 |
| - | - | - | - |
| click | 当 tab 被点击时派发 | 点中的tab的label值 |
| change | 当点击不同的 tab 时派发 | 点中的tab的label值 |
### 实例方法
- CubeTabBar
当该实例的 `showSlider` 属性设置为true,该方法才有效。
| 方法名 | 说明 | 参数类型 |
| - | - | - |
| setSliderTransform | 改变 `cube-tab-bar` 组件的下划线的 transformX,如果传 Number,会转成像素,也可以传带有单位的 String | Number/String |
......@@ -213,6 +213,11 @@ $drawer-title-bdc := $color-light-grey-ss
$drawer-title-bgc := $color-white
$drawer-panel-bgc := $color-white
$drawer-item-active-bgc := $color-light-grey-opacity
// tab-bar & tab-panel
$tab-color := $color-grey
$tab-active-color := $color-dark-orange
$tab-slider-bgc := $color-dark-orange
```
### 配置 webpack
......
......@@ -47,6 +47,10 @@
{
path: '/toolbar',
text: 'Toolbar'
},
{
path: '/tab-bar',
text: 'TabBar'
}
]
},
......
此差异已折叠。
<template>
<cube-page type="tabs" title="Tab Demos">
<div slot="content">
<cube-button-group>
<cube-button @click="goTo('tab-bar')">TabBar Demos</cube-button>
<cube-button @click="goTo('tab')">Tab Demos</cube-button>
<cube-button @click="goTo('scroll-tab')">ScrollTab Demo</cube-button>
</cube-button-group>
<cube-view></cube-view>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from 'example/components/cube-page.vue'
import CubeButtonGroup from 'example/components/cube-button-group.vue'
import CubeView from 'example/components/cube-view.vue'
export default {
components: {
CubePage,
CubeButtonGroup,
CubeView
},
methods: {
goTo(subPath) {
this.$router.push('/tab-bar/' + subPath)
}
}
}
</script>
<template>
<cube-page type="scroll-tab-view" title="ScrollTab">
<div slot="content">
<div class="left-panel">
<cube-scroll>
<cube-tab-bar v-model="selectedLabel" :data="tabs" @change="changeHandler"></cube-tab-bar>
</cube-scroll>
</div>
<div class="right-panel">
<cube-scroll ref="scroll">
<ul>
<li v-for="(hero, index) in scrollData">
<img :src="hero.avatar" alt="">
<span>{{hero.name}}</span>
</li>
</ul>
</cube-scroll>
</div>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../../components/cube-page.vue'
import * as DATAS from '../../data/tab-bar'
const DATA_MAP = {
'全部': DATAS.ALL_HEROES,
'近战': DATAS.MELEE_HEROES,
'远程': DATAS.REMOTE_HEROES,
'辅助': DATAS.SUPPORT_HEROES,
'法师': DATAS.MAGIC_HEROES,
'打野': DATAS.JUNGLE_HEROES,
'坦克': DATAS.TANK_HEROES,
'隐身': DATAS.INVISIBLE_HEROES,
'后期': DATAS.CARRY_HEROES,
'闪烁': DATAS.BLINK_HEROES,
'爆发': DATAS.HIGH_DAMAGE_HEROES,
'召唤': DATAS.INVOKE_HEROES,
'眩晕': DATAS.DIZZY_HEROES,
'治疗': DATAS.HEALER_HEROES
}
const genTabLabels = Object.keys(DATA_MAP).map(label => ({
label
}))
export default {
data () {
return {
selectedLabel: '全部',
scrollData: [],
tabs: genTabLabels
}
},
created () {
this.scrollData = DATA_MAP[this.selectedLabel]
},
methods: {
changeHandler (label) {
this.scrollData = DATA_MAP[label]
this.$nextTick(() => {
// reset better-scroll'postion
this.$refs.scroll.scrollTo(0, 0)
// you need to caculate scroll-content height when your dom has changed in nextTick
this.$refs.scroll.refresh()
})
}
},
components: {
CubePage
},
watch: {
selectedLabel (newV) {
console.log(newV)
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.cube-scroll-list-wrapper
.cube-tab-bar
flex-wrap: wrap
.cube-tab
width: 100%
flex-basis: unset
height: 40px
line-height: 40px
font-size: 14px
color: #db8931
transition: all .3s ease-in
&.cube-tab_active
color: #fff
font-size: 16px
background-color: #a74b00
.left-panel
position: absolute
top: 44px
left: 0
bottom: 0
width: 80px
background-color: #2d2d2d
.right-panel
position: absolute
top: 44px
left: 80px
right: 0
bottom: 0
background-color: #171819
li
height: 80px
display: flex
align-items: center
background-color: #171819
img
width: 102px
margin: 0 10px 0 20px
border: 1px solid #ff9f38
border-radius: 3px
box-shadow: 0 1px 5px 0 #000
span
color: #db8931
</style>
<template>
<cube-page type="tab-bar-view" title="TabBar">
<div slot="content">
<!-- default -->
<div class="section">
<cube-tab-bar
v-model="selectedLabelDefault"
:data="tabs"
@click="clickHandler"
@change="changeHandler">
</cube-tab-bar>
</div>
<!-- showSlider -->
<div class="section">
<cube-tab-bar
v-model="selectedLabelSlots"
showSlider
@click="clickHandler"
@change="changeHandler">
<cube-tab v-for="(item, index) in tabs" :icon="item.icon" :label="item.label" :key="item.label">
</cube-tab>
</cube-tab-bar>
</div>
<!-- inline -->
<div class="section">
<cube-tab-bar
v-model="selectedLabelInline"
showSlider
inline
@click="clickHandler"
@change="changeHandler">
<cube-tab v-for="(item, index) in tabs" :icon="item.icon" :label="item.label" :key="item.label">
</cube-tab>
</cube-tab-bar>
</div>
<!-- icon-slot-->
<div class="section">
<cube-tab-bar v-model="selectedLabelSlotsOnly" @click="clickHandler">
<cube-tab v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<i slot="icon" :class="item.icon"></i>
<!-- use en empty tag to replace default slot -->
<span></span>
</cube-tab>
</cube-tab-bar>
</div>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../../components/cube-page.vue'
export default {
data() {
return {
selectedLabelDefault: 'Vip',
selectedLabelSlots: 'Like',
selectedLabelInline: 'Me',
selectedLabelSlotsOnly: 'Home',
tabs: [{
label: 'Home',
icon: 'cubeic-home'
}, {
label: 'Like',
icon: 'cubeic-like'
}, {
label: 'Vip',
icon: 'cubeic-vip'
}, {
label: 'Me',
icon: 'cubeic-person'
}]
}
},
methods: {
clickHandler (label) {
console.log('tab was clicked', label)
},
changeHandler (label) {
console.log('value has changed, now is', label)
}
},
components: {
CubePage
},
watch: {
selectedLabel (newV) {
console.log(newV)
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import "~@/common/stylus/variable.styl"
.tab-bar-view
.section
margin-bottom: 10px
background-color: white
.option-list
.group
margin-bottom: 15px
.item
height: 52px
border: 1px solid rgba(0, 0, 0, .1)
background-color: white
border-radius: 5px
.cube-tab-bar
background-color: white
</style>
<template>
<cube-page type="tabs-basic-view" title="tab-basic">
<div slot="content">
<cube-tab-bar v-model="selectedLabel" showSlider>
<cube-tab v-for="(item, index) in tabs" :label="item.label" :icon="item.icon" :key="item.label"></cube-tab>
</cube-tab-bar>
<cube-tab-panels v-model="selectedLabel">
<cube-tab-panel v-for="(item, index) in tabs" :label="item.label" :key="item.label">
<ul>
<li class="tab-panel-li" v-for="(hero, index) in item.heroes" :key="index">
{{hero}}
</li>
</ul>
</cube-tab-panel>
</cube-tab-panels>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../../components/cube-page.vue'
export default {
data() {
return {
selectedLabel: '天辉',
tabs: [{
label: '天辉',
icon: 'cubeic-like',
heroes: ['敌法师', '卓尔游侠', '主宰', '米拉娜', '变体精灵']
}, {
label: '夜魇',
icon: 'cubeic-star',
heroes: ['血魔', '影魔', '剃刀', '剧毒术士', '虚空假面', '幻影刺客', '冥界亚龙', '克林克兹', '育母蜘蛛', '编织者', '幽鬼', '司夜刺客', '米波']
}]
}
},
components: {
CubePage
},
watch: {
selectedLabel (newV) {
console.log(newV)
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.tabs-basic-view
.cube-tab-bar
background-color: #fff
.cube-tab-panels
background-color: #fff
.tab-panel-li
padding: 0 16px
height: 40px
line-height: 40px
border-top: 1px solid #eee
&:last-child
border-bottom: 1px solid #eee
</style>
<template>
<cube-page type="tab-composite-view" title="tab-composite">
<div slot="content">
<cube-tab-bar v-model="selectedLabel"
showSlider
:useTransition="disabled"
ref="tabNav"
:data="tabLabels">
</cube-tab-bar>
<div class="slide-container">
<cube-slide
ref="slide"
:loop="loop"
:initial-index="initialIndex"
:auto-play="autoPlay"
:show-dots="showDots"
:options="options"
@scroll="scroll"
@change="changePage"
>
<!-- 关注 -->
<cube-slide-item>
<cube-scroll :data="followersData">
<ul class="list-wrapper">
<li v-for="(item, index) in followersData" class="list-item" :key="index">
<div class="top">
<img :src="item.avatar" class="avatar">
<span class="time">{{resolveTitle(item)}}</span>
</div>
<div class="middle is-bold line-height">{{item.question}}</div>
<div>{{resolveQuestionFollowers(item)}}</div>
</li>
</ul>
</cube-scroll>
</cube-slide-item>
<!-- 推荐 -->
<cube-slide-item>
<cube-scroll :data="recommendData">
<ul class="list-wrapper">
<li v-for="(item, index) in recommendData" class="list-item" :key="index">
<div class="top is-black is-bold line-height">
{{item.question}}
</div>
<div class="middle is-grey line-height">{{item.content}}</div>
<div>{{resolveQuestionFollowers(item)}}</div>
</li>
</ul>
</cube-scroll>
</cube-slide-item>
<cube-slide-item>
<cube-scroll :data="hotData">
<ul class="list-wrapper">
<li v-for="(item, index) in hotData" class="list-item" :key="index">
<div class="hot-title">
<span class="hot-sequence">{{item.sequence}}</span>
<span></span>
{{item.label}}
</div>
<div class="hot-content is-bold is-black">{{item.question}}</div>
</li>
</ul>
</cube-scroll>
</cube-slide-item>
</cube-slide>
</div>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../../components/cube-page.vue'
import { FOLLOWERS_DATA, RECOMMEND_DATA, HOT_DATA } from '../../data/tab-bar'
import { findIndex } from '../../../src/common/helpers/util'
export default {
data () {
return {
selectedLabel: '推荐',
disabled: false,
tabLabels: [{
label: '关注'
}, {
label: '推荐'
}, {
label: '热榜'
}],
loop: false,
autoPlay: false,
showDots: false,
options: {
listenScroll: true,
probeType: 3
},
followersData: FOLLOWERS_DATA,
recommendData: RECOMMEND_DATA,
hotData: HOT_DATA
}
},
methods: {
changePage (current) {
this.selectedLabel = this.tabLabels[current].label
console.log(current)
},
scroll (pos) {
const x = Math.abs(pos.x)
const tabItemWidth = this.$refs.tabNav.$el.clientWidth
const slideScrollerWidth = this.$refs.slide.slide.scrollerWidth
const deltaX = x / slideScrollerWidth * tabItemWidth
this.$refs.tabNav.setSliderTransform(deltaX)
},
resolveTitle (item) {
return `${item.name}关注了问题 · ${item.postTime} 小时前`
},
resolveQuestionFollowers (item) {
return `${item.answers} 赞同 · ${item.followers} 评论`
}
},
computed: {
initialIndex () {
let index = 0
index = findIndex(this.tabLabels, item => item.label === this.selectedLabel)
return index
}
},
components: {
CubePage
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
/* 覆盖样式 */
.cube-page
&.tab-composite-view
> .wrapper
> .content
margin: 0
.tab-composite-view
.cube-tab-bar
background-color: white
.cube-tab, .cube-tab_active
color: black
.cube-tab-bar-slider
background-color: black
.slide-container
position: fixed
top: 74px
left: 0
right: 0
bottom: 0
.list-wrapper
overflow: hidden
li
padding: 15px 10px
margin-top: 10px
text-align: left
background-color: white
font-size: 14px
color: #999
white-space: normal
.line-height
line-height: 1.5
.is-black
color: black
.is-grey
color: #999
.is-bold
font-weight: bold
.top
display: flex
.avatar
width: 15px
height: 15px
margin-right: 2px
border-radius: 100%
.time
flex: 1
.middle
display: flex
margin: 10px 0
color: black
.hot-title
display: flex
align-items: center
font-size: 12px
.hot-sequence
display: inline-block
margin-right: 2px
padding: 3px 6px
border-radius: 2px
background-color: darkgoldenrod
color: white
.hot-content
margin-top: 15px
</style>
<template>
<cube-page type="tab-entry" title="Tab Entry">
<div slot="content">
<cube-button-group>
<cube-button @click="goTo('basic')">tab-basic</cube-button>
<cube-button @click="goTo('composite')">tab-composite</cube-button>
</cube-button-group>
<cube-view></cube-view>
</div>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from 'example/components/cube-page.vue'
import CubeButtonGroup from 'example/components/cube-button-group.vue'
import CubeView from 'example/components/cube-view.vue'
export default {
components: {
CubePage,
CubeButtonGroup,
CubeView
},
methods: {
goTo(subPath) {
this.$router.push('/tab-bar/tab/' + subPath)
}
}
}
</script>
......@@ -50,6 +50,12 @@ import SlideVertical from '../pages/slide/vertical.vue'
import SlideHorizontal from '../pages/slide/horizontal.vue'
import Toolbar from '../pages/toolbar.vue'
import ImagePreview from '../pages/image-preview.vue'
import TabBarIndex from '../pages/tab-bar/index.vue'
import TabBar from '../pages/tab-bar/tab-bar.vue'
import Tab from '../pages/tab-bar/tab-entry.vue'
import TabBasic from '../pages/tab-bar/tab-basic.vue'
import TabComposite from '../pages/tab-bar/tab-composite.vue'
import ScrollTab from '../pages/tab-bar/scroll-tab.vue'
const routes = [
{
......@@ -273,6 +279,36 @@ const routes = [
component: SlideHorizontal
}
]
},
{
path: '/toolbar',
component: Toolbar
},
{
path: '/tab-bar',
component: TabBarIndex,
children: [
{
path: 'tab-bar',
component: TabBar
},
{
path: 'scroll-tab',
component: ScrollTab
},
{
path: 'tab',
component: Tab,
children: [
{ path: 'basic',
component: TabBasic
},
{ path: 'composite',
component: TabComposite
}
]
}
]
}
]
......
......@@ -209,3 +209,8 @@ $drawer-item-active-bgc := $color-light-grey-opacity
// image-preview
$image-preview-counter-color := $color-white
// tab-bar & tab-panel
$tab-color := $color-grey
$tab-active-color := $color-dark-orange
$tab-slider-bgc := $color-dark-orange
......@@ -28,6 +28,7 @@
const EVENT_CHANGE = 'change'
const EVENT_SELECT = 'click'
const EVENT_SCROLL_END = 'scroll-end'
const EVENT_SCROLL = 'scroll'
const DIRECTION_H = 'horizontal'
const DIRECTION_V = 'vertical'
......@@ -203,7 +204,12 @@
this.slide.goToPage(this.currentPageIndex, 0, 0)
this.slide.on('scrollEnd', this._onScrollEnd)
/* dispatch scroll position */
if (this.options.listenScroll) {
// ensure dispatch scroll position constantly
this.options.probeType = 3
this.slide.on('scroll', this._onScroll)
}
const slideEl = this.$refs.slide
slideEl.removeEventListener('touchend', this._touchEndEvent, false)
this._touchEndEvent = () => {
......@@ -233,6 +239,9 @@
this._play()
}
},
_onScroll(pos) {
this.$emit(EVENT_SCROLL, pos)
},
_initDots() {
this.dots = new Array(this.children.length)
},
......
<template>
<div class="cube-tab-bar" :class="{'cube-tab-bar_inline': inline}">
<slot>
<cube-tab
v-for="(item, index) in data"
:label="item.label"
:icon="item.icon"
:key="item.label">
</cube-tab>
</slot>
<div v-if="showSlider" ref="slider" class="cube-tab-bar-slider"></div>
</div>
</template>
<script type="text/ecmascript-6">
import { prefixStyle } from '../../common/helpers/dom'
import { findIndex } from '../../common/helpers/util'
import CubeTab from './tab.vue'
const COMPONENT_NAME = 'cube-tab-bar'
const EVENT_INPUT = 'input'
const EVENT_CHANGE = 'change'
const EVENT_CLICK = 'click'
const TRANSFORM = prefixStyle('transform')
const TRANSITION = prefixStyle('transition')
export default {
name: COMPONENT_NAME,
components: {
CubeTab
},
props: {
value: {
type: [String, Number],
required: true
},
data: {
type: Array,
default () {
return []
}
},
inline: {
type: Boolean,
default: false
},
showSlider: {
type: Boolean,
default: false
},
useTransition: {
type: Boolean,
default: true
}
},
created () {
this.tabs = []
},
mounted () {
this._updateSliderStyle()
},
methods: {
addTab (tab) {
this.tabs.push(tab)
},
removeTab (tab) {
const index = this.tabs.indexOf(tab)
if (index > -1) this.tabs.splice(index, 1)
},
trigger (label) {
// emit click event as long as tab is clicked
this.$emit(EVENT_CLICK, label)
// only when value changed, emit change & input event
if (label !== this.value) {
const changedEvents = [EVENT_INPUT, EVENT_CHANGE]
changedEvents.forEach((eventType) => {
this.$emit(eventType, label)
})
}
},
_updateSliderStyle () {
/* istanbul ignore if */
if (!this.showSlider) return
const slider = this.$refs.slider
this.$nextTick(() => {
const { width, index } = this._getSliderWidthAndIndex()
slider.style.width = `${width}px`
this.setSliderTransform(this._getOffsetLeft(index))
})
},
setSliderTransform (offset) {
const slider = this.$refs.slider
if (typeof offset === 'number') {
offset = `${offset}px`
}
if (slider) {
if (this.useTransition) slider.style[TRANSITION] = `all 0.2s linear`
slider.style[TRANSFORM] = `translateX(${offset}) translateZ(0)`
}
},
_getSliderWidthAndIndex () {
let width = 0
let index = 0
if (this.tabs.length > 0) {
index = findIndex(this.tabs, (tab) => tab.label === this.value)
width = this.tabs[index].$el.clientWidth
}
return {
width,
index
}
},
_getOffsetLeft (index) {
let offsetLeft = 0
this.tabs.forEach((tab, i) => {
if (i < index) offsetLeft += tab.$el.clientWidth
})
return offsetLeft
}
},
watch: {
value () {
this._updateSliderStyle()
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
.cube-tab-bar
position: relative
display: flex
align-items: center
justify-content: center
.cube-tab-bar_inline
.cube-tab
display: flex
align-content: center
justify-content: center
.cube-tab-bar-slider
position: absolute
left: 0
bottom: 0
height: 2px
width: 20px
background-color: $tab-slider-bgc
</style>
<template>
<div
class="cube-tab"
:class="{'cube-tab_active': isActive}"
@click="handleClick">
<slot name="icon">
<i :class="icon"></i>
</slot>
<slot>
<div v-html="label"></div>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
const COMPONENT_NAME = 'cube-tab'
export default {
name: COMPONENT_NAME,
props: {
label: {
type: [String, Number],
required: true
},
icon: {
type: String,
default: ''
}
},
mounted () {
this.$parent.addTab(this)
},
destroyed () {
this.$parent.removeTab(this)
},
computed: {
isActive () {
return this.$parent.value === this.label
}
},
methods: {
handleClick (item) {
this.$parent.trigger(this.label)
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
.cube-tab
flex: 1
padding: 7px 0
color: $tab-color
text-align: center
.cube-tab_active
color: $tab-active-color
</style>
<template>
<div class="cube-tab-panel">
<slot>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
const COMPONENT_NAME = 'cube-tab-panel'
export default {
name: COMPONENT_NAME,
props: {
label: {
type: [String, Number],
required: true
}
},
mounted () {
this.$parent.addPanel(this)
},
destroyed () {
this.$parent.removePanel(this)
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.cube-tab-panel
width: 100%
flex: 1 0 auto
</style>
<template>
<div class="cube-tab-panels" ref="panels">
<div class="cube-tab-panels-group" ref="panelsGroup">
<slot>
<cube-tab-panel
v-for="(item, index) in data"
:label="item.label"
:key="item.label">
{{item.label}}
</cube-tab-panel>
</slot>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import TabPanel from './tab-panel.vue'
import { findIndex } from '../../common/helpers/util'
import { prefixStyle } from '../../common/helpers/dom'
const TRANSFORM = prefixStyle('transform')
const COMPONENT_NAME = 'cube-tab-panels'
export default {
name: COMPONENT_NAME,
props: {
value: {
type: [String, Number]
},
data: {
type: Array,
default () {
return []
}
}
},
created () {
this.panels = []
},
mounted () {
this._move(this.value)
},
methods: {
_move(label) {
const curIndex = findIndex(this.panels, panel => panel.label === label)
const panelsGroup = this.$refs.panelsGroup
const distance = -(curIndex * 100)
panelsGroup.style[TRANSFORM] = `translateX(${distance}%)`
},
addPanel (panel) {
this.panels.push(panel)
},
removePanel (panel) {
const index = this.panels.indexOf(panel)
if (index > -1) this.panels.splice(index, 1)
}
},
watch: {
value (newV) {
this._move(newV)
}
},
components: { TabPanel }
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.cube-tab-panels
position: relative
overflow: hidden
.cube-tab-panels-group
display: flex
transition: all .4s cubic-bezier(.86, 0, .07, 1)
</style>
......@@ -38,7 +38,10 @@ import {
Swipe,
// module
BetterScroll,
createAPI
createAPI,
// tabs
TabBar,
TabPanels
} from './module'
import { processComponentName } from './common/helpers/util'
......@@ -48,7 +51,8 @@ const components = [
Loading,
Tip,
Toolbar,
ImagePreview,
TabBar,
TabPanels,
// form
Checkbox,
CheckboxGroup,
......@@ -73,6 +77,7 @@ const components = [
Dialog,
ActionSheet,
Drawer,
ImagePreview,
// scroll
Scroll,
Slide,
......
......@@ -5,7 +5,8 @@ import Button from './modules/button'
import Loading from './modules/loading'
import Tip from './modules/tip'
import Toolbar from './modules/toolbar'
import ImagePreview from './modules/image-preview'
import TabBar from './modules/tab-bar'
import TabPanels from './modules/tab-panels'
// Form
import Checkbox from './modules/checkbox'
......@@ -31,6 +32,7 @@ import SegmentPicker from './modules/segment-picker'
import Dialog from './modules/dialog'
import ActionSheet from './modules/action-sheet'
import Drawer from './modules/drawer'
import ImagePreview from './modules/image-preview'
// Scroll
import Scroll from './modules/scroll'
......@@ -49,6 +51,8 @@ const Radio = RadioGroup.Radio
const SwipeItem = Swipe.Item
const DrawerPanel = Drawer.Panel
const DrawerItem = Drawer.Item
const Tab = TabBar.Tab
const TabPanel = TabPanels.Panel
export {
// style
......@@ -58,6 +62,10 @@ export {
Loading,
Tip,
Toolbar,
TabBar,
Tab,
TabPanels,
TabPanel,
// form
Checkbox,
CheckboxGroup,
......
import TabBar from '../../components/tab-bar/tab-bar.vue'
import Tab from '../../components/tab-bar/tab.vue'
TabBar.install = function (Vue) {
Vue.component(TabBar.name, TabBar)
Vue.component(Tab.name, Tab)
}
TabBar.Tab = Tab
export default TabBar
import TabPanels from '../../components/tab-panels/tab-panels.vue'
import TabPanel from '../../components/tab-panels/tab-panel.vue'
TabPanels.install = function (Vue) {
Vue.component(TabPanels.name, TabPanels)
Vue.component(TabPanel.name, TabPanel)
}
TabPanels.Panel = TabPanel
export default TabPanels
......@@ -176,6 +176,36 @@ describe('Slide.vue', () => {
}, 100)
})
it('should trigger scroll when changed', function (done) {
this.timeout(10000)
const scrollHandler = sinon.spy()
vm = createVue({
template: `
<cube-slide :autoPlay="autoPlay" :interval="interval" :threshold="threshold" :options="options" @scroll="scroll" style="width:300px;height:100px;">
<cube-slide-item v-for="(item,index) in items" :key="index" :item="item"></cube-slide-item>
</cube-slide>
`,
data: {
items,
autoPlay: true,
threshold: 0.1,
interval: 100,
options: {
listenScroll: true,
probeType: 3
}
},
methods: {
scroll: scrollHandler
}
})
setTimeout(() => {
expect(scrollHandler).to.be.called
done()
}, 2000)
})
it('should go to right pageIndex if set initialIndex', function (done) {
vm = createVue({
template: `
......
import Vue from 'vue2'
import CubeTabBar from '@/modules/tab-bar'
import CubeTabPanels from '@/modules/tab-panels'
import createVue from '../utils/create-vue'
describe('TabBar', () => {
let vm
it('use', () => {
Vue.use(CubeTabBar)
expect(Vue.component(CubeTabBar.name))
.to.be.a('function')
Vue.use(CubeTabPanels)
expect(Vue.component(CubeTabPanels.name))
.to.be.a('function')
})
it('props', (done) => {
vm = createVue({
template: `
<cube-tab-bar v-model="selectedLabel" :data="tabs" showSlider>
</cube-tab-bar>
`,
data: {
selectedLabel: '夜魇',
tabs: [{ label: '天辉', icon: 'cubeic-like' }, { label: '夜魇', icon: 'cubeic-star' }]
}
})
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.cube-tab-bar-slider').length)
.to.be.equal(1)
expect(vm.$el.querySelectorAll('.cube-tab')[0].getElementsByTagName('div')[0].textContent)
.to.include('天辉')
done()
})
})
it('should render correct content when pass data prop', (done) => {
const vm = createVue({
template: `
<div>
<cube-tab-bar v-model="selectedLabel">
<cube-tab ref="tab" v-for="(item, index) in tabs" :label="item.label" :key="index">
{{item.label}}
</cube-tab>
</cube-tab-bar>
<cube-tab-panels v-model="selectedLabel" :data="tabs"></cube-tab-panels>
</div>
`,
data: {
selectedLabel: '夜魇',
tabs: [{ label: '天辉', icon: 'cubeic-like' }, { label: '夜魇', icon: 'cubeic-star' }]
}
})
setTimeout(() => {
const items = vm.$parent.$el.querySelectorAll('.cube-tab-panel')
const firstTab = vm.$parent.$refs.tab[0].$el
firstTab.click()
expect(items[0].textContent)
.to.include('天辉')
done()
}, 300)
})
it('should toggle v-model value', () => {
vm = createTabBar()
const items = vm.$el.querySelectorAll('.cube-tab')
items[1].click()
expect(vm.$parent.selectedLabel)
.to.be.equal('夜魇')
})
it('should trigger click and change event', () => {
const clickHandler = sinon.spy()
const changeHandler = sinon.spy()
vm = createTabBar({ clickHandler, changeHandler })
const items = vm.$el.querySelectorAll('.cube-tab')
items[1].click()
expect(clickHandler)
.to.be.calledOnce
expect(changeHandler)
.to.be.calledOnce
})
it('should remove child dom when child component destroyed', (done) => {
vm = createVue({
template: `
<div class="cube-tabs-container">
<cube-tab-bar ref="tabBar" v-model="selectedLabel">
<cube-tab ref="tab" v-for="(item, index) in tabs" :label="item.label" :key="index">
{{item.label}}
</cube-tab>
</cube-tab-bar>
<cube-tab-panels v-model="selectedLabel">
<cube-tab-panel ref="panel" v-for="(item, index) in tabs" :label="item.label" :key="index">
{{item.label}}
</cube-tab-panel>
</cube-tab-panels>
</div>
`,
data: {
selectedLabel: '天辉',
tabs: [{ label: '天辉', class: 'cubeic-like' }, { label: '夜魇', class: 'cubeic-star' }]
}
})
// destroyed tab and panel
vm.$parent.tabs.splice(0, 1)
setTimeout(() => {
expect(vm.$parent.$el.querySelectorAll('.cube-tab').length)
.to.be.equal(1)
expect(vm.$parent.$el.querySelectorAll('.cube-tab-panel').length)
.to.be.equal(1)
done()
}, 1000)
})
})
function createTabBar (options) {
const vm = createVue({
template: `
<cube-tab-bar v-model="selectedLabel" showSlider @click="clickHandler" @change="changeHandler">
<cube-tab v-for="(item, index) in tabs" :label="item.label" :key="index" >
<i slot="icon" :class="item.icon"></i>
{{item.label}}
</cube-tab>
</cube-tab-bar>
`,
data: {
selectedLabel: '天辉',
tabs: [{ label: '天辉', icon: 'cubeic-like' }, { label: '夜魇', icon: 'cubeic-star' }]
},
methods: {
clickHandler: (options && options.clickHandler) || function () {},
changeHandler: (options && options.changeHandler) || function () {}
}
})
return vm
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册