|
|
|
@ -26,6 +26,7 @@ |
|
|
|
<!-- 用户已预约 橙色角标 --> |
|
|
|
<view v-if="day.userHasBooked" class="corner-block-booked"></view> |
|
|
|
|
|
|
|
|
|
|
|
<text class="card-week">{{ day.isToday ? '今天' : day.weekText }}</text> |
|
|
|
<text class="card-num">{{ day.day }}</text> |
|
|
|
<text class="card-status"> |
|
|
|
@ -36,6 +37,7 @@ |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 时间选择:闭馆时整体禁用 --> |
|
|
|
<view class="section" :class="{disabled: isSelectCloseDay}"> |
|
|
|
<view class="section-title">时间选择</view> |
|
|
|
@ -72,6 +74,7 @@ |
|
|
|
<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"> |
|
|
|
@ -91,17 +94,20 @@ |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
|
|
|
|
<view class="bottom-bar"> |
|
|
|
<button class="btn-primary" :disabled="isSelectCloseDay" @click="confirmBooking">确认预约</button> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
<script> |
|
|
|
import { FetchInitLibraryReservationSettings,FetchReserve } from '@/api/reservation'; |
|
|
|
import { FetchInitLibraryReservationSettings, FetchInitMyReservation,FetchReserve } from '@/api/reservation'; |
|
|
|
import { getOpenId } from '@/utils/storage'; |
|
|
|
import config from '@/utils/config'; |
|
|
|
|
|
|
|
|
|
|
|
export default { |
|
|
|
data() { |
|
|
|
return { |
|
|
|
@ -153,11 +159,99 @@ export default { |
|
|
|
}, |
|
|
|
async onLoad(options) { |
|
|
|
await this.initReservationSettings(); |
|
|
|
await this.initMyReservation(); |
|
|
|
if(this.reserveConfig){ |
|
|
|
this.generateCalendar(); |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
// 时间戳转 YYYY‑MM‑DD |
|
|
|
formatTsToDate(ts) { |
|
|
|
const d = new Date(ts); |
|
|
|
const y = d.getFullYear(); |
|
|
|
const m = String(d.getMonth() + 1).padStart(2, '0'); |
|
|
|
const day = String(d.getDate()).padStart(2, '0'); |
|
|
|
return `${y}-${m}-${day}`; |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 将闭馆起止毫秒时间戳,展开成每一天日期字符串数组 |
|
|
|
* @param {number} startTs closureStartDate |
|
|
|
* @param {number} endTs closureEndDate |
|
|
|
* @returns string[] ['2026‑08‑01','2026‑08‑02'] |
|
|
|
*/ |
|
|
|
expandClosureRange(startTs, endTs){ |
|
|
|
const result = [] |
|
|
|
let cur = new Date(startTs) |
|
|
|
const end = new Date(endTs) |
|
|
|
// 只比对日期,去掉时分秒干扰 |
|
|
|
cur.setHours(0,0,0,0) |
|
|
|
end.setHours(0,0,0,0) |
|
|
|
while(cur <= end){ |
|
|
|
result.push(this.formatTsToDate(cur.getTime())) |
|
|
|
cur.setDate(cur.getDate() + 1) |
|
|
|
} |
|
|
|
return result |
|
|
|
}, |
|
|
|
|
|
|
|
// 把 HH:mm:ss 截取为 HH:mm |
|
|
|
formatTimeHm(timeStr) { |
|
|
|
if(!timeStr) return '' |
|
|
|
return timeStr.substring(0,5) |
|
|
|
}, |
|
|
|
// 初始化我的预约 |
|
|
|
async initMyReservation() { |
|
|
|
try { |
|
|
|
const openId = await getOpenId(); |
|
|
|
const res = await FetchInitMyReservation({ |
|
|
|
libcode: config.LIB_CODE, |
|
|
|
openId: openId |
|
|
|
}) |
|
|
|
console.log('初始化我的预约',res) |
|
|
|
if(res.code === 200 && res.data){ |
|
|
|
// 闭馆 |
|
|
|
this.tempCloseDateList = [] |
|
|
|
this.tempCloseNoticeMap = {} |
|
|
|
// 处理闭馆公告 vxLibraryClosureNotices |
|
|
|
const closureList = Array.isArray(res.data.vxLibraryClosureNotices) ? res.data.vxLibraryClosureNotices : [] |
|
|
|
closureList.forEach(notice=>{ |
|
|
|
const dayArr = this.expandClosureRange(notice.closureStartDate, notice.closureEndDate) |
|
|
|
dayArr.forEach(dateStr=>{ |
|
|
|
if(!this.tempCloseDateList.includes(dateStr)){ |
|
|
|
this.tempCloseDateList.push(dateStr) |
|
|
|
} |
|
|
|
// 同一个日期多条公告,用换行拼接;覆盖也可以看业务需求 |
|
|
|
if(this.tempCloseNoticeMap[dateStr]){ |
|
|
|
this.tempCloseNoticeMap[dateStr] += '\n\n' + notice.content |
|
|
|
}else{ |
|
|
|
this.tempCloseNoticeMap[dateStr] = notice.content |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
// ==========处理用户预约 myReservation========== |
|
|
|
this.userBookedDateList = [] |
|
|
|
this.userBookedMap = {} |
|
|
|
const myRes = Array.isArray(res.data.myReservation) ? res.data.myReservation : [] |
|
|
|
myRes.forEach(item=>{ |
|
|
|
// specificDate 毫秒时间戳 |
|
|
|
const dateStr = this.formatTsToDate(item.specificDate) |
|
|
|
const startHm = this.formatTimeHm(item.startTime) |
|
|
|
const endHm = this.formatTimeHm(item.endTime) |
|
|
|
const timeSlotText = `${startHm} - ${endHm}` |
|
|
|
|
|
|
|
if(!this.userBookedMap[dateStr]){ |
|
|
|
this.userBookedMap[dateStr] = [] |
|
|
|
} |
|
|
|
this.userBookedMap[dateStr].push(timeSlotText) |
|
|
|
}) |
|
|
|
// 收集已预约日期 |
|
|
|
this.userBookedDateList = Object.keys(this.userBookedMap) |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.error('获取我的预约异常', e) |
|
|
|
} |
|
|
|
}, |
|
|
|
transformReserveConfig(apiData){ |
|
|
|
const { baseSetting, weekJson } = apiData; |
|
|
|
const weekKeyMap = { |
|
|
|
@ -197,6 +291,7 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 初始化预约设置 |
|
|
|
async initReservationSettings() { |
|
|
|
try { |
|
|
|
@ -204,7 +299,6 @@ export default { |
|
|
|
libcode: config.LIB_CODE |
|
|
|
}) |
|
|
|
if (res && res.code === 200 && res.data) { |
|
|
|
// 转换为旧格式,原有业务逻辑完全不用改动 |
|
|
|
this.reserveConfig = this.transformReserveConfig(res.data); |
|
|
|
}else{ |
|
|
|
console.error('初始化预约设置失败',res) |
|
|
|
@ -214,6 +308,7 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
getWeekNum(dateStr) { |
|
|
|
const d = new Date(dateStr); |
|
|
|
const day = d.getDay(); |
|
|
|
@ -224,6 +319,7 @@ export default { |
|
|
|
return map[weekNo] |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
generateCalendar() { |
|
|
|
if(!this.reserveConfig) return; |
|
|
|
const today = new Date(); |
|
|
|
@ -237,6 +333,7 @@ export default { |
|
|
|
allowDateList.push(ds) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const list = [] |
|
|
|
allowDateList.forEach(dateStr=>{ |
|
|
|
const weekNo = this.getWeekNum(dateStr) |
|
|
|
@ -244,9 +341,11 @@ export default { |
|
|
|
const available = !!weekCfg?.checked |
|
|
|
const d = new Date(dateStr) |
|
|
|
|
|
|
|
|
|
|
|
const tempClose = this.tempCloseDateList.includes(dateStr) |
|
|
|
const userHasBooked = this.userBookedDateList.includes(dateStr) |
|
|
|
|
|
|
|
|
|
|
|
list.push({ |
|
|
|
day: d.getDate(), |
|
|
|
date: dateStr, |
|
|
|
@ -259,6 +358,7 @@ export default { |
|
|
|
}) |
|
|
|
this.calendarDays = list |
|
|
|
|
|
|
|
|
|
|
|
// 找第一个不是闭馆的有效日期作为初始选中 |
|
|
|
const firstAvail = this.calendarDays.find(d => d.available && !d.tempClose); |
|
|
|
if(firstAvail){ |
|
|
|
@ -270,6 +370,7 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
refreshTimeSlots(dateStr){ |
|
|
|
if(!dateStr) { |
|
|
|
this.timeSlots = [] |
|
|
|
@ -284,6 +385,7 @@ export default { |
|
|
|
// 当前日期已经预约的时间段 |
|
|
|
const bookedTimeArr = this.userBookedMap[dateStr] || [] |
|
|
|
|
|
|
|
|
|
|
|
this.timeSlots = weekCfg.times.map((t, idx)=>{ |
|
|
|
return { |
|
|
|
id: idx + 1, |
|
|
|
@ -296,6 +398,7 @@ export default { |
|
|
|
this.selectedTimeList = [...bookedTimeArr] |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
selectDate(day) { |
|
|
|
// 普通不可选(非临时闭馆):弹提示,禁止选中 |
|
|
|
if (!day.available && !day.tempClose) { |
|
|
|
@ -315,12 +418,13 @@ export default { |
|
|
|
// 已预约时间段禁止取消 |
|
|
|
if(slot.isUserBooked){ |
|
|
|
uni.showToast({ |
|
|
|
title:"该时间段已预约,不可取消", |
|
|
|
title:"该时间段已预约,不可操作取消", |
|
|
|
icon:"none" |
|
|
|
}) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const idx = this.selectedTimeList.indexOf(slot.time) |
|
|
|
if(idx > -1){ |
|
|
|
// 本次新增的,可以取消 |
|
|
|
@ -339,15 +443,18 @@ export default { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
uni.showLoading({ title: '预约中...' }); |
|
|
|
const openId = await getOpenId(); |
|
|
|
if (!openId) { |
|
|
|
@ -356,24 +463,27 @@ export default { |
|
|
|
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 |
|
|
|
libcode: config.LIB_CODE, // 馆代码 |
|
|
|
openid: 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)){ |
|
|
|
@ -382,14 +492,22 @@ export default { |
|
|
|
const old = this.userBookedMap[this.selectedDate] || [] |
|
|
|
this.userBookedMap[this.selectedDate] = [...new Set([...old,...newSelectTimes])] |
|
|
|
|
|
|
|
|
|
|
|
const timeText = newSelectTimes.join('\n'); |
|
|
|
uni.showModal({ |
|
|
|
title: '预约成功', |
|
|
|
content: `您已新增预约\n日期:${this.selectedDate}\n时间段:${timeText}`, |
|
|
|
showCancel: false, |
|
|
|
confirmText: '确定', |
|
|
|
success: () => { |
|
|
|
uni.navigateBack({ delta: 2 }); |
|
|
|
success: async () => { |
|
|
|
// 重新拉取我的预约数据(里面同时拉闭馆公告) |
|
|
|
await this.initMyReservation(); |
|
|
|
// 重新生成日历 |
|
|
|
this.generateCalendar(); |
|
|
|
// 重新刷新当前选中日期的时间段 |
|
|
|
if(this.selectedDate){ |
|
|
|
this.refreshTimeSlots(this.selectedDate); |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}else{ |
|
|
|
@ -405,6 +523,7 @@ export default { |
|
|
|
}; |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
.booking-detail { |
|
|
|
min-height: 100vh; |
|
|
|
@ -412,6 +531,7 @@ export default { |
|
|
|
padding-bottom: 70px; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.section { |
|
|
|
background-color: #fff; |
|
|
|
margin: 10px; |
|
|
|
@ -434,6 +554,7 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 时间板块整体置灰 |
|
|
|
.section.disabled{ |
|
|
|
opacity:0.55; |
|
|
|
@ -446,6 +567,7 @@ export default { |
|
|
|
text-align:center; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.calendar-wrap{ |
|
|
|
background:#ffffff; |
|
|
|
border-radius:16px; |
|
|
|
@ -467,6 +589,7 @@ export default { |
|
|
|
font-weight:bold; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.calendar-days-grid{ |
|
|
|
display:flex; |
|
|
|
flex-wrap:wrap; |
|
|
|
@ -485,6 +608,7 @@ export default { |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 今天左上角蓝色三角 */ |
|
|
|
.corner-block-today{ |
|
|
|
position:absolute; |
|
|
|
@ -506,6 +630,7 @@ export default { |
|
|
|
border-right: 12px solid transparent; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.card-week{ |
|
|
|
font-size:12px; |
|
|
|
color:#666; |
|
|
|
@ -521,6 +646,7 @@ export default { |
|
|
|
margin-top:4px; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 可预约 */ |
|
|
|
.day-card.card-available{ |
|
|
|
background:#e8f4fd; |
|
|
|
@ -529,6 +655,7 @@ export default { |
|
|
|
color:#01a4fe; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 闭馆/不可选 */ |
|
|
|
.day-card.card-disabled{ |
|
|
|
background:#f7f8fa; |
|
|
|
@ -539,6 +666,7 @@ export default { |
|
|
|
color:#999; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 选中状态 */ |
|
|
|
.day-card.card-selected{ |
|
|
|
background:#01a4fe; |
|
|
|
@ -557,6 +685,7 @@ export default { |
|
|
|
color:#ff7800; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.day-card.card-user-booked.card-selected{ |
|
|
|
background:#ff7800; |
|
|
|
} |
|
|
|
@ -566,14 +695,17 @@ export default { |
|
|
|
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{ |
|
|
|
@ -581,6 +713,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 时间选择 */ |
|
|
|
.section-title { |
|
|
|
font-size: 16px; |
|
|
|
@ -641,6 +774,7 @@ export default { |
|
|
|
border-radius: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 底部按钮 */ |
|
|
|
.bottom-bar { |
|
|
|
position: fixed; |
|
|
|
|