hldy_app_mini/component/public/calendarsimple.vue

475 lines
11 KiB
Vue
Raw Normal View History

2026-01-09 17:17:45 +08:00
<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,
2026-01-12 17:03:21 +08:00
'selected': isSelected(cell.key),
'disabled': isDisabled(cell)
2026-01-09 17:17:45 +08:00
}" @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,
2026-01-12 17:03:21 +08:00
computed,
watch
2026-01-09 17:17:45 +08:00
} from 'vue';
import solarlunar from 'solarlunar'; // npm install solarlunar
2026-01-12 17:03:21 +08:00
// 支持对象和旧类型String/Date/Number
const props = defineProps({
modelValue: {
2026-01-19 17:35:31 +08:00
2026-01-12 17:03:21 +08:00
default: null
}
});
const emit = defineEmits(['update:modelValue', 'datachange']);
// 当前视图年月
2026-01-09 17:17:45 +08:00
const now = new Date();
const year = ref(now.getFullYear());
const month = ref(now.getMonth()); // 0-based
2026-01-12 17:03:21 +08:00
// 选中 keyYYYY-MM-DD 格式字符串)或 null
2026-01-09 17:17:45 +08:00
const selectedKey = ref(null);
2026-01-12 17:03:21 +08:00
// 如果外部传入的是对象,我们记录这个事实,以便 emit 回对象
let incomingWasObject = false;
// 保存时间部分(针对字符串/Date/Number 输入)
// 如果输入是对象,则不使用时间部分
let lastKnownTime = null;
// 辅助:把年月日转成便于比较的数字 YYYYMMDDm 为 0-based
2026-01-09 17:17:45 +08:00
function toDateNumber(y, m, d) {
const mm = m + 1;
return y * 10000 + mm * 100 + d;
}
2026-01-12 17:03:21 +08:00
// 获取今天的 YYYYMMDD按本地时间
function getTodayNumber() {
const d = new Date();
return toDateNumber(d.getFullYear(), d.getMonth(), d.getDate());
}
2026-01-09 17:17:45 +08:00
// 格式化 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 = ['一', '二', '三', '四', '五', '六', '日'];
const firstWeekdayOfMonth = (y, m) => {
const d = new Date(y, m, 1).getDay(); // 0 (Sun) - 6 (Sat)
2026-01-12 17:03:21 +08:00
return (d + 6) % 7; // Monday=0 .. Sunday=6
2026-01-09 17:17:45 +08:00
};
2026-01-12 17:03:21 +08:00
// 生成 cells和你原来逻辑一致
2026-01-09 17:17:45 +08:00
function buildCells(y, m) {
const list = [];
2026-01-12 17:03:21 +08:00
const prevDate = new Date(y, m, 0);
2026-01-09 17:17:45 +08:00
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);
list.push({
key: `prev-${prevYear}-${prevMonth + 1}-${day}`,
dateText: day,
lunarText: lunar ? lunar.dayCn : '',
prev: true,
next: false,
year: prevYear,
month: prevMonth,
day,
2026-01-12 17:03:21 +08:00
dateNumber: toDateNumber(prevYear, prevMonth, day),
2026-01-09 17:17:45 +08:00
});
}
const totalDays = new Date(y, m + 1, 0).getDate();
for (let d = 1; d <= totalDays; d++) {
const lunar = solarlunar.solar2lunar(y, m + 1, d);
list.push({
key: formatKey(y, m, d),
dateText: d,
lunarText: lunar ? lunar.dayCn : '',
prev: false,
next: false,
year: y,
month: m,
day: d,
2026-01-12 17:03:21 +08:00
dateNumber: toDateNumber(y, m, d),
2026-01-09 17:17:45 +08:00
});
}
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);
list.push({
key: `next-${ny}-${nm + 1}-${i}`,
dateText: i,
lunarText: lunar ? lunar.dayCn : '',
prev: false,
next: true,
year: ny,
month: nm,
day: i,
2026-01-12 17:03:21 +08:00
dateNumber: toDateNumber(ny, nm, i),
2026-01-09 17:17:45 +08:00
});
}
return list;
}
const cells = computed(() => buildCells(year.value, month.value));
function isSelected(key) {
return selectedKey.value === key;
}
2026-01-12 17:03:21 +08:00
// 是否禁用(大于今天)
function isDisabled(cell) {
// 只按年月日比较cell.month 已是 0-based
return toDateNumber(cell.year, cell.month, cell.day) > getTodayNumber();
}
// 解析外部 modelValue返回 { y, m (1-12), d, time|null, isObject:boolean } 或 null
function parseIncomingModel(val) {
if (val == null) return null;
// 对象形式(优先)
if (typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) {
const hasYear = val.year != null && Number(val.year) > 0;
const hasMonth = val.month != null && String(val.month).trim() !== '';
const hasDay = val.day != null && String(val.day).trim() !== '';
if (!hasYear || !hasMonth || !hasDay) {
return null;
}
const y = Number(val.year);
const m = Number(val.month);
const d = Number(val.day);
if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
return {
y,
m,
d,
time: null,
isObject: true
};
}
// Date 对象
if (val instanceof Date) {
return {
y: val.getFullYear(),
m: val.getMonth() + 1,
d: val.getDate(),
time: `${String(val.getHours()).padStart(2, '0')}:${String(val.getMinutes()).padStart(2,'0')}:${String(val.getSeconds()).padStart(2,'0')}`,
isObject: false
};
}
// 数字 -> 视为时间戳
if (typeof val === 'number') {
const dObj = new Date(val);
if (isNaN(dObj.getTime())) return null;
return {
y: dObj.getFullYear(),
m: dObj.getMonth() + 1,
d: dObj.getDate(),
time: `${String(dObj.getHours()).padStart(2,'0')}:${String(dObj.getMinutes()).padStart(2,'0')}:${String(dObj.getSeconds()).padStart(2,'0')}`,
isObject: false
};
}
// 字符串:支持 YYYY-MM-DD、YYYY-MM-DD hh:mm[:ss]
if (typeof val === 'string') {
const parts = val.trim().split(/[ T]/);
const datePart = parts[0];
const timePart = parts[1] || null;
const m = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (m) {
return {
y: Number(m[1]),
m: Number(m[2]),
d: Number(m[3]),
time: timePart ? timePart.split('.')[0] : null,
isObject: false
};
}
// 回退 new Date
const dt = new Date(val);
if (!isNaN(dt.getTime())) {
return {
y: dt.getFullYear(),
m: dt.getMonth() + 1,
d: dt.getDate(),
time: `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`,
isObject: false
};
}
return null;
}
return null;
}
// 把 props.modelValue 应用到组件(设置视图年月、选中项)
// 注意:如果传入日期 > 今天,则不会被设为选中(保持 UI 与“禁用未来日”的逻辑一致)
function applyModelValue(val) {
const parsed = parseIncomingModel(val);
if (!parsed) {
selectedKey.value = null;
lastKnownTime = null;
incomingWasObject = false;
return;
}
incomingWasObject = !!parsed.isObject;
const parsedDateNumber = toDateNumber(parsed.y, parsed.m - 1, parsed.d);
if (parsedDateNumber > getTodayNumber()) {
// 传入是未来日期 -> 不选中(如果你想保留初始选中,请改这里)
selectedKey.value = null;
lastKnownTime = null;
// 仍把视图切到该年月以便用户看到传入的年月(可选:若不想切换可注释下面)
year.value = parsed.y;
month.value = parsed.m - 1;
return;
}
// 设置视图到对应月份parsed.m 是 1-12
year.value = parsed.y;
month.value = parsed.m - 1;
selectedKey.value = formatKey(parsed.y, parsed.m - 1, parsed.d);
lastKnownTime = parsed.time || null;
}
// 组件初始化时应用一次
applyModelValue(props.modelValue);
// 监听外部 modelValue 变化(同步)
watch(() => props.modelValue, (v) => {
applyModelValue(v);
});
// 选择日期(单选,重复点击取消)
// 当外部传入对象时emit 回对象;否则 emit 字符串(与旧行为兼容)
2026-01-09 17:17:45 +08:00
function selectDate(cell) {
2026-01-12 17:03:21 +08:00
// 禁用或跨月的格子不可选
2026-01-09 17:17:45 +08:00
if (cell.prev || cell.next) return;
2026-01-12 17:03:21 +08:00
if (isDisabled(cell)) return;
2026-01-09 17:17:45 +08:00
const key = formatKey(cell.year, cell.month, cell.day);
2026-01-12 17:03:21 +08:00
// 取消选择
2026-01-09 17:17:45 +08:00
if (selectedKey.value === key) {
selectedKey.value = null;
2026-01-12 17:03:21 +08:00
emit('update:modelValue', null);
emit('datachange', {
2026-01-09 17:17:45 +08:00
date: null
});
return;
}
2026-01-12 17:03:21 +08:00
// 选中
2026-01-09 17:17:45 +08:00
selectedKey.value = key;
2026-01-12 17:03:21 +08:00
if (incomingWasObject) {
// 重点:返回的 month 和 day 都是两位字符串(带前导 0
const outObj = {
year: cell.year,
month: String(cell.month + 1).padStart(2, '0'),
day: String(cell.day).padStart(2, '0')
};
emit('update:modelValue', outObj);
emit('datachange', {
date: outObj
});
} else {
let outStr = key;
if (lastKnownTime) outStr = `${key} ${lastKnownTime}`;
emit('update:modelValue', outStr);
emit('datachange', {
date: outStr
});
}
2026-01-09 17:17:45 +08:00
}
2026-01-12 17:03:21 +08:00
// 年月切换
2026-01-09 17:17:45 +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++;
}
}
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);
margin: 0 auto;
overflow: hidden;
}
.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;
2026-01-12 17:03:21 +08:00
cursor: pointer;
2026-01-09 17:17:45 +08:00
}
/* 前后月份灰色显示 */
.day-cell.prev-month .gregorian,
.day-cell.next-month .gregorian {
color: #ccc;
}
2026-01-12 17:03:21 +08:00
/* 禁用(未来日期)样式 & 行为 */
.day-cell.disabled {
pointer-events: none;
/* 完全不可点击 */
opacity: 1;
/* 保持背景,但把文字变灰 */
}
.day-cell.disabled .gregorian,
.day-cell.disabled .lunar {
color: #ccc !important;
}
2026-01-09 17:17:45 +08:00
/* 选中(单选)样式 */
.day-cell.selected {
background-color: #0B98DC;
border-radius: 8rpx;
}
.day-cell.selected .gregorian,
.day-cell.selected .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>