瀏覽代碼

feat(diet): S-cfc-127 diet.recommendation.uiRework 推荐食材页 UI 重构(去标题/4Tab/分页/我标签/生成loading)

E2E Test Bot 3 天之前
父節點
當前提交
1bad02ec1
共有 1 個文件被更改,包括 68 次插入 和 30 次删除
  1. 68 30
      cfc-frontend/pages/diet/recommendation.vue

+ 68 - 30
cfc-frontend/pages/diet/recommendation.vue

@@ -1,13 +1,7 @@
 <template>
   <view class="recommendation-page" :style="{ '--font-scale': fontScale }">
 
-    <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
-      <!-- ===== 推荐食材 ===== -->
-      <view class="section-title-bar">
-        <text class="section-title">推荐食材</text>
-        <text class="section-subtitle">查看家庭成员适合与戒备的食材</text>
-      </view>
-
+    <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
       <view class="food-section">
         <!-- 成员切换条(统一组件 MemberSelector:家庭总览 / 个人 / 各成员) -->
         <MemberSelector
@@ -17,9 +11,29 @@
           :selected-member-id="currentMemberId"
           :show-overview="true"
           :overview-active="currentView === 'family'"
+          self-label="我"
           @overview="switchView('family')"
           @select="switchMember" />
 
+        <!-- Tab 切换行:今日食谱 | 适合 | 戒备 | 全部 -->
+        <view class="filter-bar">
+          <view class="filter-item" :class="{'filter-active': currentTab === 'recipe'}" @tap="switchTab('recipe')">
+            <text>🍽️ 今日食谱</text>
+          </view>
+          <view class="filter-item" :class="{'filter-active': currentTab === 'suitable'}" @tap="switchTab('suitable')">
+            <text>✅ 适合</text>
+          </view>
+          <view class="filter-item" :class="{'filter-active': currentTab === 'unsuitable'}" @tap="switchTab('unsuitable')">
+            <text>❌ 戒备</text>
+          </view>
+          <view class="filter-item" :class="{'filter-active': currentTab === 'all'}" @tap="switchTab('all')">
+            <text>📋 全部</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 食材区(适合 / 戒备 / 全部 Tab 内容) -->
+      <view class="food-section" v-if="currentTab !== 'recipe'">
         <!-- 模式切换(仅家庭模式展示) -->
         <view class="mode-bar" v-if="familyMode">
           <view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
@@ -61,19 +75,6 @@
           </view>
         </view>
 
-        <!-- 筛选栏 -->
-        <view class="filter-bar">
-          <view class="filter-item" :class="{'filter-active': filterType === 'suitable'}" @tap="setFilter('suitable')">
-            <text>✅ 适合</text>
-          </view>
-          <view class="filter-item" :class="{'filter-active': filterType === 'unsuitable'}" @tap="setFilter('unsuitable')">
-            <text>❌ 戒备</text>
-          </view>
-          <view class="filter-item" :class="{'filter-active': filterType === 'all'}" @tap="setFilter('all')">
-            <text>📋 全部</text>
-          </view>
-        </view>
-
         <!-- 家庭总览(取家庭成员该项食材推荐指数的最小值) -->
         <view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
           <view class="overview-title">
@@ -101,7 +102,7 @@
 
         <!-- 食材列表 -->
         <view class="food-list" v-if="currentView === 'personal'">
-          <view class="food-card" v-for="(item, idx) in filteredFoods" :key="getFoodKey(item, idx)">
+          <view class="food-card" v-for="(item, idx) in pagedFoods" :key="getFoodKey(item, idx)">
             <!-- 健康问卷个性化调整提示条:blocked 红 / warning 黄 / boost 绿(normal/空不渲染) -->
             <view v-if="item.adjustment === 'blocked'" class="health-adjust-bar">
               <text class="health-adjust-text health-adjust-blocked">⚠ 健康问卷不建议:{{ item.adjustmentReason }}</text>
@@ -142,10 +143,8 @@
         </view>
       </view>
 
-      <!-- ===== 今日食谱 ===== -->
-      <view class="section-title-bar">
-        <text class="section-title">今日食谱</text>
-      </view>
+      <!-- 今日食谱区(recipe Tab 内容) -->
+      <view v-if="currentTab === 'recipe'">
 
       <!-- 无推荐状态 -->
       <view class="empty-state" v-if="!recommendation">
@@ -243,6 +242,7 @@
           </view>
         </view>
       </view>
+      </view>
     </scroll-view>
 
     <!-- 共餐人选择弹层(需求7:家庭总览「生成食谱」先选共餐人,确认后生成) -->
