2025-07-21 16:47:04 +08:00
|
|
|
|
<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"
|
|
|
|
|
|
arrow-icon="time-line"
|
|
|
|
|
|
@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"
|
|
|
|
|
|
:headers="headers"
|
|
|
|
|
|
:columns="columns"
|
|
|
|
|
|
:reset-on-change="false"
|
|
|
|
|
|
@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, isNull, 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-time"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 组件属性定义
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
// 透传样式配置
|
|
|
|
|
|
pt: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 选择器的值
|
|
|
|
|
|
modelValue: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
|
|
|
|
|
},
|
|
|
|
|
|
// 表头
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
type: Array as PropType<string[]>,
|
|
|
|
|
|
default: () => [t("小时"), t("分钟"), t("秒数")]
|
|
|
|
|
|
},
|
|
|
|
|
|
// 选择器标题
|
|
|
|
|
|
title: {
|
|
|
|
|
|
type: String,
|
2025-07-28 18:57:37 +08:00
|
|
|
|
default: () => t("请选择")
|
2025-07-21 16:47:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 选择器占位符
|
|
|
|
|
|
placeholder: {
|
|
|
|
|
|
type: String,
|
2025-07-28 18:57:37 +08:00
|
|
|
|
default: () => t("请选择")
|
2025-07-21 16:47:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 是否显示选择器触发器
|
|
|
|
|
|
showTrigger: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否禁用选择器
|
|
|
|
|
|
disabled: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 确认按钮文本
|
|
|
|
|
|
confirmText: {
|
|
|
|
|
|
type: String,
|
2025-07-28 18:57:37 +08:00
|
|
|
|
default: () => t("确定")
|
2025-07-21 16:47:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 是否显示确认按钮
|
|
|
|
|
|
showConfirm: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 取消按钮文本
|
|
|
|
|
|
cancelText: {
|
|
|
|
|
|
type: String,
|
2025-07-28 18:57:37 +08:00
|
|
|
|
default: () => t("取消")
|
2025-07-21 16:47:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 是否显示取消按钮
|
|
|
|
|
|
showCancel: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 时间标签格式化
|
|
|
|
|
|
labelFormat: {
|
|
|
|
|
|
type: String as PropType<string>,
|
|
|
|
|
|
default: "{H}:{m}:{s}"
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 定义事件
|
|
|
|
|
|
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<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 当前选中项的索引
|
|
|
|
|
|
const indexes = ref<number[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 时间选择器列表
|
|
|
|
|
|
const columns = computed(() => {
|
|
|
|
|
|
const arr = [[], [], []] as ClSelectOption[][];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 60; i++) {
|
|
|
|
|
|
const v = i.toString().padStart(2, "0");
|
|
|
|
|
|
|
|
|
|
|
|
const item = {
|
|
|
|
|
|
label: v,
|
|
|
|
|
|
value: v
|
|
|
|
|
|
} as ClSelectOption;
|
|
|
|
|
|
|
|
|
|
|
|
if (i < 24) {
|
|
|
|
|
|
arr[0].push(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
arr[1].push(item);
|
|
|
|
|
|
arr[2].push(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return arr;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 显示文本
|
|
|
|
|
|
const text = computed(() => {
|
|
|
|
|
|
// 获取当前v-model绑定的时间字符串
|
|
|
|
|
|
const val = props.modelValue;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果值为空或为null,返回空字符串
|
|
|
|
|
|
if (isEmpty(val) || isNull(val)) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 拆分时间字符串,分别获取小时、分钟、秒
|
|
|
|
|
|
const [h, m, s] = val.split(":");
|
|
|
|
|
|
|
|
|
|
|
|
// 按照labelFormat格式化显示文本
|
|
|
|
|
|
return props.labelFormat.replace("{H}", h).replace("{m}", m).replace("{s}", s);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 选择器值改变事件
|
|
|
|
|
|
function onChange(a: number[]) {
|
|
|
|
|
|
// 复制当前组件内部维护的索引数组
|
|
|
|
|
|
const b = [...indexes.value];
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有列,处理联动逻辑
|
|
|
|
|
|
for (let i = 0; i < a.length; i++) {
|
|
|
|
|
|
// 检查当前列是否发生改变
|
|
|
|
|
|
if (b[i] != a[i]) {
|
|
|
|
|
|
// 更新当前列的索引
|
|
|
|
|
|
b[i] = a[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新组件内部维护的索引数组
|
|
|
|
|
|
indexes.value = b;
|
|
|
|
|
|
// 根据最新的索引数组,更新选中的值数组
|
|
|
|
|
|
value.value = b.map((e, i) => columns.value[i][e].value as string);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选择器显示状态
|
|
|
|
|
|
const visible = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 选择回调函数
|
|
|
|
|
|
let callback: ((value: string) => void) | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 打开选择器
|
|
|
|
|
|
function open(cb: ((value: string) => void) | null = null) {
|
|
|
|
|
|
if (props.disabled) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visible.value = true;
|
|
|
|
|
|
callback = cb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭选择器
|
|
|
|
|
|
function close() {
|
|
|
|
|
|
visible.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空选择器
|
|
|
|
|
|
function clear() {
|
|
|
|
|
|
emit("update:modelValue", "");
|
|
|
|
|
|
emit("change", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确认选择
|
|
|
|
|
|
function confirm() {
|
|
|
|
|
|
// 将选中值转换为字符串格式
|
|
|
|
|
|
const val = value.value.join(":");
|
|
|
|
|
|
|
|
|
|
|
|
// 触发更新事件
|
|
|
|
|
|
emit("update:modelValue", val);
|
|
|
|
|
|
emit("change", val);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发回调
|
|
|
|
|
|
if (callback != null) {
|
|
|
|
|
|
callback!(val);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭选择器
|
|
|
|
|
|
close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听modelValue变化
|
|
|
|
|
|
watch(
|
|
|
|
|
|
computed(() => props.modelValue),
|
|
|
|
|
|
(val: string) => {
|
|
|
|
|
|
// 声明选中值数组
|
|
|
|
|
|
let _value: string[];
|
|
|
|
|
|
|
|
|
|
|
|
// 判断输入值是否为空
|
|
|
|
|
|
if (isEmpty(val) || isNull(val)) {
|
|
|
|
|
|
// 设置空数组
|
|
|
|
|
|
_value = [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 按冒号分割字符串为数组
|
|
|
|
|
|
_value = val.split(":");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 声明索引数组
|
|
|
|
|
|
let _indexes = [] as number[];
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历时分秒三列
|
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
|
// 判断是否需要设置默认值
|
|
|
|
|
|
if (i >= _value.length) {
|
|
|
|
|
|
// 添加默认索引0
|
|
|
|
|
|
_indexes.push(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加默认值
|
|
|
|
|
|
_value.push(columns.value[i][0].value as string);
|
|
|
|
|
|
} 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>
|