1
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
export default {
|
||||
props: {
|
||||
// 是否显示级联选择器
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cascader.show
|
||||
},
|
||||
// 选择器标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.$t('uCascader.title')
|
||||
},
|
||||
// 选项数据
|
||||
options: {
|
||||
type: Array,
|
||||
default: ()=> uni.$u.props.cascader.options
|
||||
},
|
||||
// #ifdef VUE3
|
||||
// 当前选中值
|
||||
modelValue: {
|
||||
type: [String, Number, Array],
|
||||
default: ()=> uni.$u.props.cascader.modelValue
|
||||
},
|
||||
// #endif
|
||||
// v-model 的别名
|
||||
value: {
|
||||
type: [String, Number, Array],
|
||||
default: ()=> uni.$u.props.cascader.value
|
||||
},
|
||||
// 占位符文本
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.$t('uCascader.placeholder')
|
||||
},
|
||||
// 自定义字段名
|
||||
field: {
|
||||
type: Object,
|
||||
default: ()=> uni.$u.props.cascader.field
|
||||
},
|
||||
// 是否显示关闭按钮
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cascader.closeable
|
||||
},
|
||||
// 是否点击遮罩关闭
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cascader.closeOnClickOverlay
|
||||
},
|
||||
// 背景色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.bgColor
|
||||
},
|
||||
// 主题色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.activeColor
|
||||
},
|
||||
// 选中背景色
|
||||
activeBgColor: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.activeBgColor
|
||||
},
|
||||
// 选中文本加粗
|
||||
activeBold: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cascader.activeBold
|
||||
},
|
||||
// 图标颜色
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.iconColor
|
||||
},
|
||||
// 文本色
|
||||
color: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.color
|
||||
},
|
||||
// 字体大小
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.fontSize
|
||||
},
|
||||
// 标题字体大小
|
||||
titleFontSize: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.titleFontSize
|
||||
},
|
||||
// 标题颜色
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.titleColor
|
||||
},
|
||||
// 圆角
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cascader.round
|
||||
},
|
||||
// 层级
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cascader.zIndex
|
||||
},
|
||||
// 是否安全区域
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cascader.safeAreaInsetBottom
|
||||
},
|
||||
// 选项高度
|
||||
itemHeight: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cascader.itemHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<u-popup
|
||||
:show="show"
|
||||
mode="bottom"
|
||||
:closeOnClickOverlay="closeOnClickOverlay"
|
||||
:zIndex="zIndex"
|
||||
:closeable="closeable"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom"
|
||||
:bgColor="bgColor"
|
||||
:round="round"
|
||||
@close="onClose"
|
||||
>
|
||||
<view class="u-cascader">
|
||||
<!-- 头部 -->
|
||||
<view class="u-cascader__header">
|
||||
<text class="u-cascader__title" :style="[{
|
||||
color: titleColor,
|
||||
fontSize: titleFontSize
|
||||
}]">{{ title }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<view class="u-cascader__tabs">
|
||||
<u-tabs
|
||||
:list="tabsList"
|
||||
:current="activeTabIndex"
|
||||
:scrollable="true"
|
||||
:activeStyle="{ color: activeColor, fontWeight: '600' }"
|
||||
:inactiveStyle="{ color: color }"
|
||||
:lineColor="activeColor"
|
||||
:keyName="'name'"
|
||||
@change="switchTab"
|
||||
></u-tabs>
|
||||
</view>
|
||||
|
||||
<!-- 选项列表 -->
|
||||
<scroll-view class="u-cascader__content" scroll-y>
|
||||
<view class="u-cascader__list">
|
||||
<view
|
||||
v-for="(item, index) in currentOptions"
|
||||
:key="index"
|
||||
class="u-cascader__item"
|
||||
:class="{ 'u-cascader__item--selected': isSelected(item) }"
|
||||
:style="[{
|
||||
backgroundColor: isSelected(item) ? activeBgColor : '',
|
||||
height: $u.addUnit(itemHeight),
|
||||
}]"
|
||||
@tap="selectItem(item)"
|
||||
>
|
||||
<text class="u-cascader__item-text" :style="[{
|
||||
fontWeight: isSelected(item) && activeBold ? '800' : '400',
|
||||
fontSize: $u.addUnit(fontSize)
|
||||
}]">
|
||||
{{ getLabel(item) }}
|
||||
</text>
|
||||
<view v-if="isSelected(item)" class="u-cascader__item-right">
|
||||
<u-icon name="checkmark" size="18" :color="iconColor || activeColor"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from './props.js';
|
||||
import mixin from '../../libs/mixin/mixin';
|
||||
import mpMixin from '../../libs/mixin/mpMixin';
|
||||
|
||||
/**
|
||||
* Cascader 级联选择器
|
||||
* @description 级联选择器,用于多级数据的选择,支持单选和多选模式
|
||||
* @tutorial https://uview.d3u.cn/components/cascader.html
|
||||
* @property {Boolean} show 是否显示级联选择器(默认 false )
|
||||
* @property {String} title 选择器标题(默认 '请选择' )
|
||||
* @property {Array} options 选项数据(默认 [] )
|
||||
* @property {String|Number|Array} modelValue 当前选中值
|
||||
* @property {String} placeholder 占位符文本(默认 '请选择' )
|
||||
* @property {Object} field 自定义字段名(默认 {text: 'text', value: 'value', children: 'children'} )
|
||||
* @property {Boolean} closeable 是否显示关闭按钮(默认 true )
|
||||
* @property {Boolean} closeOnClickOverlay 是否点击遮罩关闭(默认 true )
|
||||
* @property {String} bgColor 背景色(默认 '#ffffff' )
|
||||
* @property {String} activeColor 主题色(默认 '#3c9cff' )
|
||||
* @property {String} color 文本色(默认 '#303133' )
|
||||
* @property {String} fontSize 字体大小(默认 '16px' )
|
||||
* @property {String} titleFontSize 标题字体大小(默认 '18px' )
|
||||
* @property {String} borderRadius 圆角(默认 '12px' )
|
||||
* @property {String|Number} zIndex 层级(默认 10075 )
|
||||
* @property {Boolean} safeAreaInsetBottom 是否安全区域(默认 true )
|
||||
* @property {String} itemHeight 选项高度(默认 '50px' )
|
||||
*
|
||||
* @event {Function} change 选择改变时触发
|
||||
* @event {Function} close 关闭时触发
|
||||
* @event {Function} confirm 确认选择时触发
|
||||
* @event {Function} selected 选择时触发
|
||||
* @example <u-cascader :show="show" :options="options" @change="onChange"></u-cascader>
|
||||
*/
|
||||
export default {
|
||||
name: "u-cascader",
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
selectedPath: [], // 选择路径
|
||||
tabs: [], // 标签页数据
|
||||
activeTabIndex: 0, // 当前激活的标签页索引
|
||||
currentOptions: [], // 当前层级的选项
|
||||
optionsStack: [] // 选项栈,存储每一级的选项
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 获取字段名配置
|
||||
fieldConfig() {
|
||||
return {
|
||||
label: this.field.text || this.field.label || 'label',
|
||||
value: this.field.value || 'value',
|
||||
children: this.field.children || 'children'
|
||||
}
|
||||
},
|
||||
|
||||
// 当前选中的值
|
||||
currentValue() {
|
||||
// #ifdef VUE2
|
||||
return this.value
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
return this.modelValue || this.value
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 是否可以添加新标签页
|
||||
canAddTab() {
|
||||
if (this.selectedPath.length === 0) return false
|
||||
const lastSelected = this.selectedPath[this.selectedPath.length - 1]
|
||||
return this.hasChildren(lastSelected)
|
||||
},
|
||||
|
||||
// 标签页列表数据
|
||||
tabsList() {
|
||||
const list = this.tabs.map(tab => ({
|
||||
name: tab.label,
|
||||
label: tab.label
|
||||
}))
|
||||
|
||||
// 首次打开或者有子项时显示占位符
|
||||
// 首次打开:tabs为空时
|
||||
// 有子项:选择了某项且该项有子项时
|
||||
if (this.tabs.length === 0 || this.canAddTab) {
|
||||
list.push({
|
||||
name: this.placeholder,
|
||||
label: this.placeholder
|
||||
})
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
|
||||
options: {
|
||||
handler() {
|
||||
if (this.show) {
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
currentValue: {
|
||||
handler() {
|
||||
if (this.show) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// #ifdef VUE3
|
||||
emits: ['update:modelValue', 'change', 'close', 'confirm', 'selected'],
|
||||
// #endif
|
||||
|
||||
methods: {
|
||||
// 初始化
|
||||
init() {
|
||||
this.selectedPath = []
|
||||
this.tabs = []
|
||||
this.activeTabIndex = 0
|
||||
this.optionsStack = []
|
||||
this.currentOptions = this.options || []
|
||||
this.optionsStack.push(this.currentOptions)
|
||||
|
||||
// 如果有初始值,则定位到对应的层级
|
||||
if (this.currentValue) {
|
||||
this.initWithValue()
|
||||
}
|
||||
},
|
||||
|
||||
// 根据初始值初始化选择状态
|
||||
initWithValue() {
|
||||
const value = Array.isArray(this.currentValue) ? this.currentValue : [this.currentValue]
|
||||
let currentOptions = this.options || []
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const targetValue = value[i]
|
||||
const item = currentOptions.find(option => this.getValue(option) === targetValue)
|
||||
|
||||
if (item) {
|
||||
this.selectedPath.push(item)
|
||||
this.tabs.push({
|
||||
label: this.getLabel(item),
|
||||
level: i
|
||||
})
|
||||
|
||||
if (this.hasChildren(item)) {
|
||||
currentOptions = this.getChildren(item)
|
||||
this.optionsStack.push(currentOptions)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.activeTabIndex = this.tabs.length > 0 ? this.tabs.length - 1 : 0
|
||||
this.currentOptions = this.optionsStack[this.activeTabIndex] || []
|
||||
},
|
||||
|
||||
// 选择项目
|
||||
selectItem(item) {
|
||||
const currentLevel = this.activeTabIndex
|
||||
|
||||
// 更新选择路径
|
||||
this.selectedPath = this.selectedPath.slice(0, currentLevel)
|
||||
this.selectedPath.push(item)
|
||||
|
||||
// 更新标签页
|
||||
this.tabs = this.tabs.slice(0, currentLevel)
|
||||
this.tabs.push({
|
||||
label: this.getLabel(item),
|
||||
level: currentLevel
|
||||
})
|
||||
|
||||
// 清理选项栈后续数据
|
||||
this.optionsStack = this.optionsStack.slice(0, currentLevel + 1)
|
||||
|
||||
// 触发选择事件
|
||||
this.emitSelected(item)
|
||||
|
||||
if (this.hasChildren(item)) {
|
||||
// 有子项,添加新的选项到栈中
|
||||
const childOptions = this.getChildren(item)
|
||||
this.optionsStack.push(childOptions)
|
||||
|
||||
// 自动切换到下一个标签页
|
||||
this.activeTabIndex = currentLevel + 1
|
||||
this.currentOptions = childOptions
|
||||
} else {
|
||||
// 没有子项,完成选择,直接触发confirm并关闭
|
||||
this.emitChange()
|
||||
this.emitConfirm()
|
||||
this.onClose()
|
||||
}
|
||||
},
|
||||
|
||||
// 切换标签页
|
||||
switchTab(event) {
|
||||
// u-tabs组件的change事件传递的是对象 {item, index}
|
||||
const index = event.index
|
||||
this.activeTabIndex = index
|
||||
this.currentOptions = this.optionsStack[index] || []
|
||||
},
|
||||
|
||||
// 获取选项的显示文本
|
||||
getLabel(item) {
|
||||
return item[this.fieldConfig.label] || ''
|
||||
},
|
||||
|
||||
// 获取选项的值
|
||||
getValue(item) {
|
||||
return item[this.fieldConfig.value]
|
||||
},
|
||||
|
||||
// 获取选项的子项
|
||||
getChildren(item) {
|
||||
return item[this.fieldConfig.children] || []
|
||||
},
|
||||
|
||||
// 判断是否有子项
|
||||
hasChildren(item) {
|
||||
const children = this.getChildren(item)
|
||||
return Array.isArray(children) && children.length > 0
|
||||
},
|
||||
|
||||
// 判断选项是否被选中
|
||||
isSelected(item) {
|
||||
if (this.selectedPath.length <= this.activeTabIndex) {
|
||||
return false
|
||||
}
|
||||
const selectedItem = this.selectedPath[this.activeTabIndex]
|
||||
return selectedItem && this.getValue(selectedItem) === this.getValue(item)
|
||||
},
|
||||
|
||||
// 获取选中的值数组
|
||||
getSelectedValues() {
|
||||
return this.selectedPath.map(item => this.getValue(item))
|
||||
},
|
||||
|
||||
// 获取选中的标签数组
|
||||
getSelectedLabels() {
|
||||
return this.selectedPath.map(item => this.getLabel(item))
|
||||
},
|
||||
|
||||
// 触发change事件
|
||||
emitChange() {
|
||||
const values = this.getSelectedValues()
|
||||
const labels = this.getSelectedLabels()
|
||||
const selectedItems = [...this.selectedPath]
|
||||
|
||||
const result = {
|
||||
value: values,
|
||||
label: labels,
|
||||
selectedItems
|
||||
}
|
||||
|
||||
// #ifdef VUE2
|
||||
this.$emit('input', values)
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:modelValue', values)
|
||||
// #endif
|
||||
|
||||
this.$emit('change', result)
|
||||
},
|
||||
|
||||
// 触发selected事件
|
||||
emitSelected(item) {
|
||||
this.$emit('selected', {
|
||||
item,
|
||||
level: this.activeTabIndex,
|
||||
selectedPath: [...this.selectedPath]
|
||||
})
|
||||
},
|
||||
|
||||
// 触发confirm事件
|
||||
emitConfirm() {
|
||||
const values = this.getSelectedValues()
|
||||
const labels = this.getSelectedLabels()
|
||||
const selectedItems = [...this.selectedPath]
|
||||
|
||||
this.$emit('confirm', {
|
||||
value: values,
|
||||
label: labels,
|
||||
selectedItems
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭选择器
|
||||
onClose() {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
|
||||
.u-cascader {
|
||||
@include flex(column);
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
// #ifndef APP-NVUE
|
||||
min-height: 0;
|
||||
// #endif
|
||||
}
|
||||
|
||||
&__list {
|
||||
padding: 0;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
&__item-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 15px;
|
||||
// #ifndef APP-NVUE
|
||||
white-space: nowrap;
|
||||
// #endif
|
||||
}
|
||||
|
||||
&__item-right {
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user