574 lines
13 KiB
Vue
574 lines
13 KiB
Vue
<template>
|
||
<view class="compact-datetime-picker">
|
||
<!-- 输入框区域 -->
|
||
<view class="input-container">
|
||
<view class="compact-input" :class="{ active: activeType === 'start' }" @click="openPicker('start')">
|
||
{{ formattedStart || '开始日期' }}
|
||
</view>
|
||
<text class="separator">至</text>
|
||
<view class="compact-input" :class="{ active: activeType === 'end' }" @click="openPicker('end')">
|
||
{{ formattedEnd || '结束日期' }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期选择弹窗 -->
|
||
<uni-popup ref="popup" type="bottom" :is-mask-click="false">
|
||
<view class="compact-popup">
|
||
<!-- 快捷按钮区域 -->
|
||
<view class="quick-actions">
|
||
<view class="mode-switch">
|
||
<text :class="['mode-btn', { active: isNatural }]" @click="toggleMode(true)">自然周期</text>
|
||
<text :class="['mode-btn', { active: !isNatural }]" @click="toggleMode(false)">相对周期</text>
|
||
</view>
|
||
<!-- 快捷操作 -->
|
||
<view class="action-buttons">
|
||
<button v-if="isNatural" v-for="btn in quickButtonszr" size="mini" :key="btn.type" class="action-btn" @click="setQuickRange(btn.type)">
|
||
{{ btn.label }}
|
||
</button>
|
||
<button v-else v-for="btn in quickButtonsxd" size="mini" :key="btn.type" class="action-btn" @click="setQuickRange(btn.type)">
|
||
{{ btn.label }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日历选择区域 -->
|
||
<view class="compact-calendar">
|
||
<view class="compact-header">
|
||
<text class="nav-arrow" @click="changeMonth(-1)">‹</text>
|
||
<text class="month-title">{{ monthText }}</text>
|
||
<text class="nav-arrow" @click="changeMonth(1)">›</text>
|
||
</view>
|
||
|
||
<view class="compact-weekdays">
|
||
<text v-for="day in weekDays" :key="day" class="weekday">
|
||
{{ day }}
|
||
</text>
|
||
</view>
|
||
|
||
<view class="compact-days">
|
||
<text
|
||
v-for="(day, index) in calendarDays"
|
||
:key="index"
|
||
:class="[
|
||
'compact-day',
|
||
{
|
||
'other-month': !day.isCurrentMonth,
|
||
'selected-start': isStart(day.date),
|
||
'selected-end': isEnd(day.date),
|
||
'in-range': isInRange(day.date),
|
||
today: isToday(day.date)
|
||
}
|
||
]"
|
||
@click="handleDayClick(day)"
|
||
>
|
||
{{ day.day }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="compact-footer">
|
||
<text class="footer-btn cancel" @click="closePicker">取消</text>
|
||
<text class="footer-btn confirm" @click="confirmSelection">确定</text>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
const MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
|
||
|
||
export default {
|
||
props: {
|
||
modelValue: {
|
||
type: Array,
|
||
default: () => [null, null]
|
||
}
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
isNatural: true,
|
||
activeType: 'start',
|
||
currentMonth: new Date(),
|
||
tempStart: null,
|
||
tempEnd: null,
|
||
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
||
quickButtonszr: [
|
||
{ label: '本周', type: 'week' },
|
||
{ label: '本月', type: 'month' },
|
||
{ label: '本季', type: 'quarter' },
|
||
{ label: '本年', type: 'year' }
|
||
],
|
||
quickButtonsxd: [
|
||
{ label: '近一周', type: 'week' },
|
||
{ label: '近一月', type: 'month' },
|
||
{ label: '近一季', type: 'quarter' },
|
||
{ label: '近一年', type: 'year' }
|
||
]
|
||
};
|
||
},
|
||
|
||
filters: {
|
||
naturalMonth(date) {
|
||
return `${date.getFullYear()}年${(date.getMonth() + 1).toString().padStart(2, '0')}月`;
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
formattedStart() {
|
||
return this.formatDate(this.modelValue[0]);
|
||
},
|
||
formattedEnd() {
|
||
return this.formatDate(this.modelValue[1]);
|
||
},
|
||
calendarDays() {
|
||
return this.generateCalendar(this.currentMonth);
|
||
},
|
||
monthText() {
|
||
const [year, month] = this.formatDate(this.currentMonth).split('-');
|
||
return `${year}年${month}月`;
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
modelValue: {
|
||
immediate: true,
|
||
handler(newVal) {
|
||
// console.log(newVal);
|
||
this.tempStart = newVal[0];
|
||
this.tempEnd = newVal[1];
|
||
}
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
// 打开弹窗并初始化临时值
|
||
openPicker(type) {
|
||
this.activeType = type;
|
||
if (!this.tempStart) this.tempStart = new Date();
|
||
if (!this.tempEnd) this.tempEnd = new Date();
|
||
this.$refs.popup.open();
|
||
},
|
||
|
||
// 日期点击处理
|
||
handleDayClick(day) {
|
||
if (!day.isCurrentMonth) return;
|
||
|
||
const clickedDate = day.date;
|
||
|
||
if (this.activeType === 'start') {
|
||
if (clickedDate > this.tempEnd) {
|
||
this.tempStart = clickedDate;
|
||
this.tempEnd = clickedDate;
|
||
this.activeType = 'end';
|
||
} else {
|
||
this.tempStart = clickedDate;
|
||
}
|
||
} else {
|
||
if (clickedDate < this.tempStart) {
|
||
this.tempStart = clickedDate;
|
||
this.tempEnd = clickedDate;
|
||
this.activeType = 'start';
|
||
} else {
|
||
this.tempEnd = clickedDate;
|
||
}
|
||
}
|
||
},
|
||
|
||
// 确认选择
|
||
confirmSelection() {
|
||
this.$emit('update:modelValue', [this.tempStart, this.tempEnd]);
|
||
this.closePicker();
|
||
},
|
||
|
||
// 关闭弹窗
|
||
closePicker() {
|
||
this.$refs.popup.close();
|
||
this.resetTempDates();
|
||
},
|
||
|
||
// 重置临时日期
|
||
resetTempDates() {
|
||
this.tempStart = this.modelValue[0];
|
||
this.tempEnd = this.modelValue[1];
|
||
},
|
||
// 生成日历数据
|
||
generateCalendar(date) {
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth();
|
||
const firstDay = new Date(year, month, 1);
|
||
const lastDay = new Date(year, month + 1, 0);
|
||
const startDay = firstDay.getDay();
|
||
const days = [];
|
||
|
||
// 填充上月日期
|
||
for (let i = startDay; i > 0; i--) {
|
||
const d = new Date(year, month, -i + 1);
|
||
days.push({
|
||
day: d.getDate(),
|
||
date: d,
|
||
isCurrentMonth: false
|
||
});
|
||
}
|
||
|
||
// 填充本月日期
|
||
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||
const d = new Date(year, month, i);
|
||
days.push({
|
||
day: i,
|
||
date: d,
|
||
isCurrentMonth: true
|
||
});
|
||
}
|
||
|
||
// 修正后的下月日期填充
|
||
const totalCells = Math.ceil(days.length / 7) * 7;
|
||
let nextMonthDay = 1;
|
||
while (days.length < totalCells) {
|
||
const d = new Date(year, month + 1, nextMonthDay);
|
||
days.push({
|
||
day: d.getDate(),
|
||
date: d,
|
||
isCurrentMonth: false
|
||
});
|
||
nextMonthDay++;
|
||
}
|
||
|
||
return days;
|
||
},
|
||
// 日期选择处理
|
||
selectDate(date) {
|
||
if (!date.isCurrentMonth) return;
|
||
|
||
const newValue = [...this.modelValue];
|
||
const index = this.activeType === 'start' ? 0 : 1;
|
||
newValue[index] = date.date;
|
||
|
||
// 自动调整日期顺序
|
||
if (newValue[0] && newValue[1] && newValue[0] > newValue[1]) {
|
||
[newValue[0], newValue[1]] = [newValue[1], newValue[0]];
|
||
}
|
||
|
||
this.$emit('update:modelValue', newValue);
|
||
},
|
||
|
||
// 切换月份
|
||
changeMonth(offset) {
|
||
const newMonth = new Date(this.currentMonth);
|
||
newMonth.setMonth(newMonth.getMonth() + offset);
|
||
this.currentMonth = newMonth;
|
||
// console.log(this.currentMonth);
|
||
},
|
||
|
||
// 判断日期状态
|
||
isSelected(date) {
|
||
return date === this.modelValue[0] || date === this.modelValue[1];
|
||
},
|
||
|
||
isEnd(date) {
|
||
return date === this.modelValue[1];
|
||
},
|
||
|
||
isToday(date) {
|
||
return this.formatDate(date) === this.formatDate(new Date());
|
||
},
|
||
isStart(date) {
|
||
return this.tempStart && this.isSameDay(date, this.tempStart);
|
||
},
|
||
|
||
isEnd(date) {
|
||
return this.tempEnd && this.isSameDay(date, this.tempEnd);
|
||
},
|
||
|
||
// 改进范围判断
|
||
isInRange(date) {
|
||
if (!this.tempStart || !this.tempEnd) return false;
|
||
return date > this.tempStart && date < this.tempEnd;
|
||
},
|
||
|
||
isSameDay(d1, d2) {
|
||
return d1 && d2 && d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
|
||
},
|
||
// 快捷范围设置
|
||
setQuickRange(type) {
|
||
const currentDate = new Date(); // 使用当前日期
|
||
const range = this.isNatural ? this.calcNaturalRange(type, currentDate) : this.calcRelativeRange(type, currentDate);
|
||
|
||
this.$emit('update:modelValue', [range.start, range.end]);
|
||
this.$refs.popup.close();
|
||
},
|
||
|
||
// 自然周期计算
|
||
calcNaturalRange(type, currentDate) {
|
||
let start, end;
|
||
|
||
switch (type) {
|
||
case 'week':
|
||
start = this.getWeekStart(currentDate);
|
||
end = this.getWeekEnd(start);
|
||
break;
|
||
case 'month':
|
||
start = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||
end = this.lastDayOfMonth(currentDate);
|
||
break;
|
||
case 'quarter':
|
||
const quarter = Math.floor(currentDate.getMonth() / 3);
|
||
start = new Date(currentDate.getFullYear(), quarter * 3, 1);
|
||
end = this.lastDayOfMonth(new Date(currentDate.getFullYear(), quarter * 3 + 2, 1));
|
||
break;
|
||
case 'year':
|
||
start = new Date(currentDate.getFullYear(), 0, 1);
|
||
end = new Date(currentDate.getFullYear(), 11, 31);
|
||
break;
|
||
}
|
||
return { start, end };
|
||
},
|
||
|
||
// 相对周期计算
|
||
calcRelativeRange(type, currentDate) {
|
||
let start = new Date(currentDate);
|
||
|
||
switch (type) {
|
||
case 'week':
|
||
start.setDate(currentDate.getDate() - 6);
|
||
break;
|
||
case 'month':
|
||
start.setMonth(currentDate.getMonth() - 1);
|
||
this.adjustMonthEnd(start, currentDate);
|
||
break;
|
||
case 'quarter':
|
||
start.setMonth(currentDate.getMonth() - 3);
|
||
this.adjustMonthEnd(start, currentDate);
|
||
break;
|
||
case 'year':
|
||
start.setFullYear(currentDate.getFullYear() - 1);
|
||
this.adjustMonthEnd(start, currentDate);
|
||
break;
|
||
}
|
||
return { start, end: new Date(currentDate) };
|
||
},
|
||
|
||
// 周计算(周一为起点)
|
||
getWeekStart(date) {
|
||
const day = date.getDay();
|
||
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
|
||
return new Date(date.setDate(diff));
|
||
},
|
||
|
||
getWeekEnd(startDate) {
|
||
const end = new Date(startDate);
|
||
end.setDate(startDate.getDate() + 6);
|
||
return end;
|
||
},
|
||
|
||
// 月末处理
|
||
lastDayOfMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||
},
|
||
|
||
// 月末日调整
|
||
adjustMonthEnd(start, end) {
|
||
const lastDay = this.lastDayOfMonth(start).getDate();
|
||
if (end.getDate() > lastDay) {
|
||
start.setDate(lastDay);
|
||
} else {
|
||
start.setDate(end.getDate());
|
||
}
|
||
},
|
||
|
||
// 闰年判断
|
||
isLeapYear(year) {
|
||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||
},
|
||
|
||
// 模式切换
|
||
toggleMode(isNatural) {
|
||
this.isNatural = isNatural;
|
||
},
|
||
|
||
// 日期格式化 monthFormat
|
||
formatDate(date) {
|
||
if (!date || !(date instanceof Date)) return '';
|
||
const pad = (n) => n.toString().padStart(2, '0');
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.input-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
/* 紧凑型输入框 */
|
||
.compact-input {
|
||
flex: 1;
|
||
padding: 6px 8px;
|
||
font-size: 16px;
|
||
height: 20px;
|
||
line-height: 20px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
.compact-input.active {
|
||
border-color: #409eff;
|
||
background-color: #f5f7ff;
|
||
}
|
||
|
||
/* 紧凑弹窗 */
|
||
.compact-popup {
|
||
padding: 12px 8px;
|
||
background: #fff;
|
||
border-radius: 12px 12px 0 0;
|
||
}
|
||
|
||
/* 紧凑头部 */
|
||
.compact-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 8px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.nav-arrow {
|
||
font-size: 20px;
|
||
padding: 0 12px;
|
||
color: #606266;
|
||
}
|
||
|
||
.month-title {
|
||
font-weight: 500;
|
||
min-width: 120px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 紧凑日期网格 */
|
||
.compact-weekdays {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.weekday {
|
||
text-align: center;
|
||
font-size: 20px;
|
||
color: #909399;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.compact-days {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 1px;
|
||
}
|
||
/* 修改非本月日期样式 */
|
||
.compact-day.other-month {
|
||
color: #c0c4cc;
|
||
opacity: 0.8;
|
||
}
|
||
.compact-day {
|
||
aspect-ratio: 1;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* 日期状态 */
|
||
.compact-day.today {
|
||
font-weight: bold;
|
||
color: #409eff;
|
||
background: #e8f3ff;
|
||
}
|
||
|
||
.compact-day.selected-start {
|
||
background: #409eff;
|
||
color: white;
|
||
border-radius: 3px 0 0 3px;
|
||
}
|
||
|
||
.compact-day.selected-end {
|
||
background: #409eff;
|
||
color: white;
|
||
border-radius: 0 3px 3px 0;
|
||
}
|
||
|
||
.compact-day.in-range {
|
||
background: #ccd2d8;
|
||
}
|
||
.quick-actions {
|
||
/* padding: 0 10px; */
|
||
}
|
||
|
||
.mode-switch {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.mode-btn {
|
||
flex: 1;
|
||
padding: 6px 8px;
|
||
text-align: center;
|
||
color: #606266;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: #409eff;
|
||
color: #fff;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 4px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.action-btn {
|
||
background: #f5f7fa;
|
||
color: #606266;
|
||
height: 24px;
|
||
font-size: 14px;
|
||
display: flex;
|
||
justify-content: center; /* 水平居中 */
|
||
align-items: center; /* 垂直居中 */
|
||
text-align: center;
|
||
}
|
||
/* 底部操作 */
|
||
.compact-footer {
|
||
display: flex;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.footer-btn {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.cancel {
|
||
color: #606266;
|
||
}
|
||
|
||
.confirm {
|
||
color: #409eff;
|
||
font-weight: 500;
|
||
}
|
||
</style>
|