Kaynağa Gözat

cl-select-date 支持范围选择

icssoa 8 ay önce
ebeveyn
işleme
524a8239ad

+ 360 - 34
uni_modules/cool-ui/components/cl-select-date/cl-select-date.uvue

@@ -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;
+
+		if (a == "" || b == "") {
+			ui.showToast({
+				message: t("请选择完整时间范围")
+			});
+
+			if (a != "") {
+				rangeIndex.value = 1;
+			}
 
-	// 触发更新事件
-	emit("update:modelValue", val);
-	emit("change", val);
+			return;
+		}
+
+		if (dayUts(a).isAfter(dayUts(b))) {
+			ui.showToast({
+				message: t("开始日期不能大于结束日期")
+			});
+
+			return;
+		}
 
-	// 触发回调
-	if (callback != null) {
-		callback!(val);
+		// 触发更新事件
+		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>