cl-select-date 支持范围选择

This commit is contained in:
icssoa
2025-07-28 18:57:02 +08:00
parent a64c6ef95a
commit 524a8239ad

View File

@@ -25,8 +25,77 @@
mask: pt.popup?.mask,
draw: pt.popup?.draw
}"
@closed="onClosed"
>
<view class="cl-select-popup" @touchmove.stop>
<view class="cl-select-popup__range" v-if="rangeable">
<view class="cl-select-popup__range-shortcuts" v-if="showShortcuts">
<cl-tag
v-for="(item, index) in shortcuts"
:key="index"
plain
:type="shortcutsIndex == index ? 'primary' : 'info'"
@tap="setRangeValue(item.value, index)"
>
{{ item.label }}
</cl-tag>
</view>
<view class="cl-select-popup__range-values">
<view
class="cl-select-popup__range-values-start"
:class="{
'is-dark': isDark,
active: rangeIndex == 0
}"
@tap="setRange(0)"
>
<cl-text
v-if="values[0] != ''"
:pt="{
className: 'text-center'
}"
>{{ values[0] }}</cl-text
>
<cl-text
v-else
:pt="{
className: 'text-center !text-surface-400'
}"
>{{ startPlaceholder }}</cl-text
>
</view>
<cl-text :pt="{ className: 'mx-3' }">{{ rangeSeparator }}</cl-text>
<view
class="cl-select-popup__range-values-end"
:class="{
'is-dark': isDark,
active: rangeIndex == 1
}"
@tap="setRange(1)"
>
<cl-text
v-if="values[1] != ''"
:pt="{
className: 'text-center'
}"
>{{ values[1] }}</cl-text
>
<cl-text
v-else
:pt="{
className: 'text-center !text-surface-400'
}"
>{{ endPlaceholder }}</cl-text
>
</view>
</view>
</view>
<view class="cl-select-popup__picker">
<cl-picker-view
:headers="headers"
@@ -65,11 +134,13 @@
<script setup lang="ts">
import { ref, computed, type PropType, watch, nextTick } from "vue";
import type { ClSelectOption } from "../../types";
import { dayUts, isEmpty, isNull, parsePt } from "@/cool";
import type { ClSelectDateShortcut, ClSelectOption } from "../../types";
import { dayUts, isDark, isEmpty, isNull, parsePt } from "@/cool";
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
import type { ClPopupPassThrough } from "../cl-popup/props";
import { t } from "@/locale";
import { useUi } from "../../hooks";
import { config } from "../../config";
defineOptions({
name: "cl-select-date"
@@ -87,6 +158,11 @@ const props = defineProps({
type: String,
default: ""
},
// 选择器的范围值外部v-model:values绑定
values: {
type: Array as PropType<string[]>,
default: () => []
},
// 表头
headers: {
type: Array as PropType<string[]>,
@@ -95,12 +171,12 @@ const props = defineProps({
// 选择器标题
title: {
type: String,
default: t("请选择")
default: () => t("请选择")
},
// 选择器占位符
placeholder: {
type: String,
default: t("请选择")
default: () => t("请选择")
},
// 是否显示选择器触发器
showTrigger: {
@@ -115,7 +191,7 @@ const props = defineProps({
// 确认按钮文本
confirmText: {
type: String,
default: t("确定")
default: () => t("确定")
},
// 是否显示确认按钮
showConfirm: {
@@ -125,7 +201,7 @@ const props = defineProps({
// 取消按钮文本
cancelText: {
type: String,
default: t("取消")
default: () => t("取消")
},
// 是否显示取消按钮
showCancel: {
@@ -145,27 +221,59 @@ const props = defineProps({
// 开始日期
start: {
type: String,
default: "1950-01-01 00:00:00"
default: config.startDate
},
// 结束日期
end: {
type: String,
default: "2050-12-31 23:59:59"
default: config.endDate
},
// 类型,控制选择的粒度
type: {
type: String as PropType<"year" | "month" | "date" | "hour" | "minute" | "second">,
default: "second"
},
// 是否范围选择
rangeable: {
type: Boolean,
default: false
},
// 开始日期占位符
startPlaceholder: {
type: String,
default: () => t("开始日期")
},
// 结束日期占位符
endPlaceholder: {
type: String,
default: () => t("结束日期")
},
// 范围分隔符
rangeSeparator: {
type: String,
default: () => t("至")
},
// 是否显示快捷选项
showShortcuts: {
type: Boolean,
default: true
},
// 快捷选项
shortcuts: {
type: Array as PropType<ClSelectDateShortcut[]>,
default: () => []
}
});
// 定义事件支持v-model和change事件
const emit = defineEmits(["update:modelValue", "change"]);
// 定义事件
const emit = defineEmits(["update:modelValue", "change", "update:values", "range-change"]);
const ui = useUi();
// 弹出层引用用于控制popup的显示与隐藏
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
// 透传样式类型定义支持trigger和popup的样式透传
// 透传样式类型定义
type PassThrough = {
trigger?: ClSelectTriggerPassThrough;
popup?: ClPopupPassThrough;
@@ -210,14 +318,78 @@ const valueFormat = computed(() => {
return props.valueFormat;
});
// 快捷选项索引
const shortcutsIndex = ref<number>(0);
// 快捷选项列表
const shortcuts = computed<ClSelectDateShortcut[]>(() => {
if (!isEmpty(props.shortcuts)) {
return props.shortcuts;
}
return [
{
label: t("今天"),
value: [dayUts().format(valueFormat.value), dayUts().format(valueFormat.value)]
},
{
label: t("近7天"),
value: [
dayUts().subtract(7, "day").format(valueFormat.value),
dayUts().format(valueFormat.value)
]
},
{
label: t("近30天"),
value: [
dayUts().subtract(30, "day").format(valueFormat.value),
dayUts().format(valueFormat.value)
]
},
{
label: t("近90天"),
value: [
dayUts().subtract(90, "day").format(valueFormat.value),
dayUts().format(valueFormat.value)
]
},
{
label: t("近一年"),
value: [
dayUts().subtract(1, "year").format(valueFormat.value),
dayUts().format(valueFormat.value)
]
}
];
});
// 范围值索引0为开始日期1为结束日期
const rangeIndex = ref<number>(0);
// 范围值,依次为开始日期、结束日期
const values = ref<string[]>(["", ""]);
// 当前选中的值,存储为数组,依次为年月日时分秒
const value = ref<number[]>([]);
// 开始日期
const start = computed(() => {
if (props.rangeable) {
if (rangeIndex.value == 0) {
return props.start;
} else {
return values.value[0];
}
} else {
return props.start;
}
});
// 时间选择器列表,动态生成每一列的选项
const list = computed(() => {
// 解析开始日期为年月日时分秒数组
const [startYear, startMonth, startDate, startHour, startMinute, startSecond] = dayUts(
props.start
start.value
).toArray();
// 解析结束日期为年月日时分秒数组
const [endYear, endMonth, endDate, endHour, endMinute, endSecond] = dayUts(props.end).toArray();
@@ -375,7 +547,13 @@ const text = ref("");
// 获取显示文本格式化为labelFormat格式
function getText() {
text.value = dayUts(toDate()).format(labelFormat.value);
if (props.rangeable) {
text.value = values.value
.map((e) => dayUts(e).format(labelFormat.value))
.join(` ${props.rangeSeparator} `);
} else {
text.value = dayUts(toDate()).format(labelFormat.value);
}
}
// 选择器值改变事件更新value
@@ -383,22 +561,88 @@ async function onChange(arr: number[]) {
for (let i = 0; i < arr.length; i++) {
value.value[i] = list.value[i][arr[i]].value as number;
}
// 设置范围值
if (props.rangeable) {
values.value[rangeIndex.value] = dayUts(toDate()).format(valueFormat.value);
// 判断开始日期是否大于结束日期
if (dayUts(values.value[0]).isAfter(dayUts(values.value[1])) && values.value[1] != "") {
values.value[1] = values.value[0];
}
// 重置快捷选项索引
shortcutsIndex.value = -1;
}
}
// 设置value
function setValue(val: string) {
// 如果值为空,使用当前时间
if (isNull(val) || isEmpty(val)) {
value.value = dayUts().toArray();
} else {
// 否则解析为数组
value.value = dayUts(val).toArray();
getText();
}
}
// 设置values
function setValues(val: string[]) {
if (isEmpty(val)) {
values.value = ["", ""];
} else {
values.value = val;
}
}
// 设置范围值索引
function setRange(index: number) {
rangeIndex.value = index;
setValue(values.value[index]);
}
// 设置范围值
function setRangeValue(val: string[], index: number) {
shortcutsIndex.value = index;
values.value = [...val] as string[];
setValue(val[rangeIndex.value]);
}
// 选择器显示状态控制popup显示
const visible = ref(false);
// 选择回调函数
let callback: ((value: string) => void) | null = null;
let callback: ((value: string | string[]) => void) | null = null;
// 打开选择器
function open(cb: ((value: string) => void) | null = null) {
function open(cb: ((value: string | string[]) => void) | null = null) {
// 如果组件被禁用,则不执行后续操作,直接返回
if (props.disabled) {
return;
}
// 显示选择器弹窗
visible.value = true;
// 保存回调函数
callback = cb;
nextTick(() => {
if (props.rangeable) {
// 如果是范围选择,初始化为选择开始时间
rangeIndex.value = 0;
// 设置范围值
setValues(props.values);
// 设置当前选中的值为范围的开始值
setValue(values.value[0]);
} else {
// 非范围选择设置当前选中的值为modelValue
setValue(props.modelValue);
}
});
}
// 关闭选择器设置visible为false
@@ -406,24 +650,69 @@ function close() {
visible.value = false;
}
// 选择器关闭后
function onClosed() {
value.value = [];
values.value = [];
}
// 清空选择器,重置显示文本并触发事件
function clear() {
text.value = "";
emit("update:modelValue", "");
emit("change", "");
if (props.rangeable) {
emit("update:values", [] as string[]);
emit("range-change", [] as string[]);
} else {
emit("update:modelValue", "");
emit("change", "");
}
}
// 确认选择,触发事件并关闭选择器
function confirm() {
const val = dayUts(toDate()).format(valueFormat.value);
if (props.rangeable) {
const [a, b] = values.value;
// 触发更新事件
emit("update:modelValue", val);
emit("change", val);
if (a == "" || b == "") {
ui.showToast({
message: t("请选择完整时间范围")
});
// 触发回调
if (callback != null) {
callback!(val);
if (a != "") {
rangeIndex.value = 1;
}
return;
}
if (dayUts(a).isAfter(dayUts(b))) {
ui.showToast({
message: t("开始日期不能大于结束日期")
});
return;
}
// 触发更新事件
emit("update:values", values.value);
emit("range-change", values.value);
// 触发回调
if (callback != null) {
callback!(values.value as string[]);
}
} else {
const val = dayUts(toDate()).format(valueFormat.value);
// 触发更新事件
emit("update:modelValue", val);
emit("change", val);
// 触发回调
if (callback != null) {
callback!(val);
}
}
// 更新显示文本
@@ -433,18 +722,22 @@ function confirm() {
close();
}
// 监听modelValue变化初始化或更新value
// 监听modelValue变化
watch(
computed(() => props.modelValue),
(val: string) => {
// 如果值为空,使用当前时间
if (isNull(val) || isEmpty(val)) {
value.value = dayUts().toArray();
} else {
// 否则解析为数组
value.value = dayUts(val).toArray();
getText();
}
setValue(val);
},
{
immediate: true
}
);
// 监听values变化
watch(
computed(() => props.values),
(val: string[]) => {
setValues(val);
},
{
immediate: true
@@ -453,7 +746,14 @@ watch(
defineExpose({
open,
close
close,
setValue,
setValues,
clear,
setRange,
setRangeValue,
toDate,
confirm
});
</script>
@@ -464,6 +764,32 @@ defineExpose({
@apply flex flex-row items-center justify-center;
padding: 24rpx;
}
&__range {
@apply px-3 pt-2 pb-5;
&-values {
@apply flex flex-row items-center justify-center;
&-start,
&-end {
@apply flex-1 bg-surface-50 rounded-xl border border-solid border-surface-200;
@apply py-2;
&.is-dark {
@apply border-surface-500 bg-surface-700;
}
&.active {
@apply border-primary-500 bg-transparent;
}
}
}
&-shortcuts {
@apply flex flex-row flex-wrap items-center mb-4;
}
}
}
}
</style>