430 lines
8.7 KiB
Plaintext
430 lines
8.7 KiB
Plaintext
<template>
|
||
<cl-select-trigger
|
||
v-if="showTrigger"
|
||
:pt="ptTrigger"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
:focus="popupRef?.isOpen"
|
||
:text="text"
|
||
@open="open()"
|
||
@clear="clear"
|
||
></cl-select-trigger>
|
||
|
||
<cl-popup ref="popupRef" v-model="visible" :title="title" :pt="ptPopup">
|
||
<view class="cl-select-popup" @touchmove.stop>
|
||
<slot name="prepend"></slot>
|
||
|
||
<view class="cl-select-popup__picker">
|
||
<cl-empty
|
||
v-if="noOptions"
|
||
:fixed="false"
|
||
:pt="{
|
||
className: '!h-[600rpx]'
|
||
}"
|
||
></cl-empty>
|
||
|
||
<cl-picker-view
|
||
:value="indexes"
|
||
:columns="columns"
|
||
@change-index="onChange"
|
||
v-else
|
||
></cl-picker-view>
|
||
</view>
|
||
|
||
<slot name="append"></slot>
|
||
|
||
<view class="cl-select-popup__op">
|
||
<cl-button
|
||
v-if="showCancel"
|
||
size="large"
|
||
text
|
||
border
|
||
type="light"
|
||
:pt="{
|
||
className: 'flex-1 !rounded-xl h-[80rpx]'
|
||
}"
|
||
@tap="close"
|
||
>{{ cancelText }}</cl-button
|
||
>
|
||
<cl-button
|
||
v-if="showConfirm && !noOptions"
|
||
size="large"
|
||
:pt="{
|
||
className: 'flex-1 !rounded-xl h-[80rpx]'
|
||
}"
|
||
@tap="confirm"
|
||
>{{ confirmText }}</cl-button
|
||
>
|
||
</view>
|
||
</view>
|
||
</cl-popup>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, type PropType, watch } from "vue";
|
||
import type { ClSelectOption, ClSelectValue } from "../../types";
|
||
import { isEmpty, isNull, parsePt, parseToObject } from "@/cool";
|
||
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
|
||
import type { ClPopupPassThrough } from "../cl-popup/props";
|
||
import { t } from "@/locale";
|
||
|
||
defineOptions({
|
||
name: "cl-select"
|
||
});
|
||
|
||
defineSlots<{
|
||
prepend(): any;
|
||
append(): any;
|
||
}>();
|
||
|
||
// 组件属性定义
|
||
const props = defineProps({
|
||
// 透传样式配置
|
||
pt: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 选择器的值
|
||
modelValue: {
|
||
type: [Array, Number, String] as PropType<ClSelectValue>,
|
||
default: null
|
||
},
|
||
// 选择器标题
|
||
title: {
|
||
type: String,
|
||
default: () => t("请选择")
|
||
},
|
||
// 选择器占位符
|
||
placeholder: {
|
||
type: String,
|
||
default: () => t("请选择")
|
||
},
|
||
// 选项数据
|
||
options: {
|
||
type: Array as PropType<ClSelectOption[]>,
|
||
default: () => []
|
||
},
|
||
// 是否显示选择器触发器
|
||
showTrigger: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否禁用选择器
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 列数
|
||
columnCount: {
|
||
type: Number as PropType<number>,
|
||
default: 1
|
||
},
|
||
// 分隔符
|
||
splitor: {
|
||
type: String,
|
||
default: " - "
|
||
},
|
||
// 确认按钮文本
|
||
confirmText: {
|
||
type: String,
|
||
default: () => t("确定")
|
||
},
|
||
// 是否显示确认按钮
|
||
showConfirm: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 取消按钮文本
|
||
cancelText: {
|
||
type: String,
|
||
default: () => t("取消")
|
||
},
|
||
// 是否显示取消按钮
|
||
showCancel: {
|
||
type: Boolean,
|
||
default: true
|
||
}
|
||
});
|
||
|
||
// 定义事件
|
||
const emit = defineEmits(["update:modelValue", "change", "changing"]);
|
||
|
||
// 弹出层引用
|
||
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
|
||
|
||
// 透传样式类型定义
|
||
type PassThrough = {
|
||
trigger?: ClSelectTriggerPassThrough;
|
||
popup?: ClPopupPassThrough;
|
||
};
|
||
|
||
// 解析透传样式配置
|
||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||
|
||
// 解析触发器透传样式配置
|
||
const ptTrigger = computed(() => parseToObject(pt.value.trigger));
|
||
|
||
// 解析弹窗透传样式配置
|
||
const ptPopup = computed(() => parseToObject(pt.value.popup));
|
||
|
||
// 是否为空选项
|
||
const noOptions = computed(() => {
|
||
return isEmpty(props.options);
|
||
});
|
||
|
||
// 当前选中的值
|
||
const value = ref<any[]>([]);
|
||
|
||
// 当前选中项的索引
|
||
const indexes = ref<number[]>([]);
|
||
|
||
// 计算选择器列表数据
|
||
const columns = computed<ClSelectOption[][]>(() => {
|
||
// 获取原始选项数据
|
||
let options = props.options;
|
||
|
||
// 用于存储每一列的选项数据
|
||
let columns = [] as ClSelectOption[][];
|
||
|
||
// 根据当前选中值构建多列数据
|
||
for (let i = 0; i < props.columnCount; i++) {
|
||
// 复制当前层级的选项数据作为当前列的选项
|
||
const column = [...options];
|
||
|
||
// 获取当前列的选中值,如果value数组长度不足则为null
|
||
const val = i >= value.value.length ? null : value.value[i];
|
||
|
||
// 在当前列选项中查找选中值对应的选项
|
||
let item = options.find((item) => item.value == val);
|
||
|
||
// 如果未找到选中项且选项不为空,则默认选中第一项
|
||
if (item == null && !isEmpty(options)) {
|
||
item = options[0];
|
||
}
|
||
|
||
// 如果选中项有子选项,则更新options为子选项数据,用于构建下一列
|
||
if (item?.children != null) {
|
||
options = item.children as ClSelectOption[];
|
||
}
|
||
|
||
// 将当前列的选项数据添加到columns数组
|
||
columns.push(column);
|
||
}
|
||
|
||
// 返回构建好的多列数据
|
||
return columns;
|
||
});
|
||
|
||
// 显示文本
|
||
const text = ref("");
|
||
|
||
// 更新文本内容
|
||
function updateText() {
|
||
// 当前绑定的值
|
||
const val = props.modelValue;
|
||
|
||
// 如果值为null或空,直接返回空字符串
|
||
if (val == null || isEmpty(val)) {
|
||
text.value = "";
|
||
} else {
|
||
// 用于存储每列的选中值
|
||
let arr: any[];
|
||
|
||
if (props.columnCount == 1) {
|
||
// 单列时将值包装为数组
|
||
arr = [val];
|
||
} else {
|
||
// 多列时直接使用数组
|
||
arr = val as any[];
|
||
}
|
||
|
||
// 遍历每列的选中值,查找对应label,找不到则用空字符串
|
||
text.value = arr
|
||
.map((e, i) => columns.value[i].find((a) => a.value == e)?.label ?? "")
|
||
.join(props.splitor);
|
||
}
|
||
}
|
||
|
||
// 获取当前选中值
|
||
function getValue() {
|
||
return props.columnCount == 1 ? value.value[0] : value.value;
|
||
}
|
||
|
||
// 解析值
|
||
function setValue(val: ClSelectValue) {
|
||
// 声明选中值数组
|
||
let _value: any[];
|
||
|
||
// 判断值是否为null
|
||
if (val == null) {
|
||
// 设置为空数组
|
||
_value = [];
|
||
}
|
||
// 判断是否为数组类型
|
||
else if (Array.isArray(val)) {
|
||
// 使用该数组
|
||
_value = [...(val as any[])];
|
||
}
|
||
// 其他类型
|
||
else {
|
||
// 转换为数组格式
|
||
_value = [val];
|
||
}
|
||
|
||
// 存储每列选中项的索引值
|
||
let _indexes = [] as number[];
|
||
|
||
// 遍历所有列
|
||
for (let i = 0; i < props.columnCount; i++) {
|
||
// 获取当前列的选项数据
|
||
const column = columns.value[i];
|
||
|
||
// 判断是否超出选中值数组长度
|
||
if (i >= _value.length) {
|
||
// 添加默认索引0
|
||
_indexes.push(0);
|
||
|
||
// 添加默认值
|
||
if (!isNull(column) && column.length > 0 && !isNull(column[0])) {
|
||
_value.push(column[0].value);
|
||
}
|
||
}
|
||
// 在范围内
|
||
else {
|
||
// 查找匹配的选项索引
|
||
let index = column.findIndex((e) => e.value == _value[i]);
|
||
|
||
// 索引无效时重置为0
|
||
if (index < 0) {
|
||
index = 0;
|
||
}
|
||
|
||
// 添加索引
|
||
_indexes.push(index);
|
||
}
|
||
}
|
||
|
||
// 更新选中值
|
||
value.value = _value;
|
||
|
||
// 更新索引值
|
||
indexes.value = _indexes;
|
||
|
||
// 更新显示文本
|
||
updateText();
|
||
}
|
||
|
||
// 选择器值改变事件
|
||
function onChange(a: number[]) {
|
||
// 复制当前组件内部维护的索引数组
|
||
const b = [...indexes.value];
|
||
|
||
// 标记是否有列发生改变
|
||
let changed = false;
|
||
|
||
// 遍历所有列,处理联动逻辑
|
||
for (let i = 0; i < a.length; i++) {
|
||
if (changed) {
|
||
// 如果前面的列发生改变,后续列重置为第一项(索引0)
|
||
b[i] = 0;
|
||
} else {
|
||
// 检查当前列是否发生改变
|
||
if (b[i] != a[i]) {
|
||
// 更新当前列的索引
|
||
b[i] = a[i];
|
||
// 标记已发生改变,后续列需要重置
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新组件内部维护的索引数组
|
||
indexes.value = b;
|
||
// 根据最新的索引数组,更新选中的值数组
|
||
value.value = b.map((e, i) => (isNull(columns.value[i][e]) ? 0 : columns.value[i][e].value));
|
||
|
||
// 触发changing事件
|
||
emit("changing", getValue());
|
||
}
|
||
|
||
// 选择器显示状态
|
||
const visible = ref(false);
|
||
|
||
// 选择回调函数
|
||
let callback: ((value: ClSelectValue) => void) | null = null;
|
||
|
||
// 打开选择器
|
||
function open(cb: ((value: ClSelectValue) => void) | null = null) {
|
||
visible.value = true;
|
||
|
||
// 设置值
|
||
setValue(props.modelValue);
|
||
|
||
// 回调
|
||
callback = cb;
|
||
}
|
||
|
||
// 关闭选择器
|
||
function close() {
|
||
visible.value = false;
|
||
}
|
||
|
||
// 清空选择器
|
||
function clear() {
|
||
text.value = "";
|
||
|
||
if (props.columnCount == 1) {
|
||
emit("update:modelValue", null);
|
||
emit("change", null);
|
||
} else {
|
||
emit("update:modelValue", [] as any[]);
|
||
emit("change", [] as any[]);
|
||
}
|
||
}
|
||
|
||
// 确认选择
|
||
function confirm() {
|
||
// 根据列数返回单个值或数组
|
||
const val = getValue();
|
||
|
||
// 触发更新事件
|
||
emit("update:modelValue", val);
|
||
emit("change", val);
|
||
|
||
// 触发回调
|
||
if (callback != null) {
|
||
callback!(val);
|
||
}
|
||
|
||
// 关闭选择器
|
||
close();
|
||
}
|
||
|
||
// 监听modelValue变化
|
||
watch(
|
||
computed(() => props.modelValue),
|
||
(val: ClSelectValue) => {
|
||
setValue(val);
|
||
},
|
||
{
|
||
immediate: true
|
||
}
|
||
);
|
||
|
||
defineExpose({
|
||
open,
|
||
close
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.cl-select {
|
||
&-popup {
|
||
&__op {
|
||
@apply flex flex-row items-center justify-center;
|
||
padding: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
</style>
|