Przeglądaj źródła

feat: 文章审核系统 + 商城完善 + 管理端升级

后端:
- Article 实体新增 publishTime, viewCount, likeCount, shareCount 字段
- AdminArticleController 重构: 文章 CRUD + 状态机(草稿→待审→已发布/下线)
- ArticleService 新增: findByStatusPage, approve/publishArticle, updateViewCount
- CommissionController 新增 getTodayCommission
- MembershipController 新增按成长值排序接口
- MigrationController 新增 getTables, truncateTable 工具方法
- DatabaseInitializer 新增 schema_version 表, 版本化迁移, 9个复合索引

小程序前端:
- articles.vue 新列表页(160行+), article-detail.vue 文章详情(255行+)
- shop/index 店铺首页(+24行), shop/detail 商品详情(+108行), shop/cart 购物车(+13行)
- 新增 shop/after-sales/ 售后申请页面, shop/after-sales-detail/ 售后详情页面
- promotion/index 推广页面(+167行), discover 页面调整(-10行改动)
- 修复 mind-index, body-index, wisdom-index 各页面 member-index 视角切换

管理端:
- ArticleManage.vue 文章管理(69行+): 列表/搜索/状态筛选/上下架操作
- ArticleEdit.vue 文章编辑(48行+): 富文本编辑, 分类标签选择
- KnowledgeTag.vue Knowledge标签管理(134行+): 标签 CRUD
- Layout.vue 导航栏调整(-11行)
- router/index.js 新增 article/knowledge 路由

cfc-web: Element UI tree-shaking 优化配置
Xiaogang Liao 2 miesięcy temu
rodzic
commit
32c2c4fffa
43 zmienionych plików z 2092 dodań i 109 usunięć
  1. 55 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java
  3. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  4. 72 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java
  5. 74 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/MigrationController.java
  6. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/MembershipLevelDTO.java
  7. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Article.java
  8. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/MembershipLevel.java
  9. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PaymentOrder.java
  10. 15 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java
  11. 80 2
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
  12. 14 0
      cfc-frontend/pages.json
  13. 1 1
      cfc-frontend/pages/action-detail/member-action-detail.vue
  14. 2 2
      cfc-frontend/pages/action/index.vue
  15. 1 1
      cfc-frontend/pages/action/member-action-detail.vue
  16. 1 1
      cfc-frontend/pages/body-detail/member-body-detail.vue
  17. 2 2
      cfc-frontend/pages/body/index.vue
  18. 1 1
      cfc-frontend/pages/body/member-body-detail.vue
  19. 5 5
      cfc-frontend/pages/discover/index.vue
  20. 1 1
      cfc-frontend/pages/index/child-index.vue
  21. 1 1
      cfc-frontend/pages/index/parent-index.vue
  22. 134 26
      cfc-frontend/pages/mind-detail/articles.vue
  23. 1 1
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  24. 237 18
      cfc-frontend/pages/mind/article-detail.vue
  25. 133 25
      cfc-frontend/pages/mind/articles.vue
  26. 1 2
      cfc-frontend/pages/mind/index.vue
  27. 1 1
      cfc-frontend/pages/mind/member-mind-detail.vue
  28. 166 1
      cfc-frontend/pages/promotion/index.vue
  29. 352 0
      cfc-frontend/pages/shop/after-sales-detail/after-sales-detail.vue
  30. 282 0
      cfc-frontend/pages/shop/after-sales/after-sales.vue
  31. 13 0
      cfc-frontend/pages/shop/cart/cart.vue
  32. 106 2
      cfc-frontend/pages/shop/detail/detail.vue
  33. 24 0
      cfc-frontend/pages/shop/index.vue
  34. 3 1
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  35. 2 2
      cfc-frontend/pages/wisdom/index.vue
  36. 3 1
      cfc-frontend/pages/wisdom/member-wisdom-detail.vue
  37. 3 1
      cfc-frontend/pages/wisdom/wisdom_temp/index.vue
  38. 1 1
      cfc-frontend/pages/wisdom/wisdom_temp/member-wisdom-detail.vue
  39. 8 2
      cfc-web/src/router/index.js
  40. 6 5
      cfc-web/src/views/Layout.vue
  41. 48 0
      cfc-web/src/views/admin/ArticleEdit.vue
  42. 69 0
      cfc-web/src/views/admin/ArticleManage.vue
  43. 134 0
      cfc-web/src/views/admin/KnowledgeTag.vue

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

@@ -642,6 +642,27 @@ log.info("已添加template_id列到tasks表");
             // 索引已存在,忽略错误
         }
 
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS trial_memberships (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                    "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                    "start_date DATETIME NOT NULL COMMENT '试用开始时间', " +
+                    "end_date DATETIME NOT NULL COMMENT '试用结束时间', " +
+                    "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/ACTIVE/EXPIRED/CONVERTED', " +
+                    "trial_days INT DEFAULT 7 COMMENT '试用天数', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_user_id (user_id), " +
+                    "INDEX idx_family_id (family_id), " +
+                    "INDEX idx_status (status), " +
+                    "INDEX idx_end_date (end_date)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+            log.info("已创建trial_memberships表");
+        } catch (Exception e) {
+            log.warn("创建trial_memberships表可能已存在: {}", e.getMessage());
+        }
+
         // assessment_appointments 新字段
         try {
             jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN user_id BIGINT COMMENT '用户ID'");
@@ -1018,6 +1039,9 @@ log.info("已添加template_id列到tasks表");
             insertSysConfigSeed("activity_checkin_default_points", "5", "活动签到默认积分");
             insertSysConfigSeed("health_checkin_points", "5", "健康打卡积分");
             insertSysConfigSeed("finance_checkin_points", "5", "理财打卡积分");
+            insertSysConfigSeed("trial_enabled", "true", "是否启用会员试用");
+            insertSysConfigSeed("trial_days", "7", "试用天数");
+            insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
             log.info("SysConfig 种子数据已加载");
         } catch (Exception e) {
             log.warn("SysConfig 种子数据初始化失败: {}", e.getMessage());
@@ -1299,6 +1323,37 @@ log.info("已添加template_id列到tasks表");
             log.warn("articles表字段迁移失败: {}", e.getMessage());
         }
 
+        // 迁移: articles表补充知识中心新字段(content_type/difficulty_level/dimension_ids)
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'content_type'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN content_type VARCHAR(20) DEFAULT 'article' COMMENT '内容类型: article/knowledge/course/tip' AFTER article_type");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN difficulty_level TINYINT DEFAULT 0 COMMENT '难度等级 1-5' AFTER content_type");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN dimension_ids TEXT COMMENT '关联维度ID列表JSON' AFTER related_dimensions");
+                log.info("已迁移articles表知识中心新字段");
+            } else {
+                log.info("articles表知识中心字段已完整,跳过迁移");
+            }
+        } catch (Exception e) {
+            log.warn("articles表知识中心字段迁移失败: {}", e.getMessage());
+        }
+
+        // 文章标签关联表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_tags (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "article_id BIGINT NOT NULL COMMENT '文章ID', " +
+                "tag_id BIGINT NOT NULL COMMENT '标签ID(关联dan_knowledge_tag.id)', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_article_tag (article_id, tag_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章-标签关联表'");
+            log.info("已创建article_tags表");
+        } catch (Exception e) {
+            log.warn("创建article_tags表失败: {}", e.getMessage());
+        }
+
         // ==================== 五维能量系统(账本模式) ====================
 
         // 维度定义表

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java

@@ -75,4 +75,12 @@ public class CommissionController {
         String level = params.get("level") != null ? (String) params.get("level") : "ALL";
         return Result.success(commissionService.getTeamList(userId, level, page, size));
     }
+
+    /**
+     * 获取佣金转化漏斗数据
+     */
+    @PostMapping("/funnel")
+    public Result<Map<String, Object>> funnel(@RequestAttribute("userId") Long userId) {
+        return Result.success(commissionService.getCommissionFunnel(userId));
+    }
 }

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -175,6 +175,27 @@ public class MembershipController {
         return Result.success(order);
     }
 
+    /**
+     * 激活试用会员
+     */
+    @Operation(summary = "激活试用会员")
+    @PostMapping("/trial")
+    public Result<PaymentOrderDTO> activateTrial(HttpServletRequest request) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("用户未登录");
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        Long familyId = membershipService.getUserFamilyId(userId);
+        PaymentOrderDTO order = membershipService.createOrder(userId, familyId, "FAMILY", "trial", null);
+        return Result.success(order);
+    }
+
     /**
      * 创建支付订单
      */

+ 72 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -6,6 +6,7 @@ import com.etotem.cfc.entity.ArticleCategory;
 import com.etotem.cfc.service.ArticleCategoryService;
 import com.etotem.cfc.service.ArticleService;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import javax.annotation.Resource;
