my-activities.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <template>
  2. <view class="my-activities-page">
  3. <!-- 自定义导航栏 -->
  4. <view class="custom-navbar">
  5. <view class="navbar-back" @click="goBack">
  6. <text class="back-icon">←</text>
  7. </view>
  8. <view class="navbar-title">我的活动</view>
  9. <view class="navbar-placeholder"></view>
  10. </view>
  11. <!-- 标签页切换 -->
  12. <view class="tabs">
  13. <view class="tab-item" :class="{ active: currentTab === 'registered' }" @click="switchTab('registered')">
  14. <text class="tab-text">已报名</text>
  15. <view class="tab-badge" v-if="registeredCount > 0">{{ registeredCount }}</view>
  16. </view>
  17. <view class="tab-item" :class="{ active: currentTab === 'joined' }" @click="switchTab('joined')">
  18. <text class="tab-text">已参加</text>
  19. </view>
  20. <view class="tab-item" :class="{ active: currentTab === 'favorite' }" @click="switchTab('favorite')">
  21. <text class="tab-text">我的收藏</text>
  22. </view>
  23. </view>
  24. <!-- 活动列表 -->
  25. <view class="activities-list">
  26. <view class="activity-card" v-for="activity in filteredActivities" :key="activity.id" @click="goToActivityDetail(activity)">
  27. <image :src="activity.coverImage" class="activity-cover" mode="aspectFill"></image>
  28. <view class="activity-info">
  29. <view class="activity-header">
  30. <text class="activity-name">{{ activity.name }}</text>
  31. <el-tag :type="getStatusType(activity.status)" size="small">{{ getStatusText(activity.status) }}</el-tag>
  32. </view>
  33. <view class="activity-meta">
  34. <view class="meta-item">
  35. <text class="meta-icon">📅</text>
  36. <text class="meta-text">{{ formatDate(activity.startTime) }}</text>
  37. </view>
  38. <view class="meta-item" v-if="activity.location">
  39. <text class="meta-icon">📍</text>
  40. <text class="meta-text">{{ activity.location }}</text>
  41. </view>
  42. <view class="meta-item">
  43. <text class="meta-icon">👥</text>
  44. <text class="meta-text">{{ activity.actualParticipants }}/{{ activity.maxParticipants }}人</text>
  45. </view>
  46. </view>
  47. <view class="activity-footer">
  48. <text class="activity-price">¥{{ activity.price || 0 }}</text>
  49. <view class="activity-actions">
  50. <button class="action-btn" size="mini" v-if="currentTab === 'registered'" @click.stop="cancelActivity(activity)">取消报名</button>
  51. <button class="action-btn primary" size="mini" @click.stop="goToActivityDetail(activity)">查看详情</button>
  52. </view>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- 空状态 -->
  57. <view class="empty-state" v-if="filteredActivities.length === 0">
  58. <text class="empty-icon">📭</text>
  59. <text class="empty-text">{{ getEmptyText() }}</text>
  60. <button class="browse-btn" @click="goToActivityList">浏览活动</button>
  61. </view>
  62. </view>
  63. </view>
  64. </template>
  65. <script>
  66. export default {
  67. data() {
  68. return {
  69. currentTab: 'registered',
  70. registeredActivities: [],
  71. joinedActivities: [],
  72. favoriteActivities: [],
  73. registeredCount: 0
  74. }
  75. },
  76. computed: {
  77. filteredActivities() {
  78. if (this.currentTab === 'registered') {
  79. return this.registeredActivities
  80. } else if (this.currentTab === 'joined') {
  81. return this.joinedActivities
  82. } else {
  83. return this.favoriteActivities
  84. }
  85. }
  86. },
  87. onLoad() {
  88. this.loadMyActivities()
  89. },
  90. methods: {
  91. // 加载我的活动
  92. async loadMyActivities() {
  93. try {
  94. const userInfo = uni.getStorageSync('userInfo')
  95. if (!userInfo || !userInfo.userId) {
  96. console.warn('用户未登录')
  97. uni.showToast({
  98. title: '请先登录',
  99. icon: 'none'
  100. })
  101. return
  102. }
  103. const baseUrl = 'https://api.zhongruanke.cn' // 本地开发
  104. // const baseUrl = 'http://115.190.125.125:8083' // 生产环境
  105. // 获取已报名的活动(status=1)
  106. const [error1, registeredRes] = await uni.request({
  107. url: `${baseUrl}/api/activity-registration/my-activities?userId=${userInfo.userId}&status=1`,
  108. method: 'GET'
  109. })
  110. if (error1) {
  111. console.error('查询已报名活动失败:', error1)
  112. }
  113. if (registeredRes && registeredRes.data && registeredRes.data.code === 200) {
  114. this.registeredActivities = (registeredRes.data.data || []).map(item => ({
  115. id: item.id,
  116. activityId: item.activityId,
  117. name: item.activityName,
  118. coverImage: item.coverImage,
  119. startTime: item.startTime,
  120. endTime: item.endTime,
  121. location: item.location,
  122. price: item.price,
  123. actualParticipants: item.actualParticipants,
  124. maxParticipants: item.maxParticipants,
  125. status: this.getActivityStatusByTime(item.startTime, item.endTime)
  126. }))
  127. this.registeredCount = this.registeredActivities.length
  128. }
  129. // 获取已签到的活动(status=3)
  130. const [error2, joinedRes] = await uni.request({
  131. url: `${baseUrl}/api/activity-registration/my-activities?userId=${userInfo.userId}&status=3`,
  132. method: 'GET'
  133. })
  134. if (error2) {
  135. console.error('查询已参加活动失败:', error2)
  136. }
  137. if (joinedRes && joinedRes.data && joinedRes.data.code === 200) {
  138. this.joinedActivities = (joinedRes.data.data || []).map(item => ({
  139. id: item.id,
  140. activityId: item.activityId,
  141. name: item.activityName,
  142. coverImage: item.coverImage,
  143. startTime: item.startTime,
  144. endTime: item.endTime,
  145. location: item.location,
  146. price: item.price,
  147. actualParticipants: item.actualParticipants,
  148. maxParticipants: item.maxParticipants,
  149. status: 3
  150. }))
  151. }
  152. // 收藏功能暂时为空
  153. this.favoriteActivities = []
  154. } catch (error) {
  155. console.error('加载我的活动失败:', error)
  156. }
  157. },
  158. // 根据时间判断活动状态
  159. getActivityStatusByTime(startTime, endTime) {
  160. if (!startTime || !endTime) return 1
  161. const now = new Date()
  162. const start = new Date(startTime)
  163. const end = new Date(endTime)
  164. if (now < start) return 1 // 未开始
  165. if (now >= start && now <= end) return 2 // 进行中
  166. return 3 // 已结束
  167. },
  168. // 切换标签页
  169. switchTab(tab) {
  170. this.currentTab = tab
  171. },
  172. // 格式化日期
  173. formatDate(dateStr) {
  174. if (!dateStr) return '时间待定'
  175. try {
  176. const date = new Date(dateStr)
  177. const month = date.getMonth() + 1
  178. const day = date.getDate()
  179. const hours = date.getHours()
  180. const minutes = date.getMinutes()
  181. return `${month}月${day}日 ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
  182. } catch (error) {
  183. return '时间待定'
  184. }
  185. },
  186. // 获取状态类型
  187. getStatusType(status) {
  188. const types = { 1: 'info', 2: 'success', 3: 'default' }
  189. return types[status] || 'info'
  190. },
  191. // 获取状态文本
  192. getStatusText(status) {
  193. const texts = { 1: '未开始', 2: '进行中', 3: '已结束' }
  194. return texts[status] || '未知'
  195. },
  196. // 获取空状态文本
  197. getEmptyText() {
  198. const texts = {
  199. registered: '暂无报名的活动',
  200. joined: '暂无参加过的活动',
  201. favorite: '暂无收藏的活动'
  202. }
  203. return texts[this.currentTab] || '暂无数据'
  204. },
  205. // 取消报名
  206. async cancelActivity(activity) {
  207. uni.showModal({
  208. title: '提示',
  209. content: '确定要取消报名吗?',
  210. success: async (res) => {
  211. if (res.confirm) {
  212. try {
  213. const userInfo = uni.getStorageSync('userInfo')
  214. const baseUrl = 'https://api.zhongruanke.cn'
  215. const [error, result] = await uni.request({
  216. url: `${baseUrl}/api/activity-registration/cancel?id=${activity.id}&userId=${userInfo.userId}`,
  217. method: 'PUT'
  218. })
  219. if (error) {
  220. console.error('取消报名请求失败:', error)
  221. uni.showToast({
  222. title: '网络错误,请重试',
  223. icon: 'none'
  224. })
  225. return
  226. }
  227. if (result && result.data && result.data.code === 200) {
  228. uni.showToast({
  229. title: '已取消报名',
  230. icon: 'success'
  231. })
  232. this.loadMyActivities()
  233. } else {
  234. uni.showToast({
  235. title: (result && result.data && result.data.msg) || '取消失败',
  236. icon: 'none'
  237. })
  238. }
  239. } catch (error) {
  240. console.error('取消报名失败:', error)
  241. uni.showToast({
  242. title: '取消失败',
  243. icon: 'none'
  244. })
  245. }
  246. }
  247. }
  248. })
  249. },
  250. // 跳转到活动详情
  251. goToActivityDetail(activity) {
  252. uni.navigateTo({
  253. url: `/pages/activities/detail?id=${activity.activityId}`
  254. })
  255. },
  256. // 跳转到活动列表
  257. goToActivityList() {
  258. uni.navigateTo({
  259. url: '/pages/activities/list'
  260. })
  261. },
  262. // 返回上一页
  263. goBack() {
  264. uni.navigateBack()
  265. }
  266. }
  267. }
  268. </script>
  269. <style scoped>
  270. .my-activities-page {
  271. min-height: 100vh;
  272. background: #F5F5F5;
  273. }
  274. /* 自定义导航栏 */
  275. .custom-navbar {
  276. height: 88rpx;
  277. background: #E91E63;
  278. display: flex;
  279. align-items: center;
  280. justify-content: space-between;
  281. padding: 0 20rpx;
  282. color: white;
  283. }
  284. .navbar-back {
  285. padding: 10rpx 20rpx;
  286. }
  287. .back-icon {
  288. font-size: 40rpx;
  289. color: white;
  290. font-weight: bold;
  291. }
  292. .navbar-title {
  293. font-size: 32rpx;
  294. font-weight: bold;
  295. position: absolute;
  296. left: 50%;
  297. transform: translateX(-50%);
  298. }
  299. .navbar-placeholder {
  300. width: 80rpx;
  301. }
  302. /* 标签页 */
  303. .tabs {
  304. display: flex;
  305. background: white;
  306. padding: 0 30rpx;
  307. }
  308. .tab-item {
  309. flex: 1;
  310. text-align: center;
  311. padding: 30rpx 0;
  312. position: relative;
  313. }
  314. .tab-text {
  315. font-size: 28rpx;
  316. color: #666;
  317. }
  318. .tab-item.active .tab-text {
  319. color: #E91E63;
  320. font-weight: bold;
  321. }
  322. .tab-item.active::after {
  323. content: '';
  324. position: absolute;
  325. bottom: 0;
  326. left: 50%;
  327. transform: translateX(-50%);
  328. width: 60rpx;
  329. height: 6rpx;
  330. background: #E91E63;
  331. border-radius: 3rpx;
  332. }
  333. .tab-badge {
  334. position: absolute;
  335. top: 20rpx;
  336. right: 20%;
  337. background: #FF4D4F;
  338. color: white;
  339. font-size: 20rpx;
  340. padding: 2rpx 8rpx;
  341. border-radius: 10rpx;
  342. min-width: 32rpx;
  343. text-align: center;
  344. }
  345. /* 活动列表 */
  346. .activities-list {
  347. padding: 20rpx;
  348. }
  349. .activity-card {
  350. background: white;
  351. border-radius: 16rpx;
  352. margin-bottom: 20rpx;
  353. overflow: hidden;
  354. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
  355. }
  356. .activity-cover {
  357. width: 100%;
  358. height: 200rpx;
  359. background: #F5F5F5;
  360. }
  361. .activity-info {
  362. padding: 24rpx;
  363. }
  364. .activity-header {
  365. display: flex;
  366. justify-content: space-between;
  367. align-items: center;
  368. margin-bottom: 16rpx;
  369. }
  370. .activity-name {
  371. font-size: 32rpx;
  372. font-weight: bold;
  373. color: #333;
  374. flex: 1;
  375. margin-right: 16rpx;
  376. }
  377. .activity-meta {
  378. display: flex;
  379. flex-direction: column;
  380. gap: 12rpx;
  381. margin-bottom: 20rpx;
  382. }
  383. .meta-item {
  384. display: flex;
  385. align-items: center;
  386. gap: 8rpx;
  387. }
  388. .meta-icon {
  389. font-size: 24rpx;
  390. }
  391. .meta-text {
  392. font-size: 24rpx;
  393. color: #666;
  394. }
  395. .activity-footer {
  396. display: flex;
  397. justify-content: space-between;
  398. align-items: center;
  399. padding-top: 16rpx;
  400. border-top: 1rpx solid #F0F0F0;
  401. }
  402. .activity-price {
  403. font-size: 32rpx;
  404. font-weight: bold;
  405. color: #E91E63;
  406. }
  407. .activity-actions {
  408. display: flex;
  409. gap: 16rpx;
  410. }
  411. .action-btn {
  412. padding: 8rpx 24rpx;
  413. background: #F5F5F5;
  414. color: #666;
  415. border-radius: 20rpx;
  416. font-size: 24rpx;
  417. border: none;
  418. }
  419. .action-btn.primary {
  420. background: #E91E63;
  421. color: white;
  422. }
  423. /* 空状态 */
  424. .empty-state {
  425. display: flex;
  426. flex-direction: column;
  427. align-items: center;
  428. justify-content: center;
  429. padding: 120rpx 0;
  430. }
  431. .empty-icon {
  432. font-size: 120rpx;
  433. margin-bottom: 30rpx;
  434. }
  435. .empty-text {
  436. font-size: 28rpx;
  437. color: #999;
  438. margin-bottom: 40rpx;
  439. }
  440. .browse-btn {
  441. padding: 16rpx 60rpx;
  442. background: #E91E63;
  443. color: white;
  444. border-radius: 40rpx;
  445. font-size: 28rpx;
  446. border: none;
  447. }
  448. </style>