Files
qiming/qiming-mobile/uni_modules/lime-cascader/components/l-cascader/l-cascader.uvue

1097 lines
38 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<l-popup :visible="show" position="bottom" @click-overlay="onClose" @opened="opened">
<view ref="cascaderRef" class="l-cascader" :style="[styles]">
<text class="l-cascader__title">{{title}}</text>
<view class="l-cascader__close-btn" @click="onCloseBtn" v-if="closeable">
<text class="l-cascader__close-icon">&#xE1EB</text>
<!-- <l-icon class="l-cascader__close-icon" name="close" size="24px"></l-icon> -->
</view>
<view class="l-cascader__content">
<l-tabs
<!-- #ifdef APP-HARMONY -->
v-if="steps.length == 1 || hosUpdate > 0"
<!-- #endif -->
:list="steps"
:value="stepIndex"
:space-evenly="false" @change="onTabChange" size="large"
:color="color"
:activeColor="activeColor"
:lineColor="activeColor"
:visible="show"
:bgColor="bgColor">
<!-- <l-tab-panel v-for="(step, index) in steps" :label="step['label']" :key="index"></l-tab-panel> -->
</l-tabs>
<template v-if="subTitles.length > 0 ">
<text v-show="subTitles.length > stepIndex" class="l-cascader__options-title">
{{subTitles.length > stepIndex ? subTitles[stepIndex]: ''}}
</text>
</template>
<view ref="containerRef" class="l-cascader__options-container" :style="[containerStyles]">
<list-view
class="l-cascader__options"
v-for="(_options, index) in items"
:key="index"
:scroll-y="true">
<!-- 多选模式使用 checkbox -->
<template v-if="props.multiple">
<list-item class="l-cascader__cell" v-for="(item, _index) in _options" :key="_index" @click="handleCellClick(item[fieldKeys.value], index, item)">
<l-checkbox
:value="item[fieldKeys.value]"
:checked="isItemChecked(item[fieldKeys.value], index)"
:disabled="item['disabled'] == true"
:size="'medium'"
:icon="'rectangle'"
:checkedColor="activeColor"
@change="(checked, event) => handleCheckboxChange(item[fieldKeys.value], index, checked, event)"
class="l-cascader__checkbox">
<text
class="l-cascader__cell-title"
:class="{'l-cascader__cell--disabled': item['disabled'] == true}"
:style="[color != null ?'color:' + color: '']">{{item[fieldKeys.label]}}</text>
</l-checkbox>
</list-item>
</template>
<!-- 单选模式保持原有逻辑 -->
<template v-else>
<list-item class="l-cascader__cell" v-for="(item, _index) in _options" :key="_index" @click="handleCellClick(item[fieldKeys.value], index, item)">
<text
class="l-cascader__cell-title"
:class="{'l-cascader__cell--disabled': item['disabled'] == true}"
:style="[color != null ?'color:' + color: '']"
>{{item[fieldKeys.label]}}</text>
<!-- #ifndef APP || WEB -->
<view class="l-cascader__cell-icon">
<!-- #endif -->
<l-icon
class="l-cascader__cell-icon"
:size="iconSize"
:color="activeColor"
v-if="selectedValue.length > index && selectedValue[index] == item[fieldKeys.value]"
name="check">
</l-icon>
<!-- #ifndef APP || WEB -->
</view>
<!-- #endif -->
</list-item>
</template>
</list-view>
</view>
</view>
<view class="l-cascader__footer" v-if="innerConfirmBtn != null">
<l-button
@click.stop="handleConfirm"
:color="innerConfirmBtn!['color']"
:shape="innerConfirmBtn!['shape'] ?? 'round'"
:type="innerConfirmBtn!['type'] ?? 'primary'">
{{
innerConfirmBtn!['content']
}}
</l-button>
</view>
</view>
</l-popup>
</template>
<script lang="uts" setup>
/**
* Cascader 级联选择器组件
* @description 支持多级联动的选择器,适用于地区选择、分类选择等场景。支持单选和多选模式
* <br> 插件类型LCascaderComponentPublicInstance
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-cascader
*
* @property {boolean} visible 控制组件显示/隐藏(必填)
* @property {boolean} checkStrictly 父子节点选择是否关联默认false联动选择
* @property {boolean} multiple 是否支持多选默认false单选模式
* @property {string[]} multipleValue 多选模式下的选中值数组支持v-model
* @property {string[]} defaultMultipleValue 多选模式下的默认选中值数组
* @property {number} maxTagCount 多选模式下最大选择数量,-1表示无限制
* @property {boolean} showCheckedCount 多选模式下是否显示选中数量
* @property {UTSJSONObject[]} options 数据源必填符合CascaderOption结构
* @property {string} title 主标题文本
* @property {UTSJSONObject} keys 字段别名配置(例:{label:'name',value:'id'}
* @property {string} value 当前选中值单选模式支持v-model
* @property {string} defaultValue 默认选中值(单选模式)
* @property {string} placeholder 未选择时的提示文字
* @property {string[]} subTitles 各级副标题(如:['省份','城市','区县']
* @property {boolean} closeable 是否显示关闭按钮
* @property {boolean} uniCloud 是否使用uniCloud数据源
* @property {boolean} swipeable 是否支持手势滑动切换层级
* @property {string} iconSize 图标尺寸支持CSS单位
* @property {string} activeColor 选中状态主题色
* @property {string} fontSize 文字字号支持CSS单位
* @property {string} color 文字颜色
* @property {string} bgColor 背景颜色
* @event {Function} change 选项变化时触发单选返回string多选返回string[]
* @event {Function} pick 选中时触发
* @event {Function} close 关闭时触发
* @event {Function} finish 点击完成时触发
*/
import { CascaderProps, KeysType, ChildrenInfoType } from './type';
import { getIndexesByValue, parseOptions, parseKeys, splitEveryTwo, pushAt, getUniCloudArea, pickUniCloudArea, getIndexByValue, updateChildren } from './utils';
import lCheckbox from '@/uni_modules/lime-checkbox/components/l-checkbox/l-checkbox.uvue';
import lButton from '@/uni_modules/lime-button/components/l-button/l-button.uvue';
const emit = defineEmits<{
pick: [level: number, index: number, value: string, selectedIndexes: number[]];
change: [value: string | string[][], selectedOptions: UTSJSONObject[]];
close: [];
finish: [selectedPaths: string[][], selectedOptions: UTSJSONObject[]];
}>()
const props = withDefaults(defineProps<CascaderProps>(), {
visible: false,
closeable: true,
uniCloud: false,
checkStrictly: false,
multiple: false,
maxTagCount: -1,
showCheckedCount: true,
subTitles: [] as string[],
options: [] as UTSJSONObject[],
placeholder: '选择选项',
// #ifdef APP
iconSize: '24px',
activeColor: '#3283ff'
// #endif
})
const hosUpdate = ref(0)
const childrenInfo: ChildrenInfoType = {
value: '',
level: 0,
};
const fieldKeys = computed(():KeysType => parseKeys(props.keys, props.uniCloud))
const modelValue = defineModel({type: String, default: ''})
const modelMultipleValue = defineModel('multipleValue', {type: Array, default: []})
const show = defineModel('visible', {type: Boolean, default: false})
// 单选模式的值
const cascaderValue = computed({
set(value: string){
if(modelValue.value == value) return
modelValue.value = value
},
get():string {
return props.value ?? modelValue.value
}
} as WritableComputedOptions<string>)
// 多选模式的值
const cascaderMultipleValue = computed({
set(value: string[][]){
if(JSON.stringify(modelMultipleValue.value) == JSON.stringify(value)) return
modelMultipleValue.value = value
},
get():string[][] {
return props.multipleValue ?? modelMultipleValue.value
}
} as WritableComputedOptions<string[][]>)
// 多选模式下的选中项集合,存储完整的路径数组
const selectedItems = reactive<Set<string>>(new Set())
// 多选模式下的完整路径集合
const selectedPaths = reactive<string[][]>([])
// 多选模式下的源数据集合,存储每个路径对应的完整源数据
const selectedSourceData = reactive<UTSJSONObject[]>([])
/**
* 添加路径到选中项
*/
const addPath = (path: string[], sourceData?: UTSJSONObject) => {
const pathKey = path.join(',');
selectedItems.add(pathKey);
selectedPaths.push([...path]);
if (sourceData) {
selectedSourceData.push(sourceData);
}
}
/**
* 从选中项中移除路径
*/
const removePath = (path: string[]) => {
const pathKey = path.join(',');
selectedItems.delete(pathKey);
const index = selectedPaths.findIndex(p => p.join(',') === pathKey);
if (index !== -1) {
selectedPaths.splice(index, 1);
// 同时移除对应的源数据
selectedSourceData.splice(index, 1);
}
}
/**
* 检查路径是否被选中
*/
const isPathSelected = (path: string[]): boolean => {
const pathKey = path.join(',');
return selectedItems.has(pathKey);
}
/**
* 构建到指定项的完整路径
*/
const buildPathToItem = (value: string, level: number): string[] => {
const path: string[] = [];
// 从第一级开始构建路径
for (let i = 0; i <= level; i++) {
if (i === level) {
// 当前级别,使用传入的值
path.push(value);
} else {
// 其他级别,需要从原始数据中查找路径
// 这里需要根据实际的数据结构来构建
// 暂时使用空字符串,后续需要完善
path.push('');
}
}
return path;
}
/**
* 从原始数据中构建完整路径
*/
const buildFullPathFromOriginalData = (value: string, level: number): string[] => {
// 从原始数据中查找完整路径
const findPathInData = (data: UTSJSONObject[], targetValue: string, currentPath: string[] = []): string[] | null => {
for (const item of data) {
const itemValue = item[fieldKeys.value.value] as string;
const newPath = [...currentPath, itemValue];
if (itemValue === targetValue) {
return newPath;
}
const children = item[fieldKeys.value.children] as UTSJSONObject[] | null;
if (children && children.length > 0) {
const result = findPathInData(children, targetValue, newPath);
if (result) {
return result;
}
}
}
return null;
};
// 从原始数据中查找路径
const fullPath = findPathInData(realColumns.value, value);
return fullPath || [value];
}
/**
* 根据路径构建完整的源数据对象
*/
const buildSourceDataFromPath = (path: string[]): UTSJSONObject => {
const sourceData: UTSJSONObject = {};
// 递归查找每个层级的源数据
const findSourceData = (data: UTSJSONObject[], targetPath: string[], currentLevel: number = 0): UTSJSONObject | null => {
if (currentLevel >= targetPath.length) {
return null;
}
const targetValue = targetPath[currentLevel];
for (const item of data) {
const itemValue = item[fieldKeys.value.value] as string;
if (itemValue === targetValue) {
if (currentLevel === targetPath.length - 1) {
// 最后一级,返回完整的源数据
return item;
} else {
// 继续查找下一级
const children = item[fieldKeys.value.children] as UTSJSONObject[] | null;
if (children && children.length > 0) {
return findSourceData(children, targetPath, currentLevel + 1);
}
}
}
}
return null;
};
// 从原始数据中查找源数据
const result = findSourceData(realColumns.value, path);
return result || {};
}
const cascaderRef = ref<UniElement|null>(null)
const cache = new Map<string, UTSJSONObject[]>();
const stepIndex = ref(0)
const selectedIndexes = reactive<number[]>([]);
const selectedValue = reactive<string[]>([]);
// const scrollTopList = reactive<number[]>([]);
const uniCloudColumns = ref<UTSJSONObject[]>([]);
const realColumns = computed(():UTSJSONObject[] =>{
if(props.uniCloud) {
return uniCloudColumns.value
}
return props.options
})
const items = reactive<UTSJSONObject[][]>([realColumns.value]);
const steps = reactive<UTSJSONObject[]>([{ label: props.placeholder }])
const innerConfirmBtn = computed(():UTSJSONObject|null=> {
if(props.confirmBtn == null) return null
if(typeof props.confirmBtn == 'string') {
return {
content: props.confirmBtn as string
}
}
return props.confirmBtn as UTSJSONObject
})
const styles = computed(():Map<string, any>=>{
const style = new Map<string, any>()
if(props.bgColor != null) {
style.set('background', props.bgColor!)
}
return style
})
const windowWidth = ref(uni.getWindowInfo().windowWidth)
const containerStyles = computed(():Map<string, any>=>{
const style = new Map<string, any>()
// #ifndef APP
style.set('width', (items.length + 1) + '00%')
style.set('transform', `translateX(${-stepIndex.value}00vw)`)
// #endif
return style
})
const onTabChange = (index : number) => {
stepIndex.value = index
// 多选模式下,当切换到不同级别时,需要重新构建数据结构
if (props.multiple) {
// 当切换到第一级时,不清空后续级别,保持现有的 tabs 结构
if (index === 0) {
// 只切换到第一级,不修改数据结构
// items 和 steps 保持不变
} else {
// 重新构建从当前级别开始的所有后续级别
rebuildItemsFromLevel(index);
}
}
}
/**
* 从指定级别重新构建数据结构
* 多选模式下,只显示当前选中项的子项,不合并其他项的子项
*/
const rebuildItemsFromLevel = (startLevel: number) => {
// 清空从指定级别开始的所有后续级别
items.length = startLevel + 1;
steps.length = startLevel + 1;
// 获取当前级别所有选中的有子级的选项
const currentLevelSelectedItems = Array.from(selectedItems).filter(selectedValue => {
const itemIndex = items[startLevel].findIndex((item: UTSJSONObject) => item[fieldKeys.value.value] === selectedValue);
if (itemIndex === -1) return false;
const item = items[startLevel][itemIndex];
const itemChildren = item[fieldKeys.value.children] as UTSJSONObject[]|null;
return itemChildren != null && itemChildren.length > 0;
});
console.log(`[rebuildItemsFromLevel] Level ${startLevel}, Selected items with children:`, currentLevelSelectedItems);
// 合并所有选中项的子级选项
const allChildren: UTSJSONObject[] = [];
currentLevelSelectedItems.forEach(selectedValue => {
const itemIndex = items[startLevel].findIndex((item: UTSJSONObject) => item[fieldKeys.value.value] === selectedValue);
if (itemIndex !== -1) {
const item = items[startLevel][itemIndex];
const children = item[fieldKeys.value.children] as UTSJSONObject[]|null;
if (children && children.length > 0) {
console.log(`[rebuildItemsFromLevel] Adding children for ${selectedValue}:`, children.length);
allChildren.push(...children);
}
}
});
console.log(`[rebuildItemsFromLevel] Total children count:`, allChildren.length);
// 去重并更新下一级选项
if (allChildren.length > 0) {
const uniqueChildren = allChildren.filter((item, idx, self) =>
idx === self.findIndex(t => t[fieldKeys.value.value] === item[fieldKeys.value.value])
);
console.log(`[rebuildItemsFromLevel] Unique children count:`, uniqueChildren.length);
setItems(startLevel + 1, uniqueChildren);
items.length = startLevel + 2;
setSteps(startLevel + 1, props.placeholder);
steps.length = startLevel + 2;
}
}
// 监听选中索引变化
const watchSelectedIndexes = () => {
if (realColumns.value.length > 0) {
items.length = 1;
items[0] = parseOptions(realColumns.value, fieldKeys.value) as UTSJSONObject[];
let current = realColumns.value;
for (let i = 0, size = selectedIndexes.length; i < size; i += 1) {
const index = selectedIndexes[i];
const next = current[index] as UTSJSONObject | null;
const value = next?.[fieldKeys.value.value] as string | null;
const label = next?.[fieldKeys.value.label] as string | null;
const children = next?.[fieldKeys.value.children] as UTSJSONObject[] | null
const stepLabel = steps.length > i ? steps[i]['label'] : null;
if(value != null) {
selectedValue.push(value);
}
if(label != null && stepLabel != label) {
steps.push({label} as UTSJSONObject);
}
if(children != null && children.length > 0) {
current = children
items.push(parseOptions(children as UTSJSONObject[], fieldKeys.value) as UTSJSONObject[]);
}
}
}
if (steps.length < items.length) {
steps.push({ label: props.placeholder } as UTSJSONObject);
}
// stepIndex.value = items.length - 1;
stepIndex.value = Math.max(Math.min(selectedIndexes.length - 1, items.length - 1), 0);
}
let timer = 0
const initWithValue = () => {
clearTimeout(timer)
timer = setTimeout(()=>{
if (props.multiple) {
// 多选模式初始化
selectedItems.clear();
selectedPaths.length = 0;
selectedSourceData.length = 0;
if (cascaderMultipleValue.value.length > 0) {
// 检查数据格式:如果是扁平数组,需要转换为路径数组
const firstItem = cascaderMultipleValue.value[0];
if (typeof firstItem === 'string') {
// 扁平数组格式:["440306", "440308", "330206", "330213"]
// 需要根据每个值构建完整路径
cascaderMultipleValue.value.forEach(value => {
const fullPath = buildFullPathFromOriginalData(value, 0);
const pathKey = fullPath.join(',');
selectedItems.add(pathKey);
selectedPaths.push([...fullPath]);
// 构建并保存源数据
const sourceData = buildSourceDataFromPath(fullPath);
selectedSourceData.push(sourceData);
});
} else if (Array.isArray(firstItem)) {
// 路径数组格式:[['440000', '440300', '440306'], ...]
cascaderMultipleValue.value.forEach(path => {
const pathKey = path.join(',');
selectedItems.add(pathKey);
selectedPaths.push([...path]);
// 构建并保存源数据
const sourceData = buildSourceDataFromPath(path);
selectedSourceData.push(sourceData);
});
}
}
// 初始化第一级选项
steps.length = 1;
steps[0] = { label: props.placeholder } as UTSJSONObject;
items.length = 1;
items[0] = parseOptions(realColumns.value, fieldKeys.value) as UTSJSONObject[];
stepIndex.value = 0;
} else {
// 单选模式初始化
if (cascaderValue.value != '') {
steps.pop()
const path = getIndexesByValue(realColumns.value, cascaderValue.value, fieldKeys.value)
path?.forEach((e : number, index) => {
// @ts-ignore
if(selectedIndexes.length > index) {
selectedIndexes[index] = e
} else {
// @ts-ignore
selectedIndexes.push(e);
}
});
watchSelectedIndexes();
} else {
selectedIndexes.length = 0;
selectedValue.length = 0;
steps.length = 1;
steps[0] = { label: props.placeholder } as UTSJSONObject;
items.length = 1;
items[0] = parseOptions(realColumns.value, fieldKeys.value) as UTSJSONObject[];
stepIndex.value = 0;
}
}
},50)
}
const setSteps = (index: number, label: string) => {
if(steps.length > index) {
steps[index] = {label} as UTSJSONObject
} else {
steps.push({label} as UTSJSONObject)
}
}
const setItems = (index: number, item: UTSJSONObject[] | null) => {
if(item == null) return
const _item = parseOptions(item, fieldKeys.value) as UTSJSONObject[]
if(items.length > index) {
items[index] = _item
} else {
items.push(_item)
}
}
// 设置级联选择器值(单选模式)
const setCascaderValue = (value: string, selectedOptions: UTSJSONObject[]) => {
cascaderValue.value = value;
emit('change', value, [...selectedOptions])
}
// 设置级联选择器值(多选模式)
const setCascaderMultipleValue = (values: string[][], selectedOptions: UTSJSONObject[]) => {
// 多选模式下始终使用路径数组格式
cascaderMultipleValue.value = values;
// console.log(values, 'values')
// 多选模式下不触发 change 事件,只在确定时触发 finish 事件
// emit('change', values, [...selectedOptions])
}
// 检查多选模式下某个选项是否被选中
const isItemChecked = (value: string, level: number): boolean => {
if (!props.multiple) return false;
// 检查是否有任何包含当前值的路径
return selectedPaths.some(path => path.length > level && path[level] === value);
}
// 处理单元格点击事件
const handleCellClick = (value: string, level: number, item: UTSJSONObject) => {
if (props.multiple) {
// 多选模式:检查是否有子级,如果有则展开显示
const children = item[fieldKeys.value.children] as UTSJSONObject[]|null;
if (children && children.length > 0) {
// 有子级,展开显示子级选项
// 更新当前级别的标题为选中项的名称
setSteps(level, `${item[fieldKeys.value.label] ?? props.placeholder}`);
// 如果是第一级,只显示当前选中项的子级,不合并其他项的子级
if (level === 0) {
// 清空所有后续级别
items.length = level + 1;
steps.length = level + 1;
// 只显示当前选中项的子级
setItems(level + 1, children);
items.length = level + 2;
setSteps(level + 1, props.placeholder);
steps.length = level + 2;
} else {
// 非第一级,合并所有选中项的子级
// 首先检查是否已经有下一级
if (items.length <= level + 1) {
// 没有下一级,直接添加
setItems(level + 1, children);
items.length = level + 2;
setSteps(level + 1, props.placeholder);
steps.length = level + 2;
} else {
// 已经有下一级,需要合并子级选项
const existingChildren = items[level + 1];
const mergedChildren = [...existingChildren, ...children];
// 去重
const uniqueChildren = mergedChildren.filter((item, index, self) =>
index === self.findIndex(t => t[fieldKeys.value.value] === item[fieldKeys.value.value])
);
setItems(level + 1, uniqueChildren);
}
}
// 切换到下一级
stepIndex.value = level + 1;
}
// 如果没有子级则不执行任何操作由checkbox处理选择状态
} else {
// 单选模式:执行原有的选择逻辑
handleSelect(value, level, true);
}
}
/**
* 递归取消所有下级项的选中状态
*/
const removeChildrenFromSelected = (children: UTSJSONObject[], currentLevel: number) => {
if (!children || children.length === 0) return;
children.forEach((child: UTSJSONObject) => {
const childValue = child[fieldKeys.value.value] as string;
selectedItems.delete(childValue);
// 递归处理子级的子级
const grandChildren = child[fieldKeys.value.children] as UTSJSONObject[]|null;
if (grandChildren && grandChildren.length > 0) {
removeChildrenFromSelected(grandChildren, currentLevel + 1);
}
});
};
// 处理多选模式下的checkbox变化
const handleCheckboxChange = (value: string, level: number, checked: boolean, event?: Event) => {
if (!props.multiple) return;
// 阻止事件冒泡,防止触发父级的点击事件
if (event) {
event.stopPropagation();
}
// 检查是否达到最大选择数量
if (checked && props.maxTagCount > 0 && selectedPaths.length >= props.maxTagCount) {
uni.showToast({
title: `最多只能选择${props.maxTagCount}项`,
icon: 'none'
});
return;
}
// 找到当前点击的选项对象
const index = items[level].findIndex((item: UTSJSONObject):boolean => item[fieldKeys.value.value] == value);
if (index === -1) return;
const currentItem = items[level][index];
const children = currentItem[fieldKeys.value.children] as UTSJSONObject[]|null;
// 构建当前路径
const currentPath = buildFullPathFromOriginalData(value, level);
if (checked) {
// 只有叶子节点(没有子级的节点)才添加到路径中
if (!children || children.length === 0) {
// 构建源数据并添加到路径中
const sourceData = buildSourceDataFromPath(currentPath);
addPath(currentPath, sourceData);
}
// 如果有子级,展开显示子级选项
if (children != null && children.length > 0) {
// 更新当前级别的标题为选中项的名称
setSteps(level, `${currentItem[fieldKeys.value.label] ?? props.placeholder}`);
// 如果是第一级,只显示当前选中项的子级,不合并其他项的子级
if (level === 0) {
// 清空所有后续级别
items.length = level + 1;
steps.length = level + 1;
// 只显示当前选中项的子级
setItems(level + 1, children);
items.length = level + 2;
setSteps(level + 1, props.placeholder);
steps.length = level + 2;
} else {
// 非第一级,合并所有选中项的子级
// 首先检查是否已经有下一级
if (items.length <= level + 1) {
// 没有下一级,直接添加
setItems(level + 1, children);
items.length = level + 2;
setSteps(level + 1, props.placeholder);
steps.length = level + 2;
} else {
// 已经有下一级,需要合并子级选项
const existingChildren = items[level + 1];
const mergedChildren = [...existingChildren, ...children];
// 去重
const uniqueChildren = mergedChildren.filter((item, index, self) =>
index === self.findIndex(t => t[fieldKeys.value.value] === item[fieldKeys.value.value])
);
setItems(level + 1, uniqueChildren);
}
}
// 切换到下一级
stepIndex.value = level + 1;
}
} else {
// 移除路径
removePath(currentPath);
// 如果有子级,递归取消所有下级项的选中状态
if (children != null && children.length > 0) {
removeChildrenFromSelected(children, level + 1);
}
// 取消选择时,需要重新计算子级选项和标题
if (children != null && children.length > 0 && items.length > level + 1) {
// 移除当前项的子级选项
const remainingChildren = items[level + 1].filter((child: UTSJSONObject) =>
!children.some((c: UTSJSONObject) => c[fieldKeys.value.value] === child[fieldKeys.value.value])
);
if (remainingChildren.length === 0) {
// 如果没有剩余的子级选项,移除这一级
items.length = level + 1;
steps.length = level + 1;
stepIndex.value = Math.min(stepIndex.value, level);
// 重置当前级别的标题
setSteps(level, props.placeholder);
} else {
setItems(level + 1, remainingChildren);
}
}
// 如果是第一级,取消选择时应该隐藏所有后续级别的 tabs
if (level === 0) {
// 检查当前级别是否还有其他选中的有子级的选项
const currentLevelSelectedItems = Array.from(selectedItems).filter(selectedValue => {
const itemIndex = items[level].findIndex((item: UTSJSONObject) => item[fieldKeys.value.value] === selectedValue);
if (itemIndex === -1) return false;
const item = items[level][itemIndex];
const itemChildren = item[fieldKeys.value.children] as UTSJSONObject[]|null;
return itemChildren != null && itemChildren.length > 0;
});
if (currentLevelSelectedItems.length === 0) {
// 如果第一级没有选中的有子级的选项,隐藏所有后续级别的 tabs
items.length = 1;
steps.length = 1;
stepIndex.value = 0;
}
}
// 取消选择时,重置当前级别的标题为默认值
// 只有当进入下一级时,才会显示当前项的名称
setSteps(level, props.placeholder);
}
// 更新多选值 - 返回路径数组
const newValues = [...selectedPaths];
setCascaderMultipleValue(newValues, []);
// 多选模式下不触发 pick 事件,只在确定时触发 finish 事件
// emit('pick', level, index, value, []);
}
// 取消选择
const cancelSelect = (value: string, level: number, index: number, item:UTSJSONObject) => {
pushAt(selectedIndexes, level, index)
selectedIndexes.length = level;
selectedValue.length = level;
setSteps(level, props.placeholder)
setSteps(level + 1, props.placeholder)
steps.length = level + 1;
const children = item[fieldKeys.value.children] as UTSJSONObject[]|null
if (children != null && children.length > 0) {
setItems(level + 1, children)
} else if (children != null && children.length == 0) {
childrenInfo.value = value;
childrenInfo.level = level;
}
}
// 选择
const chooseSelect = (value: string, level: number, index: number, item:UTSJSONObject) => {
pushAt(selectedIndexes, level, index)
selectedIndexes.length = level + 1;
pushAt(selectedValue, level, value)
selectedValue.length = level + 1;
setSteps(level, `${item[fieldKeys.value.label] ?? props.placeholder}`)
const children = item[fieldKeys.value.children] as UTSJSONObject[]|null
if (children != null && children.length > 0) {
setItems(level + 1, children)
items.length = level + 2;
stepIndex.value += 1;
setSteps(level + 1, props.placeholder)
steps.length = level + 2;
}
// else if (children != null && children.length == 0) {
// childrenInfo.value = value;
// childrenInfo.level = level;
// }
else {
// 如果存在确确按钮,则需要点按钮关闭
if(props.confirmBtn != null) return
items.length = level + 1
steps.length = level + 1;
setCascaderValue(
`${item[fieldKeys.value.value] ?? ''}`,
items.map((item, index):UTSJSONObject => toRaw(item[selectedIndexes[index]]))
)
emit('finish');
show.value = false
}
}
// 级联单选模式
const handleSelect = (value: any|null, level: number, shouldEmit: boolean) => {
// 多选模式下点击事件由checkbox处理这里直接返回
if (props.multiple) return;
const _value = `${value ?? ''}`
if(items.length <= level) return
// 1. 找到当前点击的选项
const index = items[level].findIndex((item: UTSJSONObject):boolean => item[fieldKeys.value.value] == _value);
// 2. 获取选项对象
let item = selectedIndexes.slice(0, level).reduce((acc:UTSJSONObject[], item:number, index:number):UTSJSONObject[] => {
if (index == 0) {
return [acc[item]] as UTSJSONObject[];
}
const children = acc[0][fieldKeys.value.children] as UTSJSONObject[]|null///.getArray<UTSJSONObject>(fieldKeys.value.children)
return [children?.[item] ?? {}] as UTSJSONObject[];
}, realColumns.value);
let cursor :UTSJSONObject|null
if (level == 0) {
cursor = item[index];
} else {
const children = item[0][fieldKeys.value.children] as UTSJSONObject[]|null//.getArray<UTSJSONObject>(fieldKeys.value.children)
cursor = children?.[index];
}
// 3. 检查是否禁用
const disabled = cursor?.['disabled'] == true
if (disabled || cursor == null) return;
// 反选条件1有确认按钮并且没有下级 2允许各自取消checkStrictly
const hasChildren = cursor[fieldKeys.value.children] != null;
const isSelected = selectedValue.includes(_value);
const canCancel =
(props.confirmBtn != null && !hasChildren) ||
(props.checkStrictly);
if (canCancel && isSelected) {
cancelSelect(_value, level, index, cursor);
} else {
chooseSelect(_value, level, index, cursor);
}
if(shouldEmit){
const code = cursor[fieldKeys.value.value] as string
emit('pick', level, index, code, toRaw([...selectedIndexes]))
if(!props.uniCloud) return
pickUniCloudArea(uniCloudColumns.value, level, code, toRaw([...selectedIndexes]), cache)
}
}
// 更新级联选择器值
const updateCascaderValue = () => {
setCascaderValue(
selectedValue[selectedValue.length - 1],
items
.filter((item, index):boolean => selectedIndexes.length > index)
.map((item, index):UTSJSONObject => toRaw(item[selectedIndexes[index]]))
);
};
const onClose = () => {
emit('close');
show.value = false
}
const onCloseBtn = () => {
if (props.checkStrictly) {
updateCascaderValue();
onClose()
} else {
onClose()
}
}
const handleConfirm = () => {
if (props.multiple) {
// 多选模式
if (selectedPaths.length == 0) {
show.value = false;
return;
}
// 获取选中的路径数组,只保留完整的路径(包含所有层级的路径)
const selectedPathValues = selectedPaths.filter(path => {
// 只保留完整的路径,即路径中不包含空字符串的完整路径
return path.length > 0 && !path.includes('');
});
// 如果没有完整的路径,直接关闭
if (selectedPathValues.length === 0) {
show.value = false;
return;
}
// 获取选中的选项信息(使用保存的源数据)
const selectedOptions: UTSJSONObject[] = [...selectedSourceData];
// 更新多选值
setCascaderMultipleValue(selectedPathValues, selectedOptions);
// 触发 finish 事件,传递路径数组数据和源数据
emit('finish', selectedPathValues, selectedOptions);
// 关闭弹窗
show.value = false;
} else {
// 单选模式
// 1. 如果没有选中任何选项,直接关闭
if (selectedValue.length == 0) {
show.value = false;
return;
}
// 2. 获取当前选中的完整路径(各级选项)
const selectedOptions = items
.filter((_, index) => selectedIndexes.length > index)
.map((item, index) => toRaw(item[selectedIndexes[index]]));
// 3. 更新级联选择器的值(如果是联动模式)
if (!props.checkStrictly) {
setCascaderValue(
selectedValue[selectedValue.length - 1],
//@ts-ignore
selectedOptions
);
}
// 4. 触发 finish 事件,返回选中值和选项路径
emit('finish');
// 5. 关闭弹窗
show.value = false;
}
}
const optionsWatch = watch(():UTSJSONObject[] => realColumns.value, (_:UTSJSONObject[]) => {
watchSelectedIndexes()
// 回显时 可能存在先有值才后数据,只在首次判断。如果不是首次会影响后面异步加载
if(selectedIndexes.length == 0) {
initWithValue()
}
if (show.value) {
handleSelect(childrenInfo.value, childrenInfo.level, false);
}
},{ deep: true});
const placeholderWatch = watch(():string=> props.placeholder, (newValue: string, oldValue: string) => {
const index = steps.indexOf({label : oldValue} as UTSJSONObject);
if (index != -1) {
setSteps(index, newValue)
}
});
onMounted(() => {
// #ifdef APP
// hos 获取设备尺寸比较迟,故采用这方式
nextTick(()=>{
windowWidth.value = uni.getWindowInfo().windowWidth
})
// #endif
if(props.uniCloud) {
const update = async () => {
const provinces = await getUniCloudArea({type: 0}, cache)
uniCloudColumns.value = provinces
if(cascaderValue.value != '' && /^\d{6}$/.test(cascaderValue.value)) {
const [province, citie, countie] = splitEveryTwo(cascaderValue.value );
const provinceCode = province.padEnd(6, '0')
const provinceIndex = getIndexByValue(uniCloudColumns.value, provinceCode)
const cities = await getUniCloudArea({type: 1, parent_code: provinceCode}, cache)
const citieCode = (province + citie).padEnd(6, '0')
const citiesIndex = getIndexByValue(uniCloudColumns.value, citieCode)
const counties = await getUniCloudArea({type: 2, parent_code: citieCode}, cache)
updateChildren(uniCloudColumns.value, cities, [provinceIndex])
updateChildren(uniCloudColumns.value[provinceIndex]['children'] as UTSJSONObject[], counties, [citiesIndex])
}
initWithValue()
}
update()
return
}
initWithValue()
})
watchEffect(()=>{
// 回显时 如果没有选项就重新设置
const value = cascaderValue.value
if(realColumns.value.length > 0 && selectedIndexes.length == 0) {
initWithValue()
}
})
// 监听 cascaderValue 变化,当值被清空时重置状态
const cascaderValueWatch = watch(():string => cascaderValue.value, (newValue: string, oldValue: string) => {
if (newValue == '' && oldValue != '') {
initWithValue()
}
})
// 监听多选值变化
const cascaderMultipleValueWatch = watch(():string[][] => cascaderMultipleValue.value, (newValue: string[][], oldValue: string[][]) => {
if (props.multiple && JSON.stringify(newValue) != JSON.stringify(oldValue)) {
selectedItems.clear();
selectedPaths.length = 0;
selectedSourceData.length = 0;
if (newValue.length > 0) {
// 检查数据格式:如果是扁平数组,需要转换为路径数组
const firstItem = newValue[0];
if (typeof firstItem === 'string') {
// 扁平数组格式:["440306", "440308", "330206", "330213"]
// 需要根据每个值构建完整路径
newValue.forEach(value => {
const fullPath = buildFullPathFromOriginalData(value, 0);
const pathKey = fullPath.join(',');
selectedItems.add(pathKey);
selectedPaths.push([...fullPath]);
// 构建并保存源数据
const sourceData = buildSourceDataFromPath(fullPath);
selectedSourceData.push(sourceData);
});
} else if (Array.isArray(firstItem)) {
// 路径数组格式:[['440000', '440300', '440306'], ...]
newValue.forEach(path => {
const pathKey = path.join(',');
selectedItems.add(pathKey);
selectedPaths.push([...path]);
// 构建并保存源数据
const sourceData = buildSourceDataFromPath(path);
selectedSourceData.push(sourceData);
});
}
}
}
})
// #ifdef APP
const containerRef = ref<UniElement|null>(null)
watchEffect(()=>{
if(containerRef.value == null) return
// ios/hos 使用rpx效果不达标还得转成px
containerRef.value?.children.forEach(item => {
item.style.setProperty('width', windowWidth.value + 'px')
})
containerRef.value?.style.setProperty('width', ((items.length + 1) * windowWidth.value) + 'px')
containerRef.value?.style.setProperty('transform', `translateX(${-stepIndex.value * windowWidth.value}px)`)
})
// #endif
const opened = ()=> {
// #ifdef APP-HARMONY
// 鸿蒙next 在弹窗里首次样式不生效 通过此下策让元素重绘
// hbx4.63
setTimeout(() => {
if(hosUpdate.value > 0 || steps.length == 0) return
hosUpdate.value++
}, 0);
// #endif
}
onUnmounted(()=>{
optionsWatch()
placeholderWatch()
cascaderValueWatch()
cascaderMultipleValueWatch()
})
</script>
<style lang="scss">
@import './index';
</style>