jiapu 2 ماه پیش
والد
کامیت
e7c5728fe7
2فایلهای تغییر یافته به همراه200 افزوده شده و 178 حذف شده
  1. 191 172
      cfc-frontend/components/MemberChipPicker.vue
  2. 9 6
      cfc-frontend/pages/health/gut-flora-risks-detail.vue

+ 191 - 172
cfc-frontend/components/MemberChipPicker.vue

@@ -1,49 +1,35 @@
 <template>
 <template>
-  <view class="member-chip-picker">
-    <view v-if="loading" class="mcp-loading">
-      <text class="mcp-loading-text">加载成员中...</text>
-    </view>
-
-    <view v-else-if="members.length > 0" class="mcp-chips">
-      <view
-        class="mcp-chip"
-        v-for="(m, i) in members"
-        :key="getMemberKey(m, i)"
-        :class="{ 'mcp-chip-on': isSelected(m.memberId) }"
-        @tap="toggleMember(m.memberId)"
-      >
-        <image
-          v-if="m.avatar"
-          class="mcp-chip-avatar"
-          :src="m.avatar"
-          mode="aspectFill"
-        />
-        <view v-else class="mcp-chip-avatar mcp-chip-avatar-default">
-          <text class="mcp-chip-avatar-text">{{ getInitial(m) }}</text>
+  <view class="member-picker">
+    <text class="member-load-hint">成员数:{{ memberCount }}</text>
+    <scroll-view class="member-scroll" scroll-x="true" :show-scrollbar="false" v-if="memberCount > 0">
+      <view class="member-chips">
+        <view
+          v-for="item in members"
+          :key="item.id"
+          class="member-chip"
+          :class="{ 'member-chip-on': item.selected }"
+          @click="toggleMember(item.id)">
+          <text class="member-chip-text">{{ item.nickname }}</text>
         </view>
         </view>
-        <text class="mcp-chip-text">{{ m.name || m.nickname || '成员' }}</text>
-      </view>
-
-      <view class="mcp-chip mcp-chip-add" @tap="goAddMember">
-        <text class="mcp-chip-add-icon">+</text>
-        <text class="mcp-chip-text">添加</text>
       </view>
       </view>
+    </scroll-view>
+    <view v-if="memberCount === 0 && !loading" class="empty-state">
+      <text class="empty-text">暂无家庭成员,请先添加后再制定方案</text>
+      <view class="empty-action" @click="goAddMember">去添加成员</view>
+      <view class="empty-action reload-btn" @click="reload">重新加载</view>
     </view>
     </view>
-
-    <view v-else class="mcp-empty">
-      <text class="mcp-empty-text">暂无家庭成员</text>
-      <view class="mcp-empty-btn" @tap="goAddMember">+ 去添加成员</view>
+    <view v-if="loading" class="empty-state">
+      <text class="empty-text">成员加载中...</text>
     </view>
     </view>
   </view>
   </view>
 </template>
 </template>
 
 
 <script>
 <script>
