图书馆小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
8.7 KiB

4 weeks ago
4 weeks ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <template>
  2. <view class="activity-list">
  3. <view class="tab-box">
  4. <view
  5. class="tab-item"
  6. :class="{ active: currentTab === 0 }"
  7. @click="switchTab(0)"
  8. >
  9. 未开始
  10. </view>
  11. <view
  12. class="tab-item"
  13. :class="{ active: currentTab === 1 }"
  14. @click="switchTab(1)"
  15. >
  16. 进行中
  17. </view>
  18. <view
  19. class="tab-item"
  20. :class="{ active: currentTab === 2 }"
  21. @click="switchTab(2)"
  22. >
  23. 已结束
  24. </view>
  25. </view>
  26. <scroll-view
  27. scroll-y
  28. @scrolltolower="onScrollLower"
  29. lower-threshold="150"
  30. class="scroll-view"
  31. >
  32. <view v-if="loading" class="loading-box">
  33. <text class="loading-text">加载中...</text>
  34. </view>
  35. <view
  36. class="activity-item"
  37. v-for="item in activityList"
  38. :key="item.id"
  39. >
  40. <view class="activity-info">
  41. <view class="title-row">
  42. <text class="title">{{ item.fondsName }}</text>
  43. </view>
  44. <view class="item-info">
  45. <text class="label">入馆日期</text>
  46. <text class="value">{{ item.visitDate }}</text>
  47. </view>
  48. <view class="item-info">
  49. <text class="label">入馆时间</text>
  50. <text class="value">{{ item.visitTime }}-{{ item.endTimeHm }}</text>
  51. </view>
  52. <!-- <view class="item-info">
  53. <text class="label">所在馆</text>
  54. <text class="value">{{ item.fondsName }}</text>
  55. </view> -->
  56. <view class="btn-box" v-if="item.status === 0">
  57. <button class="activity-btn" type="primary" @click.stop="cancelAppoint(item)">
  58. 取消预约
  59. </button>
  60. </view>
  61. </view>
  62. </view>
  63. <view v-if="!loading && noMore && activityList.length > 0" class="nomore-box">
  64. <text class="nomore-text">没有更多数据了</text>
  65. </view>
  66. <view class="empty-box" v-if="!loading && activityList.length === 0">
  67. <uni-icons type="empty" size="80" color="#ccc"></uni-icons>
  68. <text class="empty-text">{{ getEmptyText() }}</text>
  69. </view>
  70. </scroll-view>
  71. </view>
  72. </template>
  73. <script>
  74. import { FetchMyReservation, FetchCancelReservation } from '@/api/reservation';
  75. import { getOpenId } from '@/utils/storage';
  76. import config from '@/utils/config';
  77. export default {
  78. data() {
  79. return {
  80. currentTab: 0,
  81. activityList: [],
  82. refreshing: false,
  83. loading: false,
  84. noMore: false,
  85. page: 0,
  86. pageSize: 10
  87. };
  88. },
  89. onLoad() {
  90. this.getActivityList(true);
  91. },
  92. onPullDownRefresh() {
  93. if (this.loading) {
  94. uni.stopPullDownRefresh()
  95. return
  96. }
  97. this.getActivityList(true).finally(() => {
  98. uni.stopPullDownRefresh()
  99. })
  100. },
  101. methods: {
  102. formatTsToDate(ts) {
  103. const d = new Date(ts);
  104. const y = d.getFullYear();
  105. const m = String(d.getMonth() + 1).padStart(2, '0');
  106. const day = String(d.getDate()).padStart(2, '0');
  107. return `${y}-${m}-${day}`;
  108. },
  109. formatTimeHm(timeStr) {
  110. if (!timeStr) return ''
  111. return timeStr.substring(0, 5)
  112. },
  113. switchTab(index) {
  114. this.currentTab = index;
  115. this.page = 0;
  116. this.noMore = false;
  117. this.activityList = [];
  118. this.getActivityList(true);
  119. },
  120. async fetchMyReservation() {
  121. const openId = await getOpenId();
  122. const res = await FetchMyReservation({
  123. libcode: config.LIB_CODE,
  124. openId: openId,
  125. page: this.page,
  126. size: this.pageSize,
  127. status: this.currentTab
  128. })
  129. console.log('我的预约', res)
  130. if (res.code === 200 && Array.isArray(res.data.content)) {
  131. const list = res.data.content.map(raw => {
  132. return {
  133. id: raw.id,
  134. fondsName: raw.fondsName || '',
  135. visitDate: this.formatTsToDate(raw.specificDate),
  136. visitTime: this.formatTimeHm(raw.startTime),
  137. endTimeHm: this.formatTimeHm(raw.endTime),
  138. status: Number(this.currentTab),
  139. startTimeRaw: raw.startTime,
  140. endTimeRaw: raw.endTime,
  141. specificDate: raw.specificDate
  142. }
  143. })
  144. // 分页判断(和活动预约逻辑保持一致)
  145. const currentPage = res.data.number ?? 0;
  146. const totalPage = res.data.totalPages ?? 0;
  147. return {
  148. list,
  149. noMore: currentPage >= totalPage - 1
  150. }
  151. }
  152. return { list: [], noMore: true }
  153. },
  154. async getActivityList(isRefresh = false) {
  155. if (this.loading) return;
  156. if (isRefresh) {
  157. this.page = 0;
  158. this.noMore = false;
  159. this.refreshing = true;
  160. } else {
  161. if (this.noMore) return;
  162. }
  163. this.loading = true;
  164. try {
  165. const { list, noMore } = await this.fetchMyReservation();
  166. this.noMore = noMore;
  167. if (isRefresh) {
  168. this.activityList = list;
  169. } else {
  170. this.activityList = [...this.activityList, ...list];
  171. }
  172. } catch (e) {
  173. console.error('获取我的预约异常', e)
  174. uni.showToast({ title: '获取预约列表失败', icon: 'none' })
  175. if (isRefresh) this.activityList = [];
  176. } finally {
  177. this.refreshing = false;
  178. this.loading = false;
  179. }
  180. },
  181. onScrollLower() {
  182. if (this.loading || this.noMore) return;
  183. this.page += 1;
  184. this.getActivityList(false);
  185. },
  186. async cancelAppoint(item) {
  187. uni.showModal({
  188. title: '提示',
  189. content: `确定要取消当前时间段的预约吗?`,
  190. success: async (res) => {
  191. if (res.confirm) {
  192. const res = await FetchCancelReservation({
  193. id: item.id
  194. })
  195. if (res.code === 200) {
  196. uni.showToast({ title: '取消成功', icon: 'success' });
  197. this.getActivityList(true);
  198. } else {
  199. uni.showToast({ title: res.msg || '取消预约失败', icon: 'none' })
  200. }
  201. }
  202. }
  203. })
  204. },
  205. getEmptyText() {
  206. const textMap = {
  207. 0: '未开始',
  208. 1: '进行中',
  209. 2: '已结束'
  210. }
  211. return `暂无${textMap[this.currentTab]}的预约~`
  212. }
  213. },
  214. };
  215. </script>
  216. <style lang="scss" scoped>
  217. .activity-list {
  218. padding: 0;
  219. height: 100vh;
  220. box-sizing: border-box;
  221. background-color: #f5f5f5;
  222. display: flex;
  223. flex-direction: column;
  224. }
  225. .tab-box {
  226. display: flex;
  227. background-color: #fff;
  228. margin-bottom: 10px;
  229. position: sticky;
  230. top: 0;
  231. z-index: 99;
  232. }
  233. .tab-item {
  234. flex: 1;
  235. text-align: center;
  236. padding: 12px 0;
  237. font-size: 15px;
  238. color: #666;
  239. position: relative;
  240. }
  241. .tab-item.active {
  242. color: #01a4fe;
  243. font-weight: bold;
  244. }
  245. .tab-item.active::after {
  246. content: '';
  247. position: absolute;
  248. width: 30px;
  249. height: 3px;
  250. background-color: #01a4fe;
  251. bottom: 0;
  252. left: 50%;
  253. transform: translateX(-50%);
  254. border-radius: 2px;
  255. }
  256. .scroll-view {
  257. flex: 1;
  258. height: 0;
  259. padding: 0 12px;
  260. box-sizing: border-box;
  261. }
  262. .loading-box {
  263. padding: 20px 0;
  264. text-align: center;
  265. }
  266. .loading-text {
  267. font-size: 14px;
  268. color: #999;
  269. }
  270. .nomore-box {
  271. padding: 15px 0;
  272. text-align: center;
  273. }
  274. .nomore-text {
  275. font-size: 13px;
  276. color: #999;
  277. }
  278. .empty-box {
  279. height: calc(100vh - 180px);
  280. display: flex;
  281. flex-direction: column;
  282. align-items: center;
  283. justify-content: center;
  284. }
  285. .empty-text {
  286. margin-top: 16px;
  287. font-size: 14px;
  288. color: #999;
  289. }
  290. .activity-item {
  291. background: #fff;
  292. border-radius: 12px;
  293. margin-bottom: 12px;
  294. overflow: hidden;
  295. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  296. cursor: pointer;
  297. transition: transform 0.2s;
  298. &:active {
  299. transform: scale(0.98);
  300. }
  301. }
  302. .activity-info {
  303. display: flex;
  304. flex-direction: column;
  305. align-items: flex-start;
  306. padding: 16px;
  307. .title {
  308. font-size: 16px;
  309. font-weight: bold;
  310. color: #222;
  311. margin-bottom: 12px;
  312. display: block;
  313. }
  314. .item-info {
  315. width: 100%;
  316. display: flex;
  317. justify-content: space-between;
  318. padding: 4px 0;
  319. font-size: 14px;
  320. .label {
  321. color: #666;
  322. width: 72px;
  323. }
  324. .value {
  325. color: #333;
  326. flex: 1;
  327. text-align: right;
  328. }
  329. }
  330. }
  331. .status-tag {
  332. padding: 2px 8px;
  333. border-radius: 4px;
  334. font-size: 12px;
  335. }
  336. .status-tag.status-0 {
  337. color: #01a4fe;
  338. background: #e6f7ff;
  339. }
  340. .status-tag.status-1 {
  341. color: #00b42a;
  342. background: #e6ffed;
  343. }
  344. .status-tag.status-2 {
  345. color: #999;
  346. background: #f5f5f5;
  347. }
  348. .btn-box {
  349. width: 100%;
  350. display: flex;
  351. justify-content: flex-end;
  352. margin-top: 10px;
  353. padding-top: 10px;
  354. border-top: 1px solid #e5e5e5;
  355. }
  356. .activity-btn {
  357. background-color: #01a4fe;
  358. font-size: 13px;
  359. border-radius: 6px;
  360. padding: 0 14px;
  361. height: 28px;
  362. line-height: 28px;
  363. }
  364. </style>