Răsfoiți Sursa

feat: 补充AI对话和profile页面变更

User 4 luni în urmă
părinte
comite
669b7c5472

+ 573 - 0
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -0,0 +1,573 @@
+<template>
+  <view class="container">
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading-wrap">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 错误状态 -->
+    <view v-else-if="error" class="error-wrap">
+      <text class="error-icon">📋</text>
+      <text class="error-text">{{ errorMsg }}</text>
+      <button class="retry-btn" @click="loadDetail(activityId)">重新加载</button>
+    </view>
+
+    <!-- 活动内容 -->
+    <scroll-view v-else-if="activity.id" scroll-y class="content">
+      <!-- 封面图 -->
+      <image
+        v-if="activity.coverImage"
+        class="cover"
+        :src="activity.coverImage"
+        mode="widthFix"
+      />
+      <image v-else class="cover placeholder-cover" src="/static/default-activity.png" mode="aspectFill" />
+
+      <view class="info-section">
+        <text class="activity-title">{{ activity.title }}</text>
+
+        <!-- 活动状态标签 -->
+        <view class="status-row">
+          <text class="status-tag" :class="'status-' + (activity.status || 'upcoming')">{{ statusText(activity.status) }}</text>
+        </view>
+
+        <!-- 活动元信息 -->
+        <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>
+          </view>
+          <view class="meta-item" v-if="activity.location">
+            <text class="meta-icon">📍</text>
+            <text class="meta-text">{{ activity.location }}</text>
+          </view>
+          <view class="meta-item" v-if="activity.maxParticipants">
+            <text class="meta-icon">👥</text>
+            <text class="meta-text">{{ activity.currentParticipants || 0 }} / {{ activity.maxParticipants }} 人</text>
+          </view>
+          <view class="meta-item">
+            <text class="meta-icon">💰</text>
+            <text class="meta-text">{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
+          </view>
+        </view>
+
+        <!-- 活动简介 -->
+        <view class="desc-section" v-if="activity.description">
+          <text class="section-title">活动介绍</text>
+          <text class="desc-text">{{ activity.description }}</text>
+        </view>
+      </view>
+
+      <!-- 底部操作栏 -->
+      <view class="bottom-bar">
+        <view class="price-info">
+          <text class="bottom-type">活动</text>
+          <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
+        </view>
+        <view class="action-btns">
+          <!-- 报名按钮(仅 requireRegistration=1 时显示) -->
+          <button
+            v-if="activity.requireRegistration === 1 && !registrationStatus"
+            class="btn-register"
+            :class="{ 'btn-register-loading': registrationLoading }"
+            :disabled="registrationLoading"
+            @click="onRegister"
+          >
+            <text v-if="registrationLoading">报名中...</text>
+            <text v-else>立即报名</text>
+          </button>
+          
+          <!-- 已报名待审核 -->
+          <button
+            v-if="activity.requireRegistration === 1 && registrationStatus === 'pending'"
+            class="btn-register-done"
+            disabled
+          >
+            <text>报名待审核</text>
+          </button>
+          
+          <!-- 已报名可取消 -->
+          <button
+            v-if="activity.requireRegistration === 1 && registrationStatus === 'approved'"
+            class="btn-cancel"
+            :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>
+        </view>
+      </view>
+      
+      <!-- 能量扣减提示弹窗 -->
+      <modal v-if="showPenaltyModal" class="penalty-modal" :show="showPenaltyModal" @close="showPenaltyModal = false">
+        <view class="penalty-content">
+          <text class="penalty-title">签到成功但已扣能量</text>
+          <text class="penalty-desc">您未报名参加此活动,已被扣除 {{ penaltyAmount }} 点能量值</text>
+          <button class="penalty-btn" @click="showPenaltyModal = false">我知道了</button>
+        </view>
+      </modal>
+    </scroll-view>
+
+    <!-- 空状态 -->
+    <view v-else class="empty-wrap">
+      <text class="empty-text">活动不存在或已下架</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      activityId: null,
+      activity: {},
+      loading: false,
+      error: false,
+      errorMsg: '',
+      checkinLoading: false,
+      checkinDone: false,
+      checkinPoints: 0,
+      registrationStatus: null,
+      registrationLoading: false,
+      showPenaltyModal: false,
+      penaltyAmount: 0
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.id) {
+      this.activityId = options.id
+      this.loadDetail(options.id)
+    } else {
+      this.error = true
+      this.errorMsg = '缺少活动ID'
+    }
+
+    // 检查是否已经签到(从路由参数获取)
+    if (options && options.checkinDone === '1') {
+      this.checkinDone = true
+    }
+  },
+  methods: {
+    loadDetail: function(id) {
+      var self = this
+      self.loading = true
+      self.error = false
+      getActivityDetail(id).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+          self.activity = res.data
+          // Load registration status if logged in and activity requires registration
+          if (self.activity.requireRegistration === 1) {
+            var childId = uni.getStorageSync('currentChildId')
+            if (childId) {
+              getMyActivityRegistrations({ childId: childId, page: 1, size: 50 }).then(function(regRes) {
+                if (regRes && regRes.data && regRes.data.records) {
+                  var records = regRes.data.records
+                  for (var i = 0; i < records.length; i++) {
+                    if (records[i].activityId === self.activity.id) {
+                      self.registrationStatus = records[i].status
+                      break
+                    }
+                  }
+                }
+              }).catch(function() {})
+            }
+          }
+        } else {
+          self.error = true
+          self.errorMsg = (res && res.message) || '加载失败'
+        }
+      }).catch(function(err) {
+        self.loading = false
+        self.error = true
+        self.errorMsg = '网络异常,请稍后重试'
+      })
+    },
+    statusText: function(status) {
+      var map = { draft: '草稿', published: '进行中', ended: '已结束' }
+      return map[status] || '未知'
+    },
+    onCheckin: function() {
+      var self = this
+      if (self.checkinLoading || self.checkinDone) return
+
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请选择孩子身份', icon: 'none' })
+        return
+      }
+
+      self.checkinLoading = true
+      activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
+        self.checkinLoading = false
+        if (res && res.code === 200 && res.data) {
+          // 检查是否有能量扣减
+          if (res.data.energyPenalty) {
+            self.penaltyAmount = res.data.energyPenalty
+            self.showPenaltyModal = true
+            self.checkinDone = true
+            return
+          }
+          if (res.data.alreadyCompleted) {
+            self.checkinDone = true
+            uni.showToast({ title: '已签到', icon: 'success' })
+          } else if (res.data.pointsEarned && res.data.pointsEarned > 0) {
+            self.checkinDone = true
+            self.checkinPoints = res.data.pointsEarned
+            uni.showToast({ title: '签到成功 +' + res.data.pointsEarned + '积分', icon: 'success' })
+          } else {
+            self.checkinDone = true
+            uni.showToast({ title: '签到成功', icon: 'success' })
+          }
+        } else {
+          var msg = (res && res.message) || '签到失败'
+          // 如果是报名待审核,提示用户
+          if (msg.indexOf('报名') !== -1 || msg.indexOf('审核') !== -1) {
+            uni.showModal({ title: '提示', content: msg, showCancel: false })
+          } else {
+            uni.showToast({ title: msg, icon: 'none' })
+          }
+        }
+      }).catch(function(err) {
+        self.checkinLoading = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    onRegister: function() {
+      var self = this
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+        return
+      }
+      self.registrationLoading = true
+      registerActivity({ id: self.activity.id, childId: childId }).then(function(res) {
+        self.registrationLoading = false
+        if (res && res.code === 200) {
+          self.registrationStatus = 'pending'
+          uni.showToast({ title: '报名成功,请等待审核', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '报名失败', icon: 'none' })
+        }
+      }).catch(function(err) {
+        self.registrationLoading = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    onCancelRegistration: function() {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确定要取消报名吗?',
+        success: function(res) {
+          if (res.confirm) {
+            var childId = uni.getStorageSync('currentChildId')
+            self.registrationLoading = true
+            cancelActivityRegistration({ id: self.activity.id, childId: childId }).then(function(res) {
+              self.registrationLoading = false
+              if (res && res.code === 200) {
+                self.registrationStatus = null
+                uni.showToast({ title: '已取消报名', icon: 'success' })
+              } else {
+                uni.showToast({ title: (res && res.message) || '取消失败', icon: 'none' })
+              }
+            }).catch(function() {
+              self.registrationLoading = false
+              uni.showToast({ title: '网络异常', icon: 'none' })
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+/* 加载 / 错误 / 空状态 */
+.loading-wrap,
+.error-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 40rpx;
+}
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #FDE68A;
+  border-top-color: #F97316;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 20rpx;
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+.loading-text,
+.error-text,
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 20rpx;
+}
+.error-icon {
+  font-size: 60rpx;
+  margin-bottom: 16rpx;
+}
+.retry-btn {
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 12rpx 40rpx;
+  font-size: 26rpx;
+  line-height: 1.5;
+}
+
+/* 内容区 */
+.content {
+  height: 100vh;
+  padding-bottom: 120rpx;
+}
+.cover {
+  width: 100%;
+  display: block;
+}
+.placeholder-cover {
+  height: 400rpx;
+  background: #f0f0f0;
+}
+
+/* 信息区 */
+.info-section {
+  padding: 30rpx;
+}
+.activity-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #1f2937;
+  line-height: 1.4;
+}
+.status-row {
+  margin-top: 16rpx;
+}
+.status-tag {
+  display: inline-block;
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 8rpx;
+}
+.status-draft { background: #F5F5F5; color: #999; }
+.status-published { background: #F0FDF4; color: #22C55E; }
+.status-ended { background: #F5F5F5; color: #999; }
+.status-upcoming { background: #FFF7ED; color: #F97316; }
+
+/* 元信息列表 */
+.meta-list {
+  margin-top: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.meta-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.meta-item:last-child {
+  margin-bottom: 0;
+}
+.meta-icon {
+  font-size: 28rpx;
+  margin-right: 12rpx;
+  width: 32rpx;
+  text-align: center;
+}
+.meta-text {
+  font-size: 26rpx;
+  color: #4b5563;
+  flex: 1;
+  line-height: 1.4;
+}
+
+/* 活动介绍 */
+.desc-section {
+  margin-top: 30rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1f2937;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.desc-text {
+  font-size: 26rpx;
+  color: #4b5563;
+  line-height: 1.6;
+}
+
+/* 底部操作栏 */
+.bottom-bar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.06);
+}
+.price-info {
+  display: flex;
+  flex-direction: column;
+}
+.bottom-type {
+  font-size: 22rpx;
+  color: #999;
+}
+.bottom-vendor {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.action-btns {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.btn-checkin {
+  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-checkin:active {
+  opacity: 0.85;
+}
+.btn-checkin-done {
+  background: #22C55E;
+}
+.btn-checkin-loading {
+  opacity: 0.7;
+}
+
+/* 报名按钮 */
+.btn-register {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 32rpx;
+  font-size: 26rpx;
+  font-weight: 600;
+  line-height: 1.5;
+  min-width: 140rpx;
+  text-align: center;
+}
+.btn-register:active { opacity: 0.85; }
+.btn-register-loading { opacity: 0.7; }
+.btn-register-done {
+  background: #F5F5F5;
+  color: #999;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 32rpx;
+  font-size: 26rpx;
+  line-height: 1.5;
+  min-width: 140rpx;
+  text-align: center;
+}
+.btn-cancel {
+  background: #fff;
+  color: #999;
+  border: 2rpx solid #ddd;
+  border-radius: 40rpx;
+  padding: 14rpx 28rpx;
+  font-size: 24rpx;
+  line-height: 1.5;
+  min-width: 120rpx;
+  text-align: center;
+}
+.btn-cancel:active { opacity: 0.7; }
+
+/* 能量扣减弹窗 */
+.penalty-modal {
+  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;
+}
+.penalty-content {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 48rpx 40rpx;
+  width: 560rpx;
+  text-align: center;
+}
+.penalty-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #EF4444;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.penalty-desc {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 40rpx;
+  line-height: 1.6;
+}
+.penalty-btn {
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 60rpx;
+  font-size: 28rpx;
+}
+</style>

+ 24 - 1
cfc-frontend/pages/ai/chat.vue

@@ -40,7 +40,7 @@
         <view class="welcome-card" v-if="messages.length === 0 && !sending">
         <view class="welcome-card" v-if="messages.length === 0 && !sending">
           <view class="welcome-icon">{{ mascotIcon || (isNutrition ? '🥗' : '🤖') }}</view>
           <view class="welcome-icon">{{ mascotIcon || (isNutrition ? '🥗' : '🤖') }}</view>
           <text class="welcome-title">{{ isNutrition ? '你好!我是精准营养助手' : ('你好!我是' + (mascotName || '家庭助手')) }}</text>
           <text class="welcome-title">{{ isNutrition ? '你好!我是精准营养助手' : ('你好!我是' + (mascotName || '家庭助手')) }}</text>
-          <text class="welcome-desc">{{ isNutrition ? '我可以基于菌群检测报告,为你提供个性化的营养建议和推荐' : '我可以帮你了解孩子的成长情况,比如:' }}</text>
+          <text class="welcome-desc">{{ welcomeSubtitle }}</text>
           <view class="welcome-suggestions">
           <view class="welcome-suggestions">
             <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('小明最近任务完成怎么样?')">
             <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('小明最近任务完成怎么样?')">
               <text class="suggestion-icon">📋</text>
               <text class="suggestion-icon">📋</text>
@@ -138,6 +138,7 @@ export default {
       reportId: null,
       reportId: null,
       surveyId: null,
       surveyId: null,
       pendingFirstMessage: '',
       pendingFirstMessage: '',
+      fromPage: '',
       mode: '',
       mode: '',
       recommendations: [],
       recommendations: [],
       mascotCode: '',
       mascotCode: '',
@@ -148,6 +149,28 @@ export default {
   computed: {
   computed: {
     isNutrition: function() {
     isNutrition: function() {
       return this.mode === 'nutrition'
       return this.mode === 'nutrition'
+    },
+    welcomeSubtitle: function() {
+      if (this.isNutrition) {
+        return '我可以基于菌群检测报告,为你提供个性化的营养建议和推荐'
+      }
+      if (this.fromPage) {
+        var pageNames = {
+          'pages/body/index': '身',
+          'pages/wisdom/index': '智',
+          'pages/mind/index': '心',
+          'pages/index/index': '首页',
+          'pages/index/parent-index': '首页',
+          'pages/index/child-index': '首页',
+          'pages/profile/profile': '我的',
+          'pages/profile/children': '我的',
+          'pages/shop/index': '商城',
+          'pages/discover/index': '发现'
+        }
+        var pageName = pageNames[this.fromPage] || this.fromPage
+        return '从 ' + pageName + ' 发起,我可以帮你了解:'
+      }
+      return '我可以帮你了解孩子的成长情况,比如:'
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {

+ 4 - 0
cfc-frontend/pages/profile/profile.vue

@@ -129,6 +129,10 @@ export default {
           if (res.data.code === 200 || res.data.code === 0) {
           if (res.data.code === 200 || res.data.code === 0) {
             this.selectedMascot = mascot;
             this.selectedMascot = mascot;
             this.currentMascotName = mascot === 'xibao' ? '浠宝' : '福宝';
             this.currentMascotName = mascot === 'xibao' ? '浠宝' : '福宝';
+            // 同步更新 localStorage 以便 AIFloatingAvatar 等组件能读到最新值
+            var userInfo = uni.getStorageSync('userInfo') || {};
+            userInfo.mascot = mascot;
+            uni.setStorageSync('userInfo', userInfo);
             uni.showToast({ title: '已更换' });
             uni.showToast({ title: '已更换' });
           }
           }
         }
         }