Explorar o código

chore: merge remote rebuild + 6 commits (sort API + growth layout + web updates)

E2E Test Bot hai 1 mes
pai
achega
c1665b3c17

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -15,6 +15,7 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/articles")
@@ -47,7 +48,11 @@ public class AdminArticleController {
         String auditStatus = (String) body.get("auditStatus");
         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, contentType, difficultyLevel, dimensionCode, tagId, auditStatus, page, size));
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = body.get("sort") != null
+                ? (List<Map<String, String>>) body.get("sort")
+                : null;
+        return Result.success(articleService.getAdminList(status, categoryId, keyword, contentType, difficultyLevel, dimensionCode, tagId, auditStatus, page, size, sortSpecs));
     }
 
     @PostMapping("/create")

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -97,7 +97,11 @@ public class AdminController {
         if (params.containsKey("phone") && params.get("phone") != null && !params.get("phone").toString().isEmpty()) {
             wrapper.like(User::getPhone, String.valueOf(params.get("phone")));
         }
-        SortUtil.applySort(wrapper);
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+        SortUtil.applySort(wrapper, sortSpecs, null);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }

+ 10 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -16,6 +16,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import com.etotem.cfc.util.SortUtil;
 
 /**
  * 管理端-商品管理控制器
@@ -42,9 +43,14 @@ public class AdminProductController {
         Integer size = rawSize;
 
         Page<Product> pageParam = new Page<>(page, size);
-        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
-                .orderByAsc(Product::getSortOrder)
-                .orderByDesc(Product::getCreatedAt);
+        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>();
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+        if (sortSpecs == null || sortSpecs.isEmpty()) {
+            wrapper.orderByAsc(Product::getSortOrder).orderByDesc(Product::getCreatedAt);
+        }
 
         if (status != null && !status.isEmpty()) {
             wrapper.eq(Product::getStatus, status);
@@ -69,6 +75,7 @@ if (params.get("distributionSystemId") != null) {
     }
     wrapper.eq(Product::getDistributionSystemId, distId);
 }
+        SortUtil.applySort(wrapper, sortSpecs, null);
 
         Page<Product> result = productService.adminProductList(pageParam, wrapper);
         List<ProductDTO> records = result.getRecords().stream()

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java

@@ -12,7 +12,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
+import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.util.SortUtil;
 
 @RestController
 @RequestMapping("/api/admin/product/order")
@@ -31,7 +33,11 @@ public class AdminProductOrderController {
         String endDate = (String) params.get("endDate");
         Integer page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         Integer size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
-        return orderService.adminOrderPage(page, size, status, keyword, startDate, endDate);
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+        return orderService.adminOrderPage(page, size, status, keyword, startDate, endDate, sortSpecs);
     }
 
     @Operation(summary = "订单详情")

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPpointController.java

@@ -27,9 +27,13 @@ public class AdminProductPpointController {
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
         String keyword = (String) params.get("keyword");
         Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
 
         Page<ProductPpoint> pageParam = new Page<>(page, size);
-        Map<String, Object> result = ppointService.adminList(pageParam, keyword, productId);
+        Map<String, Object> result = ppointService.adminList(pageParam, keyword, productId, sortSpecs);
         return Result.success(result);
     }
 

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

@@ -350,7 +350,8 @@ public class ArticleService {
 
     public Page<Article> getAdminList(String status, Long categoryId, String keyword,
                                           String contentType, Integer difficultyLevel,
-                                          String dimensionCode, Long tagId, String auditStatus, int page, int size) {
+                                          String dimensionCode, Long tagId, String auditStatus, int page, int size,
+                                          List<Map<String, String>> sortSpecs) {
         LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
                 .orderByDesc(Article::getCreatedAt);
 
@@ -378,6 +379,7 @@ public class ArticleService {
         if (auditStatus != null && !auditStatus.isEmpty()) {
             wrapper.eq(Article::getAuditStatus, auditStatus);
         }
+        SortUtil.applySort(wrapper, sortSpecs, null);
 
         return articleMapper.selectPage(new Page<>(page, size), wrapper);
     }

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

@@ -1020,7 +1020,7 @@ public class ProductOrderService {
         return Result.success(list.stream().map(ProductOrderDTO::from).collect(Collectors.toList()));
     }
 
-    public Result<Map<String, Object>> adminOrderPage(int page, int size, String status, String keyword, String startDate, String endDate) {
+    public Result<Map<String, Object>> adminOrderPage(int page, int size, String status, String keyword, String startDate, String endDate, List<Map<String, String>> sortSpecs) {
         Page<ProductOrder> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
             .orderByDesc(ProductOrder::getCreatedAt);
@@ -1046,7 +1046,7 @@ public class ProductOrderService {
                 wrapper.lt(ProductOrder::getCreatedAt, cal.getTime());
             } catch (Exception ignored) {}
         }
-        SortUtil.applySort(wrapper);
+        SortUtil.applySort(wrapper, sortSpecs, null);
         Page<ProductOrder> result = orderMapper.selectPage(pageParam, wrapper);
         List<ProductOrderDTO> records = result.getRecords().stream()
             .map(ProductOrderDTO::from)

+ 141 - 68
cfc-frontend/pages/growth-main/index.vue

@@ -43,7 +43,7 @@
         </view>
         <view class="feat-card" @click="handleGuestFeatureTap">
           <text class="feat-icon">📋</text>
-          <text class="feat-title">成长回看</text>
+          <text class="feat-title">成长记录</text>
           <text class="feat-desc">每周家庭健康问卷</text>
         </view>
         <view class="feat-card" @click="handleGuestFeatureTap">
@@ -144,14 +144,15 @@
       <view class="section">
         <view class="progress-card">
           <view class="progress-header">
-            <text class="progress-title">今日干预进度</text>
+            <text class="progress-title">今日干预</text>
             <text class="progress-date">{{ todayDate }}</text>
           </view>
-          <view class="progress-item" v-for="(item, idx) in progressItems" :key="getProgressKey(item)">
-            <text class="progress-icon">{{ item.icon }}</text>
-            <text class="progress-label">{{ item.label }}</text>
-            <text class="progress-status" :class="item.done ? 'status-done' : 'status-pending'">{{ item.done ? '✔ 已完成' : '' }}</text>
-            <text class="progress-btn" v-if="!item.done" @click="handleProgressClick(item)">{{ item.actionText || '补录' }}</text>
+          <view class="progress-grid">
+            <view class="progress-grid-item" v-for="(item, idx) in progressItems" :key="getProgressKey(item)" @click="handleProgressClick(item)">
+              <text class="progress-grid-icon">{{ item.icon }}</text>
+              <text class="progress-grid-label">{{ item.label }}</text>
+              <text class="progress-grid-status" :class="item.done ? 'status-done' : 'status-pending'">{{ item.done ? '✔' : (item.actionText || '去记录') }}</text>
+            </view>
           </view>
           <view class="progress-footer">
             <text class="footer-text">今日完成 {{ doneCount }}/{{ totalCount }}</text>
@@ -170,8 +171,18 @@
         </view>
       </view>
       <view class="section">
-        <view class="action-card" @click="goToFoods">
+        <view class="action-card" @click="goToDiet">
           <text class="action-icon">🍽️</text>
+          <view class="action-info">
+            <text class="action-title">饮食主页</text>
+            <text class="action-desc">今日共餐 · 食材推荐 · 饮食记录</text>
+          </view>
+          <text class="action-arrow">›</text>
+        </view>
+      </view>
+      <view class="section">
+        <view class="action-card" @click="goToFoods">
+          <text class="action-icon">🥗</text>
           <view class="action-info">
             <text class="action-title">饮食推荐</text>
             <text class="action-desc">基于菌群报告的个性化食材建议</text>
@@ -180,17 +191,36 @@
         </view>
       </view>
 
+      <view class="section">
+        <view class="diet-card">
+          <view class="diet-card-header">
+            <text class="diet-card-title">🥬 今日推荐食材</text>
+            <text class="diet-card-link" @click="goToDiet">饮食主页 ›</text>
+          </view>
+          <view class="diet-card-content" v-if="ingredients.length > 0">
+            <view class="diet-item" v-for="(item, index) in ingredients" :key="getIngredientKey(item, index)">
+              <text class="ing-name">{{ item.name }}</text>
+              <text class="ing-reason" v-if="item.reason">{{ item.reason }}</text>
+            </view>
+          </view>
+          <view class="diet-empty" v-else>
+            <text class="empty-text">{{ ingredientsLoading ? '加载中...' : '暂无推荐食材' }}</text>
+            <text class="empty-hint" v-if="!ingredientsLoading">上传菌群报告后生成个性化推荐</text>
+          </view>
+        </view>
+      </view>
+
       <!-- 推荐文章 -->
       <view class="section" v-if="healthArticles.length > 0 || growthArticles.length > 0">
         <view class="rec-section-title">📖 推荐阅读</view>
-        <view class="rec-card" v-for="item in healthArticles" :key="'ha'+item.id" @click="goToArticle(item.id)">
+        <view class="rec-card" v-for="item in healthArticles" :key="getArticleKey(item)" @click="goToArticle(item.id)">
           <image class="rec-img" :src="item.coverImage" mode="aspectFill" v-if="item.coverImage"/>
           <view class="rec-info">
             <text class="rec-title">{{ item.title }}</text>
             <text class="rec-desc">{{ item.summary }}</text>
           </view>
         </view>
-        <view class="rec-card" v-for="item in growthArticles" :key="'ga'+item.id" @click="goToArticle(item.id)">
+        <view class="rec-card" v-for="item in growthArticles" :key="getArticleKey(item)" @click="goToArticle(item.id)">
           <image class="rec-img" :src="item.coverImage" mode="aspectFill" v-if="item.coverImage"/>
           <view class="rec-info">
             <text class="rec-title">{{ item.title }}</text>
@@ -202,14 +232,14 @@
       <!-- 推荐商品 -->
       <view class="section" v-if="healthProducts.length > 0 || growthProducts.length > 0">
         <view class="rec-section-title">🛒 推荐商品</view>
-        <view class="rec-product" v-for="item in healthProducts" :key="'hp'+item.id" @click="goToProduct(item.id)">
+        <view class="rec-product" v-for="item in healthProducts" :key="getProductKey(item)" @click="goToProduct(item.id)">
           <image class="rec-pimg" :src="item.coverImage" mode="aspectFill"/>
           <view class="rec-pinfo">
             <text class="rec-pname">{{ item.name }}</text>
             <text class="rec-pdesc">{{ item.intro || item.description || '' }}</text>
           </view>
         </view>
-        <view class="rec-product" v-for="item in growthProducts" :key="'gp'+item.id" @click="goToProduct(item.id)">
+        <view class="rec-product" v-for="item in growthProducts" :key="getProductKey(item)" @click="goToProduct(item.id)">
           <image class="rec-pimg" :src="item.coverImage" mode="aspectFill"/>
           <view class="rec-pinfo">
             <text class="rec-pname">{{ item.name }}</text>
@@ -219,23 +249,23 @@
       </view>
 
       <view class="section">
-        <view class="tracking-card">
-          <view class="tracking-tabs">
-            <view class="tracking-tab" :class="activeTab === tab.key ? 'tab-active' : ''" v-for="tab in trackingTabs" :key="tab.key" @click="switchTab(tab.key)">
-              <text class="tab-text" :class="activeTab === tab.key ? 'tab-active-text' : ''">{{ tab.label }}</text>
+        <view class="section-title-row">📊 记录概览</view>
+        <view class="record-grid">
+          <view class="record-card" v-for="tab in trackingTabs" :key="tab.key" @click="viewAllRecordsByType(tab.key)">
+            <view class="record-card-header">
+              <text class="record-card-icon">{{ tab.icon }}</text>
+              <text class="record-card-label">{{ tab.label }}</text>
             </view>
-          </view>
-          <view class="tracking-content">
-            <view class="tracking-empty" v-if="currentRecords.length === 0">
-              <text class="empty-icon">📊</text>
-              <text class="empty-text">还没有记录</text>
-              <text class="empty-hint">开始记录{{ activeTabLabel }}吧</text>
+            <view class="record-card-body" v-if="(trackingData[tab.key] || []).length > 0">
+              <view class="record-card-item" v-for="(rec, rIdx) in (trackingData[tab.key] || []).slice(0, 2)" :key="getRecordKey(rec)">
+                <text class="record-card-date">{{ rec._date || rec._text }}</text>
+                <text class="record-card-content">{{ rec._content }}</text>
+              </view>
             </view>
-            <view class="tracking-record" v-for="(rec, rIdx) in currentRecords" :key="getRecordKey(rec)">
-              <text class="record-date">{{ rec._date || rec._text }}</text>
-              <text class="record-content">{{ rec._content }}</text>
+            <view class="record-card-empty" v-else>
+              <text class="empty-text">暂无记录</text>
             </view>
-            <view class="tracking-more" v-if="currentRecords.length > 0" @click="viewAllRecords">查看全部 ›</view>
+            <view class="record-card-footer">查看全部 ›</view>
           </view>
         </view>
       </view>
@@ -263,7 +293,7 @@
       </view>
       <view class="section">
         <view class="survey-card">
-          <text class="section-title-row">📋 成长回看</text>
+          <text class="section-title-row">📋 成长记录</text>
           <view class="survey-pending" v-if="pendingSurvey">
             <text class="survey-alert">⚠ 本周还未完成调研</text>
             <text class="survey-title-text">{{ pendingSurvey.title }}</text>
@@ -289,20 +319,20 @@
 import PageBanner from '../../components/PageBanner.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import { healthCheckinList, getDietDailyRecords, getSurveyStatus, getSurveyHistory, getFoodCautionList, getGrowthRecommendations } from '../../utils/api.js'
+import { healthCheckinList, getDietDailyRecords, getDietIngredients, getSurveyStatus, getSurveyHistory, getFoodCautionList, getGrowthRecommendations, getExerciseList, getMindCheckinList } from '../../utils/api.js'
 export default {
   components: { PageBanner, LoginGuideCard, DimensionActivities },
-  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], activeTab: 'diet', trackingTabs: [{ key: 'diet', label: '饮食' }, { key: 'exercise', label: '运动' }, { key: 'sleep', label: '睡眠' }, { key: 'emotion', label: '心情' }, { key: 'survey', label: '调研' }], trackingData: { diet: [], exercise: [], sleep: [], emotion: [], survey: [] }, streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
+  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], trackingTabs: [{ key: 'diet', label: '饮食', icon: '🍽️' }, { key: 'exercise', label: '运动', icon: '🏃' }, { key: 'sleep', label: '睡眠', icon: '😴' }, { key: 'emotion', label: '心情', icon: '😊' }], trackingData: { diet: [], exercise: [], sleep: [], emotion: [] }, streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
       healthArticles: [],
       healthProducts: [],
       growthArticles: [],
       growthProducts: [],
-      pendingSurvey: null, allDone: false, surveyHistory: [], isLoggedIn: false } },
+      pendingSurvey: null, allDone: false, surveyHistory: [], isLoggedIn: false,
+      ingredients: [],
+      ingredientsLoading: false } },
   computed: {
     doneCount: function() { var c = 0; for (var i = 0; i < this.progressItems.length; i++) if (this.progressItems[i] && this.progressItems[i].done) c++; return c },
-    totalCount: function() { return this.progressItems.length },
-    activeTabLabel: function() { var m = { diet: '饮食', exercise: '运动', sleep: '睡眠', emotion: '心情', survey: '调研' }; return m[this.activeTab] || '' },
-    currentRecords: function() { return this.trackingData[this.activeTab] || [] }
+    totalCount: function() { return this.progressItems.length }
   },
   onShow() {
     this.isLoggedIn = !!uni.getStorageSync('token')
@@ -317,12 +347,18 @@ export default {
     pad: function(n) { return n < 10 ? '0' + n : '' + n },
     getProgressKey: function(item) { return item && item.label ? item.label : 'p-' + Math.random() },
     getRecordKey: function(rec) { return rec && rec._text ? rec._text + rec._content : 'r-' + Math.random() },
+    getIngredientKey: function(item, index) { return 'ing-' + (item.id || index) },
     getHistoryKey: function(h) { return h && h._date ? h._date + h._content : 'h-' + Math.random() },
+    getArticleKey: function(item) { return item && item.id ? 'a' + item.id : 'a-' + Math.random() },
+    getProductKey: function(item) { return item && item.id ? 'p' + item.id : 'p-' + Math.random() },
     dismissReminder: function() { this.hasIncomplete = false },
-    switchTab: function(key) { this.activeTab = key; this.loadTrackingData(key) },
+    viewAllRecordsByType: function(key) {
+      var m = { diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind-detail/emotion-checkin' }
+      if (m[key]) uni.navigateTo({ url: m[key] })
+    },
     handleProgressClick: function(item) {
       if (!item || !item.key) return
-      var m = { checkin: '/pages/health/daily-checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind/index' }
+      var m = { checkin: '/pages/health/daily-checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind-detail/emotion-checkin' }
       var url = m[item.key]
       if (item.key === 'survey' && this.pendingSurvey && this.pendingSurvey.templateId) {
         url = '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '')
@@ -330,24 +366,24 @@ export default {
       if (url) uni.navigateTo({ url: url })
     },
     startSurvey: function() { if (this.pendingSurvey && this.pendingSurvey.templateId) uni.navigateTo({ url: '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '') }) },
-    viewAllRecords: function() { var m = { diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', survey: '/pages/health/survey-history?memberId=' + this.memberId, emotion: '/pages/mind/index' }; if (m[this.activeTab]) uni.navigateTo({ url: m[this.activeTab] }) },
     viewAllHistory: function() { uni.navigateTo({ url: '/pages/health/survey-history?memberId=' + this.memberId }) },
     goActivityDetail: function(act) { uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id }) },
     goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
+    goToDiet: function() { uni.navigateTo({ url: '/pages/diet/index' }) },
     goToFoods: function() { uni.navigateTo({ url: '/pages/growth/foods-detail' }) },
     goToArticle: function(id) { uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
     goToProduct: function(id) { uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id }) },
     goPage: function(url) { uni.navigateTo({ url: url }) },
-    loadTrackingData: function(key) { return this.trackingData[key] || [] },
     async loadAllData() {
       var self = this
-      try { await self.loadProgressItems() } catch(e) {}
-      try { await self.loadCautionData() } catch(e) {}
-      try { await self.loadGrowthRecommendations() } catch(e) {}
-      try { await self.loadSurveyData() } catch(e) {}
-      try { await self.loadMilestones() } catch(e) {}
+      self.loadProgressItems()
+      self.loadSurveyData()
+      self.loadGrowthRecommendations()
+      self.loadCautionData()
+      self.loadMilestones()
+      self.loadDietIngredients()
     },
     async loadProgressItems() {
       var self = this
@@ -366,8 +402,22 @@ export default {
         if (dRes.code === 200 && dRes.data && dRes.data.length > 0) dietDone = true
       } catch(e) {}
       items.push({ key: 'diet', icon: '🍽️', label: '饮食记录', done: dietDone, actionText: '补录' })
-      items.push({ key: 'exercise', icon: '🏃', label: '运动记录', done: false, actionText: '记录' })
-      items.push({ key: 'emotion', icon: '😊', label: '心情记录', done: false, actionText: '记录' })
+      var exerciseDone = false
+      try {
+        var exRes = await getExerciseList({})
+        if (exRes.code === 200 && exRes.data && exRes.data.length > 0) {
+          for (var ex = 0; ex < exRes.data.length; ex++) { var exRec = exRes.data[ex]; if (exRec && exRec.startTime && typeof exRec.startTime === 'string' && exRec.startTime.substring(0, 10) === self.todayDate) { exerciseDone = true; break } }
+        }
+      } catch(e) {}
+      items.push({ key: 'exercise', icon: '🏃', label: '运动记录', done: exerciseDone, actionText: '记录' })
+      var emotionDone = false
+      try {
+        var emRes = await getMindCheckinList({ page: 1, size: 20 })
+        if (emRes.code === 200 && emRes.data && emRes.data.length > 0) {
+          for (var em = 0; em < emRes.data.length; em++) { var emRec = emRes.data[em]; if (emRec && emRec.checkinDate && typeof emRec.checkinDate === 'string' && emRec.checkinDate.substring(0, 10) === self.todayDate) { emotionDone = true; break } }
+        }
+      } catch(e) {}
+      items.push({ key: 'emotion', icon: '😊', label: '心情记录', done: emotionDone, actionText: '记录' })
       items.push({ key: 'survey', icon: '📋', label: '本周调研', done: self.allDone, actionText: self.allDone ? '' : '开始' })
       self.progressItems = items.filter(function(it) { return it && it.key })
       self.incompleteCount = 0
@@ -381,7 +431,7 @@ export default {
         if (statusRes.code === 200 && statusRes.data) {
           var list = statusRes.data
           self.pendingSurvey = null; self.allDone = true; var hasAny = false
-          for (var si = 0; si < list.length; si++) { var s = list[si]; if (!s) continue; hasAny = true; if (s.status === 'pending') { self.pendingSurvey = { templateId: s.templateId, title: s.title || '成长回看', dimension: s.dimension || '' }; self.allDone = false } }
+          for (var si = 0; si < list.length; si++) { var s = list[si]; if (!s) continue; hasAny = true; if (s.status === 'pending') { self.pendingSurvey = { templateId: s.templateId, title: s.title || '成长记录', dimension: s.dimension || '' }; self.allDone = false } }
           if (!hasAny) { self.pendingSurvey = null; self.allDone = false }
         }
       } catch(e) {}
@@ -423,6 +473,17 @@ export default {
       } catch(e) {}
       self.activeChallenges = 0
       self.completeness = self.streakDays > 0 ? Math.min(100, Math.round(self.streakDays * 10)) : 0
+    },
+    async loadDietIngredients() {
+      var self = this
+      self.ingredientsLoading = true
+      try {
+        var res = await getDietIngredients()
+        self.ingredientsLoading = false
+        if (res && res.ingredients) {
+          self.ingredients = res.ingredients
+        }
+      } catch(e) { self.ingredientsLoading = false; console.error('loadDietIngredients error:', e) }
     }
   }
 }
@@ -483,33 +544,45 @@ export default {
 .progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}
 .progress-title{font-size:28rpx;font-weight:bold;color:#333}
 .progress-date{font-size:22rpx;color:#999}
-.progress-item{display:flex;align-items:center;padding:12rpx 0;border-bottom:1rpx solid #f5f5f5}
-.progress-item:last-of-type{border-bottom:none}
-.progress-icon{font-size:28rpx;margin-right:12rpx;flex-shrink:0}
-.progress-label{font-size:26rpx;color:#333;flex:1}
-.progress-status{font-size:22rpx;margin-right:8rpx;flex-shrink:0}
+.progress-grid{display:flex;flex-wrap:wrap;gap:12rpx}
+.progress-grid-item{width:calc(33.33% - 8rpx);box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#FFF7ED;border-radius:16rpx;padding:20rpx 12rpx;border:1rpx solid rgba(249,115,22,0.12)}
+.progress-grid-item:active{opacity:0.85;background:#FFEDD5}
+.progress-grid-icon{font-size:40rpx;margin-bottom:8rpx}
+.progress-grid-label{font-size:24rpx;color:#333;margin-bottom:8rpx;text-align:center}
+.progress-grid-status{font-size:20rpx;flex-shrink:0}
 .status-done{color:#10B981}
-.status-pending{color:#ccc}
-.progress-btn{font-size:22rpx;color:#F97316;padding:4rpx 16rpx;border:1rpx solid #F97316;border-radius:20rpx;flex-shrink:0}
+.status-pending{color:#F97316}
 .progress-footer{display:flex;justify-content:space-between;align-items:center;margin-top:16rpx;padding-top:16rpx;border-top:1rpx solid #f5f5f5}
 .footer-text{font-size:24rpx;color:#666;font-weight:bold}
 .footer-streak{font-size:24rpx;color:#F97316;font-weight:bold}
-.tracking-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
-.tracking-tabs{display:flex;margin-bottom:16rpx}
-.tracking-tab{flex:1;text-align:center;padding:10rpx 0;border-radius:20rpx;margin-right:8rpx;background:#FFF7ED}
-.tracking-tab:last-child{margin-right:0}
-.tab-active{background:#F97316}
-.tab-text{font-size:24rpx;color:#666}
-.tab-active-text{color:#fff;font-weight:bold}
-.tracking-content{min-height:200rpx}
-.tracking-empty{display:flex;flex-direction:column;align-items:center;padding:60rpx 0}
-.empty-icon{font-size:60rpx;margin-bottom:12rpx}
-.empty-text{font-size:26rpx;color:#999}
-.empty-hint{font-size:22rpx;color:#ccc;margin-top:8rpx}
-.tracking-record{display:flex;align-items:center;padding:10rpx 0;border-bottom:1rpx solid #f5f5f5}
-.record-date{font-size:22rpx;color:#999;width:120rpx;flex-shrink:0}
-.record-content{font-size:24rpx;color:#333;flex:1}
-.tracking-more{text-align:right;font-size:24rpx;color:#F97316;padding:12rpx 0 0 0}
+/* 记录概览:平铺卡片 */
+.record-grid{display:flex;flex-wrap:wrap;justify-content:space-between}
+.record-card{width:calc(50% - 8rpx);box-sizing:border-box;background:#fff;border-radius:20rpx;padding:20rpx;margin-bottom:16rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
+.record-card:active{opacity:0.85}
+.record-card-header{display:flex;align-items:center;margin-bottom:12rpx}
+.record-card-icon{font-size:32rpx;margin-right:8rpx}
+.record-card-label{font-size:26rpx;font-weight:bold;color:#333}
+.record-card-body{display:flex;flex-direction:column}
+.record-card-item{display:flex;flex-direction:column;padding:10rpx 0;border-bottom:1rpx solid #f7f7f7}
+.record-card-item:last-child{border-bottom:none}
+.record-card-date{font-size:20rpx;color:#999;margin-bottom:2rpx}
+.record-card-content{font-size:24rpx;color:#666;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}
+.record-card-empty{display:flex;align-items:center;justify-content:center;padding:24rpx 0}
+.record-card-empty .empty-text{font-size:22rpx;color:#bbb}
+.record-card-footer{font-size:22rpx;color:#F97316;text-align:right;padding-top:10rpx}
+/* 今日推荐食材 */
+.diet-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
+.diet-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}
+.diet-card-title{font-size:28rpx;font-weight:bold;color:#333}
+.diet-card-link{font-size:24rpx;color:#F97316}
+.diet-card-content{display:flex;flex-direction:column}
+.diet-item{display:flex;flex-direction:column;padding:16rpx 0;border-bottom:1rpx solid #f5f5f5}
+.diet-item:last-child{border-bottom:none}
+.ing-name{font-size:26rpx;font-weight:bold;color:#333;margin-bottom:4rpx}
+.ing-reason{font-size:22rpx;color:#888}
+.diet-empty{display:flex;flex-direction:column;align-items:center;padding:40rpx 0}
+.diet-empty .empty-text{font-size:26rpx;color:#999}
+.diet-empty .empty-hint{font-size:22rpx;color:#ccc;margin-top:8rpx}
 .milestone-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
 .section-title-row{font-size:26rpx;font-weight:bold;color:#333;margin-bottom:16rpx;display:block}
 .milestone-grid{display:flex;justify-content:space-between}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-ccb6224b55f2e8f1582153c659816ca483ce79a1
+a118164c3ecb09042f3ed5caa0a7d193c8b6dab3

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,153 @@
 
 ---
 
+## v1.0.1009 (2026-08-12)
+
+### 新功能
+- 修复排序功能后端消费body sort + 前端构建修复
+
+### 其他
+- - AdminController.getUsers: 读body sort透传SortUtil
+- - AdminArticleController + ArticleService: 读body sort透传
+- - AdminProductController: 读body sort,条件化默认排序(避免SQL冲突)
+- - AdminProductOrderController + ProductOrderService: 读body sort透传
+- - AdminRecipeController + RecipeService: 加分页+读body sort
+- - AdminCouponController + CouponService: 加分页+读body sort
+- - AdminProductPpointController + PpointService: 读body sort透传
+- - PpointConfigAdminController + MealRecommendService: 适配签名变更
+- 前端:
+- - 修复7个vue文件的+col_key+语法错误+错位header+重复import
+- - npm run build通过
+- 
+
+
+## v1.0.1008 (2026-08-12)
+
+### 新功能
+- 成长页布局重构 - 饮食入口+今日推荐食材+记录卡平铺+干预快捷网格
+
+
+## v1.0.1007 (2026-08-12)
+
+### Bug 修复
+- 修复排序批量 patch 产生的语法错误,恢复构建
+
+### 其他
+- - ArticleManage.vue: header 移到 viewCount 列,修 col_key,删重复 import,补 sortableColumns
+- - OrderManage.vue: header 移到 orderNo 列,修 col_key,补 sortableColumns
+- - ProductManage.vue: header 移到 name/salesCount 列,createdAt 修 col_key,补 sortableColumns
+- - Recipes.vue: header 移到 name 列,补 import SortMixin,修 col_key
+- - CouponManagement.vue: header 移到 name 列,修 275/389 语法,补 sortableColumns,加 loadList 别名
+- - ProductPpointManage.vue: 修 156 行 }、sort 语法
+- npm run build 通过
+- 
+
+
+## v1.0.1006 (2026-08-12)
+
+### 新功能
+- rebuild cfclub with essential project files
+
+
+## v1.0.1005 (2026-08-12)
+
+### Bug 修复
+- 关卡页与报告渲染组件 :key 改为方法调用,消除非h5平台编译告警
+- stable_token重试失败时回退常规token接口
+
+### 其他
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1004 (2026-08-12)
+
+### Bug 修复
+- 成长页推荐卡片 :key 改为方法调用,修复非h5平台编译告警
+- 成长页成长回看改名为成长记录,追踪标签移除调研
+
+
+## v1.0.1003 (2026-08-12)
+
+### Bug 修复
+- 报告上传改走 JD Cloud OSS,LangGraph 解析支持远程 URL
+
+### 其他
+- saveUploadFile 仍写本地磁盘,LangGraph 容器读不到文件(400 文件不存在)。
+- 修复:
+- 1. HealthReportController 注入 StorageService,uploadOnly/uploadReport/
+-    parsePreview 改用 storageService.storeFile → 文件上传到京东 OSS
+- 2. parseByDraftId/parsePreview 对远程 OSS URL 直接传 URL 给 LangGraph,
+-    本地路径才用 getFilePath(); 本地 Java fallback 用 getFilePath 下载
+- 3. 删除不再使用的 saveUploadFile/resolveUploadFilePath 私有方法
+- 4. LangGraph report_parse.py 的 file_path 支持 http(s) URL,自动下载到
+-    临时文件后解析(不依赖共享 volume 时序)
+- 
+
+
+## v1.0.1002 (2026-08-12)
+
+### 文档
+- 明确商品积分仅按现金实付部分计算
+- 家庭平台积分(CF值)体系设计文档
+
+### Bug 修复
+- 移除报告审核 Tab 及相关路由
+- 迁移227 product_skus.id 升级为 BIGINT
+
+### 其他
+- - 生产库 id/product_id 为 INT,实体为 Long,插入超出 INT 范围的 id 报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- 
+
+
+## v1.0.1001 (2026-08-12)
+
+### 新功能
+- 健康报告三合一 - 统一报告列表/审核/代传为单页带Tab
+
+### Bug 修复
+- RegionController 移除硬编码数据,改用 StreetService 读取 DB 全量省市区数据
+
+
+## v1.0.1000 (2026-08-12)
+
+### Bug 修复
+- 迁移226 products 添加 growth_category 列
+
+### 其他
+- - Product 实体已含字段,生产库缺列,商品列表分页查询报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- - schema.sql 同步补列
+- 
+
+
+## v1.0.999 (2026-08-12)
+
+### Bug 修复
+- 迁移225 articles 添加 growth_category 列
+
+### 其他
+- - Article 实体已含字段,生产库缺列,getFeatured 精选查询报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- - schema.sql 同步补列 + 解决 unlock_gates/microbiome_article 冲突
+- 
+
+
+## v1.0.998 (2026-08-12)
+
+### Bug 修复
+- 成长页推荐只调用一次API,前端按growth_category分组显示
+
+
+## v1.0.997 (2026-08-12)
+
+### Bug 修复
+- 推荐接口移除 growth_category 过滤,直接取精选内容
+
+
+
 ## v1.0.996 (2026-08-12)
 
 ### 新功能

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.996
+> 当前版本: v1.0.1009
 
 ## 历史版本
 
@@ -8,6 +8,153 @@
 
 ---
 
+## v1.0.1009 (2026-08-12)
+
+### 新功能
+- 修复排序功能后端消费body sort + 前端构建修复
+
+### 其他
+- - AdminController.getUsers: 读body sort透传SortUtil
+- - AdminArticleController + ArticleService: 读body sort透传
+- - AdminProductController: 读body sort,条件化默认排序(避免SQL冲突)
+- - AdminProductOrderController + ProductOrderService: 读body sort透传
+- - AdminRecipeController + RecipeService: 加分页+读body sort
+- - AdminCouponController + CouponService: 加分页+读body sort
+- - AdminProductPpointController + PpointService: 读body sort透传
+- - PpointConfigAdminController + MealRecommendService: 适配签名变更
+- 前端:
+- - 修复7个vue文件的+col_key+语法错误+错位header+重复import
+- - npm run build通过
+- 
+
+
+## v1.0.1008 (2026-08-12)
+
+### 新功能
+- 成长页布局重构 - 饮食入口+今日推荐食材+记录卡平铺+干预快捷网格
+
+
+## v1.0.1007 (2026-08-12)
+
+### Bug 修复
+- 修复排序批量 patch 产生的语法错误,恢复构建
+
+### 其他
+- - ArticleManage.vue: header 移到 viewCount 列,修 col_key,删重复 import,补 sortableColumns
+- - OrderManage.vue: header 移到 orderNo 列,修 col_key,补 sortableColumns
+- - ProductManage.vue: header 移到 name/salesCount 列,createdAt 修 col_key,补 sortableColumns
+- - Recipes.vue: header 移到 name 列,补 import SortMixin,修 col_key
+- - CouponManagement.vue: header 移到 name 列,修 275/389 语法,补 sortableColumns,加 loadList 别名
+- - ProductPpointManage.vue: 修 156 行 }、sort 语法
+- npm run build 通过
+- 
+
+
+## v1.0.1006 (2026-08-12)
+
+### 新功能
+- rebuild cfclub with essential project files
+
+
+## v1.0.1005 (2026-08-12)
+
+### Bug 修复
+- 关卡页与报告渲染组件 :key 改为方法调用,消除非h5平台编译告警
+- stable_token重试失败时回退常规token接口
+
+### 其他
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1004 (2026-08-12)
+
+### Bug 修复
+- 成长页推荐卡片 :key 改为方法调用,修复非h5平台编译告警
+- 成长页成长回看改名为成长记录,追踪标签移除调研
+
+
+## v1.0.1003 (2026-08-12)
+
+### Bug 修复
+- 报告上传改走 JD Cloud OSS,LangGraph 解析支持远程 URL
+
+### 其他
+- saveUploadFile 仍写本地磁盘,LangGraph 容器读不到文件(400 文件不存在)。
+- 修复:
+- 1. HealthReportController 注入 StorageService,uploadOnly/uploadReport/
+-    parsePreview 改用 storageService.storeFile → 文件上传到京东 OSS
+- 2. parseByDraftId/parsePreview 对远程 OSS URL 直接传 URL 给 LangGraph,
+-    本地路径才用 getFilePath(); 本地 Java fallback 用 getFilePath 下载
+- 3. 删除不再使用的 saveUploadFile/resolveUploadFilePath 私有方法
+- 4. LangGraph report_parse.py 的 file_path 支持 http(s) URL,自动下载到
+-    临时文件后解析(不依赖共享 volume 时序)
+- 
+
+
+## v1.0.1002 (2026-08-12)
+
+### 文档
+- 明确商品积分仅按现金实付部分计算
+- 家庭平台积分(CF值)体系设计文档
+
+### Bug 修复
+- 移除报告审核 Tab 及相关路由
+- 迁移227 product_skus.id 升级为 BIGINT
+
+### 其他
+- - 生产库 id/product_id 为 INT,实体为 Long,插入超出 INT 范围的 id 报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- 
+
+
+## v1.0.1001 (2026-08-12)
+
+### 新功能
+- 健康报告三合一 - 统一报告列表/审核/代传为单页带Tab
+
+### Bug 修复
+- RegionController 移除硬编码数据,改用 StreetService 读取 DB 全量省市区数据
+
+
+## v1.0.1000 (2026-08-12)
+
+### Bug 修复
+- 迁移226 products 添加 growth_category 列
+
+### 其他
+- - Product 实体已含字段,生产库缺列,商品列表分页查询报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- - schema.sql 同步补列
+- 
+
+
+## v1.0.999 (2026-08-12)
+
+### Bug 修复
+- 迁移225 articles 添加 growth_category 列
+
+### 其他
+- - Article 实体已含字段,生产库缺列,getFeatured 精选查询报错
+- - 生产 DB 已直连 ALTER(即时生效)
+- - schema.sql 同步补列 + 解决 unlock_gates/microbiome_article 冲突
+- 
+
+
+## v1.0.998 (2026-08-12)
+
+### Bug 修复
+- 成长页推荐只调用一次API,前端按growth_category分组显示
+
+
+## v1.0.997 (2026-08-12)
+
+### Bug 修复
+- 推荐接口移除 growth_category 过滤,直接取精选内容
+
+
+
 ## v1.0.996 (2026-08-12)
 
 ### 新功能

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1907 - 1907
cfc-web/src/api/admin.js


+ 803 - 803
cfc-web/src/router/index.js

@@ -1,804 +1,804 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-import Layout from '@/views/Layout.vue'
-import axios from 'axios'
-import { getEffectivePermissions, hasPermission } from '@/utils/permissions'
-
-Vue.use(VueRouter)
-
-const routes = [
-  {
-    path: '/login',
-    name: 'Login',
-    component: () => import('@/views/Login.vue')
-  },
-  {
-    path: '/403',
-    name: 'Forbidden',
-    component: { template: '<div style="text-align:center;padding:100px"><h1>403</h1><p>权限不足</p><router-link to="/">返回首页</router-link></div>' },
-    meta: { title: '权限不足' }
-  },
-  {
-    path: '/',
-    component: Layout,
-    redirect: '/dashboard',
-    children: [
-      {
-        path: 'dashboard',
-        name: 'Dashboard',
-        component: () => import('@/views/Dashboard.vue'),
-        meta: { title: '首页', perm: 'dashboard' }
-      },
-      {
-        path: 'teacher-dashboard',
-        name: 'TeacherDashboard',
-        component: () => import('@/views/TeacherDashboard.vue'),
-        meta: { title: '规划师首页', requiresTeacher: true, perm: 'dashboard' }
-      },
-      {
-        path: 'admin/dashboard',
-        name: 'AdminDashboard',
-        component: () => import('@/views/admin/Dashboard'),
-        meta: { title: '数据大屏', roles: ['admin'] }
-      },
-      // 家庭管理
-      {
-        path: 'families',
-        name: 'Families',
-        component: () => import('@/views/Families.vue'),
-        meta: { title: '家庭列表', perm: 'family:list' }
-      },
-      {
-        path: 'children',
-        name: 'Children',
-        component: () => import('@/views/Children.vue'),
-        meta: { title: '孩子管理', perm: 'family:children' }
-      },
-      {
-        path: 'points',
-        name: 'Points',
-        component: () => import('@/views/Points.vue'),
-        meta: { title: '积分管理', perm: 'family:points' }
-      },
-      {
-        path: 'points-log',
-        name: 'PointsLog',
-        component: () => import('@/views/admin/PointsLog.vue'),
-        meta: { title: '积分记录', perm: 'family:points' }
-      },
-      {
-        path: 'membership-center',
-        name: 'MembershipCenter',
-        component: () => import('@/views/admin/MembershipCenter.vue'),
-        meta: { title: '会员中心', perm: 'system:config' }
-      },
-      // 任务管理
-      {
-        path: 'tasks',
-        name: 'Tasks',
-        component: () => import('@/views/Tasks.vue'),
-        meta: { title: '任务列表', perm: 'task:list' }
-      },
-      {
-        path: 'task-templates',
-        name: 'TaskTemplates',
-        component: () => import('@/views/TaskTemplates.vue'),
-        meta: { title: '任务模板', perm: 'task:templates' }
-      },
-      {
-        path: 'growth-task',
-        name: 'GrowthTaskManagement',
-        component: () => import('@/views/admin/GrowthTaskManagement'),
-        meta: { title: '成长任务管理', perm: 'task:list' }
-      },
-      // 奖励管理
-      {
-        path: 'rewards',
-        name: 'Rewards',
-        component: () => import('@/views/Rewards.vue'),
-        meta: { title: '奖励列表', perm: 'family:rewards' }
-      },
-      {
-        path: 'wishes',
-        name: 'Wishes',
-        component: () => import('@/views/Wishes.vue'),
-        meta: { title: '心愿管理', perm: 'family:wishes' }
-      },
-      // 成长规划师管理
-      {
-        path: 'guide-packages',
-        name: 'GuidePackages',
-        component: () => import('@/views/GuidePackages.vue'),
-        meta: { title: '成长规划师任务模板', requiresTeacher: true, perm: 'biz:packages' }
-      },
-      {
-        path: 'guide-family-task',
-        name: 'GuideFamilyTask',
-        component: () => import('@/views/GuideFamilyTask.vue'),
-        meta: { title: '家庭任务管理', requiresTeacher: true, perm: 'service:families' }
-      },
-      {
-        path: 'teacher-consult',
-        name: 'TeacherConsult',
-        component: () => import('@/views/TeacherConsult.vue'),
-        meta: { title: '家长咨询', requiresTeacher: true, perm: 'assessment:consult' }
-      },
-
-      {
-        path: 'teacher-families',
-        name: 'TeacherFamilies',
-        component: () => import('@/views/teacher/TeacherFamilies.vue'),
-        meta: { title: '我的家庭', requiresTeacher: true, perm: 'service:families' }
-      },
-      {
-        path: 'teacher-team',
-        name: 'TeacherTeam',
-        component: () => import('@/views/teacher/TeacherTeam.vue'),
-        meta: { title: '我的团队', requiresTeacher: true, perm: 'service:team' }
-      },
-      {
-        path: 'teacher-packages',
-        name: 'TeacherPackages',
-        component: () => import('@/views/teacher/TeacherPackages.vue'),
-        meta: { title: '任务模板管理', requiresTeacher: true, perm: 'biz:packages' }
-      },
-      {
-        path: 'teacher-orders',
-        name: 'TeacherOrders',
-        component: () => import('@/views/teacher/TeacherOrders.vue'),
-        meta: { title: '订单与佣金', requiresTeacher: true, perm: 'biz:orders' }
-      },
-      // 系统管理
-      {
-        path: 'users',
-        name: 'Users',
-        component: () => import('@/views/Users.vue'),
-        meta: { title: '用户管理', perm: 'system:users' }
-      },
-      {
-        path: 'review-center',
-        name: 'ReviewCenter',
-        component: () => import('@/views/admin/ReviewCenter.vue'),
-        meta: { title: '审核中心', perm: 'audit' }
-      },
-      {
-        path: 'operation-logs',
-        name: 'OperationLogs',
-        component: () => import('@/views/OperationLogs.vue'),
-        meta: { title: '操作日志', perm: 'system:logs' }
-      },
-      // 服务管理(管理员)
-      {
-        path: 'service-role-applications',
-        name: 'ServiceRoleApplications',
-        component: () => import('@/views/service/ServiceRoleApplications.vue'),
-        meta: { title: '服务角色申请审核', perm: 'audit' }
-      },
-      {
-        path: 'teacher-messages',
-        name: 'TeacherMessages',
-        component: () => import('@/views/teacher/TeacherMessages.vue'),
-        meta: { title: '消息中心', requiresTeacher: true, perm: 'messages' }
-      },
-      {
-        path: 'teacher-assessment',
-        name: 'TeacherAssessment',
-        component: () => import('@/views/teacher/TeacherAssessment.vue'),
-        meta: { title: 'DAN测评管理', requiresTeacher: true, perm: 'assessment:dan' }
-      },
-      {
-        path: 'teacher-proxy-report',
-        name: 'TeacherProxyReport',
-        component: () => import('@/views/teacher/ProxyReport.vue'),
-        meta: { title: '代上传报告', requiresTeacher: true, perm: 'health:reports' }
-      },
-      {
-        path: 'assessment-admin',
-        name: 'AssessmentAdmin',
-        component: () => import('@/views/admin/Assessment.vue'),
-        meta: { title: '测评管理', perm: 'assessment:dan' }
-      },
-      {
-        path: 'assessment-assign',
-        name: 'AssessmentAssign',
-        component: () => import('@/views/admin/AssessmentAssign.vue'),
-        meta: { title: '待分配规划师', perm: 'assessment:dan' }
-      },
-      {
-        path: 'assessment-orders',
-        name: 'AssessmentOrders',
-        component: () => import('@/views/admin/AssessmentOrders.vue'),
-        meta: { title: '测评订单', perm: 'assessment:dan' }
-      },
-      {
-        path: 'dan-execution',
-        name: 'DanExecution',
-        component: () => import('@/views/admin/DanExecution.vue'),
-        meta: { title: 'DAN服务执行', perm: 'assessment:dan' }
-      },
-      {
-        path: 'dan-settlement',
-        name: 'DanSettlement',
-        component: () => import('@/views/admin/DanSettlement.vue'),
-        meta: { title: 'DAN结算管理', perm: 'assessment:dan' }
-      },
-      {
-        path: 'product-manage',
-        name: 'ProductManage',
-        component: () => import('@/views/admin/ProductManage.vue'),
-        meta: { title: '商品管理', perm: 'commerce:products' }
-      },
-      {
-        path: 'inventory',
-        name: 'InventoryManage',
-        component: () => import('@/views/admin/InventoryManage.vue'),
-        meta: { title: '库存管理', perm: 'commerce:products' }
-      },
-      {
-        path: 'inventory/inbound-create',
-        name: 'InventoryInboundCreate',
-        component: () => import('@/views/admin/InventoryInboundCreate.vue'),
-        meta: { title: '创建入库单', perm: 'commerce:products' }
-      },
-      {
-        path: 'product-edit',
-        name: 'ProductEdit',
-        component: () => import('@/views/admin/ProductEdit.vue'),
-        meta: { title: '商品编辑', perm: 'commerce:products' }
-      },
-      {
-        path: 'assessment-products',
-        name: 'AssessmentProducts',
-        component: () => import('@/views/admin/AssessmentProducts.vue'),
-        meta: { title: '测评商品管理', perm: 'commerce:products' }
-      },
-      {
-        path: 'assessment-plan-rules',
-        name: 'AssessmentPlanRules',
-        component: () => import('@/views/admin/AssessmentPlanRules.vue'),
-        meta: { title: '方案生成规则', perm: 'plan:rules' }
-      },
-      {
-        path: 'plan-management',
-        name: 'PlanManagement',
-        component: () => import('@/views/admin/PlanManagement.vue'),
-        meta: { title: '方案管理', perm: 'plan:manage' }
-      },
-      {
-        path: 'category-manage',
-        name: 'CategoryManage',
-        component: () => import('@/views/admin/CategoryManage.vue'),
-        meta: { title: '商品分类', perm: 'commerce:category' }
-      },
-      {
-        path: 'order-manage',
-        name: 'OrderManage',
-        component: () => import('@/views/admin/OrderManage.vue'),
-        meta: { title: '订单管理', perm: 'commerce:orders' }
-      },
-      {
-        path: 'pending-refund',
-        name: 'PendingRefund',
-        component: () => import('@/views/admin/PendingRefund.vue'),
-        meta: { title: '待退款管理', perm: 'commerce:orders' }
-      },
-      {
-        path: 'order-detail/:orderNo',
-        name: 'OrderDetail',
-        component: () => import('@/views/admin/OrderDetail.vue'),
-        meta: { title: '订单详情', perm: 'commerce:orders' }
-      },
-      {
-      path: 'sys-config',
-      name: 'SysConfig',
-      component: () => import('@/views/admin/SysConfig.vue'),
-      meta: { title: '系统配置', perm: 'system:config' }
-    },
-    {
-      path: 'energy-sandbox',
-      name: 'EnergySandbox',
-      component: () => import('@/views/admin/EnergySandbox.vue'),
-      meta: { title: '五维能量', perm: 'energy' }
-    },
-    {
-      path: 'energy-rule',
-      name: 'EnergyRuleManagement',
-      component: () => import('@/views/admin/EnergyRuleManagement.vue'),
-      meta: { title: '能量规则管理', perm: 'energy' }
-    },
-    {
-      path: 'energy-behavior-config',
-      name: 'EnergyBehaviorConfig',
-      component: () => import('@/views/admin/EnergyBehaviorConfig.vue'),
-      meta: { title: '能量行为配置', perm: 'energy' }
-    },
-    {
-      path: 'challenge-score-config',
-      name: 'ChallengeScoreConfig',
-      component: () => import('@/views/admin/ChallengeScoreConfig.vue'),
-      meta: { title: '挑战评分配置', perm: 'energy' }
-    },
-      // ========== 文章管理 ==========
-      {
-        path: 'article-categories',
-        name: 'ArticleCategory',
-        component: () => import('@/views/admin/ArticleCategory.vue'),
-        meta: { title: '文章分类', perm: 'articles:categories' }
-      },
-      {
-        path: 'article-manage',
-        name: 'ArticleManage',
-        component: () => import('@/views/admin/ArticleManage.vue'),
-        meta: { title: '文章管理', perm: 'articles:manage' }
-      },
-      {
-        path: 'article-edit',
-        name: 'ArticleEdit',
-        component: () => import('@/views/admin/ArticleEdit.vue'),
-        meta: { title: '文章编辑', perm: 'articles:manage' }
-      },
-      {
-        path: 'comment-review',
-        name: 'CommentReview',
-        component: () => import('@/views/admin/CommentReview.vue'),
-        meta: { title: '评论审核', perm: 'articles:manage' }
-      },
-      {
-        path: 'notice-manage',
-        name: 'NoticeManage',
-        component: () => import('@/views/admin/NoticeManage.vue'),
-        meta: { title: '公告管理', perm: 'admin' }
-      },
-      {
-        path: 'periodic-service-config',
-        name: 'PeriodicServiceConfig',
-        component: () => import('@/views/admin/PeriodicServiceConfig.vue'),
-        meta: { title: '周期性服务配置', perm: 'admin' }
-      },
-      {
-        path: 'knowledge-tags',
-        name: 'KnowledgeTag',
-        component: () => import('@/views/admin/KnowledgeTag.vue'),
-        meta: { title: '知识标签', perm: 'articles:categories' }
-      },
-      {
-        path: 'virtual-goods-config',
-        name: 'VirtualGoodsConfig',
-        component: () => import('@/views/admin/VirtualGoodsConfig.vue'),
-        meta: { title: '虚拟支付道具', perm: 'system:config' }
-      },
-      {
-        path: 'gates',
-        name: 'Gates',
-        component: () => import('@/views/admin/gates.vue'),
-        meta: { title: '关卡配置', perm: 'system:config' }
-      },
-      // ========== 佣金推荐系统 ==========
-      {
-        path: 'product-profit-rate',
-        name: 'ProductProfitRate',
-        component: () => import('@/views/admin/ProductProfitRate.vue'),
-        meta: { title: '产品利润率', perm: 'commerce:profit-rate' }
-      },
-      {
-        path: 'product-ppoint',
-        name: 'ProductPpointManage',
-        component: () => import('@/views/admin/ProductPpointManage.vue'),
-        meta: { title: 'P点配置', perm: 'commerce:ppoint' }
-      },
-      {
-        path: 'cf-value',
-        name: 'CfValueManage',
-        component: () => import('@/views/admin/CfValueManage.vue'),
-        meta: { title: 'CF值管理', perm: 'finance:cf-value' }
-      },
-      {
-        path: 'supply-system',
-        name: 'SupplySystemList',
-        component: () => import('@/views/admin/supply-system/List.vue'),
-        meta: { title: '供应商体系', perm: 'system:supply' }
-      },
-      {
-        path: 'supply-system/create',
-        name: 'SupplySystemCreate',
-        component: () => import('@/views/admin/supply-system/Form.vue'),
-        meta: { title: '新建体系', perm: 'system:supply' }
-      },
-      {
-        path: 'supply-system/:id',
-        name: 'SupplySystemDetail',
-        component: () => import('@/views/admin/supply-system/Detail.vue'),
-        meta: { title: '体系详情', perm: 'system:supply' },
-    props: true
-  },
-  {
-    path: 'supplier-products',
-    name: 'SupplierProducts',
-    component: () => import('@/views/admin/SupplierProductManage.vue'),
-    meta: { title: '商品管理', perm: 'commerce:product' }
-  },
-  {
-            path: 'supply-system/:id/edit',
-            name: 'SupplySystemEdit',
-            component: () => import('@/views/admin/supply-system/Form.vue'),
-            meta: { title: '编辑体系', perm: 'system:supply' },
-            props: true
-        },
-        {
-            path: 'supply-hierarchy/change-requests',
-            name: 'SupplyHierarchyChangeRequests',
-            component: () => import('@/views/admin/supply-hierarchy/ChangeRequest.vue'),
-            meta: { title: '更换上级审核', perm: 'system:supply' }
-        },
-      // ========== 供应商管理 (supplier_admin) ==========
-      {
-        path: 'supply-manage',
-        name: 'SupplyManage',
-        component: () => import('@/views/admin/supply-manage/List.vue'),
-        meta: { title: '供应商管理', perm: 'supply:manage' }
-      },
-      {
-        path: 'supply-manage/create',
-        name: 'SupplyManageCreate',
-        component: () => import('@/views/admin/supply-manage/Form.vue'),
-        meta: { title: '新增供应商', perm: 'supply:manage' }
-      },
-      {
-        path: 'supply-manage/:id',
-        name: 'SupplyManageDetail',
-        component: () => import('@/views/admin/supply-manage/Detail.vue'),
-        meta: { title: '供应商详情', perm: 'supply:manage' },
-        props: true
-      },
-      {
-        path: 'supply-manage/:id/edit',
-        name: 'SupplyManageEdit',
-        component: () => import('@/views/admin/supply-manage/Form.vue'),
-        meta: { title: '编辑供应商', perm: 'supply:manage' },
-        props: true
-      },
-      {
-        path: 'zodiac-configs',
-        name: 'ZodiacConfigs',
-        component: () => import('@/views/admin/ZodiacConfigs.vue'),
-        meta: { title: '星座配置', perm: 'system:config' }
-      },
-      {
-        path: 'bazi-configs',
-        name: 'BaziConfigs',
-        component: () => import('@/views/admin/BaziConfigs.vue'),
-        meta: { title: '八字配置', perm: 'system:config' }
-      },
-      {
-        path: 'blood-type-configs',
-        name: 'BloodTypeConfigs',
-        component: () => import('@/views/admin/BloodTypeConfigs.vue'),
-        meta: { title: '血型配置', perm: 'system:config' }
-      },
-
-      // ========== 饮食管理 (admin) ==========
-      {
-        path: 'foods',
-        name: 'Foods',
-        component: () => import('@/views/admin/Foods.vue'),
-        meta: { title: '食材管理', perm: 'diet:foods' }
-      },
-      {
-        path: 'recipes',
-        name: 'Recipes',
-        component: () => import('@/views/admin/Recipes.vue'),
-        meta: { title: '食谱管理', perm: 'diet:recipes' }
-      },
-      
-      {
-        path: 'my-families',
-        name: 'MyFamilies',
-        component: () => import('@/views/teacher/FamilyList.vue'),
-        meta: { title: '我的家庭', perm: 'service:family' }
-      },
-      {
-        path: 'growth-records',
-        name: 'GrowthRecords',
-        component: () => import('@/views/teacher/GrowthRecords.vue'),
-        meta: { title: '成长记录', perm: 'growth:records' }
-      },
-      {
-        path: 'growth-plans',
-        name: 'GrowthPlans',
-        component: () => import('@/views/teacher/GrowthPlans.vue'),
-        meta: { title: '成长计划', perm: 'growth:plans' }
-      },
-      {
-        path: 'health-reports',
-        name: 'HealthReports',
-        component: () => import('@/views/admin/UnifiedHealthReports.vue'),
-        meta: { title: '健康报告', perm: 'health:reports' }
-      },
-      {
-        path: 'health-indicators',
-        name: 'HealthIndicators',
-        component: () => import('@/views/nutritionist/HealthIndicators.vue'),
-        meta: { title: '健康指标', perm: 'health:indicators' }
-      },
-      {
-        path: 'health-checkins',
-        name: 'HealthCheckinManage',
-        component: () => import('@/views/admin/HealthCheckinManage.vue'),
-        meta: { title: '健康打卡管理', perm: 'health:checkins' }
-      },
-      {
-        path: 'emotion-alert',
-        name: 'EmotionAlertManage',
-        component: () => import('@/views/admin/EmotionAlertManage.vue'),
-        meta: { title: '情绪告警管理', perm: 'health:checkins' }
-      },
-      {
-        path: 'nutrition-mappings',
-        name: 'NutritionMappings',
-        component: () => import('@/views/nutritionist/NutritionMappings.vue'),
-        meta: { title: '营养素映射管理', perm: 'health:mappings' }
-      },
-      {
-        path: 'activities',
-        name: 'Activities',
-        component: () => import('@/views/admin/Activities.vue'),
-        meta: { title: '活动列表', perm: 'activity:list' }
-      },
-        {
-          path: 'activity-edit',
-          name: 'ActivityEdit',
-          component: () => import('@/views/admin/ActivityEdit.vue'),
-          meta: { title: '活动编辑', perm: 'activity:list' }
-        },
-        {
-          path: 'survey-templates',
-          name: 'SurveyTemplates',
-          component: () => import('@/views/admin/SurveyTemplates.vue'),
-          meta: { title: '调研模板管理', perm: 'admin:all' }
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import Layout from '@/views/Layout.vue'
+import axios from 'axios'
+import { getEffectivePermissions, hasPermission } from '@/utils/permissions'
+
+Vue.use(VueRouter)
+
+const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue')
+  },
+  {
+    path: '/403',
+    name: 'Forbidden',
+    component: { template: '<div style="text-align:center;padding:100px"><h1>403</h1><p>权限不足</p><router-link to="/">返回首页</router-link></div>' },
+    meta: { title: '权限不足' }
+  },
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/dashboard',
+    children: [
+      {
+        path: 'dashboard',
+        name: 'Dashboard',
+        component: () => import('@/views/Dashboard.vue'),
+        meta: { title: '首页', perm: 'dashboard' }
+      },
+      {
+        path: 'teacher-dashboard',
+        name: 'TeacherDashboard',
+        component: () => import('@/views/TeacherDashboard.vue'),
+        meta: { title: '规划师首页', requiresTeacher: true, perm: 'dashboard' }
+      },
+      {
+        path: 'admin/dashboard',
+        name: 'AdminDashboard',
+        component: () => import('@/views/admin/Dashboard'),
+        meta: { title: '数据大屏', roles: ['admin'] }
+      },
+      // 家庭管理
+      {
+        path: 'families',
+        name: 'Families',
+        component: () => import('@/views/Families.vue'),
+        meta: { title: '家庭列表', perm: 'family:list' }
+      },
+      {
+        path: 'children',
+        name: 'Children',
+        component: () => import('@/views/Children.vue'),
+        meta: { title: '孩子管理', perm: 'family:children' }
+      },
+      {
+        path: 'points',
+        name: 'Points',
+        component: () => import('@/views/Points.vue'),
+        meta: { title: '积分管理', perm: 'family:points' }
+      },
+      {
+        path: 'points-log',
+        name: 'PointsLog',
+        component: () => import('@/views/admin/PointsLog.vue'),
+        meta: { title: '积分记录', perm: 'family:points' }
+      },
+      {
+        path: 'membership-center',
+        name: 'MembershipCenter',
+        component: () => import('@/views/admin/MembershipCenter.vue'),
+        meta: { title: '会员中心', perm: 'system:config' }
+      },
+      // 任务管理
+      {
+        path: 'tasks',
+        name: 'Tasks',
+        component: () => import('@/views/Tasks.vue'),
+        meta: { title: '任务列表', perm: 'task:list' }
+      },
+      {
+        path: 'task-templates',
+        name: 'TaskTemplates',
+        component: () => import('@/views/TaskTemplates.vue'),
+        meta: { title: '任务模板', perm: 'task:templates' }
+      },
+      {
+        path: 'growth-task',
+        name: 'GrowthTaskManagement',
+        component: () => import('@/views/admin/GrowthTaskManagement'),
+        meta: { title: '成长任务管理', perm: 'task:list' }
+      },
+      // 奖励管理
+      {
+        path: 'rewards',
+        name: 'Rewards',
+        component: () => import('@/views/Rewards.vue'),
+        meta: { title: '奖励列表', perm: 'family:rewards' }
+      },
+      {
+        path: 'wishes',
+        name: 'Wishes',
+        component: () => import('@/views/Wishes.vue'),
+        meta: { title: '心愿管理', perm: 'family:wishes' }
+      },
+      // 成长规划师管理
+      {
+        path: 'guide-packages',
+        name: 'GuidePackages',
+        component: () => import('@/views/GuidePackages.vue'),
+        meta: { title: '成长规划师任务模板', requiresTeacher: true, perm: 'biz:packages' }
+      },
+      {
+        path: 'guide-family-task',
+        name: 'GuideFamilyTask',
+        component: () => import('@/views/GuideFamilyTask.vue'),
+        meta: { title: '家庭任务管理', requiresTeacher: true, perm: 'service:families' }
+      },
+      {
+        path: 'teacher-consult',
+        name: 'TeacherConsult',
+        component: () => import('@/views/TeacherConsult.vue'),
+        meta: { title: '家长咨询', requiresTeacher: true, perm: 'assessment:consult' }
+      },
+
+      {
+        path: 'teacher-families',
+        name: 'TeacherFamilies',
+        component: () => import('@/views/teacher/TeacherFamilies.vue'),
+        meta: { title: '我的家庭', requiresTeacher: true, perm: 'service:families' }
+      },
+      {
+        path: 'teacher-team',
+        name: 'TeacherTeam',
+        component: () => import('@/views/teacher/TeacherTeam.vue'),
+        meta: { title: '我的团队', requiresTeacher: true, perm: 'service:team' }
+      },
+      {
+        path: 'teacher-packages',
+        name: 'TeacherPackages',
+        component: () => import('@/views/teacher/TeacherPackages.vue'),
+        meta: { title: '任务模板管理', requiresTeacher: true, perm: 'biz:packages' }
+      },
+      {
+        path: 'teacher-orders',
+        name: 'TeacherOrders',
+        component: () => import('@/views/teacher/TeacherOrders.vue'),
+        meta: { title: '订单与佣金', requiresTeacher: true, perm: 'biz:orders' }
+      },
+      // 系统管理
+      {
+        path: 'users',
+        name: 'Users',
+        component: () => import('@/views/Users.vue'),
+        meta: { title: '用户管理', perm: 'system:users' }
+      },
+      {
+        path: 'review-center',
+        name: 'ReviewCenter',
+        component: () => import('@/views/admin/ReviewCenter.vue'),
+        meta: { title: '审核中心', perm: 'audit' }
+      },
+      {
+        path: 'operation-logs',
+        name: 'OperationLogs',
+        component: () => import('@/views/OperationLogs.vue'),
+        meta: { title: '操作日志', perm: 'system:logs' }
+      },
+      // 服务管理(管理员)
+      {
+        path: 'service-role-applications',
+        name: 'ServiceRoleApplications',
+        component: () => import('@/views/service/ServiceRoleApplications.vue'),
+        meta: { title: '服务角色申请审核', perm: 'audit' }
+      },
+      {
+        path: 'teacher-messages',
+        name: 'TeacherMessages',
+        component: () => import('@/views/teacher/TeacherMessages.vue'),
+        meta: { title: '消息中心', requiresTeacher: true, perm: 'messages' }
+      },
+      {
+        path: 'teacher-assessment',
+        name: 'TeacherAssessment',
+        component: () => import('@/views/teacher/TeacherAssessment.vue'),
+        meta: { title: 'DAN测评管理', requiresTeacher: true, perm: 'assessment:dan' }
+      },
+      {
+        path: 'teacher-proxy-report',
+        name: 'TeacherProxyReport',
+        component: () => import('@/views/teacher/ProxyReport.vue'),
+        meta: { title: '代上传报告', requiresTeacher: true, perm: 'health:reports' }
+      },
+      {
+        path: 'assessment-admin',
+        name: 'AssessmentAdmin',
+        component: () => import('@/views/admin/Assessment.vue'),
+        meta: { title: '测评管理', perm: 'assessment:dan' }
+      },
+      {
+        path: 'assessment-assign',
+        name: 'AssessmentAssign',
+        component: () => import('@/views/admin/AssessmentAssign.vue'),
+        meta: { title: '待分配规划师', perm: 'assessment:dan' }
+      },
+      {
+        path: 'assessment-orders',
+        name: 'AssessmentOrders',
+        component: () => import('@/views/admin/AssessmentOrders.vue'),
+        meta: { title: '测评订单', perm: 'assessment:dan' }
+      },
+      {
+        path: 'dan-execution',
+        name: 'DanExecution',
+        component: () => import('@/views/admin/DanExecution.vue'),
+        meta: { title: 'DAN服务执行', perm: 'assessment:dan' }
+      },
+      {
+        path: 'dan-settlement',
+        name: 'DanSettlement',
+        component: () => import('@/views/admin/DanSettlement.vue'),
+        meta: { title: 'DAN结算管理', perm: 'assessment:dan' }
+      },
+      {
+        path: 'product-manage',
+        name: 'ProductManage',
+        component: () => import('@/views/admin/ProductManage.vue'),
+        meta: { title: '商品管理', perm: 'commerce:products' }
+      },
+      {
+        path: 'inventory',
+        name: 'InventoryManage',
+        component: () => import('@/views/admin/InventoryManage.vue'),
+        meta: { title: '库存管理', perm: 'commerce:products' }
+      },
+      {
+        path: 'inventory/inbound-create',
+        name: 'InventoryInboundCreate',
+        component: () => import('@/views/admin/InventoryInboundCreate.vue'),
+        meta: { title: '创建入库单', perm: 'commerce:products' }
+      },
+      {
+        path: 'product-edit',
+        name: 'ProductEdit',
+        component: () => import('@/views/admin/ProductEdit.vue'),
+        meta: { title: '商品编辑', perm: 'commerce:products' }
+      },
+      {
+        path: 'assessment-products',
+        name: 'AssessmentProducts',
+        component: () => import('@/views/admin/AssessmentProducts.vue'),
+        meta: { title: '测评商品管理', perm: 'commerce:products' }
+      },
+      {
+        path: 'assessment-plan-rules',
+        name: 'AssessmentPlanRules',
+        component: () => import('@/views/admin/AssessmentPlanRules.vue'),
+        meta: { title: '方案生成规则', perm: 'plan:rules' }
+      },
+      {
+        path: 'plan-management',
+        name: 'PlanManagement',
+        component: () => import('@/views/admin/PlanManagement.vue'),
+        meta: { title: '方案管理', perm: 'plan:manage' }
+      },
+      {
+        path: 'category-manage',
+        name: 'CategoryManage',
+        component: () => import('@/views/admin/CategoryManage.vue'),
+        meta: { title: '商品分类', perm: 'commerce:category' }
+      },
+      {
+        path: 'order-manage',
+        name: 'OrderManage',
+        component: () => import('@/views/admin/OrderManage.vue'),
+        meta: { title: '订单管理', perm: 'commerce:orders' }
+      },
+      {
+        path: 'pending-refund',
+        name: 'PendingRefund',
+        component: () => import('@/views/admin/PendingRefund.vue'),
+        meta: { title: '待退款管理', perm: 'commerce:orders' }
+      },
+      {
+        path: 'order-detail/:orderNo',
+        name: 'OrderDetail',
+        component: () => import('@/views/admin/OrderDetail.vue'),
+        meta: { title: '订单详情', perm: 'commerce:orders' }
+      },
+      {
+      path: 'sys-config',
+      name: 'SysConfig',
+      component: () => import('@/views/admin/SysConfig.vue'),
+      meta: { title: '系统配置', perm: 'system:config' }
+    },
+    {
+      path: 'energy-sandbox',
+      name: 'EnergySandbox',
+      component: () => import('@/views/admin/EnergySandbox.vue'),
+      meta: { title: '五维能量', perm: 'energy' }
+    },
+    {
+      path: 'energy-rule',
+      name: 'EnergyRuleManagement',
+      component: () => import('@/views/admin/EnergyRuleManagement.vue'),
+      meta: { title: '能量规则管理', perm: 'energy' }
+    },
+    {
+      path: 'energy-behavior-config',
+      name: 'EnergyBehaviorConfig',
+      component: () => import('@/views/admin/EnergyBehaviorConfig.vue'),
+      meta: { title: '能量行为配置', perm: 'energy' }
+    },
+    {
+      path: 'challenge-score-config',
+      name: 'ChallengeScoreConfig',
+      component: () => import('@/views/admin/ChallengeScoreConfig.vue'),
+      meta: { title: '挑战评分配置', perm: 'energy' }
+    },
+      // ========== 文章管理 ==========
+      {
+        path: 'article-categories',
+        name: 'ArticleCategory',
+        component: () => import('@/views/admin/ArticleCategory.vue'),
+        meta: { title: '文章分类', perm: 'articles:categories' }
+      },
+      {
+        path: 'article-manage',
+        name: 'ArticleManage',
+        component: () => import('@/views/admin/ArticleManage.vue'),
+        meta: { title: '文章管理', perm: 'articles:manage' }
+      },
+      {
+        path: 'article-edit',
+        name: 'ArticleEdit',
+        component: () => import('@/views/admin/ArticleEdit.vue'),
+        meta: { title: '文章编辑', perm: 'articles:manage' }
+      },
+      {
+        path: 'comment-review',
+        name: 'CommentReview',
+        component: () => import('@/views/admin/CommentReview.vue'),
+        meta: { title: '评论审核', perm: 'articles:manage' }
+      },
+      {
+        path: 'notice-manage',
+        name: 'NoticeManage',
+        component: () => import('@/views/admin/NoticeManage.vue'),
+        meta: { title: '公告管理', perm: 'admin' }
+      },
+      {
+        path: 'periodic-service-config',
+        name: 'PeriodicServiceConfig',
+        component: () => import('@/views/admin/PeriodicServiceConfig.vue'),
+        meta: { title: '周期性服务配置', perm: 'admin' }
+      },
+      {
+        path: 'knowledge-tags',
+        name: 'KnowledgeTag',
+        component: () => import('@/views/admin/KnowledgeTag.vue'),
+        meta: { title: '知识标签', perm: 'articles:categories' }
+      },
+      {
+        path: 'virtual-goods-config',
+        name: 'VirtualGoodsConfig',
+        component: () => import('@/views/admin/VirtualGoodsConfig.vue'),
+        meta: { title: '虚拟支付道具', perm: 'system:config' }
+      },
+      {
+        path: 'gates',
+        name: 'Gates',
+        component: () => import('@/views/admin/gates.vue'),
+        meta: { title: '关卡配置', perm: 'system:config' }
+      },
+      // ========== 佣金推荐系统 ==========
+      {
+        path: 'product-profit-rate',
+        name: 'ProductProfitRate',
+        component: () => import('@/views/admin/ProductProfitRate.vue'),
+        meta: { title: '产品利润率', perm: 'commerce:profit-rate' }
+      },
+      {
+        path: 'product-ppoint',
+        name: 'ProductPpointManage',
+        component: () => import('@/views/admin/ProductPpointManage.vue'),
+        meta: { title: 'P点配置', perm: 'commerce:ppoint' }
+      },
+      {
+        path: 'cf-value',
+        name: 'CfValueManage',
+        component: () => import('@/views/admin/CfValueManage.vue'),
+        meta: { title: 'CF值管理', perm: 'finance:cf-value' }
+      },
+      {
+        path: 'supply-system',
+        name: 'SupplySystemList',
+        component: () => import('@/views/admin/supply-system/List.vue'),
+        meta: { title: '供应商体系', perm: 'system:supply' }
+      },
+      {
+        path: 'supply-system/create',
+        name: 'SupplySystemCreate',
+        component: () => import('@/views/admin/supply-system/Form.vue'),
+        meta: { title: '新建体系', perm: 'system:supply' }
+      },
+      {
+        path: 'supply-system/:id',
+        name: 'SupplySystemDetail',
+        component: () => import('@/views/admin/supply-system/Detail.vue'),
+        meta: { title: '体系详情', perm: 'system:supply' },
+    props: true
+  },
+  {
+    path: 'supplier-products',
+    name: 'SupplierProducts',
+    component: () => import('@/views/admin/SupplierProductManage.vue'),
+    meta: { title: '商品管理', perm: 'commerce:product' }
+  },
+  {
+            path: 'supply-system/:id/edit',
+            name: 'SupplySystemEdit',
+            component: () => import('@/views/admin/supply-system/Form.vue'),
+            meta: { title: '编辑体系', perm: 'system:supply' },
+            props: true
         },
-      {
-        path: 'activity-review',
-        name: 'ActivityReview',
-        component: () => import('@/views/admin/ActivityReview.vue'),
-        meta: { title: '活动审核', perm: 'activity:review' }
-      },
-      {
-        path: 'activity-registration-review',
-        name: 'ActivityRegistrationReview',
-        component: () => import('@/views/admin/ActivityRegistrationReview.vue'),
-        meta: { title: '活动报名审核', perm: 'activity:review' }
-      },
-      // ========== 维度配置 + 知识库管理 ==========
-      {
-        path: 'coupon',
-        name: 'CouponManagement',
-        component: () => import('@/views/admin/CouponManagement'),
-        meta: { title: '优惠券管理', perm: 'marketing:coupon' }
-      },
-      {
-        path: 'coupon-grant-log',
-        name: 'CouponGrantLog',
-        component: () => import('@/views/admin/CouponGrantLog.vue'),
-        meta: { title: '发券记录', perm: 'marketing:coupon' }
-      },
-      {
-        path: 'promotion',
-        name: 'PromotionManagement',
-        component: () => import('@/views/admin/PromotionManagement'),
-        meta: { title: '推广管理', perm: 'marketing:promotion' }
-      },
-      {
-        path: 'family-earnings',
-        name: 'FamilyEarnings',
-        component: () => import('@/views/admin/FamilyEarnings.vue'),
-        meta: { title: '家庭收益管理', perm: 'marketing:promotion' }
-      },
-      {
-        path: 'family-earnings-withdraw',
-        name: 'FamilyEarningsWithdraw',
-        component: () => import('@/views/admin/FamilyEarningsWithdraw.vue'),
-        meta: { title: '家庭收益提现审核', perm: 'audit:withdraw' }
-      },
-      {
-        path: 'dimension-config',
-        name: 'DimensionConfig',
-        component: () => import('@/views/admin/dimension'),
-        meta: { title: '维度配置', perm: 'system:config' }
-      },
-      {
-        path: 'knowledge-base',
-        name: 'KnowledgeBase',
-        component: () => import('@/views/admin/knowledge'),
-        meta: { title: '知识库管理', perm: 'system:config' }
-      },
-      {
-        path: 'health-knowledge',
-        name: 'HealthKnowledge',
-        component: () => import('@/views/admin/health-knowledge'),
-        meta: { title: '健康知识库管理', perm: 'system:config' }
-      },
-      // ========== 健康维度配置 ==========
-      {
-        path: 'health-norm-config',
-        name: 'HealthNormConfig',
-        component: () => import('@/views/admin/dimension-config'),
-        meta: { title: '健康常模配置', perm: 'system:config' }
-      },
-      {
-        path: 'health-data-source',
-        name: 'HealthDataSource',
-        component: () => import('@/views/admin/data-source-config'),
-        meta: { title: '健康数据源配置', perm: 'system:config' }
-      },
-      {
-        path: 'health-energy-config',
-        name: 'HealthEnergyConfig',
-        component: () => import('@/views/admin/HealthEnergyConfig'),
-        meta: { title: '七维能量配置', perm: 'system:config' }
-      },
-      {
-        path: 'energy-config',
-        name: 'EnergyConfig',
-        component: () => import('@/views/admin/energy-config/WuxingConfig'),
-        meta: { title: '五行能量配置', perm: 'system:config' }
-      },
-      {
-        path: 'indicators',
-        name: 'IndicatorManage',
-        component: () => import('@/views/admin/indicators'),
-        meta: { title: '指标管理', perm: 'system:config' }
-      },
-      // ========== 虚拟服务商团队管理 ==========
-      {
-        path: 'virtual-teams',
-        name: 'VirtualTeamList',
-        component: () => import('@/views/admin/VirtualTeamList'),
-        meta: { title: '虚拟团队管理', perm: 'system:config' }
-      },
-      {
-        path: 'virtual-team-detail/:systemId',
-        name: 'VirtualTeamDetail',
-        component: () => import('@/views/admin/VirtualTeamDetail'),
-        meta: { title: '团队成员管理', perm: 'system:config' },
-        props: true
-      },
-      // ========== 电商供应商管理 ==========
-      {
-        path: 'ecom-supplier',
-        name: 'EcomSupplierManage',
-        component: () => import('@/views/admin/EcomSupplierManage'),
-        meta: { title: '电商供应商管理', perm: 'ecom:supplier' }
-      },
-      {
-        path: 'badge-manage',
-        name: 'BadgeManage',
-        component: () => import('@/views/admin/BadgeManage'),
-        meta: { title: '勋章管理', perm: 'system:config' }
-      },
-      // ========== 报告指纹解析系统 ==========
-      {
-        path: 'report-types',
-        name: 'ReportTypeManagement',
-        component: () => import('@/views/admin/ReportTypeManagement.vue'),
-        meta: { title: '报告类型管理', perm: 'system:config' }
-      },
-      {
-        path: 'report-parser-import',
-        name: 'ReportParserImport',
-        component: () => import('@/views/admin/ReportParserImport.vue'),
-        meta: { title: '解析器导入管理', perm: 'system:config' }
-      },
-      {
-        path: 'report-unknown-clusters',
-        name: 'ReportUnknownCluster',
-        component: () => import('@/views/admin/ReportUnknownCluster.vue'),
-        meta: { title: '未知报告审核', perm: 'system:config' }
-      },
-      {
-        path: 'report-auto-learn',
-        name: 'ReportAutoLearn',
-        component: () => import('@/views/admin/ReportAutoLearn.vue'),
-        meta: { title: '报告自学习', perm: 'system:config' }
-      },
-      // ========== LangGraph AI 服务管理 ==========
-      {
-        path: 'langgraph-admin',
-        name: 'LangGraphAdmin',
-        component: () => import('@/views/admin/LangGraphAdmin.vue'),
-        meta: { title: 'LangGraph 管理', perm: 'system:config' }
-      },
-      // ========== 营养产品管理 ==========
-      {
-        path: 'nutrition-products',
-        name: 'NutritionProducts',
-        component: () => import('@/views/admin/NutritionProducts.vue'),
-        meta: { title: '营养产品管理', perm: 'health:*' }
-      }
-    ]
-  }
-]
-
-const router = new VueRouter({
-  mode: 'history',
-  base: process.env.BASE_URL,
-  routes
-})
-
-// Token 验证缓存:避免 redirect 链中重复调用 /api/admin-auth/info
-let tokenValidationCache = { token: '', timestamp: 0 }
-const TOKEN_CACHE_TTL = 30000 // 30秒内不重复验证
-
-async function validateToken(token) {
-  try {
-    const baseURL = process.env.VUE_APP_BASE_API || ''
-    const res = await axios.post(`${baseURL}/api/admin-auth/info`, {}, {
-      headers: { Authorization: `Bearer ${token}` },
-      validateStatus: () => true
-    })
-    return res.status === 200 && res.data?.code === 200
-  } catch (e) {
-    return false
-  }
-}
-
-router.beforeEach(async (to, from, next) => {
-  const token = localStorage.getItem('token')
-  const role = localStorage.getItem('role')
-
-  // 1. 登录页允许访问
-  if (to.path === '/login') {
-    // 如果已有token和role,直接跳转到对应首页
-    if (token && role === 'teacher') {
-      return next('/teacher-dashboard')
-    }
-    return next()
-  }
-
-  // 2. 无Token强制跳转
-  if (!token) {
-    return next('/login')
-  }
-
-  // 3. Token有效性验证 (使用缓存避免 redirect 链重复调用)
-  const now = Date.now()
-  let isValid = false
-  if (tokenValidationCache.token === token && (now - tokenValidationCache.timestamp) < TOKEN_CACHE_TTL) {
-    isValid = true
-  } else {
-    isValid = await validateToken(token)
-    if (isValid) {
-      tokenValidationCache = { token, timestamp: now }
-    }
-  }
-  if (!isValid) {
-    tokenValidationCache = { token: '', timestamp: 0 }
-    localStorage.clear()
-    return next('/login')
-  }
-
-  // 3.5 规划师访问 /dashboard → 跳转规划师仪表板
-  if (role === 'teacher' && to.path === '/dashboard') {
-    return next('/teacher-dashboard')
-  }
-
-  // 4. 权限检查:如果路由有 perm 要求,检查用户权限
-  const requiredPerm = to.meta && to.meta.perm
-  if (requiredPerm) {
-    const rolesStr = localStorage.getItem('roles')
-    const roles = rolesStr ? JSON.parse(rolesStr) : [localStorage.getItem('role')]
-    const perms = getEffectivePermissions(roles)
-    if (!hasPermission(perms, requiredPerm)) {
-      console.warn('Access denied: insufficient permissions for', to.name)
-      return next({ path: '/403', replace: true })
-    }
-  }
-  return next()
-})
-
-// Suppress navigation guard redirect errors (teacher/admin route cross-access)
-const originalPush = VueRouter.prototype.push
-VueRouter.prototype.push = function push(location) {
-  return originalPush.call(this, location).catch(err => err)
-}
-
-export default router
+        {
+            path: 'supply-hierarchy/change-requests',
+            name: 'SupplyHierarchyChangeRequests',
+            component: () => import('@/views/admin/supply-hierarchy/ChangeRequest.vue'),
+            meta: { title: '更换上级审核', perm: 'system:supply' }
+        },
+      // ========== 供应商管理 (supplier_admin) ==========
+      {
+        path: 'supply-manage',
+        name: 'SupplyManage',
+        component: () => import('@/views/admin/supply-manage/List.vue'),
+        meta: { title: '供应商管理', perm: 'supply:manage' }
+      },
+      {
+        path: 'supply-manage/create',
+        name: 'SupplyManageCreate',
+        component: () => import('@/views/admin/supply-manage/Form.vue'),
+        meta: { title: '新增供应商', perm: 'supply:manage' }
+      },
+      {
+        path: 'supply-manage/:id',
+        name: 'SupplyManageDetail',
+        component: () => import('@/views/admin/supply-manage/Detail.vue'),
+        meta: { title: '供应商详情', perm: 'supply:manage' },
+        props: true
+      },
+      {
+        path: 'supply-manage/:id/edit',
+        name: 'SupplyManageEdit',
+        component: () => import('@/views/admin/supply-manage/Form.vue'),
+        meta: { title: '编辑供应商', perm: 'supply:manage' },
+        props: true
+      },
+      {
+        path: 'zodiac-configs',
+        name: 'ZodiacConfigs',
+        component: () => import('@/views/admin/ZodiacConfigs.vue'),
+        meta: { title: '星座配置', perm: 'system:config' }
+      },
+      {
+        path: 'bazi-configs',
+        name: 'BaziConfigs',
+        component: () => import('@/views/admin/BaziConfigs.vue'),
+        meta: { title: '八字配置', perm: 'system:config' }
+      },
+      {
+        path: 'blood-type-configs',
+        name: 'BloodTypeConfigs',
+        component: () => import('@/views/admin/BloodTypeConfigs.vue'),
+        meta: { title: '血型配置', perm: 'system:config' }
+      },
+
+      // ========== 饮食管理 (admin) ==========
+      {
+        path: 'foods',
+        name: 'Foods',
+        component: () => import('@/views/admin/Foods.vue'),
+        meta: { title: '食材管理', perm: 'diet:foods' }
+      },
+      {
+        path: 'recipes',
+        name: 'Recipes',
+        component: () => import('@/views/admin/Recipes.vue'),
+        meta: { title: '食谱管理', perm: 'diet:recipes' }
+      },
+      
+      {
+        path: 'my-families',
+        name: 'MyFamilies',
+        component: () => import('@/views/teacher/FamilyList.vue'),
+        meta: { title: '我的家庭', perm: 'service:family' }
+      },
+      {
+        path: 'growth-records',
+        name: 'GrowthRecords',
+        component: () => import('@/views/teacher/GrowthRecords.vue'),
+        meta: { title: '成长记录', perm: 'growth:records' }
+      },
+      {
+        path: 'growth-plans',
+        name: 'GrowthPlans',
+        component: () => import('@/views/teacher/GrowthPlans.vue'),
+        meta: { title: '成长计划', perm: 'growth:plans' }
+      },
+      {
+        path: 'health-reports',
+        name: 'HealthReports',
+        component: () => import('@/views/admin/UnifiedHealthReports.vue'),
+        meta: { title: '健康报告', perm: 'health:reports' }
+      },
+      {
+        path: 'health-indicators',
+        name: 'HealthIndicators',
+        component: () => import('@/views/nutritionist/HealthIndicators.vue'),
+        meta: { title: '健康指标', perm: 'health:indicators' }
+      },
+      {
+        path: 'health-checkins',
+        name: 'HealthCheckinManage',
+        component: () => import('@/views/admin/HealthCheckinManage.vue'),
+        meta: { title: '健康打卡管理', perm: 'health:checkins' }
+      },
+      {
+        path: 'emotion-alert',
+        name: 'EmotionAlertManage',
+        component: () => import('@/views/admin/EmotionAlertManage.vue'),
+        meta: { title: '情绪告警管理', perm: 'health:checkins' }
+      },
+      {
+        path: 'nutrition-mappings',
+        name: 'NutritionMappings',
+        component: () => import('@/views/nutritionist/NutritionMappings.vue'),
+        meta: { title: '营养素映射管理', perm: 'health:mappings' }
+      },
+      {
+        path: 'activities',
+        name: 'Activities',
+        component: () => import('@/views/admin/Activities.vue'),
+        meta: { title: '活动列表', perm: 'activity:list' }
+      },
+        {
+          path: 'activity-edit',
+          name: 'ActivityEdit',
+          component: () => import('@/views/admin/ActivityEdit.vue'),
+          meta: { title: '活动编辑', perm: 'activity:list' }
+        },
+        {
+          path: 'survey-templates',
+          name: 'SurveyTemplates',
+          component: () => import('@/views/admin/SurveyTemplates.vue'),
+          meta: { title: '调研模板管理', perm: 'admin:all' }
+        },
+      {
+        path: 'activity-review',
+        name: 'ActivityReview',
+        component: () => import('@/views/admin/ActivityReview.vue'),
+        meta: { title: '活动审核', perm: 'activity:review' }
+      },
+      {
+        path: 'activity-registration-review',
+        name: 'ActivityRegistrationReview',
+        component: () => import('@/views/admin/ActivityRegistrationReview.vue'),
+        meta: { title: '活动报名审核', perm: 'activity:review' }
+      },
+      // ========== 维度配置 + 知识库管理 ==========
+      {
+        path: 'coupon',
+        name: 'CouponManagement',
+        component: () => import('@/views/admin/CouponManagement'),
+        meta: { title: '优惠券管理', perm: 'marketing:coupon' }
+      },
+      {
+        path: 'coupon-grant-log',
+        name: 'CouponGrantLog',
+        component: () => import('@/views/admin/CouponGrantLog.vue'),
+        meta: { title: '发券记录', perm: 'marketing:coupon' }
+      },
+      {
+        path: 'promotion',
+        name: 'PromotionManagement',
+        component: () => import('@/views/admin/PromotionManagement'),
+        meta: { title: '推广管理', perm: 'marketing:promotion' }
+      },
+      {
+        path: 'family-earnings',
+        name: 'FamilyEarnings',
+        component: () => import('@/views/admin/FamilyEarnings.vue'),
+        meta: { title: '家庭收益管理', perm: 'marketing:promotion' }
+      },
+      {
+        path: 'family-earnings-withdraw',
+        name: 'FamilyEarningsWithdraw',
+        component: () => import('@/views/admin/FamilyEarningsWithdraw.vue'),
+        meta: { title: '家庭收益提现审核', perm: 'audit:withdraw' }
+      },
+      {
+        path: 'dimension-config',
+        name: 'DimensionConfig',
+        component: () => import('@/views/admin/dimension'),
+        meta: { title: '维度配置', perm: 'system:config' }
+      },
+      {
+        path: 'knowledge-base',
+        name: 'KnowledgeBase',
+        component: () => import('@/views/admin/knowledge'),
+        meta: { title: '知识库管理', perm: 'system:config' }
+      },
+      {
+        path: 'health-knowledge',
+        name: 'HealthKnowledge',
+        component: () => import('@/views/admin/health-knowledge'),
+        meta: { title: '健康知识库管理', perm: 'system:config' }
+      },
+      // ========== 健康维度配置 ==========
+      {
+        path: 'health-norm-config',
+        name: 'HealthNormConfig',
+        component: () => import('@/views/admin/dimension-config'),
+        meta: { title: '健康常模配置', perm: 'system:config' }
+      },
+      {
+        path: 'health-data-source',
+        name: 'HealthDataSource',
+        component: () => import('@/views/admin/data-source-config'),
+        meta: { title: '健康数据源配置', perm: 'system:config' }
+      },
+      {
+        path: 'health-energy-config',
+        name: 'HealthEnergyConfig',
+        component: () => import('@/views/admin/HealthEnergyConfig'),
+        meta: { title: '七维能量配置', perm: 'system:config' }
+      },
+      {
+        path: 'energy-config',
+        name: 'EnergyConfig',
+        component: () => import('@/views/admin/energy-config/WuxingConfig'),
+        meta: { title: '五行能量配置', perm: 'system:config' }
+      },
+      {
+        path: 'indicators',
+        name: 'IndicatorManage',
+        component: () => import('@/views/admin/indicators'),
+        meta: { title: '指标管理', perm: 'system:config' }
+      },
+      // ========== 虚拟服务商团队管理 ==========
+      {
+        path: 'virtual-teams',
+        name: 'VirtualTeamList',
+        component: () => import('@/views/admin/VirtualTeamList'),
+        meta: { title: '虚拟团队管理', perm: 'system:config' }
+      },
+      {
+        path: 'virtual-team-detail/:systemId',
+        name: 'VirtualTeamDetail',
+        component: () => import('@/views/admin/VirtualTeamDetail'),
+        meta: { title: '团队成员管理', perm: 'system:config' },
+        props: true
+      },
+      // ========== 电商供应商管理 ==========
+      {
+        path: 'ecom-supplier',
+        name: 'EcomSupplierManage',
+        component: () => import('@/views/admin/EcomSupplierManage'),
+        meta: { title: '电商供应商管理', perm: 'ecom:supplier' }
+      },
+      {
+        path: 'badge-manage',
+        name: 'BadgeManage',
+        component: () => import('@/views/admin/BadgeManage'),
+        meta: { title: '勋章管理', perm: 'system:config' }
+      },
+      // ========== 报告指纹解析系统 ==========
+      {
+        path: 'report-types',
+        name: 'ReportTypeManagement',
+        component: () => import('@/views/admin/ReportTypeManagement.vue'),
+        meta: { title: '报告类型管理', perm: 'system:config' }
+      },
+      {
+        path: 'report-parser-import',
+        name: 'ReportParserImport',
+        component: () => import('@/views/admin/ReportParserImport.vue'),
+        meta: { title: '解析器导入管理', perm: 'system:config' }
+      },
+      {
+        path: 'report-unknown-clusters',
+        name: 'ReportUnknownCluster',
+        component: () => import('@/views/admin/ReportUnknownCluster.vue'),
+        meta: { title: '未知报告审核', perm: 'system:config' }
+      },
+      {
+        path: 'report-auto-learn',
+        name: 'ReportAutoLearn',
+        component: () => import('@/views/admin/ReportAutoLearn.vue'),
+        meta: { title: '报告自学习', perm: 'system:config' }
+      },
+      // ========== LangGraph AI 服务管理 ==========
+      {
+        path: 'langgraph-admin',
+        name: 'LangGraphAdmin',
+        component: () => import('@/views/admin/LangGraphAdmin.vue'),
+        meta: { title: 'LangGraph 管理', perm: 'system:config' }
+      },
+      // ========== 营养产品管理 ==========
+      {
+        path: 'nutrition-products',
+        name: 'NutritionProducts',
+        component: () => import('@/views/admin/NutritionProducts.vue'),
+        meta: { title: '营养产品管理', perm: 'health:*' }
+      }
+    ]
+  }
+]
+
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.BASE_URL,
+  routes
+})
+
+// Token 验证缓存:避免 redirect 链中重复调用 /api/admin-auth/info
+let tokenValidationCache = { token: '', timestamp: 0 }
+const TOKEN_CACHE_TTL = 30000 // 30秒内不重复验证
+
+async function validateToken(token) {
+  try {
+    const baseURL = process.env.VUE_APP_BASE_API || ''
+    const res = await axios.post(`${baseURL}/api/admin-auth/info`, {}, {
+      headers: { Authorization: `Bearer ${token}` },
+      validateStatus: () => true
+    })
+    return res.status === 200 && res.data?.code === 200
+  } catch (e) {
+    return false
+  }
+}
+
+router.beforeEach(async (to, from, next) => {
+  const token = localStorage.getItem('token')
+  const role = localStorage.getItem('role')
+
+  // 1. 登录页允许访问
+  if (to.path === '/login') {
+    // 如果已有token和role,直接跳转到对应首页
+    if (token && role === 'teacher') {
+      return next('/teacher-dashboard')
+    }
+    return next()
+  }
+
+  // 2. 无Token强制跳转
+  if (!token) {
+    return next('/login')
+  }
+
+  // 3. Token有效性验证 (使用缓存避免 redirect 链重复调用)
+  const now = Date.now()
+  let isValid = false
+  if (tokenValidationCache.token === token && (now - tokenValidationCache.timestamp) < TOKEN_CACHE_TTL) {
+    isValid = true
+  } else {
+    isValid = await validateToken(token)
+    if (isValid) {
+      tokenValidationCache = { token, timestamp: now }
+    }
+  }
+  if (!isValid) {
+    tokenValidationCache = { token: '', timestamp: 0 }
+    localStorage.clear()
+    return next('/login')
+  }
+
+  // 3.5 规划师访问 /dashboard → 跳转规划师仪表板
+  if (role === 'teacher' && to.path === '/dashboard') {
+    return next('/teacher-dashboard')
+  }
+
+  // 4. 权限检查:如果路由有 perm 要求,检查用户权限
+  const requiredPerm = to.meta && to.meta.perm
+  if (requiredPerm) {
+    const rolesStr = localStorage.getItem('roles')
+    const roles = rolesStr ? JSON.parse(rolesStr) : [localStorage.getItem('role')]
+    const perms = getEffectivePermissions(roles)
+    if (!hasPermission(perms, requiredPerm)) {
+      console.warn('Access denied: insufficient permissions for', to.name)
+      return next({ path: '/403', replace: true })
+    }
+  }
+  return next()
+})
+
+// Suppress navigation guard redirect errors (teacher/admin route cross-access)
+const originalPush = VueRouter.prototype.push
+VueRouter.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch(err => err)
+}
+
+export default router

+ 13 - 3
cfc-web/src/views/Users.vue

@@ -70,7 +70,8 @@
         <el-table-column prop="id" label="ID" width="80"></el-table-column>
         <el-table-column prop="nickname" label="昵称" width="150">
           <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">昵称{{ sortIcon( + col_key + ) }}</span>
+            <span class="sort-header" @click.stop="onSortClick('nickname', $event)" @dblclick.stop="onSortToggle('nickname')">昵称{{ sortIcon('nickname') }}</span>
+
           </template></el-table-column>
         <el-table-column prop="realName" label="真实姓名" width="120"></el-table-column>
         <el-table-column prop="phone" label="手机号" width="120"></el-table-column>
@@ -93,7 +94,8 @@
             {{ formatDate(scope.row.createdAt) }}
           </template>
           <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">创建时间{{ sortIcon( + col_key + ) }}</span>
+            <span class="sort-header" @click.stop="onSortClick('createdAt', $event)" @dblclick.stop="onSortToggle('createdAt')">创建时间{{ sortIcon('createdAt') }}</span>
+
           </template></el-table-column>
         <el-table-column label="操作" width="200" fixed="right">
           <template slot-scope="{ row }">
@@ -195,7 +197,7 @@
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getUserList, updateUser, deleteUser, resetUserPassword } from '@/api/admin'
-import SortMixin from '@/mixins/SortMixin'
+
 import { getRoleLabel } from '@/utils/permissions'
 
 export default {
@@ -211,6 +213,8 @@ export default {
       loading: false,
       submitting: false,
       tableData: [],
+      sortableColumns: { nickname: '昵称', createdAt: '创建时间' },
+
       searchForm: {
         role: '',
         nickname: '',
@@ -253,6 +257,10 @@ export default {
     this.loadUsers()
   },
   methods: {
+    loadList() {
+      this.loadUsers()
+    },
+
     handleActionCmd(row, cmd) {
       switch (cmd) {
         case 'edit': this.handleEdit(row); break;
@@ -268,6 +276,8 @@ export default {
           size: this.pagination.size,
           ...this.searchForm
         }
+        if (this.sortState.length > 0) params.sort = this.sortState
+
         const res = await getUserList(params)
         this.tableData = res.data.records || res.data || []
         this.pagination.total = res.data.total || this.tableData.length

+ 14 - 6
cfc-web/src/views/admin/ArticleManage.vue

@@ -111,14 +111,18 @@
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column prop="viewCount" label="阅读" width="70" />
+        <el-table-column prop="viewCount" label="阅读" width="70">
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('viewCount', $event)" @dblclick.stop="onSortToggle('viewCount')">阅读{{ sortIcon('viewCount') }}</span>
+          </template>
+        </el-table-column>
+
         <el-table-column label="发布时间" width="160">
           <template slot-scope="{ row }">
             {{ formatTime(row.publishedAt) }}
           </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">浏览{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+        </el-table-column>
+
         <el-table-column label="操作" width="200" fixed="right">
           <template slot-scope="{ row }">
             <!-- 草稿箱:可编辑 -->
@@ -252,7 +256,7 @@ import {
   adminArticleWithdraw,
   adminArticleReDraft
 } from '@/api/article.js'
-import SortMixin from '@/mixins/SortMixin'
+
 import { getTagList } from '@/api/dimension.js'
 
 export default {
@@ -287,6 +291,8 @@ export default {
         tagId: '',
         keyword: ''
       },
+      sortableColumns: { viewCount: '阅读' },
+
       categories: [],
       tagOptions: [],
       rejectDialogVisible: false,
@@ -334,7 +340,9 @@ export default {
           difficultyLevel: this.filters.difficultyLevel || undefined,
           dimensionCode: this.filters.dimensionCode || undefined,
           tagId: this.filters.tagId || undefined,
-          keyword: this.filters.keyword || undefined
+          keyword: this.filters.keyword || undefined,
+          sort: this.sortState.length > 0 ? this.sortState : undefined
+
         })
         if (res.data) {
           this.list = res.data.records || res.data.list || res.data

+ 19 - 7
cfc-web/src/views/admin/CouponManagement.vue

@@ -23,14 +23,18 @@
       <div class="table-scroll-wrap-sm">
         <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
         <el-table-column prop="id" label="ID" width="70" />
-        <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('name', $event)" @dblclick.stop="onSortToggle('name')">名称{{ sortIcon('name') }}</span>
+          </template>
+        </el-table-column>
+
         <el-table-column label="类型" width="110">
           <template slot-scope="{ row }">
             <el-tag size="mini">{{ typeLabel(row.type) }}</el-tag>
           </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">名称{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+        </el-table-column>
+
         <el-table-column label="面值/折扣" width="100">
           <template slot-scope="{ row }">
             <span v-if="row.type === 'DISCOUNT'">{{ (row.discountRate / 100).toFixed(1) }}折</span>
@@ -230,7 +234,9 @@ export default {
       batchUserIds: '',
       batchSubmitting: false,
       couponOptions: [],
-      keyword: ''
+      keyword: '',
+      sortableColumns: { name: '名称' }
+
     }
   },
   created() {
@@ -272,7 +278,8 @@ export default {
     async loadData() {
       this.loading = true
       try {
-        const res = await getCouponList({ page: this.page, size: this.size, keyword: this.keyword || undefined }, sort: this.sortState.length > 0 ? this.sortState : undefined)
+        const res = await getCouponList({ page: this.page, size: this.size, keyword: this.keyword || undefined, sort: this.sortState.length > 0 ? this.sortState : undefined })
+
         if (res.data) {
           this.list = res.data.records || res.data.list || res.data
           this.total = res.data.total || this.list.length
@@ -283,6 +290,10 @@ export default {
         this.loading = false
       }
     },
+    loadList() {
+      this.loadData()
+    },
+
     handleSearch() {
       this.page = 1
       this.loadData()
@@ -386,7 +397,8 @@ export default {
       this.batchUserIds = ''
       this.batchDialogVisible = true
       try {
-        const res = await getCouponList({ page: 1, size: 999 }, sort: this.sortState.length > 0 ? this.sortState : undefined)
+        const res = await getCouponList({ page: 1, size: 999, sort: this.sortState.length > 0 ? this.sortState : undefined })
+
         this.couponOptions = res.data.records || res.data.list || res.data || []
       } catch (e) {
         console.error(e)

+ 12 - 4
cfc-web/src/views/admin/OrderManage.vue

@@ -32,7 +32,12 @@
 
     <div class="table-scroll-wrap-lg">
       <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
-      <el-table-column prop="orderNo" label="订单号" width="180" />
+      <el-table-column prop="orderNo" label="订单号" width="180">
+        <template slot="header">
+          <span class="sort-header" @click.stop="onSortClick('orderNo', $event)" @dblclick.stop="onSortToggle('orderNo')">订单号{{ sortIcon('orderNo') }}</span>
+        </template>
+      </el-table-column>
+
       <el-table-column label="商品" min-width="160">
         <template slot-scope="{ row }">
           <div style="display:flex;align-items:center;gap:8px;">
@@ -40,9 +45,8 @@
             <span>{{ row.productName || '-' }}</span>
           </div>
         </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">订单号{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+      </el-table-column>
+
       <el-table-column prop="quantity" label="数量" width="60" align="center" />
       <el-table-column label="金额" width="100">
         <template slot-scope="{ row }">
@@ -199,6 +203,8 @@ export default {
       page: 1,
       size: 20,
       total: 0,
+      sortableColumns: { orderNo: '订单号' },
+
       statusFilter: '',
       keyword: '',
       dateRange: null,
@@ -236,6 +242,8 @@ export default {
           params.startDate = this.dateRange[0]
           params.endDate = this.dateRange[1]
         }
+        if (this.sortState.length > 0) params.sort = this.sortState
+
         var res = await getOrderList(params)
         this.list = res.data.records || []
         this.total = res.data.total || 0

+ 22 - 9
cfc-web/src/views/admin/ProductManage.vue

@@ -49,14 +49,18 @@
           <span v-else style="color:#999">-</span>
         </template>
       </el-table-column>
-      <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip>
+        <template slot="header">
+          <span class="sort-header" @click.stop="onSortClick('name', $event)" @dblclick.stop="onSortToggle('name')">商品名称{{ sortIcon('name') }}</span>
+        </template>
+      </el-table-column>
+
       <el-table-column label="类型" width="90">
         <template slot-scope="{ row }">
           {{ typeLabel(row.productType) }}
         </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">名称{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+      </el-table-column>
+
       <el-table-column label="售价(元)" width="90">
         <template slot-scope="{ row }">
           {{ formatPriceWithSymbol(row.price || 0) }}
@@ -69,14 +73,18 @@
           </span>
         </template>
       </el-table-column>
-      <el-table-column prop="salesCount" label="销量" width="70" />
+      <el-table-column prop="salesCount" label="销量" width="70">
+        <template slot="header">
+          <span class="sort-header" @click.stop="onSortClick('salesCount', $event)" @dblclick.stop="onSortToggle('salesCount')">销量{{ sortIcon('salesCount') }}</span>
+        </template>
+      </el-table-column>
+
       <el-table-column label="供应商" width="120" show-overflow-tooltip>
         <template slot-scope="{ row }">
           {{ row.vendorName || '-' }}
         </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">销量{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+      </el-table-column>
+
       <el-table-column label="状态" width="90">
         <template slot-scope="{ row }">
           <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
@@ -92,7 +100,8 @@
           {{ formatTime(row.createdAt) }}
         </template>
           <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">创建时间{{ sortIcon( + col_key + ) }}</span>
+            <span class="sort-header" @click.stop="onSortClick('createdAt', $event)" @dblclick.stop="onSortToggle('createdAt')">创建时间{{ sortIcon('createdAt') }}</span>
+
           </template></el-table-column>
       <el-table-column label="操作" width="200" fixed="right">
         <template slot-scope="{ row }">
@@ -225,6 +234,8 @@ export default {
         size: 20,
         total: 0
       },
+      sortableColumns: { name: '商品名称', salesCount: '销量', createdAt: '创建时间' },
+
       rejectDialogVisible: false,
       rejectTarget: null,
       rejectReason: '',
@@ -249,6 +260,8 @@ export default {
           productType: this.filters.productType || null,
           keyword: this.filters.keyword || null
         }
+        if (this.sortState.length > 0) params.sort = this.sortState
+
         const res = await getProductList(params)
         this.list = res.data.records || []
         this.pagination.total = res.data.total || 0

+ 2 - 1
cfc-web/src/views/admin/ProductPpointManage.vue

@@ -153,7 +153,8 @@ export default {
     async loadList() {
       this.loading = true
       try {
-        const res = await getProductPpointList({ keyword: this.searchText, page: this.page, size: this.size }, sort: this.sortState.length > 0 ? this.sortState : undefined)
+        const res = await getProductPpointList({ keyword: this.searchText, page: this.page, size: this.size, sort: this.sortState.length > 0 ? this.sortState : undefined })
+
         this.list = res.data.records || res.data || []
         this.total = res.data.total || 0
       } catch (e) {

+ 12 - 5
cfc-web/src/views/admin/Recipes.vue

@@ -34,7 +34,12 @@
       <div class="table-scroll-wrap-sm">
       <el-table :data="list" v-loading="loading" border stripe :max-height="tableHeight">
         <el-table-column prop="id" label="ID" width="80" />
-        <el-table-column prop="name" label="名称" />
+        <el-table-column prop="name" label="名称">
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('name', $event)" @dblclick.stop="onSortToggle('name')">名称{{ sortIcon('name') }}</span>
+          </template>
+        </el-table-column>
+
         <el-table-column prop="mealType" label="类型" width="80" />
         <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
         <el-table-column prop="status" label="状态" width="80" />
@@ -50,9 +55,8 @@
               </el-dropdown-menu>
             </el-dropdown>
           </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick( + col_key + ',$event)" @dblclick.stop="onSortToggle( + col_key + )">名称{{ sortIcon( + col_key + ) }}</span>
-          </template></el-table-column>
+        </el-table-column>
+
     </el-table></div>
     </el-card>
     <el-dialog :title="editId ? '编辑食谱' : '新建食谱'" :visible.sync="dialogVisible" width="700px">
@@ -108,6 +112,8 @@
 </template>
 
 <script>
+import SortMixin from '@/mixins/SortMixin'
+
 export default {
   mixins: [SortMixin],
   name: 'Recipes',
@@ -151,7 +157,8 @@ export default {
         const baseURL = process.env.VUE_APP_BASE_API || ''
         const token = localStorage.getItem('token')
         const axios = this.$axios || (await import('axios')).default
-        const res = await axios.post(baseURL + '/api/admin/recipes/list', { mealType: this.mealTypeFilter || undefined, keyword: this.keyword || undefined }, {
+        const res = await axios.post(baseURL + '/api/admin/recipes/list', { mealType: this.mealTypeFilter || undefined, keyword: this.keyword || undefined, sort: this.sortState.length > 0 ? this.sortState : undefined }, {
+
           headers: { Authorization: 'Bearer ' + token }
         })
         this.list = res.data && res.data.data || []

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio