欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue+Element Plus實(shí)現(xiàn)自定義日期選擇器

 更新時(shí)間:2024年12月26日 10:13:23   作者:王六歲  
這篇文章主要為大家詳細(xì)介紹了如何基于Vue和Element Plus提供的現(xiàn)有組件,設(shè)計(jì)并實(shí)現(xiàn)了一個(gè)自定義的日期選擇器組件,感興趣的小伙伴可以參考一下

今天和大家分享一個(gè)基于 Vue 的日期選擇器組件開發(fā)實(shí)踐。由于 Element Plus 中并沒有單獨(dú)的月份和日期的選擇方式,所以我們基于 Vue 和 Element Plus 提供的現(xiàn)有組件,設(shè)計(jì)并實(shí)現(xiàn)了一個(gè)自定義的日期選擇器組件,支持單獨(dú)選擇月份和日期

先看效果

核心代碼解析

1. 彈窗結(jié)構(gòu)

我們使用了 Vue 的 teleport 功能,將彈窗元素直接掛載到 body,避免父級(jí)樣式干擾:

<template>
  <div class="el-month-day-select">
    <!-- 綁定輸入框 -->
    <div ref="inputElement">
      <el-input
        v-model="value"
        placeholder="月/日"
        readonly
        @focus="onFocus"
        @blur="onBlur"
        :suffix-icon="Calendar"
      />
    </div>

    <!-- 使用 teleport 將彈窗渲染到 body -->
    <teleport to="body">
      <div
        v-if="showDropdown"
        class="custom-popover"
        :style="popoverStyle"
        @blur="handleBlur"
        tabindex="-1"
      >
        <div class="picker-wrapper" @wheel.stop>
          <div class="picker">
            <!-- 月份選擇 -->
            <el-scrollbar class="scrollbar" @wheel.stop>
              <ul class="scroll-list">
                <li
                  v-for="month in months"
                  :key="month"
                  ref="monthRefs"
                  :class="{ selected: month === tempSelectedMonth }"
                  @click="tempSelectMonth(month)"
                >
                  {{ month.toString().padStart(2, "0") }} 月
                </li>
              </ul>
            </el-scrollbar>
          </div>
          <div class="picker">
            <!-- 天數(shù)選擇 -->
            <el-scrollbar class="scrollbar" @wheel.stop>
              <ul class="scroll-list">
                <li
                  v-for="day in days"
                  :key="day"
                  ref="dayRefs"
                  :class="{ selected: day === tempSelectedDay }"
                  @click="tempSelectDay(day)"
                >
                  {{ day.toString().padStart(2, "0") }} 日
                </li>
              </ul>
            </el-scrollbar>
          </div>
        </div>
        <!-- 按鈕操作 -->
        <div class="action-buttons">
          <el-button size="small" @click="cancel">取消</el-button>
          <el-button size="small" type="primary" @click="confirm"
            >確定</el-button
          >
        </div>
      </div>
    </teleport>
  </div>
</template>

2. 彈窗定位與樣式

為確保彈窗出現(xiàn)在輸入框正下方,我們計(jì)算了彈窗的絕對(duì)位置:

const updatePopoverPosition = () => {
  if (inputElement.value) {
    const inputRect = inputElement.value.getBoundingClientRect();
    popoverTop.value = inputRect.bottom + window.scrollY;
    popoverLeft.value = inputRect.left + window.scrollX;
  }
};

彈窗樣式中,我們加入了一個(gè)旋轉(zhuǎn) 45° 的尖角,并添加了陰影,提升視覺層次感:

.custom-popover {
  position: absolute;
  background: white;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  padding: 10px;
}

.custom-popover::after {
  content: '';
  position: absolute;
  top: -4px;
  left: 20px;
  width: 16px;
  height: 16px;
  background: white;
  transform: rotate(45deg); /* 旋轉(zhuǎn)尖角 */
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 10001;
}

3. 交互邏輯

我們通過(guò) focusblur 事件實(shí)現(xiàn)彈窗的顯示與關(guān)閉,同時(shí)確保組件內(nèi)部交互時(shí)彈窗不會(huì)意外關(guān)閉:

const onFocus = () => {
  showDropdown.value = true;
  nextTick(updatePopoverPosition);
};

const onBlur = () => {
  setTimeout(() => {
    if (!document.activeElement.closest(".custom-popover")) {
      showDropdown.value = false;
    }
  }, 100);
};

