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

521 lines
13 KiB

1 month ago
1 month ago
1 month ago
3 weeks ago
1 month ago
1 month ago
1 month ago
3 weeks ago
1 month ago
3 weeks ago
1 month ago
  1. <template>
  2. <view class="activity-list">
  3. <!-- tab栏 -->
  4. <view class="tab-box">
  5. <view
  6. class="tab-item"
  7. v-for="(tab,idx) in tabList"
  8. :key="idx"
  9. :class="{active:activeTab === idx}"
  10. @click="switchTab(idx)"
  11. >
  12. {{tab.label}}
  13. </view>
  14. </view>
  15. <scroll-view
  16. scroll-y
  17. @scrolltolower="onScrollLower"
  18. lower-threshold="150"
  19. class="scroll-view"
  20. >
  21. <view v-if="loading" class="loading-box">
  22. <text class="loading-text">加载中...</text>
  23. </view>
  24. <!-- 左滑组件 uni-swipe-action -->
  25. <!-- :disabled="!(item.status === '未开始' && !item.isCancel)" -->
  26. <uni-swipe-action>
  27. <uni-swipe-action-item
  28. v-for="item in activityList"
  29. :key="item.signId"
  30. >
  31. <!-- 左侧主体内容 -->
  32. <view class="activity-item" @click="toActivityDetail(item)">
  33. <view class="activity-info">
  34. <view class="title-row">
  35. <text class="title">{{ item.title }}</text>
  36. <text class="status-tag" :class="getStatusClass(item.status)">
  37. {{ item.status }}
  38. </text>
  39. </view>
  40. <view class="item-info">
  41. <text class="label">开始时间</text>
  42. <text class="value">{{ formatShowDateTime(item.start_time) }}</text>
  43. </view>
  44. <view class="item-info">
  45. <text class="label">结束时间</text>
  46. <text class="value">{{ formatShowDateTime(item.end_time) }}</text>
  47. </view>
  48. <view class="item-info">
  49. <text class="label">预约人</text>
  50. <text class="value">{{ item.signName }}</text>
  51. </view>
  52. <view class="item-info">
  53. <text class="label">参加人数</text>
  54. <text class="value">{{ item.signNum }}</text>
  55. </view>
  56. <!-- 未开始 && 未取消预约 才显示取消预约按钮 -->
  57. <view class="btn-box" v-if="item.status === '未开始' && !item.isCancel">
  58. <button class="activity-btn" type="primary" @click.stop="cancelAppoint(item)"> 取消预约 </button>
  59. </view>
  60. </view>
  61. </view>
  62. <!-- 右滑出来的按钮插槽 -->
  63. <!-- v-if="!(item.status === '未开始' && !item.isCancel)" -->
  64. <!-- v-if="item.status !== '进行中'" -->
  65. <template #right >
  66. <view class="swipe-right-wrap">
  67. <view class="swipe-btn swipe-btn-delete" @click.stop="deleteSignActivity(item)">
  68. <uni-icons type="trash" size="24"></uni-icons>
  69. </view>
  70. </view>
  71. </template>
  72. </uni-swipe-action-item>
  73. </uni-swipe-action>
  74. <view v-if="!loading && noMore && activityList.length > 0" class="nomore-box">
  75. <text class="nomore-text">没有更多数据了</text>
  76. </view>
  77. <view class="empty-box" v-if="!loading && activityList.length === 0">
  78. <uni-icons type="empty" size="80" color="#ccc"></uni-icons>
  79. <text class="empty-text">暂无相关活动~</text>
  80. </view>
  81. </scroll-view>
  82. </view>
  83. </template>
  84. <script>
  85. import config from '@/utils/config';
  86. import { FetchSessionKey } from '@/api/user.js';
  87. import { FetchMySignActivityInfo, FetchCancelSignActivity,FetchDeleteSignActivity } from '@/api/activity.js'
  88. export default {
  89. data() {
  90. return {
  91. tabList:[
  92. {label:'全部',value:null},
  93. {label:'预约成功',value:false},
  94. {label:'已取消',value:true}
  95. ],
  96. activeTab:0,
  97. activityList: [],
  98. baseUrl: config.baseUrl,
  99. refreshing: false,
  100. loading: false,
  101. noMore: false,
  102. page: 0,
  103. pageSize: 20,
  104. sessionKey: '',
  105. openid: ''
  106. };
  107. },
  108. async onLoad() {
  109. await this.getSessionKey();
  110. this.getActivityList(true);
  111. uni.$on('refreshMySignList',()=>{
  112. this.getActivityList(true)
  113. })
  114. },
  115. onUnload(){
  116. uni.$off('refreshMySignList')
  117. },
  118. onPullDownRefresh() {
  119. if(this.loading){
  120. uni.stopPullDownRefresh()
  121. return
  122. }
  123. this.getActivityList(true).finally(()=>{
  124. uni.stopPullDownRefresh()
  125. })
  126. },
  127. methods: {
  128. switchTab(index){
  129. this.activeTab = index
  130. this.page = 0
  131. this.noMore = false
  132. this.activityList = []
  133. this.getActivityList(true)
  134. },
  135. // 新增:时间戳 → 今天/昨天/明天 HH:mm
  136. formatShowDateTime(timestamp) {
  137. if (!timestamp) return '';
  138. const d = new Date(timestamp);
  139. const targetY = d.getFullYear();
  140. const targetM = d.getMonth();
  141. const targetD = d.getDate();
  142. const hh = String(d.getHours()).padStart(2, '0');
  143. const mm = String(d.getMinutes()).padStart(2, '0');
  144. const timeStr = `${hh}:${mm}`;
  145. const now = new Date();
  146. const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  147. const targetDayTs = new Date(targetY, targetM, targetD).getTime();
  148. const oneDay = 24 * 60 * 60 * 1000;
  149. const diff = Math.round((targetDayTs - today.getTime()) / oneDay);
  150. let dateText = '';
  151. if (diff === 0) {
  152. dateText = '今天';
  153. } else if (diff === -1) {
  154. dateText = '昨天';
  155. } else if (diff === 1) {
  156. dateText = '明天';
  157. } else {
  158. const m = String(targetM + 1).padStart(2, '0');
  159. const day = String(targetD).padStart(2, '0');
  160. dateText = `${targetY}-${m}-${day}`;
  161. }
  162. return `${dateText} ${timeStr}`;
  163. },
  164. formatTime(timestamp) {
  165. if (!timestamp) return '';
  166. const date = new Date(timestamp);
  167. const y = date.getFullYear();
  168. const m = String(date.getMonth() + 1).padStart(2, '0');
  169. const d = String(date.getDate()).padStart(2, '0');
  170. const h = String(date.getHours()).padStart(2, '0');
  171. const min = String(date.getMinutes()).padStart(2, '0');
  172. return `${y}-${m}-${d} ${h}:${min}`;
  173. },
  174. getStatusClass(status) {
  175. if (status === '未开始') return 'status-0';
  176. if (status === '进行中') return 'status-1';
  177. if (status === '已结束') return 'status-2';
  178. return '';
  179. },
  180. async getSessionKey() {
  181. return new Promise((resolve) => {
  182. uni.login({
  183. success: async (loginRes) => {
  184. console.log('loginRes', loginRes)
  185. if (loginRes.code) {
  186. const res = await FetchSessionKey({
  187. libcode: getApp().globalData.libcode,
  188. code: loginRes.code
  189. });
  190. if (res.code === 200) {
  191. this.sessionKey = res.data.session_key;
  192. this.openid = res.data.openid;
  193. }
  194. }
  195. resolve()
  196. },
  197. fail: () => {
  198. resolve()
  199. }
  200. });
  201. })
  202. },
  203. async getActivityList(isRefresh = false) {
  204. if (!this.openid) {
  205. uni.showToast({ title: '获取用户信息失败', icon: 'none' })
  206. return
  207. }
  208. if (this.loading) return;
  209. if (isRefresh) {
  210. this.page = 0;
  211. this.noMore = false;
  212. this.refreshing = true;
  213. } else {
  214. if (this.noMore) return;
  215. }
  216. this.loading = true;
  217. const currentTabItem = this.tabList[this.activeTab]
  218. const params = {
  219. openId: this.openid,
  220. page: this.page,
  221. pageSize: this.pageSize
  222. }
  223. if(currentTabItem.value !== null){
  224. params.isCancel = currentTabItem.value
  225. }
  226. try {
  227. const res = await FetchMySignActivityInfo(params);
  228. console.log('FetchMySignActivityInfo返回结果', res)
  229. if (res.code === 200) {
  230. const pageData = res.data;
  231. const list = pageData.content || [];
  232. // ==========新增:统一处理图片地址,和第一个接口逻辑保持一致==========
  233. list.forEach(item => {
  234. if(item.cover_image){
  235. const imgId = encodeURIComponent(item.cover_image);
  236. item.cover_image = `${this.baseUrl}/files/${imgId}`;
  237. }else{
  238. item.cover_image = ''
  239. }
  240. })
  241. const currentPage = pageData.number ?? 0;
  242. const totalPage = pageData.totalPages ?? 0;
  243. this.noMore = currentPage >= totalPage - 1;
  244. if (isRefresh) {
  245. this.activityList = list;
  246. } else {
  247. this.activityList = [...this.activityList, ...list];
  248. }
  249. } else {
  250. if (isRefresh) this.activityList = [];
  251. }
  252. } catch (err) {
  253. console.error('请求活动接口异常', err)
  254. uni.showToast({ title: '网络请求失败', icon: 'none' })
  255. } finally {
  256. this.refreshing = false;
  257. this.loading = false;
  258. }
  259. },
  260. onRefresh() {
  261. if(this.loading) return
  262. this.getActivityList(true);
  263. },
  264. onScrollLower() {
  265. if(this.loading || this.noMore) return;
  266. this.page += 1;
  267. this.getActivityList(false);
  268. },
  269. cancelAppoint(item) {
  270. uni.showModal({
  271. title: '提示',
  272. content: `确定要取消活动【${item.title}】的预约吗?`,
  273. success: async (res) => {
  274. if (res.confirm) {
  275. try {
  276. const apiRes = await FetchCancelSignActivity({
  277. id: item.signId
  278. })
  279. if(apiRes.code === 200){
  280. uni.showToast({ title: '取消成功', icon: 'success' })
  281. this.getActivityList(true)
  282. }else{
  283. uni.showToast({ title: apiRes.message || '取消失败', icon: 'none' })
  284. }
  285. }catch(err){
  286. console.error('取消预约失败', err)
  287. uni.showToast({ title: '网络异常,取消失败', icon: 'none' })
  288. }
  289. }
  290. }
  291. })
  292. },
  293. deleteSignActivity(item){
  294. uni.showModal({
  295. title: '提示',
  296. content: `确定要删除活动【${item.title}】的预约吗?`,
  297. success: async (res) => {
  298. if (res.confirm) {
  299. try {
  300. const apiRes = await FetchDeleteSignActivity({
  301. id: item.signId
  302. })
  303. if(apiRes.code === 200){
  304. uni.showToast({ title: '删除成功', icon: 'success' })
  305. this.getActivityList(true)
  306. }else{
  307. uni.showToast({ title: apiRes.message || '删除失败', icon: 'none' })
  308. }
  309. }catch(err){
  310. console.error('删除预约失败', err)
  311. uni.showToast({ title: '网络异常,删除失败', icon: 'none' })
  312. }
  313. }
  314. }
  315. })
  316. },
  317. toActivityDetail(item) {
  318. console.log('toActivityDetail',item);
  319. uni.navigateTo({
  320. url: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(item))
  321. });
  322. }
  323. },
  324. };
  325. </script>
  326. <style lang="scss" scoped>
  327. .activity-list {
  328. padding: 0;
  329. height: 100vh;
  330. box-sizing: border-box;
  331. background-color: #f5f5f5;
  332. display:flex;
  333. flex-direction:column;
  334. }
  335. .tab-box {
  336. display: flex;
  337. background-color: #fff;
  338. margin-bottom: 10px;
  339. position: sticky;
  340. top: 0;
  341. z-index: 99;
  342. }
  343. .tab-item{
  344. flex:1;
  345. text-align:center;
  346. padding:12px 0;
  347. font-size:14px;
  348. color:#666;
  349. position:relative;
  350. }
  351. .tab-item.active{
  352. color:#01a4fe;
  353. font-weight:bold;
  354. }
  355. .tab-item.active::after{
  356. content:"";
  357. position:absolute;
  358. bottom:0;
  359. left:50%;
  360. transform:translateX(-50%);
  361. width:28px;
  362. height:2px;
  363. background:#01a4fe;
  364. }
  365. .scroll-view {
  366. flex:1;
  367. height: 0;
  368. padding: 0 12px;
  369. box-sizing: border-box;
  370. }
  371. .loading-box{
  372. padding:20px 0;
  373. text-align:center;
  374. }
  375. .loading-text{
  376. font-size:14px;
  377. color:#999;
  378. }
  379. .nomore-box{
  380. padding:15px 0;
  381. text-align:center;
  382. }
  383. .nomore-text{
  384. font-size:13px;
  385. color:#999;
  386. }
  387. .empty-box {
  388. height: calc(100vh - 180px);
  389. display: flex;
  390. flex-direction: column;
  391. align-items: center;
  392. justify-content: center;
  393. }
  394. .empty-text {
  395. margin-top: 16px;
  396. font-size: 14px;
  397. color: #999;
  398. }
  399. .activity-item {
  400. background: #fff;
  401. border-radius: 12px;
  402. overflow: hidden;
  403. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  404. .activity-info {
  405. display: flex;
  406. flex-direction: column;
  407. align-items: flex-start;
  408. padding: 16px;
  409. .title-row{
  410. width:100%;
  411. display:flex;
  412. justify-content: space-between;
  413. align-items:flex-start;
  414. margin-bottom:12px;
  415. }
  416. .title {
  417. flex: 1;
  418. font-size: 16px;
  419. font-weight: bold;
  420. color: #222;
  421. }
  422. .item-info {
  423. display: flex;
  424. justify-content: space-between;
  425. padding: 4px 0;
  426. font-size: 14px;
  427. width:100%;
  428. .label {
  429. color: #666;
  430. width: 72px;
  431. }
  432. .value {
  433. color: #333;
  434. flex: 1;
  435. text-align: right;
  436. }
  437. }
  438. }
  439. }
  440. .btn-box {
  441. width: 100%;
  442. display: flex;
  443. justify-content: flex-end;
  444. margin-top: 10px;
  445. padding-top: 10px;
  446. border-top: 1px solid #e5e5e5;
  447. }
  448. .status-tag {
  449. padding: 2px 8px;
  450. border-radius: 4px;
  451. font-size: 12px;
  452. }
  453. .status-tag.status-0 {
  454. color: #01a4fe;
  455. background: #e6f7ff;
  456. }
  457. .status-tag.status-1 {
  458. color: #00b42a;
  459. background: #e6ffed;
  460. }
  461. .status-tag.status-2 {
  462. color: #999;
  463. background: #f5f5f5;
  464. }
  465. /* 👉 插槽外层容器:占满高度,垂直水平居中 */
  466. .swipe-right-wrap {
  467. width: 80px;
  468. height: calc(100% - 20px);
  469. display: flex;
  470. align-items: center; /* 垂直居中 */
  471. justify-content: center; /*水平居中*/
  472. // background-color: #f5222d;
  473. }
  474. /* 圆形按钮,不再写height:100%,固定大小做圆圈 */
  475. .swipe-btn {
  476. width: 48px;
  477. height: 48px;
  478. border-radius: 50%;
  479. display: flex;
  480. align-items: center;
  481. justify-content: center;
  482. color: #ffffff !important;
  483. }
  484. .swipe-btn-delete {
  485. background-color: #f5222d;
  486. }
  487. /* 深度修改uni-icons图标颜色,scoped必须加 ::v‑deep */
  488. ::v-deep .swipe-btn .uni-icons {
  489. color: #ffffff !important;
  490. }
  491. </style>