Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-select/cl-select.uvue
2025-10-17 23:22:31 +08:00

430 lines
8.7 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>
<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>