后期優(yōu)化計(jì)劃

目前組件默認(rèn)使用 "MM-DD" 的日期格式。后期優(yōu)化方向包括:

1.支持自定義日期格式

通過(guò)新增 format 屬性,允許開發(fā)者指定輸出格式,例如 MM-DD, DD/MM, MM月DD日 等。

示例代碼:

props: {
  format: {
    type: String,
    default: "MM-DD"
  }
},
const formatValue = (month, day) => {
  return props.format
    .replace("MM", month.toString().padStart(2, "0"))
    .replace("DD", day.toString().padStart(2, "0"));
};

完整代碼

<template>
  <div class="el-month-day-select">
    <!-- 綁定輸入框 -->
    <div ref="inputElement">
      <el-input
        v-model="value"
        placeholder="月/日"
        readonly
        @focus="onFocus"
        @blur="onBlur"
        :suffix-icon="Calendar"
      />
    </div>

    <!-- 使用 teleport 將彈窗渲染到 body -->
    <teleport to="body">
      <div
        v-if="showDropdown"
        class="custom-popover"
        :style="popoverStyle"
        @blur="handleBlur"
        tabindex="-1"
      >
        <div class="picker-wrapper" @wheel.stop>
          <div class="picker">
            <!-- 月份選擇 -->
            <el-scrollbar class="scrollbar" @wheel.stop>
              <ul class="scroll-list">
                <li
                  v-for="month in months"
                  :key="month"
                  ref="monthRefs"
                  :class="{ selected: month === tempSelectedMonth }"
                  @click="tempSelectMonth(month)"
                >
                  {{ month.toString().padStart(2, "0") }} 月
                </li>
              </ul>
            </el-scrollbar>
          </div>
          <div class="picker">
            <!-- 天數(shù)選擇 -->
            <el-scrollbar class="scrollbar" @wheel.stop>
              <ul class="scroll-list">
                <li
                  v-for="day in days"
                  :key="day"
                  ref="dayRefs"
                  :class="{ selected: day === tempSelectedDay }"
                  @click="tempSelectDay(day)"
                >
                  {{ day.toString().padStart(2, "0") }} 日
                </li>
              </ul>
            </el-scrollbar>
          </div>
        </div>
        <!-- 按鈕操作 -->
        <div class="action-buttons">
          <el-button size="small" @click="cancel">取消</el-button>
          <el-button size="small" type="primary" @click="confirm"
            >確定</el-button
          >
        </div>
      </div>
    </teleport>
  </div>
</template>

<script setup lang="ts">
import { Calendar } from "@element-plus/icons-vue";
const props = defineProps<{
  modelValue: string;
}>();

const emit = defineEmits<{
  "update:modelValue": [value: string];
}>();

const formatValue = (month, day) => {
  return `${month.toString().padStart(2, "0")}/${day
    .toString()
    .padStart(2, "0")}`;
};

const currentYear = new Date().getFullYear();

const showDropdown = ref(false);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const selectedMonth = ref(1);
const selectedDay = ref(1);
const tempSelectedMonth = ref(1);
const tempSelectedDay = ref(1);

const monthRefs = ref([]);
const dayRefs = ref([]);

const value = ref("");

const days = computed(() => {
  const daysInMonth = new Date(
    currentYear,
    tempSelectedMonth.value,
    0
  ).getDate();
  return Array.from({ length: daysInMonth }, (_, i) => i + 1);
});

watch(
  () => props.modelValue,
  newValue => {
    if (newValue) {
      const [month, day] = newValue.split("/").map(Number);
      selectedMonth.value = month || 1;
      selectedDay.value = day || 1;
      tempSelectedMonth.value = month || 1;
      tempSelectedDay.value = day || 1;
      value.value = formatValue(selectedMonth.value, selectedDay.value);
    } else {
      value.value = "";
    }
  },
  { immediate: true }
);

const scrollToSelected = (refs, index) => {
  nextTick(() => {
    const element = refs.value[index];
    if (element) {
      element.scrollIntoView({
        behavior: "smooth",
        block: "center"
      });
    }
  });
};

const tempSelectMonth = month => {
  tempSelectedMonth.value = month;
  scrollToSelected(monthRefs, month - 1);
};

