1097 lines
38 KiB
Plaintext
1097 lines
38 KiB
Plaintext
<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"></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> |