hldy_app_mini/component/public/calendarsimple.vue

475 lines
11 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,
'selected': isSelected(cell.key),
'disabled': isDisabled(cell)
}" @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,
watch
} from 'vue';
import solarlunar from 'solarlunar'; // npm install solarlunar
// 支持对象和旧类型String/Date/Number
const props = defineProps({
modelValue: {
type: [String, Date, Number, Object],
default: null
}
});
const emit = defineEmits(['update:modelValue', 'datachange']);
// 当前视图年月
const now = new Date();
const year = ref(now.getFullYear());
const month = ref(now.getMonth()); // 0-based
// 选中 keyYYYY-MM-DD 格式字符串)或 null
const selectedKey = ref(null);
// 如果外部传入的是对象,我们记录这个事实,以便 emit 回对象
let incomingWasObject = false;
// 保存时间部分(针对字符串/Date/Number 输入)
// 如果输入是对象,则不使用时间部分
let lastKnownTime = null;
// 辅助:把年月日转成便于比较的数字 YYYYMMDDm 为 0-based
function toDateNumber(y, m, d) {
const mm = m + 1;
return y * 10000 + mm * 100 + d;
}
// 获取今天的 YYYYMMDD按本地时间
function getTodayNumber() {
const d = new Date();
return toDateNumber(d.getFullYear(), d.getMonth(), d.getDate());
}
// 格式化 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)
return (d + 6) % 7; // Monday=0 .. Sunday=6
};
// 生成 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);
list.push({
key: `prev-${prevYear}-${prevMonth + 1}-${day}`,
dateText: day,
lunarText: lunar ? lunar.dayCn : '',
prev: true,
next: false,
year: prevYear,
month: prevMonth,
day,
dateNumber: toDateNumber(prevYear, prevMonth, day),
});
}
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,
dateNumber: toDateNumber(y, m, d),
});
}
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,
dateNumber: toDateNumber(ny, nm, i),
});
}
return list;
}
const cells = computed(() => buildCells(year.value, month.value));
function isSelected(key) {
return selectedKey.value === key;
}
// 是否禁用(大于今天)
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 字符串(与旧行为兼容)
function selectDate(cell) {
// 禁用或跨月的格子不可选
if (cell.prev || cell.next) return;
if (isDisabled(cell)) return;
const key = formatKey(cell.year, cell.month, cell.day);
// 取消选择
if (selectedKey.value === key) {
selectedKey.value = null;
emit('update:modelValue', null);
emit('datachange', {
date: null
});
return;
}
// 选中
selectedKey.value = key;
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
});
}
}
// 年月切换
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;
cursor: pointer;
}
/* 前后月份灰色显示 */
.day-cell.prev-month .gregorian,
.day-cell.next-month .gregorian {
color: #ccc;
}
/* 禁用未来日期样式 & 行为 */
.day-cell.disabled {
pointer-events: none;
/* 完全不可点击 */
opacity: 1;
/* 保持背景但把文字变灰 */
}
.day-cell.disabled .gregorian,
.day-cell.disabled .lunar {
color: #ccc !important;
}
/* 选中单选样式 */
.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>