u-cascader.vue 8.76 KB
<template>
	<up-popup 
		:show="popupShow" 
		mode="bottom" 
		:popup="false"
		:mask="true" 
		:closeable="closeable" 
		:safe-area-inset-bottom="true"
		:close-icon-color="closeIconColor" 
		:z-index="uZIndex"
		:maskCloseAble="maskCloseAble" 
		@close="close"
	>
		<view class="up-p-t-30 up-p-l-20 up-m-b-10" v-if="headerDirection ==='column'">
			<up-steps v-if="popupShow" dot direction="column" v-model:current="tabsIndex">
                <view  v-for="(item, index) in genTabsList"  @click="toFatherIndex(index)">
                    <up-steps-item :title="item.name"/>
                </view>
			</up-steps>
		</view>
		<view class="up-p-t-20 up-m-b-10" v-else>
			<up-tabs 
				v-if="popupShow" 
				:list="genTabsList"
				:scrollable="true" 
				v-model:current="tabsIndex" 
				@change="tabsChange" 
				ref="tabs"
			></up-tabs>
		</view>
		<view class="area-box">
			<view class="u-flex" :class="{ 'change':isChange }"
				:style="{transform: optionsCols == 2 && isChange ? 'translateX(-33.3333333%)' : ''}">
				<template v-for="(levelData, levelIndex) in levelList" :key="levelIndex">
					<view 
						v-if="optionsCols == 2 || levelIndex == tabsIndex" 
						class="area-item"
						:style="{ width: optionsCols == 2 ? '33.33333%' : '750rpx'}"
					>
						<view class="u-padding-10" :style="[levelPaneStyle, { height: '100%' }]">
							<scroll-view :scroll-y="true" style="height: 100%">
								<up-cell-group v-if="levelIndex === 0 || selectedValueIndexs[levelIndex - 1] !== undefined">
									<up-cell 
										v-for="(item,index) in levelData"
										:title="item[labelKey]" 
										:arrow="false"
										:index="index" 
										:key="index"
										@click="levelChange(levelIndex, index)"
									>
										<template v-slot:right-icon>
											<up-icon 
												v-if="selectedValueIndexs[levelIndex] === index"
												size="17" 
												name="checkbox-mark"
											></up-icon>
										</template>
									</up-cell>
								</up-cell-group>
							</scroll-view>
						</view>
					</view>
				</template>
			</view>
		</view>
		<!-- 按钮区域 -->
		<view class="u-cascader-action up-flex up-flex-between">
			<view class="u-padding-20 up-flex-fill">
				<up-button @click="handleCancel" type="default">{{ t("up.common.cancel") }}</up-button>
			</view>
			<view class="u-padding-20 up-flex-fill">
				<up-button @click="handleConfirm" type="primary">{{ t("up.common.confirm") }}</up-button>
			</view>
		</view>
	</up-popup>
</template>

