5 changed files with 506 additions and 244 deletions
-
28api/reservation.js
-
3pages.json
-
4subpkg/pages/book-detail/book-detail.vue
-
561subpkg/pages/booking-record/booking-record.vue
-
154subpkg/pages/seat-booking/seat-booking.vue
@ -1,289 +1,386 @@ |
|||
<template> |
|||
<view class="activity-list"> |
|||
<view class="tab-box"> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 0 }" |
|||
@click="switchTab(0)" |
|||
> |
|||
未开始 |
|||
</view> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 1 }" |
|||
@click="switchTab(1)" |
|||
> |
|||
进行中 |
|||
</view> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 2 }" |
|||
@click="switchTab(2)" |
|||
> |
|||
已结束 |
|||
</view> |
|||
</view> |
|||
<view class="activity-list"> |
|||
<view class="tab-box"> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 0 }" |
|||
@click="switchTab(0)" |
|||
> |
|||
未开始 |
|||
</view> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 1 }" |
|||
@click="switchTab(1)" |
|||
> |
|||
进行中 |
|||
</view> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: currentTab === 2 }" |
|||
@click="switchTab(2)" |
|||
> |
|||
已结束 |
|||
</view> |
|||
</view> |
|||
|
|||
<scroll-view |
|||
scroll-y |
|||
refresher-enabled |
|||
:refresher-triggered="refreshing" |
|||
@refresherrefresh="onRefresh" |
|||
lower-threshold="100" |
|||
class="scroll-view" |
|||
> |
|||
<view |
|||
class="activity-item" |
|||
v-for="item in displayList" |
|||
:key="item.id" |
|||
> |
|||
<view class="activity-info"> |
|||
<!-- <text class="title">{{ item.area }}</text> --> |
|||
<view class="item-info"> |
|||
<text class="label">入馆日期</text> |
|||
<text class="value">{{ item.startTime }}</text> |
|||
</view> |
|||
<scroll-view |
|||
scroll-y |
|||
@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 activityList" |
|||
:key="item.id" |
|||
> |
|||
<view class="activity-info"> |
|||
<view class="title-row"> |
|||
<text class="title">{{ item.fondsName }}</text> |
|||
</view> |
|||
<view class="item-info"> |
|||
<text class="label">入馆日期</text> |
|||
<text class="value">{{ item.visitDate }}</text> |
|||
</view> |
|||
<view class="item-info"> |
|||
<text class="label">入馆时间</text> |
|||
<text class="value">{{ item.startTime }}</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> |
|||
</view> --> |
|||
<view class="btn-box" v-if="item.status === 0"> |
|||
<button class="activity-btn" type="primary" @click.stop="cancelAppoint(item)"> |
|||
取消预约 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="empty-box" v-if="displayList.length === 0"> |
|||
<uni-icons type="empty" size="80" color="#ccc"></uni-icons> |
|||
<text class="empty-text">{{ getEmptyText() }}</text> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
<text class="label">入馆时间</text> |
|||
<text class="value">{{ item.visitTime }}-{{ item.endTimeHm }}</text> |
|||
</view> |
|||
<!-- <view class="item-info"> |
|||
<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)"> |
|||
取消预约 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<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> |
|||
</scroll-view> |
|||
</view> |
|||
</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 |
|||
}; |
|||
}, |
|||
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(); |
|||
}, |
|||
methods: { |
|||
switchTab(index) { |
|||
this.currentTab = index; |
|||
}, |
|||
data() { |
|||
return { |
|||
currentTab: 0, |
|||
activityList: [], |
|||
refreshing: false, |
|||
loading: false, |
|||
noMore: false, |
|||
page: 0, |
|||
pageSize: 10 |
|||
}; |
|||
}, |
|||
onLoad() { |
|||
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); |
|||
}, |
|||
|
|||
getActivityList() { |
|||
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}` |
|||
})); |
|||
this.refreshing = false; |
|||
}, 500); |
|||
}, |
|||
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 } |
|||
}, |
|||
|
|||
onRefresh() { |
|||
this.getActivityList(); |
|||
}, |
|||
|
|||
cancelAppoint(item) { |
|||
uni.showModal({ |
|||
title: '提示', |
|||
content: `确定要取消${item.area}的预约吗?`, |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
uni.showToast({ title: '取消成功', icon: 'success' }); |
|||
this.getActivityList(); |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
async getActivityList(isRefresh = false) { |
|||
if (this.loading) return; |
|||
if (isRefresh) { |
|||
this.page = 0; |
|||
this.noMore = false; |
|||
this.refreshing = true; |
|||
} 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; |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
|
|||
getEmptyText() { |
|||
const textMap = { |
|||
0: '未开始', |
|||
1: '进行中', |
|||
2: '已结束' |
|||
} |
|||
return `暂无${textMap[this.currentTab]}的预约~` |
|||
} |
|||
}, |
|||
onScrollLower() { |
|||
if (this.loading || this.noMore) return; |
|||
this.page += 1; |
|||
this.getActivityList(false); |
|||
}, |
|||
|
|||
async cancelAppoint(item) { |
|||
uni.showModal({ |
|||
title: '提示', |
|||
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(true); |
|||
} else { |
|||
uni.showToast({ title: res.msg || '取消预约失败', icon: 'none' }) |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
getEmptyText() { |
|||
const textMap = { |
|||
0: '未开始', |
|||
1: '进行中', |
|||
2: '已结束' |
|||
} |
|||
return `暂无${textMap[this.currentTab]}的预约~` |
|||
} |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.activity-list { |
|||
padding: 0; |
|||
height: 100vh; |
|||
box-sizing: border-box; |
|||
background-color: #f5f5f5; |
|||
padding: 0; |
|||
height: 100vh; |
|||
box-sizing: border-box; |
|||
background-color: #f5f5f5; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.tab-box { |
|||
display: flex; |
|||
background-color: #fff; |
|||
margin-bottom: 10px; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 99; |
|||
display: flex; |
|||
background-color: #fff; |
|||
margin-bottom: 10px; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 99; |
|||
} |
|||
.tab-item { |
|||
flex: 1; |
|||
text-align: center; |
|||
padding: 12px 0; |
|||
font-size: 15px; |
|||
color: #666; |
|||
position: relative; |
|||
flex: 1; |
|||
text-align: center; |
|||
padding: 12px 0; |
|||
font-size: 15px; |
|||
color: #666; |
|||
position: relative; |
|||
} |
|||
.tab-item.active { |
|||
color: #01a4fe; |
|||
font-weight: bold; |
|||
color: #01a4fe; |
|||
font-weight: bold; |
|||
} |
|||
.tab-item.active::after { |
|||
content: ''; |
|||
position: absolute; |
|||
width: 30px; |
|||
height: 3px; |
|||
background-color: #01a4fe; |
|||
bottom: 0; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
border-radius: 2px; |
|||
content: ''; |
|||
position: absolute; |
|||
width: 30px; |
|||
height: 3px; |
|||
background-color: #01a4fe; |
|||
bottom: 0; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.scroll-view { |
|||
height: calc(100vh - 60px); |
|||
padding: 0 12px; |
|||
box-sizing: border-box; |
|||
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); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: calc(100vh - 180px); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.empty-text { |
|||
margin-top: 16px; |
|||
font-size: 14px; |
|||
color: #999; |
|||
margin-top: 16px; |
|||
font-size: 14px; |
|||
color: #999; |
|||
} |
|||
|
|||
.activity-item { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
margin-bottom: 12px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); |
|||
cursor: pointer; |
|||
transition: transform 0.2s; |
|||
|
|||
&:active { |
|||
transform: scale(0.98); |
|||
} |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
margin-bottom: 12px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); |
|||
cursor: pointer; |
|||
transition: transform 0.2s; |
|||
|
|||
&:active { |
|||
transform: scale(0.98); |
|||
} |
|||
} |
|||
|
|||
.activity-info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
padding: 16px; |
|||
.title { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #222; |
|||
margin-bottom: 12px; |
|||
display: block; |
|||
} |
|||
.item-info { |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
padding: 16px; |
|||
.title { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #222; |
|||
margin-bottom: 12px; |
|||
display: block; |
|||
} |
|||
.item-info { |
|||
width: 100%; |
|||
display: flex; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
padding: 4px 0; |
|||
font-size: 14px; |
|||
.label { |
|||
color: #666; |
|||
width: 72px; |
|||
} |
|||
.value { |
|||
color: #333; |
|||
flex: 1; |
|||
padding: 4px 0; |
|||
font-size: 14px; |
|||
.label { |
|||
color: #666; |
|||
width: 72px; |
|||
} |
|||
.value { |
|||
color: #333; |
|||
flex: 1; |
|||
text-align: right; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 正确的状态样式 */ |
|||
.status-tag { |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 12px; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 12px; |
|||
} |
|||
.status-tag.status-0 { |
|||
color: #01a4fe; |
|||
background: #e6f7ff; |
|||
color: #01a4fe; |
|||
background: #e6f7ff; |
|||
} |
|||
.status-tag.status-1 { |
|||
color: #00b42a; |
|||
background: #e6ffed; |
|||
color: #00b42a; |
|||
background: #e6ffed; |
|||
} |
|||
.status-tag.status-2 { |
|||
color: #999; |
|||
background: #f5f5f5; |
|||
color: #999; |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
.btn-box { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
margin-top: 10px; |
|||
.btn-box { |
|||
width: 100%; |
|||
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; |
|||
border-radius: 6px; |
|||
padding: 0 14px; |
|||
height: 28px; |
|||
line-height: 28px; |
|||
background-color: #01a4fe; |
|||
font-size: 13px; |
|||
border-radius: 6px; |
|||
padding: 0 14px; |
|||
height: 28px; |
|||
line-height: 28px; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue