Browse Source

预约

master
xuhuajiao 4 weeks ago
parent
commit
bfd1e76d82
  1. 28
      api/reservation.js
  2. 3
      pages.json
  3. 4
      subpkg/pages/book-detail/book-detail.vue
  4. 211
      subpkg/pages/booking-record/booking-record.vue
  5. 154
      subpkg/pages/seat-booking/seat-booking.vue

28
api/reservation.js

@ -16,3 +16,31 @@ export function FetchReserve(data) {
data
})
}
// 我的预约-预约登记界面初始化
// ?libcode=&openId=
export function FetchInitMyReservation(data) {
return request({
url: '/api/weixin/myReservationInit',
data
})
}
// 我的预约
// ?libcode=&openId=&page=&size=
// libcode可选填(我不知道小程序是一家图书馆还是多加图书馆的 所以可为空)
export function FetchMyReservation(data) {
return request({
url: '/api/weixin/myReservation',
data
})
}
// 取消预约
export function FetchCancelReservation(data) {
return request({
url: '/api/weixin/cancelReservation',
method: 'POST',
data
})
}

3
pages.json

@ -167,7 +167,8 @@
{
"path": "pages/booking-record/booking-record",
"style": {
"navigationBarTitleText": "预约入馆记录"
"navigationBarTitleText": "预约入馆记录",
"enablePullDownRefresh": true
}
},
{

4
subpkg/pages/book-detail/book-detail.vue

@ -359,7 +359,7 @@ export default {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 60px;
padding-bottom: 70px;
}
.article-detail-container {
@ -491,5 +491,7 @@ export default {
}
.detail-bottom{
justify-content: space-around;
padding: 0 15px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
}
</style>

211
subpkg/pages/booking-record/booking-record.vue

@ -26,38 +26,34 @@
<scroll-view
scroll-y
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
lower-threshold="100"
@scrolltolower="onScrollLower"
lower-threshold="150"
class="scroll-view"
>
<view v-if="loading" class="loading-box">
<text class="loading-text">加载中...</text>
</view>
<view
class="activity-item"
v-for="item in displayList"
v-for="item in activityList"
:key="item.id"
>
<view class="activity-info">
<!-- <text class="title">{{ item.area }}</text> -->
<view class="title-row">
<text class="title">{{ item.fondsName }}</text>
</view>
<view class="item-info">
<text class="label">入馆日期</text>
<text class="value">{{ item.startTime }}</text>
<text class="value">{{ item.visitDate }}</text>
</view>
<view class="item-info">
<text class="label">入馆时间</text>
<text class="value">{{ item.startTime }}</text>
<text class="value">{{ item.visitTime }}-{{ item.endTimeHm }}</text>
</view>
<!-- <view class="item-info">
<text class="label">结束时间</text>
<text class="value">{{ item.endTime }}</text>
</view> -->
<!-- <view class="item-info">
<text class="label">座位号</text>
<text class="value">{{ item.seatNumber }}</text>
</view> -->
<!-- <view class="item-info">
<text class="label">状态</text>
<text class="value status-tag" :class="item.statusClass">{{ item.statusText }}</text>
<text class="label">所在馆</text>
<text class="value">{{ item.fondsName }}</text>
</view> -->
<view class="btn-box" v-if="item.status === 0">
<button class="activity-btn" type="primary" @click.stop="cancelAppoint(item)">
@ -66,7 +62,12 @@
</view>
</view>
</view>
<view class="empty-box" v-if="displayList.length === 0">
<view v-if="!loading && noMore && activityList.length > 0" class="nomore-box">
<text class="nomore-text">没有更多数据了</text>
</view>
<view class="empty-box" v-if="!loading && activityList.length === 0">
<uni-icons type="empty" size="80" color="#ccc"></uni-icons>
<text class="empty-text">{{ getEmptyText() }}</text>
</view>
@ -75,69 +76,142 @@
</template>
<script>
import { FetchMyReservation, FetchCancelReservation } from '@/api/reservation';
import { getOpenId } from '@/utils/storage';
import config from '@/utils/config';
export default {
data() {
return {
currentTab: 0,
activityList: [],
refreshing: false
refreshing: false,
loading: false,
noMore: false,
page: 0,
pageSize: 10
};
},
computed: {
displayList() {
const statusMap = {
0: [0, '0', '未开始', 'pending'],
1: [1, '1', '进行中', 'ongoing'],
2: [2, '2', '已结束', 'ended']
};
const targetStatuses = statusMap[this.currentTab] || [];
return this.activityList.filter(item => {
const itemStatus = String(item.status);
return targetStatuses.some(status => String(status) === itemStatus);
});
}
},
onLoad() {
this.getActivityList();
this.getActivityList(true);
},
onPullDownRefresh() {
if (this.loading) {
uni.stopPullDownRefresh()
return
}
this.getActivityList(true).finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
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}`;
},
formatTimeHm(timeStr) {
if (!timeStr) return ''
return timeStr.substring(0, 5)
},
switchTab(index) {
this.currentTab = index;
this.page = 0;
this.noMore = false;
this.activityList = [];
this.getActivityList(true);
},
async fetchMyReservation() {
const openId = await getOpenId();
const res = await FetchMyReservation({
libcode: config.LIB_CODE,
openId: openId,
page: this.page,
size: this.pageSize,
status: this.currentTab
})
console.log('我的预约', res)
if (res.code === 200 && Array.isArray(res.data.content)) {
const list = res.data.content.map(raw => {
return {
id: raw.id,
fondsName: raw.fondsName || '',
visitDate: this.formatTsToDate(raw.specificDate),
visitTime: this.formatTimeHm(raw.startTime),
endTimeHm: this.formatTimeHm(raw.endTime),
status: Number(this.currentTab),
startTimeRaw: raw.startTime,
endTimeRaw: raw.endTime,
specificDate: raw.specificDate
}
})
//
const currentPage = res.data.number ?? 0;
const totalPage = res.data.totalPages ?? 0;
return {
list,
noMore: currentPage >= totalPage - 1
}
}
return { list: [], noMore: true }
},
getActivityList() {
async getActivityList(isRefresh = false) {
if (this.loading) return;
if (isRefresh) {
this.page = 0;
this.noMore = false;
this.refreshing = true;
setTimeout(() => {
const rawData = [
{id:1,area:'一楼自习区',startTime:'2026-05-21 08:00:00',endTime:'2026-05-21 11:30:00',seatNumber:'8号',status:0,statusText:'已预约'},
{id:2,area:'综合阅览一',startTime:'2026-05-21 08:00:00',endTime:'2026-05-21 11:30:00',seatNumber:'8号',status:1,statusText:'进行中'},
{id:4,area:'专题阅览室',startTime:'2026-05-21 08:00:00',endTime:'2026-05-21 11:30:00',seatNumber:'8号',status:0,statusText:'已预约'},
];
this.activityList = rawData.map(item => ({
...item,
statusClass: `status-${item.status}`
}));
} else {
if (this.noMore) return;
}
this.loading = true;
try {
const { list, noMore } = await this.fetchMyReservation();
this.noMore = noMore;
if (isRefresh) {
this.activityList = list;
} else {
this.activityList = [...this.activityList, ...list];
}
} catch (e) {
console.error('获取我的预约异常', e)
uni.showToast({ title: '获取预约列表失败', icon: 'none' })
if (isRefresh) this.activityList = [];
} finally {
this.refreshing = false;
}, 500);
this.loading = false;
}
},
onRefresh() {
this.getActivityList();
onScrollLower() {
if (this.loading || this.noMore) return;
this.page += 1;
this.getActivityList(false);
},
cancelAppoint(item) {
async cancelAppoint(item) {
uni.showModal({
title: '提示',
content: `确定要取消${item.area}的预约吗?`,
success: (res) => {
content: `确定要取消当前时间段的预约吗?`,
success: async (res) => {
if (res.confirm) {
const res = await FetchCancelReservation({
id: item.id
})
if (res.code === 200) {
uni.showToast({ title: '取消成功', icon: 'success' });
this.getActivityList();
this.getActivityList(true);
} else {
uni.showToast({ title: res.msg || '取消预约失败', icon: 'none' })
}
}
}
})
},
getEmptyText() {
const textMap = {
0: '未开始',
@ -156,6 +230,8 @@ export default {
height: 100vh;
box-sizing: border-box;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.tab-box {
@ -191,13 +267,32 @@ export default {
}
.scroll-view {
height: calc(100vh - 60px);
flex: 1;
height: 0;
padding: 0 12px;
box-sizing: border-box;
}
.loading-box {
padding: 20px 0;
text-align: center;
}
.loading-text {
font-size: 14px;
color: #999;
}
.nomore-box {
padding: 15px 0;
text-align: center;
}
.nomore-text {
font-size: 13px;
color: #999;
}
.empty-box {
height: calc(100vh - 200px);
height: calc(100vh - 180px);
display: flex;
flex-direction: column;
align-items: center;
@ -253,7 +348,6 @@ export default {
}
}
/* 正确的状态样式 */
.status-tag {
padding: 2px 8px;
border-radius: 4px;
@ -277,7 +371,10 @@ export default {
display: flex;
justify-content: flex-end;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e5e5e5;
}
.activity-btn {
background-color: #01a4fe;
font-size: 13px;

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

@ -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: {
// YYYYMMDD
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;

Loading…
Cancel
Save