123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- <template>
- <u-popup
- :show="show"
- mode="bottom"
- :title="title"
- :titleStyle="titleStyle"
- :closeOnClickOverlay="closeOnClickOverlay"
- :zIndex="zIndex"
- :closeable="closeable"
- :safeAreaInsetBottom="safeAreaInsetBottom"
- :bgColor="bgColor"
- :round="round"
- @close="onClose"
- >
- <view class="u-cascader">
- <!-- 标签页 -->
- <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 {String} titleAlign 标题对齐方式(默认 'center' )
- * @property {String} titleColor 标题颜色(默认 '' )
- * @property {String | Number} titleFontSize 标题字体大小(默认 '' )
- * @property {Boolean} titleBold 标题是否加粗(默认 false )
- * @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);
- &__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>
|