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

518 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. if (loginRes.code) {
  185. const res = await FetchSessionKey({
  186. libcode: getApp().globalData.libcode,
  187. code: loginRes.code
  188. });
  189. if (res.code === 200) {
  190. this.sessionKey = res.data.session_key;
  191. this.openid = res.data.openid;
  192. }
  193. }
  194. resolve()
  195. },
  196. fail: () => {
  197. resolve()
  198. }
  199. });
  200. })
  201. },
  202. async getActivityList(isRefresh = false) {
  203. if (!this.openid) {
  204. uni.showToast({ title: '获取用户信息失败', icon: 'none' })
  205. return
  206. }
  207. if (this.loading) return;
  208. if (isRefresh) {
  209. this.page = 0;
  210. this.noMore = false;
  211. this.refreshing = true;
  212. } else {
  213. if (this.noMore) return;
  214. }
  215. this.loading = true;
  216. const currentTabItem = this.tabList[this.activeTab]
  217. const params = {
  218. openId: this.openid,
  219. page: this.page,
  220. pageSize: this.pageSize
  221. }
  222. if(currentTabItem.value !== null){
  223. params.isCancel = currentTabItem.value
  224. }
  225. try {
  226. const res = await FetchMySignActivityInfo(params);
  227. if (res.code === 200) {
  228. const pageData = res.data;
  229. const list = pageData.content || [];
  230. // ==========新增:统一处理图片地址,和第一个接口逻辑保持一致==========
  231. list.forEach(item => {
  232. if(item.cover_image){
  233. const imgId = encodeURIComponent(item.cover_image);
  234. item.cover_image = `${this.baseUrl}/files/${imgId}`;
  235. }else{
  236. item.cover_image = ''
  237. }
  238. })
  239. const currentPage = pageData.number ?? 0;
  240. const totalPage = pageData.totalPages ?? 0;
  241. this.noMore = currentPage >= totalPage - 1;
  242. if (isRefresh) {
  243. this.activityList = list;
  244. } else {
  245. this.activityList = [...this.activityList, ...list];
  246. }
  247. } else {
  248. if (isRefresh) this.activityList = [];
  249. }
  250. } catch (err) {
  251. console.error('请求活动接口异常', err)
  252. uni.showToast({ title: '网络请求失败', icon: 'none' })
  253. } finally {
  254. this.refreshing = false;
  255. this.loading = false;
  256. }
  257. },
  258. onRefresh() {
  259. if(this.loading) return
  260. this.getActivityList(true);
  261. },
  262. onScrollLower() {
  263. if(this.loading || this.noMore) return;
  264. this.page += 1;
  265. this.getActivityList(false);
  266. },
  267. cancelAppoint(item) {
  268. uni.showModal({
  269. title: '提示',
  270. content: `确定要取消活动【${item.title}】的预约吗?`,
  271. success: async (res) => {
  272. if (res.confirm) {
  273. try {
  274. const apiRes = await FetchCancelSignActivity({
  275. id: item.signId
  276. })
  277. if(apiRes.code === 200){
  278. uni.showToast({ title: '取消成功', icon: 'success' })
  279. this.getActivityList(true)
  280. }else{
  281. uni.showToast({ title: apiRes.message || '取消失败', icon: 'none' })
  282. }
  283. }catch(err){
  284. console.error('取消预约失败', err)
  285. uni.showToast({ title: '网络异常,取消失败', icon: 'none' })
  286. }
  287. }
  288. }
  289. })
  290. },
  291. deleteSignActivity(item){
  292. uni.showModal({
  293. title: '提示',
  294. content: `确定要删除活动【${item.title}】的预约吗?`,
  295. success: async (res) => {
  296. if (res.confirm) {
  297. try {
  298. const apiRes = await FetchDeleteSignActivity({
  299. id: item.signId
  300. })
  301. if(apiRes.code === 200){
  302. uni.showToast({ title: '删除成功', icon: 'success' })
  303. this.getActivityList(true)
  304. }else{
  305. uni.showToast({ title: apiRes.message || '删除失败', icon: 'none' })
  306. }
  307. }catch(err){
  308. console.error('删除预约失败', err)
  309. uni.showToast({ title: '网络异常,删除失败', icon: 'none' })
  310. }
  311. }
  312. }
  313. })
  314. },
  315. toActivityDetail(item) {
  316. uni.navigateTo({
  317. url: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(item))
  318. });
  319. }
  320. },
  321. };
  322. </script>
  323. <style lang="scss" scoped>
  324. .activity-list {
  325. padding: 0;
  326. height: 100vh;
  327. box-sizing: border-box;
  328. background-color: #f5f5f5;
  329. display:flex;
  330. flex-direction:column;
  331. }
  332. .tab-box {
  333. display: flex;
  334. background-color: #fff;
  335. margin-bottom: 10px;
  336. position: sticky;
  337. top: 0;
  338. z-index: 99;
  339. }
  340. .tab-item{
  341. flex:1;
  342. text-align:center;
  343. padding:12px 0;
  344. font-size:14px;
  345. color:#666;
  346. position:relative;
  347. }
  348. .tab-item.active{
  349. color:#01a4fe;
  350. font-weight:bold;
  351. }
  352. .tab-item.active::after{
  353. content:"";
  354. position:absolute;
  355. bottom:0;
  356. left:50%;
  357. transform:translateX(-50%);
  358. width:28px;
  359. height:2px;
  360. background:#01a4fe;
  361. }
  362. .scroll-view {
  363. flex:1;
  364. height: 0;
  365. padding: 0 12px;
  366. box-sizing: border-box;
  367. }
  368. .loading-box{
  369. padding:20px 0;
  370. text-align:center;
  371. }
  372. .loading-text{
  373. font-size:14px;
  374. color:#999;
  375. }
  376. .nomore-box{
  377. padding:15px 0;
  378. text-align:center;
  379. }
  380. .nomore-text{
  381. font-size:13px;
  382. color:#999;
  383. }
  384. .empty-box {
  385. height: calc(100vh - 180px);
  386. display: flex;
  387. flex-direction: column;
  388. align-items: center;
  389. justify-content: center;
  390. }
  391. .empty-text {
  392. margin-top: 16px;
  393. font-size: 14px;
  394. color: #999;
  395. }
  396. .activity-item {
  397. background: #fff;
  398. border-radius: 12px;
  399. overflow: hidden;
  400. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  401. .activity-info {
  402. display: flex;
  403. flex-direction: column;
  404. align-items: flex-start;
  405. padding: 16px;
  406. .title-row{
  407. width:100%;
  408. display:flex;
  409. justify-content: space-between;
  410. align-items:flex-start;
  411. margin-bottom:12px;
  412. }
  413. .title {
  414. flex: 1;
  415. font-size: 16px;
  416. font-weight: bold;
  417. color: #222;
  418. }
  419. .item-info {
  420. display: flex;
  421. justify-content: space-between;
  422. padding: 4px 0;
  423. font-size: 14px;
  424. width:100%;
  425. .label {
  426. color: #666;
  427. width: 72px;
  428. }
  429. .value {
  430. color: #333;
  431. flex: 1;
  432. text-align: right;
  433. }
  434. }
  435. }
  436. }
  437. .btn-box {
  438. width: 100%;
  439. display: flex;
  440. justify-content: flex-end;
  441. margin-top: 10px;
  442. padding-top: 10px;
  443. border-top: 1px solid #e5e5e5;
  444. }
  445. .status-tag {
  446. padding: 2px 8px;
  447. border-radius: 4px;
  448. font-size: 12px;
  449. }
  450. .status-tag.status-0 {
  451. color: #01a4fe;
  452. background: #e6f7ff;
  453. }
  454. .status-tag.status-1 {
  455. color: #00b42a;
  456. background: #e6ffed;
  457. }
  458. .status-tag.status-2 {
  459. color: #999;
  460. background: #f5f5f5;
  461. }
  462. /* 👉 插槽外层容器:占满高度,垂直水平居中 */
  463. .swipe-right-wrap {
  464. width: 80px;
  465. height: calc(100% - 20px);
  466. display: flex;
  467. align-items: center; /* 垂直居中 */
  468. justify-content: center; /*水平居中*/
  469. // background-color: #f5222d;
  470. }
  471. /* 圆形按钮,不再写height:100%,固定大小做圆圈 */
  472. .swipe-btn {
  473. width: 48px;
  474. height: 48px;
  475. border-radius: 50%;
  476. display: flex;
  477. align-items: center;
  478. justify-content: center;
  479. color: #ffffff !important;
  480. }
  481. .swipe-btn-delete {
  482. background-color: #f5222d;
  483. }
  484. /* 深度修改uni-icons图标颜色,scoped必须加 ::v‑deep */
  485. ::v-deep .swipe-btn .uni-icons {
  486. color: #ffffff !important;
  487. }
  488. </style>