my-activities.vue 11 KB

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