const tempSelectDay = day => {
  tempSelectedDay.value = day;
  scrollToSelected(dayRefs, day - 1);
};

const close = () => {
  showDropdown.value = false; // 關(guān)閉彈窗
};

const confirm = () => {
  selectedMonth.value = tempSelectedMonth.value;
  selectedDay.value = tempSelectedDay.value;
  const formattedValue = formatValue(selectedMonth.value, selectedDay.value);
  emit("update:modelValue", formattedValue);
  value.value = formattedValue;
  close();
};

const cancel = () => {
  close();
};

const onFocus = () => {
  if (!showDropdown.value) {
    showDropdown.value = true; // 僅在未打開時(shí)才打開下拉框
    nextTick(() => {
      scrollToSelected(monthRefs, tempSelectedMonth.value - 1);
      scrollToSelected(dayRefs, tempSelectedDay.value - 1);
    });
  }
};

const onBlur = () => {
  setTimeout(() => {
    // 在失去焦點(diǎn)時(shí)關(guān)閉彈窗,給彈窗內(nèi)容一些時(shí)間渲染
    if (!document.activeElement.closest(".custom-popover")) {
      close();
    }
  }, 100); // 延時(shí)關(guān)閉,避免與其他操作沖突
};

const handleBlur = e => {
  // 監(jiān)聽popover的blur事件
  const popover = e.target;
  setTimeout(() => {
    // 如果失去焦點(diǎn)且popover沒有被重新激活,關(guān)閉彈窗
    if (!popover.contains(document.activeElement)) {
      close();
    }
  }, 200);
};

const inputElement = ref(null);
const popoverTop = ref(0);
const popoverLeft = ref(0);
const popoverZIndex = ref(10000);

// 計(jì)算彈窗的位置
const updatePopoverPosition = () => {
  if (inputElement.value) {
    const inputRect = inputElement.value.getBoundingClientRect();
    popoverTop.value = inputRect.bottom + window.scrollY + 18;
    popoverLeft.value = inputRect.left + window.scrollX;
  }
};

// 每次顯示彈窗時(shí)都更新位置
watch(
  () => showDropdown.value,
  newValue => {
    if (newValue) {
      updatePopoverPosition();
    }
  }
);

// 彈窗樣式
const popoverStyle = computed(() => ({
  top: `${popoverTop.value}px`,
  left: `${popoverLeft.value}px`,
  zIndex: popoverZIndex.value
}));

// 銷毀
onBeforeUnmount(() => {
  value.value = "";
});
</script>

<style scoped>
.custom-popover {
  position: absolute;
  background: white;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  width: 300px;
  padding: 10px;
  border-radius: 4px;
}

.custom-popover::after {
  content: "";
  position: absolute;
  top: -9px; /* 調(diào)整位置以匹配旋轉(zhuǎn)后的尖角 */
  left: 20px;
  width: 16px;
  height: 16px;
  background: white;
  transform: rotate(45deg); /* 旋轉(zhuǎn) 45 度 */
  border: 1px solid #e4e7ed;
  background: #fff;
  border-bottom-color: transparent !important;
  border-right-color: transparent !important;
  z-index: 10001;
}

.picker-wrapper {
  display: flex;
  justify-content: space-between;
}

.picker {
  width: 48%;
}

.scrollbar {
  max-height: 200px;
  overflow-y: auto;
}

.scroll-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.scroll-list li {
  padding: 5px;
  cursor: pointer;
}

.scroll-list li.selected {
  background-color: #e6f7ff;
}

.action-buttons {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
}

.el-month-day-select {
  position: relative;
  display: inline-block;
}
.picker-wrapper {
  display: flex;
  gap: 10px;
  margin-bottom: 10px;
}
.picker {
  flex: 1;
  width: 48%;
  height: 200px;
  overflow: hidden;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  position: relative;
}
.scrollbar {
  height: 100%;
  overflow: hidden;
}
.scroll-list {
  list-style: none;
  padding: 0;
  margin: 0;
  text-align: center;
}
.scroll-list li {
  padding: 10px 0;
  margin-right: 10px;
  cursor: pointer;
  transition: all 0.3s ease;
}
.scroll-list li.selected {
  color: #409eff;
  font-weight: bold;
  transform: scale(1.1);
}
.action-buttons {
  display: flex;
  justify-content: flex-end;
}
</style>

