版本发布

This commit is contained in:
icssoa
2025-07-21 16:47:04 +08:00
parent 1abed7a2e1
commit 6d8193880a
307 changed files with 41718 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
<template>
<cl-select-trigger
v-if="showTrigger"
:pt="{
className: pt.trigger?.className,
icon: pt.trigger?.icon
}"
:placeholder="placeholder"
:disabled="disabled"
:focus="popupRef?.isOpen"
:text="text"
@tap="open()"
@clear="clear"
></cl-select-trigger>
<cl-popup
ref="popupRef"
v-model="visible"
:title="title"
:pt="{
className: pt.popup?.className,
header: pt.popup?.header,
container: pt.popup?.container,
mask: pt.popup?.mask,
draw: pt.popup?.draw
}"
>
<view class="cl-select-popup" @touchmove.stop>
<view class="cl-select-popup__picker">
<cl-picker-view
:value="indexes"
:columns="columns"
@change="onChange"
></cl-picker-view>
</view>
<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"
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 } from "../../types";
import { isEmpty, parsePt } 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"
});
// 值类型
type Value = string[] | number[] | number | string | null;
// 组件属性定义
const props = defineProps({
// 透传样式配置
pt: {
type: Object,
default: () => ({})
},
// 选择器的值
modelValue: {
type: [Array, Number, String] as PropType<Value>,
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"]);
// 弹出层引用
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
// 透传样式类型定义
type PassThrough = {
trigger?: ClSelectTriggerPassThrough;
popup?: ClPopupPassThrough;
};
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 当前选中的值
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 = computed(() => {
// 获取当前v-model绑定的值
const val = props.modelValue;
// 如果值为null或空直接返回空字符串
if (val == null || isEmpty(val)) {
return "";
}
// 用于存储每列的选中值
let arr: any[];
if (props.columnCount == 1) {
// 单列时将值包装为数组
arr = [val];
} else {
// 多列时直接使用数组
arr = val as any[];
}
// 遍历每列的选中值查找对应label找不到则用空字符串
return arr
.map((e, i) => columns.value[i].find((a) => a.value == e)?.label ?? "")
.join(props.splitor);
});
// 选择器值改变事件
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) => columns.value[i][e].value);
}
// 选择器显示状态
const visible = ref(false);
// 选择回调函数
let callback: ((value: Value) => void) | null = null;
// 打开选择器
function open(cb: ((value: Value) => void) | null = null) {
if (props.disabled) {
return;
}
visible.value = true;
callback = cb;
}
// 关闭选择器
function close() {
visible.value = false;
}
// 清空选择器
function clear() {
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 = props.columnCount == 1 ? value.value[0] : value.value;
// 触发更新事件
emit("update:modelValue", val);
emit("change", val);
// 触发回调
if (callback != null) {
callback!(val);
}
// 关闭选择器
close();
}
// 监听modelValue变化
watch(
computed(() => props.modelValue),
(val: Value) => {
// 声明选中值数组
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++) {
// 判断是否超出选中值数组长度
if (i >= _value.length) {
// 添加默认索引0
_indexes.push(0);
// 添加默认值
_value.push(columns.value[i][0].value);
}
// 在范围内
else {
// 查找匹配的选项索引
let index = columns.value[i].findIndex((e) => e.value == _value[i]);
// 索引无效时重置为0
if (index < 0) {
index = 0;
}
// 添加索引
_indexes.push(index);
}
}
// 更新选中值
value.value = _value;
// 更新索引值
indexes.value = _indexes;
},
{
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>

View File

@@ -0,0 +1,25 @@
import type { ClSelectOption } from "../../types";
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
import type { ClPopupPassThrough } from "../cl-popup/props";
export type ClSelectPassThrough = {
trigger?: ClSelectTriggerPassThrough;
popup?: ClPopupPassThrough;
};
export type ClSelectProps = {
className?: string;
pt?: ClSelectPassThrough;
modelValue?: Value;
title?: string;
placeholder?: string;
options?: ClSelectOption[];
showTrigger?: boolean;
disabled?: boolean;
columnCount?: number;
splitor?: string;
confirmText?: string;
showConfirm?: boolean;
cancelText?: string;
showCancel?: boolean;
};