diff --git a/msh_single_uniapp/pages/tool/food-encyclopedia.vue b/msh_single_uniapp/pages/tool/food-encyclopedia.vue index 574285a..d6747bb 100644 --- a/msh_single_uniapp/pages/tool/food-encyclopedia.vue +++ b/msh_single_uniapp/pages/tool/food-encyclopedia.vue @@ -118,15 +118,23 @@ @click="handleFoodItemClick(index)" > - + + - @@ -371,6 +386,12 @@ export default { if (result.data != null && typeof result.data === 'object' && Array.isArray(result.data.items)) { return result.data.items; } + if (result.data != null && typeof result.data === 'object' && Array.isArray(result.data.foods)) { + return result.data.foods; + } + if (result.data != null && typeof result.data === 'object' && Array.isArray(result.data.result)) { + return result.data.result; + } // 再包一层:CommonResult.data.data.list(list 可能为 JSON 字符串) if (result.data != null && typeof result.data === 'object' && result.data.data != null && typeof result.data.data === 'object' && result.data.data.list != null) { @@ -388,6 +409,8 @@ export default { if (Array.isArray(p.results)) return p.results; if (Array.isArray(p.content)) return p.content; if (Array.isArray(p.items)) return p.items; + if (Array.isArray(p.foods)) return p.foods; + if (Array.isArray(p.result)) return p.result; if (p.data != null && typeof p.data === 'object' && !Array.isArray(p.data) && p.data.list != null) { const dNorm = this.parsePageListIfString(p.data); if (Array.isArray(dNorm.list)) return dNorm.list; @@ -438,7 +461,8 @@ export default { }; const keys = [ 'image', 'imageUrl', 'image_url', 'img', 'picture', 'pictureUrl', 'picture_url', - 'foodPicture', 'food_picture', 'coverImage', 'cover_image', 'coverUrl', 'cover_url', + 'foodPicture', 'food_picture', 'foodImageUrl', 'food_image_url', + 'coverImage', 'cover_image', 'coverUrl', 'cover_url', 'nutrientsJson', 'nutrients_json', 'nutritionJson', 'nutrition_json', 'nutrition', 'nutrients', 'nutritions', 'nutrientList', 'nutritionList', 'nutrientVoList', 'nutrient_vo_list', 'nutritionOverview', 'nutrition_overview', @@ -468,7 +492,7 @@ export default { if (!o || typeof o !== 'object' || Array.isArray(o)) return false; const nameOk = o.name != null && String(o.name).trim() !== ''; const idOk = (o.id != null && o.id !== '') || (o.foodId != null && o.foodId !== '') || (o.food_id != null && o.food_id !== ''); - const nutOk = ['image', 'imageUrl', 'image_url', 'nutrientsJson', 'nutrients_json', 'nutritionJson', 'nutrition_json', 'energy', 'protein', 'potassium', 'phosphorus', 'calcium', 'sodium'].some( + const nutOk = ['image', 'imageUrl', 'image_url', 'foodImageUrl', 'food_image_url', 'nutrientsJson', 'nutrients_json', 'nutritionJson', 'nutrition_json', 'energy', 'protein', 'potassium', 'phosphorus', 'calcium', 'sodium'].some( (k) => o[k] != null && o[k] !== '' ); return nameOk || idOk || nutOk; @@ -480,7 +504,8 @@ export default { this.restoreNonEmptyFoodMediaFields(base, raw); } const inner = base.food || base.foodVo || base.foodVO || base.foodDto || base.foodDTO - || base.v2Food || base.v2_food || base.V2Food || base.record || base.info || base.detail || base.vo || base.item || base.row || base.entity; + || base.v2Food || base.v2_food || base.V2Food || base.toolFood || base.tool_food + || base.record || base.info || base.detail || base.vo || base.item || base.row || base.entity; if (inner && typeof inner === 'object' && !Array.isArray(inner)) { // 以 inner 为主,避免外层空字段(如 image/nutrients 为空)覆盖内层真实数据 // 同时把外层标识字段补齐回结果(如 id、foodId 等) @@ -505,6 +530,7 @@ export default { const keys = [ 'id', 'foodId', 'food_id', 'foodID', 'v2FoodId', 'v2_food_id', 'toolFoodId', 'tool_food_id', 'image', 'imageUrl', 'image_url', 'img', 'picture', 'pictureUrl', 'picture_url', + 'foodImageUrl', 'food_image_url', 'photoUrl', 'photo_url', 'nutrientsJson', 'nutrients_json', 'nutritionJson', 'nutrition_json', 'nutrition', 'nutrients', 'nutritions', 'nutrientList', 'nutritionList', 'nutrientVoList', 'nutrient_vo_list', 'nutritionOverview', 'nutrition_overview', @@ -534,7 +560,7 @@ export default { } } const candidates = [ - item.image, item.imageUrl, item.image_url, + item.image, item.imageUrl, item.image_url, item.foodImageUrl, item.food_image_url, item.picture, item.pictureUrl, item.picture_url, item.foodPicture, item.food_picture, item.imagePath, item.image_path, item.fileUrl, item.file_url, @@ -599,6 +625,44 @@ export default { resolvedFoodImage(item, index) { return this.foodImageSrc(item, index); }, + /** + * 按食物分类返回 emoji 占位(test-0415 反馈4-2) + * 优先 categoryType(fruit/vegetable/...),回落到 category 中文文案匹配 + */ + categoryPlaceholderEmoji(item) { + const map = { + grain: '🌾', vegetable: '🥬', fruit: '🍎', + meat: '🍖', seafood: '🐟', dairy: '🥛', + bean: '🫘', nut: '🥜', + }; + const key = this.resolveCategoryKey(item); + return map[key] || '🍽️'; + }, + categoryPlaceholderBg(item) { + const map = { + grain: '#FFF4D6', vegetable: '#E6F7E6', fruit: '#FFE6E6', + meat: '#F4E1D2', seafood: '#DCEEF7', dairy: '#F1F1F8', + bean: '#EFE5D5', nut: '#F4E5C8', + }; + const key = this.resolveCategoryKey(item); + return map[key] || '#F2F4F8'; + }, + resolveCategoryKey(item) { + if (!item) return ''; + const t = item.categoryType || item.category_type; + if (t && typeof t === 'string') return t.toLowerCase(); + const c = item.category; + if (!c || typeof c !== 'string') return ''; + if (c.includes('谷') || c.includes('薯')) return 'grain'; + if (c.includes('蔬')) return 'vegetable'; + if (c.includes('果')) return 'fruit'; + if (c.includes('肉') || c.includes('蛋') || c.includes('禽')) return 'meat'; + if (c.includes('鱼') || c.includes('水产') || c.includes('虾') || c.includes('蟹')) return 'seafood'; + if (c.includes('奶') || c.includes('乳')) return 'dairy'; + if (c.includes('豆')) return 'bean'; + if (c.includes('坚果')) return 'nut'; + return ''; + }, hasFoodImage(item, index) { const url = this.getFoodImage(item, index); return !!(url && String(url).trim()); @@ -780,32 +844,59 @@ export default { */ firstNonEmptyNutritionArray(item) { if (!item || typeof item !== 'object') return null; - const keys = ['nutrition', 'nutrients', 'nutritions']; + const asNonEmptyArray = (v) => { + if (Array.isArray(v) && v.length > 0) return v; + if (typeof v === 'string' && v.trim()) { + try { + const j = JSON.parse(v); + if (Array.isArray(j) && j.length > 0) return j; + } catch (e) { /* ignore */ } + } + return null; + }; + const keys = [ + 'nutrition', 'nutrients', 'nutritions', + 'nutrientList', 'nutritionList', 'nutrition_list', + 'nutrientVoList', 'nutrient_vo_list', 'nutrientItems', 'nutritionItems' + ]; for (let i = 0; i < keys.length; i++) { - const arr = item[keys[i]]; - if (Array.isArray(arr) && arr.length > 0) return arr; + const hit = asNonEmptyArray(item[keys[i]]); + if (hit) return hit; } return null; }, /** 模板 .nutrition-item v-for:非空数组优先,否则 displayNutritionList(nutrientsJson/扁平字段/默认行) */ resolvedNutritionList(item) { - const mapNutRow = (n) => { - if (!n || typeof n !== 'object') return { label: '—', value: '—', colorClass: 'green' }; - const rawValue = n.value != null ? n.value : (n.amount != null ? n.amount : (n.content != null ? n.content : n.val)); - const unit = n.unit != null ? String(n.unit) : ''; - const valueStr = this.nutritionScalarToDisplayString(rawValue); - return { - label: n.label || n.name || n.title || n.key || n.nutrientName || n.nutrient_name || n.labelName || n.text || '—', - value: valueStr !== '—' && unit && !valueStr.endsWith(unit) ? (valueStr + unit) : valueStr, - colorClass: n.colorClass || n.color || 'green' + try { + const mapNutRow = (n) => { + try { + if (!n || typeof n !== 'object') return { label: '—', value: '—', colorClass: 'green' }; + const rawValue = n.value != null ? n.value : (n.amount != null ? n.amount : (n.content != null ? n.content : n.val)); + const unit = n.unit != null ? String(n.unit) : ''; + const valueStr = this.nutritionScalarToDisplayString(rawValue); + return { + label: n.label || n.name || n.title || n.key || n.nutrientName || n.nutrient_name || n.labelName || n.text || '—', + value: valueStr !== '—' && unit && !valueStr.endsWith(unit) ? (valueStr + unit) : valueStr, + colorClass: n.colorClass || n.color || 'green' + }; + } catch (e) { + return { label: '—', value: '—', colorClass: 'green' }; + } }; - }; - // 勿用 nutrients||nutrition:空数组 [] 为 truthy,会挡住后面有数据的 nutrition(BUG-003) - const arr = this.firstNonEmptyNutritionArray(item); - if (arr) { - return arr.map(mapNutRow); + // 勿用 nutrients||nutrition:空数组 [] 为 truthy,会挡住后面有数据的 nutrition(BUG-003) + const arr = this.firstNonEmptyNutritionArray(item); + if (arr) { + const mapped = arr.map(mapNutRow).filter((r) => r != null); + if (mapped.length > 0) return mapped; + } + return this.displayNutritionList(item); + } catch (e) { + return [ + { label: '能量', value: '—', colorClass: 'green' }, + { label: '蛋白质', value: '—', colorClass: 'green' }, + { label: '钾', value: '—', colorClass: 'green' } + ]; } - return this.displayNutritionList(item); }, getNutritionList(item) { if (!item) return []; @@ -1038,7 +1129,7 @@ export default { } } if (item && typeof item === 'object' && !Array.isArray(item)) { - const nests = [item.data, item.vo, item.record, item.detail].filter( + const nests = [item.data, item.vo, item.record, item.detail, item.v2Food, item.v2_food, item.food, item.toolFood, item.tool_food, item.attrs, item.attr, item.extra].filter( (x) => x && typeof x === 'object' && !Array.isArray(x) ); for (let ni = 0; ni < nests.length; ni++) { @@ -1404,9 +1495,16 @@ export default { object-fit: cover; } -/* 无图或加载失败时的占位(灰色背景,与 wrapper 一致) */ +/* 无图或加载失败时的占位(按分类着色,emoji 居中) */ .food-image-placeholder { - background: #e4e5e7; + background: #f2f4f8; + display: flex; + align-items: center; + justify-content: center; +} +.food-image-placeholder .category-emoji { + font-size: 64rpx; + line-height: 1; } .warning-badge {