一、HTML部分
<template><div class="custom-date-picker" ref="root"><!-- 输入框 --><inputref="input"type="text"readonlystyle="width: 100%":style="{ '--theme-color': primaryColor }":value="displayValue"@click="togglePanel"placeholder="请选择日期"/><!-- 浮层 --><transition name="fade"><divv-if="showPanel"ref="panel"class="date-panel":style="{left: panelLeft + 'px',top: panelTop + 'px','--theme-color': primaryColor,}"><template v-if="panelMode === 'date'"><div class="date-header"><span class="nav nav-left nav-jump" @click="prevDecade"><<</span><span class="nav nav-left" @click="prevMonth"><</span><span class="year-label" @click="switchToYear">{{ year }}年</span><span class="month-label" @click="switchToMonth">{{ month }}月</span><span class="nav nav-right" @click="nextMonth">></span><span class="nav nav-right nav-jump" @click="nextDecade">>></span></div><div class="date-week"><span v-for="w in weekText" :key="w">{{ w }}</span></div><div class="date-days"><spanv-for="(day, i) in days":key="i":class="{today: day.today,selected: day.selected,other: day.otherMonth,}"@click="selectDay(day)">{{ day.text }}</span></div></template><template v-if="panelMode === 'month'"><div class="date-header"><span>{{ year }}年</span><span class="nav nav-right" @click="switchToDate">✕</span></div><div class="date-months"><spanv-for="m in 12":key="m":class="{ selected: m === month }"@click="selectMonth(m)">{{ m }}月</span></div></template><template v-if="panelMode === 'year'"><div class="date-header"><span class="nav nav-left" @click="prevDecade"><</span><span>{{ yearStart }} - {{ yearEnd }}</span><span class="nav nav-right" @click="nextDecade">></span></div><div class="date-years"><spanv-for="y in yearRange":key="y":class="{ selected: y === year }"@click="selectYear(y)">{{ y }}</span></div></template><!-- 时间 --><template v-if="panelMode === 'time'"><div v-if="showTime" class="dp-time"><div class="dp-time-column" ref="hourCol"><ul><liv-for="i in 24":key="i":class="{ 'dp-time-selected': time[0] === i - 1 }"@click="selectTimeUnit(0, i - 1)">{{ (i - 1).toString().padStart(2, "0") }}</li></ul></div><div class="dp-time-column" ref="minuteCol"><ul><liv-for="i in 60":key="i":class="{ 'dp-time-selected': time[1] === i - 1 }"@click="selectTimeUnit(1, i - 1)">{{ (i - 1).toString().padStart(2, "0") }}</li></ul></div><div class="dp-time-column" ref="secondCol"><ul><liv-for="i in 60":key="i":class="{ 'dp-time-selected': time[2] === i - 1 }"@click="selectTimeUnit(2, i - 1)">{{ (i - 1).toString().padStart(2, "0") }}</li></ul></div></div></template><!-- 今天按钮 --><div class="date-footer" v-show="showToday && !showTime"><a @click="selectToday">今天</a></div><divclass="date-footer2"v-show="showTime && (panelMode === 'date' || panelMode === 'time')"><a @click="selectThisTime">此刻</a><div><a @click="switchToTime" v-show="panelMode === 'date'">选择时间</a><a @click="switchToDate" v-show="panelMode === 'time'">选择日期</a><a@click="confirm"v-show="panelMode === 'date' || panelMode === 'time'"style="margin-left: 5px">确定</a></div></div></div></transition></div>
</template>
二、JS部分
<script>
import moment from "moment";
import systemSet from "@/store/index.js";export default {name: "CustomDatePicker",props: {value: {type: String,default: "",},format: {type: String,default: "YYYY-MM-DD",},showToday: {type: Boolean,default: false,},showTime: {type: Boolean,default: false,},},data() {return {showPanel: false,year: moment().year(),month: moment().month() + 1,weekText: ["日", "一", "二", "三", "四", "五", "六"],panelTop: 0,panelLeft: 0,primaryColor: systemSet.state.systemSet.primaryColor,panelMode: "date", // date日 | month月 | year年time: [0, 0, 0],clickHandler:null};},mounted() {// 点击其它区域关闭下拉this.clickHandler = (e) => {if (!this.$el.contains(e.target)) {this.isOpen = false;}};document.addEventListener("click", this.clickHandler);},computed: {/* 输入框回显 */displayValue() {return this.value ? moment(this.value).format(this.format) : "";},/* 日历网格 6*7 */days() {const first = moment([this.year, this.month - 1, 1]);const start = first.clone().startOf("week");const res = [];for (let i = 0; i < 42; i++) {const d = start.clone().add(i, "day");res.push({text: d.date(),value: d.format("YYYY-MM-DD"),today: d.isSame(moment(), "day"),selected: d.isSame(this.value, "day"),otherMonth: d.month() !== first.month(),});}return res;},/* 年份面板:起始年份 */yearStart() {return Math.floor(this.year / 10) * 10;},/* 年份面板:终止年份 */yearEnd() {return this.yearStart + 9;},/* 年份面板:10 年数组 */yearRange() {return Array.from({ length: 10 }, (_, i) => this.yearStart + i);},},watch: {/* 面板切换/值变化时,把 value 里的时间同步到 time */value: {handler(val) {if (!val) return;const m = moment(val);this.time = [m.hour(), m.minute(), m.second()];this.$nextTick(() => {if (this.panelMode === "time") this.scrollCurrentToTop();});},immediate: true,},},methods: {/* 点击某一项时:改值 + 滚动到顶 */selectTimeUnit(type, val) {// type: 0=时 1=分 2=秒this.$set(this.time, type, val);this.$nextTick(() => this.scrollToTop(type));},/* 把对应列滚动到选中项顶端 */scrollToTop(type) {console.log(121)const map = ["hourCol", "minuteCol", "secondCol"];const colEl = this.$refs[map[type]];if (!colEl) return;const liHeight = 24; // 与 CSS 保持一致const index = this.time[type];colEl.scrollTop = index * liHeight;},scrollCurrentToTop() {[0, 1, 2].forEach((type) => this.scrollToTop(type));},/* 1. 点击“此刻” */selectThisTime() {const now = moment();this.year = now.year();this.month = now.month() + 1;this.time = [now.hour(), now.minute(), now.second()];this.emitDateTime();this.showPanel = false;},/* 2. 点击“确定”或在日期面板点击日期后自动关闭时 */confirm(){this.emitDateTime()this.showPanel = false;},emitDateTime() {const dateStr = moment([this.year, this.month - 1, 1]).date(this.days.find((d) => d.selected)?.text || moment().date()).format("YYYY-MM-DD");const timeStr = this.time.map((n) => n.toString().padStart(2, "0")).join(":");console.log(dateStr,'showToday')const full = `${dateStr} ${timeStr}`;this.$emit("input", this.showTime ? full : dateStr);},/* 打开 / 关闭浮层 */togglePanel() {this.showPanel = !this.showPanel;if (this.showPanel) {this.positionPanel();this.panelMode = "date"; // 每次重新打开回到日期模式}},/* 定位浮层到输入框下方 */positionPanel() {this.$nextTick(() => {const inputRect = this.$refs.input.getBoundingClientRect();this.panelTop = inputRect.bottom + window.scrollY;this.panelLeft = inputRect.left + window.scrollX;});},/* 选具体日期 */selectDay(day) {if (day.otherMonth) return;this.$emit("input", day.value); // 先回显日期console.log(1,this.showTime)if (!this.showTime) {this.showPanel = false;} else {/* 日期已更新,再把时间拼上去 */this.$nextTick(this.emitDateTime);}},selectToday() {const today = moment();this.year = today.year();this.month = today.month() + 1;this.$emit("input", today.format("YYYY-MM-DD"));// this.time = [today.hour(), today.minute(), today.second()];// this.emitDateTime();this.showPanel = false;},/* 月翻页 */prevMonth() {const m = moment([this.year, this.month - 1, 1]).subtract(1, "month");this.year = m.year();this.month = m.month() + 1;this.$nextTick(this.positionPanel);},nextMonth() {const m = moment([this.year, this.month - 1, 1]).add(1, "month");this.year = m.year();this.month = m.month() + 1;this.$nextTick(this.positionPanel);},/* 年翻页(单年) */prevYear() {this.year -= 1;this.$nextTick(this.positionPanel);},nextYear() {this.year += 1;this.$nextTick(this.positionPanel);},/* 十年翻页 */prevDecade() {this.year -= 10;},nextDecade() {this.year += 10;},/* 切换面板模式 */switchToYear() {this.panelMode = "year";},switchToMonth() {this.panelMode = "month";},switchToDate() {this.panelMode = "date";},switchToTime() {this.panelMode = "time";this.$nextTick(()=>{this.scrollCurrentToTop()});},/* 选择月份后回到日期面板 */selectMonth(m) {this.month = m;this.panelMode = "date";},/* 选择年份后回到日期面板 */selectYear(y) {this.year = y;this.panelMode = "date";},},beforeDestroy() {document.removeEventListener("click", this.clickHandler);},
};
</script>
三、CSS部分
<style scoped>
/* 根元素 */
.custom-date-picker {position: relative;display: inline-block;width: 100%;
}/* 输入框 */
input {width: 100%;height: 32px;padding: 0 8px;border: 1px solid #d9d9d9;border-radius: 4px;cursor: pointer;transition: all 0.2s;
}
input:focus {border-color: var(--theme-color);box-shadow: 0 0 0 1px var(--theme-color);outline: none;
}/* 浮层主容器 */
.date-panel {position: fixed;z-index: 9999;background: #fff;border-radius: 4px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);padding: 8px;user-select: none;font-size: 14px;width: 220px;
}/* 头部导航 */
.date-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 6px;
}
.nav {cursor: pointer;padding: 0 4px;font-size: 14px;transition: color 0.2s;
}
.nav:hover {color: var(--theme-color);
}
.year-label,
.month-label {cursor: pointer;
}
.year-label:hover,
.month-label:hover {color: var(--theme-color);
}/* 星期条 */
.date-week {display: grid;grid-template-columns: repeat(7, 1fr);text-align: center;margin-bottom: 4px;font-weight: 600;color: rgba(0, 0, 0, 0.45);
}/* 日期格子 */
.date-days {display: grid;grid-template-columns: repeat(7, 1fr);gap: 2px;text-align: center;
}
.date-days span {cursor: pointer;line-height: 26px;border-radius: 3px;transition: all 0.2s;
}
.date-days span:hover {background: #f0f0f0;
}
.date-days .today {color: var(--theme-color);font-weight: bold;
}
.date-days .selected {background: var(--theme-color);color: #fff;
}
.date-days .other {color: #bbb;cursor: not-allowed;
}/* 月份面板 */
.date-months {display: grid;grid-template-columns: repeat(3, 1fr);gap: 4px;padding: 8px 0;
}
.date-months span {text-align: center;line-height: 32px;cursor: pointer;border-radius: 3px;transition: all 0.2s;
}
.date-months span:hover {background: #f0f0f0;
}
.date-months .selected {background: var(--theme-color);color: #fff;
}/* 年份面板 */
.date-years {display: grid;grid-template-columns: repeat(3, 1fr);gap: 4px;padding: 8px 0;
}
.date-years span {text-align: center;line-height: 32px;cursor: pointer;border-radius: 3px;transition: all 0.2s;
}
.date-years span:hover {background: #f0f0f0;
}
.date-years .selected {background: var(--theme-color);color: #fff;
}/* 今天按钮 */
.date-footer {text-align: center;margin-top: 6px;border-top: 1px solid #f0f0f0;padding-top: 6px;
}
.date-footer a {cursor: pointer;color: var(--theme-color);font-size: 12px;
}
.date-footer a:hover {text-decoration: underline;
}
.space-between {text-decoration: underline;
}
.date-footer2 {display: flex;justify-content: space-between;margin-top: 6px;border-top: 1px solid #f0f0f0;padding-top: 6px;
}
.date-footer2 a {display: inline-block;cursor: pointer;color: var(--theme-color);font-size: 12px;
}/* 动画 */
.fade-enter-active,
.fade-leave-active {transition: opacity 0.2s;
}
.fade-enter,
.fade-leave-to {opacity: 0;
}
/* 时间 */
.dp-time {display: flex;justify-content: center;border-top: 1px solid #f0f0f0;padding: 8px 0;
}
.dp-time-column {width: 56px;max-height: 144px;overflow-y: auto;scrollbar-width: thin;
}
.dp-time-column::-webkit-scrollbar {width: 4px;
}
.dp-time-column::-webkit-scrollbar-thumb {background: rgba(0, 0, 0, 0.2);border-radius: 2px;
}
.dp-time-column li {height: 24px;line-height: 24px;text-align: center;cursor: pointer;
}
.dp-time-selected {color: var(--theme-color);font-weight: 600;
}
</style>