dynamic tabs (#812)

pull/816/head
baiyaaaaa 2016-11-04 11:28:23 +08:00 committed by cinwell.li
parent cb5572f9d7
commit 8eae476f7f
7 changed files with 114 additions and 125 deletions

View File

@ -12,6 +12,7 @@
- 新增 Dropdown 的 command api #432 - 新增 Dropdown 的 command api #432
- 修复 Slider 在 Form 中的显示问题 - 修复 Slider 在 Form 中的显示问题
- 修复 Upload 在 onSuccess、onError 钩子无法拿到服务端返回信息的问题 - 修复 Upload 在 onSuccess、onError 钩子无法拿到服务端返回信息的问题
- 改善 tabs 现在支持动态更新
#### 非兼容性更新 #### 非兼容性更新

View File

@ -3,7 +3,13 @@
data() { data() {
return { return {
activeName: 'first', activeName: 'first',
activeName2: '' activeName2: '',
tabs: [
{label: '用户管理', content: ''},
{label: '配置管理', content: ''},
{label: '角色管理', content: ''},
{label: '定时任务补偿', content: ''}
]
} }
}, },
methods: { methods: {
@ -28,17 +34,19 @@
```html ```html
<template> <template>
<el-tabs> <el-tabs>
<el-tab-pane label="用户管理"></el-tab-pane> <el-tab-pane v-for="tab in tabs" :label="tab.label">{{tab.content}}</el-tab-pane>
<el-tab-pane label="配置管理"></el-tab-pane>
<el-tab-pane label="角色管理"></el-tab-pane>
<el-tab-pane label="定时任务补偿"></el-tab-pane>
</el-tabs> </el-tabs>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
activeName: 'first' tabs: [
{label: '用户管理', content: ''},
{label: '配置管理', content: ''},
{label: '角色管理', content: ''},
{label: '定时任务补偿', content: ''}
]
}; };
} }
}; };

View File

@ -17,19 +17,19 @@
paneStyle: { paneStyle: {
position: 'relative' position: 'relative'
}, },
key: '' index: ''
}; };
}, },
created() { created() {
if (!this.key) { if (!this.index) {
this.key = this.$parent.$children.indexOf(this) + 1 + ''; this.index = this.$parent.$children.indexOf(this) + 1 + '';
} }
}, },
computed: { computed: {
show() { show() {
return this.$parent.currentName === this.key; return this.$parent.currentName === this.index;
} }
}, },
@ -41,21 +41,20 @@
name: { name: {
immediate: true, immediate: true,
handler(val) { handler(val) {
this.key = val; this.index = val;
} }
}, },
'$parent.currentName'(newValue, oldValue) { '$parent.currentName'(newValue, oldValue) {
if (this.key === newValue) { if (this.index === newValue) {
this.transition = newValue > oldValue ? 'slideInRight' : 'slideInLeft'; this.transition = newValue > oldValue ? 'slideInRight' : 'slideInLeft';
} }
if (this.key === oldValue) { if (this.index === oldValue) {
this.transition = oldValue > newValue ? 'slideInRight' : 'slideInLeft'; this.transition = oldValue > newValue ? 'slideInRight' : 'slideInLeft';
} }
} }
} }
}; };
</script> </script>
<template> <template>
<div class="el-tab-pane" v-show="show && $slots.default"> <div class="el-tab-pane" v-show="show && $slots.default">
<slot></slot> <slot></slot>

View File

@ -1,30 +0,0 @@
<script>
module.exports = {
name: 'el-tab',
props: {
tab: {
type: Object,
required: true
},
closable: Boolean
}
};
</script>
<template>
<div
class="el-tabs__item"
:class="{
'is-active': $parent.currentName === tab.key,
'is-disabled': tab.disabled,
'is-closable': closable
}">
{{tab.label}}
<span
class="el-icon-close"
v-if="closable"
@click="$emit('remove', tab, $event)">
</span>
</div>
</template>

View File