到此這篇關(guān)于Vue+Element Plus實(shí)現(xiàn)自定義日期選擇器的文章就介紹到這了,更多相關(guān)Element Plus日期選擇器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue component 中引入less文件報(bào)錯(cuò) Module build failed

    vue component 中引入less文件報(bào)錯(cuò) Module build failed

    這篇文章主要介紹了vue component 中引入less文件報(bào)錯(cuò) Module build failed的解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-04-04
  • Vue簡(jiǎn)明介紹配置對(duì)象的配置選項(xiàng)

    Vue簡(jiǎn)明介紹配置對(duì)象的配置選項(xiàng)

    我們知道每一個(gè)vue項(xiàng)目應(yīng)用都是通過(guò)vue的構(gòu)造函數(shù)進(jìn)行創(chuàng)建一個(gè)新的vue項(xiàng)目的。創(chuàng)建vue實(shí)例的配置對(duì)象,可以包括一下屬性選項(xiàng),比如:data、methods、watch、template等等,每一個(gè)選項(xiàng)都有不同的功能,大家可以根據(jù)自己的需求選擇不同的配置
    2022-08-08
  • vue中實(shí)現(xiàn)子組件相互切換且數(shù)據(jù)不丟失的策略詳解

    vue中實(shí)現(xiàn)子組件相互切換且數(shù)據(jù)不丟失的策略詳解

    項(xiàng)目為數(shù)據(jù)報(bào)表,但是一個(gè)父頁(yè)面中有很多的子頁(yè)面,而且子頁(yè)面中不是相互關(guān)聯(lián),但是數(shù)據(jù)又有聯(lián)系,所以本文給大家介紹了vue中如何實(shí)現(xiàn)子組件相互切換,而且數(shù)據(jù)不會(huì)丟失,并有詳細(xì)的代碼供大家參考,需要的朋友可以參考下
    2024-03-03
  • vue中實(shí)現(xiàn)點(diǎn)擊變成全屏的多種方法

    vue中實(shí)現(xiàn)點(diǎn)擊變成全屏的多種方法

    這篇文章主要介紹了vue中實(shí)現(xiàn)點(diǎn)擊變成全屏的多種方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-09-09
  • Vue mixin實(shí)現(xiàn)組件功能復(fù)用示例詳解

    Vue mixin實(shí)現(xiàn)組件功能復(fù)用示例詳解

    這篇文章主要為大家介紹了Vue mixin實(shí)現(xiàn)組件功能復(fù)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • vue3如何使用ref獲取元素

    vue3如何使用ref獲取元素

    這篇文章主要介紹了vue3如何使用ref獲取元素,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • Vue開發(fā)實(shí)現(xiàn)吸頂效果的示例代碼

    Vue開發(fā)實(shí)現(xiàn)吸頂效果的示例代碼

    這篇文章主要介紹了Vue開發(fā)實(shí)現(xiàn)吸頂效果的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-08-08
  • Vue3實(shí)現(xiàn)動(dòng)態(tài)路由與手動(dòng)導(dǎo)航

    Vue3實(shí)現(xiàn)動(dòng)態(tài)路由與手動(dòng)導(dǎo)航

    這篇文章主要為大家詳細(xì)介紹了如何在?Vue?3?中實(shí)現(xiàn)動(dòng)態(tài)路由注冊(cè)和手動(dòng)導(dǎo)航,確保用戶訪問的頁(yè)面與權(quán)限對(duì)應(yīng),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-12-12
  • Element-Plus Select組件實(shí)現(xiàn)滾動(dòng)分頁(yè)加載功能

    Element-Plus Select組件實(shí)現(xiàn)滾動(dòng)分頁(yè)加載功能

    Element-Plus的select組件并沒有自帶滾動(dòng)分頁(yè)加載的功能,其雖然提供了自定義下拉菜單的底部的方式可以自定義上一頁(yè)及下一頁(yè)操作按鈕的方式進(jìn)行分頁(yè)加載切換,這篇文章主要介紹了Element-Plus Select組件實(shí)現(xiàn)滾動(dòng)分頁(yè)加載功能,需要的朋友可以參考下
    2024-03-03
  • vue?按需引入vant跟全局引入方式

    vue?按需引入vant跟全局引入方式

    這篇文章主要介紹了vue?按需引入vant跟全局引入方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-10-10

最新評(píng)論