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

feat: 活动详情页新增报名表单 Modal(家庭成员选择 + 手动输入 + 合计)

点击立即报名弹出报名表单,可从家庭成员(孩子+家长)勾选,也可手动输入姓名手机号,支持多人同时报名。底部显示人数合计和金额。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
iwt 3 месяцев назад
Родитель
Сommit
1ed45821ba
1 измененных файлов с 354 добавлено и 24 удалено
  1. 354 24
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue

+ 354 - 24
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -189,6 +189,58 @@
           </view>
         </view>
       </modal>
+
+      <!-- 报名表单弹窗 -->
+      <modal v-if="showRegModal" class="reg-modal-overlay" :show="showRegModal" @close="showRegModal = false">
+        <view class="reg-modal">
+          <text class="reg-modal-title">活动报名</text>
+
+          <!-- 家庭成员选择 -->
+          <view class="reg-family-section" v-if="familyMembers.length > 0">
+            <text class="reg-section-label">从家庭成员中选择</text>
+            <view class="reg-family-list">
+              <view
+                class="reg-family-item"
+                v-for="member in familyMembers"
+                :key="member.id"
+                :class="{'reg-family-item-selected': isMemberSelected(member)}"
+                @click="toggleFamilyMember(member)"
+              >
+                <view class="reg-family-info">
+                  <text class="reg-family-name">{{ member.name }}</text>
+                  <text class="reg-family-relation">{{ member.relation }}</text>
+                </view>
+                <text class="reg-checkmark" v-if="isMemberSelected(member)">✓</text>
+              </view>
+            </view>
+          </view>
+
+          <!-- 报名人列表 -->
+          <view class="reg-list">
+            <text class="reg-section-label">报名人</text>
+            <view class="reg-entry" v-for="(entry, idx) in registrants" :key="idx">
+              <input class="reg-input reg-input-name" v-model="entry.name" placeholder="姓名" />
+              <input class="reg-input reg-input-phone" v-model="entry.phone" placeholder="手机号" type="number" maxlength="11" />
+              <text class="reg-remove" @click="removeRegistrant(idx)">✕</text>
+            </view>
+            <button class="reg-add-btn" @click="addManualEntry">+ 添加报名人</button>
+          </view>
+
+          <!-- 合计 -->
+          <view class="reg-total" v-if="activity.price > 0">
+            <text>共 {{ registrants.length }} 人 · 合计 ¥{{ activity.price * registrants.length }}</text>
+          </view>
+
+          <!-- 操作按钮 -->
+          <view class="reg-actions">
+            <button class="reg-cancel" @click="closeRegModal">取消</button>
+            <button class="reg-submit" :disabled="regSubmitting || registrants.length === 0" @click="submitRegistration">
+              <text v-if="regSubmitting">提交中...</text>
+              <text v-else>确认报名</text>
+            </button>
+          </view>
+        </view>
+      </modal>
     </scroll-view>
 
     <!-- 空状态 -->
@@ -199,7 +251,7 @@
 </template>
 
 <script>
-import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback } from '@/utils/api.js'
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback, getFamilyMembers } from '@/utils/api.js'
 import shareMixin from '../../../components/share-mixin.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 