@ -1,13 +1,7 @@
<script> <script>
import ElTab from './tab';
module.exports = { module.exports = {
name: 'el-tabs', name: 'el-tabs',
components: {
ElTab
},
props: { props: {
type: String, type: String,
tabPosition: String, tabPosition: String,
@ -18,11 +12,9 @@
data() { data() {
return { return {
tabs: [],
children: null, children: null,
activeTab: null, activeTab: null,
currentName: 0, currentName: 0
barStyle: ''
}; };
}, },
@ -31,45 +23,40 @@
handler(val) { handler(val) {
this.currentName = val; this.currentName = val;
} }
},
'currentName'() {
this.calcBarStyle();
} }
}, },
methods: { methods: {
handleTabRemove(tab, ev) { handleTabRemove(tab, event) {
ev.stopPropagation(); event.stopPropagation();
let tabs = this.$children;
var index = tabs.indexOf(tab);
tab.$destroy(true); tab.$destroy(true);
var index = this.tabs.indexOf(tab); if (tab.index === this.currentName) {
let nextChild = tabs[index];
let prevChild = tabs[index - 1];
if (index !== -1) { this.currentName = nextChild ? nextChild.index : prevChild ? prevChild.index : '-1';
this.tabs.splice(index, 1);
}
if (tab.key === this.currentName) {
let nextChild = this.tabs[index];
let prevChild = this.tabs[index - 1];
this.currentName = nextChild ? nextChild.key : prevChild ? prevChild.key : '-1';
} }
this.$emit('tab-remove', tab); this.$emit('tab-remove', tab);
this.$forceUpdate();
}, },
handleTabClick(tab, event) { handleTabClick(tab, event) {
this.currentName = tab.key; this.currentName = tab.index;
this.$emit('tab-click', tab, event); this.$emit('tab-click', tab, event);
}, },
calcBarStyle(firstRendering) { calcBarStyle() {
if (this.type || !this.$refs.tabs) return {}; if (this.type || !this.$refs.tabs) return {};
var style = {}; var style = {};
var offset = 0; var offset = 0;
var tabWidth = 0; var tabWidth = 0;
this.tabs.every((tab, index) => { this.$children.every((panel, index) => {
let $el = this.$refs.tabs[index].$el; let $el = this.$refs.tabs[index];
if (tab.key !== this.currentName) { if (!$el) { return false; }
if (panel.index !== this.currentName) {
offset += $el.clientWidth; offset += $el.clientWidth;
return true; return true;
} else { } else {
@ -81,41 +68,66 @@
style.width = tabWidth + 'px'; style.width = tabWidth + 'px';
style.transform = `translateX(${offset}px)`; style.transform = `translateX(${offset}px)`;
if (!firstRendering) { return style;
style.transition = 'transform .3s cubic-bezier(.645,.045,.355,1), -webkit-transform .3s cubic-bezier(.645,.045,.355,1)';
}
this.barStyle = style;
} }
}, },
mounted() { mounted() {
var fisrtKey = this.$children[0].key || '1'; this.$nextTick(() => {
this.currentName = this.activeName || fisrtKey; this.currentName = this.activeName || this.$children[0].index || '1';
this.$children.forEach(tab => this.tabs.push(tab)); });
this.$nextTick(() => this.calcBarStyle(true)); },
render(h) {
let {
type,
closable,
handleTabRemove,
handleTabClick,
currentName
} = this;
const barStyle = this.calcBarStyle();
const activeBar = !type
? <div class="el-tabs__active-bar" style={barStyle}></div>
: null;
const tabs = this.$children.map((tab, index) => {
let btnClose = h('span', {
class: {
'el-icon-close': true
},
on: { click: (ev) => { handleTabRemove(tab, ev); } }
});
const _tab = h('div', {
class: {
'el-tabs__item': true,
'is-active': currentName === tab.index,
'is-disabled': tab.disabled,
'is-closable': closable
},
ref: 'tabs',
refInFor: true,
on: { click: (ev) => { handleTabClick(tab, ev); } }
}, [
tab.label,
closable ? btnClose : null,
index === 0 ? activeBar : null
]);
return _tab;
});
return (
<div class={{
'el-tabs': true,
'el-tabs--card': type === 'card',
'el-tabs--border-card': type === 'border-card'
}}>
<div class="el-tabs__header">
{tabs}
</div>
<div class="el-tabs__content">
{this.$slots.default}
</div>
</div>
);
} }
}; };
</script> </script>
<template>
<div class="el-tabs" :class="[type ? 'el-tabs--' + type : '']">
<div class="el-tabs__header">
<el-tab
v-for="tab in tabs"
ref="tabs"
:tab="tab"
:closable="closable"
@remove="handleTabRemove"
@click.native="handleTabClick(tab, $event)">
</el-tab>
<div
class="el-tabs__active-bar"
:style="barStyle"
v-if="!this.type && tabs.length > 0">
</div>
</div>
<div class="el-tabs__content">
<slot></slot>
</div>
</div>
</template>

View File

@ -14,12 +14,13 @@
} }
@e active-bar { @e active-bar {
position: absolute; position: absolute;
bottom: -1px; bottom: 0;
left: 0; left: 0;
width: 100%;
height: 3px; height: 3px;
background-color: var(--color-primary); background-color: var(--color-primary);
z-index: 1; z-index: 1;
/*transition: transform .3s cubic-bezier(.645,.045,.355,1);*/ transition: transform .3s cubic-bezier(.645,.045,.355,1);
list-style: none; list-style: none;
} }
@e item { @e item {
@ -44,7 +45,6 @@
} }
} }
@e content { @e content {
white-space: nowrap;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }

View File

@ -9,22 +9,27 @@ describe('Tabs', () => {
it('create', done => { it('create', done => {
vm = createVue({ vm = createVue({
template: ` template: `
<el-tabs> <el-tabs ref="tabs">
<el-tab-pane label="用户管理">A</el-tab-pane> <el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane> <el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane> <el-tab-pane label="角色管理" ref="pane-click">C</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane> <el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs> </el-tabs>
` `
}, true); }, true);
let tabList = vm.$el.querySelector('.el-tabs__header').children; let tabList = vm.$el.querySelector('.el-tabs__header').children;
let paneList = vm.$el.querySelector('.el-tabs__content').children; let paneList = vm.$el.querySelector('.el-tabs__content').children;
let spy = sinon.spy();
vm.$refs.tabs.$on('tab-click', spy);
setTimeout(_ => { setTimeout(_ => {
expect(tabList[0].classList.contains('is-active')).to.be.true; expect(tabList[0].classList.contains('is-active')).to.be.true;
expect(paneList[0].style.display).to.not.ok; expect(paneList[0].style.display).to.not.ok;
tabList[2].click(); tabList[2].click();
vm.$nextTick(_ => { vm.$nextTick(_ => {
expect(spy.withArgs(vm.$refs['pane-click']).calledOnce).to.true;
expect(tabList[2].classList.contains('is-active')).to.be.true; expect(tabList[2].classList.contains('is-active')).to.be.true;
expect(paneList[2].style.display).to.not.ok; expect(paneList[2].style.display).to.not.ok;
done(); done();
@ -98,33 +103,27 @@ describe('Tabs', () => {
it('closable', done => { it('closable', done => {
vm = createVue({ vm = createVue({
template: ` template: `
<el-tabs type="card" closable @tab-remove="handleRemove"> <el-tabs type="card" :closable="true" ref="tabs">
<el-tab-pane label="用户管理">A</el-tab-pane> <el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane> <el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane> <el-tab-pane label="角色管理">C</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane> <el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs> </el-tabs>
`, `
data() {
return {
removeTabName: ''
};
},
methods: {
handleRemove(tab) {
this.removeTabName = tab.label;
}
}
}, true); }, true);
let tabList = vm.$el.querySelector('.el-tabs__header').children; let tabList = vm.$el.querySelector('.el-tabs__header').children;
let paneList = vm.$el.querySelector('.el-tabs__content').children; let paneList = vm.$el.querySelector('.el-tabs__content').children;
let spy = sinon.spy();
vm.$refs.tabs.$on('tab-remove', spy);
setTimeout(_ => { setTimeout(_ => {
tabList[1].querySelector('.el-icon-close').click(); tabList[1].querySelector('.el-icon-close').click();
vm.$nextTick(_ => { vm.$nextTick(_ => {
expect(tabList.length).to.be.equal(3); expect(tabList.length).to.be.equal(3);
expect(paneList.length).to.be.equal(3); expect(paneList.length).to.be.equal(3);
expect(vm.removeTabName).to.be.equal('配置管理'); expect(spy.calledOnce).to.true;
expect(tabList[1].innerText.trim()).to.be.equal('角色管理'); expect(tabList[1].innerText.trim()).to.be.equal('角色管理');
expect(paneList[0].innerText.trim()).to.be.equal('A'); expect(paneList[0].innerText.trim()).to.be.equal('A');
done(); done();
@ -134,7 +133,7 @@ describe('Tabs', () => {
it('closable edge', done => { it('closable edge', done => {
vm = createVue({ vm = createVue({
template: ` template: `
<el-tabs type="card" closable> <el-tabs type="card" :closable="true">
<el-tab-pane label="用户管理">A</el-tab-pane> <el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane> <el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane> <el-tab-pane label="角色管理">C</el-tab-pane>