<script>
import { t } from '../../libs/i18n'
export default {
	name: 'up-cascader',
	props: {
		show: {
			type: Boolean,
			default: false
		},
		data: {
			type: Array,
			default() {
				return [];
			}
		},
		modelValue: {
			type: Array,
			default() {
				return [];
			}
		},
		valueKey: {
			type: String,
			default: 'value'
		},
		labelKey: {
			type: String,
			default: 'label'
		},
		childrenKey: {
			type: String,
			default: 'children'
		},
		maskCloseAble: {
			type: Boolean,
			default: true
		},
		zIndex: {
			type: [String, Number],
			default: 0
		},
		autoClose: {
			type: Boolean,
			default: false
		},
		headerDirection: {
			type: String,
			default: 'row'
		},
		optionsCols: {
			type: [Number],
			default: 2
		},
		closeable: {
			type: Boolean,
			default: true
		}
	},
	data() {
		return {
			levelList: [],
			selectedValueIndexs: [],
			tabsIndex: 0,
			popupShow: false,
			confirmValues: []
		}
	},
	watch: {
		data: {
			handler() {
				this.initLevelList();
				this.setDefaultValue();
			},
			immediate: true
		},
		show: {
			handler(val) {
				this.popupShow = val;
				if (val) {
					this.tabsIndex = 0;
					this.initLevelList();
					this.setDefaultValue();
				}
			},
			immediate: true
		},
		modelValue: {
			handler() {
				this.setDefaultValue();
			},
			immediate: true
		}
	},
	computed: {
		isChange() {
			return this.tabsIndex > 1;
		},
		genTabsList() {
			let tabsList = [{ name: "请选择" }];
			for (let i = 0; i < this.selectedValueIndexs.length; i++) {
				if (this.selectedValueIndexs[i] !== undefined && this.levelList[i]) {
					const selectedItem = this.levelList[i][this.selectedValueIndexs[i]];
					if (selectedItem) {
						tabsList[i] = { name: selectedItem[this.labelKey] };
						if (i === this.selectedValueIndexs.length - 1 &&
							selectedItem[this.childrenKey] &&
							selectedItem[this.childrenKey].length > 0) {
							tabsList.push({ name: "请选择" });
						}
					}
				}
			}
			return tabsList;
		},
		uZIndex() {
			return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
		},
		closeIconColor() {
			return 'var(--up-main-color, #303133)';
		},
		levelPaneStyle() {
			return {
				backgroundColor: 'var(--up-bg-color, #f7f7f7)'
			};
		}
	},
	emits: ['update:modelValue', 'update:show', 'change', 'confirm', 'cancel'],
	methods: {
		t,
		// 获取选中值数组
		getSelectedValues() {
			const result = [];
			for (let i = 0; i < this.selectedValueIndexs.length; i++) {
				const selectedIndex = this.selectedValueIndexs[i];
				if (selectedIndex === undefined) continue;
				const list = this.levelList[i];
				if (!list || !list[selectedIndex]) continue;
				result.push(list[selectedIndex][this.valueKey]);
			}
			return result;
		},
		// 判断当前选中项是否为最后一级(叶子节点)
		isCurrentLeafNode() {
			const len = this.selectedValueIndexs.length;
			if (len === 0) return false;
			const lastLevel = len - 1;
			const lastIndex = this.selectedValueIndexs[lastLevel];
			const lastItem = this.levelList[lastLevel]?.[lastIndex];
			if (!lastItem) return false;
			// 无子级 = 最后一级
			return !lastItem[this.childrenKey] || lastItem[this.childrenKey].length === 0;
		},
		initLevelList() {
			if (this.data && this.data.length > 0) {
				this.levelList = [this.data];
				this.selectedValueIndexs = [];
			} else {
				this.levelList = [];
				this.selectedValueIndexs = [];
			}
		},
		setDefaultValue() {
			if (!this.data || this.data.length === 0) {
				this.confirmValues = [];
				return;
			}
			if (!this.modelValue || this.modelValue.length === 0) {
				this.confirmValues = [];
				return;
			}
			this.selectedValueIndexs = [];
			this.levelList = [];
			let currentLevelData = this.data;

			for (let i = 0; i < this.modelValue.length; i++) {
				const value = this.modelValue[i];
				const index = currentLevelData.findIndex(item => item[this.valueKey] === value);
				this.levelList[i] = currentLevelData;

				if (index !== -1) {
					this.selectedValueIndexs.push(index);
					if (currentLevelData[index][this.childrenKey]) {
						currentLevelData = currentLevelData[index][this.childrenKey];
					} else {
						break;
					}
				} else {
					break;
				}
			}
			this.confirmValues = this.getSelectedValues();
		},

		close() {
			this.popupShow = false;
			this.levelList = [];
			this.selectedValueIndexs = [];
			this.tabsIndex = 0;
			this.confirmValues = [];
			this.$emit('update:show', false);
			this.$emit('cancel');
		},

		tabsChange(item) {},
		toFatherIndex(index) {
			this.tabsIndex = index;
		},
		levelChange(levelIndex, index) {
			this.$set(this.selectedValueIndexs, levelIndex, index);
			this.selectedValueIndexs.splice(levelIndex + 1);
			this.tabsIndex = Math.min(this.tabsIndex, levelIndex);
			this.levelList.splice(levelIndex + 1);

			const currentItem = this.levelList[levelIndex][index];
			if (currentItem && currentItem[this.childrenKey] && currentItem[this.childrenKey].length > 0) {
				if (this.levelList.length <= levelIndex + 1) {
					this.levelList.push(currentItem[this.childrenKey]);
				} else {
					this.$set(this.levelList, levelIndex + 1, currentItem[this.childrenKey]);
				}
				this.tabsIndex = levelIndex + 1;
				// 有子级,不自动确认
				if (this.autoClose) return;
			} else {
				// 已选到最后一级,自动确认
				if (this.autoClose) {
					this.handleConfirm();
				}
			}
		},
		emitChange(closePopup = true) {
			const result = this.getSelectedValues();
			this.confirmValues = [...result];
			this.$emit('change', this.confirmValues);
			if (closePopup) {
				this.close();
			}
		},
		handleCancel() {
			this.close();
		},
		handleConfirm() {
			// 校验:必须选择到最后一级
			if (!this.isCurrentLeafNode()) {
				uni.showToast({ title: '请选择到最后一级', icon: 'none' });
				return;
			}
			const values = this.getSelectedValues();
			this.confirmValues = [...values];
			// 向外输出 选中值数组
			this.$emit('update:modelValue', values);
			this.$emit('confirm', values);
			this.close();
		}
	}
}
</script>

<style lang="scss">
	.area-box {
		width: 100%;
		overflow: hidden;
		height: 800rpx;

		>view {
			width: 150%;
			transition: transform 0.3s ease-in-out 0s;
			transform: translateX(0);

		&.change {
				transform: translateX(-33.3333333%);
			}
		}

		.area-item {
			height: 800rpx;
		}
	}

	.u-cascader-action {
		border-top: 1px solid var(--up-border-color, #eee);
	}
</style>