@@ -227,7 +279,12 @@ export default {
       feedbackComment: '',
       submittedFeedback: null,
       feedbackSubmitting: false,
-      feedbackList: []
+      feedbackList: [],
+      // registration form modal
+      showRegModal: false,
+      familyMembers: [],
+      registrants: [],
+      regSubmitting: false
     }
   },
   onLoad: function(options) {
@@ -352,28 +409,10 @@ export default {
     },
     onRegister: function() {
       var self = this
-      var childId = uni.getStorageSync('currentChildId')
-      if (!childId) {
-        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)
-      }
+      // Open registration form modal
+      self.showRegModal = true
+      self.registrants = []
+      self.loadFamilyMembers()
     },
     doRegister: function(childId) {
       var self = this
@@ -493,6 +532,136 @@ export default {
       var full = parseInt(val) || 0
       var empty = 5 - full
       return '★'.repeat(full) + '☆'.repeat(empty)
+    },
+    // ===== 报名表单 =====
+    loadFamilyMembers: function() {
+      var self = this
+      getFamilyMembers().then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          var list = []
+          // parents
+          var parents = res.data.parents || []
+          for (var i = 0; i < parents.length; i++) {
+            list.push({
+              id: 'p_' + parents[i].id,
+              name: parents[i].nickname || parents[i].name || '',
+              phone: parents[i].phone || '',
+              relation: '家长',
+              childId: parents[i].id,
+              source: 'parent'
+            })
+          }
+          // children
+          var children = res.data.children || []
+          for (var j = 0; j < children.length; j++) {
+            list.push({
+              id: 'c_' + children[j].id,
+              name: children[j].nickname || children[j].name || '',
+              phone: children[j].phone || '',
+              relation: '孩子',
+              childId: children[j].userId || children[j].id,
+              source: 'child'
+            })
+          }
+          self.familyMembers = list
+        }
+      }).catch(function() {})
+    },
+    isMemberSelected: function(member) {
+      for (var i = 0; i < this.registrants.length; i++) {
+        if (this.registrants[i].memberId === member.id) return true
+      }
+      return false
+    },
+    toggleFamilyMember: function(member) {
+      for (var i = 0; i < this.registrants.length; i++) {
+        if (this.registrants[i].memberId === member.id) {
+          this.registrants.splice(i, 1)
+          return
+        }
+      }
+      this.registrants.push({
+        memberId: member.id,
+        name: member.name,
+        phone: member.phone,
+        childId: member.childId
+      })
+    },
+    addManualEntry: function() {
+      this.registrants.push({
+        memberId: null,
+        name: '',
+        phone: '',
+        childId: null
+      })
+    },
+    removeRegistrant: function(idx) {
+      this.registrants.splice(idx, 1)
+    },
+    closeRegModal: function() {
+      this.showRegModal = false
+      this.registrants = []
+      this.familyMembers = []
+    },
+    submitRegistration: function() {
+      var self = this
+      // Validate
+      if (self.registrants.length === 0) {
+        uni.showToast({ title: '请至少添加一个报名人', icon: 'none' })
+        return
+      }
+      for (var i = 0; i < self.registrants.length; i++) {
+        if (!self.registrants[i].name || self.registrants[i].name.trim() === '') {
+          uni.showToast({ title: '请输入第 ' + (i + 1) + ' 个报名人的姓名', icon: 'none' })
+          return
+        }
+      }
+
+      // 付费活动 → 先确认支付
+      if (self.activity && self.activity.price > 0) {
+        var total = self.activity.price * self.registrants.length
+        uni.showModal({
+          title: '确认报名',
+          content: '共 ' + self.registrants.length + ' 人,需支付 ¥' + total + ',报名成功后不支持退费。是否继续?',
+          confirmText: '确认支付',
+          cancelText: '再想想',
+          success: function(confirmRes) {
+            if (confirmRes.confirm) {
+              self.doBatchRegister()
+            }
+          }
+        })
+      } else {
+        self.doBatchRegister()
+      }
+    },
+    doBatchRegister: function() {
+      var self = this
+      self.regSubmitting = true
+
+      var registrantsData = []
+      for (var i = 0; i < self.registrants.length; i++) {
+        var entry = self.registrants[i]
+        registrantsData.push({
+          name: entry.name.trim(),
+          phone: entry.phone || '',
+          childId: entry.childId
+        })
+      }
+
+      registerActivity({ id: self.activity.id, registrants: registrantsData }).then(function(res) {
+        self.regSubmitting = false
+        if (res && res.code === 200) {
+          self.closeRegModal()
+          self.registrationStatus = 'pending'
+          uni.showToast({ title: '报名成功,请等待审核', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '报名失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.regSubmitting = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
     }
   },
   computed: {
@@ -877,4 +1046,165 @@ export default {
 .share-btn::after { border: none; }
 .share-btn-icon { font-size: 28rpx; margin-right: 4rpx; }
 .share-btn-text { font-size: 24rpx; }
+
+/* 报名表单弹窗 */
+.reg-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.reg-modal {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+  width: 85%;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+.reg-modal-title {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.reg-section-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #555;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.reg-family-section {
+  margin-bottom: 24rpx;
+}
+.reg-family-list {
+  max-height: 300rpx;
+  overflow-y: auto;
+}
+.reg-family-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 20rpx;
+  background: #f9f9f9;
+  border-radius: 12rpx;
+  margin-bottom: 10rpx;
+}
+.reg-family-item-selected {
+  background: #FFF7ED;
+  border: 2rpx solid #F97316;
+}
+.reg-family-info {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.reg-family-name {
+  font-size: 28rpx;
+  color: #333;
+  margin-right: 12rpx;
+}
+.reg-family-relation {
+  font-size: 22rpx;
+  color: #999;
+  background: #eee;
+  padding: 2rpx 12rpx;
+  border-radius: 8rpx;
+}
+.reg-checkmark {
+  color: #F97316;
+  font-size: 32rpx;
+  font-weight: bold;
+}
+.reg-list {
+  margin-bottom: 20rpx;
+}
+.reg-entry {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 12rpx;
+  gap: 10rpx;
+}
+.reg-input {
+  flex: 1;
+  background: #f5f5f5;
+  border: none;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 26rpx;
+  color: #333;
+}
+.reg-input-name {
+  flex: 2;
+}
+.reg-input-phone {
+  flex: 3;
+}
+.reg-remove {
+  color: #EF4444;
+  font-size: 28rpx;
+  padding: 10rpx;
+  flex-shrink: 0;
+}
+.reg-add-btn {
+  background: none;
+  border: 2rpx dashed #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  font-size: 26rpx;
+  color: #F97316;
+  text-align: center;
+  width: 100%;
+  margin-top: 6rpx;
+}
+.reg-total {
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #F97316;
+  margin-bottom: 24rpx;
+  padding: 16rpx;
+  background: #FFF7ED;
+  border-radius: 12rpx;
+}
+.reg-actions {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  gap: 20rpx;
+}
+.reg-cancel {
+  flex: 1;
+  background: #f5f5f5;
+  color: #666;
+  border: none;
+  border-radius: 40rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  text-align: center;
+}
+.reg-submit {
+  flex: 1;
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  text-align: center;
+}
+.reg-submit:active { opacity: 0.85; }
+.reg-submit[disabled] { opacity: 0.5; }
 </style>