@@ -301,6 +301,9 @@ export default {
       filterType: 'suitable',
       sortType: 'desc',
       currentView: 'personal',
+      currentTab: 'recipe',
+      visibleCount: 20,
+      memberSelectLocked: false,
       allFoods: [],
       familyData: null,
       familyMembers: [],
@@ -386,6 +389,9 @@ export default {
 
       return result
     },
+    pagedFoods: function() {
+      return this.filteredFoods.slice(0, this.visibleCount)
+    },
     // 共餐人选择弹层列表(原始 memberList 归一化为 {id, nickname, selected},避免模板内方法调用)
     pendingParticipantOptions: function() {
       var self = this
@@ -481,12 +487,14 @@ export default {
       // 个人/成员:只对当前选中人生成(participant_member_ids 只传当前成员)
       self.loading = true
       var participantIds = self.currentMemberId ? [self.currentMemberId] : []
-      generateDietRecommendation({
+      uni.showLoading({ title: '正在生成食谱...' })
+      return generateDietRecommendation({
         date: self.formatDate(new Date()),
         meal_type: 'all',
         participant_member_ids: JSON.stringify(participantIds)
       }).then(function(res) {
         self.loading = false
+        uni.hideLoading()
         if (res && res.data && res.data.id) {
           uni.showToast({ title: '食谱生成成功', icon: 'success' })
           self.loadRecommendation()
@@ -495,6 +503,7 @@ export default {
         }
       }).catch(function() {
         self.loading = false
+        uni.hideLoading()
         uni.showToast({ title: '生成失败', icon: 'none' })
       })
     },
@@ -602,14 +611,17 @@ export default {
       if (!token) return
 
       uni.request({
-        url: config.API_BASE_URL + '/api/food/recommendation/members',
+        // config 在契约测试桩下可能为 undefined(stub 仅提供 default 键),防御性取址
+        url: ((config && config.API_BASE_URL) || '') + '/api/food/recommendation/members',
         method: 'POST',
         header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
         success: function(res) {
           if (res.data && res.data.code === 200 && res.data.data) {
             self.memberList = res.data.data
-            // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)
-            if (!self.currentMemberId && self.memberList.length > 0) {
+            // 默认选中「个人」= isSelf 行(统一身份键 familyMemberId,不再用 userId)。
+            // 仅限首次 onLoad:切到家庭总览(switchView('family') 置 memberSelectLocked)后,
+            // loadMembers 不得再自动默认选「个人」,个人/成员 chip 保持不高亮。
+            if (!self.memberSelectLocked && !self.currentMemberId && self.memberList.length > 0) {
               var defaultMember = self.getSelfRow() || self.memberList[0]
               self.currentMemberId = memberIdOf(defaultMember)
             }
@@ -760,6 +772,7 @@ export default {
     },
     switchCategory: function(category) {
       this.activeCategory = category
+      this.visibleCount = 20
     },
     getCategoryKeywords: function(category) {
       var map = {
@@ -780,12 +793,34 @@ export default {
       } else {
         this.sortType = 'desc'
       }
+      this.visibleCount = 20
     },
     setSort: function(type) {
       this.sortType = type
     },
+    switchTab: function(tab) {
+      // 4 Tab:今日食谱 | 适合 | 戒备 | 全部;切 Tab 重置分页(请求式上滑加载从首批重新开始)
+      this.currentTab = tab
+      this.visibleCount = 20
+      if (tab !== 'recipe') {
+        this.filterType = tab
+        this.sortType = tab === 'unsuitable' ? 'asc' : 'desc'
+      }
+    },
+    loadMore: function() {
+      // 上滑触底加载更多:未达全量末尾时每批追加 20 条,到达末尾自动停止
+      if (this.visibleCount < this.filteredFoods.length) {
+        this.visibleCount += 20
+      }
+    },
     switchView: function(view) {
       this.currentView = view
+      if (view === 'family') {
+        // 家庭总览 ↔ 个人/成员互斥:清空成员选中态(个人/成员 chip 不高亮);
+        // 并锁定「不再自动默认选个人」,loadMembers 再次加载时不回灌默认选中
+        this.currentMemberId = null
+        this.memberSelectLocked = true
+      }
     },
     // 取「本人」行(isSelf):后端已按 user_id 折叠,此处仅取第一行 isSelf
     getSelfRow: function() {
@@ -799,6 +834,7 @@ export default {
       // 统一身份键:familyMemberId(= family_members.id);userId 仅用于折叠,不再作主键
       this.currentMemberId = memberIdOf(member)
       this.currentView = 'personal'
+      this.visibleCount = 20
       this.loadFoods()
     },
     onRefresh: function() {
@@ -1087,7 +1123,9 @@ export default {
   margin-bottom: 20rpx;
 }
 .filter-item {
-  padding: 12rpx 24rpx;
+  flex: 1;
+  text-align: center;
+  padding: 12rpx 8rpx;
   border: 1rpx solid $facade-border-soft;
   border-radius: 24rpx;
   font-size: calc(24rpx * var(--font-scale, 1));