Files
YGChatCS/components/Calender/index.vue
2025-08-02 13:59:29 +08:00

423 lines
10 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>
<uni-popup ref="popup" type="bottom" @maskClick="handleMaskClick">
<!-- 弹窗主体 -->
<view class="calendar-popup" @tap.stop>
<!-- 头部区域 -->
<view class="calendar-header">
<view class="header-content">
<text class="header-title">日历选择</text>
<text class="header-subtitle"
>选择入住和离店日期以下价格为单晚参考价</text
>
</view>
<view class="header-close" @tap="handleClose">
<uni-icons type="closeempty" size="20" color="#8c8c8c"></uni-icons>
</view>
</view>
<!-- 周标题行 - 固定显示 -->
<view class="week-header">
<text class="week-day" v-for="day in weekDays" :key="day">
{{ day }}
</text>
</view>
<!-- 日历主体区域 -->
<view class="calendar-body">
<!-- 全年月份显示区域 -->
<view class="year-container">
<view
class="month-section"
v-for="monthData in yearMonthsGrid"
:key="monthData.key"
>
<text class="month-title">{{ monthData.title }}</text>
<view class="date-grid">
<view
class="date-cell"
v-for="(dateInfo, index) in monthData.grid"
:key="index"
:class="getDateCellClass(dateInfo)"
@tap="handleDateClick(dateInfo)"
>
<template v-if="dateInfo">
<text class="date-label" v-if="dateInfo.label">{{
dateInfo.label
}}</text>
<text class="date-number">{{ dateInfo.day }}</text>
<text class="date-price" v-if="dateInfo.price"
>¥{{ dateInfo.price }}</text
>
</template>
</view>
</view>
</view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import {
ref,
reactive,
computed,
watch,
onMounted,
onBeforeUnmount,
} from "vue";
// 定义组件名称
defineOptions({
name: "Calendar",
});
// uni-popup组件引用
const popup = ref(null);
// 定义Props
const props = defineProps({
// 弹窗显示控制
visible: {
type: Boolean,
default: false,
required: true,
},
// 价格数据对象
priceData: {
type: Object,
default: () => ({}),
},
// 默认选中日期
defaultValue: {
type: [String, Array],
default: "",
},
// 选择模式
mode: {
type: String,
default: "single",
validator: (value) => ["single", "range"].includes(value),
},
// 最小可选日期
minDate: {
type: String,
default: () => new Date().toISOString().split("T")[0],
},
// 最大可选日期
maxDate: {
type: String,
default: "",
},
// 禁用日期数组
disabledDates: {
type: Array,
default: () => [],
},
// 自定义标签
customLabels: {
type: Object,
default: () => ({}),
},
});
// 定义Emits
const emit = defineEmits([
"close",
"select",
"range-select",
"date-click",
"month-change",
]);
// 响应式数据
const weekDays = ref(["一", "二", "三", "四", "五", "六", "日"]);
const selectedDates = ref([]);
const currentYear = ref(new Date().getFullYear());
const isRangeSelecting = ref(false);
const rangeStart = ref(null);
const rangeEnd = ref(null);
const clickTimer = ref(null);
// 计算属性
// 生成从当前月份到明年同月份的日历数据
const yearMonthsGrid = computed(() => {
const months = [];
const now = new Date();
const currentYear = now.getFullYear();
const startMonth = now.getMonth() + 1; // 从当前月份开始getMonth()返回0-11需要+1
const totalMonths = 13; // 显示13个月当前月份到明年同月份
for (let i = 0; i < totalMonths; i++) {
const month = ((startMonth - 1 + i) % 12) + 1;
const year = currentYear + Math.floor((startMonth - 1 + i) / 12);
months.push({
key: `${year}-${month}`,
title: `${year}${month}`,
year: year,
month: month,
grid: generateCalendarGrid(year, month),
});
}
return months;
});
// 方法定义
// 处理遮罩点击
const handleMaskClick = () => {
handleClose();
};
// 处理关闭
const handleClose = () => {
if (popup.value) {
popup.value.close();
}
emit("close");
};
// 获取月份天数
const getDaysInMonth = (year, month) => {
return new Date(year, month, 0).getDate();
};
// 获取月份第一天是星期几 (0=周日, 1=周一...)
const getFirstDayOfMonth = (year, month) => {
const day = new Date(year, month - 1, 1).getDay();
return day === 0 ? 6 : day - 1; // 转换为周一开始 (0=周一, 6=周日)
};
// 生成日历网格数据
const generateCalendarGrid = (year, month) => {
const daysInMonth = getDaysInMonth(year, month);
const firstDay = getFirstDayOfMonth(year, month);
const grid = [];
// 填充空白格子
for (let i = 0; i < firstDay; i++) {
grid.push(null);
}
// 填充日期
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month).padStart(2, "0")}-${String(
day
).padStart(2, "0")}`;
grid.push({
date: dateStr,
day: day,
price: props.priceData[dateStr] || null,
disabled: isDateDisabled(dateStr),
selected: isDateSelected(dateStr),
inRange: isDateInRange(dateStr),
label: getDateLabel(dateStr),
});
}
return grid;
};
// 判断日期是否禁用
const isDateDisabled = (dateStr) => {
const date = new Date(dateStr);
const minDate = new Date(props.minDate);
const maxDate = props.maxDate ? new Date(props.maxDate) : null;
if (date < minDate) return true;
if (maxDate && date > maxDate) return true;
if (props.disabledDates.includes(dateStr)) return true;
return false;
};
// 判断日期是否选中
const isDateSelected = (dateStr) => {
if (props.mode === "single") {
return selectedDates.value.includes(dateStr);
} else {
return dateStr === rangeStart.value || dateStr === rangeEnd.value;
}
};
// 判断日期是否在选择范围内
const isDateInRange = (dateStr) => {
if (props.mode !== "range" || !rangeStart.value || !rangeEnd.value) {
return false;
}
const date = new Date(dateStr);
const start = new Date(rangeStart.value);
const end = new Date(rangeEnd.value);
return date > start && date < end;
};
// 获取日期标签
const getDateLabel = (dateStr) => {
// 优先使用自定义标签
if (props.customLabels[dateStr]) {
return props.customLabels[dateStr];
}
// 为范围选择模式自动生成入住/离店标签
if (props.mode === "range") {
if (dateStr === rangeStart.value) {
return "入住";
}
if (dateStr === rangeEnd.value) {
return "离店";
}
}
return "";
};
// 获取日期格子样式类
const getDateCellClass = (dateInfo) => {
if (!dateInfo) return "date-cell-empty";
const classes = ["date-cell-content"];
if (dateInfo.disabled) classes.push("date-cell-disabled");
if (dateInfo.selected) classes.push("date-cell-selected");
if (dateInfo.inRange) classes.push("date-cell-in-range");
return classes.join(" ");
};
// 处理日期点击
const handleDateClick = (dateInfo) => {
if (!dateInfo || dateInfo.disabled) return;
// 防抖处理
if (clickTimer.value) {
clearTimeout(clickTimer.value);
}
clickTimer.value = setTimeout(() => {
if (props.mode === "single") {
handleSingleSelect(dateInfo);
} else if (props.mode === "range") {
handleRangeSelection(dateInfo);
}
// 触发点击事件
emit("date-click", {
date: dateInfo.date,
price: dateInfo.price,
disabled: dateInfo.disabled,
selected: dateInfo.selected,
});
}, 100);
};
// 处理单选
const handleSingleSelect = (dateInfo) => {
selectedDates.value = [dateInfo.date];
emit("select", {
date: dateInfo.date,
price: dateInfo.price,
mode: "single",
});
};
// 处理范围选择
const handleRangeSelection = (dateInfo) => {
if (!rangeStart.value || (rangeStart.value && rangeEnd.value)) {
// 开始新的范围选择
rangeStart.value = dateInfo.date;
rangeEnd.value = null;
isRangeSelecting.value = true;
} else {
// 完成范围选择
rangeEnd.value = dateInfo.date;
isRangeSelecting.value = false;
// 确保开始日期小于结束日期
if (new Date(rangeStart.value) > new Date(rangeEnd.value)) {
[rangeStart.value, rangeEnd.value] = [rangeEnd.value, rangeStart.value];
}
// 检查日期跨度是否超过28天
const daysBetween = calculateDaysBetween(rangeStart.value, rangeEnd.value);
if (daysBetween > 28) {
// 使用uni.showToast显示错误提示
uni.showToast({
title: '预定时间不能超过28天',
icon: 'none',
duration: 3000
});
// 重置选择状态
rangeStart.value = null;
rangeEnd.value = null;
isRangeSelecting.value = false;
return;
}
emit("range-select", {
startDate: rangeStart.value,
endDate: rangeEnd.value,
startPrice: props.priceData[rangeStart.value],
endPrice: props.priceData[rangeEnd.value],
totalDays: daysBetween,
});
}
};
// 计算两个日期之间的天数
const calculateDaysBetween = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
const diffTime = Math.abs(end - start);
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
// 监听visible属性变化控制uni-popup显示
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
popup.value?.open();
} else {
popup.value?.close();
}
},
{ immediate: true }
);
// 生命周期钩子
onMounted(() => {
// 初始化选中状态
if (props.defaultValue) {
if (Array.isArray(props.defaultValue)) {
rangeStart.value = props.defaultValue[0];
rangeEnd.value = props.defaultValue[1];
} else {
selectedDates.value = [props.defaultValue];
}
}
});
onBeforeUnmount(() => {
if (clickTimer.value) {
clearTimeout(clickTimer.value);
}
});
</script>
<style lang="scss" scoped>
// 引入样式文件
@import "./styles/index.scss";
</style>