2025-11-05 15:59:48 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="calendar">
|
|
|
|
|
|
<view class="header">
|
|
|
|
|
|
<view class="header-title">
|
2025-11-17 16:28:02 +08:00
|
|
|
|
<view class="head-left">
|
|
|
|
|
|
<image class="head-img" src="/static/index/calendar/superleft.png" @click="prevYear" />
|
|
|
|
|
|
<image class="head-img" src="/static/index/calendar/left.png" @click="prevMonth" />
|
|
|
|
|
|
</view>
|
2025-11-05 15:59:48 +08:00
|
|
|
|
<view class="year-month">{{ year }}年{{ month + 1 }}月</view>
|
2025-11-17 16:28:02 +08:00
|
|
|
|
<view class="head-left">
|
|
|
|
|
|
<image class="head-img" src="/static/index/calendar/right.png" @click="nextMonth" />
|
|
|
|
|
|
<image class="head-img" src="/static/index/calendar/superright.png" @click="nextYear" />
|
2025-11-05 15:59:48 +08:00
|
|
|
|
</view>
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
2025-11-05 15:59:48 +08:00
|
|
|
|
</view>
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
2025-11-05 15:59:48 +08:00
|
|
|
|
<view class="weekdays">
|
|
|
|
|
|
<view v-for="(day, idx) in weekdays" :key="idx" class="weekday">{{ day }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="days">
|
2025-11-17 16:28:02 +08:00
|
|
|
|
<view v-for="cell in cells" :key="cell.key" class="day-cell" :class="{
|
|
|
|
|
|
'prev-month': cell.prev,
|
|
|
|
|
|
'next-month': cell.next,
|
|
|
|
|
|
'start': isStart(cell.key),
|
|
|
|
|
|
'end': isEnd(cell.key),
|
|
|
|
|
|
'in-range': isInRange(cell.dateNumber)
|
|
|
|
|
|
}" @click="selectDate(cell)">
|
2025-11-05 15:59:48 +08:00
|
|
|
|
<view class="gregorian">{{ cell.dateText }}</view>
|
|
|
|
|
|
<view class="lunar" v-if="cell.lunarText">{{ cell.lunarText }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import {
|
|
|
|
|
|
ref,
|
|
|
|
|
|
computed
|
|
|
|
|
|
} from 'vue';
|
|
|
|
|
|
import solarlunar from 'solarlunar'; // npm install solarlunar
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 初始当前年月
|
2025-11-05 15:59:48 +08:00
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const year = ref(now.getFullYear());
|
2025-11-17 16:28:02 +08:00
|
|
|
|
const month = ref(now.getMonth()); // 0-based
|
|
|
|
|
|
|
|
|
|
|
|
// 选中开始 / 结束 key (格式: YYYY-MM-DD)
|
|
|
|
|
|
const startKey = ref(null);
|
|
|
|
|
|
const endKey = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 把年月日转成便于比较的数字 YYYYMMDD
|
|
|
|
|
|
function toDateNumber(y, m, d) {
|
|
|
|
|
|
// m 是 0-base
|
|
|
|
|
|
const mm = m + 1;
|
|
|
|
|
|
return y * 10000 + mm * 100 + d;
|
|
|
|
|
|
}
|
2025-11-05 15:59:48 +08:00
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 格式化 key(m 0-base => 输出两位)
|
|
|
|
|
|
function formatKey(y, m, d) {
|
|
|
|
|
|
const mm = String(m + 1).padStart(2, '0');
|
|
|
|
|
|
const dd = String(d).padStart(2, '0');
|
|
|
|
|
|
return `${y}-${mm}-${dd}`;
|
|
|
|
|
|
}
|
2025-11-05 15:59:48 +08:00
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 星期一..日(与之前一致)
|
2025-11-05 15:59:48 +08:00
|
|
|
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 计算某月第一天是周几(调整为 Monday = 0)
|
2025-11-05 15:59:48 +08:00
|
|
|
|
const firstWeekdayOfMonth = (y, m) => {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
const d = new Date(y, m, 1).getDay(); // 0 (Sun) - 6 (Sat)
|
|
|
|
|
|
return (d + 6) % 7; // 转成 Monday=0 .. Sunday=6
|
2025-11-05 15:59:48 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 生成 6*7 (42) 个格子的 cells(包含前后月份)
|
|
|
|
|
|
function buildCells(y, m) {
|
2025-11-05 15:59:48 +08:00
|
|
|
|
const list = [];
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 上个月信息
|
|
|
|
|
|
const prevDate = new Date(y, m, 0); // 上个月最后一天
|
|
|
|
|
|
const prevYear = prevDate.getFullYear();
|
|
|
|
|
|
const prevMonth = prevDate.getMonth();
|
|
|
|
|
|
const prevTotal = prevDate.getDate();
|
|
|
|
|
|
|
|
|
|
|
|
const startOffset = firstWeekdayOfMonth(y, m);
|
|
|
|
|
|
|
|
|
|
|
|
// 前置填充
|
2025-11-05 15:59:48 +08:00
|
|
|
|
for (let i = 0; i < startOffset; i++) {
|
|
|
|
|
|
const day = prevTotal - startOffset + i + 1;
|
|
|
|
|
|
const lunar = solarlunar.solar2lunar(prevYear, prevMonth + 1, day);
|
2025-11-17 16:28:02 +08:00
|
|
|
|
const dateNumber = toDateNumber(prevYear, prevMonth, day);
|
2025-11-05 15:59:48 +08:00
|
|
|
|
list.push({
|
|
|
|
|
|
key: `prev-${prevYear}-${prevMonth + 1}-${day}`,
|
|
|
|
|
|
dateText: day,
|
2025-11-17 16:28:02 +08:00
|
|
|
|
lunarText: lunar ? lunar.dayCn : '',
|
2025-11-05 15:59:48 +08:00
|
|
|
|
prev: true,
|
|
|
|
|
|
next: false,
|
2025-11-17 16:28:02 +08:00
|
|
|
|
year: prevYear,
|
|
|
|
|
|
month: prevMonth,
|
|
|
|
|
|
day,
|
|
|
|
|
|
dateNumber,
|
2025-11-05 15:59:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 当前月
|
|
|
|
|
|
const totalDays = new Date(y, m + 1, 0).getDate();
|
2025-11-05 15:59:48 +08:00
|
|
|
|
for (let d = 1; d <= totalDays; d++) {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
const lunar = solarlunar.solar2lunar(y, m + 1, d);
|
|
|
|
|
|
const dateNumber = toDateNumber(y, m, d);
|
2025-11-05 15:59:48 +08:00
|
|
|
|
list.push({
|
2025-11-17 16:28:02 +08:00
|
|
|
|
key: formatKey(y, m, d),
|
2025-11-05 15:59:48 +08:00
|
|
|
|
dateText: d,
|
2025-11-17 16:28:02 +08:00
|
|
|
|
lunarText: lunar ? lunar.dayCn : '',
|
2025-11-05 15:59:48 +08:00
|
|
|
|
prev: false,
|
|
|
|
|
|
next: false,
|
2025-11-17 16:28:02 +08:00
|
|
|
|
year: y,
|
|
|
|
|
|
month: m,
|
|
|
|
|
|
day: d,
|
|
|
|
|
|
dateNumber,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尾部补位到 42
|
|
|
|
|
|
const need = 42 - list.length;
|
|
|
|
|
|
for (let i = 1; i <= need; i++) {
|
|
|
|
|
|
const nd = new Date(y, m + 1, i);
|
|
|
|
|
|
const ny = nd.getFullYear();
|
|
|
|
|
|
const nm = nd.getMonth();
|
|
|
|
|
|
const lunar = solarlunar.solar2lunar(ny, nm + 1, i);
|
|
|
|
|
|
const dateNumber = toDateNumber(ny, nm, i);
|
|
|
|
|
|
list.push({
|
|
|
|
|
|
key: `next-${ny}-${nm + 1}-${i}`,
|
|
|
|
|
|
dateText: i,
|
|
|
|
|
|
lunarText: lunar ? lunar.dayCn : '',
|
|
|
|
|
|
prev: false,
|
|
|
|
|
|
next: true,
|
|
|
|
|
|
year: ny,
|
|
|
|
|
|
month: nm,
|
|
|
|
|
|
day: i,
|
|
|
|
|
|
dateNumber,
|
2025-11-05 15:59:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
2025-11-05 15:59:48 +08:00
|
|
|
|
return list;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 当前视图的 cells
|
|
|
|
|
|
const cells = computed(() => buildCells(year.value, month.value));
|
|
|
|
|
|
|
|
|
|
|
|
// start/end 转成数值便于比较
|
|
|
|
|
|
const startNumber = computed(() => {
|
|
|
|
|
|
if (!startKey.value) return null;
|
|
|
|
|
|
const [y, m, d] = startKey.value.split('-').map(Number);
|
|
|
|
|
|
return toDateNumber(y, m - 1, d);
|
|
|
|
|
|
});
|
|
|
|
|
|
const endNumber = computed(() => {
|
|
|
|
|
|
if (!endKey.value) return null;
|
|
|
|
|
|
const [y, m, d] = endKey.value.split('-').map(Number);
|
|
|
|
|
|
return toDateNumber(y, m - 1, d);
|
2025-11-05 15:59:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 判断是否 start / end
|
|
|
|
|
|
function isStart(key) {
|
|
|
|
|
|
return startKey.value === key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isEnd(key) {
|
|
|
|
|
|
return endKey.value === key;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 判断是否在范围内(包含边界)
|
|
|
|
|
|
function isInRange(dateNumber) {
|
|
|
|
|
|
if (!startNumber.value || !endNumber.value) return false;
|
|
|
|
|
|
return dateNumber >= startNumber.value && dateNumber <= endNumber.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
const emit = defineEmits(["datachange"]); // 定义事件名
|
|
|
|
|
|
// 选择日期逻辑:支持任意先后顺序的两次点击来形成区间
|
2025-11-05 15:59:48 +08:00
|
|
|
|
function selectDate(cell) {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// console.log("ZZZZ",isInRange(cell.dateNumber))
|
|
|
|
|
|
if (cell.prev || cell.next) return
|
|
|
|
|
|
const key = formatKey(cell.year, cell.month, cell.day);
|
|
|
|
|
|
const num = cell.dateNumber;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有选 start -> 设为 start
|
|
|
|
|
|
if (!startKey.value) {
|
|
|
|
|
|
startKey.value = key;
|
|
|
|
|
|
endKey.value = null;
|
|
|
|
|
|
return;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 已有 start 但无 end
|
|
|
|
|
|
if (startKey.value && !endKey.value) {
|
|
|
|
|
|
// 如果第二次选择晚于或等于 start -> 设为 end
|
|
|
|
|
|
if (num >= startNumber.value) {
|
|
|
|
|
|
endKey.value = key;
|
|
|
|
|
|
// console.log("开始,结束", startKey.value, endKey.value)
|
|
|
|
|
|
emit("datachange", {
|
|
|
|
|
|
start: startKey.value,
|
|
|
|
|
|
end: endKey.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果第二次选择早于 start -> 把早的设为 start,晚的设为 end(即交换)
|
|
|
|
|
|
endKey.value = startKey.value;
|
|
|
|
|
|
startKey.value = key;
|
|
|
|
|
|
// console.log("开始,结束", startKey.value, endKey.value)
|
|
|
|
|
|
emit("datachange", {
|
|
|
|
|
|
start: startKey.value,
|
|
|
|
|
|
end: endKey.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 已有 start 和 end,再次点击任意日期 -> 以该日期作为新的 start,清除 end
|
|
|
|
|
|
startKey.value = key;
|
|
|
|
|
|
endKey.value = null;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
// 年/月 切换
|
2025-11-05 15:59:48 +08:00
|
|
|
|
function prevMonth() {
|
|
|
|
|
|
if (month.value === 0) {
|
|
|
|
|
|
year.value--;
|
|
|
|
|
|
month.value = 11;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
month.value--;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nextMonth() {
|
|
|
|
|
|
if (month.value === 11) {
|
|
|
|
|
|
year.value++;
|
|
|
|
|
|
month.value = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
month.value++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-17 16:28:02 +08:00
|
|
|
|
|
|
|
|
|
|
function prevYear() {
|
|
|
|
|
|
year.value--;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nextYear() {
|
|
|
|
|
|
year.value++;
|
|
|
|
|
|
}
|
2025-11-05 15:59:48 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.calendar {
|
|
|
|
|
|
padding: 16px;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 30rpx;
|
|
|
|
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
// max-width: 720rpx;
|
|
|
|
|
|
margin: 0 auto;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
width: 100%;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-title {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
width: 100%;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
margin-bottom: 10rpx;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.year-month {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.weekdays {
|
|
|
|
|
|
display: flex;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
background-color: #F8F8FA;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
padding: 8rpx;
|
|
|
|
|
|
margin-top: 8rpx;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.weekday {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
font-size: 12px;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.days {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
padding: 8rpx 0;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day-cell {
|
2025-11-17 16:28:02 +08:00
|
|
|
|
width: calc(100% / 7);
|
|
|
|
|
|
height: 78rpx;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding-top: 8rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
position: relative;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
/* 前后月份灰色显示 */
|
2025-11-05 15:59:48 +08:00
|
|
|
|
.day-cell.prev-month .gregorian,
|
|
|
|
|
|
.day-cell.next-month .gregorian {
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
/* 范围内(中间)样式 */
|
|
|
|
|
|
.day-cell.in-range {
|
|
|
|
|
|
background-color: #E6F7FF;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 开始/结束 的样式 */
|
|
|
|
|
|
.day-cell.start,
|
|
|
|
|
|
.day-cell.end {
|
2025-11-05 15:59:48 +08:00
|
|
|
|
background-color: #0B98DC;
|
2025-11-17 16:28:02 +08:00
|
|
|
|
border-radius: 8rpx;
|
2025-11-05 15:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 16:28:02 +08:00
|
|
|
|
.day-cell.start .gregorian,
|
|
|
|
|
|
.day-cell.end .gregorian,
|
|
|
|
|
|
.day-cell.start .lunar,
|
|
|
|
|
|
.day-cell.end .lunar {
|
2025-11-05 15:59:48 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gregorian {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lunar {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
}
|
2025-11-17 16:28:02 +08:00
|
|
|
|
.head-left{
|
|
|
|
|
|
width: 100rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
.head-img{
|
|
|
|
|
|
width: 40rpx;
|
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-05 15:59:48 +08:00
|
|
|
|
</style>
|