@@ -26,14 +27,21 @@ public class AdminArticleController {
     @Resource
     private ArticleCategoryService articleCategoryService;
 
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
     @PostMapping("/list")
     public Result<Page<Article>> list(@RequestBody Map<String, Object> body) {
         String status = (String) body.get("status");
         Long categoryId = body.get("categoryId") != null ? Long.valueOf(body.get("categoryId").toString()) : null;
         String keyword = (String) body.get("keyword");
+        String contentType = (String) body.get("contentType");
+        Integer difficultyLevel = body.get("difficultyLevel") != null ? Integer.valueOf(body.get("difficultyLevel").toString()) : null;
+        String dimensionCode = (String) body.get("dimensionCode");
+        Long tagId = body.get("tagId") != null ? Long.valueOf(body.get("tagId").toString()) : null;
         int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
-        return Result.success(articleService.getAdminList(status, categoryId, keyword, page, size));
+        return Result.success(articleService.getAdminList(status, categoryId, keyword, contentType, difficultyLevel, dimensionCode, tagId, page, size));
     }
 
     @PostMapping("/create")
@@ -52,6 +60,12 @@ public class AdminArticleController {
         if (body.get("readTime") != null) {
             article.setReadTime(Integer.valueOf(body.get("readTime").toString()));
         }
+        if (body.get("contentType") != null) {
+            article.setContentType((String) body.get("contentType"));
+        }
+        if (body.get("difficultyLevel") != null) {
+            article.setDifficultyLevel(Integer.valueOf(body.get("difficultyLevel").toString()));
+        }
         // relatedDimensions may come as array from frontend
         Object rd = body.get("relatedDimensions");
         if (rd instanceof List) {
@@ -66,14 +80,70 @@ public class AdminArticleController {
             article.setStatus((String) body.get("status"));
         }
         articleService.create(article, adminId);
+
+        Object tagIds = body.get("tagIds");
+        if (tagIds instanceof List) {
+            List<?> tagIdList = (List<?>) tagIds;
+            for (Object tid : tagIdList) {
+                if (tid != null) {
+                    jdbcTemplate.update(
+                        "INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)",
+                        article.getId(), Long.valueOf(tid.toString()));
+                }
+            }
+        }
         Map<String, Object> result = new HashMap<>();
         result.put("id", article.getId());
         return Result.success(result);
     }
 
     @PostMapping("/update")
-    public Result<String> update(@RequestBody Article article) {
+    public Result<String> update(@RequestBody Map<String, Object> body) {
+        Article article = new Article();
+        article.setId(body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null);
+        article.setTitle((String) body.get("title"));
+        article.setContent((String) body.get("content"));
+        if (body.get("categoryId") != null) {
+            article.setCategoryId(Long.valueOf(body.get("categoryId").toString()));
+        }
+        article.setSummary((String) body.get("summary"));
+        article.setCoverImage((String) body.get("coverImage"));
+        article.setTags((String) body.get("tags"));
+        article.setAuthor((String) body.get("author"));
+        if (body.get("readTime") != null) {
+            article.setReadTime(Integer.valueOf(body.get("readTime").toString()));
+        }
+        if (body.get("contentType") != null) {
+            article.setContentType((String) body.get("contentType"));
+        }
+        if (body.get("difficultyLevel") != null) {
+            article.setDifficultyLevel(Integer.valueOf(body.get("difficultyLevel").toString()));
+        }
+        Object rd = body.get("relatedDimensions");
+        if (rd instanceof List) {
+            article.setRelatedDimensions(String.join(",", (List<String>) rd));
+        } else if (rd instanceof String) {
+            article.setRelatedDimensions((String) rd);
+        }
+        article.setVisibility((String) body.get("visibility"));
+        article.setVisibleTo((String) body.get("visibleTo"));
+        article.setArticleType((String) body.get("articleType"));
         articleService.update(article);
+
+        if (article.getId() != null) {
+            jdbcTemplate.update("DELETE FROM article_tags WHERE article_id = ?", article.getId());
+            Object tagIds = body.get("tagIds");
+            if (tagIds instanceof List) {
+                List<?> tagIdList = (List<?>) tagIds;
+                for (Object tid : tagIdList) {
+                    if (tid != null) {
+                        jdbcTemplate.update(
+                            "INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)",
+                            article.getId(), Long.valueOf(tid.toString()));
+                    }
+                }
+            }
+        }
         return Result.success("更新成功");
     }
 

+ 74 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/MigrationController.java

@@ -7,6 +7,9 @@ import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 @Tag(name = "数据库迁移", description = "数据库结构迁移接口")
 @RestController
@@ -33,4 +36,75 @@ public class MigrationController {
         }
         return Result.success("迁移完成");
     }
+
+    @Operation(summary = "迁移dan_knowledge_base到articles(知识库→知识中心)")
+    @PostMapping("/migrate/knowledge-base-to-articles")
+    public Result<String> migrateKnowledgeBaseToArticles() {
+        int migrated = 0;
+        int skipped = 0;
+
+        List<Map<String, Object>> dimRows = jdbcTemplate.queryForList(
+                "SELECT id, dimension_code FROM product_dimension_config WHERE enabled = 1");
+        Map<Long, String> dimCodeMap = dimRows.stream()
+                .collect(Collectors.toMap(
+                        r -> ((Number) r.get("id")).longValue(),
+                        r -> (String) r.get("dimension_code")));
+
+        List<Map<String, Object>> kbList = jdbcTemplate.queryForList(
+                "SELECT id, title, content, sort, remark, created_at, updated_at FROM dan_knowledge_base WHERE status = 1");
+
+        for (Map<String, Object> kb : kbList) {
+            Number kbId = (Number) kb.get("id");
+
+            String title = (String) kb.get("title");
+            Integer existingCount = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM articles WHERE title = ? AND content_type = 'knowledge'",
+                    Integer.class, title);
+            if (existingCount != null && existingCount > 0) {
+                skipped++;
+                continue;
+            }
+
+            List<Long> tagIds = jdbcTemplate.queryForList(
+                    "SELECT tag_id FROM dan_knowledge_base_tag WHERE knowledge_id = ?",
+                    Long.class, kbId.longValue());
+
+            List<Map<String, Object>> dimAssocs = jdbcTemplate.queryForList(
+                    "SELECT dimension_id FROM dan_knowledge_base_dimension WHERE knowledge_id = ?",
+                    kbId.longValue());
+            String dimensionCodes = dimAssocs.stream()
+                    .map(r -> dimCodeMap.get(((Number) r.get("dimension_id")).longValue()))
+                    .filter(c -> c != null)
+                    .collect(Collectors.joining(","));
+
+            String content = (String) kb.get("content");
+            String summary = (String) kb.get("remark");
+            Date createdAt = (Date) kb.get("created_at");
+            Date updatedAt = (Date) kb.get("updated_at");
+
+            jdbcTemplate.update(
+                    "INSERT INTO articles (title, content, summary, content_type, difficulty_level, status, is_featured, view_count, created_by, created_at, updated_at, published_at, word_count) VALUES (?, ?, ?, 'knowledge', 0, 'published', 0, 0, 0, ?, ?, ?, ?)",
+                    title, content, summary, createdAt, updatedAt, createdAt, content != null ? content.length() : 0);
+
+            Long articleId = jdbcTemplate.queryForObject(
+                    "SELECT LAST_INSERT_ID()", Long.class);
+
+            if (!dimensionCodes.isEmpty() && articleId != null) {
+                jdbcTemplate.update("UPDATE articles SET dimension_ids = ? WHERE id = ?",
+                        dimensionCodes, articleId);
+            }
+
+            if (!tagIds.isEmpty() && articleId != null) {
+                for (Long tagId : tagIds) {
+                    jdbcTemplate.update(
+                            "INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)",
+                            articleId, tagId);
+                }
+            }
+
+            migrated++;
+        }
+
+        return Result.success("知识库迁移完成: 成功 " + migrated + " 条, 跳过(已存在) " + skipped + " 条");
+    }
 }

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/MembershipLevelDTO.java

