This commit is contained in:
Mauve
2026-09-16 10:43:59 +08:00
parent a7f2e0ca01
commit 9f2becfa67
36 changed files with 3997 additions and 264 deletions
@@ -0,0 +1,389 @@
<template>
<view class="room-calendar-grid">
<view class="weekdays">
<view v-for="day in weekdayLabels" :key="day" class="weekday-cell">{{ day }}</view>
</view>
<scroll-view scroll-y class="calendar-scroll" :scroll-top="calendarScrollTop" scroll-with-animation>
<view v-for="month in months" :key="month.monthKey" class="month-section">
<view class="month-marker">{{ month.monthLabel }}</view>
<view v-for="week in month.weeks" :key="getWeekId(month, week)">
<view class="week-block">
<view class="date-row">
<view
v-for="day in week.days"
:key="day.date"
class="date-cell"
:class="{ today: day.isToday, past: day.isPast, 'outside-month': day.isOutsideMonth }"
>
{{ day.dayNo }}
</view>
</view>
<view
v-for="roomType in roomTypes"
:key="week.weekKey + roomType.code"
class="data-row"
>
<view
v-for="day in week.days"
:key="day.date + roomType.code"
class="data-cell"
:class="{
today: day.isToday,
past: day.isPast,
'outside-month': day.isOutsideMonth,
'center-content': centerContent,
}"
>
<template v-if="!day.isOutsideMonth">
<view class="room-code" :class="getRoomCodeSizeClass(roomType)">{{ getDisplayRoomName(roomType) }}</view>
<view
class="room-criteria"
:class="mode === 'qualification' ? getCriteriaStatusClass(getCellData(day, roomType)) : ''"
>
{{ getCellData(day, roomType).criteria }}
</view>
<view
v-if="mode === 'availability'"
class="room-count"
:class="getAvailabilityStatusClass(getCellData(day, roomType))"
>
{{ getAvailabilityText(getCellData(day, roomType)) }}
</view>
<view
v-if="showLeader && hasLeader(day, roomType)"
class="room-leader"
>
{{ getCellData(day, roomType).leader }}
</view>
</template>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import {
getAvailabilityStatusClass,
getCriteriaStatusClass,
} from '../../common/roomCalendarUi.js';
export default {
name: 'room-calendar-grid',
props: {
months: {
type: Array,
default: () => [],
},
weekdayLabels: {
type: Array,
default: () => [],
},
roomTypes: {
type: Array,
default: () => [],
},
dataMap: {
type: Object,
default: () => ({}),
},
mode: {
type: String,
default: 'availability',
},
initialWeekId: {
type: String,
default: '',
},
centerContent: {
type: Boolean,
default: false,
},
showLeader: {
type: Boolean,
default: true,
},
},
data() {
return {
calendarScrollTop: 0,
};
},
watch: {
initialWeekId() {
this.scrollToInitialWeek();
},
months() {
this.scrollToInitialWeek();
},
roomTypes() {
this.scrollToInitialWeek();
},
},
mounted() {
this.scrollToInitialWeek();
},
methods: {
getCellKey(date, roomTypeCode) {
return `${date}_${roomTypeCode}`;
},
getWeekId(month, week) {
return `week-${month.monthKey}-${week.weekKey}`;
},
scrollToInitialWeek() {
this.$nextTick(() => {
const scrollTop = this.getInitialWeekScrollTop();
this.calendarScrollTop = scrollTop === 0 ? 0.1 : scrollTop;
this.$nextTick(() => {
if (scrollTop === 0) {
this.calendarScrollTop = 0;
}
});
});
},
getInitialWeekScrollTop() {
if (!this.initialWeekId) return 0;
const weekHeight = this.getWeekHeightPx();
const monthMarkerHeight = uni.upx2px(38);
let scrollTop = 0;
for (const month of this.months) {
const weekIndex = (month.weeks || []).findIndex(
week => this.getWeekId(month, week) === this.initialWeekId
);
if (weekIndex >= 0) {
return scrollTop + weekIndex * weekHeight;
}
scrollTop += monthMarkerHeight + (month.weeks || []).length * weekHeight;
}
return 0;
},
getWeekHeightPx() {
return uni.upx2px(44 + this.roomTypes.length * 128);
},
getDisplayRoomName(roomType) {
return roomType.displayName || roomType.label;
},
getRoomCodeSizeClass(roomType) {
return this.getDisplayRoomName(roomType).length > 5 ? 'compact' : '';
},
getCellData(day, roomType) {
return this.dataMap[this.getCellKey(day.date, roomType.code)] || {
criteria: '-',
count: 0,
waitlist: false,
waitlistCount: 0,
qualified: false,
statusText: '',
};
},
getAvailabilityStatusClass(item) {
return getAvailabilityStatusClass(item);
},
getAvailabilityText(item) {
if (item.waitlist) return item.waitlistCount;
return item.count;
},
getCriteriaStatusClass(item) {
return getCriteriaStatusClass(item);
},
hasLeader(day, roomType) {
const leader = this.getCellData(day, roomType).leader;
return leader !== undefined && leader !== null && String(leader).trim() !== '';
},
},
};
</script>
<style lang="scss" scoped>
.room-calendar-grid {
display: flex;
flex: 1;
flex-direction: column;
height: auto;
min-height: 0;
}
.calendar-scroll {
flex: 1;
height: auto;
min-height: 0;
}
.weekdays,
.date-row,
.data-row {
display: flex;
flex-direction: row;
}
.weekdays {
flex-shrink: 0;
}
.weekday-cell {
background-color: #c39a4b;
color: #ffffff;
font-family: "Inter-Bold";
font-size: 24rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
width: 14.2857%;
}
.month-section {
padding-top: 38rpx;
position: relative;
}
.week-block {
border-left: 1rpx solid #e1e1e1;
border-right: 1rpx solid #e1e1e1;
}
.month-marker {
background-color: #c39a4b;
border-top: 1rpx solid rgba(255, 255, 255, 0.5);
color: #ffffff;
font-family: "Inter-Bold";
font-size: 24rpx;
height: 38rpx;
letter-spacing: 0;
line-height: 38rpx;
margin-top: -38rpx;
padding-left: 16rpx;
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 5;
}
.date-cell {
background-color: #dfc895;
border-bottom: 1rpx solid #d4bd88;
border-right: 1rpx solid #d4bd88;
color: #111111;
font-family: "Inter-Bold";
font-size: 28rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
width: 14.2857%;
}
.date-cell.today {
background-color: #c39a4b;
color: #ffffff;
}
.data-cell.today {
background-color: #fff7e6;
// box-shadow: inset 0 0 0 2rpx #c39a4b;
}
.date-cell.past,
.data-cell.past {
background-color: #eeeeee;
color: #9a9a9a;
}
.data-cell.past .room-code,
.data-cell.past .room-criteria,
.data-cell.past .room-count,
.data-cell.past .room-leader {
color: #9a9a9a;
}
.date-cell.outside-month,
.data-cell.outside-month {
background-color: #f7f7f7;
color: transparent;
}
.data-cell {
background-color: #ffffff;
border-bottom: 1rpx solid #e1e1e1;
border-right: 1rpx solid #e1e1e1;
box-sizing: border-box;
min-height: 128rpx;
padding: 12rpx 2rpx 8rpx;
text-align: center;
width: 14.2857%;
}
.data-cell.center-content {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.data-cell.center-content .room-code,
.data-cell.center-content .room-criteria {
width: 100%;
}
.room-code {
color: #111111;
font-family: "Inter-Bold";
font-size: 23rpx;
line-height: 32rpx;
min-height: 32rpx;
white-space: nowrap;
}
.room-code.compact {
font-size: 20rpx;
}
.room-criteria {
color: #111111;
font-family: "Inter-Bold";
font-size: 22rpx;
line-height: 28rpx;
min-height: 28rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.room-count {
// font-family: "Inter-Bold";
// font-size: 28rpx;
// line-height: 34rpx;
// margin-top: 6rpx;
font-size: 21rpx;
}
.room-count.available {
color: #1d8b48;
}
.room-count.zero {
color: #d0021b;
}
.room-leader {
color: #111111;
font-family: "Inter-Bold";
font-size: 18rpx;
line-height: 24rpx;
margin-top: 6rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.room-criteria.qualified {
color: #1d8b48;
}
.room-criteria.unqualified {
color: #d0021b;
}
</style>