Просмотр исходного кода

feat(activity): 活动详情页报名+签到全流程 + 付费确认 + 日期格式化
- 新增 formatDate() 方法统一日期显示格式
- 新增付费活动报名确认弹窗和退费说明
- 新增报名状态检查(pending/approved)、取消报名功能
- 新增签到状态管理和 checkinTime 渲染
- discover-detail 和 discover 双路径 activity-detail.vue 同步增强

User 2 месяцев назад
Родитель
Сommit
735b25f827

+ 78 - 3
cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue

@@ -36,7 +36,7 @@
         <view class="meta-list">
           <view class="meta-item" v-if="activity.startTime">
             <text class="meta-icon">🕐</text>
-            <text class="meta-text">{{ activity.startTime }}{{ activity.endTime ? ' ~ ' + activity.endTime : '' }}</text>
+            <text class="meta-text">{{ formatDate(activity.startTime) }}{{ activity.endTime ? ' ~ ' + formatDate(activity.endTime) : '' }}</text>
           </view>
           <view class="meta-item" v-if="activity.location">
             <text class="meta-icon">📍</text>
@@ -57,6 +57,12 @@
           <text class="section-title">活动介绍</text>
           <text class="desc-text">{{ activity.description }}</text>
         </view>
+
+        <!-- 付费不退费说明 -->
+        <view class="refund-notice" v-if="activity.price > 0">
+          <text class="refund-notice-icon">⚠️</text>
+          <text class="refund-notice-text">本活动为付费活动,报名成功后不支持退费</text>
+        </view>
       </view>
 
       <!-- 底部操作栏 -->
@@ -70,11 +76,13 @@
           <button
             v-if="activity.requireRegistration === 1 && !registrationStatus"
             class="btn-register"
-            :class="{ 'btn-register-loading': registrationLoading }"
-            :disabled="registrationLoading"
+            :class="{ 'btn-register-loading': registrationLoading || paying }"
+            :disabled="registrationLoading || paying"
             @click="onRegister"
           >
             <text v-if="registrationLoading">报名中...</text>
+            <text v-else-if="paying">支付中...</text>
+            <text v-else-if="activity.price > 0">¥{{ activity.price }} 立即报名</text>
             <text v-else>立即报名</text>
           </button>
           
@@ -144,6 +152,7 @@ export default {
       checkinPoints: 0,
       registrationStatus: null,
       registrationLoading: false,
+      paying: false,
       showPenaltyModal: false,
       penaltyAmount: 0
     }
