feat: 调整项目结构

This commit is contained in:
duanshuwen
2025-09-21 17:25:09 +08:00
parent 0b66462d16
commit 9f23854ad5
410 changed files with 3806 additions and 1668 deletions

View File

@@ -0,0 +1,478 @@
<template>
<uni-popup
ref="popup"
type="bottom"
:safe-area="false"
@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: Array,
default: () => [{ date: "", price: "-", stock: "0" }],
},
// 默认选中日期
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 getPriceForDate = (dateStr) => {
if (!props.priceData || !Array.isArray(props.priceData)) {
return null;
}
const priceItem = props.priceData.find((item) => item.date === dateStr);
return priceItem ? priceItem.price : null;
};
// 生成日历网格数据
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: getPriceForDate(dateStr),
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 && dateStr === rangeEnd.value) {
// 当入住和离店是同一天时,显示"住/离"
return "住/离";
}
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天相同日期跨度为0天允许通过
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;
}
// 生成范围内所有日期的数组
const dateRange = generateDateRange(rangeStart.value, rangeEnd.value);
emit("range-select", {
startDate: rangeStart.value,
endDate: rangeEnd.value,
startPrice: getPriceForDate(rangeStart.value),
endPrice: getPriceForDate(rangeEnd.value),
totalDays: daysBetween,
dateRange: dateRange, // 新增:范围内所有日期的数组
});
}
};
// 生成日期范围内所有日期的数组
const generateDateRange = (startDate, endDate) => {
const dateRange = [];
const start = new Date(startDate);
const end = new Date(endDate);
// 如果是同一天,只返回一个日期
if (startDate === endDate) {
return [
{
date: startDate,
price: getPriceForDate(startDate),
},
];
}
// 生成范围内所有日期
const current = new Date(start);
while (current <= end) {
const dateStr = current.toISOString().split("T")[0];
dateRange.push({
date: dateStr,
price: getPriceForDate(dateStr),
});
current.setDate(current.getDate() + 1);
}
return dateRange;
};
// 计算两个日期之间的天数
const calculateDaysBetween = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
const diffTime = Math.abs(end - start);
const days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// 如果是同一天返回1天而不是0天
return days === 0 ? 1 : days;
};
// 监听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>
// 引入样式文件
@use "./styles/index.scss";
</style>