Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-select-date/cl-select-date.uvue

874 lines
19 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"
arrow-icon="calendar-line"
@open="open()"
@clear="clear"
></cl-select-trigger>
<cl-popup ref="popupRef" v-model="visible" :title="title" :pt="ptPopup" @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.length > 0 && 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.length > 1 && 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"
:value="indexes"
:columns="columns"
:reset-on-change="false"
@change-value="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, nextTick } from "vue";
import type { ClSelectDateShortcut, ClSelectOption } from "../../types";
import { dayUts, isDark, 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";
import { useUi } from "../../hooks";
import { config } from "../../config";
defineOptions({
name: "cl-select-date"
});
// 组件属性定义
const props = defineProps({
// 透传样式配置,支持外部自定义样式
pt: {
type: Object,
default: () => ({})
},
// 选择器的值外部v-model绑定
modelValue: {
type: String,
default: ""
},
// 选择器的范围值外部v-model:values绑定
values: {
type: Array as PropType<string[]>,
default: () => []
},
// 表头
headers: {
type: Array as PropType<string[]>,
default: () => [t("年"), t("月"), t("日"), t("时"), t("分"), t("秒")]
},
// 选择器标题
title: {
type: String,
default: () => t("请选择")
},
// 选择器占位符
placeholder: {
type: String,
default: () => t("请选择")
},
// 是否显示选择器触发器
showTrigger: {
type: Boolean,
default: true
},
// 是否禁用选择器
disabled: {
type: Boolean,
default: false
},
// 确认按钮文本
confirmText: {
type: String,
default: () => t("确定")
},
// 是否显示确认按钮
showConfirm: {
type: Boolean,
default: true
},
// 取消按钮文本
cancelText: {
type: String,
default: () => t("取消")
},
// 是否显示取消按钮
showCancel: {
type: Boolean,
default: true
},
// 标签格式化
labelFormat: {
type: String as PropType<string>,
default: ""
},
// 值格式化
valueFormat: {
type: String as PropType<string>,
default: ""
},
// 开始日期
start: {
type: String,
default: config.startDate
},
// 结束日期
end: {
type: String,
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: () => []
}
});
// 定义事件
const emit = defineEmits(["update:modelValue", "change", "update:values", "range-change"]);
const ui = useUi();
// 弹出层引用用于控制popup的显示与隐藏
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 formatType = computed(() => {
switch (props.type) {
case "year":
return "YYYY";
case "month":
return "YYYY-MM";
case "date":
return "YYYY-MM-DD";
case "hour":
case "minute":
case "second":
return "YYYY-MM-DD HH:mm:ss";
default:
return "YYYY-MM-DD HH:mm:ss";
}
});
// 标签格式化
const labelFormat = computed(() => {
if (isNull(props.labelFormat) || isEmpty(props.labelFormat)) {
return formatType.value;
}
return props.labelFormat;
});
// 值格式化
const valueFormat = computed(() => {
if (isNull(props.valueFormat) || isEmpty(props.valueFormat)) {
return formatType.value;
}
return props.valueFormat;
});
// 快捷选项索引
const shortcutsIndex = ref<number>(-1);
// 快捷选项列表
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(
start.value
).toArray();
// 解析结束日期为年月日时分秒数组
const [endYear, endMonth, endDate, endHour, endMinute, endSecond] = dayUts(props.end).toArray();
// 初始化年月日时分秒六个选项数组
const arr = [[], [], [], [], [], []] as ClSelectOption[][];
// 边界处理如果value为空返回空数组
if (isEmpty(value.value)) {
return arr;
}
// 获取当前选中的年月日时分秒值
const [year, month, date, hour, minute] = value.value;
// 判断是否为闰年
const isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
// 根据月份和是否闰年获取当月天数
const days = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][
month > 0 ? month - 1 : 0
];
// 计算年份范围确保至少有60年可选
const yearRange = Math.max(60, endYear - startYear + 1);
// 遍历生成年月日时分秒的选项
for (let i = 0; i < yearRange; i++) {
// 计算当前遍历的年份
const yearNum = startYear + i;
// 如果年份在结束年份范围内,添加到年份选项
if (yearNum <= endYear) {
arr[0].push({
label: yearNum.toString(),
value: yearNum
});
}
// 处理月份选项
let monthNum = startYear == year ? startMonth + i : i + 1;
let endMonthNum = endYear == year ? endMonth : 12;
// 添加有效的月份选项
if (monthNum <= endMonthNum) {
arr[1].push({
label: monthNum.toString().padStart(2, "0"),
value: monthNum
});
}
// 处理日期选项
let dateNum = startYear == year && startMonth == month ? startDate + i : i + 1;
let endDateNum = endYear == year && endMonth == month ? endDate : days;
// 添加有效的日期选项
if (dateNum <= endDateNum) {
arr[2].push({
label: dateNum.toString().padStart(2, "0"),
value: dateNum
});
}
// 处理小时选项
let hourNum =
startYear == year && startMonth == month && startDate == date ? startHour + i : i;
let endHourNum = endYear == year && endMonth == month && endDate == date ? endHour : 24;
// 添加有效的小时选项
if (hourNum < endHourNum) {
arr[3].push({
label: hourNum.toString().padStart(2, "0"),
value: hourNum
});
}
// 处理分钟选项
let minuteNum =
startYear == year && startMonth == month && startDate == date && startHour == hour
? startMinute + i
: i;
let endMinuteNum =
endYear == year && endMonth == month && endDate == date && endHour == hour
? endMinute
: 60;
// 添加有效的分钟选项
if (minuteNum < endMinuteNum) {
arr[4].push({
label: minuteNum.toString().padStart(2, "0"),
value: minuteNum
});
}
// 处理秒钟选项
let secondNum =
startYear == year &&
startMonth == month &&
startDate == date &&
startHour == hour &&
startMinute == minute
? startSecond + i
: i;
let endSecondNum =
endYear == year &&
endMonth == month &&
endDate == date &&
endHour == hour &&
endMinute == minute
? endSecond
: 60;
// 添加有效的秒钟选项
if (secondNum < endSecondNum) {
arr[5].push({
label: secondNum.toString().padStart(2, "0"),
value: secondNum
});
}
}
// 返回包含所有时间选项的数组
return arr;
});
// 列数,决定显示多少列(年、月、日、时、分、秒)
const columnNum = computed(() => {
return (
["year", "month", "date", "hour", "minute", "second"].findIndex((e) => e == props.type) + 1
);
});
// 列数据,取出需要显示的列
const columns = computed(() => {
return list.value.slice(0, columnNum.value);
});
// 当前选中项的索引,返回每一列当前选中的下标
const indexes = computed(() => {
// 如果当前值为空,返回空数组
if (isEmpty(value.value)) {
return [];
}
// 遍历每一列,查找当前值在选项中的下标
return value.value.map((e, i) => {
let index = list.value[i].findIndex((a) => a.value == e) as number;
// 如果未找到,返回最后一个
if (index == -1) {
index = list.value[i].length - 1;
}
// 如果小于0返回0
if (index < 0) {
index = 0;
}
return index;
});
});
// 将当前选中的年月日时分秒拼接为字符串
function toDate() {
// 使用数组存储日期时间各部分,避免重复字符串拼接
const parts: string[] = [];
// 月日时分秒需要补0对齐
const units = ["", "-", "-", " ", ":", ":"];
// 默认值
const defaultValue = [2000, 1, 1, 0, 0, 0];
// 遍历处理各个时间单位
units.forEach((key, i) => {
let val = value.value[i];
// 超出当前列数时,使用默认值
if (i >= columnNum.value) {
val = defaultValue[i];
}
// 拼接字符串并补0
parts.push(key + val.toString().padStart(2, "0"));
});
// 拼接所有部分返回
return parts.join("");
}
// 检查边界值
function checkDate(values: number[]): number[] {
if (values.length == 0) {
return values;
}
// 确保至少有6个元素缺失的用默认值填充
const checkedValues = [...values];
const defaultValues = [2000, 1, 1, 0, 0, 0];
for (let i = checkedValues.length; i < 6; i++) {
checkedValues.push(defaultValues[i]);
}
let [year, month, date, hour, minute, second] = checkedValues;
// 检查日期边界(根据年份和月份确定最大天数)
// 判断是否为闰年
const isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
// 每月天数数组2月根据闰年判断
const daysInMonth = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const maxDay = daysInMonth[month - 1];
if (date < 1) {
date = 1;
} else if (date > maxDay) {
date = maxDay;
}
// 检查小时边界 (0-23)
if (hour < 0) {
hour = 0;
} else if (hour > 23) {
hour = 23;
}
// 检查分钟边界 (0-59)
if (minute < 0) {
minute = 0;
} else if (minute > 59) {
minute = 59;
}
// 检查秒钟边界 (0-59)
if (second < 0) {
second = 0;
} else if (second > 59) {
second = 59;
}
return [year, month, date, hour, minute, second];
}
// 显示文本
const text = ref("");
// 更新文本内容
function updateText() {
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
async function onChange(data: number[]) {
// 更新value
value.value = checkDate(data);
// 不能大于结束日期
if (dayUts(toDate()).isAfter(dayUts(props.end))) {
value.value = dayUts(props.end).toArray();
}
// 不能小于开始日期
if (dayUts(toDate()).isBefore(dayUts(props.start))) {
value.value = dayUts(props.start).toArray();
}
// 设置范围值
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 = checkDate(dayUts().toArray());
text.value = "";
} else {
// 否则解析为数组
value.value = checkDate(dayUts(val).toArray());
updateText();
}
}
// 设置values
function setValues(val: string[]) {
if (isEmpty(val)) {
values.value = ["", ""];
text.value = "";
} else {
values.value = val;
updateText();
}
}
// 设置范围值索引
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 | 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
function close() {
visible.value = false;
}
// 选择器关闭后
function onClosed() {
values.value = ["", ""];
}
// 清空选择器,重置显示文本并触发事件
function clear() {
text.value = "";
if (props.rangeable) {
emit("update:values", [] as string[]);
emit("range-change", [] as string[]);
} else {
emit("update:modelValue", "");
emit("change", "");
}
}
// 确认选择,触发事件并关闭选择器
function confirm() {
if (props.rangeable) {
const [a, b] = values.value;
if (a == "" || b == "") {
ui.showToast({
message: t("请选择完整时间范围")
});
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);
}
}
// 更新显示文本
updateText();
// 关闭选择器
close();
}
// 监听modelValue变化
watch(
computed(() => props.modelValue),
(val: string) => {
if (!props.rangeable) {
setValue(val);
}
},
{
immediate: true
}
);
// 监听values变化
watch(
computed(() => props.values),
(val: string[]) => {
if (props.rangeable) {
setValues(val);
}
},
{
immediate: true
}
);
// 更新显示文本
watch(
computed(() => props.labelFormat),
() => {
updateText();
}
);
defineExpose({
open,
close,
clear,
confirm,
setValue,
setValues,
setRange
});
</script>
<style lang="scss" scoped>
.cl-select {
&-popup {
&__op {
@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>