-import { getFamilyMemberList } from '@/utils/api.js'
+import { getFamilyMemberList } from '../utils/api.js'
 
 
 export default {
 export default {
   name: 'MemberChipPicker',
   name: 'MemberChipPicker',
   props: {
   props: {
-    // 已选中的成员 id 数组(字符串)
     selectedIds: {
     selectedIds: {
       type: Array,
       type: Array,
       default: function() { return [] }
       default: function() { return [] }
@@ -52,182 +38,215 @@ export default {
   data: function() {
   data: function() {
     return {
     return {
       members: [],
       members: [],
-      innerSelectedIds: [],
-      loading: false,
-      loaded: false
-    }
-  },
-  watch: {
-    selectedIds: {
-      handler: function(val) {
-        this.innerSelectedIds = (val || []).map(function(id) { return String(id) })
-      },
-      immediate: true
+      memberCount: 0,
+      loading: false
     }
     }
   },
   },
-  created: function() {
-    this.reload()
+  mounted: function() {
+    this.loadMembers()
   },
   },
   methods: {
   methods: {
-    // ——— :key 方法(避免模板内运算符表达式) ———
-    getMemberKey: function(m, i) {
-      return String(m.memberId || m.id || i)
+    reload: function() {
+      this.loadMembers()
     },
     },
-    // ——— 首字(无头像兜底) ———
-    getInitial: function(m) {
-      var name = m.name || m.nickname || ''
-      return name ? name.substring(0, 1) : '?'
+    goAddMember: function() {
+      this.$emit('add-member')
+      uni.navigateTo({ url: '/pages/family/add-member' })
     },
     },
-    isSelected: function(id) {
-      return this.innerSelectedIds.indexOf(String(id)) >= 0
+    toggleMember: function(id) {
+      var strId = String(id)
+      var ids = []
+      var i
+      for (i = 0; i < this.members.length; i++) {
+        if (this.members[i].selected) ids.push(String(this.members[i].id))
+      }
+      var idx = ids.indexOf(strId)
+      if (idx >= 0) ids.splice(idx, 1)
+      else ids.push(strId)
+      this.applySelected(ids)
+      this.emitChange()
     },
     },
-    // ——— 加载家庭成员 ———
-    reload: function() {
-      var self = this
-      this.loading = true
-      getFamilyMemberList({ visibleOnly: true }).then(function(res) {
-        self.loading = false
-        if (res && res.code === 200 && res.data) {
-          var raw = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
-          var list = []
-          for (var i = 0; i < raw.length; i++) {
-            var m = raw[i]
-            // 同时输出 id/memberId、name/nickname,兼容父组件对成员字段的不同取法
-            list.push({
-              id: m.id,
-              memberId: m.memberId !== undefined ? m.memberId : m.id,
-              name: m.name || m.nickname,
-              nickname: m.nickname || m.name,
-              avatar: m.avatar || '',
-              relationship: m.relationship || m.relativeLabel || m.roleLabel || ''
-            })
-          }
-          self.members = list
-          self.emitState()
-        } else {
-          self.members = []
-          self.emitState()
-        }
-      }).catch(function() {
-        self.loading = false
-        self.members = []
-      })
+    applySelected: function(ids) {
+      var selected = {}
+      var i
+      for (i = 0; i < ids.length; i++) {
+        selected[String(ids[i])] = true
+      }
+      var list = []
+      for (i = 0; i < this.members.length; i++) {
+        var m = this.members[i]
+        list.push({
+          id: m.id,
+          memberId: m.memberId,
+          name: m.name,
+          nickname: m.nickname,
+          selected: !!selected[String(m.id)]
+        })
+      }
+      this.members = list
+      this.memberCount = list.length
     },
     },
-    // ——— 切换选中态 ———
-    toggleMember: function(id) {
-      var sid = String(id)
-      var idx = this.innerSelectedIds.indexOf(sid)
-      if (idx >= 0) {
-        this.innerSelectedIds.splice(idx, 1)
-      } else {
-        this.innerSelectedIds.push(sid)
+    emitChange: function() {
+      var ids = []
+      var i
+      for (i = 0; i < this.members.length; i++) {
+        if (this.members[i].selected) ids.push(String(this.members[i].id))
       }
       }
-      this.emitState()
+      this.$emit('change', { selectedIds: ids, members: this.members })
     },
     },
-    // ——— 通知父组件 ———
-    emitState: function() {
-      this.$emit('change', {
-        selectedIds: this.innerSelectedIds.slice(),
-        members: this.members.slice()
-      })
-      if (!this.loaded) {
-        this.loaded = true
-        this.$emit('loaded', {
-          selectedIds: this.innerSelectedIds.slice(),
-          members: this.members.slice()
+    normalizeMemberList: function(data) {
+      if (!data) return []
+      var asList = function(v) {
+        return v && typeof v === 'object' && typeof v.length === 'number' ? v : null
+      }
+      if (asList(data)) return data
+      var sources = [data.children, data.members, data.list, data.records]
+      var merged = []
+      var seen = {}
+      var s, i, m, mid, key
+      for (s = 0; s < sources.length; s++) {
+        var arr = asList(sources[s])
+        if (!arr || arr.length === 0) continue
+        for (i = 0; i < arr.length; i++) {
+          m = arr[i]
+          if (!m) continue
+          mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
+          if (mid == null) continue
+          key = String(mid)
+          if (seen[key]) continue
+          seen[key] = true
+          merged.push(m)
+        }
+      }
+      return merged
+    },
+    applyList: function(rawList) {
+      var list = rawList || []
+      var mapped = []
+      var i, m, mid, nm
+      for (i = 0; i < list.length; i++) {
+        m = list[i]
+        if (!m) continue
+        mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
+        if (mid == null) continue
+        nm = m.nickname || m.name || m.roleLabel || '成员'
+        mapped.push({
+          id: mid,
+          memberId: String(mid),
+          name: String(nm),
+          nickname: String(nm),
+          selected: false
         })
         })
       }
       }
+      var selectedIds = (this.selectedIds || []).map(function(id) { return String(id) })
+      var idSet = {}
+      for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
+      selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
+      if (selectedIds.length === 0 && mapped.length > 0) {
+        selectedIds = [String(mapped[0].id)]
+      }
+      for (i = 0; i < mapped.length; i++) {
+        mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0
+      }
+      this.members = mapped
+      this.memberCount = mapped.length
+      this.emitChange()
+      this.$emit('loaded', { members: mapped, selectedIds: selectedIds })
+      return mapped.length
     },
     },
-    // ——— 添加成员 ———
-    goAddMember: function() {
-      this.$emit('add-member')
-      uni.navigateTo({ url: '/pages/family/add-member' })
+    loadMembers: function() {
+      if (this.loading) return
+      this.loading = true
+      var self = this
+      getFamilyMemberList({ visibleOnly: true }).then(function(res) {
+        var list = self.normalizeMemberList(res && res.data)
+        if (list.length > 0) return list
+        return getFamilyMemberList({ includeFamily: true }).then(function(res2) {
+          var list2 = self.normalizeMemberList(res2 && res2.data)
+          if (list2.length > 0) return list2
+          return getFamilyMemberList({}).then(function(res3) {
+            return self.normalizeMemberList(res3 && res3.data)
+          })
+        })
+      }).then(function(list) {
+        var count = self.applyList(list || [])
+        if (count === 0) {
+          uni.showToast({ title: '成员列表为空', icon: 'none' })
+        }
+      }).catch(function(err) {
+        console.warn('[MemberChipPicker] loadMembers failed', err)
+        self.members = []
+        self.memberCount = 0
+        uni.showToast({ title: '成员加载失败', icon: 'none' })
+      }).then(function() {
+        self.loading = false
+      })
     }
     }
   }
   }
 }
 }
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.member-chip-picker {
+.member-scroll {
   width: 100%;
   width: 100%;
+  white-space: nowrap;
+  margin-bottom: 16rpx;
 }
 }
-.mcp-loading {
-  padding: 30rpx 0;
-  text-align: center;
-}
-.mcp-loading-text {
-  font-size: 26rpx;
-  color: #9CA3AF;
-}
-.mcp-chips {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
+.member-chips {
+  display: inline-flex;
+  flex-direction: row;
+  padding: 4rpx 0;
 }
 }
-.mcp-chip {
-  display: flex;
+.member-chip {
+  display: inline-flex;
   align-items: center;
   align-items: center;
-  padding: 12rpx 24rpx;
-  margin: 0 16rpx 16rpx 0;
-  background: #F3F4F6;
-  border-radius: 999rpx;
+  justify-content: center;
+  padding: 12rpx 28rpx;
+  margin-right: 16rpx;
+  border-radius: 32rpx;
+  background: #F1F5F9;
   border: 2rpx solid transparent;
   border: 2rpx solid transparent;
 }
 }
-.mcp-chip-on {
-  background: #FFEDD5;
+.member-chip-on {
+  background: #FFF7ED;
   border-color: #F97316;
   border-color: #F97316;
 }
 }
-.mcp-chip-avatar {
-  width: 40rpx;
-  height: 40rpx;
-  border-radius: 50%;
-  margin-right: 10rpx;
-  overflow: hidden;
-}
-.mcp-chip-avatar-default {
-  background: #E5E7EB;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.mcp-chip-avatar-text {
-  font-size: 22rpx;
-  color: #6B7280;
-}
-.mcp-chip-text {
+.member-chip-text {
   font-size: 26rpx;
   font-size: 26rpx;
-  color: #374151;
+  color: #334155;
+  font-weight: 600;
 }
 }
-.mcp-chip-on .mcp-chip-text {
-  color: #C2410C;
-  font-weight: 500;
+.member-chip-on .member-chip-text {
+  color: #F97316;
 }
 }
-.mcp-chip-add {
-  background: transparent;
-  border: 2rpx dashed #D1D5DB;
-  color: #9CA3AF;
-}
-.mcp-chip-add-icon {
-  font-size: 28rpx;
-  margin-right: 6rpx;
-  color: #9CA3AF;
+.member-load-hint {
+  font-size: 22rpx;
+  color: #F97316;
+  margin-bottom: 12rpx;
 }
 }
-.mcp-empty {
-  padding: 40rpx 0;
+.empty-state {
+  width: 100%;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
+  padding: 40rpx 0 24rpx;
+  box-sizing: border-box;
 }
 }
-.mcp-empty-text {
+.empty-text {
   font-size: 26rpx;
   font-size: 26rpx;
-  color: #9CA3AF;
-  margin-bottom: 16rpx;
+  color: #999;
 }
 }
-.mcp-empty-btn {
-  padding: 14rpx 40rpx;
-  background: #F97316;
-  color: #FFFFFF;
+.empty-action {
+  margin-top: 20rpx;
+  padding: 12rpx 32rpx;
   font-size: 26rpx;
   font-size: 26rpx;
-  border-radius: 40rpx;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+  border-radius: 32rpx;
+}
+.reload-btn {
+  margin-top: 16rpx;
+  border-color: #999;
+  color: #666;
 }
 }
 </style>
 </style>

+ 9 - 6
cfc-frontend/pages/health/gut-flora-risks-detail.vue

@@ -24,11 +24,11 @@
         <view class="section-line"></view>
         <view class="section-line"></view>
         <text class="section-label">⚠ 需重点关注</text>
         <text class="section-label">⚠ 需重点关注</text>
       </view>
       </view>
-      <view class="risk-card important-card" v-for="(item, idx) in importantRisks" :key="'imp-' + idx"
+      <view class="risk-card important-card" v-for="(item, idx) in importantRisks" :key="idx"
             :class="'risk-level-' + (item._cssClass || 'low')">
             :class="'risk-level-' + (item._cssClass || 'low')">
         <view class="card-top">
         <view class="card-top">
           <view class="card-title-row">
           <view class="card-title-row">
-            <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+            <text class="risk-name" @tap="showDiseaseKnowledge(item)">{{ item.diseaseName }}</text>
             <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
             <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
               {{ riskBadgeText(item.riskLevel) }}
               {{ riskBadgeText(item.riskLevel) }}
             </text>
             </text>
@@ -50,7 +50,7 @@
             {{ item.riskLevel || '--' }}
             {{ item.riskLevel || '--' }}
           </text>
           </text>
           <text class="risk-value" v-if="item.riskValue">风险值 {{ item.riskValue }}</text>
           <text class="risk-value" v-if="item.riskValue">风险值 {{ item.riskValue }}</text>
-          <text class="risk-more" @tap.stop="showKnowledge('disease', item.diseaseName)">查看知识库 ›</text>
+          <text class="risk-more" @tap.stop="showDiseaseKnowledge(item)">查看知识库 ›</text>
         </view>
         </view>
         <view class="risk-edit-row" v-if="isEditing">
         <view class="risk-edit-row" v-if="isEditing">
           <text class="edit-label">风险值:</text>
           <text class="edit-label">风险值:</text>
@@ -67,11 +67,11 @@
         <view class="section-line"></view>
         <view class="section-line"></view>
         <text class="section-label">✓ 低风险项</text>
         <text class="section-label">✓ 低风险项</text>
       </view>
       </view>
-      <view class="risk-card normal-card" v-for="(item, idx) in normalRisks" :key="'norm-' + idx"
+      <view class="risk-card normal-card" v-for="(item, idx) in normalRisks" :key="idx"
             :class="'risk-level-' + (item._cssClass || 'low')">
             :class="'risk-level-' + (item._cssClass || 'low')">
         <view class="card-top">
         <view class="card-top">
           <view class="card-title-row">
           <view class="card-title-row">
-            <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+            <text class="risk-name" @tap="showDiseaseKnowledge(item)">{{ item.diseaseName }}</text>
             <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
             <text class="risk-badge" :class="'badge-' + (item._cssClass || 'low')">
               {{ riskBadgeText(item.riskLevel) }}
               {{ riskBadgeText(item.riskLevel) }}
             </text>
             </text>
@@ -83,7 +83,7 @@
         </view>
         </view>
         <view class="risk-detail-row" v-if="!isEditing">
         <view class="risk-detail-row" v-if="!isEditing">
           <text class="risk-tag tag-low">{{ item.riskLevel || '--' }}</text>
           <text class="risk-tag tag-low">{{ item.riskLevel || '--' }}</text>
-          <text class="risk-more" @tap.stop="showKnowledge('disease', item.diseaseName)">查看知识库 ›</text>
+          <text class="risk-more" @tap.stop="showDiseaseKnowledge(item)">查看知识库 ›</text>
         </view>
         </view>
         <view class="risk-edit-row" v-if="isEditing">
         <view class="risk-edit-row" v-if="isEditing">
           <text class="edit-label">风险值:</text>
           <text class="edit-label">风险值:</text>
@@ -226,6 +226,9 @@ export default {
       item.riskLevel = this.riskLevels[e.detail.value]
       item.riskLevel = this.riskLevels[e.detail.value]
       item._cssClass = this._cssForRiskLevel(item.riskLevel)
       item._cssClass = this._cssForRiskLevel(item.riskLevel)
     },
     },
+    showDiseaseKnowledge: function(item) {
+      this.showKnowledge('disease', item && item.diseaseName)
+    },
     showKnowledge: function(type, name) {
     showKnowledge: function(type, name) {
       var self = this
       var self = this
       // 优先查询疾病知识库
       // 优先查询疾病知识库