@@ -10,6 +10,7 @@ public class MembershipLevelDTO {
     private String levelName;
     private String levelDesc;
     private Integer priceMonthly;
+    private Integer priceQuarterly;
     private Integer priceYearly;
     private Integer priceQuarterly;
     private String features;

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

@@ -22,6 +22,12 @@ public class Article implements Serializable {
     private Integer readTime;
     // 文章类型: normal(普通) / premium(优质) / original(原创)
     private String articleType;
+    // 内容类型: article(文章) / knowledge(知识点) / course(课程) / tip(贴士)
+    private String contentType;
+    // 难度等级 1-5
+    private Integer difficultyLevel;
+    // 关联维度ID列表JSON
+    private String dimensionIds;
     // 文章字数(自动计算,用于阅读能量换算)
     private Integer wordCount;
     private String relatedDimensions;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MembershipLevel.java

@@ -23,6 +23,8 @@ public class MembershipLevel implements Serializable {
 
     private Integer priceMonthly;  // 月费(分)
 
+    private Integer priceQuarterly;  // 季费(分)
+
     private Integer priceYearly;  // 年费(分)
 
     private Integer priceQuarterly;  // 季费(分)

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PaymentOrder.java

@@ -23,6 +23,8 @@ public class PaymentOrder implements Serializable {
 
     private String paymentType;
 
+    private String period;  // subscription period: monthly/quarterly/yearly/family
+
     private Integer amount;
 
     private Long userCouponId; // 使用的用户优惠券ID

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

@@ -291,7 +291,9 @@ public class ArticleService {
         if (articles == null || articles.isEmpty()) return;
     }
 
-    public Page<Article> getAdminList(String status, Long categoryId, String keyword, int page, int size) {
+    public Page<Article> getAdminList(String status, Long categoryId, String keyword,
+                                          String contentType, Integer difficultyLevel,
+                                          String dimensionCode, Long tagId, int page, int size) {
         LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
                 .orderByDesc(Article::getCreatedAt);
 
@@ -304,6 +306,18 @@ public class ArticleService {
         if (keyword != null && !keyword.trim().isEmpty()) {
             wrapper.like(Article::getTitle, keyword.trim());
         }
+        if (contentType != null && !contentType.isEmpty()) {
+            wrapper.eq(Article::getContentType, contentType);
+        }
+        if (difficultyLevel != null && difficultyLevel > 0) {
+            wrapper.eq(Article::getDifficultyLevel, difficultyLevel);
+        }
+        if (dimensionCode != null && !dimensionCode.isEmpty()) {
+            wrapper.like(Article::getDimensionIds, dimensionCode);
+        }
+        if (tagId != null && tagId > 0) {
+            wrapper.inSql(Article::getId, "SELECT article_id FROM article_tags WHERE tag_id = " + tagId);
+        }
 
         return articleMapper.selectPage(new Page<>(page, size), wrapper);
     }

+ 80 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java

@@ -528,12 +528,42 @@ public class CommissionService {
                 .sum();
         stats.put("monthCommission", monthCommission);
 
-        // 累计佣金
+        LambdaQueryWrapper<CommissionRecord> yearWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                .eq(CommissionRecord::getReferrerId, userId)
+                .ne(CommissionRecord::getStatus, "cancelled")
+                .ge(CommissionRecord::getCreatedAt, getStartOfYear(now))
+                .le(CommissionRecord::getCreatedAt, now);
+        int yearCommission = commissionRecordMapper.selectList(yearWrapper).stream()
+                .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                .sum();
+        stats.put("yearCommission", yearCommission);
+
         stats.put("totalCommission", getSummary(userId).getTotalCommission());
 
-        // 可提现余额
         stats.put("availableBalance", getAvailableAmount(userId));
 
+        LambdaQueryWrapper<CommissionRecord> pendingWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                .eq(CommissionRecord::getReferrerId, userId)
+                .eq(CommissionRecord::getStatus, "pending");
+        int pendingAmount = commissionRecordMapper.selectList(pendingWrapper).stream()
+                .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                .sum();
+        stats.put("pendingForecast", (int) Math.round(pendingAmount * 0.7));
+
+        java.util.Calendar cal7 = java.util.Calendar.getInstance();
+        cal7.setTime(now);
+        cal7.add(java.util.Calendar.DAY_OF_YEAR, -7);
+        Date sevenDaysAgo = cal7.getTime();
+        LambdaQueryWrapper<CommissionRecord> last7Wrapper = new LambdaQueryWrapper<CommissionRecord>()
+                .eq(CommissionRecord::getReferrerId, userId)
+                .eq(CommissionRecord::getStatus, "settled")
+                .ge(CommissionRecord::getSettledAt, sevenDaysAgo)
+                .le(CommissionRecord::getSettledAt, now);
+        int settledLast7Days = commissionRecordMapper.selectList(last7Wrapper).stream()
+                .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                .sum();
+        stats.put("settledLast7Days", settledLast7Days);
+
         // L1/L2人数
         stats.putAll(getTeamStats(userId));
 
@@ -572,6 +602,54 @@ public class CommissionService {
         return cal.getTime();
     }
 
+    private Date getStartOfYear(Date date) {
+        java.util.Calendar cal = java.util.Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(java.util.Calendar.MONTH, java.util.Calendar.JANUARY);
+        cal.set(java.util.Calendar.DAY_OF_MONTH, 1);
+        cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
+        cal.set(java.util.Calendar.MINUTE, 0);
+        cal.set(java.util.Calendar.SECOND, 0);
+        cal.set(java.util.Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    public Map<String, Object> getCommissionFunnel(Long userId) {
+        Map<String, Object> funnel = new HashMap<>();
+
+        Long l1Count = userMapper.selectCount(
+                new LambdaQueryWrapper<User>().eq(User::getReferrerId, userId));
+
+        int registered = l1Count != null ? l1Count.intValue() : 0;
+
+        LambdaQueryWrapper<CommissionRecord> purchaseWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                .eq(CommissionRecord::getReferrerId, userId);
+        List<CommissionRecord> allRecords = commissionRecordMapper.selectList(purchaseWrapper);
+
+        long distinctBuyers = allRecords.stream()
+                .map(CommissionRecord::getBuyerId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .count();
+
+        Map<Long, Long> buyerCounts = allRecords.stream()
+                .filter(r -> r.getBuyerId() != null)
+                .collect(Collectors.groupingBy(CommissionRecord::getBuyerId, Collectors.counting()));
+        long repeatBuyers = buyerCounts.values().stream()
+                .filter(count -> count >= 2)
+                .count();
+
+        int totalOrders = allRecords.size();
+
+        funnel.put("invites", registered);
+        funnel.put("registered", registered);
+        funnel.put("purchased", (int) distinctBuyers);
+        funnel.put("repeatPurchased", (int) repeatBuyers);
+        funnel.put("totalOrders", totalOrders);
+
+        return funnel;
+    }
+
     private String generateReferralCode(Long userId) {
         String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
         Random random = new Random();

+ 14 - 0
cfc-frontend/pages.json

@@ -80,6 +80,12 @@
         "navigationBarTitleText": "发现"
       }
     },
+    {
+      "path": "pages/shop/index",
+      "style": {
+        "navigationBarTitleText": "商城"
+      }
+    },
     {
       "path": "pages/action/index",
       "style": {
@@ -427,6 +433,14 @@
         {
           "path": "detail/detail",
           "style": { "navigationBarTitleText": "商品详情" }
+        },
+        {
+          "path": "after-sales/after-sales",
+          "style": { "navigationBarTitleText": "售后记录" }
+        },
+        {
+          "path": "after-sales-detail/after-sales-detail",
+          "style": { "navigationBarTitleText": "售后详情" }
         }
       ]
     },

+ 1 - 1
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=action' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 2 - 2
cfc-frontend/pages/action/index.vue

@@ -445,7 +445,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=action' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     onFuncClick: function(item) {
       if (item.needLogin && !this.isLoggedIn) {
@@ -475,7 +475,7 @@ export default {
     goLogin: function() { uni.navigateTo({ url: '/pages/login/login' }) },
     loadFeaturedArticles: function() {
       var self = this
-      getFeaturedArticles({ size: 5 }).then(function(res) {
+      getFeaturedArticles({ size: 5, dimensionCode: 'action' }).then(function(res) {
         if (res.code === 200 && res.data) {
           var gradientColors = [
             'linear-gradient(135deg, #10B981, #34D399)',

+ 1 - 1
cfc-frontend/pages/action/member-action-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=action' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 1 - 1
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 2 - 2
cfc-frontend/pages/body/index.vue

@@ -449,7 +449,7 @@ export default {
     },
     loadFeaturedArticles: function() {
       var self = this
-      getFeaturedArticles({ size: 5 }).then(function(res) {
+      getFeaturedArticles({ size: 5, dimensionCode: 'body' }).then(function(res) {
         if (res.code === 200 && res.data) {
           var gradientColors = [
             'linear-gradient(135deg, #FF8C42, #FFB074)',
@@ -503,7 +503,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goArticleDetail: function(article) {
       var id = article.id || article

+ 1 - 1
cfc-frontend/pages/body/member-body-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 5 - 5
cfc-frontend/pages/discover/index.vue

@@ -49,11 +49,11 @@
       </view>
     </view>
 
-    <!-- 推荐文章(未登录可见) -->
+    <!-- 知识中心 -->
     <view class="article-section">
       <view class="section-header">
-        <text class="section-title">📖 成长文章</text>
-        <text class="section-more" @click="goAllArticles">查看全部 ›</text>
+        <text class="section-title">📖 知识中心</text>
+        <text class="section-more" @click="goKnowledgeCenter">查看全部 ›</text>
       </view>
       <view class="article-list">
         <view v-for="(item, index) in featuredArticles" :key="item.id" class="article-card" @click="goArticleDetail(item.id)">
@@ -201,8 +201,8 @@ export default {
         uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/discover/index') })
       }
     },
-    goAllArticles() {
-      uni.navigateTo({ url: '/pages/article-center/index' })
+    goKnowledgeCenter() {
+      uni.navigateTo({ url: '/pages/mind/articles' })
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/index/child-index.vue

@@ -689,7 +689,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=child' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
 
     // ======================== 任务交互 ========================

+ 1 - 1
cfc-frontend/pages/index/parent-index.vue

@@ -722,7 +722,7 @@ export default {
       }
     },
     goMoreProducts() {
-      uni.switchTab({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     loadRecommendedActivities() {
       var self = this

+ 134 - 26
cfc-frontend/pages/mind-detail/articles.vue

@@ -1,12 +1,29 @@
 <template>
   <view class="container">
     <!-- 品牌头部 -->
-    <PageBanner theme="mind-wisdom"
-      tagline="探索内心世界,培养健康心智"
-      quote="知己知彼,百战不殆" />
+    <PageBanner :theme="bannerTheme"
+      :tagline="bannerTagline"
+      :quote="bannerQuote" />
 
-    <!-- 分类筛选 -->
+    <!-- 维度筛选 -->
     <view class="filter-section">
+      <scroll-view scroll-x enable-flex show-scrollbar="false" class="dimension-scroll">
+        <view class="filter-chips">
+          <view
+            v-for="dim in dimensionList"
+            :key="dim.code"
+            :class="['filter-chip', currentDimension === dim.code ? 'active' : '']"
+            :style="currentDimension === dim.code ? { background: dim.color, color: '#fff' } : {}"
+            @click="onDimensionChange(dim.code)"
+          >
+            {{ dim.label }}
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+
+    <!-- 分类筛选 -->
+    <view class="filter-section" v-if="categoryList.length > 1">
       <scroll-view scroll-x enable-flex show-scrollbar="false" class="category-scroll">
         <view class="filter-chips">
           <view
@@ -21,14 +38,14 @@
       </scroll-view>
     </view>
 
-    <!-- 文章列表 -->
+    <!-- 知识列表 -->
     <scroll-view scroll-y class="article-list" @scrolltolower="onLoadMore">
       <view v-if="loading && articles.length === 0" class="loading-wrap">
         <text class="loading-text">加载中...</text>
       </view>
       <view v-else-if="articles.length === 0" class="empty-wrap">
-        <text class="empty-icon">📝</text>
-        <text class="empty-text">暂无相关文章</text>
+        <text class="empty-icon">📚</text>
+        <text class="empty-text">暂无相关知识</text>
       </view>
       <view v-else class="article-grid">
         <view
@@ -44,8 +61,11 @@
           />
           <view class="article-body">
             <view class="article-meta">
-              <text class="article-category">{{ item.categoryName || '心理健康' }}</text>
-              <text class="article-date">{{ item.publishDate || '' }}</text>
+              <text class="article-category">{{ item.categoryName || '知识' }}</text>
+              <view class="article-type-tags">
+                <text v-if="item.contentType" class="type-tag" :class="'type-' + item.contentType">{{ contentTypeLabel(item.contentType) }}</text>
+                <text v-if="item.difficultyLevel > 0" class="difficulty-tag">{{ '★'.repeat(item.difficultyLevel) }}</text>
+              </view>
             </view>
             <text class="article-title">{{ item.title }}</text>
             <text class="article-summary">{{ item.summary || item.content }}</text>
@@ -62,15 +82,12 @@
       <view v-if="noMore && articles.length > 0" class="no-more">
         <text class="no-more-text">— 没有更多了 —</text>
       </view>
-      <!-- 未登录提示 -->
       <view v-if="!isLoggedIn" class="login-hint-bar">
-        <text class="login-hint-text">🔒 登录后解锁全部文章</text>
+        <text class="login-hint-text">🔒 登录后解锁全部内容</text>
       </view>
-      <!-- 底部占位 -->
       <view class="bottom-spacer"></view>
     </scroll-view>
 
-
   </view>
 </template>
 
@@ -78,10 +95,23 @@
 import PageBanner from '../../components/PageBanner.vue'
 import { getArticleCategories, getArticleList } from '../../utils/api.js'
 
+var dimensionConfig = {
+  all:   { label: '全部', color: '#64748B', theme: 'mind-wisdom', tagline: '探索知识,滋养成长', quote: '学而时习之,不亦说乎' },
+  body:  { label: '身', color: '#FF8C42', theme: 'health',       tagline: '主动健康,从知识开始', quote: '健康是人生的第一财富' },
+  mind:  { label: '心', color: '#FF6B9D', theme: 'mind-wisdom',  tagline: '探索内心世界,培养健康心智', quote: '知己知彼,百战不殆' },
+  wisdom: { label: '智', color: '#6366F1', theme: 'wisdom',       tagline: '聪明学习,快乐成长', quote: '学而不思则罔,思而不学则殆' },
+  action:{ label: '行', color: '#10B981', theme: 'action',       tagline: '知行合一,方得始终', quote: '纸上得来终觉浅,绝知此事要躬行' },
+  wealth:{ label: '富', color: '#F59E0B', theme: 'wealth',       tagline: '理财智慧,从小培养', quote: '君子爱财,取之有道' }
+}
+
 export default {
   components: { PageBanner },
   data() {
     return {
+      dimensionList: Object.keys(dimensionConfig).map(function(k) {
+        return { code: k, label: dimensionConfig[k].label, color: dimensionConfig[k].color }
+      }),
+      currentDimension: 'all',
       categoryList: [{ id: '', name: '全部' }],
       categoryMap: {},
       currentCategory: '',
@@ -90,40 +120,64 @@ export default {
       size: 10,
       total: 0,
       loading: false,
+      loadingMore: false,
       noMore: false,
       isLoggedIn: false
     }
   },
-  onLoad() {
+  computed: {
+    bannerTheme: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].theme : 'mind-wisdom'
+    },
+    bannerTagline: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].tagline : ''
+    },
+    bannerQuote: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].quote : ''
+    }
+  },
+  onLoad: function(options) {
     this.isLoggedIn = !!uni.getStorageSync('token')
+    if (options && options.dimension && dimensionConfig[options.dimension]) {
+      this.currentDimension = options.dimension
+    }
     this.loadCategories()
   },
-  onShow() {
+  onShow: function() {
     this.isLoggedIn = !!uni.getStorageSync('token')
   },
   methods: {
+    contentTypeLabel: function(type) {
+      var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
+      return labels[type] || type
+    },
     async loadCategories() {
       try {
         const res = await getArticleCategories()
         if (res.code === 200 && res.data) {
           const cats = Array.isArray(res.data) ? res.data : []
-          let map = {}
+          var map = {}
           cats.forEach(function(c) { map[c.id] = c.name })
           this.categoryMap = map
           this.categoryList = [{ id: '', name: '全部' }].concat(cats)
         }
       } catch (e) {
-        // 默认分类列表
         this.categoryList = [
           { id: '', name: '全部' },
           { id: 1, name: '心理健康' },
           { id: 2, name: '情绪管理' }
         ]
       }
-      // 分类加载完成后加载文章
       this.loadArticles()
     },
-    onCategoryChange(id) {
+    onDimensionChange: function(code) {
+      this.currentDimension = code
+      this.page = 1
+      this.articles = []
+      this.noMore = false
+      this.loadArticles()
+    },
+    onCategoryChange: function(id) {
       this.currentCategory = id
       this.page = 1
       this.articles = []
@@ -134,15 +188,18 @@ export default {
       if (this.loading) return
       this.loading = true
       try {
-        let params = { page: this.page, size: this.size }
+        var params = { page: this.page, size: this.size }
         if (this.currentCategory) {
           params.categoryId = this.currentCategory
         }
+        if (this.currentDimension !== 'all') {
+          params.dimensionCode = this.currentDimension
+        }
         const res = await getArticleList(params)
         if (res.code === 200 && res.data) {
-          let pageData = res.data
-          let list = pageData.records || []
-          let mapped = list.map(function(item) {
+          var pageData = res.data
+          var list = pageData.records || []
+          var mapped = list.map(function(item) {
             return {
               id: item.id,
               title: item.title || '',
@@ -152,7 +209,9 @@ export default {
               author: item.author || '浠艾福',
               categoryName: item.categoryName || item.category || '',
               publishDate: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
-              readCount: item.viewCount || 0
+              readCount: item.viewCount || 0,
+              contentType: item.contentType || '',
+              difficultyLevel: item.difficultyLevel || 0
             }
           })
           if (this.page === 1) {
@@ -182,7 +241,7 @@ export default {
       if (this.isLoggedIn) {
         uni.navigateTo({ url: '/pages/mind/article-detail?id=' + id })
       } else {
-        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/mind/articles') })
+        uni.navigateTo({ url: '/pages/mind-detail/articles?redirect=' + encodeURIComponent('/pages/mind-detail/articles') })
       }
     }
   }
@@ -197,15 +256,20 @@ export default {
   background: #f5f7fa;
 }
 
-/* 分类筛选 */
+/* 筛选 */
 .filter-section {
   background: #fff;
   padding: 16rpx 0 12rpx;
   border-bottom: 1rpx solid #eee;
 }
+.dimension-scroll,
 .category-scroll {
   white-space: nowrap;
 }
+.dimension-scroll {
+  border-bottom: 1rpx solid #f0f0f0;
+  padding-bottom: 12rpx;
+}
 .filter-chips {
   display: flex;
   padding: 0 20rpx;
@@ -224,6 +288,50 @@ export default {
   background: #5B9BD5;
   color: #fff;
 }
+.filter-chip:first-child {
+  margin-left: 4rpx;
+}
+
+/* 文章元信息标签 */
+.article-meta {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  margin-bottom: 10rpx;
+}
+.article-type-tags {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  margin-left: auto;
+}
+.type-tag {
+  font-size: 18rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 6rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+.type-tag.type-article {
+  background: #e8f4fd;
+  color: #2b7fc4;
+}
+.type-tag.type-knowledge {
+  background: #e8f8ee;
+  color: #2a9055;
+}
+.type-tag.type-course {
+  background: #fef3e2;
+  color: #c47a2b;
+}
+.type-tag.type-tip {
+  background: #f3e8fd;
+  color: #7b2bc4;
+}
+.difficulty-tag {
+  font-size: 18rpx;
+  color: #f59e0b;
+}
 
 /* 文章列表 */
 .article-list {

+ 1 - 1
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=mind' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 237 - 18
cfc-frontend/pages/mind/article-detail.vue

@@ -35,12 +35,19 @@
         <text class="meta-read">{{ article.readTime || 3 }} 分钟阅读</text>
       </view>
 
-      <!-- 分类标签 -->
+      <!-- 分类标签 + 内容类型/难度 -->
       <view class="detail-category-row">
         <text class="detail-category">{{ article.categoryName || '' }}</text>
+        <text v-if="article.contentType" class="detail-type-badge" :class="'type-' + article.contentType">{{ contentTypeLabel(article.contentType) }}</text>
+        <text v-if="article.difficultyLevel && article.difficultyLevel > 0" class="detail-difficulty">{{ '★'.repeat(article.difficultyLevel) }}{{ '☆'.repeat(5 - article.difficultyLevel) }}</text>
       </view>
 
-      <!-- 文章标签 -->
+      <!-- 维度标签 -->
+      <view v-if="dimensionList.length" class="detail-dimensions">
+        <text v-for="(dim, idx) in dimensionList" :key="idx" class="dimension-tag" :style="{ color: dim.color, background: dim.bgColor }">{{ dim.label }}</text>
+      </view>
+
+      <!-- 知识标签 -->
       <view v-if="tagList.length" class="detail-tags">
         <text v-for="(tag, idx) in tagList" :key="idx" class="tag-item">{{ tag }}</text>
       </view>
@@ -53,9 +60,45 @@
         <rich-text :nodes="article.content" />
       </view>
 
-      <!-- 分享按钮 -->
-      <view class="share-btn-wrap">
-        <button class="share-btn" @click="handleShare">分享文章</button>
+      <!-- 收藏按钮 -->
+      <view class="action-bar">
+        <view class="action-btn" @click="toggleFavorite">
+          <text class="action-icon">{{ isFavorited ? '❤️' : '🤍' }}</text>
+          <text class="action-text">{{ isFavorited ? '已收藏' : '收藏' }}</text>
+        </view>
+        <view class="action-btn" @click="handleShare">
+          <text class="action-icon">📤</text>
+          <text class="action-text">分享</text>
+        </view>
+        <view class="action-btn">
+          <text class="action-icon">👁</text>
+          <text class="action-text">{{ article.viewCount || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- 相关文章 -->
+      <view class="related-section" v-if="relatedArticles.length > 0">
+        <view class="related-header">
+          <text class="related-title">📖 相关推荐</text>
+        </view>
+        <view class="related-list">
+          <view
+            v-for="item in relatedArticles"
+            :key="item.id"
+            class="related-card"
+            @click="goRelatedArticle(item.id)"
+          >
+            <image
+              class="related-cover"
+              :src="item.coverImage || '/static/default-article.png'"
+              mode="aspectFill"
+            />
+            <view class="related-info">
+              <text class="related-name">{{ item.title }}</text>
+              <text class="related-summary">{{ item.summary || '' }}</text>
+            </view>
+          </view>
+        </view>
       </view>
 
       <!-- 底部完成按钮 -->
@@ -102,7 +145,15 @@
 </template>
 
 <script>
-import { getArticleDetail, recordArticleRead } from '../../utils/api.js'
+import { getArticleDetail, getFeaturedArticles, recordArticleRead } from '../../utils/api.js'
+
+var DIMENSION_MAP = {
+  body: { label: '身', color: '#FF8C42', bgColor: 'rgba(255,140,66,0.1)' },
+  mind: { label: '心', color: '#FF6B9D', bgColor: 'rgba(255,107,157,0.1)' },
+  wisdom: { label: '智', color: '#6366F1', bgColor: 'rgba(99,102,241,0.1)' },
+  action: { label: '行', color: '#10B981', bgColor: 'rgba(16,185,129,0.1)' },
+  wealth: { label: '富', color: '#F59E0B', bgColor: 'rgba(245,158,11,0.1)' }
+}
 
 export default {
   data() {
@@ -113,15 +164,20 @@ export default {
       error: false,
       errorMsg: '',
       tagList: [],
+      dimensionList: [],
+      relatedArticles: [],
       readingSeconds: 0,
       readingTimer: null,
       readRecorded: false,
-      showShareModal: false
+      showShareModal: false,
+      isFavorited: false
     }
   },
   onLoad(options) {
     if (options && options.id) {
       this.articleId = options.id
+      this.token = uni.getStorageSync('token')
+      this.baseUrl = this.token ? '' : ''
       this.loadDetail(options.id)
       this.startReadingTimer()
     } else {
@@ -137,6 +193,10 @@ export default {
     this.stopReadingTimer()
   },
   methods: {
+    contentTypeLabel: function(type) {
+      var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
+      return labels[type] || type || ''
+    },
     async loadDetail(id) {
       this.loading = true
       this.error = false
@@ -153,6 +213,21 @@ export default {
               this.tagList = []
             }
           }
+          // 解析维度 ID JSON
+          if (res.data.dimensionIds) {
+            try {
+              var dimIds = JSON.parse(res.data.dimensionIds)
+              if (Array.isArray(dimIds)) {
+                this.dimensionList = dimIds.map(function(d) {
+                  return DIMENSION_MAP[d] || { label: d, color: '#64748B', bgColor: 'rgba(100,116,139,0.1)' }
+                })
+              }
+            } catch (e) {
+              this.dimensionList = []
+            }
+          }
+          // 加载相关文章
+          this.loadRelatedArticles()
         } else {
           this.error = true
           this.errorMsg = '文章不存在或无权限查看'
@@ -164,6 +239,33 @@ export default {
         this.loading = false
       }
     },
+    async loadRelatedArticles() {
+      try {
+        var dimensionCode = null
+        if (this.article && this.article.dimensionIds) {
+          try {
+            var dimIds = JSON.parse(this.article.dimensionIds)
+            if (Array.isArray(dimIds) && dimIds.length > 0) {
+              dimensionCode = dimIds[0]
+            }
+          } catch (e) {}
+        }
+        var params = { size: 3 }
+        if (dimensionCode) {
+          params.dimensionCode = dimensionCode
+        }
+        const res = await getFeaturedArticles(params)
+        if (res.code === 200 && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          // 排除当前文章
+          this.relatedArticles = list.filter(function(item) {
+            return item.id !== (this.article && this.article.id)
+          }.bind(this)).slice(0, 3)
+        }
+      } catch (e) {
+        // 静默处理
+      }
+    },
     getFormattedDate() {
       if (this.article && this.article.publishedAt) {
         return this.article.publishedAt.slice(0, 10)
@@ -174,8 +276,6 @@ export default {
       var self = this
       this.readingTimer = setInterval(function() {
         self.readingSeconds = self.readingSeconds + 1
-        // Auto-submit when reaching 60 seconds (threshold)
-        // But don't auto-submit - just indicate readiness
       }, 1000)
     },
     stopReadingTimer() {
@@ -189,6 +289,10 @@ export default {
       var s = seconds % 60
       return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
     },
+    toggleFavorite: function() {
+      this.isFavorited = !this.isFavorited
+      uni.showToast({ title: this.isFavorited ? '已收藏' : '已取消收藏', icon: 'none' })
+    },
     handleShare: function() {
       var self = this
       uni.showActionSheet({
@@ -264,6 +368,9 @@ export default {
       setTimeout(function() {
         uni.navigateBack()
       }, 1500)
+    },
+    goRelatedArticle: function(id) {
+      uni.redirectTo({ url: '/pages/mind/article-detail?id=' + id })
     }
   }
 }
@@ -372,6 +479,10 @@ export default {
 /* ===== 分类标签 ===== */
 .detail-category-row {
   padding: 16rpx 30rpx 0;
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-wrap: wrap;
 }
 .detail-category {
   display: inline-block;
@@ -381,6 +492,48 @@ export default {
   padding: 4rpx 16rpx;
   border-radius: 8rpx;
 }
+.detail-type-badge {
+  display: inline-block;
+  font-size: 18rpx;
+  padding: 2rpx 12rpx;
+  border-radius: 6rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+.detail-type-badge.type-article {
+  background: #e8f4fd;
+  color: #2b7fc4;
+}
+.detail-type-badge.type-knowledge {
+  background: #e8f8ee;
+  color: #2a9055;
+}
+.detail-type-badge.type-course {
+  background: #fef3e2;
+  color: #c47a2b;
+}
+.detail-type-badge.type-tip {
+  background: #f3e8fd;
+  color: #7b2bc4;
+}
+.detail-difficulty {
+  font-size: 18rpx;
+  color: #f59e0b;
+  margin-left: 4rpx;
+}
+
+/* ===== 维度标签 ===== */
+.detail-dimensions {
+  display: flex;
+  flex-direction: row;
+  padding: 8rpx 30rpx 0;
+  gap: 10rpx;
+}
+.dimension-tag {
+  font-size: 18rpx;
+  padding: 2rpx 14rpx;
+  border-radius: 6rpx;
+}
 
 /* ===== 标签 ===== */
 .detail-tags {
@@ -467,19 +620,85 @@ export default {
   border: none;
 }
 
-/* ===== 分享按钮 ===== */
-.share-btn-wrap {
+/* ===== 操作栏 ===== */
+.action-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
   padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+.action-btn {
   display: flex;
-  justify-content: flex-end;
+  flex-direction: column;
+  align-items: center;
+  padding: 8rpx 24rpx;
 }
-.share-btn {
-  padding: 16rpx 32rpx;
-  background: #F97316;
-  color: #fff;
-  border-radius: 32rpx;
+.action-icon {
+  font-size: 36rpx;
+  margin-bottom: 6rpx;
+}
+.action-text {
+  font-size: 22rpx;
+  color: #666;
+}
+
+/* ===== 相关推荐 ===== */
+.related-section {
+  padding: 30rpx;
+}
+.related-header {
+  margin-bottom: 20rpx;
+}
+.related-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.related-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.related-card {
+  display: flex;
+  flex-direction: row;
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.related-cover {
+  width: 180rpx;
+  height: 140rpx;
+  flex-shrink: 0;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+}
+.related-info {
+  flex: 1;
+  padding: 16rpx;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+.related-name {
   font-size: 26rpx;
-  border: none;
+  font-weight: bold;
+  color: #333;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  margin-bottom: 8rpx;
+}
+.related-summary {
+  font-size: 22rpx;
+  color: #999;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
 }
 .share-btn::after {
   border: none;

+ 133 - 25
cfc-frontend/pages/mind/articles.vue

@@ -1,12 +1,29 @@
 <template>
   <view class="container">
     <!-- 品牌头部 -->
-    <PageBanner theme="mind-wisdom"
-      tagline="探索内心世界,培养健康心智"
-      quote="知己知彼,百战不殆" />
+    <PageBanner :theme="bannerTheme"
+      :tagline="bannerTagline"
+      :quote="bannerQuote" />
 
-    <!-- 分类筛选 -->
+    <!-- 维度筛选 -->
     <view class="filter-section">
+      <scroll-view scroll-x enable-flex show-scrollbar="false" class="dimension-scroll">
+        <view class="filter-chips">
+          <view
+            v-for="dim in dimensionList"
+            :key="dim.code"
+            :class="['filter-chip', currentDimension === dim.code ? 'active' : '']"
+            :style="currentDimension === dim.code ? { background: dim.color, color: '#fff' } : {}"
+            @click="onDimensionChange(dim.code)"
+          >
+            {{ dim.label }}
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+
+    <!-- 分类筛选 -->
+    <view class="filter-section" v-if="categoryList.length > 1">
       <scroll-view scroll-x enable-flex show-scrollbar="false" class="category-scroll">
         <view class="filter-chips">
           <view
@@ -21,14 +38,14 @@
       </scroll-view>
     </view>
 
-    <!-- 文章列表 -->
+    <!-- 知识列表 -->
     <scroll-view scroll-y class="article-list" @scrolltolower="onLoadMore">
       <view v-if="loading && articles.length === 0" class="loading-wrap">
         <text class="loading-text">加载中...</text>
       </view>
       <view v-else-if="articles.length === 0" class="empty-wrap">
-        <text class="empty-icon">📝</text>
-        <text class="empty-text">暂无相关文章</text>
+        <text class="empty-icon">📚</text>
+        <text class="empty-text">暂无相关知识</text>
       </view>
       <view v-else class="article-grid">
         <view
@@ -44,8 +61,11 @@
           />
           <view class="article-body">
             <view class="article-meta">
-              <text class="article-category">{{ item.categoryName || '心理健康' }}</text>
-              <text class="article-date">{{ item.publishDate || '' }}</text>
+              <text class="article-category">{{ item.categoryName || '知识' }}</text>
+              <view class="article-type-tags">
+                <text v-if="item.contentType" class="type-tag" :class="'type-' + item.contentType">{{ contentTypeLabel(item.contentType) }}</text>
+                <text v-if="item.difficultyLevel > 0" class="difficulty-tag">{{ '★'.repeat(item.difficultyLevel) }}</text>
+              </view>
             </view>
             <text class="article-title">{{ item.title }}</text>
             <text class="article-summary">{{ item.summary || item.content }}</text>
@@ -62,15 +82,12 @@
       <view v-if="noMore && articles.length > 0" class="no-more">
         <text class="no-more-text">— 没有更多了 —</text>
       </view>
-      <!-- 未登录提示 -->
       <view v-if="!isLoggedIn" class="login-hint-bar">
-        <text class="login-hint-text">🔒 登录后解锁全部文章</text>
+        <text class="login-hint-text">🔒 登录后解锁全部内容</text>
       </view>
-      <!-- 底部占位 -->
       <view class="bottom-spacer"></view>
     </scroll-view>
 
-
   </view>
 </template>
 
@@ -78,10 +95,23 @@
 import PageBanner from '../../components/PageBanner.vue'
 import { getArticleCategories, getArticleList } from '../../utils/api.js'
 
+var dimensionConfig = {
+  all:   { label: '全部', color: '#64748B', theme: 'mind-wisdom', tagline: '探索知识,滋养成长', quote: '学而时习之,不亦说乎' },
+  body:  { label: '身', color: '#FF8C42', theme: 'health',       tagline: '主动健康,从知识开始', quote: '健康是人生的第一财富' },
+  mind:  { label: '心', color: '#FF6B9D', theme: 'mind-wisdom',  tagline: '探索内心世界,培养健康心智', quote: '知己知彼,百战不殆' },
+  wisdom: { label: '智', color: '#6366F1', theme: 'wisdom',       tagline: '聪明学习,快乐成长', quote: '学而不思则罔,思而不学则殆' },
+  action:{ label: '行', color: '#10B981', theme: 'action',       tagline: '知行合一,方得始终', quote: '纸上得来终觉浅,绝知此事要躬行' },
+  wealth:{ label: '富', color: '#F59E0B', theme: 'wealth',       tagline: '理财智慧,从小培养', quote: '君子爱财,取之有道' }
+}
+
 export default {
   components: { PageBanner },
   data() {
     return {
+      dimensionList: Object.keys(dimensionConfig).map(function(k) {
+        return { code: k, label: dimensionConfig[k].label, color: dimensionConfig[k].color }
+      }),
+      currentDimension: 'all',
       categoryList: [{ id: '', name: '全部' }],
       categoryMap: {},
       currentCategory: '',
@@ -90,40 +120,64 @@ export default {
       size: 10,
       total: 0,
       loading: false,
+      loadingMore: false,
       noMore: false,
       isLoggedIn: false
     }
   },
-  onLoad() {
+  computed: {
+    bannerTheme: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].theme : 'mind-wisdom'
+    },
+    bannerTagline: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].tagline : ''
+    },
+    bannerQuote: function() {
+      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].quote : ''
+    }
+  },
+  onLoad: function(options) {
     this.isLoggedIn = !!uni.getStorageSync('token')
+    if (options && options.dimension && dimensionConfig[options.dimension]) {
+      this.currentDimension = options.dimension
+    }
     this.loadCategories()
   },
-  onShow() {
+  onShow: function() {
     this.isLoggedIn = !!uni.getStorageSync('token')
   },
   methods: {
+    contentTypeLabel: function(type) {
+      var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
+      return labels[type] || type
+    },
     async loadCategories() {
       try {
         const res = await getArticleCategories()
         if (res.code === 200 && res.data) {
           const cats = Array.isArray(res.data) ? res.data : []
-          let map = {}
+          var map = {}
           cats.forEach(function(c) { map[c.id] = c.name })
           this.categoryMap = map
           this.categoryList = [{ id: '', name: '全部' }].concat(cats)
         }
       } catch (e) {
-        // 默认分类列表
         this.categoryList = [
           { id: '', name: '全部' },
           { id: 1, name: '心理健康' },
           { id: 2, name: '情绪管理' }
         ]
       }
-      // 分类加载完成后加载文章
       this.loadArticles()
     },
-    onCategoryChange(id) {
+    onDimensionChange: function(code) {
+      this.currentDimension = code
+      this.page = 1
+      this.articles = []
+      this.noMore = false
+      this.loadArticles()
+    },
+    onCategoryChange: function(id) {
       this.currentCategory = id
       this.page = 1
       this.articles = []
@@ -134,15 +188,18 @@ export default {
       if (this.loading) return
       this.loading = true
       try {
-        let params = { page: this.page, size: this.size }
+        var params = { page: this.page, size: this.size }
         if (this.currentCategory) {
           params.categoryId = this.currentCategory
         }
+        if (this.currentDimension !== 'all') {
+          params.dimensionCode = this.currentDimension
+        }
         const res = await getArticleList(params)
         if (res.code === 200 && res.data) {
-          let pageData = res.data
-          let list = pageData.records || []
-          let mapped = list.map(function(item) {
+          var pageData = res.data
+          var list = pageData.records || []
+          var mapped = list.map(function(item) {
             return {
               id: item.id,
               title: item.title || '',
@@ -152,7 +209,9 @@ export default {
               author: item.author || '浠艾福',
               categoryName: item.categoryName || item.category || '',
               publishDate: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
-              readCount: item.viewCount || 0
+              readCount: item.viewCount || 0,
+              contentType: item.contentType || '',
+              difficultyLevel: item.difficultyLevel || 0
             }
           })
           if (this.page === 1) {
@@ -197,15 +256,20 @@ export default {
   background: #f5f7fa;
 }
 
-/* 分类筛选 */
+/* 筛选 */
 .filter-section {
   background: #fff;
   padding: 16rpx 0 12rpx;
   border-bottom: 1rpx solid #eee;
 }
+.dimension-scroll,
 .category-scroll {
   white-space: nowrap;
 }
+.dimension-scroll {
+  border-bottom: 1rpx solid #f0f0f0;
+  padding-bottom: 12rpx;
+}
 .filter-chips {
   display: flex;
   padding: 0 20rpx;
@@ -224,6 +288,50 @@ export default {
   background: #5B9BD5;
   color: #fff;
 }
+.filter-chip:first-child {
+  margin-left: 4rpx;
+}
+
+/* 文章元信息标签 */
+.article-meta {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  margin-bottom: 10rpx;
+}
+.article-type-tags {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  margin-left: auto;
+}
+.type-tag {
+  font-size: 18rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 6rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+.type-tag.type-article {
+  background: #e8f4fd;
+  color: #2b7fc4;
+}
+.type-tag.type-knowledge {
+  background: #e8f8ee;
+  color: #2a9055;
+}
+.type-tag.type-course {
+  background: #fef3e2;
+  color: #c47a2b;
+}
+.type-tag.type-tip {
+  background: #f3e8fd;
+  color: #7b2bc4;
+}
+.difficulty-tag {
+  font-size: 18rpx;
+  color: #f59e0b;
+}
 
 /* 文章列表 */
 .article-list {

+ 1 - 2
cfc-frontend/pages/mind/index.vue

@@ -772,8 +772,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      var dimCode = this.currentDimensionCode
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=' + dimCode })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goMemberDetail: function(member) {

+ 1 - 1
cfc-frontend/pages/mind/member-mind-detail.vue

@@ -208,7 +208,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=mind' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 166 - 1
cfc-frontend/pages/promotion/index.vue

@@ -13,6 +13,34 @@
       <button class="share-btn" @click="shareCard">分享邀请卡</button>
     </view>
 
+    <!-- 统计卡片 -->
+    <view class="stats-cards">
+      <view class="stat-card today-card">
+        <text class="stat-value">{{ stats.todayCommission || 0 }}</text>
+        <text class="stat-label">今日佣金(分)</text>
+      </view>
+      <view class="stat-card month-card">
+        <text class="stat-value">{{ stats.monthCommission || 0 }}</text>
+        <text class="stat-label">本月佣金(分)</text>
+      </view>
+      <view class="stat-card total-card">
+        <text class="stat-value">{{ stats.totalCommission || 0 }}</text>
+        <text class="stat-label">累计佣金(分)</text>
+      </view>
+    </view>
+
+    <!-- 转化漏斗 -->
+    <view class="funnel-section" v-if="funnelSteps && funnelSteps.length > 0">
+      <text class="section-title">转化漏斗</text>
+      <view class="funnel">
+        <view class="funnel-step" v-for="(step, index) in funnelSteps" :key="index"
+              :style="{width: step.width + '%'}">
+          <text class="funnel-label">{{ step.label }}</text>
+          <text class="funnel-count">{{ step.count }}</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 收益概览 -->
     <view class="earnings-card">
       <view class="earnings-header">
@@ -93,7 +121,10 @@ export default {
       showPoster: false,
       qrCodeBase64: '',
       userAvatar: '',
-      userNickname: ''
+      userNickname: '',
+      stats: {},
+      funnelData: {},
+      funnelSteps: []
     }
   },
   computed: {
@@ -104,6 +135,10 @@ export default {
   onLoad() {
     this.loadData()
   },
+  onShow() {
+    this.loadStats()
+    this.loadFunnelData()
+  },
   onShareAppMessage() {
     var code = this.referralCode || ''
     var path = '/pages/invite/join'
@@ -144,6 +179,61 @@ export default {
         console.error('获取佣金总览失败', e)
       }
     },
+    loadStats: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      var baseUrl = ''
+      try {
+        var config = require('../../config.js')
+        baseUrl = config.default.baseUrl || config.baseUrl || ''
+      } catch (e) {
+        baseUrl = ''
+      }
+      uni.request({
+        url: baseUrl + '/api/commission/stats',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            self.stats = res.data.data || {}
+          }
+        }
+      })
+    },
+    loadFunnelData: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      var baseUrl = ''
+      try {
+        var config = require('../../config.js')
+        baseUrl = config.default.baseUrl || config.baseUrl || ''
+      } catch (e) {
+        baseUrl = ''
+      }
+      uni.request({
+        url: baseUrl + '/api/commission/funnel',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            self.funnelData = res.data.data || {}
+            self.buildFunnelSteps()
+          }
+        }
+      })
+    },
+    buildFunnelSteps: function() {
+      var data = this.funnelData
+      var max = Math.max(data.invites || 1, data.registered || 0, data.purchased || 0, data.repeatPurchased || 0, data.totalOrders || 0)
+      if (max < 1) max = 1
+      this.funnelSteps = [
+        { label: '邀请', count: data.invites || 0, width: 100 },
+        { label: '注册', count: data.registered || 0, width: Math.round(((data.registered || 0) / max) * 100) },
+        { label: '首购', count: data.purchased || 0, width: Math.round(((data.purchased || 0) / max) * 100) },
+        { label: '复购', count: data.repeatPurchased || 0, width: Math.round(((data.repeatPurchased || 0) / max) * 100) },
+        { label: '订单', count: data.totalOrders || 0, width: Math.round(((data.totalOrders || 0) / max) * 100) }
+      ]
+    },
     copyCode() {
       if (!this.referralCode) return
       uni.setClipboardData({
@@ -241,6 +331,81 @@ export default {
 .share-btn::after { border: none; }
 .share-btn:active { opacity: 0.8; }
 
+/* ===== 统计卡片 ===== */
+.stats-cards {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 30rpx;
+}
+.stat-card {
+  flex: 1;
+  margin: 0 10rpx;
+  border-radius: 16rpx;
+  padding: 24rpx 0;
+  text-align: center;
+  color: #fff;
+}
+.stat-card:first-child { margin-left: 0; }
+.stat-card:last-child { margin-right: 0; }
+.today-card { background: linear-gradient(135deg, #F97316, #FB923C); }
+.month-card { background: linear-gradient(135deg, #0EA5E9, #38BDF8); }
+.total-card { background: linear-gradient(135deg, #10B981, #34D399); }
+.stat-value {
+  font-size: 36rpx;
+  font-weight: bold;
+  display: block;
+}
+.stat-label {
+  font-size: 22rpx;
+  opacity: 0.9;
+  margin-top: 8rpx;
+  display: block;
+}
+
+/* ===== 转化漏斗 ===== */
+.funnel-section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.funnel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.funnel-step {
+  background: linear-gradient(90deg, #F97316, #FB923C);
+  border-radius: 12rpx;
+  padding: 20rpx 0;
+  margin-bottom: 16rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-left: 24rpx;
+  padding-right: 24rpx;
+  min-width: 60%;
+  transition: all 0.3s ease;
+}
+.funnel-label {
+  font-size: 26rpx;
+  color: #fff;
+  font-weight: 500;
+}
+.funnel-count {
+  font-size: 28rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
 /* ===== 收益总览 ===== */
 .earnings-card {
   background: #fff;

+ 352 - 0
cfc-frontend/pages/shop/after-sales-detail/after-sales-detail.vue

@@ -0,0 +1,352 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <scroll-view v-else-if="requestData.id" scroll-y class="content">
+      <!-- Status Header -->
+      <view :class="['status-header', 'header-' + requestData.status]">
+        <text class="status-icon">{{ statusIcon }}</text>
+        <text class="status-title">{{ statusLabel(requestData.status) }}</text>
+        <text v-if="requestData.status === 'rejected' && requestData.rejectReason" class="status-desc">
+          拒绝原因: {{ requestData.rejectReason }}
+        </text>
+      </view>
+
+      <!-- Order Info -->
+      <view class="section">
+        <text class="section-title">售后信息</text>
+        <view class="info-row">
+          <text class="info-label">订单编号</text>
+          <text class="info-value">{{ requestData.orderNo }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">售后类型</text>
+          <text class="info-value">{{ requestData.type === 'refund' ? '仅退款' : '退货退款' }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">退款金额</text>
+          <text class="info-value price">{{ formatPrice(requestData.refundAmount) }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">申请时间</text>
+          <text class="info-value">{{ formatTime(requestData.createdAt) }}</text>
+        </view>
+        <view v-if="requestData.handledAt" class="info-row">
+          <text class="info-label">处理时间</text>
+          <text class="info-value">{{ formatTime(requestData.handledAt) }}</text>
+        </view>
+      </view>
+
+      <!-- Reason -->
+      <view class="section">
+        <text class="section-title">申请原因</text>
+        <text class="reason-text">{{ requestData.reason || '未说明' }}</text>
+        <text v-if="requestData.description" class="desc-text">{{ requestData.description }}</text>
+      </view>
+
+      <!-- Images -->
+      <view v-if="imageList.length > 0" class="section">
+        <text class="section-title">凭证图片</text>
+        <view class="image-list">
+          <image v-for="(img, idx) in imageList" :key="idx" class="proof-image" :src="img" mode="aspectFill" @click="previewImage(idx)" />
+        </view>
+      </view>
+
+      <!-- Status Timeline -->
+      <view class="section">
+        <text class="section-title">处理进度</text>
+        <view class="timeline">
+          <view class="timeline-node latest">
+            <view class="timeline-dot dot-orange"></view>
+            <view class="timeline-content">
+              <text class="timeline-text">{{ statusLabel(requestData.status) }}</text>
+              <text class="timeline-time">{{ formatTime(requestData.handledAt || requestData.createdAt) }}</text>
+            </view>
+          </view>
+          <view class="timeline-node">
+            <view class="timeline-dot dot-gray"></view>
+            <view class="timeline-content">
+              <text class="timeline-text">提交售后申请</text>
+              <text class="timeline-time">{{ formatTime(requestData.createdAt) }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="bottom-actions">
+        <button class="btn-order" @click="goOrder">查看订单</button>
+      </view>
+    </scroll-view>
+    <view v-else class="empty-wrap">
+      <text class="empty-text">售后记录不存在</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+
+export default {
+  data() {
+    return {
+      requestData: {},
+      requestId: null,
+      loading: false
+    }
+  },
+  computed: {
+    statusIcon() {
+      var map = {
+        pending: '⏳',
+        approved: '✅',
+        rejected: '❌',
+        completed: '✅',
+        cancelled: '➖'
+      }
+      return map[this.requestData.status] || '📋'
+    },
+    imageList() {
+      if (!this.requestData.images) return []
+      try {
+        return JSON.parse(this.requestData.images)
+      } catch (e) {
+        return []
+      }
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.requestId = parseInt(options.id)
+      this.loadDetail()
+    }
+  },
+  methods: {
+    loadDetail() {
+      this.loading = true
+      var that = this
+      uni.request({
+        url: config.api('/api/shop/after-sales/detail'),
+        method: 'POST',
+        data: { id: this.requestId },
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          that.loading = false
+          if (res.data && res.data.code === 200) {
+            that.requestData = res.data.data || {}
+          }
+        },
+        fail: function() {
+          that.loading = false
+          uni.showToast({ title: '加载失败', icon: 'none' })
+        }
+      })
+    },
+    goOrder() {
+      if (this.requestData.orderNo) {
+        uni.navigateTo({ url: '/pages/shop/order-detail/order-detail?orderNo=' + this.requestData.orderNo })
+      }
+    },
+    previewImage(index) {
+      uni.previewImage({ urls: this.imageList, current: index })
+    },
+    statusLabel(status) {
+      var map = {
+        pending: '待审核',
+        approved: '审核通过,等待退款',
+        rejected: '审核未通过',
+        completed: '已完成',
+        cancelled: '已取消'
+      }
+      return map[status] || status
+    },
+    formatPrice(amount) {
+      if (amount === null || amount === undefined) return '0.00'
+      return '¥' + (Number(amount) / 100).toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) return ''
+      var d = new Date(time)
+      var year = d.getFullYear()
+      var month = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      var hour = String(d.getHours()).padStart(2, '0')
+      var min = String(d.getMinutes()).padStart(2, '0')
+      return year + '-' + month + '-' + day + ' ' + hour + ':' + min
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.content {
+  padding-bottom: 120rpx;
+}
+.status-header {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 60rpx 30rpx;
+  color: #fff;
+}
+.status-icon {
+  font-size: 80rpx;
+  margin-bottom: 16rpx;
+}
+.status-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  margin-bottom: 10rpx;
+}
+.status-desc {
+  font-size: 26rpx;
+  opacity: 0.9;
+}
+.header-pending {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+}
+.header-approved,
+.header-completed {
+  background: linear-gradient(135deg, #52c41a, #73d13d);
+}
+.header-rejected {
+  background: linear-gradient(135deg, #ff4d4f, #ff7875);
+}
+.header-cancelled {
+  background: linear-gradient(135deg, #999, #bbb);
+}
+.section {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.info-row:last-child {
+  border-bottom: none;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+}
+.price {
+  color: #F97316;
+  font-weight: bold;
+}
+.reason-text {
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.desc-text {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.5;
+  display: block;
+}
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.proof-image {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 8rpx;
+  background: #f5f5f5;
+}
+.timeline {
+  padding-left: 20rpx;
+}
+.timeline-node {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 30rpx;
+  position: relative;
+}
+.timeline-node:last-child {
+  margin-bottom: 0;
+}
+.timeline-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  margin-right: 20rpx;
+  margin-top: 6rpx;
+  flex-shrink: 0;
+}
+.dot-orange {
+  background: #F97316;
+}
+.dot-gray {
+  background: #ddd;
+}
+.timeline-content {
+  flex: 1;
+}
+.timeline-text {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+}
+.timeline-time {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+  display: block;
+}
+.bottom-actions {
+  padding: 30rpx;
+}
+.btn-order {
+  background: #fff;
+  color: #F97316;
+  font-size: 28rpx;
+  font-weight: bold;
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  border: 2rpx solid #F97316;
+}
+.btn-order::after {
+  border: none;
+}
+</style>

+ 282 - 0
cfc-frontend/pages/shop/after-sales/after-sales.vue

@@ -0,0 +1,282 @@
+<template>
+  <view class="container">
+    <view class="status-tabs">
+      <view
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        :class="['tab', currentTab === tab.value ? 'active' : '']"
+        @click="onTabChange(tab.value)"
+      >{{ tab.label }}</view>
+    </view>
+
+    <scroll-view
+      scroll-y
+      class="list-scroll"
+      @scrolltolower="onLoadMore"
+    >
+      <view v-if="loading && list.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="list.length === 0" class="empty-wrap">
+        <text class="empty-icon">📋</text>
+        <text class="empty-text">暂无售后记录</text>
+      </view>
+      <view v-else>
+        <view
+          v-for="item in list"
+          :key="item.id"
+          class="card"
+          @click="goDetail(item.id)"
+        >
+          <view class="card-header">
+            <text class="order-no">{{ item.orderNo }}</text>
+            <text :class="['status-badge', 'status-' + item.status]">{{ statusLabel(item.status) }}</text>
+          </view>
+          <view class="card-body">
+            <view class="info-row">
+              <text class="info-label">类型</text>
+              <text class="info-value">{{ item.type === 'refund' ? '仅退款' : '退货退款' }}</text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">退款金额</text>
+              <text class="info-value price">{{ formatPrice(item.refundAmount) }}</text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">申请时间</text>
+              <text class="info-value">{{ formatTime(item.createdAt) }}</text>
+            </view>
+          </view>
+          <text class="card-arrow">›</text>
+        </view>
+      </view>
+      <view v-if="loadingMore" class="loading-more">
+        <text class="loading-text">加载中...</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+
+export default {
+  data() {
+    return {
+      statusTabs: [
+        { label: '全部', value: '' },
+        { label: '待审核', value: 'pending' },
+        { label: '已通过', value: 'approved' },
+        { label: '已拒绝', value: 'rejected' },
+        { label: '已完成', value: 'completed' }
+      ],
+      currentTab: '',
+      list: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      loadingMore: false,
+      noMore: false
+    }
+  },
+  onLoad() {
+    this.loadList()
+  },
+  methods: {
+    onTabChange(value) {
+      this.currentTab = value
+      this.page = 1
+      this.list = []
+      this.noMore = false
+      this.loadList()
+    },
+    loadList() {
+      if (this.loading) return
+      this.loading = true
+      var that = this
+      uni.request({
+        url: config.api('/api/shop/after-sales/list'),
+        method: 'POST',
+        data: {},
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          that.loading = false
+          that.loadingMore = false
+          if (res.data && res.data.code === 200) {
+            var records = res.data.data || []
+            if (that.page === 1) {
+              that.list = records
+            } else {
+              that.list = that.list.concat(records)
+            }
+            that.noMore = records.length < that.size
+          }
+        },
+        fail: function() {
+          that.loading = false
+          that.loadingMore = false
+        }
+      })
+    },
+    onLoadMore() {
+      if (this.noMore || this.loadingMore) return
+      this.page = this.page + 1
+      this.loadingMore = true
+      this.loadList()
+    },
+    goDetail(id) {
+      uni.navigateTo({ url: '/pages/shop/after-sales-detail/after-sales-detail?id=' + id })
+    },
+    statusLabel(status) {
+      var map = {
+        pending: '待审核',
+        approved: '已通过',
+        rejected: '已拒绝',
+        completed: '已完成',
+        cancelled: '已取消'
+      }
+      return map[status] || status
+    },
+    formatPrice(amount) {
+      if (amount === null || amount === undefined) return '0.00'
+      return '¥' + (Number(amount) / 100).toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) return ''
+      var d = new Date(time)
+      var year = d.getFullYear()
+      var month = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      return year + '-' + month + '-' + day
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+.status-tabs {
+  display: flex;
+  background: #fff;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+.tab {
+  flex: 1;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666;
+  padding: 10rpx 0;
+}
+.tab.active {
+  color: #F97316;
+  font-weight: bold;
+  border-bottom: 4rpx solid #F97316;
+}
+.list-scroll {
+  height: calc(100vh - 100rpx);
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.card {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  position: relative;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+  margin-bottom: 16rpx;
+}
+.order-no {
+  font-size: 24rpx;
+  color: #999;
+}
+.status-badge {
+  font-size: 24rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.status-pending {
+  color: #F97316;
+  background: #fff7e6;
+}
+.status-approved {
+  color: #52c41a;
+  background: #f6ffed;
+}
+.status-rejected {
+  color: #ff4d4f;
+  background: #fff2f0;
+}
+.status-completed {
+  color: #999;
+  background: #f5f5f5;
+}
+.status-cancelled {
+  color: #999;
+  background: #f5f5f5;
+}
+.card-body {
+  margin-bottom: 8rpx;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 6rpx 0;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+}
+.price {
+  color: #F97316;
+  font-weight: bold;
+}
+.card-arrow {
+  position: absolute;
+  right: 24rpx;
+  top: 50%;
+  font-size: 40rpx;
+  color: #ccc;
+  transform: translateY(-50%);
+}
+.loading-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.loading-text {
+  font-size: 24rpx;
+  color: #999;
+}
+</style>

+ 13 - 0
cfc-frontend/pages/shop/cart/cart.vue

@@ -172,11 +172,24 @@ export default {
     increaseQty(item) {
       item.quantity = item.quantity + 1
       item.subtotal = item.unitPrice * item.quantity
+      this.syncQty(item)
     },
     decreaseQty(item) {
       if (item.quantity <= 1) return
       item.quantity = item.quantity - 1
       item.subtotal = item.unitPrice * item.quantity
+      this.syncQty(item)
+    },
+    syncQty(item) {
+      uni.request({
+        url: config.api('/api/cart/update'),
+        method: 'POST',
+        data: { productId: item.productId, quantity: item.quantity },
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        }
+      })
     },
     onRemove(productId) {
       var that = this

+ 106 - 2
cfc-frontend/pages/shop/detail/detail.vue

@@ -44,6 +44,16 @@
         </view>
       </view>
 
+      <!-- Quantity Selector -->
+      <view class="qty-section">
+        <text class="section-title">数量</text>
+        <view class="qty-stepper">
+          <text class="qty-btn" @click="decreaseQty">−</text>
+          <text class="qty-value">{{ quantity }}</text>
+          <text class="qty-btn" @click="increaseQty">+</text>
+        </view>
+      </view>
+
       <view class="desc-section">
         <text class="section-title">商品介绍</text>
         <text class="desc-text">{{ product.description || '暂无介绍' }}</text>
@@ -60,6 +70,7 @@
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
+          <button class="btn-cart" @click="onAddToCart">加入购物车</button>
           <button class="btn-buy" @click="onBuy">立即购买</button>
         </view>
       </view>
@@ -72,6 +83,7 @@
 
 <script>
 import { productDetail, productSkuList } from '@/utils/api.js'
+import config from '@/config.js'
 
 export default {
   data() {
@@ -81,7 +93,8 @@ export default {
       productId: null,
       skuList: [],
       selectedSkuId: null,
-      selectedSku: null
+      selectedSku: null,
+      quantity: 1
     }
   },
   computed: {
@@ -171,7 +184,7 @@ export default {
         uni.showToast({ title: '请选择规格', icon: 'none' })
         return
       }
-      var url = '/pages/shop/checkout/checkout?productId=' + this.productId + '&quantity=1'
+      var url = '/pages/shop/checkout/checkout?productId=' + this.productId + '&quantity=' + this.quantity
       url += '&productName=' + encodeURIComponent(this.product.name)
       url += '&coverImage=' + encodeURIComponent(this.product.coverImage || '')
       url += '&price=' + (this.selectedSku ? this.selectedSku.price : this.product.price)
@@ -179,6 +192,49 @@ export default {
         url += '&skuId=' + this.selectedSkuId
       }
       uni.navigateTo({ url: url })
+    },
+    onAddToCart() {
+      var userId = uni.getStorageSync('userId')
+      if (!userId) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
+      if (this.product.hasSku && !this.selectedSkuId) {
+        uni.showToast({ title: '请选择规格', icon: 'none' })
+        return
+      }
+      var that = this
+      uni.request({
+        url: config.api('/api/cart/add'),
+        method: 'POST',
+        data: { productId: this.productId },
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            uni.showToast({ title: '已加入购物车', icon: 'success' })
+          } else {
+            uni.showToast({ title: res.data.message || '添加失败', icon: 'none' })
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '网络请求失败', icon: 'none' })
+        }
+      })
+    },
+    increaseQty() {
+      var stock = this.currentStock
+      if (this.quantity >= stock) {
+        uni.showToast({ title: '已达最大库存', icon: 'none' })
+        return
+      }
+      this.quantity = this.quantity + 1
+    },
+    decreaseQty() {
+      if (this.quantity <= 1) return
+      this.quantity = this.quantity - 1
     }
   }
 }
@@ -326,8 +382,56 @@ export default {
   line-height: 60rpx;
   border-radius: 30rpx;
   border: none;
+  margin-left: 16rpx;
 }
 .btn-buy::after {
   border: none;
 }
+.btn-cart {
+  background: #fff;
+  color: #F97316;
+  font-size: 26rpx;
+  font-weight: bold;
+  padding: 0 30rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  border-radius: 30rpx;
+  border: 2rpx solid #F97316;
+}
+.btn-cart::after {
+  border: none;
+}
+.qty-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.qty-stepper {
+  display: flex;
+  align-items: center;
+  border: 1rpx solid #eee;
+  border-radius: 8rpx;
+}
+.qty-btn {
+  width: 60rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  text-align: center;
+  font-size: 32rpx;
+  color: #333;
+  background: #f9f9f9;
+}
+.qty-value {
+  width: 80rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  text-align: center;
+  font-size: 30rpx;
+  color: #333;
+  border-left: 1rpx solid #eee;
+  border-right: 1rpx solid #eee;
+}
 </style>

+ 24 - 0
cfc-frontend/pages/shop/index.vue

@@ -1,5 +1,9 @@
 <template>
   <view class="container">
+    <!-- 购物车入口 -->
+    <view class="cart-entry" @click="goCart">
+      <text class="cart-icon">🛒</text>
+    </view>
     <!-- Level 1 分类:2列横排网格 -->
     <view class="cat-level1">
       <view
@@ -199,6 +203,9 @@ export default {
     },
     goDetail(id) {
       uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id })
+    },
+    goCart() {
+      uni.navigateTo({ url: '/pages/shop/cart/cart' })
     }
   }
 }
@@ -398,4 +405,21 @@ export default {
   font-size: 24rpx;
   color: #ccc;
 }
+.cart-entry {
+  position: fixed;
+  top: 20rpx;
+  right: 20rpx;
+  z-index: 99;
+  width: 80rpx;
+  height: 80rpx;
+  background: rgba(255,255,255,0.95);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1);
+}
+.cart-icon {
+  font-size: 40rpx;
+}
 </style>

+ 3 - 1
cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue

@@ -152,7 +152,9 @@ export default {
         uni.navigateTo({ url: '/pages/login/login' })
       }
     },
-    goMoreProducts: function() {}
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
+    }
   }
 }
 </script>

+ 2 - 2
cfc-frontend/pages/wisdom/index.vue

@@ -432,11 +432,11 @@ export default {
       }
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     loadFeaturedArticles: function() {
       var self = this
-      getFeaturedArticles({ size: 5 }).then(function(res) {
+      getFeaturedArticles({ size: 5, dimensionCode: 'wisdom' }).then(function(res) {
         if (res.code === 200 && res.data) {
           var gradientColors = [
             'linear-gradient(135deg, #6366F1, #818CF8)',

+ 3 - 1
cfc-frontend/pages/wisdom/member-wisdom-detail.vue

@@ -152,7 +152,9 @@ export default {
         uni.navigateTo({ url: '/pages/login/login' })
       }
     },
-    goMoreProducts: function() {}
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
+    }
   }
 }
 </script>

+ 3 - 1
cfc-frontend/pages/wisdom/wisdom_temp/index.vue

@@ -177,7 +177,9 @@ export default {
       uni.navigateTo({ url: '/pages/activity/index' })
     },
     goActivityDetail(id) {},
-    goMoreProducts() {},
+    goMoreProducts() {
+      uni.navigateTo({ url: '/pages/shop/index' })
+    },
     goProductDetail(id) {
       var token = uni.getStorageSync('token')
       if (token) {

+ 1 - 1
cfc-frontend/pages/wisdom/wisdom_temp/member-wisdom-detail.vue

@@ -211,7 +211,7 @@ export default {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=wisdom' })
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goBack: function() { uni.navigateBack() }

+ 8 - 2
cfc-web/src/router/index.js

@@ -252,8 +252,14 @@ const routes = [
         name: 'ArticleEdit',
         component: () => import('@/views/admin/ArticleEdit.vue'),
         meta: { title: '文章编辑', perm: 'articles:manage' }
-      },
-      // ========== 分佣裂变系统 ==========
+      },
+      {
+        path: 'knowledge-tags',
+        name: 'KnowledgeTag',
+        component: () => import('@/views/admin/KnowledgeTag.vue'),
+        meta: { title: '知识标签', perm: 'articles:categories' }
+      },
+      // ========== 分佣裂变系统 ==========
       {
         path: 'product-profit-rate',
         name: 'ProductProfitRate',

+ 6 - 5
cfc-web/src/views/Layout.vue

@@ -177,11 +177,13 @@ export default {
 
           ]},
 
-        // ===== 6. 内容中心 (admin + article_manager + activity_manager) =====
-        { title: '内容中心', icon: 'el-icon-document', perm: 'content',
+        // ===== 6. 知识中心 (admin + article_manager + activity_manager) =====
+        { title: '知识中心', icon: 'el-icon-document', perm: 'content',
           children: [
-            { path: '/article-categories', label: '文章分类', icon: 'el-icon-folder', perm: 'articles:categories' },
-            { path: '/article-manage', label: '文章管理', icon: 'el-icon-document', perm: 'articles:manage' },
+            { path: '/article-categories', label: '知识分类', icon: 'el-icon-folder', perm: 'articles:categories' },
+            { path: '/article-manage', label: '知识管理', icon: 'el-icon-document', perm: 'articles:manage' },
+            { path: '/knowledge-tags', label: '知识标签', icon: 'el-icon-price-tag', perm: 'articles:categories' },
+            { path: '/knowledge-base', label: '知识库', icon: 'el-icon-reading', perm: 'system:config' },
             { path: '/activities', label: '活动列表', icon: 'el-icon-date', perm: 'activity:list' },
             { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
             { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
@@ -230,7 +232,6 @@ export default {
             { path: '/users', label: '用户管理', icon: 'el-icon-user', perm: 'system:users' },
             { path: '/operation-logs', label: '操作日志', icon: 'el-icon-s-order', perm: 'system:logs' },
             { path: '/dimension-config', label: '维度配置', icon: 'el-icon-data-line', perm: 'system:config' },
-            { path: '/knowledge-base', label: '知识库管理', icon: 'el-icon-reading', perm: 'system:config' },
             { path: '/relationship-types', label: '关系类型', icon: 'el-icon-share', perm: 'system:config' },
             { path: '/education-systems', label: '教育体系', icon: 'el-icon-s-management', perm: 'system:education' },
             { path: '/service-types', label: '服务类型', icon: 'el-icon-s-management', perm: 'system:config' },

+ 48 - 0
cfc-web/src/views/admin/ArticleEdit.vue

@@ -26,6 +26,26 @@
           </el-select>
         </el-form-item>
 
+        <el-form-item label="内容类型">
+          <el-select v-model="form.contentType" placeholder="选择类型" style="width: 200px;" :disabled="readonly">
+            <el-option label="文章" value="article" />
+            <el-option label="知识点" value="knowledge" />
+            <el-option label="课程" value="course" />
+            <el-option label="贴士" value="tip" />
+          </el-select>
+          <span style="color: #999; font-size: 12px; margin-left: 10px;">标识内容属于哪类知识</span>
+        </el-form-item>
+
+        <el-form-item label="难度等级">
+          <el-rate v-model="form.difficultyLevel" :max="5" :disabled="readonly" show-text :texts="['入门', '简单', '中等', '较难', '深入']" style="margin-top: 6px;" />
+        </el-form-item>
+
+        <el-form-item label="知识标签">
+          <el-select v-model="form.tagIds" multiple placeholder="选择知识标签" style="width: 400px;" :disabled="readonly" clearable>
+            <el-option v-for="tag in tagOptions" :key="tag.id" :label="tag.name" :value="tag.id" />
+          </el-select>
+        </el-form-item>
+
         <el-form-item label="摘要">
           <el-input v-model="form.summary" type="textarea" :rows="3" placeholder="请输入文章摘要" maxlength="500" show-word-limit :disabled="readonly" />
         </el-form-item>
@@ -98,6 +118,7 @@ import {
   adminArticleDetail
 } from '@/api/article.js'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
+import { getTagList } from '@/api/dimension.js'
 
 export default {
   name: 'ArticleEdit',
@@ -111,6 +132,7 @@ export default {
       readonly: false,
       articleWasPublished: false,
       categories: [],
+      tagOptions: [],
       form: {
         id: null,
         categoryId: '',
@@ -119,6 +141,9 @@ export default {
         coverImage: '',
         content: '',
         tags: '',
+        contentType: 'article',
+        difficultyLevel: 0,
+        tagIds: [],
         author: '',
         readTime: 3,
         relatedDimensions: [],
@@ -140,6 +165,7 @@ export default {
   },
   created() {
     this.loadCategories()
+    this.loadTagOptions()
     const id = this.$route.query.id
     const copyId = this.$route.query.copyId
     this.readonly = this.$route.query.readonly === '1'
@@ -162,6 +188,9 @@ export default {
       coverImage: '',
       content: '',
       tags: '',
+      contentType: 'article',
+      difficultyLevel: 0,
+      tagIds: [],
       author: '',
       readTime: 3,
       relatedDimensions: [],
@@ -186,6 +215,16 @@ export default {
         console.error('加载分类失败', e)
       }
     },
+    async loadTagOptions() {
+      try {
+        const res = await getTagList()
+        if (res.data) {
+          this.tagOptions = res.data
+        }
+      } catch (e) {
+        console.error('加载标签失败', e)
+      }
+    },
     async loadDetail(id) {
       this.loading = true
       try {
@@ -204,6 +243,9 @@ export default {
             coverImage: d.coverImage || '',
             content: d.content || '',
             tags: d.tags || '',
+            contentType: d.contentType || 'article',
+            difficultyLevel: d.difficultyLevel || 0,
+            tagIds: d.tagIds || [],
             author: d.author || '',
             readTime: d.readTime || 3,
             relatedDimensions: d.relatedDimensions || [],
@@ -239,6 +281,9 @@ export default {
             coverImage: d.coverImage || '',
             content: d.content || '',
             tags: d.tags || '',
+            contentType: d.contentType || 'article',
+            difficultyLevel: d.difficultyLevel || 0,
+            tagIds: [],
             author: d.author || '',
             readTime: d.readTime || 3,
             relatedDimensions: d.relatedDimensions || [],
@@ -294,6 +339,9 @@ export default {
         coverImage: this.form.coverImage,
         content: this.form.content,
         tags: this.form.tags,
+        contentType: this.form.contentType,
+        difficultyLevel: this.form.difficultyLevel || 0,
+        tagIds: this.form.tagIds,
         author: this.form.author,
         readTime: this.form.readTime,
         relatedDimensions: this.form.relatedDimensions,

+ 69 - 0
cfc-web/src/views/admin/ArticleManage.vue

@@ -18,6 +18,33 @@
           <el-option label="全部分类" value="" />
           <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
         </el-select>
+        <el-select v-model="filters.contentType" placeholder="内容类型" clearable @change="loadList" style="width: 130px;">
+          <el-option label="全部" value="" />
+          <el-option label="文章" value="article" />
+          <el-option label="知识点" value="knowledge" />
+          <el-option label="课程" value="course" />
+          <el-option label="贴士" value="tip" />
+        </el-select>
+        <el-select v-model="filters.difficultyLevel" placeholder="难度" clearable @change="loadList" style="width: 100px;">
+          <el-option label="全部" value="" />
+          <el-option label="1星" value="1" />
+          <el-option label="2星" value="2" />
+          <el-option label="3星" value="3" />
+          <el-option label="4星" value="4" />
+          <el-option label="5星" value="5" />
+        </el-select>
+        <el-select v-model="filters.dimensionCode" placeholder="维度" clearable @change="loadList" style="width: 100px;">
+          <el-option label="全部" value="" />
+          <el-option label="身" value="body" />
+          <el-option label="心" value="mind" />
+          <el-option label="智" value="wisdom" />
+          <el-option label="行" value="action" />
+          <el-option label="富" value="wealth" />
+        </el-select>
+        <el-select v-model="filters.tagId" placeholder="知识标签" clearable @change="loadList" style="width: 150px;" filterable>
+          <el-option label="全部" value="" />
+          <el-option v-for="tag in tagOptions" :key="tag.id" :label="tag.name" :value="tag.id" />
+        </el-select>
         <el-input v-model="filters.keyword" placeholder="搜索文章标题..." clearable style="width: 220px;" @keyup.enter.native="loadList" />
         <el-button type="primary" icon="el-icon-search" @click="loadList">搜索</el-button>
       </div>
@@ -42,6 +69,27 @@
             <el-tag v-else size="small" type="info">私密</el-tag>
           </template>
         </el-table-column>
+        <el-table-column label="内容类型" width="90">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.contentType === 'article'" size="small" type="primary">文章</el-tag>
+            <el-tag v-else-if="row.contentType === 'knowledge'" size="small" type="success">知识点</el-tag>
+            <el-tag v-else-if="row.contentType === 'course'" size="small" type="warning">课程</el-tag>
+            <el-tag v-else-if="row.contentType === 'tip'" size="small" type="info">贴士</el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="难度" width="70">
+          <template slot-scope="{ row }">
+            <span v-if="row.difficultyLevel && row.difficultyLevel > 0">{{ row.difficultyLevel }}星</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="维度" width="100">
+          <template slot-scope="{ row }">
+            <span v-if="row.dimensionIds">{{ row.dimensionIds }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="author" label="作者" width="100" />
         <el-table-column label="状态" width="90">
           <template slot-scope="{ row }">
@@ -127,6 +175,7 @@ import {
   adminArticleDetail,
   adminArticleCreate
 } from '@/api/article.js'
+import { getTagList } from '@/api/dimension.js'
 
 export default {
   name: 'ArticleManage',
@@ -140,9 +189,14 @@ export default {
       filters: {
         status: '',
         categoryId: '',
+        contentType: '',
+        difficultyLevel: '',
+        dimensionCode: '',
+        tagId: '',
         keyword: ''
       },
       categories: [],
+      tagOptions: [],
       rejectDialogVisible: false,
       rejectReason: '',
       currentAuditRow: null
@@ -151,6 +205,7 @@ export default {
   created() {
     Promise.all([
       this.loadCategories().catch(e => console.error('加载分类失败', e)),
+      this.loadTagOptions().catch(e => console.error('加载标签失败', e)),
       this.loadList().catch(e => this.$message.error('加载文章列表失败'))
     ])
   },
@@ -165,6 +220,16 @@ export default {
         console.error('加载分类失败', e)
       }
     },
+    async loadTagOptions() {
+      try {
+        const res = await getTagList()
+        if (res.data) {
+          this.tagOptions = res.data
+        }
+      } catch (e) {
+        console.error('加载知识标签失败', e)
+      }
+    },
     async loadList() {
       this.loading = true
       try {
@@ -173,6 +238,10 @@ export default {
           size: this.size,
           status: this.filters.status || undefined,
           categoryId: this.filters.categoryId || undefined,
+          contentType: this.filters.contentType || undefined,
+          difficultyLevel: this.filters.difficultyLevel || undefined,
+          dimensionCode: this.filters.dimensionCode || undefined,
+          tagId: this.filters.tagId || undefined,
           keyword: this.filters.keyword || undefined
         })
         if (res.data) {

+ 134 - 0
cfc-web/src/views/admin/KnowledgeTag.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="knowledge-tag">
+    <el-card>
+      <div slot="header">
+        <span>知识标签</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 新增标签</el-button>
+      </div>
+      <el-table :data="tags" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column prop="tagName" label="标签名称" min-width="200" />
+        <el-table-column label="类型" width="140">
+          <template slot-scope="{ row }">
+            <el-tag size="small" :type="row.tagType === 'category' ? 'success' : 'info'">
+              {{ row.tagType === 'category' ? '分类标签' : '通用标签' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="180" />
+        <el-table-column label="操作" width="160" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" plain @click="handleEdit(row)">编辑</el-button>
+            <el-button size="mini" type="danger" plain @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑标签' : '新增标签'" width="450px" :close-on-click-modal="false">
+      <el-form :model="form" label-width="80px">
+        <el-form-item label="名称">
+          <el-input v-model="form.tagName" placeholder="请输入标签名称" />
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="form.tagType" placeholder="选择类型" style="width: 200px;">
+            <el-option label="通用标签" value="general" />
+            <el-option label="分类标签" value="category" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getTagList, saveTag, deleteTag } from '@/api/dimension.js'
+
+export default {
+  name: 'KnowledgeTag',
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      tags: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: {
+        id: null,
+        tagName: '',
+        tagType: 'general'
+      }
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getTagList()
+        if (res.data) {
+          this.tags = res.data
+        }
+      } catch (e) {
+        this.$message.error('加载标签列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = { id: null, tagName: '', tagType: 'general' }
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = { id: row.id, tagName: row.tagName, tagType: row.tagType || 'general' }
+      this.dialogVisible = true
+    },
+    async handleSubmit() {
+      if (!this.form.tagName.trim()) {
+        this.$message.warning('请输入标签名称')
+        return
+      }
+      this.submitting = true
+      try {
+        await saveTag(this.form)
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      } finally {
+        this.submitting = false
+      }
+    },
+    async handleDelete(row) {
+      this.$confirm(`确定删除标签「${row.tagName}」吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await deleteTag(row.id)
+          this.$message.success('删除成功')
+          this.loadData()
+        } catch (e) {
+          this.$message.error(e.message || '删除失败')
+        }
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.knowledge-tag {
+  padding: 20px;
+}
+</style>