390 lines
9.7 KiB
Vue
390 lines
9.7 KiB
Vue
<template>
|
|
<div class="md-tab-picker">
|
|
<md-popup
|
|
v-model="isTabPickerShow"
|
|
position="bottom"
|
|
:mask-closable="false"
|
|
@show="$_onShow"
|
|
@hide="$_onHide"
|
|
>
|
|
<md-popup-title-bar
|
|
:title="title"
|
|
:ok-text="okText"
|
|
:cancel-text="cancelText"
|
|
@confirm="$_onConfirm"
|
|
@cancel="$_onCancel"
|
|
></md-popup-title-bar>
|
|
<div class="md-tab-picker-content">
|
|
<md-tabs
|
|
ref="tabs"
|
|
:titles="subTitles"
|
|
:default-index="defaultTabIndex"
|
|
:force-use-array="hasTitleSlotScope"
|
|
@indexChanged="$_onIndexChange"
|
|
>
|
|
<template
|
|
slot="title"
|
|
slot-scope="props">
|
|
<slot :label="props" name="titles"></slot>
|
|
</template>
|
|
<div v-for="(title, index) of subTitles" :key="index">
|
|
<md-radio
|
|
ref="radio1"
|
|
:options="renderData[index].data"
|
|
:default-index="~renderData[index].clickedKey ? renderData[index].clickedKey : -1"
|
|
:key="renderData[index].clickedKey"
|
|
:is-slot-scope="hasOptionSlotScope"
|
|
@change="$_onRadioChange"
|
|
>
|
|
<template
|
|
slot-scope="props">
|
|
<slot :option="props.option" :index="index" name="option"></slot>
|
|
</template>
|
|
</md-radio>
|
|
</div>
|
|
</md-tabs>
|
|
<div class="slot-wrapper" v-if="isLoading || isDataError">
|
|
<div class="slot-inner">
|
|
<slot name="error" v-if="isDataError">数据异常</slot>
|
|
<slot name="loading" v-if="isLoading">loading</slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</md-popup>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Popup from '../popup'
|
|
import PopupTitlebar from '../popup/title-bar'
|
|
import Tabs from '../tabs'
|
|
import Icon from '../icon'
|
|
import Radio from '../radio'
|
|
import {noop, compareObjects} from '../_util'
|
|
|
|
export default {
|
|
name: 'md-tab-picker',
|
|
|
|
components: {
|
|
[Popup.name]: Popup,
|
|
[PopupTitlebar.name]: PopupTitlebar,
|
|
[Tabs.name]: Tabs,
|
|
[Icon.name]: Icon,
|
|
[Radio.name]: Radio,
|
|
},
|
|
|
|
props: {
|
|
value: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
okText: {
|
|
type: String,
|
|
default: '确认',
|
|
},
|
|
cancelText: {
|
|
type: String,
|
|
default: '取消',
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default() {
|
|
return []
|
|
},
|
|
},
|
|
dataStruct: {
|
|
type: String,
|
|
default: 'normal',
|
|
},
|
|
defaultIndex: {
|
|
type: Array,
|
|
default() {
|
|
return []
|
|
},
|
|
},
|
|
optionRender: {
|
|
type: Array,
|
|
default() {
|
|
return []
|
|
},
|
|
validator(value) {
|
|
if (value.length > 0) {
|
|
return value.every(item => {
|
|
return typeof item === 'function'
|
|
})
|
|
} else {
|
|
return true
|
|
}
|
|
},
|
|
},
|
|
asyncFunc: {
|
|
type: Function,
|
|
default() {
|
|
return noop
|
|
},
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
isTabPickerShow: false,
|
|
subTitles: [],
|
|
renderData: [],
|
|
defaultTabIndex: 0,
|
|
currentIndex: 0,
|
|
isLoading: true,
|
|
isDataError: false,
|
|
currentColumnLock: false,
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
value(val) {
|
|
val && (this.isTabPickerShow = val)
|
|
},
|
|
isTabPickerShow(val) {
|
|
!val && this.$emit('input', val)
|
|
},
|
|
data: {
|
|
handler(val, oldVal) {
|
|
if (!compareObjects(val, oldVal)) {
|
|
this.$_initTabPicker()
|
|
}
|
|
},
|
|
deep: true,
|
|
},
|
|
isDataError(val) {
|
|
if (val) {
|
|
setTimeout(() => {
|
|
this.isDataError = false
|
|
}, 2000)
|
|
}
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
hasTitleSlotScope() {
|
|
return !!this.$scopedSlots.titles
|
|
},
|
|
hasOptionSlotScope() {
|
|
return !!this.$scopedSlots.option
|
|
},
|
|
},
|
|
|
|
created() {
|
|
this.$_initTabPicker()
|
|
},
|
|
|
|
methods: {
|
|
// MARK: private methods
|
|
$_initTabPicker() {
|
|
switch (this.dataStruct) {
|
|
case 'normal':
|
|
this.$_initNoCascadeData()
|
|
break
|
|
case 'cascade':
|
|
this.$_initCascadeData()
|
|
break
|
|
case 'async':
|
|
this.$_initAsyncCascadeData()
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
},
|
|
$_initCascadeData() {
|
|
if (this.data.length === 0) {
|
|
return
|
|
}
|
|
this.isLoading = false
|
|
if (this.defaultIndex && this.defaultIndex.length > 0) {
|
|
let i = 0
|
|
const func = array => {
|
|
if (i < this.defaultIndex.length) {
|
|
const temp = this.defaultIndex[i]
|
|
array.forEach((item, eq) => {
|
|
if (eq === temp) {
|
|
this.subTitles.push(item.label)
|
|
this.renderData.push({index: i, clickedKey: temp, data: array})
|
|
if (item && item.children && Array.isArray(item.children)) {
|
|
i++
|
|
func(item.children)
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
this.defaultTabIndex = i
|
|
return false
|
|
}
|
|
}
|
|
func(this.data)
|
|
} else {
|
|
this.subTitles.push('请选择')
|
|
this.renderData.push({index: 0, clickedKey: -1, data: this.data})
|
|
}
|
|
},
|
|
$_initNoCascadeData() {
|
|
if (this.data.length === 0) {
|
|
return
|
|
}
|
|
this.isLoading = false
|
|
this.data.forEach((item, index) => {
|
|
const temp = {
|
|
index: index,
|
|
clickedKey: this.defaultIndex.length > 0 && ~this.defaultIndex[index] ? this.defaultIndex[index] : -1,
|
|
data: item.children,
|
|
}
|
|
this.renderData.push(temp)
|
|
const currentColumn = this.renderData[index]
|
|
if (this.defaultIndex && this.defaultIndex.length > 0) {
|
|
this.subTitles.push(currentColumn.data[currentColumn.clickedKey].label)
|
|
} else {
|
|
this.subTitles.push(item.label)
|
|
}
|
|
})
|
|
},
|
|
$_initAsyncCascadeData() {
|
|
this.asyncFunc(null, this.$_renderNextTabContent)
|
|
},
|
|
$_renderNextTabContent(err, data) {
|
|
this.isLoading = false
|
|
if (err) {
|
|
this.isDataError = err
|
|
return
|
|
}
|
|
try {
|
|
this.subTitles.splice(this.currentIndex + 1, this.subTitles.length - 1, '请选择')
|
|
this.renderData.splice(this.currentIndex + 1, this.renderData.length - 1, {
|
|
index: this.currentIndex,
|
|
clickedKey: -1,
|
|
data: data.options,
|
|
asyncFunc: data.asyncFunc,
|
|
})
|
|
if (this.renderData.length > 1) {
|
|
this.$nextTick(() => {
|
|
this.$refs.tabs.selectTab(++this.currentIndex)
|
|
})
|
|
}
|
|
} catch (error) {
|
|
this.isDataError = true
|
|
}
|
|
},
|
|
|
|
// MARK: events handler, 如 $_onButtonClick
|
|
$_onShow() {
|
|
this.$emit('show')
|
|
},
|
|
$_onHide() {
|
|
this.$emit('hide')
|
|
},
|
|
$_onConfirm() {
|
|
this.isTabPickerShow = false
|
|
const selectedItem = this.getSelectedItem()
|
|
this.$emit('confirm', selectedItem)
|
|
},
|
|
$_onCancel() {
|
|
this.isTabPickerShow = false
|
|
this.$emit('cancel')
|
|
},
|
|
$_onRadioChange(value, index) {
|
|
if (this.dataStruct === 'cascade' && this.currentColumnLock) {
|
|
return
|
|
}
|
|
this.currentColumnLock = true
|
|
this.subTitles[this.currentIndex] = value.label
|
|
const currentColumn = this.renderData[this.currentIndex]
|
|
currentColumn.clickedKey = index
|
|
this.$emit('change', {
|
|
selectTab: this.currentIndex,
|
|
selectIndex: index,
|
|
selectItem: value,
|
|
})
|
|
if (this.dataStruct === 'cascade') {
|
|
if (value && value.children && Array.isArray(value.children) && value.children.length > 0) {
|
|
this.subTitles.splice(this.currentIndex + 1, this.subTitles.length - 1, '请选择')
|
|
this.renderData.splice(this.currentIndex + 1, this.renderData.length - 1, {
|
|
index: ++this.currentIndex,
|
|
clickedKey: -1,
|
|
data: value.children,
|
|
})
|
|
this.$nextTick(() => {
|
|
this.$refs.tabs.selectTab(this.currentIndex)
|
|
setTimeout(() => {
|
|
this.currentColumnLock = false
|
|
}, 500)
|
|
})
|
|
return
|
|
}
|
|
this.currentColumnLock = false
|
|
}
|
|
if (this.dataStruct === 'async' && currentColumn.asyncFunc) {
|
|
value = {
|
|
index: index,
|
|
...value,
|
|
}
|
|
// Object.assign(value, {index})
|
|
this.isLoading = true
|
|
typeof currentColumn.asyncFunc === 'function' && currentColumn.asyncFunc(value, this.$_renderNextTabContent)
|
|
}
|
|
},
|
|
$_onIndexChange(index) {
|
|
this.currentIndex = index
|
|
},
|
|
|
|
// MARK: public methods
|
|
getSelectedItem() {
|
|
return this.renderData.map((item, index) => {
|
|
if (~item.clickedKey) {
|
|
const selected = item.data[item.clickedKey]
|
|
return {
|
|
index: index,
|
|
item: {
|
|
label: selected.label,
|
|
value: selected.value,
|
|
},
|
|
}
|
|
} else {
|
|
return null
|
|
}
|
|
})
|
|
},
|
|
},
|
|
}
|
|
|
|
</script>
|
|
|
|
<style lang="stylus" scoped>
|
|
.md-tab-picker
|
|
color color-gray-1
|
|
-webkit-font-smoothing antialiased
|
|
font-size tab-picker-font-size
|
|
.md-tab-picker-content
|
|
background color-bg-base
|
|
</style>
|
|
|
|
<style lang="stylus">
|
|
.md-tab-picker
|
|
.md-tab-picker-content
|
|
position relative
|
|
.md-tabs
|
|
.md-tab-content-wrapper
|
|
height 400px
|
|
.md-tab-content
|
|
height 400px
|
|
overflow scroll
|
|
.slot-wrapper
|
|
background rgba(255, 255, 255, 0.7)
|
|
position absolute
|
|
top 80px
|
|
left 0
|
|
width 100%
|
|
bottom 0
|
|
.slot-inner
|
|
position absolute
|
|
top 50%
|
|
left 50%
|
|
transform translate(-50%, -50%)
|
|
</style>
|