@@ -163,6 +172,22 @@ export default {
     }
   },
   methods: {
+    // ===== 工具方法 =====
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr.replace(/-/g, '/'))
+      if (isNaN(d.getTime())) return dateStr
+      var year = d.getFullYear()
+      var month = d.getMonth() + 1
+      var day = d.getDate()
+      var hour = d.getHours()
+      var minute = d.getMinutes()
+      var ampm = hour < 12 ? '上午' : '下午'
+      var h12 = hour % 12
+      if (h12 === 0) h12 = 12
+      var minStr = minute < 10 ? '0' + minute : minute
+      return year + '年' + month + '月' + day + '日 ' + ampm + h12 + ':' + minStr
+    },
     loadDetail: function(id) {
       var self = this
       self.loading = true
@@ -255,9 +280,33 @@ export default {
         uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
         return
       }
+
+      // 付费活动 → 先确认支付
+      if (self.activity && self.activity.price > 0) {
+        uni.showModal({
+          title: '确认报名',
+          content: '该活动需支付 ¥' + self.activity.price + ',报名成功后不支持退费。是否继续?',
+          confirmText: '确认支付',
+          cancelText: '再想想',
+          success: function(confirmRes) {
+            if (confirmRes.confirm) {
+              self.doRegister(childId)
+            }
+          }
+        })
+      } else {
+        self.doRegister(childId)
+      }
+    },
+    doRegister: function(childId) {
+      var self = this
       self.registrationLoading = true
+      if (self.activity && self.activity.price > 0) {
+        self.paying = true
+      }
       registerActivity({ id: self.activity.id, childId: childId }).then(function(res) {
         self.registrationLoading = false
+        self.paying = false
         if (res && res.code === 200) {
           self.registrationStatus = 'pending'
           uni.showToast({ title: '报名成功,请等待审核', icon: 'success' })
@@ -266,6 +315,7 @@ export default {
         }
       }).catch(function(err) {
         self.registrationLoading = false
+        self.paying = false
         uni.showToast({ title: '网络异常', icon: 'none' })
       })
     },
@@ -434,6 +484,31 @@ export default {
   line-height: 1.6;
 }
 
+/* 付费不退费说明 */
+.refund-notice {
+  margin-top: 20rpx;
+  margin-left: 30rpx;
+  margin-right: 30rpx;
+  margin-bottom: 10rpx;
+  background: #FEF2F2;
+  border: 2rpx solid #FECACA;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.refund-notice-icon {
+  font-size: 24rpx;
+  margin-right: 10rpx;
+  flex-shrink: 0;
+}
+.refund-notice-text {
+  font-size: 24rpx;
+  color: #DC2626;
+  line-height: 1.4;
+}
+
 /* 底部操作栏 */
 .bottom-bar {
   position: fixed;

+ 300 - 11
cfc-frontend/pages/discover/activity-detail/activity-detail.vue

@@ -36,7 +36,7 @@
         <view class="meta-list">
           <view class="meta-item" v-if="activity.startTime">
             <text class="meta-icon">🕐</text>
-            <text class="meta-text">{{ activity.startTime }}{{ activity.endTime ? ' ~ ' + activity.endTime : '' }}</text>
+            <text class="meta-text">{{ formatDate(activity.startTime) }}{{ activity.endTime ? ' ~ ' + formatDate(activity.endTime) : '' }}</text>
           </view>
           <view class="meta-item" v-if="activity.location">
             <text class="meta-icon">📍</text>
@@ -57,6 +57,12 @@
           <text class="section-title">活动介绍</text>
           <text class="desc-text">{{ activity.description }}</text>
         </view>
+
+        <!-- 付费不退费说明 -->
+        <view class="refund-notice" v-if="hasPrice">
+          <text class="refund-notice-icon">⚠️</text>
+          <text class="refund-notice-text">本活动为付费活动,报名成功后不支持退费</text>
+        </view>
       </view>
 
       <!-- 底部操作栏 -->
@@ -66,15 +72,57 @@
           <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
         </view>
         <view class="action-btns">
+          <!-- 未报名 → 显示报名按钮 -->
+          <button
+            v-if="!registrationStatus && activity.requireRegistration !== 0"
+            class="btn-register"
+            :class="{ 'btn-register-loading': registrationLoading || paying }"
+            :disabled="registrationLoading || paying"
+            @click="onRegister"
+          >
+            <text v-if="registrationLoading">报名中...</text>
+            <text v-else-if="paying">支付中...</text>
+            <text v-else-if="hasPrice">¥{{ activity.price }} 立即报名</text>
+            <text v-else>立即报名</text>
+          </button>
+
+          <!-- 已报名(pending 待审核) -->
+          <button
+            v-if="registrationStatus === 'pending'"
+            class="btn-register-done"
+            disabled
+          >
+            <text>审核中</text>
+          </button>
+
+          <!-- 已报名(approved)→ 显示"已报名" + "签到" -->
+          <template v-if="registrationStatus === 'approved'">
+            <button
+              class="btn-registered"
+              :disabled="registrationLoading"
+              @click="onCancelRegistration"
+            >
+              <text>已报名</text>
+            </button>
+            <button
+              class="btn-checkin"
+              :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
+              :disabled="checkinLoading || checkinDone"
+              @click="onCheckin"
+            >
+              <text v-if="checkinLoading">签到中...</text>
+              <text v-else-if="checkinDone">已签到 ✓</text>
+              <text v-else>签到</text>
+            </button>
+          </template>
+
+          <!-- 已签到(无报名流程的活动) -->
           <button
-            class="btn-checkin"
-            :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
-            :disabled="checkinLoading || checkinDone"
-            @click="onCheckin"
+            v-if="registrationStatus === 'approved' && checkinDone"
+            class="btn-checkin btn-checkin-done"
+            disabled
           >
-            <text v-if="checkinLoading">签到中...</text>
-            <text v-else-if="checkinDone">已签到 ✓</text>
-            <text v-else>签到</text>
+            <text>已签到 ✓</text>
           </button>
         </view>
       </view>
@@ -88,7 +136,9 @@
 </template>
 
 <script>
-import { getActivityDetail, activityCheckin } from '@/utils/api.js'
+import { getActivityDetail, activityCheckin, registerActivity, getMyActivityRegistrations, cancelActivityRegistration } from '@/utils/api.js'
+
+var WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
 
 export default {
   data() {
@@ -100,7 +150,17 @@ export default {
       errorMsg: '',
       checkinLoading: false,
       checkinDone: false,
-      checkinPoints: 0
+      checkinPoints: 0,
+      // 报名相关
+      registrationStatus: '',     // '' | 'pending' | 'approved'
+      registrationLoading: false,
+      registrationId: null,
+      paying: false
+    }
+  },
+  computed: {
+    hasPrice: function() {
+      return this.activity && this.activity.price > 0
     }
   },
   onLoad: function(options) {
@@ -118,6 +178,24 @@ export default {
     }
   },
   methods: {
+    // ===== 工具方法 =====
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr.replace(/-/g, '/'))
+      if (isNaN(d.getTime())) return dateStr
+      var year = d.getFullYear()
+      var month = d.getMonth() + 1
+      var day = d.getDate()
+      var hour = d.getHours()
+      var minute = d.getMinutes()
+      var ampm = hour < 12 ? '上午' : '下午'
+      var h12 = hour % 12
+      if (h12 === 0) h12 = 12
+      var minStr = minute < 10 ? '0' + minute : minute
+      return year + '年' + month + '月' + day + '日 ' + ampm + h12 + ':' + minStr
+    },
+
+    // ===== 数据加载 =====
     loadDetail: function(id) {
       var self = this
       self.loading = true
@@ -126,6 +204,8 @@ export default {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           self.activity = res.data
+          // 加载详情后检查报名状态
+          self.loadRegistrationStatus(id)
         } else {
           self.error = true
           self.errorMsg = (res && res.message) || '加载失败'
@@ -140,6 +220,139 @@ export default {
       var map = { draft: '草稿', published: '进行中', ended: '已结束' }
       return map[status] || '未知'
     },
+
+    // ===== 报名相关 =====
+    loadRegistrationStatus: function(activityId) {
+      var self = this
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) return
+
+      getMyActivityRegistrations({ id: activityId, childId: childId, page: 1, size: 10 }).then(function(res) {
+        var records, i, reg
+        if (res && res.code === 200 && res.data) {
+          records = res.data.records || []
+          for (i = 0; i < records.length; i++) {
+            reg = records[i]
+            if (String(reg.activityId) === String(activityId)) {
+              self.registrationStatus = reg.status || 'pending'
+              self.registrationId = reg.id
+              if (reg.checkinTime) {
+                self.checkinDone = true
+              }
+              return
+            }
+          }
+        }
+      }).catch(function(err) {
+        // 静默失败,不阻塞页面
+      })
+    },
+    onRegister: function() {
+      var self = this
+      if (self.registrationLoading || self.paying) return
+
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请选择孩子身份', icon: 'none' })
+        return
+      }
+
+      // 付费活动 → 先确认支付
+      if (self.hasPrice) {
+        uni.showModal({
+          title: '确认报名',
+          content: '该活动需支付 ¥' + self.activity.price + ',报名成功后不支持退费。是否继续?',
+          confirmText: '确认支付',
+          cancelText: '再想想',
+          success: function(confirmRes) {
+            if (confirmRes.confirm) {
+              self.doRegister(childId)
+            }
+          }
+        })
+      } else {
+        // 免费活动 → 直接报名
+        uni.showModal({
+          title: '确认报名',
+          content: '确认报名该活动?',
+          confirmText: '立即报名',
+          cancelText: '再想想',
+          success: function(confirmRes) {
+            if (confirmRes.confirm) {
+              self.doRegister(childId)
+            }
+          }
+        })
+      }
+    },
+    doRegister: function(childId) {
+      var self = this
+      self.registrationLoading = true
+
+      // 付费活动模拟支付等待
+      if (self.hasPrice) {
+        self.paying = true
+      }
+
+      registerActivity({ id: self.activity.id, childId: childId }).then(function(res) {
+        var msg
+        self.registrationLoading = false
+        self.paying = false
+        if (res && res.code === 200) {
+          self.registrationStatus = 'pending'
+          self.registrationId = res.data && res.data.id
+          uni.showToast({ title: '报名成功', icon: 'success' })
+          // 重新加载详情获取最新人数
+          self.loadDetail(self.activityId)
+        } else {
+          msg = (res && res.message) || '报名失败'
+          uni.showToast({ title: msg, icon: 'none' })
+        }
+      }).catch(function(err) {
+        self.registrationLoading = false
+        self.paying = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    onCancelRegistration: function() {
+      var self = this
+      if (self.registrationLoading) return
+
+      uni.showModal({
+        title: '取消报名',
+        content: '确定取消报名?',
+        confirmText: '确认取消',
+        confirmColor: '#EF4444',
+        success: function(confirmRes) {
+          if (confirmRes.confirm) {
+            self.doCancelRegistration()
+          }
+        }
+      })
+    },
+    doCancelRegistration: function() {
+      var self = this
+      self.registrationLoading = true
+      cancelActivityRegistration({ id: self.activity.id, childId: uni.getStorageSync('currentChildId') }).then(function(res) {
+        var msg
+        self.registrationLoading = false
+        if (res && res.code === 200) {
+          self.registrationStatus = ''
+          self.registrationId = null
+          uni.showToast({ title: '已取消报名', icon: 'success' })
+          // 重新加载详情
+          self.loadDetail(self.activityId)
+        } else {
+          msg = (res && res.message) || '取消失败'
+          uni.showToast({ title: msg, icon: 'none' })
+        }
+      }).catch(function(err) {
+        self.registrationLoading = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+
+    // ===== 签到相关 =====
     onCheckin: function() {
       var self = this
       if (self.checkinLoading || self.checkinDone) return
@@ -152,6 +365,7 @@ export default {
 
       self.checkinLoading = true
       activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
+        var msg
         self.checkinLoading = false
         if (res && res.code === 200 && res.data) {
           if (res.data.alreadyCompleted) {
@@ -166,7 +380,7 @@ export default {
             uni.showToast({ title: '签到成功', icon: 'success' })
           }
         } else {
-          var msg = (res && res.message) || '签到失败'
+          msg = (res && res.message) || '签到失败'
           uni.showToast({ title: msg, icon: 'none' })
         }
       }).catch(function(err) {
@@ -314,6 +528,28 @@ export default {
   line-height: 1.6;
 }
 
+/* 付费不退费说明 */
+.refund-notice {
+  margin-top: 20rpx;
+  background: #FEF2F2;
+  border: 2rpx solid #FECACA;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.refund-notice-icon {
+  font-size: 24rpx;
+  margin-right: 10rpx;
+  flex-shrink: 0;
+}
+.refund-notice-text {
+  font-size: 24rpx;
+  color: #DC2626;
+  line-height: 1.4;
+}
+
 /* 底部操作栏 */
 .bottom-bar {
   position: fixed;
@@ -347,6 +583,59 @@ export default {
   flex-direction: row;
   gap: 16rpx;
 }
+
+/* 报名按钮 */
+.btn-register {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 48rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  line-height: 1.5;
+  min-width: 160rpx;
+  text-align: center;
+}
+.btn-register:active {
+  opacity: 0.85;
+}
+.btn-register-loading {
+  opacity: 0.7;
+}
+
+/* 已报名(待审核) */
+.btn-register-done {
+  background: #9CA3AF;
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 48rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  line-height: 1.5;
+  min-width: 160rpx;
+  text-align: center;
+}
+
+/* 已报名(可取消) */
+.btn-registered {
+  background: #F3F4F6;
+  color: #4B5563;
+  border: 2rpx solid #D1D5DB;
+  border-radius: 40rpx;
+  padding: 14rpx 36rpx;
+  font-size: 26rpx;
+  font-weight: 500;
+  line-height: 1.5;
+  min-width: 120rpx;
+  text-align: center;
+}
+.btn-registered:active {
+  opacity: 0.7;
+}
+
+/* 签到按钮 */
 .btn-checkin {
   background: linear-gradient(135deg, #F97316, #FB923C);
   color: #fff;