fix: 修复帖子详情营养统计显示全为 "-" 的问题

问题原因:
1. fillNutritionStatsFromCheckin 即使全为 "-" 也会更新视图
2. buildNutritionStatsFromCheckinDetail 钾、磷被写死为 "-"
3. 字段解析不兼容(下划线命名、嵌套对象)

修复内容:
- 仅当至少一项有效值时才更新 nutritionStats
- 兼容多种字段命名(驼峰/下划线/别名)
- 支持从嵌套对象解析(nutrition/dietaryData/mealData/aiResult)
- 补全钾、磷字段解析
- 计算属性保护:全为 "-" 时视为空数组

由 Cursor CLI 检测并修复
This commit is contained in:
2026-03-05 12:08:59 +08:00
parent cb05cf1044
commit cd7b3f6b40

View File

@@ -302,9 +302,13 @@ export default {
}, },
computed: { computed: {
// 营养统计数组,用于卡片显示与列表渲染(单一数据源,避免未定义) // 营养统计数组,用于卡片显示与列表渲染(单一数据源,避免未定义)
// 若全部为占位符 "-" 则视为无有效数据不展示卡片以便显示「AI 补充营养」等
nutritionStats() { nutritionStats() {
const stats = this.postData && this.postData.nutritionStats const stats = this.postData && this.postData.nutritionStats
return Array.isArray(stats) ? stats : [] const arr = Array.isArray(stats) ? stats : []
if (arr.length === 0) return []
const allDash = arr.every(s => s.value === '-' || s.value === '')
return allDash ? [] : arr
} }
}, },
onLoad(options) { onLoad(options) {
@@ -461,29 +465,59 @@ export default {
}, },
/** /**
* 根据打卡详情接口返回的数据构建 nutritionStats蛋白质、热量、钾、磷) * 根据打卡详情接口返回的数据构建 nutritionStats热量、蛋白质、钾、磷)
* 后端返回actualEnergy/actualProtein驼峰或 actual_energy/actual_protein下划线可能嵌套在 nutrition/dietaryData/aiResult 等
*/ */
buildNutritionStatsFromCheckinDetail(detail) { buildNutritionStatsFromCheckinDetail(detail) {
if (!detail || typeof detail !== 'object') return [] if (!detail || typeof detail !== 'object') return []
const energy = detail.actualEnergy ?? detail.energy // 优先从嵌套营养对象解析(兼容 aiResult、nutrition、dietaryData 等含完整营养字段)
const protein = detail.actualProtein ?? detail.protein const nested = detail.nutrition || detail.dietaryData || detail.dietary_data || detail.mealData || detail.meal_data
return [ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
{ label: '热量(kcal)', value: energy != null ? String(energy) : '-' }, const obj = typeof nested === 'string' ? (() => { try { return JSON.parse(nested) } catch (_) { return null } })() : nested
if (obj && typeof obj === 'object') {
const stats = this.buildNutritionStatsFromNutritionObject(obj)
if (stats.length > 0 && !stats.every(s => s.value === '-')) return stats
}
}
// aiResult 可能为 JSON 字符串,内含营养字段
const aiResult = detail.aiResult || detail.ai_result
if (aiResult) {
try {
const parsed = typeof aiResult === 'string' ? JSON.parse(aiResult) : aiResult
if (parsed && typeof parsed === 'object') {
const stats = this.buildNutritionStatsFromNutritionObject(parsed)
if (stats.length > 0 && !stats.every(s => s.value === '-')) return stats
}
} catch (_) {}
}
// 顶层字段:后端 getDetail 返回 actualEnergy、actualProtein驼峰
const energy = detail.actualEnergy ?? detail.actual_energy ?? detail.energy ?? detail.energy_kcal
const protein = detail.actualProtein ?? detail.actual_protein ?? detail.protein ?? detail.protein_g ?? detail.proteinG
const pot = detail.potassiumMg ?? detail.potassium_mg ?? detail.potassium ?? detail.k
const pho = detail.phosphorusMg ?? detail.phosphorus_mg ?? detail.phosphorus ?? detail.p
const stats = [
{ label: '热量(kcal)', value: energy != null && energy !== '' ? String(energy) : '-' },
{ label: '蛋白质', value: this.formatNutritionValue(protein, 'g') }, { label: '蛋白质', value: this.formatNutritionValue(protein, 'g') },
{ label: '钾', value: '-' }, { label: '钾', value: pot != null && pot !== '' ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: '-' } { label: '磷', value: pho != null && pho !== '' ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
] ]
return stats
}, },
/** /**
* 根据打卡记录 ID 拉取打卡详情,并填充 postData.nutritionStats不阻塞主流程 * 根据打卡记录 ID 拉取打卡详情,并填充 postData.nutritionStats不阻塞主流程
* 接口路径GET tool/checkin/detail/{id},返回 CommonResult 即 { code, data: { actualEnergy, actualProtein, ... } }
*/ */
async fillNutritionStatsFromCheckin(checkInRecordId) { async fillNutritionStatsFromCheckin(checkInRecordId) {
try { try {
const res = await getCheckinDetail(checkInRecordId) const res = await getCheckinDetail(checkInRecordId)
const detail = res.data || res // 兼容res 为 { code, data } 时取 res.data部分封装直接返回 payload 则 res 即为 detail
const detail = (res && res.data !== undefined && res.data !== null) ? res.data : res
if (!detail || typeof detail !== 'object') return
const stats = this.buildNutritionStatsFromCheckinDetail(detail) const stats = this.buildNutritionStatsFromCheckinDetail(detail)
if (stats.length > 0) { // 仅当至少有一项有效值时才更新,避免展示全部为 "-" 的卡片
const hasAnyValue = stats.length > 0 && stats.some(s => s.value !== '-' && s.value !== '')
if (hasAnyValue) {
this.$set(this.postData, 'nutritionStats', stats) this.$set(this.postData, 'nutritionStats', stats)
} }
} catch (e) { } catch (e) {