feat: 订单详情交互对接
This commit is contained in:
423
components/Calender/index.vue
Normal file
423
components/Calender/index.vue
Normal file
@@ -0,0 +1,423 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user