Explorar o código

chore: merge origin/cfclub into cfclub (集成远端版本构建)

# Conflicts:
#	cfc-web/.last_build_commit
#	cfc-web/package.json
#	cfc-web/public/CHANGELOG-v1.0.md
#	cfc-web/public/CHANGELOG.md
iwt hai 3 semanas
pai
achega
73517f963b
Modificáronse 63 ficheiros con 2381 adicións e 573 borrados
  1. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java
  3. 12 2
      cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java
  4. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java
  5. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityFeedback.java
  6. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java
  7. 56 9
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityFeedbackService.java
  8. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityOrderService.java
  9. 120 10
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java
  10. 96 27
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java
  11. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ContentRecommendService.java
  12. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java
  13. 14 1
      cfc-backend/src/main/resources/schema.sql
  14. 2 2
      cfc-frontend/pages/activity/components/ActivityPoster.vue
  15. 1 1
      cfc-web/.last_build_commit
  16. 1 1
      cfc-web/package.json
  17. 65 0
      cfc-web/public/CHANGELOG-v1.0.md
  18. 66 1
      cfc-web/public/CHANGELOG.md
  19. 22 12
      cfc-web/src/views/Families.vue
  20. 1 0
      cfc-web/src/views/Layout.vue
  21. 20 8
      cfc-web/src/views/Rewards.vue
  22. 19 8
      cfc-web/src/views/TaskTemplates.vue
  23. 19 10
      cfc-web/src/views/TeacherDashboard.vue
  24. 18 10
      cfc-web/src/views/Users.vue
  25. 18 11
      cfc-web/src/views/Wishes.vue
  26. 20 10
      cfc-web/src/views/admin/Activities.vue
  27. 31 18
      cfc-web/src/views/admin/ActivityEdit.vue
  28. 16 11
      cfc-web/src/views/admin/BadgeManage.vue
  29. 23 17
      cfc-web/src/views/admin/CouponManagement.vue
  30. 8 3
      cfc-web/src/views/admin/DanExecution.vue
  31. 10 5
      cfc-web/src/views/admin/DanSettlement.vue
  32. 12 9
      cfc-web/src/views/admin/DiscountRuleManagement.vue
  33. 17 8
      cfc-web/src/views/admin/EnergyBehaviorConfig.vue
  34. 35 18
      cfc-web/src/views/admin/FileManage.vue
  35. 24 21
      cfc-web/src/views/admin/Foods.vue
  36. 17 10
      cfc-web/src/views/admin/GrowthTaskManagement.vue
  37. 14 8
      cfc-web/src/views/admin/GuidePopupConfig.vue
  38. 34 19
      cfc-web/src/views/admin/HomeBannerManage.vue
  39. 17 9
      cfc-web/src/views/admin/InventoryInboundCreate.vue
  40. 24 13
      cfc-web/src/views/admin/InventoryManage.vue
  41. 46 29
      cfc-web/src/views/admin/MembershipCenter.vue
  42. 30 15
      cfc-web/src/views/admin/NutritionProducts.vue
  43. 26 12
      cfc-web/src/views/admin/ProblemDomainManage.vue
  44. 102 46
      cfc-web/src/views/admin/ProductEdit.vue
  45. 17 10
      cfc-web/src/views/admin/ProductPpointManage.vue
  46. 34 24
      cfc-web/src/views/admin/ProfileManagement.vue
  47. 14 8
      cfc-web/src/views/admin/PromotionManagement.vue
  48. 18 10
      cfc-web/src/views/admin/ProxyReportUpload.vue
  49. 11 8
      cfc-web/src/views/admin/RecommendationRules.vue
  50. 14 9
      cfc-web/src/views/admin/ReportTemplateManage.vue
  51. 16 9
      cfc-web/src/views/admin/SubscriptionManagement.vue
  52. 26 12
      cfc-web/src/views/admin/SurveyTemplates.vue
  53. 21 12
      cfc-web/src/views/admin/ZodiacConfigs.vue
  54. 17 9
      cfc-web/src/views/admin/components/ProductSkuEditor.vue
  55. 23 11
      cfc-web/src/views/admin/data-source-config.vue
  56. 30 17
      cfc-web/src/views/admin/dimension-config.vue
  57. 22 12
      cfc-web/src/views/admin/energy-config/WuxingConfig.vue
  58. 18 9
      cfc-web/src/views/admin/gates.vue
  59. 23 11
      cfc-web/src/views/teacher/HealthPlanReview.vue
  60. 13 7
      cfc-web/src/views/teacher/TeacherTeam.vue
  61. 406 0
      docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md
  62. 214 0
      docs/整改方案-活动管理差异修复-2026-10-22.md
  63. 317 0
      docs/调研报告-活动管理实现vs需求-2026-10-22.html

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2210,6 +2210,20 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         ensureColumn("activities", "require_registration", "TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与'");
         ensureColumn("activities", "require_checkin_review", "TINYINT DEFAULT 1 COMMENT '打卡是否需要审核: 1=需要, 0=直接通过'");
         ensureColumn("activities", "energy_penalty_unregistered", "INT DEFAULT 5 COMMENT '未报名打卡的行能量扣减值'");
+        // 2026-10-22 整改:报名审核开关 + 名额占用计数
+        ensureColumn("activities", "require_review", "TINYINT DEFAULT 0 COMMENT '报名是否需要审核: 1=需要, 0=免审即报名通过'");
+        ensureColumn("activities", "approved_count", "INT DEFAULT 0 COMMENT '已通过报名人数(名额占用统计)'");
+        ensureColumn("activities", "signed_count", "INT DEFAULT 0 COMMENT '签到人数(checkin 时 +1,区别于 approved_count 的报名口径)'");
+        // 修正 require_checkin_review 默认值并归一化存量(该字段此前未被读取,无业务依赖,避免存量活动突然进入待审核)
+        // 同时按已通过报名回填 approved_count,使名额统计与存量数据一致
+        try {
+            jdbcTemplate.execute("ALTER TABLE activities MODIFY COLUMN require_checkin_review TINYINT DEFAULT 0 COMMENT '打卡是否需要审核: 1=需要, 0=免审直接通过'");
+            jdbcTemplate.execute("UPDATE activities SET require_checkin_review = 0 WHERE require_checkin_review = 1");
+            jdbcTemplate.execute("UPDATE activities a SET a.approved_count = (SELECT cnt FROM (SELECT COUNT(*) AS cnt FROM activity_registrations r WHERE r.activity_id = a.id AND r.status = 'approved') t)");
+            log.info("已修正 require_checkin_review 默认值并回填 approved_count");
+        } catch (Exception e) {
+            log.warn("修正 activities 报名/签到审核字段失败: {}", e.getMessage());
+        }
 
         // family_members 表添加关系评分字段
         ensureColumn("family_members", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度 0-100'");
@@ -3152,6 +3166,19 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移390: product_orders 添加 special_deal_id 列(限时特价订单关联)
         ensureColumn("product_orders", "special_deal_id", "BIGINT NULL COMMENT '限时特价活动ID(非空=特价单)'");
+
+        // 迁移391: activity_registrations 添加取消时间/原因列(P0-2 取消报名支持 approved + 退款链路,取消时记录)
+        ensureColumn("activity_registrations", "cancelled_at", "DATETIME DEFAULT NULL COMMENT '取消报名时间'");
+        ensureColumn("activity_registrations", "cancel_reason", "VARCHAR(200) DEFAULT NULL COMMENT '取消原因'");
+
+        // 迁移392: activity_feedback 添加 updated_at 列 + activity_id+child_id 唯一索引(P1-3 评价去重:同一孩子只能评一次,重复提交改为更新)
+        try {
+            ensureColumn("activity_feedback", "updated_at", "DATETIME DEFAULT NULL COMMENT '更新时间(重复提交评价时更新)'");
+            jdbcTemplate.execute("ALTER TABLE activity_feedback ADD UNIQUE KEY uk_activity_child (activity_id, child_id)");
+            log.info("迁移392: 已为 activity_feedback 添加 updated_at 列和 uk_activity_child 唯一索引");
+        } catch (Exception e) {
+            log.warn("迁移392: 添加 activity_feedback.updated_at / uk_activity_child 失败(可能已存在): {}", e.getMessage());
+        }
     }
 
     private void migrateHealthDimensionsTable() {

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java

@@ -173,4 +173,19 @@ public class ActivityController {
         if (checkinResult.containsKey("error")) return Result.error(checkinResult.get("error").toString());
         return Result.success(checkinResult);
     }
+
+    @PostMapping("/checkin-review")
+    public Result<Map<String, Object>> checkinReview(@RequestBody Map<String, Object> params,
+                                                    @RequestAttribute("role") String role,
+                                                    @RequestAttribute(value = "roles", required = false) List<String> roles,
+                                                    @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (!hasActivityManageRole(role, roles)) {
+            return Result.error("只有管理员或活动管理员可以审核签到");
+        }
+        Long taskId = ParamUtils.getLong(params.get("taskId"));
+        if (taskId == null) return Result.error("taskId不能为空");
+        String action = (String) params.get("action");
+        boolean approve = "approve".equals(action) || "pass".equals(action);
+        return activityService.reviewCheckin(taskId, userId, approve);
+    }
 }

