hldy_app_mini/component/public/calendar.vue

348 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="calendar">
<view class="header">
<view class="header-title">
<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>
<view class="year-month">{{ year }}{{ month + 1 }}</view>
<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" />
</view>
</view>
<view class="weekdays">
<view v-for="(day, idx) in weekdays" :key="idx" class="weekday">{{ day }}</view>
</view>
</view>
<view class="days">
<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)">
<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
// 初始当前年月
const now = new Date();
const year = ref(now.getFullYear());
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;
}
// 格式化 keym 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}`;
}
// 星期一..日(与之前一致)
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
// 计算某月第一天是周几(调整为 Monday = 0
const firstWeekdayOfMonth = (y, m) => {
const d = new Date(y, m, 1).getDay(); // 0 (Sun) - 6 (Sat)
return (d + 6) % 7; // 转成 Monday=0 .. Sunday=6
};
// 生成 6*7 (42) 个格子的 cells包含前后月份
function buildCells(y, m) {
const list = [];
// 上个月信息
const prevDate = new Date(y, m, 0); // 上个月最后一天
const prevYear = prevDate.getFullYear();
const prevMonth = prevDate.getMonth();
const prevTotal = prevDate.getDate();
const startOffset = firstWeekdayOfMonth(y, m);
// 前置填充
for (let i = 0; i < startOffset; i++) {
const day = prevTotal - startOffset + i + 1;
const lunar = solarlunar.solar2lunar(prevYear, prevMonth + 1, day);
const dateNumber = toDateNumber(prevYear, prevMonth, day);
list.push({
key: `prev-${prevYear}-${prevMonth + 1}-${day}`,
dateText: day,
lunarText: lunar ? lunar.dayCn : '',
prev: true,
next: false,
year: prevYear,
month: prevMonth,
day,
dateNumber,
});
}
// 当前月
const totalDays = new Date(y, m + 1, 0).getDate();
for (let d = 1; d <= totalDays; d++) {
const lunar = solarlunar.solar2lunar(y, m + 1, d);
const dateNumber = toDateNumber(y, m, d);
list.push({
key: formatKey(y, m, d),
dateText: d,
lunarText: lunar ? lunar.dayCn : '',
prev: false,
next: false,
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,
});
}
return list;
}
// 当前视图的 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);
});
// 判断是否 start / end
function isStart(key) {
return startKey.value === key;
}
function isEnd(key) {
return endKey.value === key;
}
// 判断是否在范围内(包含边界)
function isInRange(dateNumber) {
if (!startNumber.value || !endNumber.value) return false;
return dateNumber >= startNumber.value && dateNumber <= endNumber.value;
}
const emit = defineEmits(["datachange"]); // 定义事件名
// 选择日期逻辑:支持任意先后顺序的两次点击来形成区间
function selectDate(cell) {
// 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;
}
// 已有 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;
}
// 年/月 切换
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++;
}
}
function prevYear() {
year.value--;
}
function nextYear() {
year.value++;
}
</script>
<style scoped lang="less">
.calendar {
padding: 16px;
background: #fff;
border-radius: 30rpx;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
// max-width: 720rpx;
margin: 0 auto;
}
.header {
width: 100%;
display: flex;
flex-direction: column;
}
.header-title {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.year-month {
font-size: 18px;
font-weight: bold;
}
.weekdays {
display: flex;
background-color: #F8F8FA;
border-radius: 18rpx;
padding: 8rpx;
margin-top: 8rpx;
}
.weekday {
flex: 1;
text-align: center;
font-size: 12px;
}
.days {
display: flex;
flex-wrap: wrap;
padding: 8rpx 0;
}
.day-cell {
width: calc(100% / 7);
height: 78rpx;
text-align: center;
padding-top: 8rpx;
box-sizing: border-box;
position: relative;
}
/* 前后月份灰色显示 */
.day-cell.prev-month .gregorian,
.day-cell.next-month .gregorian {
color: #ccc;
}
/* 范围内中间样式 */
.day-cell.in-range {
background-color: #E6F7FF;
}
/* 开始/结束 的样式 */
.day-cell.start,
.day-cell.end {
background-color: #0B98DC;
border-radius: 8rpx;
}
.day-cell.start .gregorian,
.day-cell.end .gregorian,
.day-cell.start .lunar,
.day-cell.end .lunar {
color: #fff;
}
.gregorian {
font-size: 14px;
}
.lunar {
font-size: 10px;
color: #888;
}
.head-left{
width: 100rpx;
display: flex;
justify-content: space-between;
.head-img{
width: 40rpx;
height: 40rpx;
}
}
</style>