Browse Source

chore: auto bump version and changelog [skip ci]

iwt 1 month ago
parent
commit
e77d59f7a1

+ 24 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -5667,7 +5667,30 @@ try {
     } catch (Exception e) {
         log.warn("创建promotion_binding表失败: {}", e.getMessage());
     }
-}
+
+    // 迁移115: 创建 activity_feedback 表(活动评价功能)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_feedback (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
+            "activity_id BIGINT NOT NULL COMMENT '活动ID', " +
+            "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+            "child_id BIGINT COMMENT '孩子ID', " +
+            "ratings TEXT COMMENT '评分JSON: {\"组织形式\":5,\"会场气氛\":4}', " +
+            "comment TEXT COMMENT '文字评价', " +
+            "status TINYINT DEFAULT 0 COMMENT '状态: 0=待审核 1=已发布', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+            "INDEX idx_activity_id (activity_id), " +
+            "INDEX idx_user_id (user_id), " +
+            "INDEX idx_child_id (child_id)" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表'");
+        log.info("已创建activity_feedback表");
+    } catch (Exception e) {
+        // 表已存在,忽略
+    }
+
+    // 迁移115b: activity表添加feedback_dimensions字段
+    ensureColumn("activities", "feedback_dimensions", "TEXT COMMENT '评价维度列表JSON: [\"组织形式\",\"会场气氛\",\"主持人\",\"专家\"]'");
+    }
 
     private void runMigration82() {
         try {

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityFeedbackController.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ActivityFeedbackService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/activity/feedback")
+public class ActivityFeedbackController {
+    @Resource
+    private ActivityFeedbackService activityFeedbackService;
+
+    @PostMapping("/submit")
+    public Result<?> submit(@RequestBody Map<String, Object> params,
+                            @RequestAttribute("userId") Long userId) {
+        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        String ratings = (String) params.get("ratings");
+        String comment = (String) params.get("comment");
+        if (activityId == null) return Result.error("活动ID不能为空");
+        return activityFeedbackService.submit(activityId, userId, childId, ratings, comment);
+    }
+
+    @PostMapping("/list")
+    public Result<?> list(@RequestBody Map<String, Object> params) {
+        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        if (activityId == null) return Result.error("活动ID不能为空");
+        return activityFeedbackService.getFeedbackList(activityId);
+    }
+
+    @PostMapping("/my")
+    public Result<?> my(@RequestBody Map<String, Object> params,
+                        @RequestAttribute("userId") Long userId) {
+        Long activityId = params.get("activityId") != null ? Long.valueOf(params.get("activityId").toString()) : null;
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        if (activityId == null) return Result.error("活动ID不能为空");
+        return activityFeedbackService.getMyFeedback(activityId, childId != null ? childId : null);
+    }
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -82,7 +82,13 @@ public class Activity implements Serializable {
 
     private String vendorName;
 
+    /** 评价维度列表JSON(如 ["组织形式","会场气氛","主持人","专家"]) */
+    private String feedbackDimensions;
+
     private Date createdAt;
 
     private Date updatedAt;
+
+    public String getFeedbackDimensions() { return feedbackDimensions; }
+    public void setFeedbackDimensions(String feedbackDimensions) { this.feedbackDimensions = feedbackDimensions; }
 }

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityFeedback.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.IdType;
+import java.util.Date;
+
+@TableName("activity_feedback")
+public class ActivityFeedback {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long activityId;
+    private Long userId;
+    private Long childId;
+    private String ratings;
+    private String comment;
+    private Integer status;
+    private Date createdAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getActivityId() { return activityId; }
+    public void setActivityId(Long activityId) { this.activityId = activityId; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public Long getChildId() { return childId; }
+    public void setChildId(Long childId) { this.childId = childId; }
+    public String getRatings() { return ratings; }
+    public void setRatings(String ratings) { this.ratings = ratings; }
+    public String getComment() { return comment; }
+    public void setComment(String comment) { this.comment = comment; }
+    public Integer getStatus() { return status; }
+    public void setStatus(Integer status) { this.status = status; }
+    public Date getCreatedAt() { return createdAt; }
+    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ActivityFeedbackMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ActivityFeedback;
+
+public interface ActivityFeedbackMapper extends BaseMapper<ActivityFeedback> {
+}

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityFeedbackService.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ActivityFeedback;
+import com.etotem.cfc.mapper.ActivityFeedbackMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ActivityFeedbackService {
+
+    @Resource
+    private ActivityFeedbackMapper activityFeedbackMapper;
+
+    public Result<?> submit(Long activityId, Long userId, Long childId, String ratings, String comment) {
+        ActivityFeedback feedback = new ActivityFeedback();
+        feedback.setActivityId(activityId);
+        feedback.setUserId(userId);
+        feedback.setChildId(childId);
+        feedback.setRatings(ratings);
+        feedback.setComment(comment);
+        feedback.setStatus(0);
+        feedback.setCreatedAt(new Date());
+        activityFeedbackMapper.insert(feedback);
+        return Result.success("提交评价成功", feedback.getId());
+    }
+
+    public Result<?> getFeedbackList(Long activityId) {
+        QueryWrapper<ActivityFeedback> wrapper = new QueryWrapper<>();
+        wrapper.eq("activity_id", activityId)
+               .orderByDesc("created_at");
+        List<ActivityFeedback> list = activityFeedbackMapper.selectList(wrapper);
+        return Result.success(list);
+    }
+
+    public Result<?> getMyFeedback(Long activityId, Long childId) {
+        QueryWrapper<ActivityFeedback> wrapper = new QueryWrapper<>();
+        wrapper.eq("activity_id", activityId);
+        if (childId != null) {
+            wrapper.eq("child_id", childId);
+        }
+        wrapper.orderByDesc("created_at").last("LIMIT 1");
+        ActivityFeedback feedback = activityFeedbackMapper.selectOne(wrapper);
+        return Result.success(feedback);
+    }
+}

+ 14 - 0
cfc-backend/src/main/resources/mapper/ActivityFeedbackMapper.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.etotem.cfc.mapper.ActivityFeedbackMapper">
+    <resultMap id="baseMap" type="com.etotem.cfc.entity.ActivityFeedback">
+        <id column="id" property="id"/>
+        <result column="activity_id" property="activityId"/>
+        <result column="user_id" property="userId"/>
+        <result column="child_id" property="childId"/>
+        <result column="ratings" property="ratings"/>
+        <result column="comment" property="comment"/>
+        <result column="status" property="status"/>
+        <result column="created_at" property="createdAt"/>
+    </resultMap>
+</mapper>

+ 16 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1724,6 +1724,7 @@ CREATE TABLE IF NOT EXISTS activities (
     price INT DEFAULT 0 COMMENT '活动价格(分)',
     vendor_id        BIGINT COMMENT '服务商ID',
     vendor_name      VARCHAR(100) COMMENT '服务商名称',
+    feedback_dimensions TEXT COMMENT '评价维度列表JSON',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_dimension (dimension_code),
@@ -3175,3 +3176,18 @@ CREATE TABLE IF NOT EXISTS health_data_sources (
     INDEX idx_member_source (member_id, source_type),
     INDEX idx_member_id (member_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录';
+
+-- 活动评价表
+CREATE TABLE IF NOT EXISTS activity_feedback (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    activity_id BIGINT NOT NULL COMMENT '活动ID',
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    child_id BIGINT COMMENT '孩子ID',
+    ratings TEXT COMMENT '评分JSON: {"组织形式":5,"会场气氛":4}',
+    comment TEXT COMMENT '文字评价',
+    status TINYINT DEFAULT 0 COMMENT '状态: 0=待审核 1=已发布',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_activity_id (activity_id),
+    INDEX idx_user_id (user_id),
+    INDEX idx_child_id (child_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表';

+ 222 - 3
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -38,6 +38,10 @@
             <text class="meta-icon">🕐</text>
             <text class="meta-text">{{ formatDate(activity.startTime) }}{{ activity.endTime ? ' ~ ' + formatDate(activity.endTime) : '' }}</text>
           </view>
+          <view class="meta-item" v-if="activity.endTime && activity.requireRegistration === 1">
+            <text class="meta-icon">⏰</text>
+            <text class="meta-text">报名截止: {{ formatDate(activity.endTime) }}</text>
+          </view>
           <view class="meta-item" v-if="activity.location">
             <text class="meta-icon">📍</text>
             <text class="meta-text">{{ activity.location }}</text>
@@ -48,7 +52,10 @@
           </view>
           <view class="meta-item">
             <text class="meta-icon">💰</text>
-            <text class="meta-text">{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
+            <view class="meta-text" v-if="activity.memberPrice && activity.memberPrice > 0 && activity.memberPrice < activity.price">
+              <text>会员价 ¥{{ activity.memberPrice }} 普通价 ¥{{ activity.price }}</text>
+            </view>
+            <text class="meta-text" v-else>{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
           </view>
         </view>
 
@@ -113,9 +120,11 @@
             @click="onCheckin"
           >
             <text v-if="checkinLoading">签到中...</text>
+            <text v-else-if="checkinDone && activity.requireCheckinReview === 1">已签到待审核</text>
             <text v-else-if="checkinDone">已签到 ✓</text>
             <text v-else>签到</text>
           </button>
+          <text v-if="activity.requireCheckinReview === 1 && !checkinDone" class="checkin-review-hint">签到需要管理员审核</text>
         </view>
       </view>
       
@@ -127,6 +136,55 @@
           <button class="penalty-btn" @click="showPenaltyModal = false">我知道了</button>
         </view>
       </modal>
+
+      <!-- 活动评价 -->
+      <view class="feedback-section" v-if="activity.status === 'ended' && checkinDone">
+        <view class="feedback-header">
+          <text class="feedback-title">活动评价</text>
+        </view>
+        <!-- Already submitted feedback -->
+        <view v-if="submittedFeedback" class="feedback-submitted">
+          <view class="feedback-ratings-display">
+            <view class="fr-row" v-for="(val, key) in parsedSubmittedRatings" :key="key">
+              <text class="fr-label">{{ key }}</text>
+              <text class="fr-stars">{{ getStarDisplay(val) }}</text>
+            </view>
+          </view>
+          <text class="feedback-comment-text" v-if="submittedFeedback.comment">{{ submittedFeedback.comment }}</text>
+          <text class="feedback-thanks">感谢您的评价!</text>
+        </view>
+        <!-- Submit feedback button -->
+        <button v-else class="feedback-btn" @click="openFeedbackModal">
+          <text>提交活动评价</text>
+        </button>
+      </view>
+
+      <!-- 评价弹窗 -->
+      <modal v-if="showFeedbackModal" class="feedback-modal-overlay" :show="showFeedbackModal" @close="showFeedbackModal = false">
+        <view class="feedback-modal">
+          <text class="feedback-modal-title">活动评价</text>
+
+          <view class="rating-row" v-for="(dim, dimIdx) in feedbackDimensions" :key="dimIdx">
+            <text class="rating-label">{{ dim }}</text>
+            <view class="stars">
+              <text v-for="star in 5" :key="star"
+                class="star-icon"
+                :class="{'star-active': star <= (feedbackRatings[dim] || 0)}"
+                @click="feedbackRatings[dim] = star">★</text>
+            </view>
+          </view>
+
+          <textarea class="feedback-textarea" v-model="feedbackComment" placeholder="说说您的体验..." maxlength="500" />
+
+          <view class="feedback-modal-actions">
+            <button class="feedback-cancel-btn" @click="showFeedbackModal = false">取消</button>
+            <button class="feedback-submit-btn" :disabled="feedbackSubmitting" @click="submitFeedback">
+              <text v-if="feedbackSubmitting">提交中...</text>
+              <text v-else>提交评价</text>
+            </button>
+          </view>
+        </view>
+      </modal>
     </scroll-view>
 
     <!-- 空状态 -->
@@ -137,7 +195,7 @@
 </template>
 
 <script>
-import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations } from '@/utils/api.js'
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback } from '@/utils/api.js'
 
 export default {
   data() {
@@ -154,7 +212,14 @@ export default {
       registrationLoading: false,
       paying: false,
       showPenaltyModal: false,
-      penaltyAmount: 0
+      penaltyAmount: 0,
+      showFeedbackModal: false,
+      feedbackDimensions: [],
+      feedbackRatings: {},
+      feedbackComment: '',
+      submittedFeedback: null,
+      feedbackSubmitting: false,
+      feedbackList: []
     }
   },
   onLoad: function(options) {
@@ -196,6 +261,8 @@ export default {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           self.activity = res.data
+          self.parseFeedbackDimensions()
+          self.loadFeedbackStatus()
           // Load registration status if logged in and activity requires registration
           if (self.activity.requireRegistration === 1) {
             var childId = uni.getStorageSync('currentChildId')
@@ -241,6 +308,7 @@ export default {
       activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
         self.checkinLoading = false
         if (res && res.code === 200 && res.data) {
+          self.loadFeedbackStatus()
           // 检查是否有能量扣减
           if (res.data.energyPenalty) {
             self.penaltyAmount = res.data.energyPenalty
@@ -343,6 +411,89 @@ export default {
           }
         }
       })
+    },
+    // ===== 活动评价 =====
+    parseFeedbackDimensions: function() {
+      var dims = ['组织形式', '会场气氛', '主持人', '专家']
+      if (this.activity.feedbackDimensions) {
+        try {
+          var parsed = JSON.parse(this.activity.feedbackDimensions)
+          if (parsed && parsed.length > 0) {
+            dims = parsed
+          }
+        } catch(e) {}
+      }
+      this.feedbackDimensions = dims
+      this.initRatings()
+    },
+    initRatings: function() {
+      var r = {}
+      for (var i = 0; i < this.feedbackDimensions.length; i++) {
+        r[this.feedbackDimensions[i]] = 0
+      }
+      this.feedbackRatings = r
+    },
+    loadFeedbackStatus: function() {
+      var self = this
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId || !self.activity || !self.activity.id) return
+
+      getMyActivityFeedback({ activityId: self.activity.id, childId: childId }).then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          self.submittedFeedback = res.data
+        }
+      }).catch(function() {})
+    },
+    openFeedbackModal: function() {
+      this.showFeedbackModal = true
+    },
+    submitFeedback: function() {
+      var self = this
+      // Validate at least one rating
+      var hasRating = false
+      for (var key in self.feedbackRatings) {
+        if (self.feedbackRatings[key] > 0) { hasRating = true; break }
+      }
+      if (!hasRating) {
+        uni.showToast({ title: '请至少评价一项', icon: 'none' })
+        return
+      }
+
+      self.feedbackSubmitting = true
+      var childId = uni.getStorageSync('currentChildId')
+      submitActivityFeedback({
+        activityId: self.activity.id,
+        childId: childId,
+        ratings: JSON.stringify(self.feedbackRatings),
+        comment: self.feedbackComment
+      }).then(function(res) {
+        self.feedbackSubmitting = false
+        if (res && res.code === 200) {
+          self.showFeedbackModal = false
+          self.loadFeedbackStatus()
+          uni.showToast({ title: '评价成功', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.feedbackSubmitting = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    getStarDisplay: function(val) {
+      var full = parseInt(val) || 0
+      var empty = 5 - full
+      return '★'.repeat(full) + '☆'.repeat(empty)
+    }
+  },
+  computed: {
+    parsedSubmittedRatings: function() {
+      if (!this.submittedFeedback || !this.submittedFeedback.ratings) return {}
+      try {
+        return JSON.parse(this.submittedFeedback.ratings)
+      } catch(e) {
+        return {}
+      }
     }
   }
 }
@@ -645,4 +796,72 @@ export default {
   padding: 16rpx 60rpx;
   font-size: 28rpx;
 }
+
+/* 活动评价 */
+.feedback-section {
+  padding: 30rpx;
+  margin: 20rpx;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.feedback-header { margin-bottom: 20rpx; }
+.feedback-title { font-size: 32rpx; font-weight: bold; color: #333; }
+.feedback-btn {
+  background: #F97316; color: #fff; border: none;
+  border-radius: 40rpx; padding: 20rpx 40rpx; font-size: 28rpx;
+  text-align: center;
+}
+.feedback-submitted { padding: 10rpx 0; }
+.feedback-ratings-display { margin-bottom: 16rpx; }
+.fr-row { display: flex; justify-content: space-between; padding: 8rpx 0; }
+.fr-label { font-size: 28rpx; color: #333; }
+.fr-stars { font-size: 28rpx; color: #F97316; }
+.feedback-comment-text { font-size: 26rpx; color: #666; margin: 10rpx 0; line-height: 1.5; }
+.feedback-thanks { font-size: 24rpx; color: #999; text-align: center; margin-top: 16rpx; }
+.feedback-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;
+}
+.feedback-modal {
+  background: #fff; border-radius: 20rpx; padding: 40rpx;
+  width: 80%; max-width: 600rpx;
+}
+.feedback-modal-title {
+  font-size: 34rpx; font-weight: bold; color: #333;
+  text-align: center; margin-bottom: 30rpx;
+}
+.rating-row {
+  display: flex; justify-content: space-between; align-items: center;
+  padding: 16rpx 0;
+}
+.rating-label { font-size: 28rpx; color: #333; min-width: 120rpx; }
+.stars { display: flex; }
+.star-icon { font-size: 40rpx; color: #ddd; padding: 0 6rpx; }
+.star-active { color: #F97316; }
+.feedback-textarea {
+  width: 100%; height: 160rpx; margin-top: 20rpx;
+  border: 1rpx solid #eee; border-radius: 12rpx;
+  padding: 16rpx; font-size: 26rpx; box-sizing: border-box;
+}
+.feedback-modal-actions {
+  display: flex; justify-content: space-between; margin-top: 30rpx;
+}
+.feedback-cancel-btn {
+  flex: 1; background: #f5f5f5; color: #666; border: none;
+  border-radius: 40rpx; padding: 20rpx; font-size: 28rpx;
+  text-align: center; margin-right: 20rpx;
+}
+.feedback-submit-btn {
+  flex: 1; background: #F97316; color: #fff; border: none;
+  border-radius: 40rpx; padding: 20rpx; font-size: 28rpx;
+  text-align: center;
+}
 </style>

+ 10 - 1
cfc-frontend/pages/discover/activity-detail/activity-detail.vue

@@ -38,6 +38,10 @@
             <text class="meta-icon">🕐</text>
             <text class="meta-text">{{ formatDate(activity.startTime) }}{{ activity.endTime ? ' ~ ' + formatDate(activity.endTime) : '' }}</text>
           </view>
+          <view class="meta-item" v-if="activity.endTime && activity.requireRegistration === 1">
+            <text class="meta-icon">⏰</text>
+            <text class="meta-text">报名截止: {{ formatDate(activity.endTime) }}</text>
+          </view>
           <view class="meta-item" v-if="activity.location">
             <text class="meta-icon">📍</text>
             <text class="meta-text">{{ activity.location }}</text>
@@ -48,7 +52,10 @@
           </view>
           <view class="meta-item">
             <text class="meta-icon">💰</text>
-            <text class="meta-text">{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
+            <view class="meta-text" v-if="activity.memberPrice && activity.memberPrice > 0 && activity.memberPrice < activity.price">
+              <text>会员价 ¥{{ activity.memberPrice }} 普通价 ¥{{ activity.price }}</text>
+            </view>
+            <text class="meta-text" v-else>{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
           </view>
         </view>
 
@@ -111,9 +118,11 @@
               @click="onCheckin"
             >
               <text v-if="checkinLoading">签到中...</text>
+              <text v-else-if="checkinDone && activity.requireCheckinReview === 1">已签到待审核</text>
               <text v-else-if="checkinDone">已签到 ✓</text>
               <text v-else>签到</text>
             </button>
+            <text v-if="activity.requireCheckinReview === 1 && !checkinDone" class="checkin-review-hint">签到需要管理员审核</text>
           </template>
 
           <!-- 已签到(无报名流程的活动) -->

+ 3 - 0
cfc-frontend/utils/api.js

@@ -1718,6 +1718,9 @@ export const activityCheckin = (data) => request('/api/activity/checkin', 'POST'
 export const registerActivity = (data) => request('/api/activity/register', 'POST', data)
 export const cancelActivityRegistration = (data) => request('/api/activity/cancel-registration', 'POST', data)
 export const getMyActivityRegistrations = (data) => request('/api/activity/my-registrations', 'POST', data)
+export const submitActivityFeedback = (data) => request('/api/activity/feedback/submit', 'POST', data)
+export const getActivityFeedbackList = (data) => request('/api/activity/feedback/list', 'POST', data)
+export const getMyActivityFeedback = (data) => request('/api/activity/feedback/my', 'POST', data)
 
 // ===== 活动管理(管理员端,复用 /api/activity/*) =====
 export const adminCreateActivity = (activity) => request('/api/activity/create', 'POST', activity)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-5a201b5113fa675551eeabe0b5733232d2be8ea4
+6b334d34fe1526c3fec629e67d9b167cfb99b2dd

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.556",
+  "version": "1.0.557",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 13 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,19 @@
 
 ---
 
+## v1.0.557 (2026-07-26)
+
+### 新功能
+- 家庭关系图视觉重编码 (信任/沟通/亲密度) + 身份形状 + 自导航维护页
+
+### 其他
+- - 节点形状按身份区分 (圆角矩形/圆形/菱形)
+- - 点自己跳转维护页 (原为添加成员)
+- - 触摸自身显示⚙提示
+- - child-index: 新增'自己出题'快速入口
+- 
+
+
 ## v1.0.556 (2026-07-26)
 
 ### 新功能

+ 14 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.556
+> 当前版本: v1.0.557
 
 ## 历史版本
 
@@ -8,6 +8,19 @@
 
 ---
 
+## v1.0.557 (2026-07-26)
+
+### 新功能
+- 家庭关系图视觉重编码 (信任/沟通/亲密度) + 身份形状 + 自导航维护页
+
+### 其他
+- - 节点形状按身份区分 (圆角矩形/圆形/菱形)
+- - 点自己跳转维护页 (原为添加成员)
+- - 触摸自身显示⚙提示
+- - child-index: 新增'自己出题'快速入口
+- 
+
+
 ## v1.0.556 (2026-07-26)
 
 ### 新功能