在vue3项目中封装时间日期选择器功能,主要实现开始结束日期选择功能,支持限制可选日期范围最大时长不超过当前时间,便于页面功能开发。
- 插件地址:element-plus日期选择器
- 引用插件:dayjs
- 功能使用需先按需引入或全局配置element-plus插件,建议使用按需引入避免文件包过大。
国际化功能导入
直接使用element-plus组件时,组件的placeholder及按钮是英文,所以需要引入国际化功能来手动全局配置,如果不需要国际化功能可跳过此步骤直接组件封装。
- 新建src/utils/elementLocale.ts
/**
* elementplus 国际化功能接入
* 按需引入,自定义要修改参数中文
* 通过Config Provider 全局配置,设置ElConfigProvider包装根组件
* 仅修改必要的字段,避免覆盖内部方法
*/
import zhCn from "element-plus/es/locale/lang/zh-cn";
export const customZhCn = {
...zhCn,
el: {
...zhCn.el,
datepicker: {
...zhCn.el.datepicker,
placeholder: "选择日期",
startPlaceholder: "开始日期",
endPlaceholder: "结束日期",
confirm: "确定",
clear: "清空",
now: "此刻",
today: "今天",
},
pagination: {
...zhCn.el.pagination,
goto: "前往",
pagesize: "条/页",
total: `共 {total} 条`,
pageClassifier: "页",
},
table: {
...zhCn.el.table,
emptyText: "暂无数据",
},
},
};
// 直接导出语言对象
export default customZhCn;
- 在app.vue页面引入方法
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { RouterView } from "vue-router";
import locale from "@/utils/elementLocale";
</script>
上述国际化导入后npm重启项目即可看到效果,elementLocale.ts文件内部现在配置为分页、表格、日期选择器,如有其他需要可按需添加。
组件封装
新建src/components/dateRangePicker/index.vue文件,通过props进行参数配置,日期选择及范围处理等都在组件内部进行,只对外暴露回调方法即可。
- 内部现在提供三种日期类型,如有其他需要可按需添加即可。
- class名:custom-date-picker为自定义class,在全局scss文件内部可根据ui图自定义全局公共样式,避免在组件内部切换class,如需特殊处理可在父组件内通过:deep进行样式穿透处理。
- 下述组件可直接复制使用。
<!--
* 时间日期范围选择器组件
* 提供开始和结束时间、支持日期、时间、日期时间三种插件类型(type)
* 支持限制可选日期范围(rangeDays)
* 支持禁用未来日期,默认当前时间
* 默认逻辑:开始时间不能小于结束时间,结束时间不能小于开始时间,年月日时分秒都做限制
-->
<template>
<div class="date-range-picker">
<el-date-picker
class="custom-date-picker"
v-model="startTime"
:type="type"
:value-format="valueFormat"
:format="displayFormat"
:placeholder="startPlaceholder"
:show-now="isShowNow"
:disabled-date="startDateDisabled"
:disabled-hours="startHoursDisabled"
:disabled-minutes="startMinutesDisabled"
:disabled-seconds="startSecondsDisabled"
@change="handleStartChange"
/>
<el-date-picker
class="custom-date-picker noneM"
v-model="endTime"
:type="type"
:value-format="valueFormat"
:format="displayFormat"
:placeholder="endPlaceholder"
:show-now="isShowNow"
:disabled-date="endDateDisabled"
:disabled-hours="endHoursDisabled"
:disabled-minutes="endMinutesDisabled"
:disabled-seconds="endSecondsDisabled"
@change="handleEndChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { dayjs } from "element-plus";
defineOptions({
name: "DateRangePickerPage",
});
// 显示类型
type PickerType = "date" | "datetime" | "time";
const props = withDefaults(
defineProps<{
// 时间日期类型
type?: PickerType;
// 开始时间
start: string;
// 结束时间
end: string;
// 绑定值格式
valueFormat?: string;
// 输入框显示格式
format?: string;
// 最大允许日期,不传值默认当天
maxDate?: Date | string | number;
// 日期范围天数限制
rangeDays?: number;
// 开始时间占位符
startPlaceholder?: string;
// 结束时间占位符
endPlaceholder?: string;
// 是否显示此刻按钮
isShowNow?: boolean;
}>(),
{
type: "datetime",
start: "",
end: "",
valueFormat: "YYYY-MM-DD HH:mm:ss",
format: "YYYY-MM-DD HH:mm:ss",
rangeDays: 7,
startPlaceholder: "请选择开始时间",
endPlaceholder: "请选择结束时间",
isShowNow: false,
}
);
// 组件传值事件
const emit = defineEmits<{
(e: "update:start", value: string): void;
(e: "update:end", value: string): void;
(e: "change", { start, end }: { start: string; end: string }): void;
}>();
// 开始结束时间
const startTime = ref<string>(props.start);
const endTime = ref<string>(props.end);
// 根据传递参数计算最大日期
const maxDateValue = computed<Date>(() => {
if (props.maxDate instanceof Date) return props.maxDate;
if (typeof props.maxDate === "string") return new Date(props.maxDate);
if (typeof props.maxDate === "number") return new Date(props.maxDate);
return new Date();
});
// 根据组件类型判断显示格式
const displayFormat = computed(() => {
if (props.type === "date") return props.format.split(" ")[0] || "YYYY-MM-DD";
if (props.type === "time") return props.format.split(" ")[1] || "HH:mm:ss";
return props.format;
});
// 时间选择变化处理
// dayjs方法:isValid()是否为空值,
// isAfter()是否开始日期在结束日期之后,isBefore()是否开始日期在结束日期之前
const handleTimeChange = (newValue: string, isStart: boolean) => {
// 更新当前选中时间
if (isStart) {
startTime.value = newValue;
emit("update:start", newValue);
} else {
endTime.value = newValue;
emit("update:end", newValue);
}
// 判断开始还是结束时间
const oppositeTimeRef = isStart ? endTime : startTime;
// 如果两个时间都有值检查是否需要重置另一个时间
if (newValue && oppositeTimeRef.value) {
const current = dayjs(newValue);
const opposite = dayjs(oppositeTimeRef.value);
// 避免开始时间小于结束时间或相反
if (current.isValid() && opposite.isValid()) {
const shouldReset = isStart ? current.isAfter(opposite) : current.isBefore(opposite);
if (shouldReset) {
oppositeTimeRef.value = "";
if (isStart) {
emit("update:end", "");
} else {
emit("update:start", "");
}
}
}
}
emit("change", { start: startTime.value, end: endTime.value });
};
// 开始时间变化处理
const handleStartChange = (val: string) => {
handleTimeChange(val, true);
};
// 结束时间变化处理
const handleEndChange = (val: string) => {
handleTimeChange(val, false);
};
// 日期禁用(只比较日期部分,避免时分秒选择后当天不可选中)
// date:当前选中日期;isStartDate:是否是开始时间
const disabledDate = (date: Date, isStartDate: boolean): boolean => {
// 获取当前日期(年月日)
const current = dayjs(date).startOf("day");
// 获取最大允许日期超出禁用
const max = dayjs(maxDateValue.value).startOf("day");
if (current.isAfter(max)) return true;
// 获取对应的另一个日期值(选开始对应结束、如果没有不做限制)
const counterpartValue = isStartDate ? endTime.value : startTime.value;
if (!counterpartValue) return false;
const counterpartDate = dayjs(counterpartValue).startOf("day");
if (isStartDate) {
// 计算允许的最早开始日期
const minStartDate = counterpartDate.subtract(props.rangeDays - 1, "day");
return current.isBefore(minStartDate) || current.isAfter(counterpartDate);
} else {
// 计算允许的最晚结束日期
const maxEndDate = counterpartDate.add(props.rangeDays - 1, "day");
return current.isBefore(counterpartDate) || current.isAfter(maxEndDate);
}
};
// 开始日期禁用
const startDateDisabled = (date: Date) => disabledDate(date, true);
// 结束日期禁用
const endDateDisabled = (date: Date) => disabledDate(date, false);
// 时分秒禁用(值比较时间区间)
// isStartTime: 是否开始时间;level: 时分秒区分;
// selectedHour:已选择的小时;selectedMinute:已选择的分钟
const generateDisabledTime = (
isStartTime: boolean,
level: "hour" | "minute" | "second",
selectedHour?: number,
selectedMinute?: number
): number[] => {
// 获取当前控件对应的另一个时间值,如果对应控件没选择或无值不限制
const counterpartValue = isStartTime ? endTime.value : startTime.value;
if (!counterpartValue) return [];
// 获取时间对象
const counterpart = dayjs(counterpartValue);
// 检查两个时间是否在同一日期,如果不在同一天,不需要限制时间
if (!dayjs(isStartTime ? startTime.value : endTime.value).isSame(counterpart, "day")) {
return [];
}
// 动态判断当前时间是否被禁用
const isValueInvalid = (value: number, counterpartValue: number) =>
isStartTime ? value > counterpartValue : value < counterpartValue;
// 根据时分秒区分,禁用比开始、结束时间晚、早的时分秒
switch (level) {
case "hour":
return Array.from({ length: 24 }, (_, i) => i).filter((hour) =>
isValueInvalid(hour, counterpart.hour())
);
case "minute":
// 确保在同一小时才限制分钟
if (selectedHour !== counterpart.hour()) return [];
return Array.from({ length: 60 }, (_, i) => i).filter((minute) =>
isValueInvalid(minute, counterpart.minute())
);
case "second":
// 确保在同一小时和同一分钟才限制秒
if (selectedHour !== counterpart.hour() || selectedMinute !== counterpart.minute()) {
return [];
}
return Array.from({ length: 60 }, (_, i) => i).filter((second) =>
isValueInvalid(second, counterpart.second())
);
default:
return [];
}
};
// 开始时间时分秒禁用
const startHoursDisabled = () => generateDisabledTime(true, "hour");
const startMinutesDisabled = (hour: number) => generateDisabledTime(true, "minute", hour);
const startSecondsDisabled = (hour: number, minute: number) =>
generateDisabledTime(true, "second", hour, minute);
// 结束时间时分秒禁用
const endHoursDisabled = () => generateDisabledTime(false, "hour");
const endMinutesDisabled = (hour: number) => generateDisabledTime(false, "minute", hour);
const endSecondsDisabled = (hour: number, minute: number) =>
generateDisabledTime(false, "second", hour, minute);
// 暴露方法给父组件
defineExpose({
reset: () => {
startTime.value = "";
endTime.value = "";
},
getValues: () => ({
start: startTime.value,
end: endTime.value,
}),
});
</script>
<style lang="scss" scoped>
.date-range-picker {
display: flex;
align-items: center;
:deep(.noneM) {
margin-right: 0;
}
}
</style>
组件使用
项目内部vite.config.ts如果已经配置自动导入,在父页面使用时可不导入组件直接使用。
// html
<div class="serach">
<DateRangePicker
ref="roteDatePickerRef"
v-model:start="roteStartTime"
v-model:end="roteEndTime"
type="datetime"
:range-days="7"
:isShowNow="true"
/>
</div>
<script setup lang="ts">
// 组件导入(可不引入直接使用,需首字母大写的驼峰写法,同文件名保持一致)
import DateRangePicker from "@/components/dateRangePicker/index.vue";
// dom
const roteDatePickerRef = ref(null);
// 开始时间
const roteStartTime = ref<string>("");
// 结束时间
const roteEndTime = ref<string>("");
// 值
// console.log(roteStartTime.value, roteEndTime.value)
// 方法使用
// roteDatePickerRef.reset()
</script>
在其他父页面使用同上述代码一致,如果当前页面多次使用,需保证ref、开始结束时间绑定参数不一致。
1715

被折叠的 条评论
为什么被折叠?



