最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

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

先看效果

核心代碼解析

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

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

<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)在輸入框正下方,我們計算了彈窗的絕對位置:

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

彈窗樣式中,我們加入了一個旋轉(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. 交互邏輯

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

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

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

后期優(yōu)化計劃

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

1.支持自定義日期格式

通過新增 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; // 僅在未打開時才打開下拉框
    nextTick(() => {
      scrollToSelected(monthRefs, tempSelectedMonth.value - 1);
      scrollToSelected(dayRefs, tempSelectedDay.value - 1);
    });
  }
};

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

const handleBlur = e => {
  // 監(jiān)聽popover的blur事件
  const popover = e.target;
  setTimeout(() => {
    // 如果失去焦點且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);

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

// 每次顯示彈窗時都更新位置
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實現(xiàn)自定義日期選擇器的文章就介紹到這了,更多相關(guān)Element Plus日期選擇器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

南丹县| 大化| 乌苏市| 广昌县| 拉萨市| 民乐县| 东丽区| 临湘市| 高雄县| 柳江县| 新乡市| 镇康县| 瑞金市| 东城区| 安国市| 铜陵市| 海南省| 左权县| 芜湖市| 含山县| 仙桃市| 广南县| 廊坊市| 肇源县| 遵化市| 来凤县| 全州县| 青田县| 富阳市| 新沂市| 南开区| 怀来县| 玉屏| 利川市| 微山县| 府谷县| 麻栗坡县| 柏乡县| 云阳县| 乐安县| 克山县|