基于element-Plus时间日期选择器组件封装

在vue3项目中封装时间日期选择器功能,主要实现开始结束日期选择功能,支持限制可选日期范围最大时长不超过当前时间,便于页面功能开发。

  1. 插件地址:element-plus日期选择器
  2. 引用插件:dayjs
  3. 功能使用需先按需引入或全局配置element-plus插件,建议使用按需引入避免文件包过大。

国际化功能导入

直接使用element-plus组件时,组件的placeholder及按钮是英文,所以需要引入国际化功能来手动全局配置,如果不需要国际化功能可跳过此步骤直接组件封装。

  1. 新建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;
  1. 在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进行参数配置,日期选择及范围处理等都在组件内部进行,只对外暴露回调方法即可。

  1. 内部现在提供三种日期类型,如有其他需要可按需添加即可。
  2. class名:custom-date-picker为自定义class,在全局scss文件内部可根据ui图自定义全局公共样式,避免在组件内部切换class,如需特殊处理可在父组件内通过:deep进行样式穿透处理。
  3. 下述组件可直接复制使用。
<!--
 * 时间日期范围选择器组件
 * 提供开始和结束时间、支持日期、时间、日期时间三种插件类型(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、开始结束时间绑定参数不一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值