Browse Source

预约入馆

master
xuhuajiao 1 month ago
parent
commit
182217775f
  1. 18
      api/reservation.js
  2. 5
      static/iconfont.css
  3. BIN
      static/iconfont.ttf
  4. 476
      subpkg/pages/seat-booking/seat-booking.vue

18
api/reservation.js

@ -0,0 +1,18 @@
import request from '../utils/request';
export function FetchInitLibraryReservationSettings(data) {
return request({
url: '/api/weixin/initLibraryReservationSettings',
data
})
}
// 预约入馆
export function FetchReserve(data) {
return request({
url: '/api/weixin/reserve',
method: 'POST',
data
})
}

5
static/iconfont.css

@ -3,7 +3,6 @@
src: url('/static/iconfont.ttf') format('truetype'); src: url('/static/iconfont.ttf') format('truetype');
} }
.iconfont { .iconfont {
font-family: "iconfont" !important; font-family: "iconfont" !important;
font-size: 16px; font-size: 16px;
@ -12,6 +11,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-xuanze:before {
content: "\e63e";
}
.icon-ruguanyuyue:before { .icon-ruguanyuyue:before {
content: "\e61d"; content: "\e61d";
} }

BIN
static/iconfont.ttf

476
subpkg/pages/seat-booking/seat-booking.vue

@ -4,7 +4,7 @@
<view class="calendar-wrap"> <view class="calendar-wrap">
<view class="calendar-top"> <view class="calendar-top">
<text class="calendar-title">选择入馆日期</text> <text class="calendar-title">选择入馆日期</text>
<text class="calendar-month">{{ currentYear }}.{{ String(currentMonth).padStart(2,'0') }}</text>
<text class="calendar-month">{{ displayYear }}.{{ String(displayMonth).padStart(2,'0') }}</text>
</view> </view>
<view class="calendar-days-grid"> <view class="calendar-days-grid">
<view <view
@ -13,127 +13,207 @@
class="day-card" class="day-card"
:class="{ :class="{
'card-selected': day.date === selectedDate, 'card-selected': day.date === selectedDate,
'card-available': day.available,
'card-disabled': !day.available,
'card-today': day.isToday
'card-available': day.available && !day.tempClose && !day.userHasBooked,
'card-disabled': !day.available || day.tempClose,
'card-today': day.isToday,
'card-user-booked': day.userHasBooked,
'card-temp-close': day.tempClose
}" }"
@click="selectDate(day)" @click="selectDate(day)"
> >
<!-- 今天左上角纯色块三角无文字 -->
<!-- 今天蓝色角标 -->
<view v-if="day.isToday" class="corner-block-today"></view> <view v-if="day.isToday" class="corner-block-today"></view>
<!-- 如果是今天就显示今天否则显示星期 -->
<!-- 用户已预约 橙色角标 -->
<view v-if="day.userHasBooked" class="corner-block-booked"></view>
<text class="card-week">{{ day.isToday ? '今天' : day.weekText }}</text> <text class="card-week">{{ day.isToday ? '今天' : day.weekText }}</text>
<text class="card-num">{{ day.day }}</text> <text class="card-num">{{ day.day }}</text>
<text class="card-status"> <text class="card-status">
{{ day.available ? '可预约' : '' }}
{{ day.tempClose ? '闭馆' : (day.userHasBooked ? '已预约' : (day.available ? '可预约' : '')) }}
</text> </text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<view class="section">
<!-- 时间选择闭馆时整体禁用 -->
<view class="section" :class="{disabled: isSelectCloseDay}">
<view class="section-title">时间选择</view> <view class="section-title">时间选择</view>
<view class="time-slots"> <view class="time-slots">
<view <view
v-for="slot in timeSlots" v-for="slot in timeSlots"
:key="slot.id" :key="slot.id"
class="time-slot"
:class="{ 'selected': selectedTime === slot.time }"
class="time-item"
:class="{
'selected': selectedTimeList.includes(slot.time),
disabled: isSelectCloseDay,
'booked-item': slot.isUserBooked
}"
@click="selectTime(slot)" @click="selectTime(slot)"
> >
<text class="time-text">{{ slot.time }}</text> <text class="time-text">{{ slot.time }}</text>
<text v-if="slot.available" class="available-badge">可选</text>
<text v-else class="unavailable-badge">已满</text>
<text v-if="slot.isUserBooked" class="booked-tag">已预约</text>
<uni-icons
v-if="selectedTimeList.includes(slot.time) && !slot.isUserBooked"
style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%);"
custom-prefix="iconfont"
type="icon-xuanze"
size="20"
color="#01a4fe"
></uni-icons>
</view> </view>
</view> </view>
<view v-if="isSelectCloseDay" class="disable-tip">该日期闭馆不可选择时间段</view>
</view>
<!-- 闭馆公告板块仅选中闭馆日期显示 -->
<view v-if="isSelectCloseDay" class="section notice-section">
<view class="notice-title">闭馆公告</view>
<view class="notice-content">{{ closeNoticeText }}</view>
</view>
<view class="section operate-desc">
<view class="section-title" style="font-size: 14px;">预约说明</view>
<view class="desc-item">
<text class="color-blue">1<text>蓝色</text>日期 可预约</text>
</view>
<view class="desc-item">
<text class="color-orange">2<text>橙色</text>日期 已有预约</text>
</view>
<view class="desc-item">
<text class="color-gray">3<text>灰色</text>日期 超出可选范围不可点击预约</text>
</view>
<view class="desc-item">
<text class="color-close">4<text>闭馆</text>日期 可查看公告但无法预约或入馆</text>
</view>
<view class="desc-item">
<text>5已预约时间段仅可查看不可取消可新增其他时间段</text>
</view>
</view> </view>
<view class="bottom-bar"> <view class="bottom-bar">
<button class="btn-primary" @click="confirmBooking">确认预约</button>
<button class="btn-primary" :disabled="isSelectCloseDay" @click="confirmBooking">确认预约</button>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { FetchInitLibraryReservationSettings,FetchReserve } from '@/api/reservation';
import { getOpenId } from '@/utils/storage';
import config from '@/utils/config';
export default { export default {
data() { data() {
return { return {
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
selectedDate: '', selectedDate: '',
selectedTime: '',
selectedTimeList: [], //
calendarDays: [], calendarDays: [],
timeSlots: [], timeSlots: [],
reserveConfig: null
reserveConfig: null,
// '2026-08-20'
tempCloseDateList: [],
// key: value:
tempCloseNoticeMap: {},
//
userBookedDateList: [],
// key:value:
userBookedMap:{}
}; };
}, },
onLoad(options) {
this.reserveConfig = {
"frequency": "15",
"weekDays": [
{
"id": 1,
"name": "周一",
"checked": true,
"times": [
"00:00 - 23:59"
],
"showPicker": false,
"newTime": ""
computed: {
//
isSelectCloseDay(){
if(!this.selectedDate) return false
return this.tempCloseDateList.includes(this.selectedDate)
}, },
{
"id": 2,
"name": "周二",
"checked": false,
"times": [],
"showPicker": false,
"newTime": ""
//
closeNoticeText(){
return this.tempCloseNoticeMap[this.selectedDate] || "本日场馆闭馆,暂不支持预约。"
}, },
{
"id": 3,
"name": "周三",
"checked": false,
"times": [],
"showPicker": false,
"newTime": ""
displayYear() {
let targetDateStr = this.selectedDate;
if (!targetDateStr && this.calendarDays.length > 0) {
const firstValid = this.calendarDays.find(d => d.available && !d.tempClose) || this.calendarDays[0];
targetDateStr = firstValid.date;
}
if (!targetDateStr) return new Date().getFullYear();
const d = new Date(targetDateStr);
return d.getFullYear();
}, },
{
"id": 4,
"name": "周四",
"checked": false,
"times": [],
"showPicker": false,
"newTime": ""
displayMonth() {
let targetDateStr = this.selectedDate;
if (!targetDateStr && this.calendarDays.length > 0) {
const firstValid = this.calendarDays.find(d => d.available && !d.tempClose) || this.calendarDays[0];
targetDateStr = firstValid.date;
}
if (!targetDateStr) return new Date().getMonth() + 1;
const d = new Date(targetDateStr);
return d.getMonth() + 1;
}
}, },
{
"id": 5,
"name": "周五",
"checked": false,
"times": [],
"showPicker": false,
"newTime": ""
async onLoad(options) {
await this.initReservationSettings();
if(this.reserveConfig){
this.generateCalendar();
}
}, },
{
"id": 6,
"name": "周六",
"checked": false,
"times": [],
"showPicker": false,
"newTime": ""
methods: {
transformReserveConfig(apiData){
const { baseSetting, weekJson } = apiData;
const weekKeyMap = {
1: 'monday',
2: 'tuesday',
3: 'wednesday',
4: 'thursday',
5: 'friday',
6: 'saturday',
7: 'sunday'
};
const weekDays = [];
for(let wId=1;wId<=7;wId++){
const propName = weekKeyMap[wId];
const checked = !!baseSetting[propName];
const timeArr = weekJson[wId] || [];
// startTime[h,m], endTime[h,m] "HH:mm - HH:mm"
const times = timeArr.map(item=>{
const sh = String(item.startTime[0]).padStart(2,'0');
const sm = String(item.startTime[1]).padStart(2,'0');
const eh = String(item.endTime[0]).padStart(2,'0');
const em = String(item.endTime[1]).padStart(2,'0');
return `${sh}:${sm} - ${eh}:${em}`;
})
weekDays.push({
id:wId,
name:this.getWeekText(wId),
checked: checked,
times: times,
showPicker: false,
newTime: ""
})
}
return {
frequency: String(baseSetting.advanceBookingDays),
weekDays
}
}, },
{
"id": 7,
"name": "周日",
"checked": false,
"showPicker": false,
"newTime": ""
//
async initReservationSettings() {
try {
const res = await FetchInitLibraryReservationSettings({
libcode: config.LIB_CODE
})
if (res && res.code === 200 && res.data) {
//
this.reserveConfig = this.transformReserveConfig(res.data);
}else{
console.error('初始化预约设置失败',res)
} }
]
} catch (e) {
console.error('获取预约设置异常', e)
} }
this.generateCalendar();
}, },
methods: {
getWeekNum(dateStr) { getWeekNum(dateStr) {
const d = new Date(dateStr); const d = new Date(dateStr);
const day = d.getDay(); const day = d.getDay();
@ -145,6 +225,7 @@ export default {
}, },
generateCalendar() { generateCalendar() {
if(!this.reserveConfig) return;
const today = new Date(); const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const freq = Number(this.reserveConfig.frequency); const freq = Number(this.reserveConfig.frequency);
@ -162,17 +243,24 @@ export default {
const weekCfg = this.reserveConfig.weekDays.find(w => w.id === weekNo) const weekCfg = this.reserveConfig.weekDays.find(w => w.id === weekNo)
const available = !!weekCfg?.checked const available = !!weekCfg?.checked
const d = new Date(dateStr) const d = new Date(dateStr)
const tempClose = this.tempCloseDateList.includes(dateStr)
const userHasBooked = this.userBookedDateList.includes(dateStr)
list.push({ list.push({
day: d.getDate(), day: d.getDate(),
date: dateStr, date: dateStr,
weekText: this.getWeekText(weekNo), weekText: this.getWeekText(weekNo),
available: available, available: available,
isToday: dateStr === todayStr
isToday: dateStr === todayStr,
tempClose: tempClose,
userHasBooked: userHasBooked
}) })
}) })
this.calendarDays = list this.calendarDays = list
const firstAvail = this.calendarDays.find(d => d.available);
//
const firstAvail = this.calendarDays.find(d => d.available && !d.tempClose);
if(firstAvail){ if(firstAvail){
this.selectedDate = firstAvail.date this.selectedDate = firstAvail.date
this.refreshTimeSlots(this.selectedDate) this.refreshTimeSlots(this.selectedDate)
@ -183,7 +271,6 @@ export default {
}, },
refreshTimeSlots(dateStr){ refreshTimeSlots(dateStr){
this.selectedTime = ''
if(!dateStr) { if(!dateStr) {
this.timeSlots = [] this.timeSlots = []
return return
@ -194,47 +281,125 @@ export default {
this.timeSlots = [] this.timeSlots = []
return return
} }
//
const bookedTimeArr = this.userBookedMap[dateStr] || []
this.timeSlots = weekCfg.times.map((t, idx)=>{ this.timeSlots = weekCfg.times.map((t, idx)=>{
return { return {
id: idx + 1, id: idx + 1,
time: t, time: t,
available: true
available: true,
isUserBooked: bookedTimeArr.includes(t)
} }
}) })
//
this.selectedTimeList = [...bookedTimeArr]
}, },
selectDate(day) { selectDate(day) {
if (!day.available) return;
//
if (!day.available && !day.tempClose) {
uni.showToast({
title: '超出可选的时间范围,请选择其他日期',
icon: 'none'
})
return;
}
// ||
this.selectedDate = day.date; this.selectedDate = day.date;
this.refreshTimeSlots(day.date)
this.refreshTimeSlots(day.date);
}, },
selectTime(slot) { selectTime(slot) {
if(this.isSelectCloseDay) return
if (!slot.available) return; if (!slot.available) return;
this.selectedTime = slot.time;
//
if(slot.isUserBooked){
uni.showToast({
title:"该时间段已预约,不可取消",
icon:"none"
})
return
}
const idx = this.selectedTimeList.indexOf(slot.time)
if(idx > -1){
//
this.selectedTimeList.splice(idx,1)
}else{
this.selectedTimeList.push(slot.time)
}
}, },
confirmBooking() {
async confirmBooking() {
if(this.isSelectCloseDay){
uni.showToast({title:"该日期闭馆,无法预约",icon:"none"})
return
}
if (!this.selectedDate) { if (!this.selectedDate) {
uni.showToast({ title: '请选择日期', icon: 'none' }); uni.showToast({ title: '请选择日期', icon: 'none' });
return; return;
} }
if (!this.selectedTime) {
uni.showToast({ title: '请选择时间段', icon: 'none' });
const bookedTimeArr = this.userBookedMap[this.selectedDate] || []
//
const newSelectTimes = this.selectedTimeList.filter(item=> !bookedTimeArr.includes(item))
if (!newSelectTimes || newSelectTimes.length === 0) {
uni.showToast({ title: '请选择需要新增预约的时间段', icon: 'none' });
return; return;
} }
uni.showLoading({ title: '预约中...' }); uni.showLoading({ title: '预约中...' });
setTimeout(() => {
const openId = await getOpenId();
if (!openId) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ title: '获取用户openid失败', icon: 'none' });
return;
}
//
const param = newSelectTimes.map(timeStr => {
const [startPart, endPart] = timeStr.split(' - ');
const startTime = startPart.trim() + ':00';
const endTime = endPart.trim() + ':00';
return {
libcode: config.LIB_CODE,
openid: openId,
specificDate: this.selectedDate,
startTime: startTime,
endTime: endTime
}
})
try {
const res = await FetchReserve(param);
uni.hideLoading();
if(res.code === 200){
//
if(!this.userBookedDateList.includes(this.selectedDate)){
this.userBookedDateList.push(this.selectedDate)
}
const old = this.userBookedMap[this.selectedDate] || []
this.userBookedMap[this.selectedDate] = [...new Set([...old,...newSelectTimes])]
const timeText = newSelectTimes.join('\n');
uni.showModal({ uni.showModal({
title: '预约成功', title: '预约成功',
content: `您已成功预约\n日期:${this.selectedDate}\n时间:${this.selectedTime}`,
content: `您已新增预约\n日期:${this.selectedDate}\n时间${timeText}`,
showCancel: false, showCancel: false,
confirmText: '确定', confirmText: '确定',
success: () => { success: () => {
uni.navigateBack({ delta: 2 }); uni.navigateBack({ delta: 2 });
} }
});
}, 1500);
})
}else{
uni.showToast({ title: res.msg || '预约失败,请重试', icon: 'none' })
}
} catch (err) {
uni.hideLoading();
console.error('预约接口异常', err);
uni.showToast({ title: '预约失败,请重试', icon: 'none' })
}
} }
} }
}; };
@ -253,11 +418,37 @@ export default {
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
} }
//
.notice-section{
background:#FCECE9;
.notice-title{
font-size:15px;
font-weight:bold;
color:#FD7359;
margin-bottom:8px;
}
.notice-content{
font-size:14px;
color:#595959;
line-height:1.6;
}
}
//
.section.disabled{
opacity:0.55;
pointer-events:none;
}
.disable-tip{
margin-top:12px;
font-size:13px;
color:#999;
text-align:center;
}
.calendar-wrap{ .calendar-wrap{
background:#ffffff; background:#ffffff;
border-radius:16px; border-radius:16px;
// padding:10px;
} }
.calendar-top{ .calendar-top{
display:flex; display:flex;
@ -283,19 +474,18 @@ export default {
} }
.day-card{ .day-card{
width: calc((100% - 48px) / 7); width: calc((100% - 48px) / 7);
// aspect-ratio: 0.85;
background:#f7f8fa; background:#f7f8fa;
border-radius: 4px; border-radius: 4px;
padding: 4px 0; padding: 4px 0;
display:flex; display:flex;
flex-direction:column; flex-direction:column;
align-items:center; align-items:center;
justify-content:center;
justify-content:flex-start;
position:relative; position:relative;
overflow: hidden; overflow: hidden;
} }
/* 左上角纯三角色块,无文字,模仿截图角标 */
/* 今天左上角蓝色三角 */
.corner-block-today{ .corner-block-today{
position:absolute; position:absolute;
top:0; top:0;
@ -304,7 +494,16 @@ export default {
height: 0; height: 0;
border-top: 12px solid #01a4fe; border-top: 12px solid #01a4fe;
border-right: 12px solid transparent; border-right: 12px solid transparent;
border-bottom-left-radius: 0;
}
/* 已预约 橙色三角角标 */
.corner-block-booked{
position:absolute;
top:0;
left:0;
width: 0;
height: 0;
border-top: 12px solid #ff7800;
border-right: 12px solid transparent;
} }
.card-week{ .card-week{
@ -330,7 +529,7 @@ export default {
color:#01a4fe; color:#01a4fe;
} }
/* 闭馆(不可选) */
/* 闭馆/不可选 */
.day-card.card-disabled{ .day-card.card-disabled{
background:#f7f8fa; background:#f7f8fa;
} }
@ -349,6 +548,38 @@ export default {
.day-card.card-selected .card-status{ .day-card.card-selected .card-status{
color:#ffffff; color:#ffffff;
} }
.day-card.card-user-booked{
background:#FFF3E5;
}
.day-card.card-user-booked .card-week,
.day-card.card-user-booked .card-num,
.day-card.card-user-booked .card-status{
color:#ff7800;
}
.day-card.card-user-booked.card-selected{
background:#ff7800;
}
.day-card.card-user-booked.card-selected .card-week,
.day-card.card-user-booked.card-selected .card-num,
.day-card.card-user-booked.card-selected .card-status{
color:#fff;
}
.day-card.card-temp-close .card-status{
color:#999;
}
.day-card.card-temp-close.card-selected {
background:#888;
}
.day-card.card-temp-close.card-selected .card-week,
.day-card.card-temp-close.card-selected .card-num,
.day-card.card-temp-close.card-selected .card-status{
color:#fff;
}
/* 时间选择 */ /* 时间选择 */
.section-title { .section-title {
@ -362,7 +593,8 @@ export default {
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
} }
.time-slot {
.time-item {
position: relative;
width: calc(50% - 5px); width: calc(50% - 5px);
display: flex; display: flex;
align-items: center; align-items: center;
@ -372,10 +604,24 @@ export default {
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 1px solid transparent;
} }
.time-slot.selected {
.time-item.selected {
background-color: #e8f4fd; background-color: #e8f4fd;
border-color: #01a4fe; border-color: #01a4fe;
} }
.time-item.disabled{
pointer-events:none;
}
.time-item.booked-item{
background: #fff3e5;
border:1px solid #ffb870;
.time-text{
color:#ff7800;
}
}
.booked-tag{
font-size:11px;
color:#ff7800;
}
.time-text { .time-text {
font-size: 14px; font-size: 14px;
color: #333; color: #333;
@ -418,4 +664,42 @@ export default {
.btn-primary::after { .btn-primary::after {
border: none; border: none;
} }
.btn-primary[disabled]{
background:#b8d8f5 !important;
}
.operate-desc{
margin:10px;
padding:16px;
border-radius:12px;
background:#fff;
.section-title{
margin-bottom:10px !important;
}
.desc-item{
line-height:2;
font-size:13px;
}
.color-blue{
text{
color:#01a4fe;
}
}
.color-orange{
text{
color:#ff7800;
}
}
.color-gray{
text{
color:#999;
}
}
.color-close{
text{
color:#FD7359;
font-weight:500;
}
}
}
</style> </style>
Loading…
Cancel
Save