|
|
@@ -1,52 +1,83 @@
|
|
|
<template>
|
|
|
<view class="page">
|
|
|
- <view class="nav-bar">
|
|
|
- <view class="nav-back" @tap="goBack">
|
|
|
- <text class="back-text">‹ 返回</text>
|
|
|
+ <!-- 搜索框 -->
|
|
|
+ <view class="search-bar">
|
|
|
+ <view class="search-input-wrap">
|
|
|
+ <text class="search-icon">🔍</text>
|
|
|
+ <input class="search-input" v-model="searchText" placeholder="搜索食材名称,查看推荐值" confirm-type="search" />
|
|
|
+ <text class="search-clear" v-if="searchText" @tap="searchText = ''">✕</text>
|
|
|
</view>
|
|
|
- <text class="nav-title">饮食推荐</text>
|
|
|
- <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <scroll-view class="filter-bar" scroll-x>
|
|
|
- <view class="filter-item" v-for="(cat, idx) in categories" :key="idx"
|
|
|
- :class="'filter-' + (selectedCategory === cat ? 'active' : '')"
|
|
|
- @tap="selectedCategory = cat">
|
|
|
- <text>{{ cat }}</text>
|
|
|
- </view>
|
|
|
- </scroll-view>
|
|
|
-
|
|
|
- <view class="sort-bar">
|
|
|
- <text class="sort-text" @tap="sortDesc = !sortDesc">排序: {{ sortDesc ? '推荐↓' : '推荐↑' }}</text>
|
|
|
- <text class="sort-text" @tap="showRecommendedOnly = !showRecommendedOnly">{{ showRecommendedOnly ? '全部' : '仅推荐' }}</text>
|
|
|
</view>
|
|
|
|
|
|
<scroll-view class="content" scroll-y>
|
|
|
- <view class="food-card" v-for="(item, idx) in filteredFoods" :key="idx">
|
|
|
+ <view
|
|
|
+ class="food-card"
|
|
|
+ v-for="(item, idx) in filteredFoods"
|
|
|
+ :key="idx"
|
|
|
+ :class="'score-band-' + _scoreBand(item.score)"
|
|
|
+ @tap="showFoodKnowledge(item)"
|
|
|
+ >
|
|
|
<view class="food-header">
|
|
|
<text class="food-name">{{ item.foodName || item.name }}</text>
|
|
|
<view class="food-cat-tag">
|
|
|
<text>{{ item.category }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view class="food-detail-row" v-if="!isEditing">
|
|
|
- <text class="food-nutrition">蛋白{{ item.protein || '--' }} | 脂肪{{ item.fat || '--' }} | 碳水{{ item.carb || '--' }} | 纤维{{ item.fiber || '--' }}</text>
|
|
|
- </view>
|
|
|
- <view class="food-detail-row" v-if="!isEditing && item.energy">
|
|
|
- <text class="food-nutrition">能量 {{ item.energy }}KJ</text>
|
|
|
+ <view class="food-score-row">
|
|
|
+ <text class="food-score-label">推荐值</text>
|
|
|
+ <text class="food-score-value">{{ item.score }}</text>
|
|
|
</view>
|
|
|
<view class="food-edit-row" v-if="isEditing">
|
|
|
- <text class="edit-label">推荐指数:</text>
|
|
|
+ <text class="edit-label">推荐值:</text>
|
|
|
<input class="edit-input" type="number" v-model="item.score" />
|
|
|
</view>
|
|
|
- <view class="food-score" :class="(item.score || 0) >= 5 ? 'score-good' : 'score-normal'">
|
|
|
- <text>推荐: {{ item.score }}</text>
|
|
|
- </view>
|
|
|
</view>
|
|
|
<view class="empty-hint" v-if="filteredFoods.length === 0">暂无数据</view>
|
|
|
<view class="bottom-spacer"></view>
|
|
|
</scroll-view>
|
|
|
|
|
|
+ <!-- 食材知识库弹窗 -->
|
|
|
+ <view class="knowledge-overlay" v-if="knowledgeVisible" @tap="closeKnowledge">
|
|
|
+ <view class="knowledge-popup" @tap.stop>
|
|
|
+ <view class="knowledge-header">
|
|
|
+ <text class="knowledge-title">{{ knowledgeData.itemName || '食材详情' }}</text>
|
|
|
+ <text class="knowledge-close" @tap="closeKnowledge">✕</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-body">
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.category">
|
|
|
+ <text class="knowledge-label">分类</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.category }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.protein != null">
|
|
|
+ <text class="knowledge-label">蛋白</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.protein }}g</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.fat != null">
|
|
|
+ <text class="knowledge-label">脂肪</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.fat }}g</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.carbs != null">
|
|
|
+ <text class="knowledge-label">碳水</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.carbs }}g</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.fiber != null">
|
|
|
+ <text class="knowledge-label">纤维</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.fiber }}g</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-row" v-if="knowledgeData.energy != null">
|
|
|
+ <text class="knowledge-label">能量</text>
|
|
|
+ <text class="knowledge-value">{{ knowledgeData.energy }}KJ</text>
|
|
|
+ </view>
|
|
|
+ <view class="knowledge-desc" v-if="knowledgeData.description">
|
|
|
+ <text class="knowledge-desc-text">{{ knowledgeData.description }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="edit-fab" v-if="!isEditing && reportId" @tap="toggleEdit">
|
|
|
+ <text>编辑</text>
|
|
|
+ </view>
|
|
|
<view class="bottom-bar" v-if="isEditing">
|
|
|
<view class="btn-cancel" @tap="cancelEdit">取消</view>
|
|
|
<view class="btn-save" @tap="saveEdit">保存</view>
|
|
|
@@ -55,7 +86,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getReportDetail, editHealthReport, getHealthFoods } from '../../utils/api.js'
|
|
|
+import { getHealthFoods, editHealthReport, queryFoodKnowledge } from '../../utils/api.js'
|
|
|
|
|
|
export default {
|
|
|
data() {
|
|
|
@@ -65,66 +96,124 @@ export default {
|
|
|
foods: [],
|
|
|
isEditing: false,
|
|
|
editFoods: [],
|
|
|
- selectedCategory: '全部',
|
|
|
- sortDesc: true,
|
|
|
- showRecommendedOnly: false,
|
|
|
- categories: ['全部', '主食', '蔬菜', '水果', '肉类', '其它']
|
|
|
+ searchText: '',
|
|
|
+ knowledgeVisible: false,
|
|
|
+ knowledgeData: {},
|
|
|
+ _kbCache: {}
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
filteredFoods: function() {
|
|
|
var list = this.isEditing ? this.editFoods : this.foods
|
|
|
- if (this.selectedCategory !== '全部') {
|
|
|
+ var kw = (this.searchText || '').trim()
|
|
|
+ if (kw) {
|
|
|
list = list.filter(function(item) {
|
|
|
- return (item.category || '') === this.selectedCategory
|
|
|
- }.bind(this))
|
|
|
- }
|
|
|
- if (this.showRecommendedOnly) {
|
|
|
- list = list.filter(function(item) {
|
|
|
- return (item.score || 0) >= 5
|
|
|
+ var name = item.foodName || item.name || ''
|
|
|
+ return name.indexOf(kw) >= 0
|
|
|
})
|
|
|
}
|
|
|
- list = [].concat(list)
|
|
|
- list.sort(function(a, b) {
|
|
|
- var sa = a.score || 0
|
|
|
- var sb = b.score || 0
|
|
|
- return this.sortDesc ? sb - sa : sa - sb
|
|
|
- }.bind(this))
|
|
|
return list
|
|
|
}
|
|
|
},
|
|
|
onLoad: function(options) {
|
|
|
+ // 不传 reportId 时取当前用户最新报告的食材推荐
|
|
|
this.reportId = options.reportId ? parseInt(options.reportId) : null
|
|
|
- if (this.reportId) {
|
|
|
- this.loadData()
|
|
|
- }
|
|
|
+ this.loadData()
|
|
|
},
|
|
|
methods: {
|
|
|
goBack: function() {
|
|
|
uni.navigateBack()
|
|
|
},
|
|
|
+ /** 推荐值 → 背景色分级:<-30 红,>30 绿,中间每 10 换一档 */
|
|
|
+ _scoreBand: function(score) {
|
|
|
+ var s = parseFloat(score)
|
|
|
+ if (isNaN(s)) return 'null'
|
|
|
+ if (s < -30) return 'neg3'
|
|
|
+ if (s < -20) return 'neg2'
|
|
|
+ if (s < -10) return 'neg1'
|
|
|
+ if (s < 0) return 'neg0'
|
|
|
+ if (s < 10) return 'pos0'
|
|
|
+ if (s < 20) return 'pos1'
|
|
|
+ if (s < 30) return 'pos2'
|
|
|
+ return 'pos3'
|
|
|
+ },
|
|
|
loadData: function() {
|
|
|
var self = this
|
|
|
- getHealthFoods(this.reportId).then(function(res) {
|
|
|
+ getHealthFoods(self.reportId).then(function(res) {
|
|
|
if (res.code === 200 && res.data) {
|
|
|
- // 后端 Food 实体字段:name, category, score, protein, fat, carbs, fiber, energyKj, status
|
|
|
- // 兼容 foodName(旧 draft)和 name(实体),carb/carbs 双字段
|
|
|
- self.foods = (res.data || []).map(function(item) {
|
|
|
+ // 后端 Food 实体字段:name, category, score;兼容 foodName(旧 draft)
|
|
|
+ self.foods = (Array.isArray(res.data) ? res.data : []).map(function(item) {
|
|
|
return {
|
|
|
- foodName: item.name || item.foodName || item.name,
|
|
|
+ foodName: item.name || item.foodName || '',
|
|
|
name: item.name || item.foodName || '',
|
|
|
score: item.score != null ? parseInt(item.score, 10) : 0,
|
|
|
category: item.category || '其它',
|
|
|
- protein: item.protein != null ? parseFloat(item.protein) : 0,
|
|
|
- fat: item.fat != null ? parseFloat(item.fat) : 0,
|
|
|
- carb: item.carbs != null ? parseFloat(item.carbs) : (item.carb != null ? parseFloat(item.carb) : 0),
|
|
|
- fiber: item.fiber != null ? parseFloat(item.fiber) : 0,
|
|
|
- energy: item.energyKj != null ? parseFloat(item.energyKj) : (item.energy != null ? parseFloat(item.energy) : 0)
|
|
|
+ protein: item.protein != null ? parseFloat(item.protein) : null,
|
|
|
+ fat: item.fat != null ? parseFloat(item.fat) : null,
|
|
|
+ carb: item.carbs != null ? parseFloat(item.carbs) : (item.carb != null ? parseFloat(item.carb) : null),
|
|
|
+ fiber: item.fiber != null ? parseFloat(item.fiber) : null,
|
|
|
+ energy: item.energyKj != null ? parseFloat(item.energyKj) : (item.energy != null ? parseFloat(item.energy) : null)
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
+ showFoodKnowledge: function(item) {
|
|
|
+ var self = this
|
|
|
+ var name = item.foodName || item.name || ''
|
|
|
+ if (!name) return
|
|
|
+ // 本地营养数据优先(已在列表里)
|
|
|
+ var local = {
|
|
|
+ itemName: name,
|
|
|
+ category: item.category || '',
|
|
|
+ protein: item.protein,
|
|
|
+ fat: item.fat,
|
|
|
+ carbs: item.carb,
|
|
|
+ fiber: item.fiber,
|
|
|
+ energy: item.energy,
|
|
|
+ description: ''
|
|
|
+ }
|
|
|
+ // 有固定营养数据直接展示,否则查知识库
|
|
|
+ if (item.protein != null || item.fat != null || item.carbs != null) {
|
|
|
+ self.knowledgeData = local
|
|
|
+ self.knowledgeVisible = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 查知识库
|
|
|
+ var cacheKey = 'food:' + name
|
|
|
+ if (self._kbCache[cacheKey]) {
|
|
|
+ self.knowledgeData = self._kbCache[cacheKey]
|
|
|
+ self.knowledgeVisible = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ queryFoodKnowledge(name).then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ var d = res.data
|
|
|
+ var kd = {
|
|
|
+ itemName: d['名称'] || name,
|
|
|
+ category: d['分类'] || local.category || '',
|
|
|
+ protein: d['蛋白g'] != null ? d['蛋白g'] : null,
|
|
|
+ fat: d['脂肪g'] != null ? d['脂肪g'] : null,
|
|
|
+ carbs: d['碳水化合物g'] != null ? d['碳水化合物g'] : null,
|
|
|
+ fiber: d['总膳食纤维g'] != null ? d['总膳食纤维g'] : null,
|
|
|
+ energy: d['能量KJ'] != null ? d['能量KJ'] : null,
|
|
|
+ description: d['说明'] || ''
|
|
|
+ }
|
|
|
+ self._kbCache[cacheKey] = kd
|
|
|
+ self.knowledgeData = kd
|
|
|
+ } else {
|
|
|
+ self.knowledgeData = local
|
|
|
+ }
|
|
|
+ self.knowledgeVisible = true
|
|
|
+ }).catch(function() {
|
|
|
+ self.knowledgeData = local
|
|
|
+ self.knowledgeVisible = true
|
|
|
+ })
|
|
|
+ },
|
|
|
+ closeKnowledge: function() {
|
|
|
+ this.knowledgeVisible = false
|
|
|
+ this.knowledgeData = {}
|
|
|
+ },
|
|
|
toggleEdit: function() {
|
|
|
this.isEditing = true
|
|
|
this.editFoods = JSON.parse(JSON.stringify(this.foods))
|
|
|
@@ -151,30 +240,67 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
.page { min-height: 100vh; background: #f8f8f8; }
|
|
|
-.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
|
|
|
-.nav-back { padding: 10rpx 0; }
|
|
|
-.back-text { font-size: 28rpx; color: #4A9BD7; }
|
|
|
-.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
|
|
|
-.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
|
|
|
-.filter-bar { background: #fff; padding: 10rpx 20rpx; white-space: nowrap; }
|
|
|
-.filter-item { display: inline-block; padding: 12rpx 28rpx; margin: 0 10rpx; border-radius: 30rpx; font-size: 26rpx; background: #f0f0f0; color: #666; }
|
|
|
-.filter-active { background: #4A9BD7; color: #fff; }
|
|
|
-.sort-bar { display: flex; justify-content: space-between; padding: 16rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; }
|
|
|
-.sort-text { font-size: 24rpx; color: #999; }
|
|
|
-.content { padding: 20rpx 30rpx; height: calc(100vh - 200rpx); }
|
|
|
-.food-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
|
|
-.food-header { display: flex; align-items: center; margin-bottom: 12rpx; }
|
|
|
-.food-name { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
|
|
|
-.food-cat-tag { padding: 4rpx 16rpx; background: #f0f0f0; border-radius: 20rpx; font-size: 22rpx; color: #666; }
|
|
|
-.food-detail-row { margin-bottom: 6rpx; }
|
|
|
-.food-nutrition { font-size: 24rpx; color: #999; }
|
|
|
-.food-edit-row { display: flex; align-items: center; margin-bottom: 6rpx; }
|
|
|
-.edit-label { font-size: 26rpx; color: #666; margin-right: 12rpx; }
|
|
|
-.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 120rpx; }
|
|
|
-.food-score { margin-top: 8rpx; font-size: 24rpx; }
|
|
|
-.score-good { color: #10B981; }
|
|
|
-.score-normal { color: #999; }
|
|
|
+
|
|
|
+/* 搜索框 */
|
|
|
+.search-bar { padding: 20rpx 30rpx; background: #fff; }
|
|
|
+.search-input-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-radius: 40rpx;
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+}
|
|
|
+.search-icon { font-size: 28rpx; margin-right: 12rpx; }
|
|
|
+.search-input { flex: 1; font-size: 28rpx; }
|
|
|
+.search-clear { font-size: 28rpx; color: #999; padding: 0 8rpx; }
|
|
|
+
|
|
|
+.content { padding: 20rpx 30rpx; height: calc(100vh - 120rpx); }
|
|
|
+
|
|
|
+.food-card {
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
|
|
|
+}
|
|
|
+/* 推荐值背景色分级:<-30 深红,>30 深绿,中间每10换一档 */
|
|
|
+.score-band-neg3 { background: #C62828; }
|
|
|
+.score-band-neg2 { background: #E65100; }
|
|
|
+.score-band-neg1 { background: #F57C00; }
|
|
|
+.score-band-neg0 { background: #F9A825; }
|
|
|
+.score-band-pos0 { background: #AFB42B; }
|
|
|
+.score-band-pos1 { background: #7CB342; }
|
|
|
+.score-band-pos2 { background: #43A047; }
|
|
|
+.score-band-pos3 { background: #1B5E20; }
|
|
|
+.score-band-null { background: #f0f0f0; }
|
|
|
+
|
|
|
+.food-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
|
|
|
+.food-name { font-size: 30rpx; font-weight: 600; color: #fff; flex: 1; }
|
|
|
+.food-cat-tag { padding: 4rpx 16rpx; background: rgba(255,255,255,0.3); border-radius: 20rpx; font-size: 22rpx; color: #fff; }
|
|
|
+
|
|
|
+.food-score-row { display: flex; align-items: baseline; gap: 12rpx; }
|
|
|
+.food-score-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }
|
|
|
+.food-score-value { font-size: 40rpx; font-weight: 700; color: #fff; }
|
|
|
+
|
|
|
+.food-edit-row { display: flex; align-items: center; margin-top: 8rpx; }
|
|
|
+.edit-label { font-size: 26rpx; color: #fff; margin-right: 12rpx; }
|
|
|
+.edit-input { border: 1rpx solid #fff; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 140rpx; color: #fff; background: rgba(255,255,255,0.2); }
|
|
|
+
|
|
|
.empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
|
|
|
+
|
|
|
+/* 知识库弹窗 */
|
|
|
+.knowledge-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
|
|
+.knowledge-popup { width: 80%; background: #fff; border-radius: 20rpx; overflow: hidden; }
|
|
|
+.knowledge-header { display: flex; align-items: center; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
|
|
|
+.knowledge-title { font-size: 32rpx; font-weight: 600; color: #333; }
|
|
|
+.knowledge-close { font-size: 32rpx; color: #999; padding: 0 8rpx; }
|
|
|
+.knowledge-body { padding: 30rpx; max-height: 60vh; overflow-y: auto; }
|
|
|
+.knowledge-row { display: flex; align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
|
|
+.knowledge-label { font-size: 26rpx; color: #999; width: 120rpx; }
|
|
|
+.knowledge-value { font-size: 26rpx; color: #333; flex: 1; }
|
|
|
+.knowledge-desc { margin-top: 16rpx; }
|
|
|
+.knowledge-desc-text { font-size: 26rpx; color: #666; line-height: 1.6; }
|
|
|
+
|
|
|
+.edit-fab { position: fixed; right: 30rpx; bottom: 40rpx; background: #4A9BD7; color: #fff; border-radius: 40rpx; padding: 16rpx 32rpx; font-size: 26rpx; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
|
|
|
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; }
|
|
|
.btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
|
|
|
.btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
|