图书馆小程序
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.

405 lines
9.5 KiB

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