+ 12 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -23,6 +23,10 @@ public class ActivityDTO {
     private String location;
     private Integer maxParticipants;
     private Integer currentParticipants;
+    /** 已通过报名人数(名额占用统计,区别于 currentParticipants 的签到计数) */
+    private Integer approvedCount;
+    /** 签到人数(checkin 时 +1) */
+    private Integer signedCount;
     private Integer price;
     private String vendorName;
     private String priceLabel;
@@ -33,6 +37,9 @@ public class ActivityDTO {
     /** 是否需要审核签到(0=否,1=是) */
     private Integer requireCheckinReview;
 
+    /** 报名是否需要审核(0=否,1=是) */
+    private Integer requireReview;
+
     /** 会员价(分) */
     private Integer memberPrice;
 
@@ -54,10 +61,10 @@ public class ActivityDTO {
     /** 创建人userId */
     private Long creatorId;
 
-    /** 展示状态: open/registered/ongoing/checked_in/summarizing/ended/pending_start */
+    /** 展示状态: open/registered/ongoing/checked_in/ended/pending_start */
     private String displayStatus;
 
-    /** 展示状态文案: 报名中/已报名/进行中/已签到/总结中/已结束/待开始 */
+    /** 展示状态文案: 报名中/已报名/进行中/已签到/已结束/待开始 */
     private String displayLabel;
 
     /** 报名状态: not_registered/registered/pending/rejected/cancelled/checked_in */
@@ -94,9 +101,12 @@ public class ActivityDTO {
         dto.setLocation(activity.getLocation());
         dto.setMaxParticipants(activity.getMaxParticipants());
         dto.setCurrentParticipants(activity.getCurrentParticipants());
+        dto.setApprovedCount(activity.getApprovedCount());
+        dto.setSignedCount(activity.getSignedCount());
         dto.setVendorName(activity.getVendorName());
         dto.setRequireRegistration(activity.getRequireRegistration());
         dto.setRequireCheckinReview(activity.getRequireCheckinReview());
+        dto.setRequireReview(activity.getRequireReview());
         dto.setMemberPrice(activity.getMemberPrice());
         dto.setRegistrationDeadline(activity.getRegistrationDeadline() != null ? activity.getRegistrationDeadline().toString() : null);
         dto.setChildFee(activity.getChildFee());

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

@@ -58,6 +58,12 @@ public class Activity implements Serializable {
 
     private Integer currentParticipants;
 
+    /** 已通过报名人数(名额占用统计) */
+    private Integer approvedCount;
+
+    /** 签到人数(checkin 时 +1,区别于 approvedCount 的报名口径) */
+    private Integer signedCount;
+
     private Integer price;
 
     /** 会员价(分) */
@@ -96,6 +102,9 @@ public class Activity implements Serializable {
     /** 是否需要审核签到(0=否,1=是) */
     private Integer requireCheckinReview;
 
+    /** 报名是否需要审核(0=否,1=是);默认 0,免审即报名通过并占用名额 */
+    private Integer requireReview;
+
     /** 未报名签到扣减能量值(正数) */
     private Integer energyPenaltyUnregistered;
 

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

@@ -16,6 +16,7 @@ public class ActivityFeedback {
     private String comment;
     private Integer status;
     private Date createdAt;
+    private Date updatedAt;
 
     public Long getId() { return id; }
     public void setId(Long id) { this.id = id; }
@@ -33,4 +34,6 @@ public class ActivityFeedback {
     public void setStatus(Integer status) { this.status = status; }
     public Date getCreatedAt() { return createdAt; }
     public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+    public Date getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityRegistration.java

@@ -21,6 +21,10 @@ public class ActivityRegistration implements Serializable {
     private String registrantPhone;
     private String status;
     private String rejectReason;
+    /** 取消时间(取消报名时记录) */
+    private LocalDateTime cancelledAt;
+    /** 取消原因 */
+    private String cancelReason;
     private LocalDateTime registeredAt;
     private LocalDateTime approvedAt;
     private Long approvedBy;

+ 56 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityFeedbackService.java

@@ -1,12 +1,17 @@
 package com.etotem.cfc.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Activity;
 import com.etotem.cfc.entity.ActivityFeedback;
 import com.etotem.cfc.mapper.ActivityFeedbackMapper;
+import com.etotem.cfc.mapper.TaskMapper;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
@@ -16,17 +21,59 @@ public class ActivityFeedbackService {
 
     @Resource
     private ActivityFeedbackMapper activityFeedbackMapper;
+    @Resource
+    private TaskMapper taskMapper;
+    @Resource
+    private ActivityRegistrationService registrationService;
+    @Resource
+    private ActivityService activityService;
 
     public Result<?> submit(Long activityId, Long userId, Long memberId, String ratings, String comment) {
-        ActivityFeedback feedback = new ActivityFeedback();
-        feedback.setActivityId(activityId);
-        feedback.setUserId(userId);
-        feedback.setChildId(memberId);
-        feedback.setRatings(ratings);
-        feedback.setComment(comment);
-        feedback.setStatus(0);
-        feedback.setCreatedAt(new Date());
-        activityFeedbackMapper.insert(feedback);
+        // Gate: 活动必须已结束(status=ended 或 endTime < now)
+        Activity activity = activityService.getById(activityId);
+        if (activity == null) return Result.error("活动不存在");
+        if (!"ended".equals(activity.getStatus())) {
+            Date now = new Date();
+            if (activity.getEndTime() != null && !now.after(activity.getEndTime())) {
+                return Result.error("活动尚未结束,暂不能评价");
+            }
+        }
+        // Gate: 已报名且已签到
+        if (memberId == null || !registrationService.isApproved(activityId, memberId)) {
+            return Result.error("请先报名此活动后再评价");
+        }
+        boolean hasCheckedIn = taskMapper.selectCount(new LambdaQueryWrapper<com.etotem.cfc.entity.Task>()
+                .eq(com.etotem.cfc.entity.Task::getSourceType, "activity")
+                .eq(com.etotem.cfc.entity.Task::getSourceId, activityId)
+                .eq(com.etotem.cfc.entity.Task::getChildId, memberId)
+                .eq(com.etotem.cfc.entity.Task::getStatus, "completed")) > 0;
+        if (!hasCheckedIn) {
+            return Result.error("请先完成签到后再评价");
+        }
+        // Dedup: 同一 activityId + childId 只保留一条;重复提交则更新
+        ActivityFeedback existing = activityFeedbackMapper.selectOne(new LambdaQueryWrapper<ActivityFeedback>()
+                .eq(ActivityFeedback::getActivityId, activityId)
+                .eq(ActivityFeedback::getChildId, memberId)
+                .last("LIMIT 1"));
+        ActivityFeedback feedback;
+        if (existing != null) {
+            feedback = existing;
+            feedback.setRatings(ratings);
+            feedback.setComment(comment);
+            feedback.setStatus(1);
+            feedback.setUpdatedAt(new Date());
+            activityFeedbackMapper.updateById(feedback);
+        } else {
+            feedback = new ActivityFeedback();
+            feedback.setActivityId(activityId);
+            feedback.setUserId(userId);
+            feedback.setChildId(memberId);
+            feedback.setRatings(ratings);
+            feedback.setComment(comment);
+            feedback.setStatus(1);
+            feedback.setCreatedAt(new Date());
+            activityFeedbackMapper.insert(feedback);
+        }
         return Result.success("提交评价成功", feedback.getId());
     }
 

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityOrderService.java

@@ -47,6 +47,9 @@ public class ActivityOrderService {
     @Resource
     private PaymentService paymentService;
 
+    @Resource
+    private ActivityRegistrationService registrationService;
+
     public Result<Map<String, Object>> create(Long activityId, Long userId, List<Map<String, Object>> registrants) {
         Activity activity = activityMapper.selectById(activityId);
         if (activity == null) return Result.error("活动不存在");
@@ -177,6 +180,11 @@ public class ActivityOrderService {
             Object memberIdObj = item.get("memberId");
             Long memberId = memberIdObj != null ? Long.valueOf(memberIdObj.toString()) : null;
             if (memberId != null && isRegistered(activityId, memberId)) continue;
+            // 报名通过(免审/支付成功)时占用名额;名额已满则跳过该报名人(退款见 P0-2)
+            if ("approved".equals(status) && !registrationService.occupyQuota(activityId)) {
+                log.warn("活动 {} 名额已满,跳过报名人 {}", activityId, item.get("name"));
+                continue;
+            }
             ActivityRegistration ar = new ActivityRegistration();
             ar.setActivityId(activityId);
             ar.setUserId(userId);

+ 120 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.DataMaskUtil;
 import com.etotem.cfc.common.Result;
@@ -34,6 +35,8 @@ public class ActivityRegistrationService {
     private ActivityMapper activityMapper;
     @Resource
     private ActivityOrderMapper orderMapper;
+    @Resource
+    private PaymentService paymentService;
 
     @Transactional
     public Result<Map<String, Object>> batchRegister(Long activityId, Long userId, List<Map<String, Object>> registrants) {
@@ -45,6 +48,7 @@ public class ActivityRegistrationService {
         }
         Result<Void> gate = checkRegistrationGate(activity, userId);
         if (gate != null) return Result.error(gate.getMessage());
+        boolean needReview = activity.getRequireReview() != null && activity.getRequireReview() == 1;
         List<Long> ids = new ArrayList<>();
         for (Map<String, Object> reg : registrants) {
             String name = (String) reg.get("name");
@@ -57,11 +61,21 @@ public class ActivityRegistrationService {
                     return Result.error("已报名: " + name);
                 }
             }
+            // 免审活动:报名即通过并占用名额;名额已满则中止(已写入的本次批次有效)
+            if (!needReview) {
+                if (!occupyQuota(activityId)) {
+                    return Result.error("名额已满,报名失败(含: " + name + ")");
+                }
+            }
             ActivityRegistration ar = new ActivityRegistration();
             ar.setActivityId(activityId); ar.setUserId(userId); ar.setChildId(memberId);
             ar.setRegistrantName(name.trim()); ar.setRegistrantPhone(phone != null ? phone.trim() : null);
-            ar.setStatus("pending"); ar.setRegisteredAt(LocalDateTime.now());
+            ar.setStatus(needReview ? "pending" : "approved"); ar.setRegisteredAt(LocalDateTime.now());
             ar.setCreatedAt(LocalDateTime.now()); ar.setUpdatedAt(LocalDateTime.now());
+            if (!needReview) {
+                ar.setApprovedAt(LocalDateTime.now());
+                ar.setApprovedBy(userId);
+            }
             registrationMapper.insert(ar);
             ids.add(ar.getId());
         }
@@ -82,21 +96,60 @@ public class ActivityRegistrationService {
         Result<Void> gate = checkRegistrationGate(activity, userId);
         if (gate != null) return Result.error(gate.getMessage());
         if (isRegistered(activityId, memberId)) return Result.error("您已报名此活动");
+        boolean needReview = activity.getRequireReview() != null && activity.getRequireReview() == 1;
+        // 免审活动:报名即通过并占用名额
+        if (!needReview) {
+            if (!occupyQuota(activityId)) {
+                return Result.error("名额已满,报名失败");
+            }
+        }
         ActivityRegistration reg = new ActivityRegistration();
         reg.setActivityId(activityId); reg.setUserId(userId); reg.setChildId(memberId);
-        reg.setStatus("pending"); reg.setRegisteredAt(LocalDateTime.now());
+        reg.setStatus(needReview ? "pending" : "approved"); reg.setRegisteredAt(LocalDateTime.now());
         reg.setCreatedAt(LocalDateTime.now()); reg.setUpdatedAt(LocalDateTime.now());
+        if (!needReview) {
+            reg.setApprovedAt(LocalDateTime.now());
+            reg.setApprovedBy(userId);
+        }
         registrationMapper.insert(reg);
         Map<String, Object> result = new HashMap<>();
-        result.put("registrationId", reg.getId()); result.put("status", "pending");
+        result.put("registrationId", reg.getId()); result.put("status", reg.getStatus());
         return Result.success(result);
     }
 
     @Transactional
     public Result<Void> cancel(Long activityId, Long userId, Long memberId) {
-        ActivityRegistration reg = findRegistration(activityId, memberId, userId, "pending");
+        ActivityRegistration reg = registrationMapper.selectOne(new LambdaQueryWrapper<ActivityRegistration>()
+                .eq(ActivityRegistration::getActivityId, activityId)
+                .eq(ActivityRegistration::getChildId, memberId)
+                .eq(ActivityRegistration::getUserId, userId)
+                .in(ActivityRegistration::getStatus, "pending", "approved")
+                .last("LIMIT 1"));
         if (reg == null) return Result.error("报名记录不存在");
-        reg.setStatus("cancelled"); reg.setUpdatedAt(LocalDateTime.now());
+        String prevStatus = reg.getStatus();
+        if ("approved".equals(prevStatus)) {
+            // 付费活动取消:原路退款(金额取该报名人对应 fee;如一单多人则按单退款)
+            ActivityOrder order = orderMapper.selectOne(new LambdaQueryWrapper<ActivityOrder>()
+                    .eq(ActivityOrder::getActivityId, activityId)
+                    .eq(ActivityOrder::getUserId, userId)
+                    .eq(ActivityOrder::getStatus, "paid")
+                    .last("LIMIT 1"));
+            if (order != null) {
+                int refundFee = resolveRegistrantFee(order, memberId);
+                try {
+                    paymentService.refund(order.getOrderNo(), refundFee > 0 ? refundFee : order.getTotalAmount(), "取消活动报名");
+                } catch (Exception e) {
+                    log.error("活动报名退款失败: orderNo={}, memberId={}", order.getOrderNo(), memberId, e);
+                    return Result.error("退款处理失败,请联系客服");
+                }
+                order.setStatus("refunded");
+                orderMapper.updateById(order);
+            }
+            releaseQuota(activityId);
+        }
+        reg.setStatus("cancelled");
+        reg.setCancelledAt(LocalDateTime.now());
+        reg.setUpdatedAt(LocalDateTime.now());
         registrationMapper.updateById(reg);
         return Result.success(null);
     }
@@ -106,6 +159,10 @@ public class ActivityRegistrationService {
         ActivityRegistration reg = registrationMapper.selectById(registrationId);
         if (reg == null) return Result.error("报名记录不存在");
         if (!"pending".equals(reg.getStatus())) return Result.error("当前状态不允许审核");
+        // 审核通过即占用名额(原子校验,满员则无法通过)
+        if (!occupyQuota(reg.getActivityId())) {
+            return Result.error("名额已满,无法通过审核");
+        }
         reg.setStatus("approved"); reg.setApprovedAt(LocalDateTime.now());
         reg.setApprovedBy(adminUserId); reg.setUpdatedAt(LocalDateTime.now());
         registrationMapper.updateById(reg);
@@ -131,6 +188,37 @@ public class ActivityRegistrationService {
         return Result.success(null);
     }
 
+    /**
+     * 原子占用一个名额:仅当 approved_count < max_participants 时自增,返回是否成功。
+     * 无人数限制(maxParticipants 为空或 <=0)视为不限,直接自增计数。
+     */
+    @Transactional
+    public boolean occupyQuota(Long activityId) {
+        Activity activity = activityMapper.selectById(activityId);
+        if (activity == null) return false;
+        Integer max = activity.getMaxParticipants();
+        if (max != null && max > 0) {
+            int rows = activityMapper.update(null, new UpdateWrapper<Activity>()
+                    .eq("id", activityId)
+                    .lt("approved_count", max)
+                    .setSql("approved_count = approved_count + 1"));
+            return rows > 0;
+        }
+        activityMapper.update(null, new UpdateWrapper<Activity>()
+                .eq("id", activityId)
+                .setSql("approved_count = approved_count + 1"));
+        return true;
+    }
+
+    /** 原子回退一个名额(仅当 approved_count > 0 时减一)。 */
+    @Transactional
+    public void releaseQuota(Long activityId) {
+        activityMapper.update(null, new UpdateWrapper<Activity>()
+                .eq("id", activityId)
+                .gt("approved_count", 0)
+                .setSql("approved_count = approved_count - 1"));
+    }
+
     public Result<Map<String, Object>> getMyRegistrations(Long userId, Long memberId, Integer page, Integer size) {
         if (page == null || page < 1) page = 1;
         if (size == null || size < 1) size = 10;
@@ -202,9 +290,6 @@ public class ActivityRegistrationService {
         Activity activity = activityMapper.selectById(activityId);
         if (activity == null) return Result.error("活动不存在");
 
-        long approvedCount = registrationMapper.selectCount(new LambdaQueryWrapper<ActivityRegistration>()
-            .eq(ActivityRegistration::getActivityId, activityId)
-            .eq(ActivityRegistration::getStatus, "approved"));
         long pendingCount = registrationMapper.selectCount(new LambdaQueryWrapper<ActivityRegistration>()
             .eq(ActivityRegistration::getActivityId, activityId)
             .eq(ActivityRegistration::getStatus, "pending"));
@@ -223,15 +308,17 @@ public class ActivityRegistrationService {
         }).collect(Collectors.toList());
 
         Integer max = activity.getMaxParticipants();
+        // 名额占用以 approved_count 列(原子维护)为准,与报名审核状态一致
+        Integer approved = activity.getApprovedCount() != null ? activity.getApprovedCount() : 0;
         Integer remaining = null;
         if (max != null && max > 0) {
-            remaining = (int) Math.max(0, max - approvedCount);
+            remaining = (int) Math.max(0, max - approved);
         }
 
         Map<String, Object> data = new HashMap<>();
         data.put("activityId", activityId);
         data.put("maxParticipants", max != null ? max : 0);
-        data.put("approvedCount", approvedCount);
+        data.put("approvedCount", approved);
         data.put("pendingCount", pendingCount);
         data.put("remaining", remaining);
         data.put("registrants", registrants);
@@ -352,6 +439,29 @@ public class ActivityRegistrationService {
             .eq(ActivityRegistration::getStatus, "approved").last("LIMIT 1")) != null;
     }
 
+    private int resolveRegistrantFee(ActivityOrder order, Long memberId) {
+        if (order.getRegistrantsJson() == null) return 0;
+        try {
+            com.alibaba.fastjson.TypeReference<java.util.List<java.util.Map<String, Object>>> typeRef
+                    = new com.alibaba.fastjson.TypeReference<java.util.List<java.util.Map<String, Object>>>() {};
+            java.util.List<java.util.Map<String, Object>> list
+                    = com.alibaba.fastjson.JSON.parseObject(order.getRegistrantsJson(), typeRef);
+            if (list != null) {
+                for (java.util.Map<String, Object> item : list) {
+                    Object mid = item.get("memberId");
+                    if (mid != null && memberId != null
+                            && String.valueOf(mid).equals(String.valueOf(memberId))) {
+                        Object fee = item.get("fee");
+                        if (fee instanceof Number) return ((Number) fee).intValue();
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("解析报名人 fee 失败: orderNo={}, memberId={}", order.getOrderNo(), memberId, e);
+        }
+        return 0;
+    }
+
     private ActivityRegistration findRegistration(Long activityId, Long memberId, Long userId, String status) {
         LambdaQueryWrapper<ActivityRegistration> wrapper = new LambdaQueryWrapper<ActivityRegistration>()
             .eq(ActivityRegistration::getActivityId, activityId)

+ 96 - 27
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -18,6 +18,7 @@ import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
@@ -153,6 +154,8 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     public Result<ActivityDTO> create(Activity activity, Long creatorId) {
         activity.setStatus("draft");
         activity.setCurrentParticipants(0);
+        activity.setApprovedCount(0);
+        activity.setSignedCount(0);
         activity.setCreatorId(creatorId);
         activity.setCreatedAt(new Date());
         activity.setUpdatedAt(new Date());
@@ -178,6 +181,10 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         if (!"draft".equals(activity.getStatus())) {
             return Result.error("仅草稿状态可发布");
         }
+        // P1-2: 发布前校验维度代码,两路(直发/审核流)统一要求 dimensionCode 非空
+        if (activity.getDimensionCode() == null || activity.getDimensionCode().trim().isEmpty()) {
+            return Result.error("请先设置活动维度(dimensionCode)");
+        }
         activity.setStatus("published");
         activity.setUpdatedAt(new Date());
         this.updateById(activity);
@@ -317,7 +324,10 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
             }
         }
 
-        // 5. Create task with status='completed' (auto-completed by check-in)
+        // 是否需要审核签到(活动级开关,默认免审)
+        boolean needReview = activity.getRequireCheckinReview() != null && activity.getRequireCheckinReview() == 1;
+
+        // 6. 创建签到任务;开启审核时进入 pending_review,待后台审核通过才发积分/能量
         Task task = new Task();
         if (child != null) {
             task.setFamilyId(child.getFamilyId());
@@ -331,13 +341,17 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         task.setPoints(points);
         task.setRepeatType("none");
         task.setCategory("行动类");
-        task.setNeedReview(0);
+        task.setNeedReview(needReview ? 1 : 0);
         task.setReviewByCategory(0);
         task.setTaskType("onetime");
         task.setSourceType("activity");
         task.setSourceId(activityId);
-        task.setStatus("completed");
-        task.setCompletedAt(new Date());
+        if (needReview) {
+            task.setStatus("pending_review");
+        } else {
+            task.setStatus("completed");
+            task.setCompletedAt(new Date());
+        }
         task.setCreatedAt(new Date());
         task.setUpdatedAt(new Date());
 
@@ -350,21 +364,40 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
 
         taskMapper.insert(task);
 
-        // 6. Update activity currentParticipants
-        activity.setCurrentParticipants(activity.getCurrentParticipants() != null ? activity.getCurrentParticipants() + 1 : 1);
+        // 7. 签到计数(无论是否需审核都计入 signedCount)
+        activity.setSignedCount(activity.getSignedCount() != null ? activity.getSignedCount() + 1 : 1);
         activity.setUpdatedAt(new Date());
         this.updateById(activity);
 
-        // 7. Update child totalPoints
+        // 8. 发放奖励:免审直接发放;需审核在审核通过(reviewCheckin)时发放
+        if (needReview) {
+            result.put("needReview", true);
+            result.put("pointsEarned", 0);
+            log.info("活动签到进入待审核: activityId={}, memberId={}, taskId={}", activityId, memberId, task.getId());
+        } else {
+            awardCheckinRewards(activity, child, task, userId, points);
+            result.put("needReview", false);
+            result.put("pointsEarned", points);
+        }
+
+        // 9. Return result
+        result.put("taskId", task.getId());
+        return result;
+    }
+
+    /**
+     * 签到奖励发放(积分/能量/CF)。免审签到在 checkin 内调用;需审核签到在审核通过时调用。
+     */
+    private void awardCheckinRewards(Activity activity, FamilyMember child, Task task, Long userId, int points) {
+        // 更新孩子总积分
         if (child != null) {
             child.setTotalPoints(child.getTotalPoints() + points);
             child.setUpdatedAt(new Date());
             familyMemberMapper.updateById(child);
         }
-
-        // 8. Insert PointsLog
+        // 积分流水
         PointsLog pointsLog = new PointsLog();
-        pointsLog.setChildId(memberId);
+        pointsLog.setChildId(task.getChildId());
         pointsLog.setTaskId(task.getId());
         pointsLog.setAmount(points);
         pointsLog.setType("earn");
@@ -372,41 +405,77 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         pointsLog.setDescription("活动签到: " + activity.getTitle());
         pointsLog.setCreatedAt(new Date());
         pointsLogMapper.insert(pointsLog);
-
-        // 9. Award energy via EnergyService
+        // 发放能量
         try {
-            energyService.awardEnergy(memberId, "activity", activityId, points,
+            energyService.awardEnergy(task.getChildId(), "activity", activity.getId(), points,
                 "活动签到: " + activity.getTitle(), null);
         } catch (Exception e) {
-            log.warn("能量发放失败(不影响积分): memberId={}, activityId={}, error={}",
-            memberId, activityId, e.getMessage());
+            log.warn("能量发放失败(不影响积分): childId={}, activityId={}, error={}",
+                task.getChildId(), activity.getId(), e.getMessage());
         }
-
-        // 9b. 活动下发 CF 值(平台积分)
+        // 活动下发 CF 值(平台积分)
         if (activity.getPlatformPoints() != null && activity.getPlatformPoints() > 0 && userId != null) {
             try {
                 platformPointsService.earn(userId, activity.getPlatformPoints(),
-                        "activity", activityId, "活动签到: " + activity.getTitle());
+                        "activity", activity.getId(), "活动签到: " + activity.getTitle());
             } catch (Exception e) {
                 log.warn("活动CF值发放失败: userId={}, activityId={}, error={}",
-                        userId, activityId, e.getMessage());
+                        userId, activity.getId(), e.getMessage());
             }
         }
+    }
 
-        // 10. Return result
-        result.put("taskId", task.getId());
-        result.put("pointsEarned", points);
-        return result;
+    /**
+     * 审核签到(活动级 requireCheckinReview 开启时):审批通过发放积分/能量/CF;驳回则不发。
+     */
+    @Transactional
+    public Result<Map<String, Object>> reviewCheckin(Long taskId, Long reviewerId, boolean approve) {
+        Task task = taskMapper.selectById(taskId);
+        if (task == null) return Result.error("签到任务不存在");
+        if (!"activity".equals(task.getSourceType())) return Result.error("非活动签到任务");
+        if (!"pending_review".equals(task.getStatus())) return Result.error("当前状态不可审核");
+        Map<String, Object> result = new HashMap<>();
+        if (approve) {
+            Activity activity = this.getById(task.getSourceId());
+            FamilyMember child = task.getChildId() != null ? familyMemberMapper.selectById(task.getChildId()) : null;
+            task.setStatus("completed");
+            task.setCompletedAt(new Date());
+            task.setUpdatedAt(new Date());
+            taskMapper.updateById(task);
+            int points = task.getPoints() != null ? task.getPoints() : 5;
+            if (activity != null) {
+                awardCheckinRewards(activity, child, task, task.getCreatorId(), points);
+            }
+            result.put("approved", true);
+            return Result.success(result);
+        } else {
+            task.setStatus("rejected");
+            task.setUpdatedAt(new Date());
+            taskMapper.updateById(task);
+            result.put("approved", false);
+            return Result.success(result);
+        }
     }
 
-    private void autoEndExpiredActivities() {
+    /**
+     * 自动结束过期活动
+     * 逻辑:发布中且结束时间已过(endTime < now),将状态置为 ended。
+     * 备注:正式上线建议改为 Spring @Scheduled(cron = "0 0 2 * * ?") 每日 02:00 执行(见 TODOs),此处保留方法供 list/detail 触发兜底。
+     */
+    /**
+     * 自动结束过期活动(P2-1 定时化)
+     * 每日 02:00 定时执行,结束阈值 = endTime < now()(去掉旧的 7 天延迟)。
+     * 同时保留在 list/detail 查询时轻量调用兜底(实时状态判断)。
+     */
+    @Scheduled(cron = "0 0 2 * * ?")
+    public void autoEndExpiredActivities() {
         try {
-            Date threshold = new Date(System.currentTimeMillis() - 7L * 24 * 3600 * 1000);
+            Date now = new Date();
             this.update(new LambdaUpdateWrapper<Activity>()
                     .eq(Activity::getStatus, "published")
-                    .lt(Activity::getEndTime, threshold)
+                    .lt(Activity::getEndTime, now)
                     .set(Activity::getStatus, "ended")
-                    .set(Activity::getUpdatedAt, new Date()));
+                    .set(Activity::getUpdatedAt, now));
         } catch (Exception e) {
             log.warn("自动结束过期活动失败: {}", e.getMessage());
         }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ContentRecommendService.java

@@ -413,7 +413,7 @@ private List<RecommendItemVO> recommendProducts(String dimensionCode, Long userI
             score += calcFreshnessScore(a.getCreatedAt(), 15);
 
             // 参与热度 (0-10)
-            int participants = a.getCurrentParticipants() != null ? a.getCurrentParticipants() : 0;
+            int participants = a.getApprovedCount() != null ? a.getApprovedCount() : 0;
             score += Math.min(10, participants);
             if (participants >= 20) reasons.add("热门");
 

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java

@@ -433,6 +433,10 @@ public class PaymentService implements PaymentServiceInterface {
     }
 
     public void refund(String orderNo, Integer amount, String reason) {
+        if (testMode) {
+            log.info("【测试模式】跳过微信退款请求: orderNo={}, amount={}, reason={}", orderNo, amount, reason);
+            return;
+        }
         try {
             PackageOrder order = getOrderStatus(orderNo);
             Integer totalFee = order != null ? order.getPrice() : amount;

+ 14 - 1
cfc-backend/src/main/resources/schema.sql

@@ -2091,6 +2091,8 @@ CREATE TABLE IF NOT EXISTS activity_registrations (
     registrant_phone VARCHAR(20) COMMENT '报名人联系方式',
     status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected/cancelled',
     reject_reason VARCHAR(500) COMMENT '驳回原因',
+    cancelled_at DATETIME DEFAULT NULL COMMENT '取消报名时间',
+    cancel_reason VARCHAR(200) DEFAULT NULL COMMENT '取消原因',
     registered_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间',
     approved_at DATETIME COMMENT '审核通过时间',
     approved_by BIGINT COMMENT '审核人用户ID',
@@ -2626,6 +2628,15 @@ ALTER TABLE activities ADD COLUMN require_registration TINYINT DEFAULT 1 COMMENT
 ALTER TABLE activities ADD COLUMN require_checkin_review TINYINT DEFAULT 1 COMMENT '打卡是否需要审核: 1=需要, 0=直接通过';
 ALTER TABLE activities ADD COLUMN energy_penalty_unregistered INT DEFAULT 5 COMMENT '未报名打卡的行能量扣减值';
 
+-- 2026-10-22 整改:报名审核开关 + 名额占用计数
+ALTER TABLE activities ADD COLUMN require_review TINYINT DEFAULT 0 COMMENT '报名是否需要审核: 1=需要, 0=免审即报名通过';
+ALTER TABLE activities ADD COLUMN approved_count INT DEFAULT 0 COMMENT '已通过报名人数(名额占用统计)';
+ALTER TABLE activities ADD COLUMN signed_count INT DEFAULT 0 COMMENT '签到人数(checkin 时 +1,区别于 approved_count 的报名口径)';
+-- 修正打卡审核默认值与存量归一化(2026-10-22)
+ALTER TABLE activities MODIFY COLUMN require_checkin_review TINYINT DEFAULT 0 COMMENT '打卡是否需要审核: 1=需要, 0=免审直接通过';
+UPDATE activities SET require_checkin_review = 0 WHERE require_checkin_review = 1;
+UPDATE activities a SET a.approved_count = (SELECT cnt FROM (SELECT COUNT(*) AS cnt FROM activity_registrations r WHERE r.activity_id = a.id AND r.status = 'approved') t);
+
 -- ============================================================
 -- 新增:系统配置表(角色年龄阈值等)
 -- ============================================================
@@ -3947,9 +3958,11 @@ CREATE TABLE IF NOT EXISTS activity_feedback (
     comment TEXT COMMENT '文字评价',
     status TINYINT DEFAULT 0 COMMENT '状态: 0=待审核 1=已发布',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT NULL COMMENT '更新时间(重复提交评价时更新)',
     INDEX idx_activity_id (activity_id),
     INDEX idx_user_id (user_id),
-    INDEX idx_child_id (child_id)
+    INDEX idx_child_id (child_id),
+    UNIQUE KEY uk_activity_child (activity_id, child_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表';
 
 -- ============================================================

+ 2 - 2
cfc-frontend/pages/activity/components/ActivityPoster.vue

@@ -20,7 +20,7 @@
         <view class="tpl-options">
           <view
             v-for="tpl in tplOptions"
-            :key="'tpl_' + tpl.id"
+            :key="tpl.id"
             class="tpl-option"
             :class="{ 'tpl-active': activeTemplateId === tpl.id }"
             @click="switchTemplate(tpl)"
@@ -36,7 +36,7 @@
         <view class="bg-options">
           <view
             v-for="(c, i) in bgOptions"
-            :key="'bg_' + i"
+            :key="i"
             class="bg-option"
             :class="{ 'bg-active': bgColor === c }"
             :style="{ background: c }"

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-7427f3981efbea8981ba405688f009a321c0c3e7
+c104beeacda23ad8de56c9e1104c5eedc45636ac

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,71 @@
 
 ---
 
+## v1.0.1753 (2026-10-22)
+
+### Bug 修复
+- 管理端 header-right 桌面端固定靠右对齐
+- ActivityPoster 两个 :key 去掉字符串拼接表达式
+- 修复管理端暗色侧边栏菜单文字对比度过低
+
+### 样式
+- C-9 GrowthTaskManagement/data-source-config/Activities/SubscriptionManagement/ReportTemplateManage 内联 style 归零
+- C-8 SurveyTemplates/HealthPlanReview/BadgeManage/Users/ProxyReportUpload 内联 style 归零
+- C-7 ZodiacConfigs/WuxingConfig/ProblemDomain/Families/Wishes 内联 style 归零
+- C-6 Foods/dimension-config/TeacherDashboard/NutritionProducts/InventoryManage 内联 style 归零
+- C-5 ActivityEdit/CouponManagement 内联 style 清理 35 处全归零 - full-width/w-200 宽度类 + success-* 成功弹窗类 + editor-body/form-hint 类
+- C-4 HomeBannerManage/FileManage 内联 style 清理 38 处全归零 - w-*/full-width 宽度类 + file-icon/tag-chip/stat-chips 结构类 + 文件类型图标 fi-* 色值类
+- C-3 ProfileManagement 内联 style 清理 23 处全归零 - 过滤栏 filter-bar 合并 + 标签 tag-mr + 分区 section-title/rec-block/rec-label/dim-label 类
+- C-2 MembershipCenter 内联 style 清理 29 处全归零 - 宽度 w-160/180/240/320 工具类 + 对话框分区 mv-* 类 + hint 提示类
+- C-1 ProductEdit 内联 style 清理 44 处全归零 - 表单控件宽度 w-*/full-width 工具类 + form-hint 系列提示文字类 + 间距 ml/mr/mt-8 + bundle-row/sp-opt-* 行内布局类
+
+### 新功能
+- 报名审核开关+名额原子占用+签到审核落地+清理总结死代码
+
+### 其他
+- P0-1 报名审核与名额(D4): Activity 新增 requireReview(默认免审) 与 approvedCount(名额占用计数);register/batchRegister 按 requireReview 决定 pending/approved,免审即占名额;approve 审过才占名额;名额用原子 SQL(approved_count<max 条件自增)防超卖;cancel 支持 approved 并回退名额;getRegistrationRoster 改读 approved_count;ActivityOrderService.writeRegistrations 同步占名额。
+- P0-3 签到审核(D1): checkin 按活动级 requireCheckinReview 进入 pending_review,抽取 awardCheckinRewards;新增 reviewCheckin 审核通过发积分/能量/CF、驳回不发;ActivityController 新增 /checkin-review 端点。
+- P1-4 清理死代码(D2): ActivityDTO 删除 summarizing/总结中 注释并暴露 requireReview 字段与映射。
+- 迁移: DatabaseInitializer 与 schema.sql 同步新增列,并将存量 require_checkin_review 归一为 0(免审)、按已通过报名回填 approved_count。
+- 注意: 退款链路(P0-2)与删除规划师/履约子系统(P2-2)按方案仍待独立实施。
+- 
+- - 新增线下课程打卡: 复用主 activities 模块承载(发布/报名/海报二维码/可见范围), 通过 planner_offline_course_map 桥接到认证类别
+- - 落地 activity_checkins 通用签到记录, 修复 activities 原"签到审核形同虚设"旧患
+- - 课时池 = 线上(小鹅通) + 线下(活动签到) 按类别合并; 接单闸门改为类别感知 assertCertified(userId, categoryCode)
+- 
+- 
+- 
+-   'tpl_' + tpl.id / 'bg_' + i
+- 改为纯成员访问(与全仓既有写法一致):
+- - v-for="tpl in tplOptions"  :key="tpl.id"   (serverTemplates 已保证 id 非空)
+- - v-for="(c, i) in bgOptions" :key="i"       (元素是色值字符串,按索引取键)
+- 校验:audit-mp-antipatterns ERROR 0 / WARN 185;verify-vue-compile PASS
+- 
+
+### 文档
+- 规划师多类别认证 + 线下课复用活动管理 设计规格 (v2.1)
+- 锁定活动管理整改决策(D1-D4)并更新方案
+- 活动管理实现vs需求差异调研与整改方案
+
+
+## v1.0.1752 (2026-10-22)
+
+### 其他
+- - 公司地址:中国 · 北京 → 北京市海淀区和盈中心三层
+- - 新增联系电话:010-58733188(联系区 + footer.js 已有)
+- - 清理所有 html/css/js 中的「亿图腾」品牌名残留
+- - articles/js/footer.js 地址补全至完整街道
+- 
+- - 唯一消费方 pages/activity/activity-detail/activity-detail.vue 的 import 改为
+-   @/pages/activity/components/ActivityPoster.vue
+- - 组件内只有 @/utils/format.js、@/utils/fontScale.js 两处别名引用,移动后无需改动
+- - 校验:verify-vue-compile 两个文件 模板/样式/脚本 均通过;全仓无残留旧路径引用
+- 
+
+### 重构
+- ActivityPoster 移入 pages/activity 子包,主包减重
+
+
 ## v1.0.1751 (2026-10-22)
 
 ### 新功能

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1751
+> 当前版本: v1.0.1753
 
 ## 历史版本
 
@@ -8,6 +8,71 @@
 
 ---
 
+## v1.0.1753 (2026-10-22)
+
+### Bug 修复
+- 管理端 header-right 桌面端固定靠右对齐
+- ActivityPoster 两个 :key 去掉字符串拼接表达式
+- 修复管理端暗色侧边栏菜单文字对比度过低
+
+### 样式
+- C-9 GrowthTaskManagement/data-source-config/Activities/SubscriptionManagement/ReportTemplateManage 内联 style 归零
+- C-8 SurveyTemplates/HealthPlanReview/BadgeManage/Users/ProxyReportUpload 内联 style 归零
+- C-7 ZodiacConfigs/WuxingConfig/ProblemDomain/Families/Wishes 内联 style 归零
+- C-6 Foods/dimension-config/TeacherDashboard/NutritionProducts/InventoryManage 内联 style 归零
+- C-5 ActivityEdit/CouponManagement 内联 style 清理 35 处全归零 - full-width/w-200 宽度类 + success-* 成功弹窗类 + editor-body/form-hint 类
+- C-4 HomeBannerManage/FileManage 内联 style 清理 38 处全归零 - w-*/full-width 宽度类 + file-icon/tag-chip/stat-chips 结构类 + 文件类型图标 fi-* 色值类
+- C-3 ProfileManagement 内联 style 清理 23 处全归零 - 过滤栏 filter-bar 合并 + 标签 tag-mr + 分区 section-title/rec-block/rec-label/dim-label 类
+- C-2 MembershipCenter 内联 style 清理 29 处全归零 - 宽度 w-160/180/240/320 工具类 + 对话框分区 mv-* 类 + hint 提示类
+- C-1 ProductEdit 内联 style 清理 44 处全归零 - 表单控件宽度 w-*/full-width 工具类 + form-hint 系列提示文字类 + 间距 ml/mr/mt-8 + bundle-row/sp-opt-* 行内布局类
+
+### 新功能
+- 报名审核开关+名额原子占用+签到审核落地+清理总结死代码
+
+### 其他
+- P0-1 报名审核与名额(D4): Activity 新增 requireReview(默认免审) 与 approvedCount(名额占用计数);register/batchRegister 按 requireReview 决定 pending/approved,免审即占名额;approve 审过才占名额;名额用原子 SQL(approved_count<max 条件自增)防超卖;cancel 支持 approved 并回退名额;getRegistrationRoster 改读 approved_count;ActivityOrderService.writeRegistrations 同步占名额。
+- P0-3 签到审核(D1): checkin 按活动级 requireCheckinReview 进入 pending_review,抽取 awardCheckinRewards;新增 reviewCheckin 审核通过发积分/能量/CF、驳回不发;ActivityController 新增 /checkin-review 端点。
+- P1-4 清理死代码(D2): ActivityDTO 删除 summarizing/总结中 注释并暴露 requireReview 字段与映射。
+- 迁移: DatabaseInitializer 与 schema.sql 同步新增列,并将存量 require_checkin_review 归一为 0(免审)、按已通过报名回填 approved_count。
+- 注意: 退款链路(P0-2)与删除规划师/履约子系统(P2-2)按方案仍待独立实施。
+- 
+- - 新增线下课程打卡: 复用主 activities 模块承载(发布/报名/海报二维码/可见范围), 通过 planner_offline_course_map 桥接到认证类别
+- - 落地 activity_checkins 通用签到记录, 修复 activities 原"签到审核形同虚设"旧患
+- - 课时池 = 线上(小鹅通) + 线下(活动签到) 按类别合并; 接单闸门改为类别感知 assertCertified(userId, categoryCode)
+- 
+- 
+- 
+-   'tpl_' + tpl.id / 'bg_' + i
+- 改为纯成员访问(与全仓既有写法一致):
+- - v-for="tpl in tplOptions"  :key="tpl.id"   (serverTemplates 已保证 id 非空)
+- - v-for="(c, i) in bgOptions" :key="i"       (元素是色值字符串,按索引取键)
+- 校验:audit-mp-antipatterns ERROR 0 / WARN 185;verify-vue-compile PASS
+- 
+
+### 文档
+- 规划师多类别认证 + 线下课复用活动管理 设计规格 (v2.1)
+- 锁定活动管理整改决策(D1-D4)并更新方案
+- 活动管理实现vs需求差异调研与整改方案
+
+
+## v1.0.1752 (2026-10-22)
+
+### 其他
+- - 公司地址:中国 · 北京 → 北京市海淀区和盈中心三层
+- - 新增联系电话:010-58733188(联系区 + footer.js 已有)
+- - 清理所有 html/css/js 中的「亿图腾」品牌名残留
+- - articles/js/footer.js 地址补全至完整街道
+- 
+- - 唯一消费方 pages/activity/activity-detail/activity-detail.vue 的 import 改为
+-   @/pages/activity/components/ActivityPoster.vue
+- - 组件内只有 @/utils/format.js、@/utils/fontScale.js 两处别名引用,移动后无需改动
+- - 校验:verify-vue-compile 两个文件 模板/样式/脚本 均通过;全仓无残留旧路径引用
+- 
+
+### 重构
+- ActivityPoster 移入 pages/activity 子包,主包减重
+
+
 ## v1.0.1751 (2026-10-22)
 
 ### 新功能

+ 22 - 12
cfc-web/src/views/Families.vue

@@ -6,7 +6,7 @@
       </PageHeader>
 
       <!-- 搜索 -->
-      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+      <el-form :inline="true" :model="searchForm" class="mb-20">
         <el-form-item label="家庭名称">
           <el-input
             v-model="searchForm.name"
@@ -27,7 +27,7 @@
           :data="tableData"
           border
           v-loading="loading"
-          style="width: 100%"
+          class="full-width"
           height="100%"
         >
         <el-table-column prop="id" label="ID" width="80">
@@ -77,7 +77,7 @@
 
         <!-- 分页 -->
       <el-pagination
-        style="margin-top: 20px; text-align: right"
+        class="pagination-right"
         @size-change="handleSizeChange"
         @current-change="handleCurrentChange"
         :current-page="pagination.current"
@@ -112,7 +112,7 @@
       width="900px"
     >
       <div v-if="membersData.family">
-        <el-descriptions :column="2" border style="margin-bottom: 20px">
+        <el-descriptions :column="2" border class="mb-20">
           <el-descriptions-item label="家庭名称">{{ membersData.family.name }}</el-descriptions-item>
           <el-descriptions-item label="邀请码">{{ membersData.family.inviteCode }}</el-descriptions-item>
         </el-descriptions>
@@ -121,7 +121,7 @@
         <el-table
           :data="membersData.members"
           border
-          style="width: 100%"
+          class="full-width"
           :max-height="350"
           v-if="membersData.members && membersData.members.length > 0"
         >
@@ -179,7 +179,7 @@
           <el-input :value="couponForm.familyName" disabled></el-input>
         </el-form-item>
         <el-form-item label="优惠券" required>
-          <el-select v-model="couponForm.couponId" placeholder="请选择优惠券" style="width: 100%">
+          <el-select v-model="couponForm.couponId" placeholder="请选择优惠券" class="full-width">
             <el-option
               v-for="c in couponOptions"
               :key="c.id"
@@ -204,9 +204,9 @@
       :visible.sync="couponDialogVisible"
       width="900px"
     >
-      <el-form :inline="true" style="margin-bottom: 12px">
+      <el-form :inline="true" class="mb-12">
         <el-form-item label="状态">
-          <el-select v-model="couponStatusFilter" placeholder="全部" clearable size="small" style="width:120px">
+          <el-select v-model="couponStatusFilter" placeholder="全部" clearable size="small" class="w-120">
             <el-option label="可用" value="AVAILABLE"></el-option>
             <el-option label="已使用" value="USED"></el-option>
           </el-select>
@@ -215,7 +215,7 @@
           <el-button size="small" @click="loadCoupons(currentRow.id)">刷新</el-button>
         </el-form-item>
       </el-form>
-      <el-table :data="couponListData" border v-loading="couponLoading" style="width:100%" max-height="400">
+      <el-table :data="couponListData" border v-loading="couponLoading" class="full-width" max-height="400">
         <el-table-column prop="name" label="券名称" min-width="140"></el-table-column>
         <el-table-column prop="type" label="类型" width="80">
           <template slot-scope="scope">
@@ -241,7 +241,7 @@
           <template slot-scope="scope">{{ formatTime(scope.row.usedAt) }}</template>
         </el-table-column>
       <el-pagination
-        style="margin-top: 12px; text-align: right"
+        class="pagination-right-mt12"
         @size-change="onCouponSizeChange"
         @current-change="onCouponPageChange"
         :current-page="couponPagination.current"
@@ -258,7 +258,7 @@
       :visible.sync="usageDialogVisible"
       width="800px"
     >
-      <el-table :data="usageListData" border v-loading="usageLoading" style="width:100%" max-height="400">
+      <el-table :data="usageListData" border v-loading="usageLoading" class="full-width" max-height="400">
         <el-table-column prop="name" label="券名称" min-width="140"></el-table-column>
         <el-table-column prop="type" label="类型" width="80">
           <template slot-scope="scope">
@@ -279,7 +279,7 @@
           <template slot-scope="scope">{{ formatTime(scope.row.usedAt) }}</template>
         </el-table-column>
       <el-pagination
-        style="margin-top: 12px; text-align: right"
+        class="pagination-right-mt12"
         @size-change="onUsageSizeChange"
         @current-change="onUsagePageChange"
         :current-page="usagePagination.current"
@@ -604,4 +604,14 @@ export default {
 }
 </script>
 
+<style scoped>
+.page-container { padding: 20px; }
+.mb-20 { margin-bottom: 20px; }
+.mb-12 { margin-bottom: 12px; }
+.full-width { width: 100%; }
+.w-120 { width: 120px; }
+.pagination-right { margin-top: 20px; text-align: right; }
+.pagination-right-mt12 { margin-top: 12px; text-align: right; }
+</style>
+
 

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -756,6 +756,7 @@ export default {
   display: flex;
   align-items: center;
   gap: 12px;
+  margin-left: auto;
 }
 
 .username {

+ 20 - 8
cfc-web/src/views/Rewards.vue

@@ -9,7 +9,7 @@
         <!-- 奖励列表 -->
         <el-tab-pane label="奖励列表" name="list">
           <!-- 搜索筛选 -->
-          <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+          <el-form :inline="true" :model="searchForm" class="mb-20">
             <el-form-item label="家庭">
               <el-input
                 v-model="searchForm.familyName"
@@ -22,7 +22,7 @@
                   icon="el-icon-close"
                   v-if="searchForm.familyId"
                   @click.stop="clearFamily"
-                  style="padding: 8px"
+                  class="padding-8"
                 ></el-button>
                 <i slot="suffix" class="el-input__icon el-icon-arrow-down" @click.stop="showFamilySelector"></i>
               </el-input>
@@ -43,7 +43,7 @@
 
           <!-- 家庭选择对话框 -->
           <el-dialog title="选择家庭" :visible.sync="familyDialogVisible" width="600px">
-            <el-table :max-height="tableHeight" style="cursor: pointer" :data="familyList" border v-loading="familyLoading" @row-click="selectFamily">
+            <el-table :max-height="tableHeight" class="cursor-pointer" :data="familyList" border v-loading="familyLoading" @row-click="selectFamily">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="name" label="家庭名称" min-width="150"></el-table-column>
               <el-table-column prop="inviteCode" label="邀请码" width="120"></el-table-column>
@@ -53,7 +53,7 @@
                 </template>
               </el-table-column>
             <el-pagination
-              style="margin-top: 15px; text-align: right"
+              class="pagination-mt15"
               @current-change="loadFamilies"
               :current-page="familyPagination.current"
               :page-size="familyPagination.size"
@@ -63,7 +63,7 @@
           </el-dialog>
 
                     <div class="adm-table-wrap">
-<el-table :data="tableData" border v-loading="loading" style="width: 100%" height="100%">
+<el-table :data="tableData" border v-loading="loading" class="full-width" height="100%">
              <el-table-column prop="id" label="ID" width="80">
                <template slot="header">
                  <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -132,7 +132,7 @@
           </div>
 
           <el-pagination
-            style="margin-top: 20px; text-align: right"
+            class="pagination-mt20"
             @size-change="handleSizeChange"
             @current-change="handleCurrentChange"
             :current-page="pagination.current"
@@ -145,11 +145,11 @@
 
         <!-- 奖励模板 -->
         <el-tab-pane label="奖励模板" name="templates">
-          <el-button type="primary" size="small" style="margin-bottom: 15px" @click="showCreateTemplate">
+          <el-button type="primary" size="small" class="mb-15" @click="showCreateTemplate">
             新建模板
           </el-button>
                     <div class="adm-table-wrap">
-<el-table :data="templates" border v-loading="loadingTemplates" style="width: 100%" height="100%">
+<el-table :data="templates" border v-loading="loadingTemplates" class="full-width" height="100%">
             <el-table-column prop="id" label="ID" width="80"></el-table-column>
             <el-table-column prop="title" label="模板名称" min-width="200"></el-table-column>
             <el-table-column prop="pointsRequired" label="所需积分" width="120">
@@ -444,4 +444,16 @@ export default {
 }
 </script>
 
+<style scoped>
+.page-container { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.mb-20 { margin-bottom: 20px; }
+.mb-15 { margin-bottom: 15px; }
+.padding-8 { padding: 8px; }
+.cursor-pointer { cursor: pointer; }
+.pagination-mt15 { margin-top: 15px; text-align: right; }
+.pagination-mt20 { margin-top: 20px; text-align: right; }
+.full-width { width: 100%; }
+</style>
+
 

+ 19 - 8
cfc-web/src/views/TaskTemplates.vue

@@ -7,7 +7,7 @@
       </PageHeader>
 
       <!-- 搜索和筛选 -->
-      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+      <el-form :inline="true" :model="searchForm" class="mb-20">
         <el-form-item label="分类">
           <el-select v-model="searchForm.category" placeholder="全部分类" clearable @change="handleSearch">
             <el-option label="学习" value="学习"></el-option>
@@ -43,7 +43,7 @@
         :data="tableData"
         border
         v-loading="loading"
-        style="width: 100%"
+        class="full-width"
         height="100%"
       >
         <el-table-column prop="id" label="ID" width="80">
@@ -149,7 +149,7 @@
             <el-button
               type="text"
               size="small"
-              style="color: var(--adm-error)"
+              class="text-error"
               @click="handleDelete(scope.row)"
             >
               删除
@@ -161,7 +161,7 @@
 
       <!-- 分页 -->
       <el-pagination
-        style="margin-top: 20px; text-align: right"
+        class="pagination-right"
         @size-change="handleSizeChange"
         @current-change="handleCurrentChange"
         :current-page="pagination.current"
@@ -213,11 +213,11 @@
         </el-form-item>
     <el-form-item label="推荐年龄" prop="recommendedAge">
       <el-input-number v-model="editForm.recommendedAge" :min="0" :max="18" placeholder="推荐年龄"></el-input-number>
-      <span style="margin-left: 10px; color: var(--adm-text-45)">岁及以上</span>
+      <span class="suffix-hint">岁及以上</span>
     </el-form-item>
     <el-form-item label="预计时长" prop="duration">
       <el-input-number v-model="editForm.duration" :min="1" :max="480" placeholder="预计时长"></el-input-number>
-      <span style="margin-left: 10px; color: var(--adm-text-45)">分钟</span>
+      <span class="suffix-hint">分钟</span>
     </el-form-item>
     <el-form-item label="完成方式" prop="completeTypes">
       <el-checkbox-group v-model="editForm.completeTypes">
@@ -243,7 +243,7 @@
     </el-form-item>
     <el-form-item label="周期次数" prop="maxFrequency" v-if="editForm.taskType === 'recurring'">
       <el-input-number v-model="editForm.maxFrequency" :min="1" :max="100" placeholder="周期内最多完成次数"></el-input-number>
-      <span style="margin-left: 10px; color: var(--adm-text-45); font-size: 12px">
+      <span class="suffix-hint-sm">
         超过此次数不计积分
       </span>
     </el-form-item>
@@ -260,7 +260,7 @@
     </el-form-item>
     <el-form-item label="按分类审核" prop="reviewByCategory" v-if="editForm.needReview === 1 && editForm.reviewType === 'parent'">
       <el-switch v-model="editForm.reviewByCategory" :active-value="1" :inactive-value="0"></el-switch>
-      <span style="margin-left: 10px; color: var(--adm-text-45); font-size: 12px">
+      <span class="suffix-hint-sm">
         开启后,将按照分类自动分配审核人
       </span>
     </el-form-item>
@@ -531,4 +531,15 @@ export default {
 }
 </script>
 
+<style scoped>
+.admin-page { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.mb-20 { margin-bottom: 20px; }
+.full-width { width: 100%; }
+.text-error { color: var(--adm-error); }
+.pagination-right { margin-top: 20px; text-align: right; }
+.suffix-hint { margin-left: 10px; color: var(--adm-text-45); }
+.suffix-hint-sm { margin-left: 10px; color: var(--adm-text-45); font-size: 12px; }
+</style>
+
 

+ 19 - 10
cfc-web/src/views/TeacherDashboard.vue

@@ -26,31 +26,31 @@
           <div class="td-quick-actions">
             <div class="td-action-item" @click="$router.push('/teacher-families')">
               <div class="td-action-icon" style="background: #EBF5FF">
-                <i class="el-icon-user" style="color: var(--adm-primary)"></i>
+                <i class="el-icon-user td-action-icon-color-primary"></i>
               </div>
               <span>管理家庭</span>
             </div>
             <div class="td-action-item" @click="$router.push('/teacher-messages')">
               <div class="td-action-icon" style="background: #ECFDF5">
-                <i class="el-icon-chat-line-round" style="color: var(--adm-success)"></i>
+                <i class="el-icon-chat-line-round td-action-icon-color-success"></i>
               </div>
               <span>消息中心</span>
             </div>
             <div class="td-action-item" @click="$router.push('/teacher-orders')">
               <div class="td-action-icon" style="background: #FFFBEB">
-                <i class="el-icon-document" style="color: var(--adm-warning)"></i>
+                <i class="el-icon-document td-action-icon-color-warning"></i>
               </div>
               <span>查看订单</span>
             </div>
             <div class="td-action-item" @click="$router.push('/teacher-assessment')">
               <div class="td-action-icon" style="background: #F5F3FF">
-                <i class="el-icon-edit-outline" style="color: var(--adm-primary)"></i>
+                <i class="el-icon-edit-outline td-action-icon-color-primary"></i>
               </div>
               <span>DAN测评</span>
             </div>
             <div class="td-action-item" @click="$router.push('/teacher-health-plan-review')">
               <div class="td-action-icon" style="background: #FEF3C7">
-                <i class="el-icon-document-checked" style="color: #D97706"></i>
+                <i class="el-icon-document-checked td-action-icon-color-warning-strong"></i>
               </div>
               <span>健康方案审核</span>
             </div>
@@ -65,19 +65,19 @@
           </div>
           <div class="td-today-stats">
             <div class="td-today-stat-item">
-              <div class="td-today-stat-value" style="color: var(--adm-primary)">{{ todayStats.newFamilies }}</div>
+              <div class="td-today-stat-value td-stat-primary">{{ todayStats.newFamilies }}</div>
               <div class="td-today-stat-label">新增家庭</div>
             </div>
             <div class="td-today-stat-item">
-              <div class="td-today-stat-value" style="color: var(--adm-success)">{{ todayStats.completedTasks }}</div>
+              <div class="td-today-stat-value td-stat-success">{{ todayStats.completedTasks }}</div>
               <div class="td-today-stat-label">完成任务</div>
             </div>
             <div class="td-today-stat-item">
-              <div class="td-today-stat-value" style="color: var(--adm-warning)">{{ todayStats.newOrders }}</div>
+              <div class="td-today-stat-value td-stat-warning">{{ todayStats.newOrders }}</div>
               <div class="td-today-stat-label">新订单</div>
             </div>
             <div class="td-today-stat-item">
-              <div class="td-today-stat-value" style="color: var(--adm-primary)">{{ todayStats.pendingReviews }}</div>
+              <div class="td-today-stat-value td-stat-primary">{{ todayStats.pendingReviews }}</div>
               <div class="td-today-stat-label">待审核</div>
             </div>
           </div>
@@ -155,7 +155,7 @@
             <el-button type="text" @click="$router.push('/teacher-orders')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
           </div>
                     <div class="adm-table-wrap">
-<el-table :data="recentOrders" stripe style="width: 100%" class="td-orders-table" height="100%">
+<el-table :data="recentOrders" stripe class="td-orders-table" height="100%">
             <el-table-column prop="orderNo" label="订单号" width="200">
               <template slot-scope="scope">
                 <span class="td-order-no">{{ scope.row.orderNo }}</span>
@@ -321,4 +321,13 @@ export default {
 .td-order-no { color: var(--adm-primary); font-weight: 500; }
 .td-family-cell i { margin-right: 4px; color: var(--adm-text-45); }
 .td-amount-value { font-weight: 600; color: var(--adm-text-88); }
+/* == C 批内联清理工具类 == */
+.td-action-icon-color-primary { color: var(--adm-primary); }
+.td-action-icon-color-success { color: var(--adm-success); }
+.td-action-icon-color-warning { color: var(--adm-warning); }
+.td-action-icon-color-warning-strong { color: #D97706; }
+.td-stat-primary { color: var(--adm-primary); }
+.td-stat-success { color: var(--adm-success); }
+.td-stat-warning { color: var(--adm-warning); }
+.td-orders-table { width: 100%; }
 </style>

+ 18 - 10
cfc-web/src/views/Users.vue

@@ -6,7 +6,7 @@
       </PageHeader>
 
       <!-- 搜索和筛选 -->
-      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+      <el-form :inline="true" :model="searchForm" class="mb-20">
         <el-form-item label="角色">
           <el-select v-model="searchForm.role" placeholder="全部角色" clearable @change="handleSearch">
             <el-option label="家长" value="parent"></el-option>
@@ -60,7 +60,7 @@
         :data="tableData" 
         border 
         v-loading="loading"
-        style="width: 100%"
+        class="full-width"
         height="100%"
       >
         <el-table-column prop="id" label="ID" width="80">
@@ -91,7 +91,7 @@
               :type="getRoleType(r)"
               :text="getRoleText(r)"
               size="small"
-              style="margin-right:4px"
+              class="mr-4"
             />
           </template>
         </el-table-column>
@@ -173,7 +173,7 @@
     </el-dialog>
 
       <el-pagination
-        style="margin-top: 20px; text-align: right"
+        class="pagination-right"
         @size-change="handleSizeChange"
         @current-change="handleCurrentChange"
         :current-page="pagination.current"
@@ -210,7 +210,7 @@
             placeholder="搜索推荐人(昵称/手机号/ID)"
             :remote-method="searchUsersForReferrer"
             :loading="userSearchLoading"
-            style="width: 100%"
+            class="full-width"
           >
             <el-option
               v-for="u in userSearchResults"
@@ -221,7 +221,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="会员等级">
-          <el-select v-model="editForm.memberLevel" clearable placeholder="选择会员等级" style="width: 100%">
+          <el-select v-model="editForm.memberLevel" clearable placeholder="选择会员等级" class="full-width">
             <el-option
               v-for="lv in memberLevelOptions"
               :key="lv.levelCode"
@@ -239,7 +239,7 @@
               :disabled="r.disabled"
             >{{ r.label }}</el-checkbox>
           </el-checkbox-group>
-          <div style="font-size:12px;color:var(--adm-text-45);margin-top:4px">
+          <div class="role-hint">
             平台角色: admin / operator / finance;入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
           </div>
         </el-form-item>
@@ -294,10 +294,10 @@
       </div>
       <el-form :model="grantCouponForm" label-width="100px">
         <el-form-item label="券面值(元)" required>
-          <el-input-number v-model="grantCouponForm.amountYuan" :min="0.01" :step="10" :precision="2" style="width: 200px" />
+          <el-input-number v-model="grantCouponForm.amountYuan" :min="0.01" :step="10" :precision="2" class="w-200" />
         </el-form-item>
         <el-form-item label="有效期止">
-          <el-date-picker v-model="grantCouponForm.validUntil" type="date" value-format="yyyy-MM-dd" placeholder="默认 +30 天" style="width: 100%" />
+          <el-date-picker v-model="grantCouponForm.validUntil" type="date" value-format="yyyy-MM-dd" placeholder="默认 +30 天" class="full-width" />
         </el-form-item>
         <el-form-item label="备注">
           <el-input v-model="grantCouponForm.remark" type="textarea" :rows="2" maxlength="30" show-word-limit placeholder="选填,用于区分该券" />
@@ -306,7 +306,7 @@
           title="该券仅限会员开通/续费/升级订单抵扣,将发放到该用户所在家庭"
           type="info"
           :closable="false"
-          style="margin-bottom: 10px"
+          class="mb-10"
         />
       </el-form>
       <div slot="footer">
@@ -722,4 +722,12 @@ const res = await getUserList(params)
   color: var(--adm-text-45);
   font-size: 12px;
 }
+/* == C 批内联清理工具类 == */
+.mb-20 { margin-bottom: 20px; }
+.mb-10 { margin-bottom: 10px; }
+.mr-4 { margin-right: 4px; }
+.full-width { width: 100%; }
+.w-200 { width: 200px; }
+.pagination-right { margin-top: 20px; text-align: right; }
+.role-hint { font-size: 12px; color: var(--adm-text-45); margin-top: 4px; }
 </style>

+ 18 - 11
cfc-web/src/views/Wishes.vue

@@ -9,7 +9,7 @@
         <!-- 待处理列表 -->
         <el-tab-pane label="待处理" name="pending">
           <!-- 统计卡片 -->
-          <el-row :gutter="20" style="margin-bottom: 20px">
+          <el-row :gutter="20" class="mb-20">
             <el-col :span="6">
               <StatCard label="待定价" :value="draftCount" color="info" icon="el-icon-price-tag" />
             </el-col>
@@ -28,7 +28,7 @@
           <div v-if="draftWishes.length > 0" class="wish-section">
             <SectionTitle title="待定价" />
 <div class="adm-table-wrap">
-<el-table style="width: 100%" height="100%" :data="draftWishes" border v-loading="loading">
+<el-table class="full-width" height="100%" :data="draftWishes" border v-loading="loading">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
               <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
@@ -59,7 +59,7 @@
           <div v-if="pendingExchangeWishes.length > 0" class="wish-section">
             <SectionTitle title="⏳ 待审批兑换" />
                         <div class="adm-table-wrap">
-<el-table :data="pendingExchangeWishes" border v-loading="loading" style="width: 100%" height="100%">
+<el-table :data="pendingExchangeWishes" border v-loading="loading" class="full-width" height="100%">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
               <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
@@ -94,7 +94,7 @@
           <div v-if="approvedWishes.length > 0" class="wish-section">
             <SectionTitle title="待兑现" />
                         <div class="adm-table-wrap">
-<el-table :data="approvedWishes" border v-loading="loading" style="width: 100%" height="100%">
+<el-table :data="approvedWishes" border v-loading="loading" class="full-width" height="100%">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
               <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
@@ -121,7 +121,7 @@
           <div v-if="exchangingWishes.length > 0" class="wish-section">
             <SectionTitle title="待收货确认" />
                         <div class="adm-table-wrap">
-<el-table :data="exchangingWishes" border v-loading="loading" style="width: 100%" height="100%">
+<el-table :data="exchangingWishes" border v-loading="loading" class="full-width" height="100%">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
               <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
@@ -146,7 +146,7 @@
         <!-- 全部心愿 -->
         <el-tab-pane label="全部心愿" name="all">
           <!-- 搜索筛选 -->
-          <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+          <el-form :inline="true" :model="searchForm" class="mb-20">
             <el-form-item label="家庭">
               <el-input
                 v-model="searchForm.familyName"
@@ -159,7 +159,7 @@
                   icon="el-icon-close"
                   v-if="searchForm.familyId"
                   @click.stop="clearFamily"
-                  style="padding: 8px"
+                  class="pt-8"
                 ></el-button>
                 <i slot="suffix" class="el-input__icon el-icon-arrow-down" @click.stop="showFamilySelector"></i>
               </el-input>
@@ -184,12 +184,12 @@
 
           <!-- 家庭选择对话框 -->
           <el-dialog title="选择家庭" :visible.sync="familyDialogVisible" width="600px">
-            <el-table :data="familyList" border v-loading="familyLoading" @row-click="selectFamily" style="cursor: pointer" :max-height="300">
+            <el-table :data="familyList" border v-loading="familyLoading" @row-click="selectFamily" class="cursor-pointer" :max-height="300">
               <el-table-column prop="id" label="ID" width="80"></el-table-column>
               <el-table-column prop="name" label="家庭名称" min-width="150"></el-table-column>
               <el-table-column prop="inviteCode" label="邀请码" width="120"></el-table-column>
             <el-pagination
-              style="margin-top: 15px; text-align: right"
+              class="mb-15-rows"
               @current-change="loadFamilies"
               :current-page="familyPagination.current"
               :page-size="familyPagination.size"
@@ -199,7 +199,7 @@
           </el-dialog>
 
                     <div class="adm-table-wrap">
-<el-table :data="tableData" border v-loading="loading" style="width: 100%" height="100%">
+<el-table :data="tableData" border v-loading="loading" class="full-width" height="100%">
             <el-table-column prop="id" label="ID" width="80">
               <template slot="header">
                 <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -262,7 +262,7 @@
           <
 
           <el-pagination
-            style="margin-top: 20px; text-align: right"
+            class="mb-20-rows"
             @size-change="handleSizeChange"
             @current-change="handleCurrentChange"
             :current-page="pagination.current"
@@ -567,4 +567,11 @@ export default {
   color: var(--adm-text-45);
   font-size: 12px;
 }
+/* == C 批内联清理工具类 == */
+.mb-20 { margin-bottom: 20px; }
+.mb-20-rows { margin-top: 20px; text-align: right; }
+.mb-15-rows { margin-top: 15px; text-align: right; }
+.pt-8 { padding: 8px; }
+.full-width { width: 100%; }
+.cursor-pointer { cursor: pointer; }
 </style>

+ 20 - 10
cfc-web/src/views/admin/Activities.vue

@@ -7,7 +7,7 @@
       
 </PageHeader>
 
-      <div class="filter-bar" style="margin-bottom:12px;gap:12px;">
+      <div class="filter-bar filter-bar-gap">
         <el-input
           v-model="keyword"
           placeholder="搜索活动标题/地点"
@@ -22,7 +22,7 @@
           placeholder="活动状态"
           clearable
           @change="loadActivities"
-          style="width:130px;"
+          class="w-130"
         >
           <el-option label="全部状态" value="" />
           <el-option label="草稿" value="draft" />
@@ -34,7 +34,7 @@
           placeholder="所属维度"
           clearable
           @change="loadActivities"
-          style="width:120px;"
+          class="w-120"
         >
           <el-option label="全部维度" value="" />
           <el-option label="身" value="body" />
@@ -48,7 +48,7 @@
           placeholder="审核状态"
           clearable
           @change="loadActivities"
-          style="width:130px;"
+          class="w-130"
         >
           <el-option label="全部审核状态" value="" />
           <el-option label="待审核" value="pending" />
@@ -59,7 +59,7 @@
         <el-button size="mini" @click="resetSearch">重置</el-button>
       </div>
                 <div class="adm-table-wrap">
-<el-table :data="activities" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+<el-table :data="activities" v-loading="loading" border stripe class="min-width-900" height="100%">
         <el-table-column prop="id" label="ID" width="80">
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -67,8 +67,8 @@
         </el-table-column>
         <el-table-column label="封面" width="80">
           <template slot-scope="{ row }">
-            <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
-            <span v-else style="color:var(--adm-text-45)">无</span>
+            <img v-if="row.coverImage" :src="row.coverImage" class="cover-thumb" />
+            <span v-else class="text-muted">无</span>
           </template>
         </el-table-column>
         <el-table-column prop="title" label="活动标题" min-width="160" show-overflow-tooltip>
@@ -98,7 +98,7 @@
         </el-table-column>
         <el-table-column label="人数" width="90">
           <template slot-scope="{ row }">
-            <el-button type="text" size="mini" @click="showRegistrations(row)" style="padding:0;">
+            <el-button type="text" size="mini" @click="showRegistrations(row)" class="btn-no-pad">
               {{ row.currentParticipants || 0 }}/{{ row.maxParticipants || '不限' }}
             </el-button>
           </template>
@@ -181,10 +181,10 @@
           <template slot-scope="{ row }">
             <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleRegApprove(row)">通过</el-button>
             <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="handleRegReject(row)">拒绝</el-button>
-            <span v-else style="color: var(--adm-text-45);">已处理</span>
+            <span v-else class="text-muted-inline">已处理</span>
           </template>
         </el-table-column>
-      <p v-if="!registrations.length && !regLoading" style="text-align:center;color:var(--adm-text-45);">暂无报名记录</p>
+      <p v-if="!registrations.length && !regLoading" class="empty-hint">暂无报名记录</p>
     </el-dialog>
   
   </el-table>
@@ -451,4 +451,14 @@ export default {
 .header-search {
   width: 220px;
 }
+/* == C 批内联清理工具类 == */
+.filter-bar-gap { margin-bottom: 12px; gap: 12px; }
+.w-130 { width: 130px; }
+.w-120 { width: 120px; }
+.min-width-900 { min-width: 900px; }
+.cover-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
+.text-muted { color: var(--adm-text-45); }
+.btn-no-pad { padding: 0; }
+.text-muted-inline { color: var(--adm-text-45); }
+.empty-hint { text-align: center; color: var(--adm-text-45); }
 </style>

+ 31 - 18
cfc-web/src/views/admin/ActivityEdit.vue

@@ -27,7 +27,7 @@
         :description="statusDescription"
         show-icon
         :closable="false"
-        style="margin-bottom: 16px;"
+        class="mb-16"
       />
 
       <div class="activity-edit-form-wrap">
@@ -41,7 +41,7 @@
         </el-form-item>
 
         <el-form-item label="活动类型" prop="activityType">
-          <el-select v-model="form.activityType" placeholder="请选择活动类型" style="width:100%" :disabled="readonly">
+          <el-select v-model="form.activityType" placeholder="请选择活动类型" class="full-width" :disabled="readonly">
             <el-option label="线下活动" value="offline" />
             <el-option label="线上活动" value="online" />
             <el-option label="主题活动" value="campaign" />
@@ -51,7 +51,7 @@
         <el-form-item label="活动描述" prop="description">
           <div class="rich-editor" :class="{ 'is-disabled': readonly }">
             <Toolbar :editor="editorInstance" :defaultConfig="toolbarConfig" />
-            <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" :disabled="readonly" style="min-height: 300px;" />
+            <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" :disabled="readonly" class="editor-body" />
           </div>
         </el-form-item>
 
@@ -69,10 +69,10 @@
               <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
               <el-button v-else type="primary" plain>+ 上传封面</el-button>
             </el-upload>
-            <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
+            <el-button v-if="form.coverImage" size="mini" type="text" class="ml-10" @click="form.coverImage = ''">清除</el-button>
           </template>
           <img v-else-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
-          <span v-else style="color:var(--adm-text-45)">无封面</span>
+          <span v-else class="text-muted">无封面</span>
         </el-form-item>
 
         <el-form-item label="五维权重">
@@ -90,12 +90,12 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="最大人数">
-              <el-input-number v-model="form.maxParticipants" :min="0" :max="99999" placeholder="0表示不限" style="width:100%" :disabled="readonly" />
+              <el-input-number v-model="form.maxParticipants" :min="0" :max="99999" placeholder="0表示不限" class="full-width" :disabled="readonly" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="价格(元)">
-              <el-input-number v-model="form.priceYuan" :min="0" :precision="2" :step="0.01" placeholder="0表示免费" style="width:100%" :disabled="readonly" />
+              <el-input-number v-model="form.priceYuan" :min="0" :precision="2" :step="0.01" placeholder="0表示免费" class="full-width" :disabled="readonly" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -103,7 +103,7 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="会员价(元)">
-              <el-input-number v-model="form.memberPriceYuan" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" style="width:100%" :disabled="readonly" />
+              <el-input-number v-model="form.memberPriceYuan" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" class="full-width" :disabled="readonly" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -111,12 +111,12 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="开始时间" prop="startTime">
-              <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" style="width:100%" :disabled="readonly" />
+              <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" class="full-width" :disabled="readonly" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="结束时间" prop="endTime">
-              <el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" style="width:100%" :disabled="readonly" />
+              <el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" class="full-width" :disabled="readonly" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -124,7 +124,7 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="可见范围">
-              <el-select v-model="form.visibility" placeholder="请选择可见范围" style="width:100%" :disabled="readonly">
+              <el-select v-model="form.visibility" placeholder="请选择可见范围" class="full-width" :disabled="readonly">
                 <el-option label="公开" value="public" />
                 <el-option label="受限" value="restricted" />
                 <el-option label="全体" value="all" />
@@ -134,7 +134,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="可见对象">
-              <el-select v-model="form.visibleScope" placeholder="请选择可见对象类型" style="width:100%" :disabled="readonly">
+              <el-select v-model="form.visibleScope" placeholder="请选择可见对象类型" class="full-width" :disabled="readonly">
                 <el-option label="不限" value="" />
                 <el-option label="家庭" value="family" />
                 <el-option label="角色" value="role" />
@@ -149,8 +149,8 @@
         </el-form-item>
 
         <el-form-item label="签到积分">
-          <el-input-number v-model="form.checkinPoints" :min="0" :max="9999" placeholder="0使用系统默认" style="width:200px" :disabled="readonly" />
-          <span style="margin-left:8px;color:var(--adm-text-45);font-size:12px;">0表示使用系统默认积分</span>
+          <el-input-number v-model="form.checkinPoints" :min="0" :max="9999" placeholder="0使用系统默认" class="w-200" :disabled="readonly" />
+          <span class="form-hint">0表示使用系统默认积分</span>
         </el-form-item>
       </el-form>
       </div>
@@ -158,11 +158,11 @@
 
     <!-- 创建成功弹窗 -->
     <el-dialog title="创建成功" :visible.sync="successDialogVisible" width="420px" :close-on-click-modal="false" :show-close="false">
-      <div style="text-align:center;padding:10px 0;">
-        <i class="el-icon-circle-check" style="font-size:48px;color:var(--adm-success);"></i>
-        <p style="font-size:16px;margin-top:12px;">活动「{{ form.title }}」已创建成功</p>
+      <div class="success-body">
+        <i class="el-icon-circle-check success-icon"></i>
+        <p class="success-text">活动「{{ form.title }}」已创建成功</p>
       </div>
-      <div slot="footer" style="text-align:center;">
+      <div slot="footer" class="success-footer">
         <el-button @click="handleContinueAdd">继续添加</el-button>
         <el-button @click="$router.push('/activities')">返回列表</el-button>
         <el-button type="primary" @click="handlePublishAndBack" :loading="publishing">直接发布</el-button>
@@ -560,6 +560,19 @@ activityId
 </script>
 
 <style scoped>
+/* == C 批内联清理工具类 == */
+.mb-16 { margin-bottom: 16px; }
+.full-width { width: 100%; }
+.w-200 { width: 200px; }
+.ml-10 { margin-left: 10px; }
+.text-muted { color: var(--adm-text-45); }
+.editor-body { min-height: 300px; }
+.form-hint { margin-left: 8px; color: var(--adm-text-45); font-size: 12px; }
+.success-body { text-align: center; padding: 10px 0; }
+.success-icon { font-size: 48px; color: var(--adm-success); }
+.success-text { font-size: 16px; margin-top: 12px; }
+.success-footer { text-align: center; }
+
 .activity-edit {
   padding: 20px;
   flex: 1;

+ 16 - 11
cfc-web/src/views/admin/BadgeManage.vue

@@ -6,24 +6,24 @@
       </PageHeader>
 
       <!-- 搜索 -->
-      <el-form :model="filters" inline size="small" style="margin-bottom:16px;">
+      <el-form :model="filters" inline size="small" class="mb-16">
         <el-form-item label="名称">
           <el-input v-model="filters.name" placeholder="搜索勋章名称" clearable @input="onSearchInput" />
         </el-form-item>
         <el-form-item label="分类">
-          <el-select v-model="filters.category" placeholder="全部分类" clearable @change="fetchList" style="width:140px;">
+          <el-select v-model="filters.category" placeholder="全部分类" clearable @change="fetchList" class="w-140">
             <el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
           </el-select>
         </el-form-item>
         <el-form-item label="等级">
-          <el-select v-model="filters.level" placeholder="全部等级" clearable @change="fetchList" style="width:140px;">
+          <el-select v-model="filters.level" placeholder="全部等级" clearable @change="fetchList" class="w-140">
             <el-option v-for="c in levelOptions" :key="c.value" :label="c.label" :value="c.value" />
           </el-select>
         </el-form-item>
       </el-form>
 
             <div class="adm-table-wrap">
-<el-table :data="list" border stripe v-loading="loading" empty-text="暂无数据" style="width:100%" height="100%">
+<el-table :data="list" border stripe v-loading="loading" empty-text="暂无数据" class="full-width" height="100%">
         <el-table-column prop="id" label="ID" width="60">
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -69,7 +69,7 @@
           <template slot-scope="{ row }">
             <el-button type="text" @click="editRow(row)">编辑</el-button>
             <el-popconfirm title="确定删除此勋章吗?" @confirm="deleteRow(row)">
-              <el-button type="text" style="color: var(--adm-error-text)" slot="reference">删除</el-button>
+              <el-button type="text" class="text-error" slot="reference">删除</el-button>
             </el-popconfirm>
           </template>
         </el-table-column>
@@ -88,7 +88,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="分类" required>
-              <el-select v-model="dialog.form.category" placeholder="请选择" style="width:100%">
+              <el-select v-model="dialog.form.category" placeholder="请选择" class="full-width">
                 <el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
               </el-select>
             </el-form-item>
@@ -97,14 +97,14 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="等级">
-              <el-select v-model="dialog.form.level" placeholder="请选择" style="width:100%">
+              <el-select v-model="dialog.form.level" placeholder="请选择" class="full-width">
                 <el-option v-for="c in levelOptions" :key="c.value" :label="c.label" :value="c.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="稀有度">
-              <el-select v-model="dialog.form.rarity" placeholder="请选择" style="width:100%">
+              <el-select v-model="dialog.form.rarity" placeholder="请选择" class="full-width">
                 <el-option v-for="c in rarityOptions" :key="c.value" :label="c.label" :value="c.value" />
               </el-select>
             </el-form-item>
@@ -124,19 +124,19 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="触发阈值">
-              <el-input-number v-model="dialog.form.threshold" :min="0" style="width:100%" />
+              <el-input-number v-model="dialog.form.threshold" :min="0" class="full-width" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="排序">
-              <el-input-number v-model="dialog.form.sortOrder" :min="0" style="width:100%" />
+              <el-input-number v-model="dialog.form.sortOrder" :min="0" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="过期天数">
-              <el-input-number v-model="dialog.form.expireDays" :min="0" style="width:100%" />
+              <el-input-number v-model="dialog.form.expireDays" :min="0" class="full-width" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -369,4 +369,9 @@ export default {
 .sort-header:hover {
   color: var(--adm-primary);
 }
+/* == C 批内联清理工具类 == */
+.mb-16 { margin-bottom: 16px; }
+.w-140 { width: 140px; }
+.full-width { width: 100%; }
+.text-error { color: var(--adm-error-text); }
 </style>

+ 23 - 17
cfc-web/src/views/admin/CouponManagement.vue

@@ -110,24 +110,24 @@
           <el-input v-model="form.name" placeholder="请输入优惠券名称" />
         </el-form-item>
         <el-form-item label="类型" required>
-          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
+          <el-select v-model="form.type" placeholder="请选择类型" class="full-width">
             <el-option label="FIXED(满减)" value="FIXED" />
             <el-option label="CASH(无门槛)" value="CASH" />
             <el-option label="DISCOUNT(折扣)" value="DISCOUNT" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="form.type !== 'DISCOUNT'" label="面值(元)" required>
-          <el-input-number v-model="form.valueYuan" :min="0.01" :step="0.5" :precision="2" style="width: 200px" />
+          <el-input-number v-model="form.valueYuan" :min="0.01" :step="0.5" :precision="2" class="w-200" />
         </el-form-item>
         <el-form-item v-if="form.type === 'DISCOUNT'" label="折扣率(千分比)" required>
-          <el-input-number v-model="form.discountRate" :min="1" :max="10000" :step="100" style="width: 200px" />
+          <el-input-number v-model="form.discountRate" :min="1" :max="10000" :step="100" class="w-200" />
           <div class="form-item-tip">9000 = 9折,10000 = 无折扣</div>
         </el-form-item>
         <el-form-item v-if="form.type === 'FIXED'" label="使用门槛(元)">
-          <el-input-number v-model="form.minSpendYuan" :min="0" :step="10" :precision="2" style="width: 200px" />
+          <el-input-number v-model="form.minSpendYuan" :min="0" :step="10" :precision="2" class="w-200" />
         </el-form-item>
         <el-form-item label="适用对象">
-          <el-select v-model="form.applicableTo" placeholder="请选择适用对象" style="width: 100%">
+          <el-select v-model="form.applicableTo" placeholder="请选择适用对象" class="full-width">
             <el-option label="全部商品" value="ALL" />
             <el-option label="会员服务" value="MEMBERSHIP" />
             <el-option label="特定商品" value="PRODUCT" />
@@ -139,11 +139,11 @@
           <div class="form-item-tip">留空 = 全场通用;仅适用对象选"特定商品"时必填</div>
         </el-form-item>
         <el-form-item label="积分兑换价">
-          <el-input-number v-model="form.pointsPrice" :min="0" :step="100" style="width: 200px" />
+          <el-input-number v-model="form.pointsPrice" :min="0" :step="100" class="w-200" />
           <div class="form-item-tip">0 = 不可积分兑换</div>
         </el-form-item>
         <el-form-item label="自动发放规则">
-          <el-select v-model="form.grantType" placeholder="不自动发放" style="width: 100%">
+          <el-select v-model="form.grantType" placeholder="不自动发放" class="full-width">
             <el-option label="不自动发放" value="" />
             <el-option label="JOIN(开通会员赠)" value="JOIN" />
             <el-option label="PERIODIC(周期补发)" value="PERIODIC" />
@@ -151,27 +151,27 @@
           </el-select>
         </el-form-item>
         <el-form-item v-if="form.grantType === 'JOIN' || form.grantType === 'PERIODIC'" label="适用会员等级">
-          <el-select v-model="form.grantLevelCode" placeholder="请选择会员等级" style="width: 100%">
+          <el-select v-model="form.grantLevelCode" placeholder="请选择会员等级" class="full-width">
             <el-option v-for="lv in memberLevelOptions" :key="lv.levelCode" :label="lv.levelName + '(' + lv.levelCode + ')'" :value="lv.levelCode" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="form.grantType === 'PERIODIC'" label="补发周期">
-          <el-select v-model="form.grantPeriod" placeholder="请选择补发周期" style="width: 100%">
+          <el-select v-model="form.grantPeriod" placeholder="请选择补发周期" class="full-width">
             <el-option label="MONTHLY(每月)" value="MONTHLY" />
             <el-option label="QUARTERLY(每季度)" value="QUARTERLY" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="form.grantType && form.grantType !== ''" label="每次发放数量">
-          <el-input-number v-model="form.grantQuantity" :min="1" :step="1" style="width: 200px" />
+          <el-input-number v-model="form.grantQuantity" :min="1" :step="1" class="w-200" />
         </el-form-item>
         <el-form-item label="有效期起" required>
-          <el-date-picker v-model="form.validFrom" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" style="width: 100%" />
+          <el-date-picker v-model="form.validFrom" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" class="full-width" />
         </el-form-item>
         <el-form-item label="有效期止" required>
-          <el-date-picker v-model="form.validUntil" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" style="width: 100%" />
+          <el-date-picker v-model="form.validUntil" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" class="full-width" />
         </el-form-item>
         <el-form-item label="发放总量" required>
-          <el-input-number v-model="form.totalCount" :min="1" :step="100" style="width: 200px" />
+          <el-input-number v-model="form.totalCount" :min="1" :step="100" class="w-200" />
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -181,9 +181,9 @@
     </el-dialog>
 
     <el-dialog title="操作成功" :visible.sync="successDialogVisible" width="400px" :close-on-click-modal="false">
-      <div style="text-align:center;padding:10px 0">
-        <i class="el-icon-success" style="color:var(--adm-success);font-size:32px"></i>
-        <p style="margin:12px 0 0;font-size:16px;color:var(--adm-text-88)">保存成功</p>
+      <div class="success-body">
+        <i class="el-icon-success success-icon"></i>
+        <p class="success-text">保存成功</p>
       </div>
       <div slot="footer">
         <el-button type="primary" @click="handleContinue">继续添加</el-button>
@@ -194,7 +194,7 @@
     <el-dialog title="批量发放优惠券" :visible.sync="batchDialogVisible" width="500px">
       <el-form label-width="120px">
         <el-form-item label="选择优惠券" required>
-          <el-select v-model="batchCouponId" placeholder="请选择优惠券" style="width: 100%">
+          <el-select v-model="batchCouponId" placeholder="请选择优惠券" class="full-width">
             <el-option v-for="c in couponOptions" :key="c.id" :label="c.name" :value="c.id" />
           </el-select>
         </el-form-item>
@@ -487,4 +487,10 @@ export default {
 
 <style scoped>
 .coupon-management { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.full-width { width: 100%; }
+.w-200 { width: 200px; }
+.success-body { text-align: center; padding: 10px 0; }
+.success-icon { color: var(--adm-success); font-size: 32px; }
+.success-text { margin: 12px 0 0; font-size: 16px; color: var(--adm-text-88); }
 </style>

+ 8 - 3
cfc-web/src/views/admin/DanExecution.vue

@@ -4,7 +4,7 @@
       <PageHeader slot="header" title="DAN测评服务执行管理">
         
         <div class="filter-bar">
-          <el-select v-model="statusFilter" placeholder="执行状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+          <el-select v-model="statusFilter" placeholder="执行状态" clearable @change="handleSearch" class="w-140 mr-10">
             <el-option label="全部状态" value="" />
             <el-option label="进行中" value="pending" />
             <el-option label="已完成" value="completed" />
@@ -41,7 +41,7 @@
               <i :class="executionIconClass(row.plannerStatus)" :style="{ color: executionIconColor(row.plannerStatus) }"></i>
               <StatusTag size="mini" :type="executionStatusType(row.plannerStatus)" :text="executionStatusLabel(row.plannerStatus)" />
             </div>
-            <span v-else style="color: var(--adm-text-45);">—</span>
+            <span v-else class="text-muted">—</span>
           </template>
         </el-table-column>
         <el-table-column label="用户确认" width="110">
@@ -118,7 +118,7 @@
           <el-descriptions-item label="更新时间">{{ formatTime(currentRecord.updatedAt) }}</el-descriptions-item>
         </el-descriptions>
 
-        <div v-if="currentRecord.assessorSettlement || currentRecord.plannerSettlement" style="margin-top: 20px;">
+        <div v-if="currentRecord.assessorSettlement || currentRecord.plannerSettlement" class="mt-20">
           <h4>结算信息</h4>
           <el-descriptions :column="2" border size="small">
             <el-descriptions-item v-if="currentRecord.assessorSettlement" label="评估师结算金额">
@@ -311,4 +311,9 @@ export default {
   margin-top: 20px;
   text-align: right;
 }
+/* == C 批内联清理工具类 == */
+.w-140 { width: 140px; }
+.mr-10 { margin-right: 10px; }
+.text-muted { color: var(--adm-text-45); }
+.mt-20 { margin-top: 20px; }
 </style>

+ 10 - 5
cfc-web/src/views/admin/DanSettlement.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="dan-settlement admin-page">
     <!-- 统计卡片 -->
-    <el-row :gutter="20" style="margin-bottom: 20px;">
+    <el-row :gutter="20" class="mb-20">
       <el-col :span="8">
         <StatCard color="primary" icon="el-icon-money" label="总结算金额({{ stats.totalCount }} 笔)" :value="'¥' + (stats.totalAmount / 100).toFixed(2)" />
       </el-col>
@@ -18,13 +18,13 @@
       <PageHeader slot="header" title="结算记录管理">
         
         <div class="filter-bar">
-          <el-select v-model="statusFilter" placeholder="结算状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+          <el-select v-model="statusFilter" placeholder="结算状态" clearable @change="handleSearch" class="w-140 mr-10">
             <el-option label="全部状态" value="" />
             <el-option label="待结算" value="pending" />
             <el-option label="已结算" value="settled" />
             <el-option label="结算失败" value="failed" />
           </el-select>
-          <el-select v-model="providerTypeFilter" placeholder="服务类型" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+          <el-select v-model="providerTypeFilter" placeholder="服务类型" clearable @change="handleSearch" class="w-140 mr-10">
             <el-option label="全部类型" value="" />
             <el-option label="评估师" value="assessor" />
             <el-option label="规划师" value="planner" />
@@ -61,7 +61,7 @@
         </el-table-column>
         <el-table-column label="结算金额" width="120">
           <template slot-scope="{ row }">
-            <span class="adm-num" style="font-weight: bold; color: var(--adm-primary);">
+            <span class="adm-num text-primary-bold">
               ¥{{ (row.commissionAmount / 100).toFixed(2) }}
             </span>
           </template>
@@ -130,7 +130,7 @@
           <el-descriptions-item label="服务费">¥{{ (currentRecord.serviceFee / 100).toFixed(2) }}</el-descriptions-item>
           <el-descriptions-item label="佣金比例">{{ currentRecord.commissionRate }}%</el-descriptions-item>
           <el-descriptions-item label="结算金额">
-            <span class="adm-num" style="font-weight: bold; color: var(--adm-primary);">
+            <span class="adm-num text-primary-bold">
               ¥{{ (currentRecord.commissionAmount / 100).toFixed(2) }}
             </span>
           </el-descriptions-item>
@@ -320,4 +320,9 @@ export default {
   margin-top: 20px;
   text-align: right;
 }
+/* == C 批内联清理工具类 == */
+.mb-20 { margin-bottom: 20px; }
+.w-140 { width: 140px; }
+.mr-10 { margin-right: 10px; }
+.text-primary-bold { font-weight: bold; color: var(--adm-primary); }
 </style>

+ 12 - 9
cfc-web/src/views/admin/DiscountRuleManagement.vue

@@ -80,39 +80,39 @@
           <el-input v-model="form.name" placeholder="请输入规则名称" />
         </el-form-item>
         <el-form-item label="折扣类型" required>
-          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
+          <el-select v-model="form.type" placeholder="请选择类型" class="full-width">
             <el-option label="PERCENT - 按百分比减免" value="PERCENT" />
             <el-option label="FIXED - 固定金额减免" value="FIXED" />
             <el-option label="THRESHOLD - 满减折扣" value="THRESHOLD" />
           </el-select>
         </el-form-item>
         <el-form-item label="折扣值" required>
-          <el-input-number v-model="form.value" :min="0" :step="1" style="width: 200px" />
+          <el-input-number v-model="form.value" :min="0" :step="1" class="w-200" />
           <span class="form-item-tip" v-if="form.type === 'PERCENT'">(百分比,如20表示减免20%)</span>
           <span class="form-item-tip" v-else>(金额,单位:分)</span>
         </el-form-item>
         <el-form-item label="满减门槛" v-if="form.type === 'THRESHOLD'">
-          <el-input-number v-model="form.thresholdAmount" :min="0" :step="100" style="width: 200px" />
+          <el-input-number v-model="form.thresholdAmount" :min="0" :step="100" class="w-200" />
           <span class="form-item-tip">(达到此金额后方可享受折扣,单位:分)</span>
         </el-form-item>
         <el-form-item label="开始时间">
-          <el-date-picker v-model="form.startTimeStr" type="datetime" placeholder="选择开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" />
+          <el-date-picker v-model="form.startTimeStr" type="datetime" placeholder="选择开始时间" value-format="yyyy-MM-dd HH:mm:ss" class="full-width" />
         </el-form-item>
         <el-form-item label="结束时间">
-          <el-date-picker v-model="form.endTimeStr" type="datetime" placeholder="选择结束时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" />
+          <el-date-picker v-model="form.endTimeStr" type="datetime" placeholder="选择结束时间" value-format="yyyy-MM-dd HH:mm:ss" class="full-width" />
         </el-form-item>
         <el-form-item label="最大使用次数">
-          <el-input-number v-model="form.maxUses" :min="0" :step="1" style="width: 200px" />
+          <el-input-number v-model="form.maxUses" :min="0" :step="1" class="w-200" />
           <span class="form-item-tip">(0 表示不限)</span>
         </el-form-item>
         <el-form-item label="适用类目ID">
-          <el-input-number v-model="form.categoryId" :min="1" :step="1" style="width: 200px" placeholder="留空=全部类目" />
+          <el-input-number v-model="form.categoryId" :min="1" :step="1" class="w-200" placeholder="留空=全部类目" />
         </el-form-item>
         <el-form-item label="适用体系ID">
-          <el-input-number v-model="form.distributionSystemId" :min="1" :step="1" style="width: 200px" placeholder="留空=全部体系" />
+          <el-input-number v-model="form.distributionSystemId" :min="1" :step="1" class="w-200" placeholder="留空=全部体系" />
         </el-form-item>
         <el-form-item label="适用商品ID">
-          <el-input v-model="form.productIds" placeholder="逗号分隔的商品ID,如1,2,3;留空=全部商品" style="width: 100%" />
+          <el-input v-model="form.productIds" placeholder="逗号分隔的商品ID,如1,2,3;留空=全部商品" class="full-width" />
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -283,4 +283,7 @@ export default {
   margin-top: 16px;
   text-align: right;
 }
+/* == C 批内联清理工具类 == */
+.full-width { width: 100%; }
+.w-200 { width: 200px; }
 </style>

+ 17 - 8
cfc-web/src/views/admin/EnergyBehaviorConfig.vue

@@ -2,8 +2,8 @@
   <div class="energy-behavior-config admin-page">
     <PageHeader title="能量行为配置">
       <div class="filters">
-        <el-input v-model="keyword" placeholder="搜索行为名称/编码..." prefix-icon="el-icon-search" style="width:260px" clearable @keyup.enter.native="loadList" @clear="loadList" />
-        <el-select v-model="enabledFilter" placeholder="状态筛选" @change="loadList" style="width: 140px;" clearable>
+        <el-input v-model="keyword" placeholder="搜索行为名称/编码..." prefix-icon="el-icon-search" class="w-260" clearable @keyup.enter.native="loadList" @clear="loadList" />
+        <el-select v-model="enabledFilter" placeholder="状态筛选" @change="loadList" class="w-140" clearable>
           <el-option label="全部" value="" />
           <el-option label="已启用" value="1" />
           <el-option label="已禁用" value="0" />
@@ -40,7 +40,7 @@
       </el-table-column>
       <el-table-column label="维度分配" min-width="200">
         <template slot-scope="{ row }">
-          <span v-if="!row.dimensionAssign" style="color:var(--adm-text-45)">未配置</span>
+          <span v-if="!row.dimensionAssign" class="text-muted">未配置</span>
           <el-tag v-for="d in parseDimAssign(row.dimensionAssign)" :key="d.dim" size="mini" :style="{ background: dimColor(d.dim), color: '#fff', marginRight: '4px' }">
             {{ dimLabel(d.dim) }} {{ d.ratio }}%
           </el-tag>
@@ -94,7 +94,7 @@
           </el-col>
           <el-col :span="8">
             <el-form-item label="值来源">
-              <el-select v-model="form.amountSource" style="width:100%">
+              <el-select v-model="form.amountSource" class="full-width">
                 <el-option label="固定值" value="fixed" />
                 <el-option label="跟随积分" value="points" />
                 <el-option label="跟随金额" value="order_amount" />
@@ -121,12 +121,12 @@
           </el-col>
         </el-row>
         <el-form-item label="维度分配">
-          <div v-for="(d, i) in dimAssignList" :key="i" style="display:flex;align-items:center;margin-bottom:8px;gap:8px">
-            <el-select v-model="d.dim" style="width:120px" @change="normalizeDimRatio">
+          <div v-for="(d, i) in dimAssignList" :key="i" class="dim-row">
+            <el-select v-model="d.dim" class="w-120" @change="normalizeDimRatio">
               <el-option v-for="dim in dimensions" :key="dim.code" :label="dim.name" :value="dim.code" />
             </el-select>
-            <el-slider v-model="d.ratio" :min="0" :max="100" style="flex:1" />
-            <span style="width:40px;text-align:right">{{ d.ratio }}%</span>
+            <el-slider v-model="d.ratio" :min="0" :max="100" class="flex-1" />
+            <span class="ratio-label">{{ d.ratio }}%</span>
             <el-button type="danger" size="mini" icon="el-icon-delete" circle @click="removeDim(i)" v-if="dimAssignList.length > 1" />
           </div>
           <el-button size="mini" @click="addDim" v-if="dimAssignList.length < 5">+ 添加维度</el-button>
@@ -251,4 +251,13 @@ export default {
 
 <style scoped>
 .filters { display: flex; gap: 10px; align-items: center; }
+/* == C 批内联清理工具类 == */
+.w-260 { width: 260px; }
+.w-140 { width: 140px; }
+.w-120 { width: 120px; }
+.text-muted { color: var(--adm-text-45); }
+.full-width { width: 100%; }
+.dim-row { display: flex; align-items: center; margin-bottom: 8px; gap: 8px; }
+.flex-1 { flex: 1; }
+.ratio-label { width: 40px; text-align: right; }
 </style>

+ 35 - 18
cfc-web/src/views/admin/FileManage.vue

@@ -2,7 +2,7 @@
   <div class="file-manage">
     <el-card>
       <PageHeader slot="header" title="文件管理" />
-      <el-row :gutter="16" style="margin-bottom:16px">
+      <el-row :gutter="16" class="mb-16">
         <el-col :span="6">
           <StatCard label="总文件数" :value="stats.total || 0" />
         </el-col>
@@ -11,7 +11,7 @@
         </el-col>
         <el-col :span="12">
           <el-card shadow="never" class="stat-card">
-            <div style="display:flex;gap:12px;flex-wrap:wrap">
+            <div class="stat-chips">
               <StatusTag v-for="c in stats.byCategory || []" :key="c.category" type="primary" :text="c.category + ': ' + c.cnt" size="small" />
             </div>
             <div class="stat-label">分类分布</div>
@@ -20,7 +20,7 @@
       </el-row>
 
       <div class="filter-bar">
-        <el-select v-model="query.category" placeholder="分类" clearable style="width:130px">
+        <el-select v-model="query.category" placeholder="分类" clearable class="w-130">
           <el-option label="全部" value=""></el-option>
           <el-option label="articles" value="articles"></el-option>
           <el-option label="kb-images" value="kb-images"></el-option>
@@ -29,7 +29,7 @@
           <el-option label="health-reports" value="health-reports"></el-option>
           <el-option label="general" value="general"></el-option>
         </el-select>
-        <el-select v-model="query.fileType" placeholder="文件类型" clearable style="width:120px">
+        <el-select v-model="query.fileType" placeholder="文件类型" clearable class="w-120">
           <el-option label="全部" value=""></el-option>
           <el-option label="image" value="image"></el-option>
           <el-option label="pdf" value="pdf"></el-option>
@@ -37,16 +37,16 @@
           <el-option label="audio" value="audio"></el-option>
           <el-option label="other" value="other"></el-option>
         </el-select>
-        <el-select v-model="query.tagIds" placeholder="标签" multiple clearable style="width:200px">
+        <el-select v-model="query.tagIds" placeholder="标签" multiple clearable class="w-200">
           <el-option v-for="t in tagOptions" :key="t.id" :label="t.name" :value="t.id"></el-option>
         </el-select>
-        <el-input v-model="query.search" placeholder="搜索文件名/描述" style="width:200px" @keyup.enter.native="loadData"></el-input>
-        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
+        <el-input v-model="query.search" placeholder="搜索文件名/描述" class="w-200" @keyup.enter.native="loadData"></el-input>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" class="w-240"></el-date-picker>
         <el-button type="primary" @click="loadData">查询</el-button>
       </div>
 
             <div class="adm-table-wrap">
-<el-table :data="tableData" border stripe v-loading="loading" style="margin-top:16px" height="100%">
+<el-table :data="tableData" border stripe v-loading="loading" class="mt-16" height="100%">
         <el-table-column prop="id" label="ID" width="70">
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -54,11 +54,11 @@
         </el-table-column>
         <el-table-column label="预览" width="70">
           <template slot-scope="scope">
-            <el-image v-if="scope.row.fileType === 'image'" :src="scope.row.url" style="width:40px;height:40px;border-radius:4px" fit="cover" :preview-src-list="[scope.row.url]"></el-image>
-            <i v-else-if="scope.row.fileType === 'pdf'" class="el-icon-document" style="font-size:28px;color:var(--adm-error);display:block;text-align:center"></i>
-            <i v-else-if="scope.row.fileType === 'video'" class="el-icon-video-camera" style="font-size:28px;color:var(--adm-primary);display:block;text-align:center"></i>
-            <i v-else-if="scope.row.fileType === 'audio'" class="el-icon-headset" style="font-size:28px;color:var(--adm-success);display:block;text-align:center"></i>
-            <i v-else class="el-icon-document" style="font-size:28px;color:var(--adm-text-45);display:block;text-align:center"></i>
+            <el-image v-if="scope.row.fileType === 'image'" :src="scope.row.url" class="file-thumb" fit="cover" :preview-src-list="[scope.row.url]"></el-image>
+            <i v-else-if="scope.row.fileType === 'pdf'" class="el-icon-document file-icon fi-error"></i>
+            <i v-else-if="scope.row.fileType === 'video'" class="el-icon-video-camera file-icon fi-primary"></i>
+            <i v-else-if="scope.row.fileType === 'audio'" class="el-icon-headset file-icon fi-success"></i>
+            <i v-else class="el-icon-document file-icon fi-muted"></i>
           </template>
         </el-table-column>
         <el-table-column prop="originalFilename" label="文件名" min-width="180" show-overflow-tooltip>
@@ -87,7 +87,7 @@
         </el-table-column>
         <el-table-column label="标签" min-width="140">
           <template slot-scope="scope">
-            <el-tag v-for="t in (scope.row.tags || [])" :key="t.id" size="mini" style="margin-right:4px;margin-bottom:2px;border:0" :style="{ backgroundColor: t.color + '22', color: t.color }">{{ t.name }}</el-tag>
+            <el-tag v-for="t in (scope.row.tags || [])" :key="t.id" size="mini" class="tag-chip" :style="{ backgroundColor: t.color + '22', color: t.color }">{{ t.name }}</el-tag>
             <span v-if="!scope.row.tags || scope.row.tags.length === 0" class="no-tag">-</span>
           </template>
         </el-table-column>
@@ -105,13 +105,13 @@
         <el-table-column label="操作" width="120" fixed="right">
           <template slot-scope="scope">
             <el-button size="mini" type="text" @click="openEdit(scope.row)">编辑</el-button>
-            <el-button size="mini" type="text" style="color:var(--adm-error)" @click="handleDelete(scope.row)">删除</el-button>
+            <el-button size="mini" type="text" class="text-error" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
       </div>
 
-      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="query.page" :page-size="query.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" style="margin-top:16px"></el-pagination>
+      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="query.page" :page-size="query.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" class="mt-16"></el-pagination>
     </el-card>
 
     <el-dialog title="编辑文件" :visible.sync="editVisible" width="500px">
@@ -124,7 +124,7 @@
         </el-form-item>
         <el-form-item label="描述"><el-input v-model="editForm.description" type="textarea" :rows="3" maxlength="500"></el-input></el-form-item>
         <el-form-item label="标签">
-          <el-select v-model="editForm.tagIds" multiple placeholder="选择标签" style="width:100%">
+          <el-select v-model="editForm.tagIds" multiple placeholder="选择标签" class="full-width">
             <el-option v-for="t in tagOptions" :key="t.id" :label="t.name" :value="t.id">
               <span :style="{ color: t.color }">{{ t.name }}</span>
             </el-option>
@@ -208,8 +208,25 @@ export default {
 </script>
 
 <style scoped>
-.filter-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
+.filter-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
 .stat-card { text-align: center; }
 .stat-label { font-size: 12px; color: var(--adm-text-45); margin-top: 4px; }
 .no-tag { color: var(--adm-text-25); }
+/* == C 批内联清理工具类 == */
+.w-130 { width: 130px; }
+.w-120 { width: 120px; }
+.w-200 { width: 200px; }
+.w-240 { width: 240px; }
+.full-width { width: 100%; }
+.mt-16 { margin-top: 16px; }
+.mb-16 { margin-bottom: 16px; }
+.stat-chips { display: flex; gap: 12px; flex-wrap: wrap; }
+.file-thumb { width: 40px; height: 40px; border-radius: 4px; }
+.file-icon { font-size: 28px; display: block; text-align: center; }
+.fi-error { color: var(--adm-error); }
+.fi-primary { color: var(--adm-primary); }
+.fi-success { color: var(--adm-success); }
+.fi-muted { color: var(--adm-text-45); }
+.tag-chip { margin-right: 4px; margin-bottom: 2px; border: 0; }
+.text-error { color: var(--adm-error); }
 </style>

+ 24 - 21
cfc-web/src/views/admin/Foods.vue

@@ -11,7 +11,7 @@
             prefix-icon="el-icon-search"
             clearable
             size="mini"
-            style="width: 200px; margin-right: 8px;"
+            class="w-200 mr-8"
             @keyup.enter.native="handleSearch"
             @clear="handleSearch"
           />
@@ -20,7 +20,7 @@
             placeholder="分类筛选"
             size="mini"
             clearable
-            style="width: 140px; margin-right: 8px;"
+            class="w-140 mr-8"
             @change="handleSearch"
           >
             <el-option label="全部分类" value="" />
@@ -48,13 +48,13 @@
         border
         stripe
         size="mini"
-        style="width: 100%"
+        class="full-width"
         height="100%"
       >
         <el-table-column type="index" label="#" width="50" align="center" />
         <el-table-column prop="name" label="名称" min-width="120" fixed :class-name="sortClass('name')">
           <template slot-scope="{ row }">
-            <span style="font-weight: 500;">{{ row.name }}</span>
+            <span class="num-val">{{ row.name }}</span>
           </template>
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('name',$event)" @dblclick.stop="onSortToggle('name')">名称<span v-if="sortIndex('name') !== null" class="sort-index" :class="sortClass('name') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('name') }}</span>{{ sortIcon('name') }}</span>
@@ -147,7 +147,7 @@
               </el-button>
               <el-dropdown-menu slot="dropdown">
                 <el-dropdown-item command="detail" icon="el-icon-view">查看详情</el-dropdown-item>
-                <el-dropdown-item command="delete" icon="el-icon-delete" divided style="color: var(--adm-error);">删除</el-dropdown-item>
+                <el-dropdown-item command="delete" icon="el-icon-delete" divided class="text-error">删除</el-dropdown-item>
               </el-dropdown-menu>
             </el-dropdown>
           </template>
@@ -184,7 +184,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="分类">
-              <el-select v-model="form.category" style="width:100%" placeholder="选择分类">
+              <el-select v-model="form.category" class="full-width" placeholder="选择分类">
                 <el-option v-for="opt in categoryOptions" :key="opt" :label="opt" :value="opt" />
               </el-select>
             </el-form-item>
@@ -193,51 +193,51 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="热量(kcal/100g)">
-              <el-input-number v-model="form.calories" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.calories" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="能量(kJ/100g)">
-              <el-input-number v-model="form.energyKj" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.energyKj" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="20">
           <el-col :span="8">
             <el-form-item label="蛋白质(g)">
-              <el-input-number v-model="form.protein" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.protein" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="碳水(g)">
-              <el-input-number v-model="form.carbs" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.carbs" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="脂肪(g)">
-              <el-input-number v-model="form.fat" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.fat" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="20">
           <el-col :span="8">
             <el-form-item label="膳食纤维(g)">
-              <el-input-number v-model="form.fiber" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.fiber" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="淀粉(g)">
-              <el-input-number v-model="form.starch" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.starch" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="胆固醇(mg)">
-              <el-input-number v-model="form.cholesterol" :min="0" :precision="1" style="width:100%" />
+              <el-input-number v-model="form.cholesterol" :min="0" :precision="1" class="full-width" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-form-item label="单位">
-          <el-input v-model="form.unit" placeholder="如: 克、个、份" style="width: 200px;" />
+          <el-input v-model="form.unit" placeholder="如: 克、个、份" class="w-200" />
         </el-form-item>
         <el-form-item label="应季月份">
           <el-checkbox-group v-model="form.months">
@@ -277,7 +277,7 @@
         <el-descriptions-item label="排序">{{ detailData.sortOrder != null ? detailData.sortOrder : '-' }}</el-descriptions-item>
         <el-descriptions-item label="应季月份" :span="2">
           <span v-if="detailData.months && detailData.months.length">{{ detailData.months.map(function(m) { return m + '月' }).join('、') }}</span>
-          <span v-else style="color:var(--adm-text-45)">未设置</span>
+          <span v-else class="text-muted">未设置</span>
         </el-descriptions-item>
       </el-descriptions>
     </el-dialog>
@@ -508,30 +508,33 @@ export default {
   flex-wrap: wrap;
   gap: 4px;
 }
-
 .pagination-wrap {
   display: flex;
   justify-content: flex-end;
   padding: 16px 0 0;
 }
-
 .num-val {
   font-weight: 500;
   font-family: 'Helvetica Neue', Arial, sans-serif;
 }
-
 .num-unit {
   font-size: 11px;
   color: var(--adm-text-45);
   margin-left: 2px;
 }
-
 .el-checkbox.is-bordered {
   margin-right: 6px;
   margin-bottom: 4px;
 }
-
 .el-descriptions {
   margin-top: 8px;
 }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-140 { width: 140px; }
+.full-width { width: 100%; }
+.ml-8 { margin-left: 8px; }
+.mr-8 { margin-right: 8px; }
+.text-error { color: var(--adm-error); }
+.text-muted { color: var(--adm-text-45); }
 </style>

+ 17 - 10
cfc-web/src/views/admin/GrowthTaskManagement.vue

@@ -2,8 +2,8 @@
   <div class="growth-task admin-page">
     <el-card>
       <PageHeader slot="header" title="成长任务管理">
-        <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
-        <el-select v-model="filter.status" placeholder="状态筛选" size="small" style="width:130px" clearable @change="loadTasks">
+        <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
+        <el-select v-model="filter.status" placeholder="状态筛选" size="small" class="w-130" clearable @change="loadTasks">
           <el-option label="全部状态" value="" />
           <el-option label="开启" value="enabled" />
           <el-option label="关闭" value="disabled" />
@@ -45,9 +45,9 @@
         <el-table-column label="奖励" width="160">
           <template slot-scope="{ row }">
             <div v-if="row.rewardItems && row.rewardItems.length">
-              <StatusTag size="mini" v-for="(ri, i) in row.rewardItems" :key="i" :text="ri.type + ':' + ri.value" style="margin:1px" />
+              <StatusTag size="mini" v-for="(ri, i) in row.rewardItems" :key="i" :text="ri.type + ':' + ri.value" class="m-1" />
             </div>
-            <span v-else style="color:var(--adm-text-45)">—</span>
+            <span v-else class="text-muted">—</span>
           </template>
         </el-table-column>
         <el-table-column label="状态" width="80">
@@ -82,7 +82,7 @@
         :page-size="filter.size"
         layout="total, sizes, prev, pager, next, jumper"
         :total="total"
-        style="margin-top:16px;text-align:right"
+        class="pagination-right"
       />
     </el-card>
 
@@ -98,7 +98,7 @@
         </el-form-item>
 
         <el-form-item label="任务类型">
-          <el-select v-model="form.type" placeholder="选择任务类型" style="width:100%">
+          <el-select v-model="form.type" placeholder="选择任务类型" class="full-width">
             <el-option label="每日任务" value="daily" />
             <el-option label="每周任务" value="weekly" />
             <el-option label="每月任务" value="monthly" />
@@ -109,7 +109,7 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="关联维度">
-              <el-select v-model="form.dimensionCode" placeholder="选择维度" style="width:100%">
+              <el-select v-model="form.dimensionCode" placeholder="选择维度" class="full-width">
                 <el-option label="身" value="body" />
                 <el-option label="心" value="heart" />
                 <el-option label="智" value="mind" />
@@ -120,7 +120,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="触发方式">
-              <el-select v-model="form.triggerType" placeholder="选择触发方式" style="width:100%" clearable>
+              <el-select v-model="form.triggerType" placeholder="选择触发方式" class="full-width" clearable>
                 <el-option label="手动" value="manual" />
                 <el-option label="自动" value="auto" />
                 <el-option label="条件" value="condition" />
@@ -136,12 +136,12 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="任务值">
-              <el-input-number v-model="form.taskValue" :min="0" :step="1" style="width:100%" placeholder="0" />
+              <el-input-number v-model="form.taskValue" :min="0" :step="1" class="full-width" placeholder="0" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="排序">
-              <el-input-number v-model="form.sortOrder" :min="0" :step="1" style="width:100%" placeholder="0" />
+              <el-input-number v-model="form.sortOrder" :min="0" :step="1" class="full-width" placeholder="0" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -366,4 +366,11 @@ export default {
 
 <style scoped>
 .growth-task { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-130 { width: 130px; }
+.m-1 { margin: 1px; }
+.text-muted { color: var(--adm-text-45); }
+.pagination-right { margin-top: 16px; text-align: right; }
+.full-width { width: 100%; }
 </style>

+ 14 - 8
cfc-web/src/views/admin/GuidePopupConfig.vue

@@ -6,10 +6,10 @@
       </PageHeader>
       <el-form inline>
         <el-form-item label="标识">
-          <el-input v-model="query.slug" placeholder="slug" clearable style="width:180px" />
+          <el-input v-model="query.slug" placeholder="slug" clearable class="w-180" />
         </el-form-item>
         <el-form-item label="状态">
-          <el-select v-model="query.status" clearable placeholder="全部" style="width:120px">
+          <el-select v-model="query.status" clearable placeholder="全部" class="w-120">
             <el-option label="启用" :value="1" />
             <el-option label="禁用" :value="0" />
           </el-select>
@@ -82,15 +82,15 @@
         </el-form-item>
         <SectionTitle title="步骤(留空则单步弹窗)" />
         <div v-for="(step, idx) in form.steps" :key="idx" class="step-row">
-          <el-input v-model="step.title" placeholder="步骤标题" style="width:160px" />
-          <el-input v-model="step.icon" placeholder="图标" style="width:100px" />
-          <el-input v-model="step.desc" placeholder="步骤描述" style="flex:1" />
+          <el-input v-model="step.title" placeholder="步骤标题" class="w-160" />
+          <el-input v-model="step.icon" placeholder="图标" class="w-100" />
+          <el-input v-model="step.desc" placeholder="步骤描述" class="flex-1" />
           <el-button type="text" @click="removeStep(idx)">删除</el-button>
         </div>
         <el-button type="text" @click="addStep">+ 添加步骤</el-button>
         <SectionTitle title="触发条件" />
         <div v-for="(trg, idx) in form.triggers" :key="'t' + idx" class="step-row">
-          <el-select v-model="trg.triggerType" placeholder="触发类型" style="width:160px">
+          <el-select v-model="trg.triggerType" placeholder="触发类型" class="w-160">
             <el-option label="首次登录" value="first_login" />
             <el-option label="进入页面" value="page_enter" />
             <el-option label="完成任务" value="task_complete" />
@@ -98,8 +98,8 @@
             <el-option label="每日活跃" value="daily_active" />
             <el-option label="切换角色" value="role_change" />
           </el-select>
-          <el-input v-model="trg.targetPage" placeholder="目标页面" style="width:160px" />
-          <el-input v-model="trg.minRole" placeholder="角色 parent/child/teacher" style="width:160px" />
+          <el-input v-model="trg.targetPage" placeholder="目标页面" class="w-160" />
+          <el-input v-model="trg.minRole" placeholder="角色 parent/child/teacher" class="w-160" />
           <el-button type="text" @click="removeTrigger(idx)">删除</el-button>
         </div>
         <el-button type="text" @click="addTrigger">+ 添加触发条件</el-button>
@@ -178,4 +178,10 @@ export default {
 .step-row { display: flex; align-items: center; margin-bottom: 8px; }
 .step-row .el-input, .step-row .el-select { margin-right: 8px; }
 .drawer-footer { margin-top: 16px; text-align: right; }
+.flex-1 { flex: 1; }
+/* == C 批内联清理工具类 == */
+.w-180 { width: 180px; }
+.w-120 { width: 120px; }
+.w-160 { width: 160px; }
+.w-100 { width: 100px; }
 </style>

+ 34 - 19
cfc-web/src/views/admin/HomeBannerManage.vue

@@ -7,13 +7,13 @@
       
 </PageHeader>
 
-      <div class="filter-bar" style="margin-bottom:12px;">
-        <el-select v-model="filterStatus" placeholder="启用状态" @change="loadList" clearable style="width:120px;" clearable>
+      <div class="filter-bar">
+        <el-select v-model="filterStatus" placeholder="启用状态" @change="loadList" clearable class="w-120" clearable>
           <el-option label="全部状态" value="" />
           <el-option label="已启用" value="1" />
           <el-option label="已禁用" value="0" />
         </el-select>
-        <el-select v-model="filterType" placeholder="类型" @change="loadList" clearable style="width:120px;">
+        <el-select v-model="filterType" placeholder="类型" @change="loadList" clearable class="w-120">
           <el-option label="全部类型" value="" />
           <el-option label="活动" value="activity" />
           <el-option label="商品" value="product" />
@@ -50,13 +50,13 @@
               <div v-else-if="row.bannerType === 'link' && row.linkUrl">
                 <StatusTag size="mini" type="info" :text="row.linkUrl" />
               </div>
-              <div v-else style="color:var(--adm-text-25)">-</div>
+              <div v-else class="text-faint">-</div>
             </template>
           </el-table-column>
           <el-table-column label="封面" width="80">
             <template slot-scope="{ row }">
-              <img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:35px;object-fit:cover;border-radius:4px;" />
-              <span v-else style="color:var(--adm-text-45)">无</span>
+              <img v-if="row.coverImage" :src="row.coverImage" class="banner-thumb" />
+              <span v-else class="text-muted">无</span>
             </template>
           </el-table-column>
           <el-table-column label="排序" width="80">
@@ -67,7 +67,7 @@
           <el-table-column label="投放时间" width="180">
             <template slot-scope="{ row }">
               <span v-if="row.startTime || row.endTime">{{ formatDate(row.startTime) }} ~ {{ formatDate(row.endTime) }}</span>
-              <span v-else style="color:var(--adm-text-25)">不限</span>
+              <span v-else class="text-faint">不限</span>
             </template>
           </el-table-column>
           <el-table-column label="状态" width="80">
@@ -87,7 +87,7 @@
       </el-table>
       </div>
 
-      <div class="pagination-wrap" style="margin-top:16px;text-align:right;">
+      <div class="pagination-wrap pager-right">
         <el-pagination
           @current-change="handlePageChange"
           :current-page="page"
@@ -107,7 +107,7 @@
         </el-form-item>
 
         <el-form-item label="类型" prop="bannerType">
-          <el-select v-model="form.bannerType" placeholder="选择类型" style="width:100%;">
+          <el-select v-model="form.bannerType" placeholder="选择类型" class="full-width">
             <el-option label="活动" value="activity" />
             <el-option label="商品" value="product" />
             <el-option label="文章" value="article" />
@@ -124,7 +124,7 @@
             :remote="true"
             :remote-method="searchActivities"
             :loading="activityLoading"
-            style="width:100%"
+            class="full-width"
             clearable
           >
             <el-option v-for="item in activityOptions" :key="item.id" :label="item.title + ' (ID:' + item.id + ')'" :value="item.id" />
@@ -141,7 +141,7 @@
             :remote="true"
             :remote-method="searchProducts"
             :loading="productLoading"
-            style="width:100%"
+            class="full-width"
             clearable
           >
             <el-option v-for="item in productOptions" :key="item.id" :label="item.name + ' (¥' + (item.price/100).toFixed(2) + ')'" :value="item.id" />
@@ -158,7 +158,7 @@
             :remote="true"
             :remote-method="searchArticles"
             :loading="articleLoading"
-            style="width:100%"
+            class="full-width"
             clearable
           >
             <el-option v-for="item in articleOptions" :key="item.id" :label="item.title + ' (ID:' + item.id + ')'" :value="item.id" />
@@ -186,15 +186,15 @@
             <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
             <el-button v-else type="primary" plain>+ 上传封面</el-button>
           </el-upload>
-          <div v-if="form.coverImage" style="margin-top:8px;">
-            <el-image :src="form.coverImage" style="width:200px;height:120px;" fit="cover" />
+          <div v-if="form.coverImage" class="mt-8">
+            <el-image :src="form.coverImage" class="cover-lg" fit="cover" />
           </div>
-          <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
-          <div style="margin-top:4px;font-size:12px;color:var(--adm-text-45);">建议尺寸 750×320rpx(约 2.35:1),单张不超过 5MB</div>
+          <el-button v-if="form.coverImage" size="mini" type="text" class="ml-10" @click="form.coverImage = ''">清除</el-button>
+          <div class="form-hint">建议尺寸 750×320rpx(约 2.35:1),单张不超过 5MB</div>
         </el-form-item>
 
         <el-form-item label="排序">
-          <el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:100px;" />
+          <el-input-number v-model="form.sortOrder" :min="0" :max="999" class="w-100" />
         </el-form-item>
 
         <el-form-item label="投放时间">
@@ -204,7 +204,7 @@
             placeholder="开始时间"
             format="yyyy-MM-dd HH:mm:ss"
             value-format="timestamp"
-            style="margin-right:8px;"
+            class="mr-8"
           />
           <span>至</span>
           <el-date-picker
@@ -213,7 +213,7 @@
             placeholder="结束时间"
             format="yyyy-MM-dd HH:mm:ss"
             value-format="timestamp"
-            style="margin-left:8px;"
+            class="ml-8"
           />
         </el-form-item>
 
@@ -641,5 +641,20 @@ export default {
   align-items: center;
   gap: 12px;
   flex-wrap: wrap;
+  margin-bottom: 12px;
 }
+/* == C 批内联清理工具类 == */
+.w-120 { width: 120px; }
+.w-100 { width: 100px; }
+.full-width { width: 100%; }
+.text-faint { color: var(--adm-text-25); }
+.text-muted { color: var(--adm-text-45); }
+.mt-8 { margin-top: 8px; }
+.pager-right { margin-top: 16px; text-align: right; }
+.banner-thumb { width: 50px; height: 35px; object-fit: cover; border-radius: 4px; }
+.cover-lg { width: 200px; height: 120px; }
+.form-hint { margin-top: 4px; font-size: 12px; color: var(--adm-text-45); }
+.ml-10 { margin-left: 10px; }
+.mr-8 { margin-right: 8px; }
+.ml-8 { margin-left: 8px; }
 </style>

+ 17 - 9
cfc-web/src/views/admin/InventoryInboundCreate.vue

@@ -9,39 +9,39 @@
       <div class="inbound-create-form-wrap">
       <el-form label-width="100px">
         <el-form-item label="入库日期">
-          <el-date-picker v-model="form.inboundDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" style="width:200px;" />
+          <el-date-picker v-model="form.inboundDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" class="w-200" />
         </el-form-item>
 
         <el-form-item label="经办人">
-          <el-input v-model="form.operatorName" placeholder="经办人姓名" style="width:200px;" />
+          <el-input v-model="form.operatorName" placeholder="经办人姓名" class="w-200" />
         </el-form-item>
 
         <el-form-item label="供应商">
-          <el-select v-model="form.supplierId" filterable placeholder="搜索选择供应商" style="width:300px;" @change="(v) => { const s = supplierOptions.find(x => x.id === v); form.supplierName = s ? s.name : ''; }">
+          <el-select v-model="form.supplierId" filterable placeholder="搜索选择供应商" class="w-300" @change="(v) => { const s = supplierOptions.find(x => x.id === v); form.supplierName = s ? s.name : ''; }">
             <el-option v-for="s in supplierOptions" :key="s.id" :label="s.name" :value="s.id" />
           </el-select>
         </el-form-item>
 
         <el-form-item label="总金额(元)">
-          <el-input v-model.number="form.totalAmount" type="number" placeholder="入库总金额" style="width:200px;" />
+          <el-input v-model.number="form.totalAmount" type="number" placeholder="入库总金额" class="w-200" />
         </el-form-item>
 
         <el-form-item label="入库明细">
-          <div v-for="(item, idx) in form.items" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;flex-wrap:wrap;">
-            <el-select v-model="item.productId" filterable remote placeholder="搜索商品" :remote-method="(q) => searchProduct(q, idx)" style="width:220px;" @change="(v) => onProductSelect(v, idx)">
+          <div v-for="(item, idx) in form.items" :key="idx" class="item-row">
+            <el-select v-model="item.productId" filterable remote placeholder="搜索商品" :remote-method="(q) => searchProduct(q, idx)" class="w-220" @change="(v) => onProductSelect(v, idx)">
               <el-option v-for="p in item.productOptions" :key="p.id" :label="p.name + ' (ID:' + p.id + ')'" :value="p.id" />
             </el-select>
-            <el-select v-model="item.skuId" placeholder="选择规格" clearable style="width:150px;" v-if="item.skuOptions && item.skuOptions.length">
+            <el-select v-model="item.skuId" placeholder="选择规格" clearable class="w-150" v-if="item.skuOptions && item.skuOptions.length">
               <el-option v-for="s in item.skuOptions" :key="s.id" :label="s.specs || '默认'" :value="s.id" />
             </el-select>
-            <el-input v-model.number="item.quantity" placeholder="数量" type="number" min="1" style="width:100px;" />
+            <el-input v-model.number="item.quantity" placeholder="数量" type="number" min="1" class="w-100" />
             <el-button type="danger" size="mini" @click="form.items.splice(idx,1)">删除</el-button>
           </div>
           <el-button type="primary" size="mini" @click="addItem">添加行</el-button>
         </el-form-item>
 
         <el-form-item label="备注">
-          <el-input v-model="form.remark" type="textarea" :rows="2" style="width:400px;" />
+          <el-input v-model="form.remark" type="textarea" :rows="2" class="w-400" />
         </el-form-item>
 
         <el-form-item>
@@ -167,4 +167,12 @@ export default {
   padding-right: 8px;
   margin-right: -8px;
 }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-300 { width: 300px; }
+.w-220 { width: 220px; }
+.w-150 { width: 150px; }
+.w-100 { width: 100px; }
+.w-400 { width: 400px; }
+.item-row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap; }
 </style>

+ 24 - 13
cfc-web/src/views/admin/InventoryManage.vue

@@ -10,9 +10,9 @@
       <el-tabs v-model="activeTab">
         <!-- 库存流水 -->
         <el-tab-pane label="库存流水" name="transactions">
-          <div class="filter-bar" style="margin-bottom:12px;">
-            <el-input v-model="txFilters.productName" placeholder="商品名称" clearable style="width:160px;margin-right:8px;" />
-            <el-select v-model="txFilters.type" placeholder="类型" clearable style="width:120px;margin-right:8px;">
+          <div class="filter-bar mb-12">
+            <el-input v-model="txFilters.productName" placeholder="商品名称" clearable class="w-160 mr-8" />
+            <el-select v-model="txFilters.type" placeholder="类型" clearable class="w-120 mr-8">
               <el-option label="全部" value="" />
               <el-option label="入库" value="inbound" />
               <el-option label="出库" value="outbound" />
@@ -20,7 +20,7 @@
               <el-option label="盘点" value="counting" />
               <el-option label="退款入库" value="refund" />
             </el-select>
-            <el-date-picker v-model="txFilters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width:260px;margin-right:8px;" />
+            <el-date-picker v-model="txFilters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" class="w-260 mr-8" />
             <el-button type="primary" size="mini" @click="loadTransactions">查询</el-button>
           </div>
                     <div class="adm-table-wrap">
@@ -115,7 +115,7 @@
 
         <!-- 库存预警 -->
         <el-tab-pane label="库存预警" name="alerts">
-          <el-button type="primary" size="mini" @click="loadAlerts" style="margin-bottom:12px;">刷新</el-button>
+          <el-button type="primary" size="mini" @click="loadAlerts" class="mb-12">刷新</el-button>
                     <div class="adm-table-wrap">
 <el-table :data="alertList" v-loading="alertLoading" border stripe height="100%">
             <el-table-column prop="productName" label="商品名称" min-width="200" />
@@ -123,7 +123,7 @@
               <template slot-scope="{ row }">{{ row.specs || '-' }}</template>
             </el-table-column>
             <el-table-column prop="currentStock" label="当前库存" width="80">
-              <template slot-scope="{ row }"><span style="color:var(--adm-error-text);font-weight:bold;">{{ row.currentStock }}</span></template>
+              <template slot-scope="{ row }"><span class="text-error-bold">{{ row.currentStock }}</span></template>
             </el-table-column>
             <el-table-column prop="minStockAlert" label="预警下限" width="80" />
           
@@ -134,7 +134,7 @@
 
         <!-- 出库单 -->
         <el-tab-pane label="出库单" name="outbound">
-          <el-button type="primary" size="mini" @click="showCreateOutbound = true" style="margin-bottom:12px;">新建出库单</el-button>
+          <el-button type="primary" size="mini" @click="showCreateOutbound = true" class="mb-12">新建出库单</el-button>
                     <div class="adm-table-wrap">
 <el-table :data="outboundList" v-loading="outboundLoading" border stripe height="100%">
             <el-table-column prop="id" label="ID" width="60">
@@ -146,7 +146,7 @@
               <template slot-scope="{ row }">{{ row.productName || 'ID:' + row.productId }}</template>
             </el-table-column>
             <el-table-column label="数量" width="70">
-              <template slot-scope="{ row }"><span style="color:var(--adm-error-text);">-{{ row.quantity }}</span></template>
+              <template slot-scope="{ row }"><span class="text-error">-{{ row.quantity }}</span></template>
             </el-table-column>
             <el-table-column prop="remark" label="出库原因" min-width="200" show-overflow-tooltip>
               <template slot="header">
@@ -167,7 +167,7 @@
 
         <!-- 盘点记录 -->
         <el-tab-pane label="盘点记录" name="counting">
-          <el-button type="primary" size="mini" @click="showCreateCounting = true" style="margin-bottom:12px;">新建盘点</el-button>
+          <el-button type="primary" size="mini" @click="showCreateCounting = true" class="mb-12">新建盘点</el-button>
                     <div class="adm-table-wrap">
 <el-table :data="countingList" v-loading="countingLoading" border stripe height="100%">
             <el-table-column prop="id" label="ID" width="60">
@@ -223,7 +223,7 @@
         <el-descriptions-item label="状态">{{ inboundDetail.order.status }}</el-descriptions-item>
         <el-descriptions-item label="备注">{{ inboundDetail.order.remark || '-' }}</el-descriptions-item>
       </el-descriptions>
-      <el-table :data="inboundDetail.items" border stripe style="margin-top:12px;" v-if="inboundDetail">
+      <el-table :data="inboundDetail.items" border stripe class="mt-12" v-if="inboundDetail">
         <el-table-column prop="productId" label="商品ID" width="70" />
         <el-table-column prop="skuId" label="SKU ID" width="70" />
         <el-table-column prop="quantity" label="数量" width="70" />
@@ -233,12 +233,12 @@
     <el-dialog title="新建出库单" :visible.sync="showCreateOutbound" width="500px">
       <el-form label-width="100px">
         <el-form-item label="商品">
-          <el-select v-model="outboundForm.productId" filterable remote placeholder="搜索商品" :remote-method="searchOutboundProduct" style="width:300px;">
+          <el-select v-model="outboundForm.productId" filterable remote placeholder="搜索商品" :remote-method="searchOutboundProduct" class="w-300">
             <el-option v-for="p in outboundProductOptions" :key="p.id" :label="p.name + ' (库存:' + (p.stock || 0) + ')'" :value="p.id" />
           </el-select>
         </el-form-item>
         <el-form-item label="SKU" v-if="outboundSkuOptions.length">
-          <el-select v-model="outboundForm.skuId" placeholder="选择规格" clearable style="width:200px;">
+          <el-select v-model="outboundForm.skuId" placeholder="选择规格" clearable class="w-200">
             <el-option v-for="s in outboundSkuOptions" :key="s.id" :label="s.specs || '默认' + ' (库存:' + (s.stock || 0) + ')'" :value="s.id" />
           </el-select>
         </el-form-item>
@@ -246,7 +246,7 @@
           <el-input v-model.number="outboundForm.quantity" type="number" min="1" />
         </el-form-item>
         <el-form-item label="出库原因">
-          <el-select v-model="outboundForm.reason" placeholder="选择原因" style="width:200px;">
+          <el-select v-model="outboundForm.reason" placeholder="选择原因" class="w-200">
             <el-option label="赠品" value="赠品" />
             <el-option label="损耗" value="损耗" />
             <el-option label="样品" value="样品" />
@@ -455,4 +455,15 @@ export default {
 .inventory-manage { padding: 20px; }
 .filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
 .pagination-wrap { margin-top: 12px; text-align: right; }
+/* == C 批内联清理工具类 == */
+.mb-12 { margin-bottom: 12px; }
+.mt-12 { margin-top: 12px; }
+.mr-8 { margin-right: 8px; }
+.w-160 { width: 160px; }
+.w-120 { width: 120px; }
+.w-260 { width: 260px; }
+.w-300 { width: 300px; }
+.w-200 { width: 200px; }
+.text-error { color: var(--adm-error-text); }
+.text-error-bold { color: var(--adm-error-text); font-weight: bold; }
 </style>

+ 46 - 29
cfc-web/src/views/admin/MembershipCenter.vue

@@ -7,7 +7,7 @@
     </PageHeader>
 
     <!-- 统计概览 -->
-    <el-row :gutter="16" style="margin-bottom:16px;">
+    <el-row :gutter="16" class="mb-16">
       <el-col :xs="12" :sm="8" :md="4" v-for="(card, idx) in statCards" :key="idx">
         <StatCard :label="card.label" :value="card.value" :icon="card.icon" :color="card.color" />
       </el-col>
@@ -137,7 +137,7 @@
               v-model="maintainSearch"
               placeholder="手机号 / 昵称 / 用户ID"
               clearable
-              style="width:240px;"
+              class="w-240"
               @keyup.enter.native="handleMaintainSearch"
             />
             <el-button type="primary" size="small" icon="el-icon-search" @click="handleMaintainSearch" :loading="maintainSearching">搜索</el-button>
@@ -176,11 +176,11 @@
     <!-- 权限详情对话框 -->
     <el-dialog title="功能权限详情" :visible.sync="featuresVisible" width="520px" @close="featuresVisible = false">
       <div v-if="currentLevel">
-        <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
+        <div class="mv-head">
           <StatusTag :type="levelTagType(currentLevel.levelCode)" size="medium" :text="currentLevel.levelCode" />
-          <span style="font-size:16px;font-weight:600;">{{ currentLevel.levelName }}</span>
+          <span class="mv-name">{{ currentLevel.levelName }}</span>
         </div>
-        <p class="text-muted" style="margin:0 0 16px;">{{ currentLevel.levelDesc || '暂无描述' }}</p>
+        <p class="text-muted mv-desc">{{ currentLevel.levelDesc || '暂无描述' }}</p>
         <el-descriptions :column="2" border size="small">
           <el-descriptions-item label="月费">{{ formatPrice(currentLevel.priceMonthly) }} 元</el-descriptions-item>
           <el-descriptions-item label="年费">{{ formatPrice(currentLevel.priceYearly) }} 元</el-descriptions-item>
@@ -196,9 +196,9 @@
             <StatusTag :type="currentLevel.teacherConsultation ? 'success' : 'info'" size="mini" :text="currentLevel.teacherConsultation ? '支持' : '不支持'" />
           </el-descriptions-item>
         </el-descriptions>
-        <div v-if="parseFeatures(currentLevel.features).length" style="margin-top:16px;">
-          <div style="font-weight:600;margin-bottom:8px;color:var(--adm-text-65);">功能权限列表</div>
-          <div style="display:flex;flex-wrap:wrap;gap:6px;">
+        <div v-if="parseFeatures(currentLevel.features).length" class="mv-feat-section">
+          <div class="mv-feat-title">功能权限列表</div>
+          <div class="mv-feat-chips">
             <el-tag v-for="(f, i) in parseFeatures(currentLevel.features)" :key="i" size="small">{{ f }}</el-tag>
           </div>
         </div>
@@ -212,18 +212,18 @@
     <el-dialog title="会员维护" :visible.sync="maintainVisible" width="480px" @close="maintainVisible = false">
       <el-form :model="maintainForm" label-width="90px" size="small">
         <el-form-item label="用户">
-          <span style="font-weight:500;">{{ maintainForm.nickname }}</span>
-          <span class="text-muted" style="margin-left:6px;">ID: {{ maintainForm.id }}</span>
+          <span class="mv-uid">{{ maintainForm.nickname }}</span>
+          <span class="text-muted mv-id">ID: {{ maintainForm.id }}</span>
         </el-form-item>
         <el-form-item label="会员等级" required>
-          <el-select v-model="maintainForm.memberLevel" style="width:180px;" placeholder="选择等级">
+          <el-select v-model="maintainForm.memberLevel" class="w-180" placeholder="选择等级">
             <el-option v-for="lv in safeLevels" :key="lv.levelCode" :label="lv.levelName + ' (' + lv.levelCode + ')'" :value="lv.levelCode" />
           </el-select>
-          <span class="text-muted" style="margin-left:8px;font-size:12px;">选终身会员则永久有效</span>
+          <span class="text-muted hint-ml">选终身会员则永久有效</span>
         </el-form-item>
         <el-form-item label="推荐人ID">
-          <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填 0 清除" style="width:180px;" />
-          <span class="text-muted" style="margin-left:8px;font-size:12px;">填目标用户ID</span>
+          <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填 0 清除" class="w-180" />
+          <span class="text-muted hint-ml">填目标用户ID</span>
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -237,42 +237,42 @@
       <el-form :model="editForm" label-width="100px" size="small">
         <el-form-item label="等级编码">
           <el-tag>{{ editForm.levelCode }}</el-tag>
-          <span style="color:var(--adm-text-45);margin-left:8px;font-size:12px;">编码不可修改</span>
+          <span class="form-hint">编码不可修改</span>
         </el-form-item>
         <el-form-item label="等级名称" required>
-          <el-input v-model="editForm.levelName" placeholder="如:家庭会员" maxlength="32" show-word-limit style="width:320px;" />
+          <el-input v-model="editForm.levelName" placeholder="如:家庭会员" maxlength="32" show-word-limit class="w-320" />
         </el-form-item>
         <el-form-item label="等级描述">
-          <el-input v-model="editForm.levelDesc" type="textarea" :rows="2" maxlength="200" show-word-limit style="width:320px;" />
+          <el-input v-model="editForm.levelDesc" type="textarea" :rows="2" maxlength="200" show-word-limit class="w-320" />
         </el-form-item>
         <el-form-item label="级别排序">
-          <el-input-number v-model="editForm.levelOrder" :min="1" :max="999" :step="1" style="width:160px;" />
-          <span style="color:var(--adm-text-45);margin-left:8px;font-size:12px;">数字越小级别越低,低级别可见则更高级别也可见</span>
+          <el-input-number v-model="editForm.levelOrder" :min="1" :max="999" :step="1" class="w-160" />
+          <span class="mv-hint">数字越小级别越低,低级别可见则更高级别也可见</span>
         </el-form-item>
         <el-form-item label="月费(元)">
-          <el-input-number v-model="editForm.priceMonthly" :min="0" :step="100" :precision="0" style="width:160px;" />
+          <el-input-number v-model="editForm.priceMonthly" :min="0" :step="100" :precision="0" class="w-160" />
         </el-form-item>
         <el-form-item label="季费(元)">
-          <el-input-number v-model="editForm.priceQuarterly" :min="0" :step="100" :precision="0" style="width:160px;" />
+          <el-input-number v-model="editForm.priceQuarterly" :min="0" :step="100" :precision="0" class="w-160" />
         </el-form-item>
         <el-form-item label="年费(元)">
-          <el-input-number v-model="editForm.priceYearly" :min="0" :step="100" :precision="0" style="width:160px;" />
+          <el-input-number v-model="editForm.priceYearly" :min="0" :step="100" :precision="0" class="w-160" />
         </el-form-item>
         <el-form-item label="月原价(元)">
-          <el-input-number v-model="editForm.originalPriceMonthly" :min="0" :step="100" :precision="0" style="width:160px;" />
-          <span style="color:var(--adm-text-45);margin-left:8px;font-size:12px;">留空/0 表示无折扣</span>
+          <el-input-number v-model="editForm.originalPriceMonthly" :min="0" :step="100" :precision="0" class="w-160" />
+          <span class="mv-hint">留空/0 表示无折扣</span>
         </el-form-item>
         <el-form-item label="季原价(元)">
-          <el-input-number v-model="editForm.originalPriceQuarterly" :min="0" :step="100" :precision="0" style="width:160px;" />
+          <el-input-number v-model="editForm.originalPriceQuarterly" :min="0" :step="100" :precision="0" class="w-160" />
         </el-form-item>
         <el-form-item label="年原价(元)">
-          <el-input-number v-model="editForm.originalPriceYearly" :min="0" :step="100" :precision="0" style="width:160px;" />
+          <el-input-number v-model="editForm.originalPriceYearly" :min="0" :step="100" :precision="0" class="w-160" />
         </el-form-item>
         <el-form-item label="家庭成员上限">
-          <el-input-number v-model="editForm.maxChildren" :min="0" :max="99" :step="1" style="width:160px;" />
+          <el-input-number v-model="editForm.maxChildren" :min="0" :max="99" :step="1" class="w-160" />
         </el-form-item>
         <el-form-item label="每日任务上限">
-          <el-input-number v-model="editForm.maxTasksPerDay" :min="0" :max="999" :step="1" style="width:160px;" />
+          <el-input-number v-model="editForm.maxTasksPerDay" :min="0" :max="999" :step="1" class="w-160" />
         </el-form-item>
         <el-form-item label="AI审核">
           <el-switch v-model="editForm.aiReviewEnabled" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="关闭" />
@@ -284,7 +284,7 @@
           <el-switch v-model="editForm.teacherConsultation" :active-value="1" :inactive-value="0" active-text="支持" inactive-text="不支持" />
         </el-form-item>
         <el-form-item label="功能权限">
-          <el-input v-model="editForm.features" type="textarea" :rows="2" placeholder="JSON 数组或逗号分隔,如:[\"basic_tasks\",\"points_system\"]" style="width:320px;" />
+          <el-input v-model="editForm.features" type="textarea" :rows="2" placeholder="JSON 数组或逗号分隔,如:[\"basic_tasks\",\"points_system\"]" class="w-320" />
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -596,6 +596,23 @@ export default {
 </script>
 
 <style scoped>
+/* == C 批内联 style 清理:工具类 == */
+.w-240 { width: 240px; }
+.w-180 { width: 180px; }
+.w-160 { width: 160px; }
+.w-320 { width: 320px; }
+.mb-16 { margin-bottom: 16px; }
+.mv-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
+.mv-name { font-size: 16px; font-weight: 600; }
+.mv-desc { margin: 0 0 16px; }
+.mv-feat-section { margin-top: 16px; }
+.mv-feat-title { font-weight: 600; margin-bottom: 8px; color: var(--adm-text-65); }
+.mv-feat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
+.mv-uid { font-weight: 500; }
+.mv-id { margin-left: 6px; }
+.mv-hint { color: var(--adm-text-45); margin-left: 8px; font-size: 12px; }
+.hint-ml { margin-left: 8px; font-size: 12px; }
+
 /* ---- 升级路径 ---- */
 .level-path {
   display: inline-flex;

+ 30 - 15
cfc-web/src/views/admin/NutritionProducts.vue

@@ -11,7 +11,7 @@
             prefix-icon="el-icon-search"
             clearable
             size="mini"
-            style="width: 200px; margin-right: 8px;"
+            class="w-200 mr-8"
             @keyup.enter.native="handleSearch"
             @clear="handleSearch"
           />
@@ -20,7 +20,7 @@
             placeholder="分类筛选"
             size="mini"
             clearable
-            style="width: 140px; margin-right: 8px;"
+            class="w-140 mr-8"
             @change="handleSearch"
           >
             <el-option label="全部分类" value="" />
@@ -31,7 +31,7 @@
             placeholder="品牌筛选"
             size="mini"
             clearable
-            style="width: 140px; margin-right: 8px;"
+            class="w-140 mr-8"
             @change="handleSearch"
           >
             <el-option label="全部品牌" value="" />
@@ -48,7 +48,7 @@
         border
         stripe
         size="mini"
-        style="width: 100%"
+        class="full-width"
         height="100%"
       >
         <el-table-column type="index" label="#" width="50" align="center" />
@@ -59,7 +59,7 @@
               :src="productImage(row)"
               :preview-src-list="productImageList(row)"
               fit="cover"
-              style="width: 60px; height: 60px; border-radius: 4px;"
+              class="img-thumb"
               lazy
             />
             <span v-else class="no-data">-</span>
@@ -67,7 +67,7 @@
         </el-table-column>
         <el-table-column label="产品名称" min-width="200">
           <template slot-scope="{ row }">
-            <span style="font-weight: 500;">{{ row.name }}</span>
+            <span class="font-medium">{{ row.name }}</span>
           </template>
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('name', $event)">产品名称{{ sortIcon('name') }}</span>
@@ -128,7 +128,7 @@
         <el-table-column label="操作" width="110" align="center" fixed="right">
           <template slot-scope="{ row }">
             <el-button type="text" size="mini" @click="showDetail(row.id)">详情</el-button>
-            <el-link v-if="row.sourceUrl" :href="row.sourceUrl" target="_blank" type="primary" :underline="false" style="margin-left: 6px;">原平台</el-link>
+            <el-link v-if="row.sourceUrl" :href="row.sourceUrl" target="_blank" type="primary" :underline="false" class="ml-6">原平台</el-link>
           </template>
         </el-table-column>
       </el-table>
@@ -156,17 +156,17 @@
       width="800px"
       :close-on-click-modal="false"
     >
-      <div v-loading="detailLoading" v-if="detailData" style="max-height: 60vh; overflow-y: auto;">
-        <el-row :gutter="12" style="margin-bottom: 12px;">
+      <div v-loading="detailLoading" v-if="detailData" class="detail-scroll">
+        <el-row :gutter="12" class="mb-12">
           <el-col :span="8">
             <el-image
               v-if="detailImages.length"
               :src="detailImages[0]"
               :preview-src-list="detailImages"
               fit="cover"
-              style="width: 100%; height: 180px; border-radius: 6px;"
+              class="detail-img"
             />
-            <div v-else class="no-data" style="height: 180px; display: flex; align-items: center; justify-content: center;">暂无图片</div>
+            <div v-else class="no-data img-placeholder">暂无图片</div>
           </el-col>
           <el-col :span="16">
             <el-descriptions :column="2" border size="mini">
@@ -235,7 +235,7 @@
         <SectionTitle title="本平台关联商品" />
         <div v-if="detailData.localProduct">
           <StatusTag type="success" :text="'已关联商品 ID: ' + detailData.localProduct.id" />
-          <p style="margin-top: 8px;">名称: {{ detailData.localProduct.name }}</p>
+          <p class="mt-8">名称: {{ detailData.localProduct.name }}</p>
           <p>价格: ¥{{ (detailData.localProduct.price / 100).toFixed(2) }}</p>
         </div>
         <div v-else class="no-data">未关联本平台商品</div>
@@ -244,10 +244,10 @@
       
       </el-table>
       </div>
-      <div v-else-if="detailLoading" style="text-align: center; padding: 40px;">
-        <i class="el-icon-loading" style="font-size: 24px;"></i>
+      <div v-else-if="detailLoading" class="center-pad">
+        <i class="el-icon-loading loading-icon"></i>
       </div>
-      <div v-else class="no-data" style="text-align: center; padding: 40px;">未找到产品信息</div>
+      <div v-else class="no-data center-pad">未找到产品信息</div>
     </el-dialog>
   </div>
 </template>
@@ -428,4 +428,19 @@ export default {
 .no-data {
   color: var(--adm-text-45);
 }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-140 { width: 140px; }
+.mr-8 { margin-right: 8px; }
+.ml-6 { margin-left: 6px; }
+.mb-12 { margin-bottom: 12px; }
+.mt-8 { margin-top: 8px; }
+.full-width { width: 100%; }
+.font-medium { font-weight: 500; }
+.img-thumb { width: 60px; height: 60px; border-radius: 4px; }
+.detail-scroll { max-height: 60vh; overflow-y: auto; }
+.detail-img { width: 100%; height: 180px; border-radius: 6px; }
+.img-placeholder { height: 180px; display: flex; align-items: center; justify-content: center; }
+.center-pad { text-align: center; padding: 40px; }
+.loading-icon { font-size: 24px; }
 </style>

+ 26 - 12
cfc-web/src/views/admin/ProblemDomainManage.vue

@@ -29,7 +29,7 @@
             <span class="sort-header" @click.stop="onSortClick('icon', $event)">图标<span v-if="sortIndex('icon') !== null" class="sort-index" :class="sortClass('icon') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('icon') }}</span>{{ sortIcon('icon') }}</span>
           </template>
           <template slot-scope="scope">
-            <span style="font-size:20px">{{ scope.row.icon || '🧭' }}</span>
+            <span class="font-20">{{ scope.row.icon || '🧭' }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip>
@@ -58,7 +58,7 @@
         <el-table-column label="操作" width="150" align="center">
           <template slot-scope="scope">
             <el-button type="text" size="mini" @click="openEdit(scope.row)">编辑</el-button>
-            <el-button type="text" size="mini" style="color:var(--adm-error)" @click="handleDelete(scope.row)">删除</el-button>
+            <el-button type="text" size="mini" class="text-error" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -74,15 +74,15 @@
           <el-input v-model="form.name" placeholder="如:减肥 / 睡眠 / 便秘" />
         </el-form-item>
         <el-form-item label="图标">
-          <el-input v-model="form.icon" placeholder="emoji,如 🌙" style="width:120px" />
+          <el-input v-model="form.icon" placeholder="emoji,如 🌙" class="w-120" />
         </el-form-item>
         <el-form-item label="描述">
           <el-input v-model="form.description" type="textarea" :rows="2" placeholder="一句话描述" />
         </el-form-item>
         <el-form-item label="调研模板ID">
-          <el-input v-model.number="form.surveyTemplateId" placeholder="SurveyTemplate.id" style="width:200px" />
-          <el-button size="mini" style="margin-left:8px" @click="loadSurveys">从调研模板列表选择</el-button>
-          <el-select v-if="surveyOptions.length" v-model="form.surveyTemplateId" placeholder="选择模板" style="width:200px;margin-left:8px" clearable>
+          <el-input v-model.number="form.surveyTemplateId" placeholder="SurveyTemplate.id" class="w-200" />
+          <el-button size="mini" class="ml-8" @click="loadSurveys">从调研模板列表选择</el-button>
+          <el-select v-if="surveyOptions.length" v-model="form.surveyTemplateId" placeholder="选择模板" class="w-200 ml-8" clearable>
             <el-option
               v-for="s in surveyOptions"
               :key="s.id"
@@ -120,18 +120,18 @@
           />
         </el-form-item>
         <el-form-item label="可视化编辑">
-          <div style="width:100%">
+          <div class="flow-container">
             <div
               v-for="(step, idx) in flowSteps"
               :key="idx"
-              style="margin-bottom:8px;padding:8px;border:1px solid var(--adm-border-split);border-radius:4px;"
+              class="flow-step-card"
             >
-              <el-input v-model="step.title" placeholder="步骤标题" style="width:40%;margin-right:8px" size="small" />
-              <el-input v-model="step.desc" placeholder="步骤描述" style="width:40%;margin-right:8px" size="small" />
-              <el-button type="text" size="small" style="color:var(--adm-error)" @click="removeStep(idx)">删除</el-button>
+              <el-input v-model="step.title" placeholder="步骤标题" class="step-input" size="small" />
+              <el-input v-model="step.desc" placeholder="步骤描述" class="step-input" size="small" />
+              <el-button type="text" size="small" class="text-error" @click="removeStep(idx)">删除</el-button>
             </div>
             <el-button type="primary" size="mini" plain @click="addStep">+ 添加步骤</el-button>
-            <el-button type="success" size="mini" plain @click="syncFlowJson" style="margin-left:8px">同步到JSON</el-button>
+            <el-button type="success" size="mini" plain class="ml-8" @click="syncFlowJson">同步到JSON</el-button>
           </div>
         </el-form-item>
       </el-form>
@@ -274,3 +274,17 @@ export default {
     }
   }
 }
+</script>
+
+<style scoped>
+.admin-page { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.font-20 { font-size: 20px; }
+.text-error { color: var(--adm-error); }
+.w-120 { width: 120px; }
+.w-200 { width: 200px; }
+.ml-8 { margin-left: 8px; }
+.flow-container { width: 100%; }
+.flow-step-card { margin-bottom: 8px; padding: 8px; border: 1px solid var(--adm-border-split); border-radius: 4px; }
+.step-input { width: 40%; margin-right: 8px; }
+</style>

+ 102 - 46
cfc-web/src/views/admin/ProductEdit.vue

@@ -17,7 +17,7 @@
         </el-form-item>
 
         <el-form-item label="商品类型" prop="productType">
-          <el-select v-model="form.productType" placeholder="请选择商品类型" style="width: 200px;">
+          <el-select v-model="form.productType" placeholder="请选择商品类型" class="w-200">
             <el-option label="实物商品" value="physical" />
             <el-option label="虚拟商品" value="virtual" />
             <el-option label="优惠券" value="coupon" />
@@ -31,27 +31,27 @@
             <el-radio :label="1">是</el-radio>
             <el-radio :label="0">否</el-radio>
           </el-radio-group>
-          <span style="margin-left: 10px; font-size: 12px; color: var(--adm-text-45);">开启后自动同步商品信息至营养产品,小程序营养产品详情可跳转购买</span>
+          <span class="form-hint">开启后自动同步商品信息至营养产品,小程序营养产品详情可跳转购买</span>
         </el-form-item>
 
         <el-form-item label="商品分类">
-          <el-select v-model="form.categoryId" placeholder="请选择分类(用于前台商城筛选)" clearable filterable style="width: 300px;">
+          <el-select v-model="form.categoryId" placeholder="请选择分类(用于前台商城筛选)" clearable filterable class="w-300">
             <el-option-group v-for="group in categoryGroups" :key="group.label" :label="group.label">
               <el-option v-for="cat in group.options" :key="cat.id" :label="cat.label" :value="cat.id" />
             </el-option-group>
           </el-select>
-          <div style="font-size: 12px; color: var(--adm-text-45); margin-top: 4px;">不选则不出现在商城分类筛选中;可在「商品分类管理」中维护分类</div>
+          <div class="form-hint-mt4">不选则不出现在商城分类筛选中;可在「商品分类管理」中维护分类</div>
         </el-form-item>
 
         <el-form-item label="商品标签">
-          <el-input v-model="form.tags" placeholder="逗号分隔,如: 育儿,亲子,心理" maxlength="200" clearable style="width: 400px;" />
-          <div style="font-size: 12px; color: var(--adm-text-45); margin-top: 4px;">用于小程序文章详情页「相关商品」推荐:标签与文章标签一字不差才会匹配,留空则不参与匹配</div>
+          <el-input v-model="form.tags" placeholder="逗号分隔,如: 育儿,亲子,心理" maxlength="200" clearable class="w-400" />
+          <div class="form-hint-mt4">用于小程序文章详情页「相关商品」推荐:标签与文章标签一字不差才会匹配,留空则不参与匹配</div>
         </el-form-item>
 
         <!-- 测评商品扩展信息 -->
         <template v-if="form.productType === 'assessment'">
           <el-form-item label="测评类型">
-            <el-select v-model="form.assessmentType" placeholder="请选择测评类型" style="width: 200px;">
+            <el-select v-model="form.assessmentType" placeholder="请选择测评类型" class="w-200">
               <el-option label="单包" value="single" />
               <el-option label="组合" value="bundle" />
             </el-select>
@@ -59,17 +59,17 @@
           <el-row :gutter="20">
             <el-col :span="8">
               <el-form-item label="测评次数">
-                <el-input-number v-model="form.totalSessions" :min="1" :max="999" :precision="0" style="width: 100%;" controls />
+                <el-input-number v-model="form.totalSessions" :min="1" :max="999" :precision="0" class="full-width" controls />
               </el-form-item>
             </el-col>
             <el-col :span="8">
               <el-form-item label="有效期(天)">
-                <el-input-number v-model="form.validityDays" :min="1" :max="3650" :precision="0" :default-value="365" style="width: 100%;" controls />
+                <el-input-number v-model="form.validityDays" :min="1" :max="3650" :precision="0" :default-value="365" class="full-width" controls />
               </el-form-item>
             </el-col>
             <el-col :span="8">
               <el-form-item label="规划师范围">
-                <el-select v-model="form.guideScope" placeholder="不限" style="width: 100%;">
+                <el-select v-model="form.guideScope" placeholder="不限" class="full-width">
                   <el-option label="不限" value="all" />
                   <el-option label="指定规划师" value="assigned" />
                   <el-option label="自动分配" value="auto" />
@@ -83,18 +83,18 @@
         <template v-if="form.productType === 'bundle'">
           <SectionTitle title="套餐组成" />
           <el-form-item label="组成商品">
-            <div v-for="(item, idx) in bundleItems" :key="'bi' + idx" style="display:flex; align-items:center; margin-bottom:8px; gap:8px; flex-wrap:wrap;">
-              <el-select v-model="item.childProductId" filterable remote placeholder="搜索子商品" :remote-method="(q) => searchBundleProduct(q, idx)" style="width:220px;" @change="(pid) => onBundleProductChange(pid, idx)">
+            <div v-for="(item, idx) in bundleItems" :key="'bi' + idx" class="bundle-row">
+              <el-select v-model="item.childProductId" filterable remote placeholder="搜索子商品" :remote-method="(q) => searchBundleProduct(q, idx)" class="w-220" @change="(pid) => onBundleProductChange(pid, idx)">
                 <el-option v-for="p in item.productOptions" :key="p.id" :label="(p.name || 'ID:' + p.id) + ' (库存:' + (p.stock || 0) + ')'" :value="p.id" />
               </el-select>
-              <el-select v-if="item.skuOptions.length" v-model="item.childSkuId" placeholder="选择规格" clearable style="width:160px;">
+              <el-select v-if="item.skuOptions.length" v-model="item.childSkuId" placeholder="选择规格" clearable class="w-160">
                 <el-option v-for="s in item.skuOptions" :key="s.id" :label="(s.specs || '默认') + ' (库存:' + (s.stock || 0) + ')'" :value="s.id" />
               </el-select>
-              <el-input-number v-model="item.quantity" :min="1" :precision="0" size="small" style="width:110px;" />
+              <el-input-number v-model="item.quantity" :min="1" :precision="0" size="small" class="w-110" />
               <el-button type="danger" size="mini" @click="removeBundleItem(idx)">删除</el-button>
             </div>
             <el-button type="primary" size="mini" plain @click="addBundleItem()">+ 添加子商品</el-button>
-            <div style="margin-top:8px; font-size:12px; color:var(--adm-text-45);">套餐出库时自动完成所有组成商品出库(数量 × 套餐数量)</div>
+            <div class="form-hint-mt8">套餐出库时自动完成所有组成商品出库(数量 × 套餐数量)</div>
           </el-form-item>
         </template>
 
@@ -106,7 +106,7 @@
             active-text="开启"
             inactive-text="关闭"
           />
-          <span style="margin-left: 10px; font-size: 12px; color: var(--adm-text-45);">在成长档案上传页推荐该商品</span>
+          <span class="form-hint">在成长档案上传页推荐该商品</span>
         </el-form-item>
 
         <el-form-item label="商品简介" prop="intro">
@@ -114,14 +114,14 @@
         </el-form-item>
 
         <el-form-item label="排序值">
-          <el-input-number v-model="form.sortOrder" :min="0" :precision="0" style="width: 200px;" controls />
-          <span style="margin-left: 10px; font-size: 12px; color: var(--adm-text-45);">越小越靠前</span>
+          <el-input-number v-model="form.sortOrder" :min="0" :precision="0" class="w-200" controls />
+          <span class="form-hint">越小越靠前</span>
         </el-form-item>
 
         <el-form-item label="商品描述" prop="description">
           <div class="rich-editor">
             <Toolbar :editor="editorInstance" :defaultConfig="toolbarConfig" />
-            <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" style="min-height: 300px;" />
+            <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" class="editor-body" />
           </div>
         </el-form-item>
 
@@ -165,38 +165,38 @@
         <el-row :gutter="20">
           <el-col :span="6">
             <el-form-item label="售价(元)" prop="price">
-              <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
+              <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" class="full-width" controls />
             </el-form-item>
           </el-col>
           <el-col :span="6">
             <el-form-item label="会员价(元)">
-              <el-input-number v-model="form.memberPrice" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" style="width: 100%;" controls />
+              <el-input-number v-model="form.memberPrice" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" class="full-width" controls />
             </el-form-item>
           </el-col>
           <el-col :span="6">
             <el-form-item label="库存" prop="stock">
-              <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" controls />
+              <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" class="full-width" controls />
             </el-form-item>
           </el-col>
           <el-col :span="6">
             <el-form-item label="最小购买数">
-              <el-input-number v-model="form.minQuantity" :min="1" :precision="0" placeholder="1" style="width: 100%;" controls />
+              <el-input-number v-model="form.minQuantity" :min="1" :precision="0" placeholder="1" class="full-width" controls />
             </el-form-item>
           </el-col>
         </el-row>
 
         <el-form-item label="所属体系">
-          <el-select v-model="form.distributionSystemId" placeholder="不属于任何体系" clearable :disabled="isSupplierAdmin" style="width: 100%" @change="handleSystemChange">
+          <el-select v-model="form.distributionSystemId" placeholder="不属于任何体系" clearable :disabled="isSupplierAdmin" class="full-width" @change="handleSystemChange">
             <el-option v-for="s in supplySystems" :key="s.id" :label="s.name" :value="s.id"></el-option>
           </el-select>
-          <span v-if="selectedSystem" style="font-size: 12px; color: var(--adm-text-45); margin-left: 8px">
+          <span v-if="selectedSystem" class="form-hint-ml8">
             平台留利: {{ selectedSystem.platformProfitRate }}% → {{ selectedSystem.platformProfitRate * 10 }}‰
           </span>
         </el-form-item>
 
         <el-form-item label="仅体系可见">
           <el-switch v-model="form.vendorVisible" />
-          <span style="margin-left: 10px; font-size: 12px; color: var(--adm-text-45);">开启后仅所属供应体系的用户可在列表中看到该商品,分享商品详情页所有人可访问</span>
+          <span class="form-hint">开启后仅所属供应体系的用户可在列表中看到该商品,分享商品详情页所有人可访问</span>
         </el-form-item>
 
         <el-form-item label="配送方式">
@@ -214,17 +214,17 @@
           <div class="service-person-list">
             <!-- 添加服务者下拉 -->
             <div class="add-person-row">
-              <el-select v-model="selectedGuideId" placeholder="选择服务商(已入驻)" style="width: 260px"
+              <el-select v-model="selectedGuideId" placeholder="选择服务商(已入驻)" class="w-260"
                          filterable @change="onSelectGuide">
                 <el-option v-for="g in filteredGuides" :key="g.userId"
                   :label="g.realName || g.nickname"
                   :value="g.userId">
-                  <span style="float:left">{{ g.realName || g.nickname }}</span>
-                  <span style="float:right;color:var(--adm-text-45);font-size:12px">{{ g.title || g.specialty || '' }}</span>
+                  <span class="sp-opt-name">{{ g.realName || g.nickname }}</span>
+                  <span class="sp-opt-desc">{{ g.title || g.specialty || '' }}</span>
                 </el-option>
               </el-select>
-              <el-button type="primary" size="mini" style="margin-left:8px" @click="addSelectedGuide()">添加</el-button>
-              <div class="delivery-config-hint" style="margin-top:4px">时段由服务商在「服务商管理」中统一配置</div>
+              <el-button type="primary" size="mini" class="ml-8" @click="addSelectedGuide()">添加</el-button>
+              <div class="delivery-config-hint">时段由服务商在「服务商管理」中统一配置</div>
             </div>
             <!-- 已选服务者列表 -->
             <div v-for="(person, pIdx) in servicePersons" :key="pIdx" class="service-person-card">
@@ -240,7 +240,7 @@
                   <el-input v-model="person.title" placeholder="头衔" size="small" />
                 </el-col>
               </el-row>
-              <el-row :gutter="10" style="margin-top:8px">
+              <el-row :gutter="10" class="mt-8">
                 <el-col :span="12">
                   <el-input v-model.number="person.price" placeholder="专属价格(元)" size="small" type="number" />
                 </el-col>
@@ -248,7 +248,7 @@
                   <el-input v-model.number="person.originalPrice" placeholder="原价(元)" size="small" type="number" />
                 </el-col>
               </el-row>
-              <el-input v-model="person.intro" type="textarea" :rows="2" placeholder="个人介绍(可留空,自动从服务商资料代入)" size="small" style="margin-top:8px" />
+              <el-input v-model="person.intro" type="textarea" :rows="2" placeholder="个人介绍(可留空,自动从服务商资料代入)" size="small" class="mt-8" />
             </div>
           </div>
         </el-form-item>
@@ -257,9 +257,9 @@
         <el-form-item v-if="form.deliveryMethod === 5" label="线上服务时段">
           <div class="online-slot-list">
             <div v-for="(slot, idx) in onlineSlots" :key="idx" class="sp-slot-row">
-              <el-date-picker v-model="slot.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" style="width: 190px" />
-              <el-date-picker v-model="slot.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" style="width: 190px; margin-left: 8px" />
-              <el-input v-model.number="slot.maxCapacity" placeholder="容量" style="width: 90px; margin-left: 8px" />
+              <el-date-picker v-model="slot.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" class="w-190" />
+              <el-date-picker v-model="slot.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" class="w-190 ml-8" />
+              <el-input v-model.number="slot.maxCapacity" placeholder="容量" class="w-90 ml-8" />
               <el-button type="text" size="mini" @click="removeOnlineSlot(idx)">删除</el-button>
             </div>
             <el-button type="primary" plain size="mini" @click="addOnlineSlot()">+ 添加时段</el-button>
@@ -274,7 +274,7 @@
         </el-form-item>
 
         <el-form-item label="最低可见级别">
-          <el-select v-model="form.minVisibleLevel" placeholder="全部可见" clearable style="width: 200px;">
+          <el-select v-model="form.minVisibleLevel" placeholder="全部可见" clearable class="w-200">
             <el-option
               v-for="lv in membershipLevels"
               :key="lv.levelCode"
@@ -282,7 +282,7 @@
               :value="lv.levelCode"
             />
           </el-select>
-          <span style="margin-left: 10px; font-size: 12px; color: var(--adm-text-45);">留空=全部会员可见;选择后仅该级别及更高级别会员可见</span>
+          <span class="form-hint">留空=全部会员可见;选择后仅该级别及更高级别会员可见</span>
         </el-form-item>
 
         <el-form-item label="商品维度">
@@ -329,24 +329,24 @@
 
         <el-form-item label="赠送会员">
           <el-switch v-model="giftRuleEnabled" active-text="启用" inactive-text="停用" />
-          <el-select v-if="giftRuleEnabled" v-model="giftRuleLevel" placeholder="选择赠送等级" style="margin-left: 12px; width: 200px">
+          <el-select v-if="giftRuleEnabled" v-model="giftRuleLevel" placeholder="选择赠送等级" class="w-200 ml-12">
             <el-option v-for="lv in membershipLevels" :key="lv.levelCode" :label="lv.levelName" :value="lv.levelCode" />
           </el-select>
         </el-form-item>
 
         <el-form-item label="可选赠品">
-          <div style="width: 100%">
+          <div class="full-width">
             <div v-for="lv in membershipLevels" :key="lv.levelCode" class="gift-config-level">
               <div class="gift-config-level-title">{{ lv.levelName }} 赠品</div>
               <div v-for="(item, idx) in getGiftItems(lv.levelCode)" :key="getGiftItemKey(item, idx)" class="gift-config-item">
-                <el-select v-model="item.giftType" size="mini" style="width: 110px; margin-right: 8px">
+                <el-select v-model="item.giftType" size="mini" class="w-110 mr-8">
                   <el-option label="赠品商品" value="product" />
                   <el-option label="CF值" value="cf" />
                 </el-select>
-                <el-select v-if="item.giftType === 'product'" v-model="item.giftProductId" size="mini" filterable placeholder="选择赠品商品" style="width: 260px; margin-right: 8px">
+                <el-select v-if="item.giftType === 'product'" v-model="item.giftProductId" size="mini" filterable placeholder="选择赠品商品" class="w-260 mr-8">
                   <el-option v-for="p in onShelfProducts" :key="p.id" :label="p.name" :value="p.id" />
                 </el-select>
-                <el-input v-if="item.giftType === 'cf'" v-model.number="item.cfValue" size="mini" placeholder="CF值数量" type="number" style="width: 140px; margin-right: 8px" />
+                <el-input v-if="item.giftType === 'cf'" v-model.number="item.cfValue" size="mini" placeholder="CF值数量" type="number" class="w-140 mr-8" />
                 <el-button size="mini" type="text" @click="removeGiftItem(lv.levelCode, idx)">删除</el-button>
               </div>
               <el-button size="mini" type="text" @click="addGiftItem(lv.levelCode)">+ 添加赠品</el-button>
@@ -354,7 +354,7 @@
           </div>
         </el-form-item>
 
-        <div class="edit-form-footer" style="display:none;"></div>
+        <div class="edit-form-footer"></div>
       </el-form>
       </div>
     </el-card>
@@ -1183,8 +1183,64 @@ async loadGiftRule(productId) {
 }}
 </script>
 
-<style scoped>
-.product-edit {
+.editor-body {
+  min-height: 300px;
+}
+.form-hint {
+  font-size: 12px;
+  color: var(--adm-text-45);
+  margin-left: 10px;
+}
+.form-hint-mt4 {
+  font-size: 12px;
+  color: var(--adm-text-45);
+  margin-top: 4px;
+}
+.form-hint-mt8 {
+  font-size: 12px;
+  color: var(--adm-text-45);
+  margin-top: 8px;
+}
+.form-hint-ml8 {
+  font-size: 12px;
+  color: var(--adm-text-45);
+  margin-left: 8px;
+}
+.bundle-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+.sp-opt-name {
+  float: left;
+}
+.sp-opt-desc {
+  float: right;
+  color: var(--adm-text-45);
+  font-size: 12px;
+}
+.full-width {
+  width: 100%;
+}
+.w-90 { width: 90px; }
+.w-110 { width: 110px; }
+.w-140 { width: 140px; }
+.w-160 { width: 160px; }
+.w-190 { width: 190px; }
+.w-200 { width: 200px; }
+.w-220 { width: 220px; }
+.w-260 { width: 260px; }
+.w-300 { width: 300px; }
+.w-400 { width: 400px; }
+.ml-8 { margin-left: 8px; }
+.ml-12 { margin-left: 12px; }
+.mr-8 { margin-right: 8px; }
+.edit-form-footer {
+  display: none;
+}
+</style>
   padding: 20px;
   flex: 1;
   display: flex;

+ 17 - 10
cfc-web/src/views/admin/ProductPpointManage.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="product-ppoint admin-page">
     <PageHeader title="P点(可分配利润)配置" subtitle="按商品维护可分配利润基点">
-      <el-input v-model="searchText" placeholder="商品名称搜索" prefix-icon="el-icon-search" style="width: 200px;" clearable @keyup.enter.native="loadList" />
+      <el-input v-model="searchText" placeholder="商品名称搜索" prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="loadList" />
       <el-button type="primary" @click="loadList">查询</el-button>
       <el-button type="success" @click="openAddDialog">新增配置</el-button>
       <el-tag
@@ -52,7 +52,7 @@
             :max="10000"
             :step="100"
             size="small"
-            style="width: 100px;"
+            class="w-100"
           />
         </template>
       </el-table-column>
@@ -92,25 +92,25 @@
       :page-size="size"
       :total="total"
       layout="total, prev, pager, next"
-      style="margin-top: 20px; text-align: right;"
+      class="pagination-right"
     />
 
     <el-dialog title="新增P点配置" :visible.sync="addDialogVisible" width="500px">
       <el-form :model="addForm" label-width="100px">
         <el-form-item label="选择商品">
-          <el-select v-model="addForm.productId" filterable placeholder="搜索商品" style="width: 100%;">
+          <el-select v-model="addForm.productId" filterable placeholder="搜索商品" class="full-width">
             <el-option v-for="p in productOptions" :key="p.id" :label="p.name" :value="p.id">
-              <span style="float:left">{{ p.name }}</span>
-              <span style="float:right;color:var(--adm-text-45);font-size:12px;">{{ p.price != null ? (p.price/100).toFixed(2) + '元' : '' }}</span>
+              <span class="pull-left">{{ p.name }}</span>
+              <span class="pull-right">{{ p.price != null ? (p.price/100).toFixed(2) + '元' : '' }}</span>
             </el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="P点(bps)">
-          <el-input-number v-model="addForm.ppoint" :min="0" :max="10000" :step="100" style="width: 100%;" />
-          <div style="color:var(--adm-text-45);font-size:12px;margin-top:4px;">10000 bps = 100%,例:1000 bps = 10%</div>
+          <el-input-number v-model="addForm.ppoint" :min="0" :max="10000" :step="100" class="full-width" />
+          <div class="hint">10000 bps = 100%,例:1000 bps = 10%</div>
         </el-form-item>
         <el-form-item label="生效日期">
-          <el-date-picker v-model="addForm.startDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" style="width: 100%;" />
+          <el-date-picker v-model="addForm.startDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" class="full-width" />
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -260,5 +260,12 @@ export default {
   object-fit: cover;
   border-radius: 4px;
 }
-
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-100 { width: 100px; }
+.pagination-right { margin-top: 20px; text-align: right; }
+.full-width { width: 100%; }
+.pull-left { float: left; }
+.pull-right { float: right; color: var(--adm-text-45); font-size: 12px; }
+.hint { color: var(--adm-text-45); font-size: 12px; margin-top: 4px; }
 </style>

+ 34 - 24
cfc-web/src/views/admin/ProfileManagement.vue

@@ -7,33 +7,33 @@
       
 </PageHeader>
 
-      <div class="filter-bar" style="margin-bottom:12px;display:flex;gap:12px;align-items:center;">
-        <el-input v-model="searchForm.memberId" placeholder="家庭成员ID" clearable style="width:160px;" @keyup.enter.native="loadList" />
+      <div class="filter-bar">
+        <el-input v-model="searchForm.memberId" placeholder="家庭成员ID" clearable class="w-160" @keyup.enter.native="loadList" />
         <el-button type="primary" size="mini" @click="loadList">搜索</el-button>
         <el-button size="mini" @click="resetSearch">重置</el-button>
       </div>
 
             <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe style="min-width:800px;" height="100%">
+<el-table :data="list" v-loading="loading" border stripe class="min-w-800" height="100%">
         <el-table-column prop="id" label="ID" width="70" />
         <el-table-column prop="name" label="姓名" width="100" />
         <el-table-column prop="age" label="年龄" width="70" />
         <el-table-column label="五维评分" width="320">
           <template slot-scope="{ row }">
             <span v-if="row.dimension_scores">
-              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('body')">{{ row.dimension_scores.body || '-' }}</el-tag>
-              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('wisdom')">{{ row.dimension_scores.wisdom || '-' }}</el-tag>
-              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('mind')">{{ row.dimension_scores.mind || '-' }}</el-tag>
-              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('action')">{{ row.dimension_scores.action || '-' }}</el-tag>
+              <el-tag size="mini" class="tag-mr" :color="dimColor('body')">{{ row.dimension_scores.body || '-' }}</el-tag>
+              <el-tag size="mini" class="tag-mr" :color="dimColor('wisdom')">{{ row.dimension_scores.wisdom || '-' }}</el-tag>
+              <el-tag size="mini" class="tag-mr" :color="dimColor('mind')">{{ row.dimension_scores.mind || '-' }}</el-tag>
+              <el-tag size="mini" class="tag-mr" :color="dimColor('action')">{{ row.dimension_scores.action || '-' }}</el-tag>
               <el-tag size="mini" :color="dimColor('wealth')">{{ row.dimension_scores.wealth || '-' }}</el-tag>
             </span>
-            <span v-else style="color:var(--adm-text-25)">暂无</span>
+            <span v-else class="text-faint">暂无</span>
           </template>
         </el-table-column>
         <el-table-column label="问题域" width="160">
           <template slot-scope="{ row }">
-            <el-tag v-for="d in (row.problem_domains || [])" :key="d" size="mini" style="margin-right:4px;">{{ domainLabel(d) }}</el-tag>
-            <span v-if="!row.problem_domains || row.problem_domains.length === 0" style="color:var(--adm-text-25)">-</span>
+            <el-tag v-for="d in (row.problem_domains || [])" :key="d" size="mini" class="tag-mr">{{ domainLabel(d) }}</el-tag>
+            <span v-if="!row.problem_domains || row.problem_domains.length === 0" class="text-faint">-</span>
           </template>
         </el-table-column>
         <el-table-column prop="computed_at" label="更新时间" width="160">
@@ -51,7 +51,7 @@
       </div>
 
       <el-pagination
-        style="margin-top:16px;text-align:right;"
+        class="pager-right"
         layout="total, prev, pager, next"
         :total="total"
         :current-page="page"
@@ -68,15 +68,15 @@
           <el-descriptions-item label="年龄">{{ currentProfile.member && currentProfile.member.age }}</el-descriptions-item>
         </el-descriptions>
 
-        <h4 style="margin:16px 0 8px;color:var(--adm-text-88);">五维评分</h4>
+        <h4 class="section-title">五维评分</h4>
         <el-row :gutter="12" v-if="currentProfile.dimension_scores">
           <el-col :span="4" v-for="(score, dim) in currentProfile.dimension_scores" :key="dim">
             <el-progress type="dashboard" :percentage="score" :color="dimColor(dim)" :width="70" :stroke-width="8" />
-            <div style="text-align:center;font-size:12px;margin-top:4px;">{{ dimName(dim) }}</div>
+            <div class="dim-label">{{ dimName(dim) }}</div>
           </el-col>
         </el-row>
 
-        <h4 style="margin:16px 0 8px;color:var(--adm-text-88);">身体指标</h4>
+        <h4 class="section-title">身体指标</h4>
         <el-descriptions :column="3" border size="small" v-if="currentProfile.body_metrics">
           <el-descriptions-item label="平均睡眠(h)">{{ currentProfile.body_metrics.sleep_dur_avg || '-' }}</el-descriptions-item>
           <el-descriptions-item label="深睡占比(%)">{{ currentProfile.body_metrics.deep_sleep_pct || '-' }}</el-descriptions-item>
@@ -85,7 +85,7 @@
           <el-descriptions-item label="月饮食记录数">{{ currentProfile.body_metrics.meal_records_count || 0 }}</el-descriptions-item>
         </el-descriptions>
 
-        <h4 style="margin:16px 0 8px;color:var(--adm-text-88);">心理指标</h4>
+        <h4 class="section-title">心理指标</h4>
         <el-descriptions :column="3" border size="small" v-if="currentProfile.mind_metrics">
           <el-descriptions-item label="正向情绪占比">{{ currentProfile.mind_metrics.emotion_joy_ratio != null ? (currentProfile.mind_metrics.emotion_joy_ratio * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
           <el-descriptions-item label="平均压力(1-10)">{{ currentProfile.mind_metrics.stress_avg || '-' }}</el-descriptions-item>
@@ -94,21 +94,21 @@
           <el-descriptions-item label="平均心情效价">{{ currentProfile.mind_metrics.mood_score_avg || '-' }}</el-descriptions-item>
         </el-descriptions>
 
-        <h4 style="margin:16px 0 8px;color:var(--adm-text-88);">行为活跃</h4>
+        <h4 class="section-title">行为活跃</h4>
         <el-descriptions :column="3" border size="small" v-if="currentProfile.action_metrics">
           <el-descriptions-item label="任务完成率">{{ currentProfile.action_metrics.task_completion_rate != null ? (currentProfile.action_metrics.task_completion_rate * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
           <el-descriptions-item label="连续打卡(天)">{{ currentProfile.action_metrics.checkin_streak || 0 }}</el-descriptions-item>
           <el-descriptions-item label="7日积分">{{ currentProfile.action_metrics.points_velocity_7d || 0 }}</el-descriptions-item>
         </el-descriptions>
 
-        <h4 style="margin:16px 0 8px;color:var(--adm-text-88);">推荐内容</h4>
-        <div v-if="currentProfile.tasks && currentProfile.tasks.length > 0" style="margin-bottom:8px;">
-          <span style="font-size:13px;color:var(--adm-text-65);">任务:</span>
-          <el-tag v-for="t in currentProfile.tasks" :key="t.id" size="small" style="margin-right:4px;">{{ t.title }}</el-tag>
+        <h4 class="section-title">推荐内容</h4>
+        <div v-if="currentProfile.tasks && currentProfile.tasks.length > 0" class="rec-block">
+          <span class="rec-label">任务:</span>
+          <el-tag v-for="t in currentProfile.tasks" :key="t.id" size="small" class="tag-mr">{{ t.title }}</el-tag>
         </div>
-        <div v-if="currentProfile.articles && currentProfile.articles.length > 0" style="margin-bottom:8px;">
-          <span style="font-size:13px;color:var(--adm-text-65);">文章:</span>
-          <el-tag v-for="a in currentProfile.articles" :key="a.id" size="small" style="margin-right:4px;">{{ a.title }}</el-tag>
+        <div v-if="currentProfile.articles && currentProfile.articles.length > 0" class="rec-block">
+          <span class="rec-label">文章:</span>
+          <el-tag v-for="a in currentProfile.articles" :key="a.id" size="small" class="tag-mr">{{ a.title }}</el-tag>
         </div>
       </div>
     </el-dialog>
@@ -189,6 +189,16 @@ export default {
 
 <style scoped>
 .admin-page { padding: 16px; }
-.filter-bar { display: flex; align-items: center; }
+.filter-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
 .el-progress { margin: 0 auto; }
+/* == C 批内联清理工具类 == */
+.w-160 { width: 160px; }
+.min-w-800 { min-width: 800px; }
+.tag-mr { margin-right: 4px; }
+.text-faint { color: var(--adm-text-25); }
+.pager-right { margin-top: 16px; text-align: right; }
+.section-title { margin: 16px 0 8px; color: var(--adm-text-88); }
+.dim-label { text-align: center; font-size: 12px; margin-top: 4px; }
+.rec-block { margin-bottom: 8px; }
+.rec-label { font-size: 13px; color: var(--adm-text-65); }
 </style>

+ 14 - 8
cfc-web/src/views/admin/PromotionManagement.vue

@@ -4,8 +4,8 @@
     <el-card>
       <el-tabs v-model="activeTab" @tab-click="handleTabChange">
         <el-tab-pane label="邀请里程碑" name="milestone">
-          <div style="margin-bottom: 16px">
-            <el-input v-model="milestoneKeyword" placeholder="搜索里程碑..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleMilestoneSearch" @clear="handleMilestoneSearch" />
+          <div class="mb-16">
+            <el-input v-model="milestoneKeyword" placeholder="搜索里程碑..." prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="handleMilestoneSearch" @clear="handleMilestoneSearch" />
     </div>
         <div class="adm-table-wrap">
 <el-table :data="filteredMilestones" v-loading="milestoneLoading" border stripe height="100%">
@@ -35,19 +35,19 @@
     </div>
     </el-tab-pane>
         <el-tab-pane label="排行榜" name="leaderboard">
-          <div style="margin-bottom: 16px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap">
+          <div class="filter-row">
             <el-date-picker
               v-model="weekStart"
               type="date"
               placeholder="选择周起始日"
               value-format="yyyy-MM-dd"
               @change="handleWeekChange"
-              style="width: 180px"
+              class="w-180"
             />
             <el-input
               v-model="searchKeyword"
               placeholder="搜索用户昵称"
-              style="width: 200px"
+              class="w-200"
               clearable
               @keyup.enter.native="handleSearch"
             />
@@ -79,7 +79,7 @@
             :page-size="leaderboardSize"
             :total="leaderboardTotal"
             layout="total, prev, pager, next"
-            style="margin-top: 20px; text-align: right"
+            class="pagination-right"
           />
         </el-tab-pane>
       </el-tabs>
@@ -91,10 +91,10 @@
           <el-input v-model="milestoneForm.milestone" disabled />
         </el-form-item>
         <el-form-item label="奖励积分" required>
-          <el-input-number v-model="milestoneForm.rewardPoints" :min="0" style="width: 200px" />
+          <el-input-number v-model="milestoneForm.rewardPoints" :min="0" class="w-200" />
         </el-form-item>
         <el-form-item label="奖励能量" required>
-          <el-input-number v-model="milestoneForm.rewardEnergy" :min="0" style="width: 200px" />
+          <el-input-number v-model="milestoneForm.rewardEnergy" :min="0" class="w-200" />
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -277,4 +277,10 @@ export default {
 .promotion-management {
   padding: 20px;
 }
+/* == C 批内联清理工具类 == */
+.mb-16 { margin-bottom: 16px; }
+.w-200 { width: 200px; }
+.w-180 { width: 180px; }
+.filter-row { margin-bottom: 16px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
+.pagination-right { margin-top: 20px; text-align: right; }
 </style>

+ 18 - 10
cfc-web/src/views/admin/ProxyReportUpload.vue

@@ -4,12 +4,12 @@
       <PageHeader slot="header" title="代传报告上传" />
       <el-form ref="uploadForm" :model="uploadForm" label-width="100px" class="upload-form">
         <el-form-item label="选择家庭" prop="familyId" required>
-          <el-select v-model="uploadForm.familyId" placeholder="请选择家庭" filterable style="width:100%" @change="onFamilyChange">
+          <el-select v-model="uploadForm.familyId" placeholder="请选择家庭" filterable class="full-width" @change="onFamilyChange">
             <el-option v-for="f in familyList" :key="f.id" :label="f.name" :value="f.id" />
           </el-select>
         </el-form-item>
         <el-form-item label="报告对象" prop="subjectId" required>
-          <el-select v-model="uploadForm.subjectId" placeholder="请选择家庭成员" filterable style="width:100%">
+          <el-select v-model="uploadForm.subjectId" placeholder="请选择家庭成员" filterable class="full-width">
             <el-option v-for="m in memberList" :key="m.id" :label="m.nickname || m.realName" :value="m.id" />
           </el-select>
         </el-form-item>
@@ -29,7 +29,7 @@
     </el-card>
 
     <!-- 解析结果预览 -->
-    <el-card v-if="previewResult" style="margin-top:20px">
+    <el-card v-if="previewResult" class="mt-20">
       <div slot="header">
         <SectionTitle title="解析结果预览">
           <StatusTag :type="previewResult.needBind ? 'warning' : 'success'" :text="previewResult.needBind ? '待绑定成员' : '已匹配成员'" />
@@ -43,14 +43,14 @@
         <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.gutHealthScore ? previewResult.payload.gutHealthScore : '-' }}</el-descriptions-item>
         <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.gutType ? previewResult.payload.gutType : '-' }}</el-descriptions-item>
       </el-descriptions>
-      <div style="margin-top:20px;text-align:right">
+      <div class="mt-20-right">
         <el-button @click="previewResult = null">取消</el-button>
         <el-button type="primary" :loading="confirming" @click="handleConfirm">确认入库</el-button>
       </div>
     </el-card>
 
     <!-- AI解读结果 -->
-    <el-card v-if="analysisResult" style="margin-top:20px">
+    <el-card v-if="analysisResult" class="mt-20">
       <div slot="header">
         <SectionTitle title="AI解读结果">
           <StatusTag type="success" size="small" text="已完成" />
@@ -60,12 +60,12 @@
     </el-card>
 
     <!-- 代传历史 -->
-    <el-card style="margin-top:20px">
+    <el-card class="mt-20">
       <div slot="header">
         <SectionTitle title="代上传历史记录" />
       </div>
       <div class="filter-bar">
-        <el-select v-model="query.type" placeholder="报告类型" clearable style="width:140px">
+        <el-select v-model="query.type" placeholder="报告类型" clearable class="w-140">
           <el-option label="全部" value=""></el-option>
           <el-option label="菌群检测" value="gut_flora"></el-option>
           <el-option label="体检报告" value="physical_exam"></el-option>
@@ -73,8 +73,8 @@
           <el-option label="保单" value="insurance"></el-option>
           <el-option label="家庭周报" value="fortune"></el-option>
         </el-select>
-        <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" clearable @keyup.enter.native="loadProxyReports"></el-input>
-        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
+        <el-input v-model="query.keyword" placeholder="搜索标题/名称" class="w-200" clearable @keyup.enter.native="loadProxyReports"></el-input>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" class="w-240"></el-date-picker>
         <el-button type="primary" @click="loadProxyReports">查询</el-button>
       </div>
 
@@ -118,7 +118,7 @@
         <el-descriptions-item label="肠型">{{ detailReport.gutType || '-' }}</el-descriptions-item>
         <el-descriptions-item label="肠道年龄">{{ detailReport.gutAge || '-' }}</el-descriptions-item>
         <el-descriptions-item label="AI解读" :span="2">
-          <div v-html="detailReport.aiAnalysis" class="analysis-content" style="max-height:300px;overflow:auto"></div>
+          <div v-html="detailReport.aiAnalysis" class="analysis-content analysis-scroll"></div>
         </el-descriptions-item>
       </el-descriptions>
       <span slot="footer"><el-button @click="detailDialogVisible = false">关闭</el-button></span>
@@ -295,4 +295,12 @@ export default {
 .pagination-wrap { margin-top: 16px; text-align: right; }
 .analysis-content { font-size: 14px; line-height: 1.8; color: var(--adm-text-88); }
 .analysis-content :deep(p) { margin: 8px 0; }
+/* == C 批内联清理工具类 == */
+.w-140 { width: 140px; }
+.w-200 { width: 200px; }
+.w-240 { width: 240px; }
+.full-width { width: 100%; }
+.mt-20 { margin-top: 20px; }
+.mt-20-right { margin-top: 20px; text-align: right; }
+.analysis-scroll { max-height: 300px; overflow: auto; }
 </style>

+ 11 - 8
cfc-web/src/views/admin/RecommendationRules.vue

@@ -104,7 +104,7 @@
         </el-form-item>
 
         <el-form-item label="关联类型" required>
-          <el-select v-model="form.ruleType" placeholder="选择类型" style="width:100%">
+          <el-select v-model="form.ruleType" placeholder="选择类型" class="full-width">
             <el-option label="共购关联" value="co_purchase"></el-option>
             <el-option label="复购关联" value="repurchase"></el-option>
             <el-option label="画像缺口关联" value="profile_gap"></el-option>
@@ -113,7 +113,7 @@
         </el-form-item>
 
         <el-form-item label="关联维度" required>
-          <el-select v-model="form.dimensionCode" placeholder="选择维度" style="width:100%">
+          <el-select v-model="form.dimensionCode" placeholder="选择维度" class="full-width">
             <el-option label="身" value="body"></el-option>
             <el-option label="智" value="wisdom"></el-option>
             <el-option label="心" value="mind"></el-option>
@@ -131,11 +131,11 @@
         </el-form-item>
 
         <el-form-item label="优先级">
-          <el-input-number v-model="form.priority" :min="1" :max="100" style="width:120px" />
+          <el-input-number v-model="form.priority" :min="1" :max="100" class="w-120" />
         </el-form-item>
 
         <el-form-item label="生效场景(多选)">
-          <el-checkbox-group v-model="form.sceneCodes" style="width:100%">
+          <el-checkbox-group v-model="form.sceneCodes" class="full-width">
             <el-option label="dimension_page" value="dimension_page"></el-option>
             <el-option label="ai_chat" value="ai_chat"></el-option>
             <el-option label="report_upload" value="report_upload"></el-option>
@@ -149,22 +149,22 @@
 
         <el-form-item>
           <el-form-item label="复购周期天数">
-            <el-input-number v-model="form.repurchaseDays" :min="1" :max="365" style="width:120px" />
+            <el-input-number v-model="form.repurchaseDays" :min="1" :max="365" class="w-120" />
           </el-form-item>
           <el-form-item v-if="form.ruleType === 'co_purchase'">
             <el-form-item label="共购时间窗口(天)">
-              <el-input-number v-model="form.coPurchaseWindowDays" :min="1" :max="90" style="width:120px" />
+              <el-input-number v-model="form.coPurchaseWindowDays" :min="1" :max="90" class="w-120" />
             </el-form-item>
           </el-form-item>
           <el-form-item v-if="form.ruleType === 'co_purchase'">
             <el-form-item label="共购计数(起始)">
-              <el-input-number v-model="form.coPurchaseCount" :min="0" style="width:120px" />
+              <el-input-number v-model="form.coPurchaseCount" :min="0" class="w-120" />
             </el-form-item>
           </el-form-item>
         </el-form-item>
 
         <el-form-item label="手动覆盖(1=运营干预优先)">
-          <el-select v-model="form.manualOverride" style="width:100%">
+          <el-select v-model="form.manualOverride" class="full-width">
             <el-option label="否(0)" value="0"></el-option>
             <el-option label="是(1)" value="1"></el-option>
           </el-select>
@@ -299,4 +299,7 @@ export default {
     float: right;
   }
 }
+/* == C 批内联清理工具类 == */
+.full-width { width: 100%; }
+.w-120 { width: 120px; }
 </style>

+ 14 - 9
cfc-web/src/views/admin/ReportTemplateManage.vue

@@ -48,7 +48,7 @@
       </PageHeader>
 
       <div class="report-template-form-wrap">
-      <el-form ref="form" :model="form" label-width="110px" style="max-width:900px">
+      <el-form ref="form" :model="form" label-width="110px" class="max-w-900">
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="模板名称" required>
@@ -57,7 +57,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="报告类型" required>
-              <el-select v-model="form.reportType" placeholder="选择报告类型" style="width:100%">
+              <el-select v-model="form.reportType" placeholder="选择报告类型" class="w-100">
                 <el-option label="肠道菌群 (gut_flora)" value="gut_flora" />
                 <el-option label="DAN测评 (dan)" value="dan" />
                 <el-option label="体检报告 (physical_exam)" value="physical_exam" />
@@ -94,10 +94,10 @@
               <!-- indicator 块配置 -->
               <template v-if="block.type === 'indicator'">
                 <el-form-item label="分组标签">
-                  <el-input v-model="block.groupLabel" placeholder="如:血脂指标" style="width:200px" />
+                  <el-input v-model="block.groupLabel" placeholder="如:血脂指标" class="w-200" />
                 </el-form-item>
                 <el-form-item label="指标名称">
-                  <el-select v-model="block.indicatorNames" multiple filterable placeholder="搜索指标名称" style="width:100%">
+                  <el-select v-model="block.indicatorNames" multiple filterable placeholder="搜索指标名称" class="w-100">
                     <el-option v-for="ind in indicatorList" :key="ind.name" :label="ind.name" :value="ind.name" />
                   </el-select>
                 </el-form-item>
@@ -106,7 +106,7 @@
               <!-- list 块配置 -->
               <template v-if="block.type === 'list'">
                 <el-form-item label="列表来源">
-                  <el-select v-model="block.listKey" style="width:200px">
+                  <el-select v-model="block.listKey" class="w-200">
                     <el-option label="菌种详情" value="flora" />
                     <el-option label="益生菌种" value="probiotic" />
                     <el-option label="分类学" value="taxonomy" />
@@ -114,14 +114,14 @@
                   </el-select>
                 </el-form-item>
                 <el-form-item label="块标题">
-                  <el-input v-model="block.title" placeholder="块显示标题" style="width:200px" />
+                  <el-input v-model="block.title" placeholder="块显示标题" class="w-200" />
                 </el-form-item>
               </template>
 
               <!-- text 块配置 -->
               <template v-if="block.type === 'text'">
                 <el-form-item label="标题">
-                  <el-input v-model="block.title" style="width:200px" />
+                  <el-input v-model="block.title" class="w-200" />
                 </el-form-item>
                 <el-form-item label="内容">
                   <el-input v-model="block.content" type="textarea" :rows="3" />
@@ -131,7 +131,7 @@
           </div>
         </div>
 
-        <el-button size="small" type="dashed" @click="addBlock" style="margin-top:8px">+ 添加固定块</el-button>
+        <el-button size="small" type="dashed" @click="addBlock" class="mt-8">+ 添加固定块</el-button>
 
         <!-- 自由显示配置 -->
         <SectionTitle title="自由显示配置" />
@@ -140,7 +140,7 @@
           <span class="hint-text">开启后,未配置在固定块的指标将追加到末尾</span>
         </el-form-item>
         <el-form-item label="追加分组名" v-if="form.freeDisplay.enabled">
-          <el-input v-model="form.freeDisplay.groupName" placeholder="如:其他指标" style="width:200px" />
+          <el-input v-model="form.freeDisplay.groupName" placeholder="如:其他指标" class="w-200" />
         </el-form-item>
 
         <el-form-item>
@@ -431,4 +431,9 @@ export default {
 .preview-table th { background: var(--adm-bg-header); font-weight: 600; }
 .preview-text { font-size: 14px; color: var(--adm-text-65); line-height: 1.7; white-space: pre-wrap; }
 .empty-hint { color: var(--adm-text-25); text-align: center; padding: 40px; }
+/* == C 批内联清理工具类 == */
+.max-w-900 { max-width: 900px; }
+.w-100 { width: 100%; }
+.w-200 { width: 200px; }
+.mt-8 { margin-top: 8px; }
 </style>

+ 16 - 9
cfc-web/src/views/admin/SubscriptionManagement.vue

@@ -2,14 +2,14 @@
   <div class="subscription-management admin-page">
     <PageHeader title="订阅管理">
       <div class="filters">
-        <el-input v-model="keyword" placeholder="搜索家庭/用户..." prefix-icon="el-icon-search" style="width: 200px;" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
-        <el-select v-model="statusFilter" placeholder="订阅状态" @change="loadList" style="width: 120px;">
+        <el-input v-model="keyword" placeholder="搜索家庭/用户..." prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
+        <el-select v-model="statusFilter" placeholder="订阅状态" @change="loadList" class="w-120">
           <el-option label="全部" value="" />
           <el-option label="激活" value="active" />
           <el-option label="已过期" value="expired" />
           <el-option label="已取消" value="cancelled" />
         </el-select>
-        <el-select v-model="levelFilter" placeholder="会员等级" @change="loadList" style="width: 120px;">
+        <el-select v-model="levelFilter" placeholder="会员等级" @change="loadList" class="w-120">
           <el-option label="全部" value="" />
           <el-option label="L1 一生一世" value="L1" />
           <el-option label="L2 久久一生" value="L2" />
@@ -27,8 +27,8 @@
         </el-table-column>
         <el-table-column label="家庭信息" min-width="180" show-overflow-tooltip>
           <template slot-scope="{ row }">
-            <div style="display:flex;align-items:center;gap:8px;">
-              <span style="font-weight:600;">{{ row.familyName || '-' }}</span>
+            <div class="flex-row">
+              <span class="font-semibold">{{ row.familyName || '-' }}</span>
               <el-tag size="mini" type="info">{{ row.memberCount || 0 }}人</el-tag>
             </div>
           </template>
@@ -120,7 +120,7 @@
 
     <!-- 取消订阅弹窗 -->
     <el-dialog title="取消订阅" :visible.sync="cancelDialogVisible" width="450px">
-      <el-alert title="确认取消以下订阅" type="warning" :closable="false" style="margin-bottom:15px;">
+      <el-alert title="确认取消以下订阅" type="warning" :closable="false" class="mb-15">
         <template slot>
           家庭:<strong>{{ cancelTarget?.familyName }}</strong><br>
           等级:{{ levelLabel(cancelTarget?.level) }}
@@ -135,7 +135,7 @@
 
     <!-- 退款弹窗 -->
     <el-dialog title="退款" :visible.sync="refundDialogVisible" width="450px">
-      <el-alert title="确认退款" type="error" :closable="false" style="margin-bottom:15px;">
+      <el-alert title="确认退款" type="error" :closable="false" class="mb-15">
         <template slot>
           订单号:<strong>{{ refundTarget?.id }}</strong><br>
           金额:<strong>{{ formatPrice(refundTarget?.amount) }}</strong>
@@ -150,7 +150,7 @@
 
     <!-- 激活弹窗 -->
     <el-dialog title="重新激活订阅" :visible.sync="activateDialogVisible" width="450px">
-      <el-alert title="确认重新激活以下订阅" type="success" :closable="false" style="margin-bottom:15px;">
+      <el-alert title="确认重新激活以下订阅" type="success" :closable="false" class="mb-15">
         <template slot>
           家庭:<strong>{{ activateTarget?.familyName }}</strong><br>
           等级:{{ levelLabel(activateTarget?.level) }}
@@ -163,7 +163,7 @@
             type="datetime"
             placeholder="选择结束时间"
             value-format="yyyy-MM-dd HH:mm:ss"
-            style="width: 240px;"
+            class="w-240"
           />
         </el-form-item>
       </el-form>
@@ -343,4 +343,11 @@ export default {
   gap: 10px;
   align-items: center;
 }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-120 { width: 120px; }
+.w-240 { width: 240px; }
+.flex-row { display: flex; align-items: center; gap: 8px; }
+.font-semibold { font-weight: 600; }
+.mb-15 { margin-bottom: 15px; }
 </style>

+ 26 - 12
cfc-web/src/views/admin/SurveyTemplates.vue

@@ -57,7 +57,7 @@
         <el-table-column label="操作" width="160" align="center">
           <template slot-scope="scope">
             <el-button type="text" size="mini" @click="openEdit(scope.row)">编辑</el-button>
-            <el-button type="text" size="mini" style="color:var(--adm-error-text)" @click="handleDelete(scope.row)">删除</el-button>
+            <el-button type="text" size="mini" class="text-error" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -70,7 +70,7 @@
           <el-input v-model="form.title" placeholder="调研标题" />
         </el-form-item>
         <el-form-item label="维度">
-          <el-select v-model="form.dimension" placeholder="适用维度" style="width:120px">
+          <el-select v-model="form.dimension" placeholder="适用维度" class="w-120">
             <el-option label="身" value="body" />
             <el-option label="智" value="wisdom" />
             <el-option label="富" value="wealth" />
@@ -79,7 +79,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="周期">
-          <el-select v-model="form.cycleType" placeholder="周期" style="width:120px">
+          <el-select v-model="form.cycleType" placeholder="周期" class="w-120">
             <el-option label="每周" value="weekly" />
             <el-option label="两周" value="biweekly" />
             <el-option label="每月" value="monthly" />
@@ -89,27 +89,27 @@
           <el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
         </el-form-item>
         <el-form-item label="题目">
-          <div style="width:100%">
-            <div v-for="(q, qIdx) in form.questions" :key="qIdx" style="margin-bottom:12px;padding:10px;border:1px solid var(--adm-border-split);border-radius:4px;">
-              <el-input v-model="q.text" placeholder="题目文本" style="margin-bottom:8px" />
+          <div class="full-width">
+            <div v-for="(q, qIdx) in form.questions" :key="qIdx" class="question-card">
+              <el-input v-model="q.text" placeholder="题目文本" class="mb-8" />
               <el-tag
                 v-for="(opt, oIdx) in q.options"
                 :key="oIdx"
                 closable
                 @close="removeOption(q, oIdx)"
-                style="margin-right:4px;margin-bottom:4px"
+                class="mr-mb-4"
               >{{ opt }}</el-tag>
               <el-input
                 v-if="addOptionText[qIdx]"
                 v-model="addOptionText[qIdx]"
                 placeholder="输入选项后回车添加"
                 size="mini"
-                style="width:200px;margin-top:4px"
+                class="w-200 mt-4"
                 @keyup.enter.native="addOption(q, qIdx)"
                 @blur="addOption(q, qIdx)"
               />
               <el-button type="text" size="mini" @click="addOptionInput(qIdx)">+ 添加选项</el-button>
-              <el-button type="text" size="mini" style="color:var(--adm-error-text)" @click="removeQuestion(qIdx)">删除题目</el-button>
+              <el-button type="text" size="mini" class="text-error" @click="removeQuestion(qIdx)">删除题目</el-button>
             </div>
             <el-button type="primary" size="mini" plain @click="addQuestion">+ 添加题目</el-button>
           </div>
@@ -124,7 +124,7 @@
     <el-dialog title="AI生成调研题目" :visible.sync="aiDialogVisible" width="500px">
       <el-form :model="aiForm" label-width="60px">
         <el-form-item label="维度">
-          <el-select v-model="aiForm.dimension" placeholder="适用维度" style="width:120px">
+          <el-select v-model="aiForm.dimension" placeholder="适用维度" class="w-120">
             <el-option label="身" value="body" />
             <el-option label="智" value="wisdom" />
             <el-option label="富" value="wealth" />
@@ -136,7 +136,7 @@
           <el-input v-model="aiForm.topic" placeholder="如:饮食习惯、睡眠质量" />
         </el-form-item>
       </el-form>
-      <div v-if="aiResult" style="margin-top:12px;padding:10px;background:var(--adm-bg-header);border-radius:4px;max-height:300px;overflow-y:auto;">
+      <div v-if="aiResult" class="ai-result-box">
         <pre>{{ aiResult }}</pre>
       </div>
       <span slot="footer">
@@ -305,4 +305,18 @@ export default {
     }
   }
 }
-</script>
+</script>
+
+<style scoped>
+.admin-page { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.w-120 { width: 120px; }
+.w-200 { width: 200px; }
+.full-width { width: 100%; }
+.mb-8 { margin-bottom: 8px; }
+.mt-4 { margin-top: 4px; }
+.mr-mb-4 { margin-right: 4px; margin-bottom: 4px; }
+.text-error { color: var(--adm-error-text); }
+.question-card { margin-bottom: 12px; padding: 10px; border: 1px solid var(--adm-border-split); border-radius: 4px; }
+.ai-result-box { margin-top: 12px; padding: 10px; background: var(--adm-bg-header); border-radius: 4px; max-height: 300px; overflow-y: auto; }
+</style>

+ 21 - 12
cfc-web/src/views/admin/ZodiacConfigs.vue

@@ -79,27 +79,27 @@
           </el-select>
         </el-form-item>
         <el-form-item label="起始日期">
-          <el-input-number v-model="form.startMonth" :min="1" :max="12" style="width:100px"></el-input-number> 月
-          <el-input-number v-model="form.startDay" :min="1" :max="31" style="width:100px"></el-input-number> 日
+          <el-input-number v-model="form.startMonth" :min="1" :max="12" class="w-100"></el-input-number> 月
+          <el-input-number v-model="form.startDay" :min="1" :max="31" class="w-100"></el-input-number> 日
         </el-form-item>
         <el-form-item label="结束日期">
-          <el-input-number v-model="form.endMonth" :min="1" :max="12" style="width:100px"></el-input-number> 月
-          <el-input-number v-model="form.endDay" :min="1" :max="31" style="width:100px"></el-input-number> 日
+          <el-input-number v-model="form.endMonth" :min="1" :max="12" class="w-100"></el-input-number> 月
+          <el-input-number v-model="form.endDay" :min="1" :max="31" class="w-100"></el-input-number> 日
         </el-form-item>
         <el-form-item label="心基础分">
-          <el-input-number v-model="form.mindBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+          <el-input-number v-model="form.mindBase" :min="0" :max="100" :precision="2" class="w-150"></el-input-number>
         </el-form-item>
         <el-form-item label="智基础分">
-          <el-input-number v-model="form.wisdomBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+          <el-input-number v-model="form.wisdomBase" :min="0" :max="100" :precision="2" class="w-150"></el-input-number>
         </el-form-item>
         <el-form-item label="身修正系数">
-          <el-input-number v-model="form.bodyModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+          <el-input-number v-model="form.bodyModifier" :min="0" :max="2" :precision="2" :step="0.05" class="w-150"></el-input-number>
         </el-form-item>
         <el-form-item label="行修正系数">
-          <el-input-number v-model="form.actionModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+          <el-input-number v-model="form.actionModifier" :min="0" :max="2" :precision="2" :step="0.05" class="w-150"></el-input-number>
         </el-form-item>
         <el-form-item label="富修正系数">
-          <el-input-number v-model="form.wealthModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+          <el-input-number v-model="form.wealthModifier" :min="0" :max="2" :precision="2" :step="0.05" class="w-150"></el-input-number>
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -109,9 +109,9 @@
     </el-dialog>
 
     <el-dialog title="操作成功" :visible.sync="successDialogVisible" width="400px" :close-on-click-modal="false">
-      <div style="text-align:center;padding:10px 0">
-        <i class="el-icon-success" style="color:var(--adm-success);font-size:32px"></i>
-        <p style="margin:12px 0 0;font-size:16px;color:var(--adm-text-88)">保存成功</p>
+      <div class="success-body">
+        <i class="el-icon-success success-icon"></i>
+        <p class="success-text">保存成功</p>
       </div>
       <div slot="footer">
         <el-button type="primary" @click="handleContinue">继续添加</el-button>
@@ -243,4 +243,13 @@ export default {
 }
 </script>
 
+<style scoped>
+/* == C 批内联清理工具类 == */
+.w-100 { width: 100px; }
+.w-150 { width: 150px; }
+.success-body { text-align: center; padding: 10px 0; }
+.success-icon { color: var(--adm-success); font-size: 32px; }
+.success-text { margin: 12px 0 0; font-size: 16px; color: var(--adm-text-88); }
+</style>
+
 

+ 17 - 9
cfc-web/src/views/admin/components/ProductSkuEditor.vue

@@ -26,8 +26,8 @@
       <el-table-column prop="stock" label="库存" width="80" />
       <el-table-column label="规格图片" width="80">
         <template slot-scope="{ row }">
-          <img v-if="row.image" :src="row.image" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
-          <span v-else style="color:var(--adm-text-45)">-</span>
+          <img v-if="row.image" :src="row.image" class="sku-thumb" />
+          <span v-else class="text-muted">-</span>
         </template>
       </el-table-column>
       <el-table-column label="启用" width="70">
@@ -46,7 +46,7 @@
 
     <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" @close="resetForm">
       <el-form ref="skuForm" :model="skuForm" :rules="skuRules" label-width="80px">
-        <el-radio-group v-model="isLinkedMode" size="small" style="margin-bottom:16px;">
+        <el-radio-group v-model="isLinkedMode" size="small" class="mb-16">
           <el-radio-button :label="false">规格变体</el-radio-button>
           <el-radio-button :label="true">关联商品</el-radio-button>
         </el-radio-group>
@@ -58,11 +58,11 @@
           </el-form-item>
           <el-form-item label="关联商品">
             <el-button size="small" @click="openProductPicker">选择商品</el-button>
-            <div v-if="skuForm.linkedProductId" style="margin-top:8px;display:flex;align-items:center;gap:10px;padding:8px;background:var(--adm-bg-header);border-radius:4px;">
-              <img v-if="skuForm.linkedProductImage" :src="skuForm.linkedProductImage" style="width:50px;height:50px;object-fit:cover;border-radius:4px;" />
+            <div v-if="skuForm.linkedProductId" class="linked-card">
+              <img v-if="skuForm.linkedProductImage" :src="skuForm.linkedProductImage" class="linked-thumb" />
               <div>
                 <div>{{ skuForm.linkedProductName }}</div>
-                <div style="font-size:12px;color:var(--adm-text-45);">¥{{ skuForm.linkedProductPrice ? (skuForm.linkedProductPrice/100).toFixed(2) : '0.00' }} | 库存 {{ skuForm.linkedProductStock }}</div>
+                <div class="linked-info">¥{{ skuForm.linkedProductPrice ? (skuForm.linkedProductPrice/100).toFixed(2) : '0.00' }} | 库存 {{ skuForm.linkedProductStock }}</div>
               </div>
               <el-button size="mini" type="text" @click="openProductPicker">更换</el-button>
             </div>
@@ -75,13 +75,13 @@
           <el-input v-model="skuForm.specs" placeholder="例如:颜色:红色;尺码:M" />
         </el-form-item>
         <el-form-item label="售价(元)" prop="price">
-          <el-input-number v-model="skuForm.price" :min="0" :precision="2" style="width: 100%;" />
+          <el-input-number v-model="skuForm.price" :min="0" :precision="2" class="full-width" />
         </el-form-item>
         <el-form-item label="原价(元)">
-          <el-input-number v-model="skuForm.originalPrice" :min="0" :precision="2" style="width: 100%;" />
+          <el-input-number v-model="skuForm.originalPrice" :min="0" :precision="2" class="full-width" />
         </el-form-item>
         <el-form-item label="库存" prop="stock">
-          <el-input-number v-model="skuForm.stock" :min="0" :precision="0" style="width: 100%;" />
+          <el-input-number v-model="skuForm.stock" :min="0" :precision="0" class="full-width" />
         </el-form-item>
         <el-form-item label="规格图片">
           <div class="sku-image-row">
@@ -521,4 +521,12 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
   border-radius: 4px;
   border: 1px solid var(--adm-border-base);
 }
+/* == C 批内联清理工具类 == */
+.sku-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
+.text-muted { color: var(--adm-text-45); }
+.mb-16 { margin-bottom: 16px; }
+.linked-card { margin-top: 8px; display: flex; align-items: center; gap: 10px; padding: 8px; background: var(--adm-bg-header); border-radius: 4px; }
+.linked-thumb { width: 50px; height: 50px; object-fit: cover; border-radius: 4px; }
+.linked-info { font-size: 12px; color: var(--adm-text-45); }
+.full-width { width: 100%; }
 </style>

+ 23 - 11
cfc-web/src/views/admin/data-source-config.vue

@@ -3,7 +3,7 @@
     <el-card>
       <PageHeader slot="header" title="健康数据源配置">
         
-        <el-input v-model="keyword" placeholder="搜索数据源..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
+        <el-input v-model="keyword" placeholder="搜索数据源..." prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
         <div class="admin-page-actions">
           <el-button type="primary" size="small" @click="handleCreate">+ 添加数据源</el-button>
         </div>
@@ -45,8 +45,8 @@
             <span class="sort-header" @click.stop="onSortClick('fileUrl', $event)">文件URL<span v-if="sortIndex('fileUrl') !== null" class="sort-index" :class="sortClass('fileUrl') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('fileUrl') }}</span>{{ sortIcon('fileUrl') }}</span>
           </template>
           <template slot-scope="scope">
-            <a v-if="scope.row.fileUrl" :href="scope.row.fileUrl" target="_blank" style="color:var(--adm-primary);font-size:12px;">查看文件</a>
-            <span v-else style="color:var(--adm-text-45)">-</span>
+            <a v-if="scope.row.fileUrl" :href="scope.row.fileUrl" target="_blank" class="text-primary">查看文件</a>
+            <span v-else class="text-muted">-</span>
           </template>
         </el-table-column>
         <el-table-column prop="createdAt" label="创建时间" width="160">
@@ -73,7 +73,6 @@
       <el-pagination
         v-if="total > 0"
         class="pagination-wrap"
-        style="margin-top:16px;text-align:right"
         :page.sync="page"
         :limit.sync="size"
         @current-change="fetchRecords"
@@ -84,10 +83,10 @@
     <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑数据源' : '添加数据源'" width="550px">
       <el-form :model="form" label-width="100px">
         <el-form-item label="成员ID" required>
-          <el-input-number v-model="form.memberId" :min="1" style="width:100%"/>
+          <el-input-number v-model="form.memberId" :min="1" class="full-width"/>
         </el-form-item>
         <el-form-item label="来源类型" required>
-          <el-select v-model="form.sourceType" placeholder="选择类型" style="width:100%">
+          <el-select v-model="form.sourceType" placeholder="选择类型" class="full-width">
             <el-option label="REPORT" value="REPORT"></el-option>
             <el-option label="PHOTO" value="PHOTO"></el-option>
             <el-option label="VOICE" value="VOICE"></el-option>
@@ -98,7 +97,7 @@
           <el-input v-model="form.dimensionCodes" placeholder="如: growth,sleep,vision"/>
         </el-form-item>
         <el-form-item label="置信度">
-          <el-input-number v-model="form.confidence" :min="0" :max="1" :precision="2" style="width:100%"/>
+          <el-input-number v-model="form.confidence" :min="0" :max="1" :precision="2" class="full-width"/>
         </el-form-item>
         <el-form-item label="解析结果">
           <el-input type="textarea" v-model="form.parsedResult" :rows="3" placeholder="JSON格式"/>
@@ -114,9 +113,9 @@
     </el-dialog>
 
     <el-dialog title="操作成功" :visible.sync="successDialogVisible" width="400px" :close-on-click-modal="false">
-      <div style="text-align:center;padding:10px 0">
-        <i class="el-icon-success" style="color:var(--adm-success);font-size:32px"></i>
-        <p style="margin:12px 0 0;font-size:16px;color:var(--adm-text-88)">保存成功</p>
+      <div class="success-body">
+        <i class="el-icon-success success-icon"></i>
+        <p class="success-text">保存成功</p>
       </div>
       <div slot="footer">
         <el-button type="primary" @click="handleContinue">继续添加</el-button>
@@ -242,4 +241,17 @@ export default {
     }
   }
 }
-</script>
+</script>
+
+<style scoped>
+.admin-page { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.text-primary { color: var(--adm-primary); font-size: 12px; }
+.text-muted { color: var(--adm-text-45); }
+.pagination-right { margin-top: 16px; text-align: right; }
+.full-width { width: 100%; }
+.success-body { text-align: center; padding: 10px 0; }
+.success-icon { color: var(--adm-success); font-size: 32px; }
+.success-text { margin: 12px 0 0; font-size: 16px; color: var(--adm-text-88); }
+</style>

+ 30 - 17
cfc-web/src/views/admin/dimension-config.vue

@@ -2,7 +2,7 @@
   <div class="norm-config admin-page">
     <el-card>
       <PageHeader slot="header" title="健康维度常模配置">
-        <el-input v-model="keyword" placeholder="搜索维度/来源..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="loadData" @clear="loadData" />
+        <el-input v-model="keyword" placeholder="搜索维度/来源..." prefix-icon="el-icon-search" class="w-200" clearable @keyup.enter.native="loadData" @clear="loadData" />
         <el-button type="primary" size="small" @click="handleCreate">+ 添加常模</el-button>
       </PageHeader>
         <div class="adm-table-wrap">
@@ -30,7 +30,7 @@
         </el-table-column>
         <el-table-column label="百分位详情" min-width="280">
           <template slot-scope="scope">
-            <span style="font-size:12px; color:var(--adm-text-45);">
+            <span class="form-hint">
               P5={{ scope.row.percentile5 }} P15={{ scope.row.percentile15 }} P25={{ scope.row.percentile25 }} |
               P50={{ scope.row.percentile50 }} |
               P75={{ scope.row.percentile75 }} P85={{ scope.row.percentile85 }} P95={{ scope.row.percentile95 }}
@@ -58,7 +58,7 @@
     <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑常模' : '添加常模'" width="600px">
       <el-form :model="form" label-width="100px">
         <el-form-item label="维度" required>
-          <el-select v-model="form.dimension" placeholder="选择维度" style="width:100%">
+          <el-select v-model="form.dimension" placeholder="选择维度" class="full-width">
             <el-option label="growth 生长发育" value="growth"></el-option>
             <el-option label="sleep 睡眠质量" value="sleep"></el-option>
             <el-option label="vision 视力健康" value="vision"></el-option>
@@ -69,41 +69,41 @@
           </el-select>
         </el-form-item>
         <el-form-item label="性别">
-          <el-select v-model="form.gender" placeholder="选择性别" style="width:100%">
+          <el-select v-model="form.gender" placeholder="选择性别" class="full-width">
             <el-option label="全部性别" value="all"></el-option>
             <el-option label="男" value="male"></el-option>
             <el-option label="女" value="female"></el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="年龄范围">
-          <el-input-number v-model="form.ageMin" :min="0" :max="240" style="width:120px" /> 月 ~
-          <el-input-number v-model="form.ageMax" :min="0" :max="240" style="width:120px" /> 月
+          <el-input-number v-model="form.ageMin" :min="0" :max="240" class="w-120" /> 月 ~
+          <el-input-number v-model="form.ageMax" :min="0" :max="240" class="w-120" /> 月
         </el-form-item>
         <el-row :gutter="10">
           <el-col :span="8">
-            <el-form-item label="P5"><el-input-number v-model="form.percentile5" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P5"><el-input-number v-model="form.percentile5" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="P15"><el-input-number v-model="form.percentile15" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P15"><el-input-number v-model="form.percentile15" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="P25"><el-input-number v-model="form.percentile25" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P25"><el-input-number v-model="form.percentile25" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="10">
           <el-col :span="8">
-            <el-form-item label="P50"><el-input-number v-model="form.percentile50" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P50"><el-input-number v-model="form.percentile50" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="P75"><el-input-number v-model="form.percentile75" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P75"><el-input-number v-model="form.percentile75" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="P85"><el-input-number v-model="form.percentile85" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P85"><el-input-number v-model="form.percentile85" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="10">
           <el-col :span="8">
-            <el-form-item label="P95"><el-input-number v-model="form.percentile95" :min="0" :max="100" style="width:100%"/></el-form-item>
+            <el-form-item label="P95"><el-input-number v-model="form.percentile95" :min="0" :max="100" class="full-width"/></el-form-item>
           </el-col>
           <el-col :span="16">
             <el-form-item label="数据来源"><el-input v-model="form.source" placeholder="如: 中国九市儿童生长发育标准"/></el-form-item>
@@ -117,9 +117,9 @@
     </el-dialog>
 
     <el-dialog title="操作成功" :visible.sync="successDialogVisible" width="400px" :close-on-click-modal="false">
-      <div style="text-align:center;padding:10px 0">
-        <i class="el-icon-success" style="color:var(--adm-success);font-size:32px"></i>
-        <p style="margin:12px 0 0;font-size:16px;color:var(--adm-text-88)">保存成功</p>
+      <div class="success-body">
+        <i class="el-icon-success success-icon"></i>
+        <p class="success-text">保存成功</p>
       </div>
       <div slot="footer">
         <el-button type="primary" @click="handleContinue">继续添加</el-button>
@@ -244,4 +244,17 @@ export default {
     }
   }
 }
-</script>
+</script>
+
+<style scoped>
+.norm-config { padding: 20px; }
+.filter-bar { display: flex; gap: 10px; align-items: center; }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.w-120 { width: 120px; }
+.full-width { width: 100%; }
+.form-hint { font-size: 12px; color: var(--adm-text-45); }
+.success-body { text-align: center; padding: 10px 0; }
+.success-icon { color: var(--adm-success); font-size: 32px; }
+.success-text { margin: 12px 0 0; font-size: 16px; color: var(--adm-text-88); }
+</style>

+ 22 - 12
cfc-web/src/views/admin/energy-config/WuxingConfig.vue

@@ -4,18 +4,18 @@
       <PageHeader slot="header" title="五行能量配置" />
 
       <!-- Section 1: Energy Source Config -->
-      <div style="margin-bottom: 30px;">
+      <div class="section-mb">
         <SectionTitle title="能量来源配置" />
-        <el-input v-model="sourceKeyword" placeholder="搜索来源类型/名称..." prefix-icon="el-icon-search" style="width: 250px; float: right; margin-bottom: 15px;" clearable @keyup.enter.native="loadSourceConfigs" @clear="loadSourceConfigs" />
-        <div style="clear: both;"></div>
+        <el-input v-model="sourceKeyword" placeholder="搜索来源类型/名称..." prefix-icon="el-icon-search" class="source-search" clearable @keyup.enter.native="loadSourceConfigs" @clear="loadSourceConfigs" />
+        <div class="clearfix"></div>
 
                 <div class="adm-table-wrap">
-<el-table :data="filteredSourceConfigs" stripe v-loading="sourceLoading" style="margin-top: 10px;" height="100%">
+<el-table :data="filteredSourceConfigs" stripe v-loading="sourceLoading" class="table-mt" height="100%">
           <el-table-column prop="sourceType" label="来源类型" width="120" show-overflow-tooltip></el-table-column>
           <el-table-column prop="sourceName" label="来源名称" width="150" show-overflow-tooltip></el-table-column>
           <el-table-column label="维度比例" min-width="300">
             <template slot-scope="scope">
-              <div v-for="ratio in getDimensionRatios(scope.row)" :key="ratio.dimensionId" style="display: inline-block; margin-right: 10px; margin-bottom: 5px;">
+              <div v-for="ratio in getDimensionRatios(scope.row)" :key="ratio.dimensionId" class="ratio-chip">
                 <StatusTag size="mini" effect="plain" :text="getDimensionLabel(ratio.dimensionId) + ': ' + (ratio.ratio / 10000) + '%'" />
               </div>
             </template>
@@ -29,7 +29,7 @@
         </el-table>
         </div>
 
-        <div style="margin-top: 15px;">
+        <div class="action-mt">
           <el-button type="primary" @click="handleCreateSource">+ 新增来源配置</el-button>
         </div>
       </div>
@@ -63,7 +63,7 @@
     <el-dialog :visible.sync="sourceDialogVisible" :title="sourceIsEdit ? '编辑来源配置' : '新增来源配置'" width="700px">
       <el-form :model="sourceForm" label-width="120px">
         <el-form-item label="来源类型" required>
-          <el-select v-model="sourceForm.sourceType" placeholder="选择来源类型" style="width: 100%">
+          <el-select v-model="sourceForm.sourceType" placeholder="选择来源类型" class="full-width">
             <el-option label="任务 (task)" value="task"></el-option>
             <el-option label="活动 (activity)" value="activity"></el-option>
             <el-option label="打卡 (streak)" value="streak"></el-option>
@@ -81,13 +81,13 @@
           </el-select>
         </el-form-item>
         <el-form-item label="来源名称" required>
-          <el-input v-model="sourceForm.sourceName" placeholder="如:任务通用、周末活动" style="width: 100%"></el-input>
+          <el-input v-model="sourceForm.sourceName" placeholder="如:任务通用、周末活动" class="full-width"></el-input>
         </el-form-item>
         <el-form-item label="维度比例">
-          <div v-for="(ratio, index) in sourceForm.ratios" :key="index" style="margin-bottom: 10px;">
+          <div v-for="(ratio, index) in sourceForm.ratios" :key="index" class="ratio-row">
             <el-row :gutter="10">
               <el-col :span="10">
-                <el-select v-model="ratio.dimensionId" placeholder="选择维度" style="width: 100%">
+                <el-select v-model="ratio.dimensionId" placeholder="选择维度" class="full-width">
                   <el-option label="身 (body)" :value="1"></el-option>
                   <el-option label="心 (mind)" :value="2"></el-option>
                   <el-option label="智 (wisdom)" :value="3"></el-option>
@@ -96,8 +96,8 @@
                 </el-select>
               </el-col>
               <el-col :span="10">
-                <el-input-number v-model="ratio.ratio" :min="0" :max="10000" :step="100" style="width: 100%" />
-                <span style="margin-left: 10px;">{{ ratio.ratio / 100 }}%</span>
+                <el-input-number v-model="ratio.ratio" :min="0" :max="10000" :step="100" class="full-width" />
+                <span class="pct-label">{{ ratio.ratio / 100 }}%</span>
               </el-col>
               <el-col :span="4">
                 <el-button size="mini" type="danger" @click="removeRatio(index)" v-if="sourceForm.ratios.length > 1">删除</el-button>
@@ -290,4 +290,14 @@ export default {
 .admin-page {
   padding: 20px;
 }
+/* == C 批内联清理工具类 == */
+.section-mb { margin-bottom: 30px; }
+.source-search { width: 250px; float: right; margin-bottom: 15px; }
+.clearfix { clear: both; }
+.table-mt { margin-top: 10px; }
+.action-mt { margin-top: 15px; }
+.ratio-chip { display: inline-block; margin-right: 10px; margin-bottom: 5px; }
+.full-width { width: 100%; }
+.ratio-row { margin-bottom: 10px; }
+.pct-label { margin-left: 10px; }
 </style>

+ 18 - 9
cfc-web/src/views/admin/gates.vue

@@ -6,7 +6,7 @@
       </PageHeader>
 
             <div class="adm-table-wrap">
-<el-table :data="gateList" border v-loading="loading" style="width:100%" height="100%">
+<el-table :data="gateList" border v-loading="loading" class="full-width" height="100%">
         <el-table-column prop="sortOrder" label="排序" width="60" align="center">
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('sortOrder', $event)">排序<span v-if="sortIndex('sortOrder') !== null" class="sort-index" :class="sortClass('sortOrder') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('sortOrder') }}</span>{{ sortIcon('sortOrder') }}</span>
@@ -60,8 +60,8 @@
         </el-table-column>
         <el-table-column label="奖励" width="140">
           <template slot-scope="scope">
-            <span v-if="scope.row.rewardPoints > 0" style="color: var(--adm-warning-text)">+{{ scope.row.rewardPoints }} CF值</span>
-            <span v-if="scope.row.rewardEnergy > 0" style="color: var(--adm-primary);margin-left:6px">+{{ scope.row.rewardEnergy }}能量</span>
+            <span v-if="scope.row.rewardPoints > 0" class="text-warning">+{{ scope.row.rewardPoints }} CF值</span>
+            <span v-if="scope.row.rewardEnergy > 0" class="text-primary-ml6">+{{ scope.row.rewardEnergy }}能量</span>
             <span v-if="!scope.row.rewardPoints && !scope.row.rewardEnergy">—</span>
           </template>
           <template slot="header">
@@ -81,7 +81,7 @@
         </el-table-column>
       </el-table>
       </div>
-      <div style="margin-top:15px;text-align:right">
+      <div class="pagination-right">
         <el-pagination
           background
           layout="total, prev, pager, next, sizes"
@@ -96,7 +96,7 @@
     </el-card>
 
     <!-- 家庭进度管理 -->
-    <el-card style="margin-top:20px">
+    <el-card class="mt-20">
       <div slot="header">
         <SectionTitle title="家庭进度管理" />
       </div>
@@ -138,7 +138,7 @@
     <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
       <el-form :model="form" :rules="formRules" ref="form" label-width="100px">
         <el-form-item label="关卡类型" prop="gateType">
-          <el-select v-model="form.gateType" style="width:100%">
+          <el-select v-model="form.gateType" class="full-width">
             <el-option label="五维自检" value="SELF_CHECK"></el-option>
             <el-option label="邀请家人" value="INVITE_FAMILY"></el-option>
             <el-option label="创建家人" value="CREATE_FAMILY_MEMBER"></el-option>
@@ -156,7 +156,7 @@
           <el-input v-model="form.description" type="textarea" :rows="2" placeholder="关卡说明文字"></el-input>
         </el-form-item>
         <el-form-item label="图标">
-          <el-input v-model="form.icon" placeholder="如:📝" style="width:120px"></el-input>
+          <el-input v-model="form.icon" placeholder="如:📝" class="w-120"></el-input>
         </el-form-item>
         <el-form-item label="排序号">
           <el-input-number v-model="form.sortOrder" :min="0" :max="999"></el-input-number>
@@ -174,7 +174,7 @@
           </el-radio-group>
         </el-form-item>
         <el-form-item label="解锁目标">
-          <el-select v-model="form.unlockTarget" style="width:100%">
+          <el-select v-model="form.unlockTarget" class="full-width">
             <el-option label="个人沙盘" value="PERSONAL_SANDBOX"></el-option>
             <el-option label="全家沙盘" value="FAMILY_SANDBOX"></el-option>
             <el-option label="孩子微行动" value="CHILD_MICRO_ACTION"></el-option>
@@ -187,7 +187,7 @@
         </el-form-item>
         <el-form-item label="条件参数">
           <el-input v-model="form.conditionParams" placeholder='如:{"count":1} 或 {"days":3}'></el-input>
-          <div style="font-size:12px;color: var(--adm-text-45);margin-top:4px">
+          <div class="hint">
             SELF_CHECK: {"minCount":1} | MICRO_ACTION: {"count":1} | STREAK_DAYS: {"days":3}
           </div>
         </el-form-item>
@@ -482,4 +482,13 @@ export default {
 .sort-header:hover {
   color: var(--adm-primary);
 }
+/* == C 批内联清理工具类 == */
+.full-width { width: 100%; }
+.w-120 { width: 120px; }
+.text-warning { color: var(--adm-warning-text); }
+.text-primary { color: var(--adm-primary); }
+.text-primary-ml6 { color: var(--adm-primary); margin-left: 6px; }
+.pagination-right { margin-top: 15px; text-align: right; }
+.mt-20 { margin-top: 20px; }
+.hint { font-size: 12px; color: var(--adm-text-45); margin-top: 4px; }
 </style>

+ 23 - 11
cfc-web/src/views/teacher/HealthPlanReview.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="health-plan-review">
     <PageHeader title="健康方案审核">
-      <el-select v-model="familyFilter" placeholder="选择家庭" clearable @change="loadPlans" size="small" style="width:180px">
+      <el-select v-model="familyFilter" placeholder="选择家庭" clearable @change="loadPlans" size="small" class="w-180">
         <el-option v-for="f in families" :key="f.id" :label="f.familyName || '家庭'+f.id" :value="f.id" />
       </el-select>
-      <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="loadPlans" size="small" style="width:140px">
+      <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="loadPlans" size="small" class="w-140">
         <el-option label="待审核" value="pending_review" />
         <el-option label="草稿" value="draft" />
         <el-option label="已驳回" value="rejected" />
@@ -13,7 +13,7 @@
     </PageHeader>
 
         <div class="adm-table-wrap">
-<el-table :data="plans" border stripe style="width:100%" v-loading="loading" height="100%">
+<el-table :data="plans" border stripe class="full-width" v-loading="loading" height="100%">
       <el-table-column prop="id" label="方案ID" width="80">
         <template slot="header">
           <span class="sort-header" @click.stop="onSortClick('id', $event)" @dblclick.stop="onSortToggle('id')">方案ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -33,7 +33,7 @@
       </el-table-column>
       <el-table-column label="维度" width="120">
         <template slot-scope="{ row }">
-          <el-tag v-for="(d,i) in (row.dimensions||'').split(',')" :key="i" size="mini" style="margin-right:4px">{{ d }}</el-tag>
+          <el-tag v-for="(d,i) in (row.dimensions||'').split(',')" :key="i" size="mini" class="mr-4">{{ d }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column prop="status" label="状态" width="100">
@@ -72,21 +72,21 @@
           <el-input v-model="editForm.planContent" type="textarea" :rows="18" placeholder="编辑方案内容..." />
         </el-form-item>
         <el-form-item label="任务条目">
-          <div v-for="(sec, si) in editTasks.sections" :key="'sec'+si" style="margin-bottom:12px;border:1px solid #ebeef5;border-radius:4px;padding:8px;">
-            <div style="font-weight:600;margin-bottom:6px;">{{ sec.title || sec.key }}</div>
-            <div v-for="(task, ti) in sec.tasks" :key="'t'+si+'-'+ti" style="display:flex;gap:6px;margin-bottom:6px;align-items:center;">
-              <el-select v-model="task.action_type" size="mini" style="width:90px">
+          <div v-for="(sec, si) in editTasks.sections" :key="'sec'+si" class="section-card">
+            <div class="section-title">{{ sec.title || sec.key }}</div>
+            <div v-for="(task, ti) in sec.tasks" :key="'t'+si+'-'+ti" class="task-row">
+              <el-select v-model="task.action_type" size="mini" class="w-90">
                 <el-option v-for="a in actionTypes" :key="a.value" :label="a.label" :value="a.value" />
               </el-select>
-              <el-select v-model="task.dimension" size="mini" style="width:80px">
+              <el-select v-model="task.dimension" size="mini" class="w-80">
                 <el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
               </el-select>
-              <el-select v-model="task.frequency" size="mini" style="width:70px">
+              <el-select v-model="task.frequency" size="mini" class="w-70">
                 <el-option label="一次性" value="once" />
                 <el-option label="每日" value="daily" />
               </el-select>
               <el-input v-model="task.title" size="mini" placeholder="任务标题" />
-              <el-input v-model="task.notes" size="mini" placeholder="备注" style="width:120px" />
+              <el-input v-model="task.notes" size="mini" placeholder="备注" class="w-120" />
               <el-button size="mini" type="danger" icon="el-icon-delete" @click="removeTask(si, ti)" />
             </div>
             <el-button size="mini" type="text" icon="el-icon-plus" @click="addTask(si)">添加任务</el-button>
@@ -314,4 +314,16 @@ export default {
 
 <style scoped>
 .health-plan-review { padding: 20px; }
+/* == C 批内联清理工具类 == */
+.w-180 { width: 180px; }
+.w-140 { width: 140px; }
+.w-120 { width: 120px; }
+.w-90 { width: 90px; }
+.w-80 { width: 80px; }
+.w-70 { width: 70px; }
+.full-width { width: 100%; }
+.mr-4 { margin-right: 4px; }
+.section-card { margin-bottom: 12px; border: 1px solid #ebeef5; border-radius: 4px; padding: 8px; }
+.section-title { font-weight: 600; margin-bottom: 6px; }
+.task-row { display: flex; gap: 6px; margin-bottom: 6px; align-items: center; }
 </style>

+ 13 - 7
cfc-web/src/views/teacher/TeacherTeam.vue

@@ -42,12 +42,12 @@
     <el-card class="table-card">
       <div slot="header">
         <SectionTitle title="团队成员">
-          <el-input v-model="keyword" placeholder="搜索成员..." prefix-icon="el-icon-search" size="small" style="width:200px" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
+          <el-input v-model="keyword" placeholder="搜索成员..." prefix-icon="el-icon-search" size="small" class="w-200" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
           <span class="card-count">共 {{ filteredMembers.length }} 人</span>
         </SectionTitle>
       </div>
             <div class="adm-table-wrap">
-<el-table :data="filteredMembers" stripe style="width: 100%;" v-loading="loading" height="100%">
+<el-table :data="filteredMembers" stripe class="full-width" v-loading="loading" height="100%">
         <el-table-column label="成员信息" min-width="200">
           <template slot="header">
             <span class="sort-header" @click.stop="onSortClick('id', $event)">成员信息<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
@@ -155,13 +155,13 @@
         <el-row :gutter="12">
           <el-col :span="12">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: var(--adm-primary)">{{ formatPriceWithSymbol(currentMember.monthlySales || 0) }}</div>
+              <div class="mini-stat-value stat-primary">{{ formatPriceWithSymbol(currentMember.monthlySales || 0) }}</div>
               <div class="mini-stat-label">本月业绩</div>
             </div>
           </el-col>
           <el-col :span="12">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: var(--adm-success)">{{ formatPriceWithSymbol(currentMember.totalSales || 0) }}</div>
+              <div class="mini-stat-value stat-success">{{ formatPriceWithSymbol(currentMember.totalSales || 0) }}</div>
               <div class="mini-stat-label">累计业绩</div>
             </div>
           </el-col>
@@ -171,19 +171,19 @@
         <el-row :gutter="12">
           <el-col :span="8">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: var(--adm-warning)">{{ currentMember.familyCount || 0 }}</div>
+              <div class="mini-stat-value stat-warning">{{ currentMember.familyCount || 0 }}</div>
               <div class="mini-stat-label">服务家庭</div>
             </div>
           </el-col>
           <el-col :span="8">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: var(--adm-primary)">{{ currentMember.childrenCount || 0 }}</div>
+              <div class="mini-stat-value stat-primary">{{ currentMember.childrenCount || 0 }}</div>
               <div class="mini-stat-label">服务孩子</div>
             </div>
           </el-col>
           <el-col :span="8">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: var(--adm-success)">{{ currentMember.taskCount || 0 }}</div>
+              <div class="mini-stat-value stat-success">{{ currentMember.taskCount || 0 }}</div>
               <div class="mini-stat-label">完成任务</div>
             </div>
           </el-col>
@@ -473,4 +473,10 @@ export default {
   color: var(--adm-text-45);
   margin-top: 4px;
 }
+/* == C 批内联清理工具类 == */
+.w-200 { width: 200px; }
+.full-width { width: 100%; }
+.stat-primary { color: var(--adm-primary); }
+.stat-success { color: var(--adm-success); }
+.stat-warning { color: var(--adm-warning); }
 </style>

+ 406 - 0
docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md

@@ -0,0 +1,406 @@
+# 规划师在线学习 · 多类别认证 · 线下打卡 · 接单闸门 设计规格
+
+日期: 2026-10-22(v2:新增「学习种类/多类别认证」+「线下课程打卡」)
+状态: 📝 设计中(待评审)
+作者: AI 助理(基于需求与现有代码梳理)
+关联: `docs/superpowers/specs/2026-07-30-service-role-apply-design.md`(服务角色统一申请,已实施)
+
+---
+
+## 1. 需求拆解
+
+| 编号 | 需求原文要点 | 系统含义 |
+|------|--------------|----------|
+| R1 | 系统应**同时承载**规划师在线学习、考核功能 | 新增「规划师培训认证」模块,与现有规划师接单/服务模块并行 |
+| R2 | 学习视频**通过小鹅通管理** | 视频内容与播放托管在外部(小鹅通),本系统只消费"学习进度/课时"(线上渠道之一) |
+| R3 | 规划师**必须完成指定课时才能考核** | 课时达标是考核的硬前置条件(未达标不可进入考核) |
+| R4 | 考核通过后才能**接单、提供服务** | 在"身份审核通过"与"可服务"之间插入认证闸门;未认证通过,对应类别服务入口关闭 |
+| **R5** | **可设置不同学习种类**(如营养师、测评师…) | 认证从「单一规划师认证」升级为「**多类别认证**」:每类别独立课程、课时要求、考核、证书;规划师可持有多个类别认证 |
+| **R6** | **支持线下课程的打卡记录** | **复用现有「活动管理」模块**承载线下培训课(发布/报名/海报二维码/可见范围),线下签到课时与线上(小鹅通)课时**合并**计入该类别总课时;不新建独立课程表 |
+
+**职责切分(设计基线)**:
+- 线上学习视频与播放 = 小鹅通(外部);课时回传/手动同步 = 本系统。
+- 线下课程**复用活动管理 `activities` 模块**承载(建课/发布/报名/海报二维码/可见范围);现场签到记录落 `activity_checkins`,课时认定与本系统认证桥接 = 本系统。
+- 课程目录映射、课时统计(线上+线下合并)、考核、认证闸门、接单管控 = 本系统。
+- 考核**自建**,不依赖小鹅通或线下工具。
+
+---
+
+## 2. 现状与冲突点(Gap Analysis)
+
+| 现状(已实施) | 缺口 |
+|----------------|------|
+| `service_role_applications`:用户申请 → 管理员审核身份 → 通过即 `Guide.status="active"`(见 `ServiceRoleApplicationServiceImpl.approve()`,planner 分支直接 `setStatus("active")`) | 没有任何学习/考核环节;审核通过即可服务,与 R4 冲突 |
+| 现有 `TrainingPlan` 是规划师给**孩子**布置的训练方案,非规划师**自身**培训 | 缺少规划师自身的学习/考核载体 |
+| 全仓检索 `小鹅通/xiaoe`:**零命中** | 小鹅通接入为全新集成点,需设计回调/同步契约(仅限线上渠道) |
+| 管家有"停接/恢复接单"开关(`accept_new_family_binding`),规划师无对应认证级开关 | 规划师的"能否接单"目前仅靠 `Guide.status`,需扩展为**类别级认证维度** |
+| 无「学习种类/多类别」概念 | 现有模型是"一个规划师一张认证",需拆为"一个规划师 × N 个类别认证" |
+| 线下课载体选型 | **决定复用主 `activities` 模块**(发布/报名/海报二维码/可见范围成熟),不新建独立课程表;其签到记录层缺失(见下),需补 `activity_checkins` 并修复"签到审核形同虚设"旧患 |
+| `guide_activities` 是空壳 | 仅 9 字段 CRUD,无报名/签到/审核/二维码;**不复用**,避免重建且语义混淆 |
+| 主 `activities` 签到记录缺失 | `Activity` 仅有 `requireCheckinReview`/`checkinPoints` 字段,无真实签到记录表(此前调研标记 check-in 从不读取);本方案一并补 `activity_checkins` 落地真实签到 |
+
+**核心改造点**:
+1. planner 审核通过后的 `status="active"` 拆为"身份已审 + 待认证 + 学习中 + 考核中 + 已认证(按类别)",仅持有对应类别认证方可提供该类别服务。
+2. 认证主表由"每用户一行"改为"每用户每类别一行"。
+3. 课时池 = 线上(小鹅通)课时 + 线下(活动签到)课时,按类别汇总。
+
+---
+
+## 3. 概念模型:学习种类(认证类别)
+
+- **学习种类 = 认证类别(certification category)**,由管理端可配置(CRUD),非硬编码。
+- 首批候选类别(可扩展):
+
+| code | 名称 | 是否基础类别 | 说明 |
+|------|------|--------------|------|
+| `planner` | 成长规划师 | ✅ 是 | 规划师基础身份,默认必学必考;获得后即 `Guide.status=active` |
+| `nutritionist` | 营养师 | 否 | 营养方案类服务的前置认证 |
+| `assessor` | 测评师 | 否 | 测评录入/解读类服务的前置认证 |
+
+- 每个类别独立配置:`required_hours`(总课时,线上+线下)、课程包、考核试卷。
+- 一名规划师可持有 1~N 个类别认证;**某类服务需持有对应类别认证**(`planner` 类别认证是接单基础,其余类别为增值服务前置)。
+
+---
+
+## 4. 规划师生命周期状态机(按类别)
+
+```
+         申请身份(service_role_applications)
+                    │  管理员审核身份
+                    ▼
+           identity_approved  (Guide.status = pending_cert)
+                    │  自动建「每类别」planner_certifications(均 pending_learning)
+                    ▼
+   ┌─────────────────────────────────────────────┐
+   │  每类别独立流转(以 nutritionist 为例):        │
+   │                                               │
+   │  pending_learning                             │
+   │   (线上小鹅通课时 + 线下活动签到课时 累计)      │
+   │        │  completed_hours >= required_hours   │
+   │        ▼                                      │
+   │  learning_completed  (开放该类别考核)          │
+   │        │  考核通过 score >= pass_score         │
+   │        ▼                                      │
+   │  certified  (持有该类别证书,可提供该类别服务)  │
+   └─────────────────────────────────────────────┘
+                    │  至少持有基础类别(planner) certified
+                    ▼
+           Guide.status = active  (可接单基础服务)
+                    │
+   增值类别(nutritionist/assessor) certified ──▶ 可接对应增值服务
+
+   异常/回流路径(每类别独立):
+   learning_completed ──考核未过──▶ exam_failed ──可重考──▶ learning_completed
+   exam_failed 累计次数超限 ──▶ 管理员复核 / 退回 pending_learning
+   certified(若启用有效期)过期 ──▶ suspended ──复训达标后──▶ certified
+   某类别 suspended 不影响其他类别 certified 状态
+```
+
+> `Guide.status` 取值:`pending_cert`(身份已审、尚无任何类别认证)/ `active`(至少基础类别 certified)/ `suspended`(如启用有效期且全部过期)。
+> `planner_certifications.status`(每用户每类别):`pending_cert` / `learning_completed` / `certified` / `suspended`。
+> `learning_status`:`pending_learning` / `learning_completed`;`exam_status`:`none` / `exam_failed` / `exam_passed`。
+
+---
+
+## 5. 小鹅通集成方案(线上渠道,关键设计决策)
+
+### 5.1 推荐模式:H5/小程序 SDK 嵌入 + 开放平台 Webhook 回传
+- **播放侧**:规划师端以 WebView 嵌入小鹅通课程 H5,或调用小鹅通小程序 SDK;视频与版权在小鹅通。课程须归属某「学习种类」(`planner_courses.category_id`)。
+- **回传侧**:注册小鹅通开放平台回调(消息推送),监听"课时完成 / 课程完成"事件。
+- **本系统只存**:课程目录映射、每用户每课程进度、总完成课时;不存视频。
+
+### 5.2 鉴权与契约
+- 配置项(`application.yml`):`xiaoe.app-id`、`xiaoe.app-secret`、`xiaoe.token`、`xiaoe.callback-path`。
+- `XiaoeWebhookController`:校验回调签名 → 解析事件 → 写入 `planner_course_progress` → 重算该类别 `planner_certifications.completed_hours`(仅累加线上部分)→ 达标则 `pending_learning → learning_completed`。
+- 幂等:以(user_id, 小鹅通 resource_id, 事件ID)去重。
+
+### 5.3 无凭证期 Fallback(保证可先开发、可演示)
+- `XiaoeAdapter` 接口,两种实现:`XiaoeWebhookAdapter`(真实)与 `XiaoeManualAdapter`(沙箱)。
+- 管理端"手动同步课时"入口:可录入/批量导入每规划师每课程完成课时。
+- 上线切换:配置开关 `xiaoe.mode = webhook | manual`,无需改代码。
+
+---
+
+## 6. 线下课程与打卡(复用活动管理模块,R6)
+
+> **选型决定(已与用户确认)**:线下培训课**不复用 `guide_activities`(空壳)**,而是**复用主 `activities` 模块**作为"事件容器",通过一张映射表桥接到认证。主 `activities` 已具备发布/报名/海报二维码/可见范围,可省去重建事件 CRUD;但其签到记录层缺失,本方案一并补 `activity_checkins` 并修复"签到审核形同虚设"的旧患。
+
+### 6.1 建课(管理端,复用 activities)
+- 在现有 `activities` 模块创建活动(标题/地点/时间/海报二维码),并设 `visibleScope='role'`、`visibleTo='["teacher"]'`,使其仅对规划师可见。
+- 通过 `planner_offline_course_map` 把该活动**映射**到某一学习种类:`(activity_id, category_id, duration_hours)`——即"此活动签到后给该类别计多少课时"。
+- 一门活动可映射一个类别(v1);活动本身仍是通用活动,培训语义只存在于映射表,不污染 `activities` 主表。
+
+### 6.2 报名与签到(规划师端 / 管理端,复用 activities)
+- **报名**:规划师(role=teacher,作为 `activity_registrations.userId`)报名参加该培训活动——复用现有报名链路。
+- **扫码签到(主)**:现场扫活动签到二维码(编码 activity_id + 签名 token)→ `POST /api/planner/training-activities/check-in {qr_token}` → 校验 → 写 `activity_checkins`(check_in_type='qr',user_id=规划师)→ 桥接发放课时。
+- **管理端手动补录(fallback)**:`check_in_type='admin'`、`check_in_by=管理员`,补漏扫。
+- **GPS 签到(可选,见 D7)**:预留 `check_in_type='gps'`,v1 默认不启用。
+
+### 6.3 课时桥接与合并
+- 签到成功后,桥接逻辑按 `planner_offline_course_map.duration_hours` 给该规划师对应类别的 `planner_certifications.completed_hours` 累加(幂等)。
+- 某类别 `planner_certifications.completed_hours` = Σ(该类别线上 `planner_course_progress.completed_hours`) + Σ(该类别 `activity_checkins` 经映射表发放的课时)。
+- 任一部分达标即可翻转 `learning_completed`;线下活动可独立撑满课时(不强制线上,见 D8)。
+- 幂等:`activity_checkins` 设 `uk_activity_user (activity_id, user_id)`,一人一活动一次签到;重复签到不重复计课时。
+
+---
+
+## 7. 数据模型(DDL 草案 v2)
+
+```sql
+-- ① 学习种类 / 认证类别(管理端可配置)
+CREATE TABLE `planner_cert_categories` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `code` varchar(32) NOT NULL COMMENT '类别编码: planner/nutritionist/assessor ...',
+  `name` varchar(64) NOT NULL COMMENT '类别名称',
+  `description` varchar(255) DEFAULT '',
+  `required_hours` decimal(6,2) NOT NULL DEFAULT 0 COMMENT '该类别要求总课时(线上+线下)',
+  `is_base` tinyint(1) DEFAULT 0 COMMENT '1=基础类别(获得即 Guide.status=active)',
+  `order_index` int(11) DEFAULT 0,
+  `status` tinyint(4) DEFAULT 1 COMMENT '1启用 0停用',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_code` (`code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师学习种类/认证类别';
+
+-- ② 线上课程目录(映射小鹅通课程/专栏)
+CREATE TABLE `planner_courses` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `xiaoe_resource_id` varchar(64) DEFAULT '' COMMENT '小鹅通课程/商品ID',
+  `title` varchar(120) NOT NULL,
+  `required_hours` decimal(6,2) NOT NULL DEFAULT 0 COMMENT '该课程要求课时',
+  `order_index` int(11) DEFAULT 0,
+  `is_required` tinyint(1) DEFAULT 1 COMMENT '是否必修',
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_category` (`category_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程目录(小鹅通)';
+
+-- ③ 每用户每线上课程学习进度(来自小鹅通回传/手动)
+CREATE TABLE `planner_course_progress` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `course_id` int(11) NOT NULL,
+  `category_id` int(11) NOT NULL,
+  `xiaoe_resource_id` varchar(64) DEFAULT '',
+  `progress_percent` int(11) DEFAULT 0,
+  `completed_hours` decimal(6,2) DEFAULT 0,
+  `completed` tinyint(1) DEFAULT 0,
+  `last_synced_at` datetime DEFAULT NULL,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_course` (`user_id`,`course_id`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程学习进度';
+
+-- ④ 活动→认证类别映射(桥接表:线下培训课复用 activities 模块)
+CREATE TABLE `planner_offline_course_map` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `activity_id` bigint NOT NULL COMMENT '关联 activities.id(线下培训活动)',
+  `category_id` int(11) NOT NULL COMMENT '关联 planner_cert_categories.id',
+  `duration_hours` decimal(6,2) DEFAULT 0 COMMENT '该活动签到后计入的课时',
+  `is_required` tinyint(1) DEFAULT 0 COMMENT '是否必修环节',
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity` (`activity_id`),
+  KEY `idx_category` (`category_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线下培训活动↔认证类别映射(桥接)';
+
+-- ⑤ 活动签到记录(复用 activities 模块,一并修复"签到审核形同虚设"旧患)
+CREATE TABLE `activity_checkins` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `activity_id` bigint NOT NULL,
+  `user_id` int(11) NOT NULL COMMENT '签到人(规划师=userId)',
+  `registration_id` bigint DEFAULT NULL COMMENT '关联报名记录',
+  `check_in_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
+  `check_in_type` varchar(16) DEFAULT 'qr' COMMENT 'qr扫码/admin手动/gps',
+  `check_in_by` int(11) DEFAULT NULL COMMENT 'admin手动补录人',
+  `review_status` varchar(16) DEFAULT 'approved' COMMENT 'approved/pending/rejected(对应原 requireCheckinReview)',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity_user` (`activity_id`,`user_id`),
+  KEY `idx_user` (`user_id`),
+  KEY `idx_activity` (`activity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动签到记录(通用,供培训课时桥接)';
+
+-- ⑥ 规划师认证主表(每用户每类别一行)
+CREATE TABLE `planner_certifications` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `guide_id` bigint DEFAULT NULL,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `category_code` varchar(32) DEFAULT '' COMMENT '冗余类别编码,便于查询',
+  `required_hours` decimal(6,2) DEFAULT 0 COMMENT '该类别总要求课时',
+  `completed_hours` decimal(6,2) DEFAULT 0 COMMENT '已完成课时(线上+线下汇总)',
+  `learning_status` varchar(20) DEFAULT 'pending_learning' COMMENT 'pending_learning/learning_completed',
+  `exam_status` varchar(20) DEFAULT 'none' COMMENT 'none/exam_failed/exam_passed',
+  `exam_score` int(11) DEFAULT NULL,
+  `exam_attempts` int(11) DEFAULT 0,
+  `cert_no` varchar(64) DEFAULT '' COMMENT '证书编号',
+  `certified_at` datetime DEFAULT NULL,
+  `expire_at` datetime DEFAULT NULL COMMENT 'NULL=不过期',
+  `status` varchar(20) DEFAULT 'pending_cert' COMMENT 'pending_cert/learning_completed/certified/suspended',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_category` (`user_id`,`category_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师每类别培训认证主表';
+
+-- ⑦ 考核试卷(每类别可有多套)
+CREATE TABLE `planner_exam_papers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `title` varchar(120) NOT NULL,
+  `pass_score` int(11) NOT NULL DEFAULT 60,
+  `duration_minutes` int(11) DEFAULT 30,
+  `question_count` int(11) DEFAULT 0,
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_category` (`category_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核试卷';
+
+-- ⑧ 考核题库
+CREATE TABLE `planner_exam_questions` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `paper_id` int(11) NOT NULL,
+  `question` text NOT NULL,
+  `options` text COMMENT 'JSON数组',
+  `answer` varchar(10) COMMENT '正确选项',
+  `score` int(11) DEFAULT 1,
+  `order_index` int(11) DEFAULT 0,
+  PRIMARY KEY (`id`),
+  KEY `idx_paper` (`paper_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考核题库';
+
+-- ⑨ 考核作答记录
+CREATE TABLE `planner_exam_attempts` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `category_id` int(11) NOT NULL,
+  `paper_id` int(11) NOT NULL,
+  `score` int(11) DEFAULT 0,
+  `passed` tinyint(1) DEFAULT 0,
+  `answers` text COMMENT 'JSON 作答明细',
+  `started_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `submitted_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核作答记录';
+```
+
+> `Guide` 表无需新增列:`planner_certifications` 承载类别级认证维度;`Guide.status` 在基础类别 `certified` 时由 `pending_cert` 翻为 `active`。
+
+---
+
+## 8. 考核(本系统自建,按类别)
+
+- **管理端**:试卷/题库维护(每类别独立,增删题、设及格线、限时);支持多套试卷。
+- **规划师端-考核入口**:仅当该类别 `planner_certifications.learning_status = learning_completed` 才开放;否则提示"请先完成必修课时(当前 X/Y 课时,含线上+线下)"。
+- **作答与判分**:限时作答 → 提交即判分 → 写入 `exam_score`/`exam_passed`/`planner_exam_attempts` → 通过则 `exam_status=exam_passed`、触发 `certified` 状态流转、生成 `cert_no`。基础类别通过时同步翻 `Guide.status=active`。
+- **重考**:未过可重考,`exam_attempts` 累加;连续超限(可配,如 3 次)转管理员复核。
+
+---
+
+## 9. 接单 / 服务闸门(类别感知,核心拦截)
+
+**中央守卫**:`PlannerCertificationGuard.assertCertified(userId, categoryCode)` —— 查询该类别 `planner_certifications.status == certified`(且 `Guide.status == active`)方可放行,否则抛 `Result.error("规划师未完成【类别名】培训认证,暂不可提供该类服务")`。
+
+**服务动作 → 所需类别映射**(代码级 enum/常量,后续可下沉为管理端配置表):
+
+| 服务动作 | 现有入口(参考 guide-system-flow) | 所需类别 | 拦截 |
+|----------|----------------------------------|----------|------|
+| 创建/上架服务套餐(基础) | `GuidePackageController.create/update-status` | `planner` | ✅ |
+| 生成家庭绑定邀请(开始服务) | `GuideFamilyController.bind/invite` | `planner` | ✅ |
+| 激活成长方案(下发任务) | `PlanReviewController.activate` | `planner` | ✅ |
+| 营养方案 / 饮食建议 | `GuidePackageController` 营养类目 / `GuideRecordController` 营养记录 | `nutritionist` | ✅ |
+| 测评录入 / 解读 | `GuideAssessmentController` | `assessor` | ✅ |
+| 接单(订单/咨询响应) | `GuideOrderController`、`TeacherMessageController.reply` | `planner`(基础)+ 具体增值类别按服务内容判定 | ✅ |
+
+**两端一致**:
+- 规划师端"服务"类入口在缺少对应类别认证时**置灰**并提示认证进度(含线上+线下课时);
+- 服务端守卫**拒绝**非认证请求(防绕过);
+- 管理端"规划师认证看板"按类别展示:课时进度(线上/线下拆分)、考核分、证书号、有效期。
+
+---
+
+## 10. 接口清单(草案 v2)
+
+### 规划师端
+- `POST /api/planner/certification/my` → 我的全部类别认证状态(课时、线上/线下拆分、是否可考、证书)
+- `POST /api/planner/courses` → 某类别线上课程列表(含小鹅通跳转/H5 链接)
+- `POST /api/planner/training-activities` → 可报名/签到的培训活动列表(复用 activities,过滤映射表+规划师可见)
+- `POST /api/planner/training-activities/check-in` → 扫码/手动签到(body: qr_token 或 activity_id+admin)→ 桥接发放课时
+- `POST /api/planner/exam/start` → 开始某类别考核(前置校验课时达标)
+- `POST /api/planner/exam/submit` → 提交判分
+- `POST /api/planner/exam/result` → 考核结果
+
+### 管理端
+- `POST /api/admin/planner-cert-categories/*` 学习种类 CRUD(含 required_hours、is_base)
+- `POST /api/admin/planner-courses/*` 线上课程目录 CRUD(挂类别)
+- `POST /api/admin/planner-offline-course-map/*` 培训活动↔类别映射 CRUD(在 activities 建课后关联类别与计课时)
+- `POST /api/admin/activity-checkins/*` 签到记录查询 / 手动补录
+- `POST /api/admin/planner-exam/*` 试卷/题库 CRUD(挂类别)
+- `POST /api/admin/planner-certifications/sync-hours` 手动同步线上课时(fallback)
+- `POST /api/admin/planner-certifications/list` 认证看板(按类别)
+
+### 小鹅通回调
+- `POST /api/xiaoe/webhook` 线上学习进度/课程完成事件(签名校验、幂等)
+
+---
+
+## 11. 前端页面(草案)
+
+- 规划师端:新增「培训认证」中心页(按类别 Tab:课时进度条【线上+线下】+ 线上课程入口 + 线下课扫码打卡 + 考核入口)。
+- 管理端:学习种类管理、线上课程管理、线下课程/打卡管理、考核题库管理、规划师认证看板(按类别)。
+- 复用现有 `pages/profile/service-role-apply` 之后的流向,新增 `planner` 子包页面。
+
+---
+
+## 12. 实施顺序(建议)
+
+1. 数据库迁移(9 张表 + 类别种子数据 `planner_cert_categories` 首批 3 类)+ `DatabaseInitializer` 迁移。
+2. 后端实体/Mapper/Service:`PlannerCertCategoryService`、`PlannerCourseService`、`OfflineCourseService`(建课+打卡)、`PlannerExamService`、`XiaoeAdapter`(webhook+manual)。
+3. `ServiceRoleApplicationServiceImpl` planner 分支改造:审核通过 → 建 Guide(`pending_cert`)+ 建**每类别** `planner_certifications`。
+4. `PlannerCertificationGuard.assertCertified(userId, categoryCode)` + 服务入口注入(基础类别 + 增值类别映射)。
+5. 小鹅通 Webhook 接入 + 线上课时手动同步 fallback。
+6. 线下培训课:复用 `activities` 建课/报名/海报二维码(可见范围限定规划师)+ 落地 `activity_checkins` 真实签到(修复旧患)+ `planner_offline_course_map` 桥接 + 签到→课时入账。
+7. 规划师端「培训认证」中心页(类别 Tab + 线上/线下课时 + 打卡 + 考核)。
+8. 管理端:类别/课程/线下课/题库/认证看板。
+9. E2E:身份审核通过 → 课时未达标禁考 → 线上+线下课时达标 → 考核通过 → 对应类别接单解禁。
+
+---
+
+## 13. 待确认决策(需你拍板)
+
+| # | 决策点 | 本设计默认假设 | 影响 |
+|---|--------|----------------|------|
+| D1 | 小鹅通接入深度 | 开放平台 Webhook 回传 + 手动同步 fallback(开发期先 manual) | 决定是否需要申请小鹅通开放平台权限 |
+| D2 | **首批学习种类范围 + 基础类别是否必须** | 首批 3 类(planner 基础 / nutritionist / assessor);`planner` 为必须基础,其余为增值 | 决定类别种子数据与接单映射表;新增类别走管理端 CRUD |
+| D3 | 考核防作弊强度 | 初版:限时 + 固定及格线,不做随机/人脸 | 影响题库规模与前端复杂度 |
+| D4 | 证书有效期 / 复训 | 初版:`expire_at = NULL`(不过期) | 若需复训,`suspended` 回流路径要启用 |
+| D5 | "接单"具体动作集合 | 按第 9 节映射表统一闸门(基础类别 + 增值类别按服务内容) | 若"接单"仅指订单类,可缩小拦截范围 |
+| D6 | 现有已通过的规划师如何处理 | 存量 Guide 按"已持有基础类别 certified/active"补登(其余类别 pending_learning),不追溯补训 | 避免存量规划师被误封 |
+| **D7** | **线下课载体与打卡方式(已定)** | 载体**复用 `activities`** + `planner_offline_course_map` 桥接(不建独立课程表、不复用 guide_activities);打卡主用**扫码签到**(活动签到二维码),管理端手动补录为 fallback;GPS 签到 v1 不启用 | 复用一并修复 activities 原"签到审核形同虚设"缺陷 |
+| **D8** | **线下课时是否可完全替代线上** | 默认**可合并撑满**(不强制线上最低占比),线下课即可独立达标 | 若要求"线上必修保底",需在类别上增 `min_online_hours` 约束 |
+
+---
+
+## 14. 风险与边界
+
+- **外部依赖**:小鹅通 Webhook 稳定性/延迟 → 以 `last_synced_at` + 手动同步兜底,不直接阻塞规划师学习。
+- **线下防作弊**:扫码二维码需带有效期与一次性/课程绑定,避免截图代打卡;管理端手动补录留痕(`check_in_by`)。
+- **课时口径一致性**:线上、线下、合计三者在看板与闸门须同源计算(`completed_hours` 单一汇总字段),避免双写不一致。
+- **合规**:考核不得承诺"包过/保收入";课程与证书表述需符合现有《对外表达禁用词表》口径(宝妈+儿童敏感)。
+- **幂等**:Webhook 重放、手动同步、重复打卡均需去重(见各表唯一键)。
+- **存量兼容**:D6 处理不当会误封已服务规划师,需灰度上线。

+ 214 - 0
docs/整改方案-活动管理差异修复-2026-10-22.md

@@ -0,0 +1,214 @@
+# 活动管理差异修复整改方案
+
+- **日期**:2026-10-22
+- **关联调研**:`docs/调研报告-活动管理实现vs需求-2026-10-22.html`
+- **范围**:cfc-backend 活动域全流程差异修复
+- **状态**:✅ 决策已锁定(2026-10-22 用户拍板),待分批改实现
+
+---
+
+## 〇、决策锁定记录(2026-10-22)
+
+| # | 议题 | 决策 |
+|---|------|------|
+| D1 | 签到是否要审核 | **在活动里定义**(活动级开关 `requireCheckinReview`),开启时签到进入"待审核",由管理端/规划师端审核通过后生效。→ 落地功能(原方案 A)|
+| D2 | 活动总结能力 | **清理死代码**:不做自动聚合/AI 总结,删除 DTO 的 `summarizing` 死值声明与注释、清理 `applyDisplayState` 中不可能产出的分支。→ 原方案 B |
+| D3 | 规划师活动 / 履约活动 | **删除**两套独立子系统(`guide_activities`、`activity_provisions`),其能力并入主活动 `activities`;删除前先做依赖影响面审计。→ 原方案 P2-2 升级为"删除+整合" |
+| D4 | 报名是否需要审核 | **默认免审核**:活动级开关 `requireReview`(默认 false)。仅当活动显式开启"需审核"时,报名进入 pending,审核通过(approve)才占用名额;免审核活动报名即 approved,立即占用名额。原"免费活动强制审核"逻辑撤销。 |
+
+> 关键语义统一:**"是否需审核"是活动级开关**(报名审核 `requireReview`、签到审核 `requireCheckinReview`),两开关默认均关闭。名额占用统一发生在"状态变为 approved"时点。
+
+---
+
+## 一、目标与原则
+
+1. **先修阻断正确性的项(P0)**:名额失控、取消报名主路径断裂、无退款、签到审核字段失效——四项直接影响业务正确性与用户信任。
+2. **再修一致性与完整性(P1)**:计数口径、发布范式、评价门槛、总结死代码清理。
+3. **收口架构(P2)**:删除规划师活动/履约活动子系统,整合进主活动;过期结束定时化。
+4. **最小侵入 + 可回滚**:每个修复独立提交,便于按项回退;涉及支付/退款与删表改动必须做影响面评估与回归。
+
+---
+
+## 二、整改项清单(已锁定)
+
+| 编号 | 级别 | 问题 | 涉及文件 | 方案要点 | 决策 |
+|------|------|------|----------|----------|------|
+| P0-1 | 🔴 高 | 名额 maxParticipants 报名时不校验 | `ActivityRegistrationService`、`Activity` | 名额占用发生在"变 approved"时点;活动级 `requireReview` 默认 false(免审即报即占,需审审过才占),原子自增+上限约束防超卖 | D4 |
+| P0-2 | 🔴 高 | 取消报名仅支持 pending;无退款、无名额回滚 | `cancel`、`ActivityOrderService`、`PaymentService` | 支持 approved 取消;付费联动退款;原子回退名额 | — |
+| P0-3 | 🔴 高 | 签到审核字段 requireCheckinReview 失效 | `checkin`、新增审核端点 | **落地功能**:按活动级开关,开启时签到进"待审核",审核通过生效并发积分 | D1 |
+| P1-1 | 🟡 中 | currentParticipants 实为签到计数 | `Activity`、`ActivityService` | 拆分报名人数/签到人数字段,统一口径 | — |
+| P1-2 | 🟡 中 | 发布范式不一致 | `publish`、`ActivityAdminService` | 两路统一要求 `dimensionCode` 非空 | — |
+| P1-3 | 🟡 中 | 评价无门槛、无去重、status 恒 0 | `ActivityFeedbackService.submit` | 校验已报名且已签到;按 child 去重 | — |
+| P1-4 | 🟡 中 | 总结死代码 | `ActivityDTO`、`ActivityService` | **清理死代码**:删 `summarizing` 声明与不可能分支 | D2 |
+| P2-1 | 🟢 低 | 过期结束仅查询触发+延迟7天 | `autoEndExpiredActivities` | 改为 Spring @Scheduled 定时任务 | — |
+| P2-2 | 🟢 低→中 | 三套子系统并存 | 架构层 | **删除** `guide_activities`、`activity_provisions`,整合进主活动(删除前做依赖审计) | D3 |
+
+---
+
+## 三、详细方案
+
+### P0-1 名额原子校验 + 报名审核活动级开关(防超卖)
+
+**现状**:`register`(:75) / `batchRegister`(:39) 与 `checkRegistrationGate`(:318) 均不读 `maxParticipants`;且当前免费活动默认强制审核,与"默认免审"诉求不符。
+
+**决策(D4)**:报名是否需审核由活动级开关 `requireReview`(默认 false)决定:
+- `requireReview = false`(默认):报名即 `approved`,**立即占用名额**;
+- `requireReview = true`:报名进入 `pending`,**仅审核通过(approve)才占用名额**;审核拒绝则释放/不占。
+
+**方案**:
+1. 实体 `Activity` 增加 `requireReview`(tinyint,默认 0)。`requireCheckinReview` 沿用既有字段(D1)。
+2. `register`/`batchRegister` 中:若 `requireReview`,`status=pending`;否则 `status=approved` 并走名额占用。
+3. 名额占用统一在"变 approved"处用原子 SQL 约束:
+   `UPDATE activities SET current_participants = current_participants + 1 WHERE id = ? AND current_participants < max_participants`
+   以 affected rows 判断是否成功;失败返回"名额已满"。
+   涉及两处:
+   - `ActivityRegistrationService.approve`(:105)(需审活动审过)
+   - `ActivityOrderService.writeRegistrations`(付费活动支付成功写 approved)
+4. 撤销"免费活动强制审核"旧逻辑(`checkRegistrationGate`/register 中对免费即 pending 的强制),改为读 `requireReview`。
+5. `getRegistrationRoster`(:225) 的"剩余名额"读取同一计数,去掉 `Math.max(0,…)` 兜底误导。
+
+**影响**:免费活动默认改为免审即过;需审活动才进审核流——符合 D4。需处理并发(原子 SQL 已解决)。
+**测试**:并发 20 通过审核/支付,名额=10 时仅 10 条 approved;免审活动报名即满员受限额约束。
+
+---
+
+### P0-2 取消报名支持 approved + 退款/名额回滚
+
+**现状**:`cancel`(:97) 用 `findRegistration(...,"pending")` 过滤,已 approved/已支付报名无法取消,且全程无退款、无名额回滚。D4 落地后绝大多数报名为 approved,本问题影响面更大,**必修**。
+
+**方案**:
+1. `cancel` 放宽到 `status IN ('pending','approved')`;`findRegistration` 去掉仅 pending 限制(或新增重载)。
+2. 判定付费:`hasPaidOrder(activityId, userId)`(:333)。命中已支付订单则:
+   - 调 `PaymentService.refund(orderNo, amount)` 原路退款;
+   - 成功后 `ActivityOrder.status='refunded'`,报名记录置 `cancelled`。
+3. 名额回滚:取消 approved 时原子 `current_participants = current_participants - 1 WHERE id = ? AND current_participants > 0`。
+4. 实体加 `cancelledAt`、`cancelReason`(可选)字段。
+5. 退款为外部异步调用,加失败重试与告警;退款失败时不置 cancelled,返回"退款处理中/失败"。
+
+**影响**:触及支付资金流,必须回归;建议先小额订单验证退款链路(D4 已明确"需审才占名额",但付费报名通常免审即占,取消即触发退款,务必验证)。
+**测试**:免费 approved→取消成功、名额-1;付费 approved→取消触发退款、订单 refunded、名额-1;pending→取消不变更资金。
+
+---
+
+### P0-3 签到审核字段落地(按活动级开关)
+
+**现状**:实体有 `requireCheckinReview`(D1 确认在活动里定义),但 `checkin`(:198) 从不读取,一律直接 `Task.status="completed"`。
+
+**方案(已锁定 A)**:
+1. `checkin`(:198) 中读取活动 `requireCheckinReview`:
+   - `=1`:生成 `Task.status='pending_review'`(待审核),返回 `needReview=true`;不立即发积分/能量。
+   - `=0`(默认):行为同现状,`Task.status='completed'` 并发积分/能量。
+2. 新增端点 `POST /api/admin/activity/checkin/review {taskId, action}`:
+   - `action=approve` → `Task.status='completed'` 并发积分/能量;
+   - `action=reject` → `Task.status='rejected'`(或删除 Task),不发积分。
+3. `applyDisplayState`(:527) 识别"已签到待审核"展示态(如 `checked_in_pending_review`),前端据 `needReview` 提示。
+4. 规划师端若需自审,复用同一审核端点(权限按角色放开)。
+
+**影响**:改动签到与 Task 状态机、新增端点;属需求内功能落地,非新需求。
+**测试**:开启审核→签到状态 pending_review→审核通过发积分/未通过不发;关闭时同现状。
+
+---
+
+### P1-1 currentParticipants 语义修正
+
+**现状**:`checkin`(:354) 中 `currentParticipants+1` 实为"签到人数",与 `maxParticipants`(报名名额)非同一口径。
+
+**方案**:
+- 新增字段 `signedCount`(签到人数),`checkin` 时 `+1`;
+- 报名名额占用(P0-1)使用独立计数(`currentParticipants` 明确为"已通过报名人数");
+- 对外明确区分:报名人数(approved 计数)、签到人数(signedCount)。
+
+**测试**:报名 10、签到 6 时,报名人数=10、签到人数=6、剩余名额=max-10。
+
+---
+
+### P1-2 发布范式统一
+
+**现状**:C 端 `publish`(:173) 不校验维度;管理端 `auditActivity` 强制五维权重=100%。
+
+**方案**:
+- 明确两类入口语义:**管理后台直发**(受信任角色)与**需审核提交**(走审核流)。
+- 统一维度校验:**两路都要求 `dimensionCode` 非空**;审核流的"权重合计=100%"作为审核专属强校验保留,直发不强要求权重但须有 dimensionCode。
+- `publish` 增加 `if (dimensionCode == null) return error("请先设置活动维度")`。
+
+**测试**:直发无 dimensionCode 被拒;审核流权重≠100% 被拒。
+
+---
+
+### P1-3 评价门槛与去重
+
+**现状**:`ActivityFeedbackService.submit`(:20) 直接 insert,无门槛、无去重,`status` 恒 0。
+
+**方案**:
+- 提交前校验:活动已结束(`status=ended` 或 `now.after(endTime)`);且 `isApproved` 且 `hasCheckedIn`(已报名且已签到),否则"仅参与活动的成员可评价"。
+- 去重:同一 `activityId + childId` 仅一条,重复提交更新而非新增。
+- `status` 复用:启用评价审核则 `pending/approved`,否则 `approved`。
+
+**测试**:未报名/未签到提交被拒;同一 child 重复提交更新不新增。
+
+---
+
+### P1-4 活动总结死代码清理(已锁定 B)
+
+**现状**:无服务端聚合/AI 总结;`ActivityDTO` 注释声明 `summarizing` 但 `applyDisplayState`(:527) 从不产出(死代码)。
+
+**方案(已锁定 B——清理)**:
+1. 删除 `ActivityDTO` 中 `summarizing` 相关声明与注释;
+2. 清理 `applyDisplayState`(:527) 中不可能产出的 `summarizing` 分支;
+3. 若前端有依赖"总结中"标签,同步移除或改为"已结束"展示。
+
+**影响**:纯清理,无功能损失(本就无总结能力)。
+**测试**:DTO/展示态无 `summarizing` 残留;编译通过、前端无报错依赖。
+
+---
+
+### P2-1 自动结束定时化
+
+**现状**:`autoEndExpiredActivities`(:402) 在 `list/detail` 调用时触发,且 `endTime` 早于"当前-7天"才结束。
+
+**方案**:改为 Spring `@Scheduled(cron = "0 0 2 * * ?")` 每日 02:00 执行;结束阈值改 `endTime < now()`(保留≤1天宽限),去掉"查询才触发"与 7 天延迟。`list/detail` 保留轻量实时判断兜底。
+
+**测试**:制造 endTime 昨天的活动,定时任务后状态=ended。
+
+---
+
+### P2-2 删除规划师活动与履约活动子系统(已锁定"删除+整合")
+
+**现状(D3 前)**:主活动 `activities`(报名/签到/评价)、规划师活动 `guide_activities`(仅创建/发布/取消,无报名签到)、商品履约 `activity_provisions`(购买活动商品后的另一套签到/核销)三者状态机/报名/签到互不相通。
+
+**决策(D3)**:**删除** `guide_activities` 与 `activity_provisions` 两套独立子系统,其业务诉求并入主活动 `activities`:
+- "规划师活动"→ 复用主活动创建/发布,可由规划师角色持有/发起,统一走主活动报名/签到/评价;
+- "商品履约活动"→ 购买活动商品后生成的履约记录,复用主活动 Task 签到/核销,消除"双签到口径"。
+
+**实施步骤(删除前必做)**:
+1. **依赖影响面审计**:全局 grep `guide_activities` / `GuideActivity` / `activity_provisions` / `ActivityProvision` / `ActivityProvisionService` 的引用点(Controller、Service、Mapper、前端、定时任务、外部调用方),列清单。
+2. 确认是否有其他模块(如规划师认证闸门、商城履约、订单回调)强依赖这两套表;若有,先迁移依赖到主活动,再删表。
+3. 删除实体、Mapper、Service、Controller、建表/迁移 SQL 中对应定义;清理前端相关页面/路由。
+4. 单独立 PR、独立灰度,避免与 P0 资金改动混在一起。
+
+**影响**:属架构级清理,范围较大,建议作为独立任务推进,不阻塞 P0/P1 上线。
+
+---
+
+## 四、实施与上线策略
+
+1. **分批提交**:P0-1/2/3、P1-1/2/3/4、P2-1 各自独立 commit;P2-2(删子系统)单独 PR。
+2. **依赖顺序**:P0-1(名额+审核开关)先于 P0-2(退款回滚,依赖名额字段口径);P1-1 与 P0-1 合并处理计数口径;P1-4 死代码清理可随时独立提交。
+3. **回归测试清单**:
+   - 报名满员拦截(并发,含免审即占与需审审过占两种)
+   - 免审/需审两条报名路径正确
+   - 免费/付费取消报名(含退款)
+   - 签到审核开关两种行为
+   - 评价门槛/去重
+   - 过期活动自动结束定时任务
+4. **上线策略**:P0 三项先在预发灰度(退款链路用小额订单);P2-2 删除子系统单独灰度。
+5. **回滚**:任一修复独立 commit,`git revert` 单笔即可。
+
+---
+
+## 五、已决事项(原"待确认"已锁定)
+
+- ✅ D1 签到审核:按活动级 `requireCheckinReview` 落地功能。
+- ✅ D2 活动总结:清理死代码(不做聚合/AI)。
+- ✅ D3 规划师活动/履约活动:删除并整合进主活动(前置依赖审计)。
+- ✅ D4 报名审核:活动级 `requireReview` 默认 false,免审即占、需审审过才占;撤销免费强制审核。

+ 317 - 0
docs/调研报告-活动管理实现vs需求-2026-10-22.html

@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>活动管理实现 vs 需求差异调研报告</title>
+<style>
+  :root {
+    --bg: #ffffff; --fg: #1f2933; --muted: #647082; --line: #e3e8ef;
+    --brand: #1565c0; --brand-bg: #e8f1fb;
+    --red: #c62828; --red-bg: #fdecea; --red-line:#f5c6c0;
+    --amber: #b26a00; --amber-bg: #fff4e0; --amber-line:#f3d79a;
+    --green: #2e7d32; --green-bg: #e9f6ea; --green-line:#bfe3c2;
+  }
+  * { box-sizing: border-box; }
+  body { font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
+         color: var(--fg); background: #f6f8fb; margin: 0; line-height: 1.65; }
+  .wrap { max-width: 1080px; margin: 0 auto; padding: 32px 28px 64px; }
+  header { border-bottom: 3px solid var(--brand); padding-bottom: 16px; margin-bottom: 8px; }
+  h1 { font-size: 26px; margin: 0 0 6px; color: var(--brand); }
+  .sub { color: var(--muted); font-size: 14px; }
+  h2 { font-size: 19px; margin: 36px 0 12px; padding-left: 10px; border-left: 4px solid var(--brand); }
+  h3 { font-size: 16px; margin: 22px 0 8px; color: var(--fg); }
+  p { margin: 8px 0; }
+  table { width: 100%; border-collapse: collapse; margin: 12px 0 18px; background: var(--bg);
+          font-size: 14px; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
+  th, td { border: 1px solid var(--line); padding: 9px 11px; text-align: left; vertical-align: top; }
+  th { background: var(--brand-bg); color: var(--brand); font-weight: 600; }
+  tr:nth-child(even) td { background: #fbfcfe; }
+  code { background: #eef1f6; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; color:#283593; }
+  .sev { display: inline-block; font-weight: 700; padding: 2px 9px; border-radius: 999px; font-size: 12px; }
+  .s-high { color: var(--red); background: var(--red-bg); border:1px solid var(--red-line); }
+  .s-mid  { color: var(--amber); background: var(--amber-bg); border:1px solid var(--amber-line); }
+  .s-low  { color: var(--green); background: var(--green-bg); border:1px solid var(--green-line); }
+  .card { border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; background: var(--bg); margin: 14px 0; }
+  .gap { border-left: 4px solid var(--red); }
+  .note { border-left: 4px solid var(--amber); }
+  .ok { border-left: 4px solid var(--green); }
+  ul { margin: 8px 0 8px 4px; padding-left: 20px; }
+  li { margin: 5px 0; }
+  .tag { font-size: 12px; color: var(--muted); background:#eef1f6; padding:1px 7px; border-radius:5px; margin-right:4px;}
+  .verdict { background: var(--brand-bg); border:1px solid #b9d4f3; border-radius:10px; padding:16px 20px; margin:18px 0; font-size:15px;}
+  .verdict b { color: var(--brand); }
+  footer { margin-top: 40px; color: var(--muted); font-size: 12.5px; border-top:1px solid var(--line); padding-top:12px;}
+  .matrix td:first-child { font-weight: 600; white-space: nowrap; }
+</style>
+</head>
+<body>
+<div class="wrap">
+<header>
+  <h1>活动管理「实现 vs 需求」差异调研报告</h1>
+  <div class="sub">调研日期:2026-10-22 | 项目:浠艾福 CFC(cfc-backend)| 主线:主活动(activities)全流程</div>
+</header>
+
+<div class="verdict">
+  <b>总体结论:</b>活动管理的<b>主链路(创建→发布→报名→签到→评价)已基本打通</b>,付费报名+微信支付、报名审核、积分/能量/CF 发放、评价与资料上传均已落地。
+  但与流程需求之间存在 <b>4 项高危差异</b>(名额失控、取消报名主路径断裂且无退款、签到审核字段失效)与 <b>多项一致性/完整性缺口</b>;
+  并且系统里<b>并存三套互不相通的活动子系统</b>,是后续维护与需求对齐的最大隐患。建议优先修复 🔴 高危项,再处理 🟡 一致性问题。
+</div>
+
+<h2>一、调研依据与范围</h2>
+<div class="card">
+  <p><b>需求侧(事实源):</b></p>
+  <ul>
+    <li><code>docs/flows/activity-flow.md</code> —— 用户使用流程图,定义了创建、发布(含审核)、报名、取消报名、报名审批、签到、评价的端到端流程与 24 个端点。</li>
+    <li><code>docs/superpowers/specs/2026-06-09-article-activity-publishing-design.md</code>、<code>2026-08-01-activity-registration-upgrade-design.md</code>、<code>2026-07-31-activity-registration-form-design.md</code>、<code>2026-10-07-activity-share-referral-design.md</code>、<code>2026-10-17-activity-poster-template-design.md</code>、<code>2026-10-22-activity-registration-roster.md</code> —— 围绕发布与报名的增强规格。</li>
+  </ul>
+  <p><b>实现侧:</b><code>cfc-backend</code> 的 <code>controller/ActivityController</code>、<code>controller/admin/AdminActivity*</code>、<code>controller/ActivityOrderController</code>、<code>service/ActivityService</code>、<code>ActivityRegistrationService</code>、<code>ActivityOrderService</code>、<code>ActivityFeedbackService</code>、<code>ActivityMaterialService</code> 等及其实体/Mapper。</p>
+  <p class="note" style="margin-top:10px;"><b>范围说明:</b>本报告"活动管理"指流程图中的<b>主活动(activities)</b>。系统另含「规划师活动(guide_activities)」与「商品履约活动(activity_provisions)」两套独立子系统,见第五节。</p>
+</div>
+
+<h2>二、六环节逐一对比</h2>
+
+<h3>① 创建</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>创建入口</td>
+    <td>管理后台/规划师端可创建,含标题、描述、封面、分类、地点、日期、最大人数、价格等字段</td>
+    <td><code>ActivityService.create</code>(:153) 强制 <code>draft</code>、<code>currentParticipants=0</code>;规划师端 <code>GuideActivityService.createActivity</code></td>
+    <td>主活动<b>无管理后台"新建"端点</b>(AdminActivityController 只有审核/列表),只能 C 端带 activity_admin 角色创建,职责割裂</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>字段校验</td>
+    <td>表单级字段约束(价格、时间、名额等)</td>
+    <td><code>create</code>/<code>update</code>(:163) 仅校验存在性,业务字段几乎零校验(价格/时间/名额/维度均可空)</td>
+    <td>合法性完全依赖前端,后端不兜底</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>海报模板</td>
+    <td>—</td>
+    <td><code>ActivityPosterTemplateService</code> 含启用/停用、配色、JSON 校验</td>
+    <td>已实现,且超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>② 发布</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>发布范式</td>
+    <td>流程图中存在「C端直发 publish」与「管理端审核流」两条路径</td>
+    <td><code>ActivityService.publish</code>(:173) 仅 <code>draft→published</code>,<b>不校验维度</b>;<code>ActivityAdminService.auditActivity</code> 审核流强制<b>五维权重合计=100%</b></td>
+    <td><b>两套发布范式不一致</b>:同一条数据可绕过审核直接 published,也可走审核,状态机存在歧义</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>审核/撤回</td>
+    <td>提交审核 → 通过/驳回 → 撤回/转草稿</td>
+    <td><code>submitForReview</code> / <code>auditActivity</code> / <code>withdraw</code> / <code>reDraft</code> 均已实现,含五维权重落库</td>
+    <td>功能完整</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>自动结束</td>
+    <td>活动到期自动结束</td>
+    <td><code>autoEndExpiredActivities</code>(:402) 仅当 <code>list/detail</code> 被调用时触发,且 <code>endTime</code> 早于"当前-7天"才转 ended</td>
+    <td><b>弱实现</b>:非定时任务、延迟 7 天才结束;活动过期后若无人查询仍显示进行中</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>标签/推荐</td>
+    <td>—</td>
+    <td>审核时 <code>ActivityTagService</code> AI 自动打标(失败静默降级),<code>getRelatedActivitiesByTags</code> 相关推荐</td>
+    <td>已实现,超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>③ 报名</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>名额控制</td>
+    <td>"最大人数 / 名额",隐含报名应受名额限制、满员截止</td>
+    <td><code>register</code>(:75)、<code>batchRegister</code>(:39)、<code>checkRegistrationGate</code>(:318) 仅校验 published / 是否需要报名 / 截止时间 / 付费闸门,<b>从不读 maxParticipants</b></td>
+    <td><b>🔴 名额失控</b>:允许超额报名,无任何阻塞逻辑;<code>maxParticipants</code> 仅用于"剩余名额"展示(<code>getRegistrationRoster</code> :225 用 <code>Math.max(0,…)</code> 兜底)</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>免费报名</td>
+    <td>提交报名 → 待审核(pending) → 管理员通过</td>
+    <td><code>register</code> 写 <code>pending</code>;管理员 <code>approve</code>(:105)/<code>reject</code>(:121) 仅 pending 可审</td>
+    <td>流程完整;但免费活动报名后<b>必须管理员审核</b>才能签到,付费活动支付即 auto-approved,体验不一致</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>付费报名</td>
+    <td>—(需求未细化收费)</td>
+    <td><code>ActivityOrderService</code>:订单→微信预支付→回调→写 approved 全链路,金额=0 时免支付直接通过</td>
+    <td>已实现且完整(含回调解密)</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>计数口径</td>
+    <td>当前参与人数</td>
+    <td><code>currentParticipants</code> 只在<b>签到时 +1</b>(见⑤),并非报名人数</td>
+    <td><b>语义错位</b>:与 <code>maxParticipants</code> 不是同一口径,满员判断失真</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h3>④ 取消报名</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>可取消状态</td>
+    <td>用户应能取消自己的报名</td>
+    <td><code>cancel</code>(:97) 用 <code>findRegistration(...,"pending")</code> 过滤</td>
+    <td><b>🔴 主路径断裂</b>:已审核通过(approved)/已支付报名无法取消,调用即返回"报名记录不存在"</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>退款/回滚</td>
+    <td>取消应释放名额、付费应退款</td>
+    <td>取消仅改状态;主活动无支付绑定、无退款逻辑;名额/计数均无回滚</td>
+    <td><b>🔴 无退款、无名额回滚</b>(付费活动取消无退款;退款逻辑仅存在于商品履约支线 ActivityProvisionService.cancel)</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>取消记录</td>
+    <td>—</td>
+    <td>无"取消原因/取消时间"字段</td>
+    <td>可观测性弱</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>⑤ 签到</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>签到主流程</td>
+    <td>扫码/按钮签到,发放积分、扣能量(未报名)、更新参与人数、发放五维能量</td>
+    <td><code>ActivityService.checkin</code>(:198) 完整:时间窗校验、本人/同家庭代签、未报名能量惩罚、去重、建 Task(completed)、加积分、写 PointsLog、发能量、发 CF 值</td>
+    <td>功能完整,超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>审核签到</td>
+    <td>实体含 <code>requireCheckinReview</code>(签到需审核)</td>
+    <td><code>checkin</code> 逻辑<b>从不读取</b>该字段,一律直接 <code>Task.status="completed"</code></td>
+    <td><b>🔴 虚假字段</b>:配置项形同虚设,无任何"待审核"中间态</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>强制报名</td>
+    <td>先报名后签到</td>
+    <td>除 <code>requireRegistration=1</code> 且未通过审核时拒绝外,并未阻止未报名者签到(无手机号孩子可代签)</td>
+    <td>与"先报名后签到"假设不符,但属业务取舍</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>平行签到机制</td>
+    <td>—</td>
+    <td>商品履约 <code>activity_provisions</code> 另有一套签到/核销(provision→signIn→complete),与主活动 Task 签到互不关联</td>
+    <td>同一活动可能两种签到并存,统计口径混乱(见第五节)</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h3>⑥ 活动总结</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>评价</td>
+    <td>流程阶段七"活动评价":提交评分/评论、查看评价列表、我的评价</td>
+    <td><code>ActivityFeedbackService.submit</code>(:20) 直接 insert,无去重、无"是否已结束/已报名/已签到"门槛;任何人可评;<code>status</code> 恒 0 无意义</td>
+    <td>基础可用但<b>无门槛、无去重、状态字段无效</b></td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>总结资料</td>
+    <td>—(需求未定义"活动总结"独立阶段)</td>
+    <td><code>ActivityMaterialService.upload</code>(:26) 限定 <code>status=published</code> 且 <code>now.after(endTime)</code>,即结束后才能上传资料</td>
+    <td>已实现"结束后的资料上传"</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>聚合/AI 总结</td>
+    <td>用户表述中的"活动总结"隐含<b>参与数据汇总 / 复盘报告</b></td>
+    <td><b>无任何服务端聚合、AI 总结文本生成或参与数据汇总报告</b>。<code>ActivityDTO</code> 注释声明了 <code>summarizing</code>/总结中展示态,但 <code>applyDisplayState</code>(:527) <b>从不产出该值</b>(死代码)</td>
+    <td><b>🟡 需求未定义 + 实现未落地</b>:若业务需要"自动总结",目前完全缺失;DTO 里的"总结中"标签前端若依赖将无法命中</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h2>三、差异汇总(按严重度)</h2>
+<table class="matrix">
+  <tr><th>级别</th><th>差异项</th><th>影响</th></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>名额(quota)报名时不校验,允许超额报名</td><td>满员控制失效,可能超售;业务正确性风险</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>取消报名仅支持 pending,已通过/已支付报名无法取消</td><td>用户正常"报名后取消"主路径断裂</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>取消报名无退款、无名额/计数回滚</td><td>付费活动取消无退款,财务与名额失准</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>签到审核字段 requireCheckinReview 形同虚设</td><td>配置项不产生任何效果,误导运营</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>两套发布范式不一致(直发不校验维度 vs 审核强制权重100%)</td><td>状态机歧义、数据质量参差</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>currentParticipants 语义错位(实为签到计数)</td><td>满员/参与统计失真</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>评价无门槛、无去重、status 恒 0</td><td>刷评/误评无防护</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>活动总结无聚合/AI 生成;DTO summarizing 死值</td><td>"总结"能力缺失,前端标签失效</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>过期自动结束弱实现(查询触发+延迟7天)</td><td>过期活动未及时结束</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>未报名者仍可签到(除需审核场景)</td><td>与"先报名后签到"假设不符</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>三套活动体系并存且互不相通(见第五节)</td><td>架构/维护风险</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>主活动无管理后台"新建"入口</td><td>职责割裂</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>创建接口零字段校验</td><td>依赖前端兜底</td></tr>
+</table>
+
+<h2>四、关键缺口详解</h2>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 名额失控(最严重)</h3>
+  <p>报名入口 <code>register</code> / <code>batchRegister</code> 与闸门 <code>checkRegistrationGate</code> 均未对 <code>maxParticipants</code> 做判断。系统只在"报名情况"接口用 <code>max - approvedCount</code> 计算剩余名额并 <code>Math.max(0, …)</code> 兜底——意味着即使报名人数远超上限,剩余名额只显示 0,但<b>报名依旧成功</b>。对线下有场地/物料限制的活动,这是直接业务风险。</p>
+</div>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 取消报名主路径断裂 + 无退款</h3>
+  <p><code>cancel</code> 用 <code>findRegistration(activityId, memberId, userId, "pending")</code> 限定只查 pending。而免费活动需管理员 approve、付费活动支付成功即 auto-approved——两者到达"可签到"状态后都不再是 pending,因此用户在前端点"取消报名"会收到"报名记录不存在"。此外,主活动报名与支付(ActivityOrder)之间缺乏取消→退款的回路,付费用户取消后无退款。</p>
+</div>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 签到审核字段失效</h3>
+  <p>实体 <code>Activity</code> 设计了 <code>requireCheckinReview</code>(是否需要审核签到),但 <code>checkin</code> 全程未读取该字段,签到一律直接生成 <code>Task.status="completed"</code>。运营在后台开启"签到需审核"后不会得到任何预期行为。</p>
+</div>
+
+<h2>五、架构风险提示:三套互不相通的活动子系统</h2>
+<div class="card note">
+  <p>当前后端并存三条"活动"数据/流程线,做需求对照时切勿混为一谈:</p>
+  <ul>
+    <li><b>主活动 activities</b>:C 端活动中心,承载本报告六环节(报名/签到/评价)。状态机 <code>draft/pending/rejected/published/withdrawn/ended</code>。</li>
+    <li><b>规划师活动 guide_activities</b>:规划师端,仅 create/update/publish/cancel/delete,<b>不参与报名/签到/评价</b>,状态机 <code>draft/published/cancelled/completed</code>,与主活动字段、状态完全独立。</li>
+    <li><b>商品履约活动 activity_provisions</b>:挂在 product_orders 上,"购买活动商品→核销码→签到→完成→退款",状态机 <code>unprovisioned→provisioned→signed_in→completed</code>,与主活动报名/签到<b>平行且互不相通</b>。</li>
+  </ul>
+  <p>其中"规划师活动"与"主活动"的报名/签到/评价能力无法复用;"商品履约签到"与"主活动 Task 签到"是两套独立的签到实现。这意味着同一业务概念(活动、报名、签到)在代码里有多套定义,需求对齐与后续迭代成本显著。</p>
+</div>
+
+<h2>六、修复优先级建议</h2>
+<table>
+  <tr><th style="width:60px">优先级</th><th>事项</th><th>建议动作</th></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>名额校验</td><td>在 <code>register/batchRegister</code> 中于写入前校验 <code>approvedCount &lt; maxParticipants</code>(含并发悲观锁/唯一约束),满员返回"名额已满"</td></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>取消报名链路</td><td><code>cancel</code> 支持 approved 状态;对付费活动联动 <code>ActivityOrderService</code> 走退款;回滚名额计数</td></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>签到审核字段</td><td>若保留该能力则在 <code>checkin</code> 读取 <code>requireCheckinReview</code> 生成待审核 Task;否则从实体/后台配置中移除,避免误导</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>currentParticipants 口径</td><td>拆分为"报名人数"与"签到人数"两个字段,或统一以 approved 计数,消除语义错位</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>发布范式统一</td><td>明确"直发"与"审核流"的适用场景,统一维度/权重校验规则,避免状态歧义</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>活动总结能力</td><td>若业务需要"总结",补充服务端聚合(参与率/签到率/评价汇总)或 AI 总结,并接线 DTO 的 summarizing 态;否则清理死代码</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>评价门槛</td><td>评价前校验"已报名且已签到",并去重(每 child 一条)</td></tr>
+  <tr><td><span class="sev s-low">P2</span></td><td>自动结束</td><td>改为定时任务(如每日 cron),消除"查询才触发"与 7 天延迟</td></tr>
+  <tr><td><span class="sev s-low">P2</span></td><td>子系统整合决策</td><td>明确主活动 / 规划师活动 / 商品履约三者边界,决定是否需要统一活动内核,避免长期分裂</td></tr>
+</table>
+
+<footer>
+  本报告基于代码静态核查(已逐行核验 register / cancel / checkin / applyDisplayState / autoEndExpiredActivities 等关键方法)与 <code>docs/flows/activity-flow.md</code> 流程需求对照生成。
+  差异判定以"流程需求隐含约束"与"代码实际行为"为准;标注 🔴 的项建议在上线前优先修复。
+</footer>
+</div>
+</body>
+</html>