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> |
<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"> |
<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> |
</template> |
||||
|
|
||||
<script> |
<script> |
||||
|
import { FetchMyReservation, FetchCancelReservation } from '@/api/reservation'; |
||||
|
import { getOpenId } from '@/utils/storage'; |
||||
|
import config from '@/utils/config'; |
||||
|
|
||||
export default { |
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> |
</script> |
||||
|
|
||||
<style lang="scss" scoped> |
<style lang="scss" scoped> |
||||
.activity-list { |
.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 { |
.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 { |
.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 { |
.tab-item.active { |
||||
color: #01a4fe; |
|
||||
font-weight: bold; |
|
||||
|
color: #01a4fe; |
||||
|
font-weight: bold; |
||||
} |
} |
||||
.tab-item.active::after { |
.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 { |
.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 { |
.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 { |
.empty-text { |
||||
margin-top: 16px; |
|
||||
font-size: 14px; |
|
||||
color: #999; |
|
||||
|
margin-top: 16px; |
||||
|
font-size: 14px; |
||||
|
color: #999; |
||||
} |
} |
||||
|
|
||||
.activity-item { |
.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 { |
.activity-info { |
||||
display: flex; |
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%; |
width: 100%; |
||||
display: flex; |
|
||||
|
display: flex; |
||||
justify-content: space-between; |
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; |
text-align: right; |
||||
} |
|
||||
} |
|
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|
||||
/* 正确的状态样式 */ |
|
||||
.status-tag { |
.status-tag { |
||||
padding: 2px 8px; |
|
||||
border-radius: 4px; |
|
||||
font-size: 12px; |
|
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
} |
} |
||||
.status-tag.status-0 { |
.status-tag.status-0 { |
||||
color: #01a4fe; |
|
||||
background: #e6f7ff; |
|
||||
|
color: #01a4fe; |
||||
|
background: #e6f7ff; |
||||
} |
} |
||||
.status-tag.status-1 { |
.status-tag.status-1 { |
||||
color: #00b42a; |
|
||||
background: #e6ffed; |
|
||||
|
color: #00b42a; |
||||
|
background: #e6ffed; |
||||
} |
} |
||||
.status-tag.status-2 { |
.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 { |
.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> |
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue