vue3+ts的日期选择器组件 可自行按需修改
效果图:
DatePicker.vue
<template>
<div class="date-picker" ref="refDatePicker">
<div>
<span>{{ frontText }}</span>
<input
class="result"
readonly
type="text"
:value="dateValue || modelValue"
@focus="showDropdown = true"
/>
</div>
<transition name="slide-up">
<div v-show="showDropdown" class="dropdown">
<!-- 日期面板 -->
<div v-show="showPanel === 'date'">
<header class="date-header">
<div class="l">
<i class="prev-year" @click="changeYearOrMonth('year', -1)"
><<</i
>
<i class="next-month" @click="changeYearOrMonth('month', -1)"
><</i
>
</div>
<div class="c">
<b class="year" @click="showYearPanel">{{ curYear }}</b>
<b @click="showPanel = 'month'">{{ MONTH_NAME[curMonth] }}</b>
<b @click="openHmPanel">{{ `${hms.hh}:${hms.mm}:${hms.ss}` }}</b>
</div>
<div class="r">
<i class="next-month" @click="changeYearOrMonth('month', 1)"
>></i
>
<i class="next-year" @click="changeYearOrMonth('year', 1)">
>>
</i>
</div>
</header>
<ul class="date-week">
<li v-for="item of WEEK_NAME" :key="item">
{{ item }}
</li>
</ul>
<ul class="date-list">
<li
v-for="item of dateList"
:key="item.value"
:data-unix="item.value"
:class="{
'no-current': item.type !== 'current',
active: item.value === selectedDay,
}"
@click="handleDayClick(item)"
>
{{ item.day }}
</li>
</ul>
</div>
<!-- 年份面板 -->
<ul v-show="showPanel === 'year'" class="year-panel">
<li
v-for="item of yearList"
:key="item"
:class="['year-panel-item', { active: curYear === item }]"
@click="
curYear = item;
showPanel = 'date';
"
>
{{ item }}
</li>
</ul>
<!-- 月份面板 -->
<ul v-show="showPanel === 'month'" class="month-panel">
<li
v-for="item of MONTH_NAME"
:key="item"
:class="[
'month-panel-item',
{ active: MONTH_NAME.indexOf(item) === curMonth },
]"
@click="
curMonth = MONTH_NAME.indexOf(item);
showPanel = 'date';
"
>
{{ item.slice(0, 3) }}
</li>
</ul>
<!-- 小时面板 -->
<!-- 分钟面板 -->
<div class="hms-panel" :style="foldHmPanel">
<div class="hms-ok" @click="showHmPanel = false">确认</div>
<p class="hms-value">{{ `${hms.hh}:${hms.mm}:${hms.ss}` }}</p>
<ul class="hms-change">
<li class="num-list" v-for="item in hms.insertEls" :key="item.type">
<div class="scroll" @click="chooseTime">
<li
v-for="(it, index) in item.count"
:id="item.type + index.toString()"
:data-value="index.toString().padStart(2, '0')"
:data-type="item.type"
:key="it"
:class="['num',index.toString().padStart(2, '0') == hms[item.type as keyof typeof hms]?'isactive':'']"
>
{{ index.toString().padStart(2, "0") }}
</li>
</div>
</li>
</ul>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import {
computed,
nextTick,
ref,
watch,
onBeforeUnmount,
defineProps,
defineEmits
} from "vue";
import { DateInfo } from "@/components/Map/types";
defineProps({
modelValue: { type: String, required: false },
frontText: { type: String, required: false },
});
const $emits = defineEmits(['update:modelValue'])
const refDatePicker = ref();
const currentDate = new Date();
const yearList = ref<number[]>([]); // 年份列表
const dateList = ref<DateInfo[]>([]);
const curYear = ref(currentDate.getFullYear());
const curMonth = ref(currentDate.getMonth());
// const curDay = ref(currentDate.getDate())
const showPanel = ref("date");
const showDropdown = ref(false);
const selectedDay = ref<number>();
const WEEK_NAME = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
const MONTH_NAME = [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
"12月",
];
// 监听鼠标是否点击外部
(() => {
const handler = (e: MouseEvent) => {
if (!refDatePicker.value.contains(e.target as Node))
showDropdown.value = false;
};
document.addEventListener("click", handler);
onBeforeUnmount(() => {
document.removeEventListener("click", handler);
});
})();
const hms = ref({
hh: "23",
mm: "59",
ss: "59",
insertEls: [
{
count: 24,
type: "hh",
},
{
count: 60,
type: "mm",
},
{
count: 60,
type: "ss",
},
],
});
const dateValue = computed<string>(() => {
if (!selectedDay.value) return "";
const d = new Date(selectedDay.value);
const y = d.getFullYear();
const m = (d.getMonth() + 1).toString().padStart(2, "0");
const day = d.getDate().toString().padStart(2, "0");
const currentValue = `${y}-${m}-${day} ${hms.value.hh}:${hms.value.mm}:${hms.value.ss}`
$emits("update:modelValue",currentValue)
return currentValue;
});
// 生成近100年的年份列表
(() => {
for (let i = 1970; i < curYear.value + 100; i++) {
yearList.value.push(i);
}
})();
// 获取传入的月份有多少天
const getDayLength = (year: number, month: number): number => {
return new Date(year, month + 1, 0).getDate();
};
// 设置日期列表
const setDateList = (year: number, month: number) => {
const curDays = getDayLength(year, month); // 当月天数
const prevDays = getDayLength(year, month - 1); // 上月天数
const curFirstDayWeek = new Date(year, month, 1).getDay(); // 当月第一天星期几
const list: DateInfo[] = [];
// 填充上月最后几天
for (let i = prevDays - curFirstDayWeek + 1; i <= prevDays; i++) {
list.push({
day: i,
value: +new Date(year, month - 1, i),
isRange: false,
isSelected: false,
type: "prev",
});
}
// 填充当月
for (let i = 1; i <= curDays; i++) {
list.push({
day: i,
value: +new Date(year, month, i),
isRange: false,
isSelected: false,
type: "current",
});
}
const nextDays = 7 - (list.length % 7);
if (nextDays !== 7) {
// 填充下月
for (let i = 1; i <= nextDays; i++) {
list.push({
day: i,
value: +new Date(year, month + 1, i),
isRange: false,
isSelected: false,
type: "next",
});
}
}
dateList.value = list;
};
watch(
[curYear, curMonth],
() => {
setDateList(curYear.value, curMonth.value);
},
{ immediate: true }
);
// 点击日期
const handleDayClick = (item: DateInfo) => {
selectedDay.value = item.value;
if (item.type !== "current") {
curMonth.value =
item.type === "prev" ? curMonth.value - 1 : curMonth.value + 1;
}
showDropdown.value = false;
};
// 切换年月
const changeYearOrMonth = (type: "year" | "month", num: number) => {
if (type === "year") {
curYear.value += num;
} else {
let month = curMonth.value + num;
if (month > 11) {
month = 0;
curYear.value++;
} else if (month < 0) {
month = 11;
curYear.value--;
}
curMonth.value = month;
}
};
// 显示年列表面板
const showYearPanel = () => {
showPanel.value = "year";
nextTick(() => {
(
document.querySelector(".year-panel-item.active") as HTMLElement
).scrollIntoView({ block: "center" });
});
};
const showHmPanel = ref(false);
const foldHmPanel = computed(() => {
let width = showHmPanel.value ? "100%" : "0";
return { width };
});
const openHmPanel = () => {
let activeEl = document.querySelectorAll(".hms-change .isactive") as any;
let count = 0;
const initScrollIntoView = () => {
if (count > activeEl.length) return;
activeEl[count].scrollIntoView({ behavior: "smooth" });
count++;
setTimeout(initScrollIntoView, Number(activeEl[count].dataset.value) * 16);
};
initScrollIntoView();
showHmPanel.value = !showHmPanel.value;
};
const chooseTime = (e: any) => {
if (e.target.nodeName == "LI") {
let { value, type } = e.target.dataset;
hms.value[type as keyof typeof hms.value] = value;
e.target.scrollIntoView({ behavior: "smooth" });
}
};
</script>
<style lang="less" scoped>
.date-picker {
position: relative;
height: 0.375rem;
margin: 0 auto;
width: max-content;
}
.date-picker .result {
width: 2.75rem;
box-sizing: border-box;
margin: 0 0 0 .05rem;
color: #000000d9;
font-size: 0.175rem;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
padding: 0.05rem 0.1375rem;
position: relative;
display: inline-flex;
align-items: center;
background: #fff;
border: 0.0125rem solid #d9d9d9;
border-radius: 0.025rem;
transition: border 0.3s, box-shadow 0.3s;
}
.date-picker .result:hover {
border-color: #bdbdbd;
}
.date-picker .result:focus {
border-color: #2187db;
box-shadow: 0 0 0 .025rem #3982c75e;
border-right-width: 1px !important;
outline: 0;
}
.date-picker .dropdown {
position: absolute;
top: 100%;
left: 50%;
width: 3.75rem;
padding: 0.2rem 0.1rem;
margin-top: 0.15rem;
margin-left: -1.875rem;
filter: drop-shadow(0.025rem 0.025rem 0.1rem rgba(0, 0, 0, 0.1));
background: rgba(0, 0, 0, 0.7);
border-radius: 0.025rem;
box-shadow: 0 0.025rem 0.1rem #00000026;
z-index: 2;
}
.date-picker .dropdown::before {
content: "";
position: absolute;
top: -0.1rem;
left: 50%;
margin-left: -0.1rem;
border-width: 0 0.1rem 0.1rem 0.1rem;
border-style: solid;
border-color: transparent transparent rgba(0, 0, 0, 0.6) transparent;
}
.date-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-header {
cursor: pointer;
> div {
> *:hover {
color: #fed039;
}
}
.c {
b {
margin: 0 0.0625rem;
}
}
.prev-year {
margin-right: .125rem;
}
.next-month {
margin-right: .125rem;
}
}
.date-week {
display: flex;
align-items: center;
margin-top: 0.375rem;
}
.date-week li {
flex: 1;
font-size: 0.15rem;
text-align: center;
}
.date-list {
display: flex;
flex-wrap: wrap;
margin-top: 0.1rem;
}
.date-list li {
width: calc(100% / 7);
height: 0.5rem;
border-radius: 50%;
line-height: 0.5rem;
text-align: center;
cursor: pointer;
}
.date-list li.active {
color: #fff;
background: #fed039;
}
.date-list .no-current {
color: #c4c4c4;
}
.year-panel {
max-height: 3.75rem;
overflow-y: auto;
}
.year-panel-item {
height: 0.375rem;
line-height: 0.375rem;
cursor: pointer;
transition: 0.15s ease-in-out;
border: 1px solid transparent;
text-indent: 0.05rem;
}
.year-panel-item.active {
font-weight: bold;
color: #fed039;
border: 1px solid transparent;
}
.year-panel-item:hover {
border: 1px solid rgba(214, 236, 87, 0.459);
}
.month-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.month-panel-item {
text-align: center;
height: 0.625rem;
line-height: 0.625rem;
cursor: pointer;
transition: 0.15s ease-in-out;
border: 1px solid transparent;
}
.month-panel-item:hover {
border: 1px solid rgba(214, 236, 87, 0.459);
}
.month-panel-item.active {
font-weight: bold;
color: #fed039;
}
.hms-panel {
height: 100%;
background-color: rgba(20, 20, 27, 0.85);
width: 0%;
position: absolute;
top: 0;
right: .025rem;
transition: width 0.3s ease-in-out;
overflow: hidden;
box-sizing: border-box;
padding: 0.25rem 0;
.hms-value {
text-align: center;
margin-bottom: 0.25rem;
font-size: 0.25rem;
}
.hms-change {
display: flex;
width: 50%;
margin: 0 auto;
.num-list {
flex: 33%;
height: 2.5rem;
text-align: center;
overflow: scroll;
padding: 0.05rem;
box-sizing: border-box;
.scroll {
.num {
line-height: 1.8;
cursor: pointer;
border: 1px solid rgba(214, 236, 87, 0);
}
.isactive {
border: 1px solid rgba(214, 236, 87, 0.459);
color: #fed039;
}
}
}
}
}
.hms-ok {
cursor: pointer;
height: 0;
margin-right: 0.125rem;
float: right;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: 0.2s ease-in-out;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translate3d(0, -0.25rem, 0);
opacity: 0;
}
</style>
使用:
<DatePicker v-model="endTime" frontText="结束时间:" />
...
<script setup lang="ts">
import { ref } from "vue";
const endTime = ref("2022-03-04 17:35:00");
</script>
// 参数 EndTime 格式 YYYY-MM-DD hh:mm:ss
const endTime = ref("2022-03-04 17:35:00");
endTime通过ref创建的话 数据是双向绑定的
评论