2026-02-13 15:19:16 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="custom-datetime-range-picker">
|
|
|
|
|
|
<!-- 输入框 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="el-input_parent el-input el-input--suffix"
|
|
|
|
|
|
:class="{'is-focus': showPanel, 'is-disabled': disabled}"
|
|
|
|
|
|
@click="togglePanel"
|
|
|
|
|
|
ref="triggerRef"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="el-input_dateIcon el-input__icon el-range__icon el-icon-time"></i>
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="el-range-input el-input_first"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
:placeholder="startPlaceholder"
|
|
|
|
|
|
:value="displayValue[0]"
|
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="disInlineBlock textAlignCenter" style="width: 30px;">{{rangeSeparator}}</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="el-range-input el-input_second"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
:placeholder="endPlaceholder"
|
|
|
|
|
|
:value="displayValue[1]"
|
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="el-input__suffix" style="right: 5px!important;">
|
|
|
|
|
|
<span class="el-input__suffix-inner">
|
|
|
|
|
|
<i
|
|
|
|
|
|
class="el-input__icon"
|
|
|
|
|
|
:class="{'el-icon-circle-close': clearable && displayValue.length > 0}"
|
|
|
|
|
|
@click.stop="clearSelection"
|
|
|
|
|
|
></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 日期时间选择面板 -->
|
|
|
|
|
|
<transition name="el-zoom-in-top">
|
|
|
|
|
|
<div v-if="showPanel" class="el-picker-panel time-range-panel" :style="panelStyle" ref="panelRef">
|
|
|
|
|
|
<div class="panel-body">
|
|
|
|
|
|
<!-- 快捷选项 -->
|
|
|
|
|
|
<template v-if="pickerOptions.shortcuts && pickerOptions.shortcuts.length > 0">
|
|
|
|
|
|
<div class="quick-panel">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="el-picker-panel__shortcut"
|
|
|
|
|
|
v-for="(shortcut, key) in pickerOptions.shortcuts"
|
|
|
|
|
|
:key="key"
|
|
|
|
|
|
@click="handleShortcutClick(shortcut)">{{shortcut.text}}</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 中间分隔线 -->
|
|
|
|
|
|
<div class="panel-divider"></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- 左侧:日期选择区域 -->
|
|
|
|
|
|
<div class="date-panel">
|
|
|
|
|
|
<!-- 开始结束时间选择 -->
|
|
|
|
|
|
<div v-if="dateType === 'datetimerange'" style="padding: 8px 0 5px;">
|
|
|
|
|
|
<el-time-picker v-model="startTime" :disabled="!(startDate && endDate)" size="small" style="width: 49%; margin-right: 1%;" placeholder="开始时间" />
|
|
|
|
|
|
<el-time-picker v-model="endTime" :disabled="!(startDate && endDate)" size="small" style="width: 49%; margin-left: 1%;" placeholder="结束时间" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="width: 100%;height: 1px; background-color: #e4e7ed;"></div>
|
|
|
|
|
|
<!-- 头部:年月切换 -->
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn prev-year-btn"
|
|
|
|
|
|
@click="prevYear"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="el-icon-d-arrow-left"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn prev-month-btn"
|
|
|
|
|
|
@click="prevMonth"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="el-icon-arrow-left"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="header-label">
|
|
|
|
|
|
{{ currentMonth }}年{{ currentDate }}月
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn next-month-btn"
|
|
|
|
|
|
@click="nextMonth"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="el-icon-arrow-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn next-year-btn"
|
|
|
|
|
|
@click="nextYear"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="el-icon-d-arrow-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 周几标题 -->
|
|
|
|
|
|
<div class="weekdays">
|
|
|
|
|
|
<div v-for="day in weekdays" :key="day" class="weekday">{{ day }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 日历表格 -->
|
|
|
|
|
|
<div class="calendar">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(day, index) in days"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="day-cell"
|
2026-03-16 18:06:58 +08:00
|
|
|
|
:class="getDayClass(day, index)"
|
2026-02-13 15:19:16 +08:00
|
|
|
|
@click="selectDate(day)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="day-content">
|
|
|
|
|
|
{{ day.date }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 面板底部 -->
|
|
|
|
|
|
<div class="panel-footer">
|
|
|
|
|
|
<div class="footer-actions w100" style="display: block;">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="footer-btn clear-btn"
|
|
|
|
|
|
@click="clearSelection"
|
|
|
|
|
|
>
|
|
|
|
|
|
清空
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="el-button is-plain el-button--mini"
|
|
|
|
|
|
:disabled="!(startDate && endDate)"
|
|
|
|
|
|
@click="applySelection"
|
|
|
|
|
|
>
|
|
|
|
|
|
确定
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'CustomDateTimeRangePicker',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
dateType: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'datetimerange'
|
|
|
|
|
|
},
|
|
|
|
|
|
value: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
startPlaceholder: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '开始日期'
|
|
|
|
|
|
},
|
|
|
|
|
|
endPlaceholder: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '结束日期'
|
|
|
|
|
|
},
|
|
|
|
|
|
rangeSeparator: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '至'
|
|
|
|
|
|
},
|
|
|
|
|
|
disabled: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
clearable: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
format: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'YYYY-MM-DD HH:mm:ss'
|
|
|
|
|
|
},
|
|
|
|
|
|
defaultTime: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => ['00:00:00', '00:00:00']
|
|
|
|
|
|
},
|
|
|
|
|
|
pickerOptions: {
|
|
|
|
|
|
type: Object,
|
2026-03-16 18:06:58 +08:00
|
|
|
|
default: () => ({shortcuts: []})
|
2026-02-13 15:19:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
placement: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'bottom-start',
|
|
|
|
|
|
validator: (value) => ['bottom-start', 'bottom-end', 'top-start', 'top-end'].includes(value)
|
|
|
|
|
|
},
|
|
|
|
|
|
offset: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 4 // 面板与触发器的距离
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
return {
|
|
|
|
|
|
showPanel: false,
|
|
|
|
|
|
currentDate: now.getMonth() + 1,
|
|
|
|
|
|
currentMonth: now.getFullYear(),
|
|
|
|
|
|
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
|
|
|
|
|
days: [],
|
|
|
|
|
|
startDate: null,
|
|
|
|
|
|
endDate: null,
|
|
|
|
|
|
startTime: '',
|
|
|
|
|
|
endTime: '',
|
|
|
|
|
|
panelStyle: {opacity: 0},
|
2026-03-16 18:06:58 +08:00
|
|
|
|
actualPlacement: this.placement,
|
|
|
|
|
|
updatePanelPositionDebounced: null, // 防抖函数引用
|
|
|
|
|
|
calendarCache: {}, // 日历数据缓存
|
|
|
|
|
|
focusedDayIndex: -1, // 当前焦点日期的索引
|
|
|
|
|
|
isKeyboardNavigating: false // 是否正在使用键盘导航
|
2026-02-13 15:19:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
displayValue() {
|
|
|
|
|
|
if (this.value && this.value.length === 2 && this.value[0] && this.value[1]) {
|
|
|
|
|
|
const start = this.formatDateTime(this.value[0]);
|
|
|
|
|
|
const end = this.formatDateTime(this.value[1]);
|
|
|
|
|
|
// return `${start} 至 ${end}`;
|
|
|
|
|
|
return [start, end]
|
|
|
|
|
|
}
|
|
|
|
|
|
return [];
|
2026-03-16 18:06:58 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 缓存键,用于判断是否需要重新生成日历
|
|
|
|
|
|
calendarCacheKey() {
|
|
|
|
|
|
return `${this.currentMonth}-${this.currentDate}`;
|
2026-02-13 15:19:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-03-16 18:06:58 +08:00
|
|
|
|
|
2026-02-13 15:19:16 +08:00
|
|
|
|
watch: {
|
|
|
|
|
|
value: {
|
|
|
|
|
|
immediate: true,
|
|
|
|
|
|
handler(val) {
|
|
|
|
|
|
if (val && val.length === 2 && val[0] && val[1]) {
|
|
|
|
|
|
const start = new Date(val[0]);
|
|
|
|
|
|
const end = new Date(val[1]);
|
|
|
|
|
|
this.startDate = start;
|
|
|
|
|
|
this.endDate = end;
|
|
|
|
|
|
if (this.dateType === 'datetimerange') {
|
|
|
|
|
|
this.startTime = start;
|
|
|
|
|
|
this.endTime = end;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 同步月份显示
|
|
|
|
|
|
this.currentMonth = start.getFullYear();
|
|
|
|
|
|
this.currentDate = start.getMonth() + 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.startDate = null;
|
|
|
|
|
|
this.endDate = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
showPanel(val) {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
document.addEventListener('click', this.handleClickOutside);
|
2026-03-16 18:06:58 +08:00
|
|
|
|
document.addEventListener('scroll', this.updatePanelPositionDebounced, true);
|
|
|
|
|
|
window.addEventListener('resize', this.updatePanelPositionDebounced);
|
|
|
|
|
|
document.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
|
// 初始化焦点到第一个当前月份的日期
|
2026-02-13 15:19:16 +08:00
|
|
|
|
this.$nextTick(() => {
|
2026-03-16 18:06:58 +08:00
|
|
|
|
this.focusFirstCurrentMonthDay();
|
2026-02-13 15:19:16 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.updatePanelPosition();
|
|
|
|
|
|
},500);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.removeEventListener('click', this.handleClickOutside);
|
2026-03-16 18:06:58 +08:00
|
|
|
|
document.removeEventListener('scroll', this.updatePanelPositionDebounced, true);
|
|
|
|
|
|
window.removeEventListener('resize', this.updatePanelPositionDebounced);
|
|
|
|
|
|
document.removeEventListener('keydown', this.handleKeydown);
|
|
|
|
|
|
this.focusedDayIndex = -1;
|
|
|
|
|
|
this.isKeyboardNavigating = false;
|
2026-02-13 15:19:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
this.generateCalendar();
|
2026-03-16 18:06:58 +08:00
|
|
|
|
// 创建防抖函数
|
|
|
|
|
|
this.updatePanelPositionDebounced = this.debounce(this.updatePanelPosition, 100);
|
2026-02-13 15:19:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
document.removeEventListener('click', this.handleClickOutside);
|
2026-03-16 18:06:58 +08:00
|
|
|
|
document.removeEventListener('scroll', this.updatePanelPositionDebounced, true);
|
|
|
|
|
|
window.removeEventListener('resize', this.updatePanelPositionDebounced);
|
|
|
|
|
|
document.removeEventListener('keydown', this.handleKeydown);
|
2026-02-13 15:19:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-03-16 18:06:58 +08:00
|
|
|
|
// 简单的防抖函数实现
|
|
|
|
|
|
debounce(func, wait) {
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
return function executedFunction(...args) {
|
|
|
|
|
|
const later = () => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
func.apply(this, args);
|
|
|
|
|
|
};
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
timeout = setTimeout(later, wait);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 解析时间字符串为Date对象
|
|
|
|
|
|
parseTimeString(timeStr) {
|
|
|
|
|
|
if (!timeStr) return new Date(0, 0, 0, 0, 0, 0);
|
|
|
|
|
|
const parts = timeStr.split(':').map(Number);
|
|
|
|
|
|
return new Date(0, 0, 0, parts[0] || 0, parts[1] || 0, parts[2] || 0);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-13 15:19:16 +08:00
|
|
|
|
// 更新面板位置
|
|
|
|
|
|
updatePanelPosition() {
|
|
|
|
|
|
if (!this.showPanel || !this.$refs.triggerRef || !this.$refs.panelRef) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const trigger = this.$refs.triggerRef;
|
|
|
|
|
|
const panel = this.$refs.panelRef;
|
|
|
|
|
|
const viewport = {
|
|
|
|
|
|
width: window.innerWidth,
|
|
|
|
|
|
height: window.innerHeight
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取触发器位置
|
|
|
|
|
|
const triggerRect = trigger.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算面板基本尺寸
|
|
|
|
|
|
const panelRect = panel.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// 应用最终位置
|
|
|
|
|
|
if ((viewport.height - panelRect.height - triggerRect.top - 8) > 0) {
|
|
|
|
|
|
this.panelStyle = {top: `100%`, opacity: 1};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.panelStyle = {top: `${Math.round(viewport.height - panelRect.height - triggerRect.top - 8)}px`, opacity: 1};
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 快捷选项点击事件
|
|
|
|
|
|
handleShortcutClick(shortcut) {
|
|
|
|
|
|
if (shortcut.onClick) {
|
|
|
|
|
|
shortcut.onClick(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
generateCalendar() {
|
2026-03-16 18:06:58 +08:00
|
|
|
|
// 检查缓存
|
|
|
|
|
|
const cacheKey = this.calendarCacheKey;
|
|
|
|
|
|
if (this.calendarCache[cacheKey]) {
|
|
|
|
|
|
this.days = this.calendarCache[cacheKey];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 15:19:16 +08:00
|
|
|
|
const year = this.currentMonth;
|
|
|
|
|
|
const month = this.currentDate - 1;
|
|
|
|
|
|
const firstDayOfMonth = new Date(year, month, 1);
|
|
|
|
|
|
const firstDayWeek = firstDayOfMonth.getDay();
|
|
|
|
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
|
|
|
|
const prevMonthDays = new Date(year, month, 0).getDate();
|
|
|
|
|
|
|
2026-03-16 18:06:58 +08:00
|
|
|
|
const days = [];
|
2026-02-13 15:19:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加上个月的天数
|
|
|
|
|
|
for (let i = 0; i < firstDayWeek; i++) {
|
|
|
|
|
|
const day = prevMonthDays - firstDayWeek + i + 1;
|
2026-03-16 18:06:58 +08:00
|
|
|
|
days.push({
|
2026-02-13 15:19:16 +08:00
|
|
|
|
date: day,
|
|
|
|
|
|
month: 'prev',
|
|
|
|
|
|
fullDate: new Date(year, month - 1, day)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加当月天数
|
|
|
|
|
|
for (let i = 1; i <= daysInMonth; i++) {
|
2026-03-16 18:06:58 +08:00
|
|
|
|
days.push({
|
2026-02-13 15:19:16 +08:00
|
|
|
|
date: i,
|
|
|
|
|
|
month: 'current',
|
|
|
|
|
|
fullDate: new Date(year, month, i)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加下个月的天数
|
|
|
|
|
|
const totalCells = 42; // 6行 * 7天
|
|
|
|
|
|
const nextMonthDay = 1;
|
2026-03-16 18:06:58 +08:00
|
|
|
|
while (days.length < totalCells) {
|
|
|
|
|
|
days.push({
|
|
|
|
|
|
date: nextMonthDay + (days.length - daysInMonth - firstDayWeek),
|
2026-02-13 15:19:16 +08:00
|
|
|
|
month: 'next',
|
2026-03-16 18:06:58 +08:00
|
|
|
|
fullDate: new Date(year, month + 1, nextMonthDay + (days.length - daysInMonth - firstDayWeek))
|
2026-02-13 15:19:16 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-16 18:06:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新数据和缓存
|
|
|
|
|
|
this.days = days;
|
|
|
|
|
|
this.calendarCache[cacheKey] = days;
|
|
|
|
|
|
|
|
|
|
|
|
// 限制缓存大小,避免内存泄漏
|
|
|
|
|
|
const cacheKeys = Object.keys(this.calendarCache);
|
|
|
|
|
|
if (cacheKeys.length > 10) {
|
|
|
|
|
|
delete this.calendarCache[cacheKeys[0]];
|
|
|
|
|
|
}
|
2026-02-13 15:19:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
selectDate(day) {
|
|
|
|
|
|
if (day.month !== 'current') {
|
|
|
|
|
|
if (day.month === 'prev') {
|
|
|
|
|
|
this.prevMonth();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.nextMonth();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.startDate) {
|
|
|
|
|
|
this.startDate = day.fullDate;
|
|
|
|
|
|
} else if (!this.endDate) {
|
|
|
|
|
|
this.endDate = day.fullDate;
|
|
|
|
|
|
// 确保结束时间不早于开始时间
|
|
|
|
|
|
if (this.endDate < this.startDate) {
|
|
|
|
|
|
[this.startDate, this.endDate] = [this.endDate, this.startDate];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.startDate = day.fullDate;
|
|
|
|
|
|
this.endDate = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.startDate && this.endDate && this.dateType === 'datetimerange') {
|
2026-03-16 18:06:58 +08:00
|
|
|
|
this.startTime = this.startTime ? this.startTime : this.parseTimeString(this.defaultTime[0]);
|
|
|
|
|
|
this.endTime = this.endTime ? this.endTime : this.parseTimeString(this.defaultTime[1]);
|
2026-02-13 15:19:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-16 18:06:58 +08:00
|
|
|
|
getDayClass(day, index) {
|
2026-02-13 15:19:16 +08:00
|
|
|
|
const classes = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (day.month !== 'current') {
|
|
|
|
|
|
classes.push('other-month');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isToday(day.fullDate)) {
|
|
|
|
|
|
classes.push('today');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.startDate && this.isSameDay(day.fullDate, this.startDate)) {
|
|
|
|
|
|
classes.push('start-date');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.endDate && this.isSameDay(day.fullDate, this.endDate)) {
|
|
|
|
|
|
classes.push('end-date');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isInRange(day.fullDate)) {
|
|
|
|
|
|
classes.push('in-range');
|
|
|
|
|
|
}
|
2026-03-16 18:06:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加焦点样式
|
|
|
|
|
|
if (this.isKeyboardNavigating && index === this.focusedDayIndex) {
|
|
|
|
|
|
classes.push('focused');
|
|
|
|
|
|
}
|
2026-02-13 15:19:16 +08:00
|
|
|
|
|
|
|
|
|
|
return classes;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isSameDay(date1, date2) {
|
|
|
|
|
|
if (!date1 || !date2) return false;
|
|
|
|
|
|
return date1.getDate() === date2.getDate() &&
|
|
|
|
|
|
date1.getMonth() === date2.getMonth() &&
|
|
|
|
|
|
date1.getFullYear() === date2.getFullYear();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isInRange(date) {
|
|
|
|
|
|
if (!this.startDate || !this.endDate) return false;
|
|
|
|
|
|
const start = new Date(this.startDate);
|
|
|
|
|
|
const end = new Date(this.endDate);
|
|
|
|
|
|
start.setHours(0, 0, 0, 0);
|
|
|
|
|
|
end.setHours(23, 59, 59, 999);
|
|
|
|
|
|
return date >= start && date <= end;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isToday(date) {
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
return date.getDate() === today.getDate() &&
|
|
|
|
|
|
date.getMonth() === today.getMonth() &&
|
|
|
|
|
|
date.getFullYear() === today.getFullYear();
|
|
|
|
|
|
},
|
|
|
|
|
|
applySelection() {
|
|
|
|
|
|
if (this.startDate && this.endDate) {
|
|
|
|
|
|
const start = new Date(this.startDate);
|
|
|
|
|
|
const end = new Date(this.endDate);
|
|
|
|
|
|
if (this.dateType === 'datetimerange') {
|
|
|
|
|
|
start.setHours(
|
|
|
|
|
|
this.startTime.getHours(),
|
|
|
|
|
|
this.startTime.getMinutes(),
|
|
|
|
|
|
this.startTime.getSeconds()
|
|
|
|
|
|
);
|
|
|
|
|
|
end.setHours(
|
|
|
|
|
|
this.endTime.getHours(),
|
|
|
|
|
|
this.endTime.getMinutes(),
|
|
|
|
|
|
this.endTime.getSeconds()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (start > end) {
|
|
|
|
|
|
this.$message.error('开始时间不能晚于结束时间');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let startFormat = this.formatDateTime(start);
|
|
|
|
|
|
let endFormat = this.formatDateTime(end);
|
|
|
|
|
|
this.$emit('change', [startFormat, endFormat]);
|
|
|
|
|
|
this.$emit('input', [startFormat, endFormat]);
|
|
|
|
|
|
this.showPanel = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
clearSelection() {
|
|
|
|
|
|
this.startDate = null;
|
|
|
|
|
|
this.endDate = null;
|
|
|
|
|
|
this.startTime = '';
|
|
|
|
|
|
this.endTime = '';
|
|
|
|
|
|
this.$emit('change', []);
|
|
|
|
|
|
this.$emit('input', []);
|
|
|
|
|
|
if (this.showPanel) {
|
|
|
|
|
|
this.closePanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
closePanel() {
|
|
|
|
|
|
this.showPanel = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
togglePanel() {
|
|
|
|
|
|
if (this.disabled) return;
|
|
|
|
|
|
this.showPanel = !this.showPanel;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
prevMonth() {
|
|
|
|
|
|
this.currentDate--;
|
|
|
|
|
|
if (this.currentDate < 1) {
|
|
|
|
|
|
this.currentDate = 12;
|
|
|
|
|
|
this.currentMonth--;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
nextMonth() {
|
|
|
|
|
|
this.currentDate++;
|
|
|
|
|
|
if (this.currentDate > 12) {
|
|
|
|
|
|
this.currentDate = 1;
|
|
|
|
|
|
this.currentMonth++;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
prevYear() {
|
|
|
|
|
|
this.currentMonth--;
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
nextYear() {
|
|
|
|
|
|
this.currentMonth++;
|
|
|
|
|
|
this.generateCalendar();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleClickOutside(event) {
|
|
|
|
|
|
if (!this.$el.contains(event.target)) {
|
|
|
|
|
|
this.showPanel = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-16 18:06:58 +08:00
|
|
|
|
// 键盘事件处理
|
|
|
|
|
|
handleKeydown(event) {
|
|
|
|
|
|
if (!this.showPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.isKeyboardNavigating = true;
|
|
|
|
|
|
|
|
|
|
|
|
switch(event.key) {
|
|
|
|
|
|
case 'ArrowLeft':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.moveFocus(-1); // 向左移动
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'ArrowRight':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.moveFocus(1); // 向右移动
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.moveFocus(-7); // 向上移动
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.moveFocus(7); // 向下移动
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Enter':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.selectFocusedDay();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.closePanel();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Tab':
|
|
|
|
|
|
// 允许Tab键正常导航
|
|
|
|
|
|
this.isKeyboardNavigating = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 聚焦到第一个当前月份的日期
|
|
|
|
|
|
focusFirstCurrentMonthDay() {
|
|
|
|
|
|
const firstCurrentMonthIndex = this.days.findIndex(day => day.month === 'current');
|
|
|
|
|
|
if (firstCurrentMonthIndex !== -1) {
|
|
|
|
|
|
this.focusedDayIndex = firstCurrentMonthIndex;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.focusedDayIndex = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 移动焦点
|
|
|
|
|
|
moveFocus(delta) {
|
|
|
|
|
|
if (this.days.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
let newIndex = this.focusedDayIndex + delta;
|
|
|
|
|
|
|
|
|
|
|
|
// 边界检查
|
|
|
|
|
|
if (newIndex < 0) {
|
|
|
|
|
|
// 移动到上个月
|
|
|
|
|
|
this.prevMonth();
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
newIndex = this.days.length - 7 + (newIndex % 7);
|
|
|
|
|
|
if (newIndex < 0) newIndex = 0;
|
|
|
|
|
|
this.focusedDayIndex = newIndex;
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (newIndex >= this.days.length) {
|
|
|
|
|
|
// 移动到下个月
|
|
|
|
|
|
this.nextMonth();
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
newIndex = newIndex % 7;
|
|
|
|
|
|
this.focusedDayIndex = newIndex;
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.focusedDayIndex = newIndex;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 选择当前焦点的日期
|
|
|
|
|
|
selectFocusedDay() {
|
|
|
|
|
|
if (this.focusedDayIndex >= 0 && this.focusedDayIndex < this.days.length) {
|
|
|
|
|
|
const day = this.days[this.focusedDayIndex];
|
|
|
|
|
|
this.selectDate(day);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-13 15:19:16 +08:00
|
|
|
|
formatDateTime(date) {
|
|
|
|
|
|
if (!date) return '';
|
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
|
const year = d.getFullYear();
|
|
|
|
|
|
const month = (d.getMonth() + 1).toString().padStart(2, '0');
|
|
|
|
|
|
const day = d.getDate().toString().padStart(2, '0');
|
|
|
|
|
|
const hour = d.getHours().toString().padStart(2, '0');
|
|
|
|
|
|
const minute = d.getMinutes().toString().padStart(2, '0');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.format === 'YYYY-MM-DD HH:mm:ss' || this.format === 'yyyy-MM-dd HH:mm:ss') {
|
|
|
|
|
|
const second = d.getSeconds().toString().padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
|
|
|
|
} else if (this.format === 'YYYY-MM-DD' || this.format === 'yyyy-MM-dd') {
|
|
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (this.dateType === 'datetimerange') {
|
|
|
|
|
|
const second = d.getSeconds().toString().padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
|
|
|
|
} else if (this.dateType === 'daterange') {
|
|
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.custom-datetime-range-picker {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 输入框前面的按钮*/
|
|
|
|
|
|
.el-input_dateIcon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-range__icon {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
float: left;
|
|
|
|
|
|
line-height: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 输入框样式 */
|
|
|
|
|
|
.el-input_parent {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
padding: 1px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-input_parent:hover {
|
|
|
|
|
|
border-color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input_parent.is-focus {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input_parent.is-disabled {
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
border-color: #e4e7ed;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input_first {
|
|
|
|
|
|
padding: 0 0 0 25px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-input_second {
|
|
|
|
|
|
padding: 0 30px 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-range-input {
|
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
background-image: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
line-height: 100%;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
|
|
|
|
width: calc((100% - 30px) / 2);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__suffix {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
right: 5px!important;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
/*.el-input:hover .el-input__suffix {*/
|
|
|
|
|
|
/* display: block;*/
|
|
|
|
|
|
/*}*/
|
|
|
|
|
|
|
|
|
|
|
|
.el-input_parent:hover .el-input__suffix {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__suffix-inner {
|
|
|
|
|
|
pointer-events: all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__icon {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
width: 25px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
line-height: 100%;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__icon:hover {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-icon-close:hover {
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 面板容器 */
|
|
|
|
|
|
.time-range-panel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 2001;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e4e7ed;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
min-height: 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 快捷选项面板 */
|
|
|
|
|
|
.quick-panel {
|
|
|
|
|
|
width: 110px;
|
|
|
|
|
|
padding-top: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 左侧日期面板 */
|
|
|
|
|
|
.date-panel {
|
|
|
|
|
|
/*flex: 1;*/
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
padding: 0 20px 20px;
|
|
|
|
|
|
border-right: 1px solid #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.icon-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.icon-btn:hover {
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-label {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-label:hover {
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 周几标题 */
|
|
|
|
|
|
.weekdays {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding: 0 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.weekday {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
line-height: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 日历表格 */
|
|
|
|
|
|
.calendar {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
|
|
|
grid-template-rows: repeat(6, 1fr);
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell {
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-content {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell:hover .day-content {
|
|
|
|
|
|
background-color: #f2f6fc;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell.today .day-content {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
border: 1px solid #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell.other-month .day-content {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell.start-date .day-content,
|
|
|
|
|
|
.day-cell.end-date .day-content {
|
|
|
|
|
|
background: #409eff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 18:06:58 +08:00
|
|
|
|
.day-cell.focused .day-content {
|
|
|
|
|
|
box-shadow: 0 0 0 2px #409eff;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 15:19:16 +08:00
|
|
|
|
.day-cell.in-range:before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background: #f0f9ff;
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell.start-date.in-range:before {
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell.end-date.in-range:before {
|
|
|
|
|
|
right: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 中间分隔线 */
|
|
|
|
|
|
.panel-divider {
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
background-color: #e4e7ed;
|
|
|
|
|
|
/*margin: 20px 0;*/
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 面板底部 */
|
|
|
|
|
|
.panel-footer {
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
border-top: 1px solid #e4e7ed;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-btn {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-btn:hover {
|
|
|
|
|
|
color: #66b1ff;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Element UI 按钮样式 */
|
|
|
|
|
|
.el-button {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
transition: 0.1s;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
padding: 7px 15px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
min-width: 60px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--mini {
|
|
|
|
|
|
padding: 7px 15px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
min-width: 50px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-plain:hover {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-plain:disabled, .is-plain:disabled:hover {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-color: #ebeef5;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
::v-deep .el-input.is-disabled .el-range-input {
|
|
|
|
|
|
background: #F5F7FA!important;
|
|
|
|
|
|
border-color: #e4e7ed!important;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|