Files
msh-system/msh_single_uniapp/pages/tool/food-detail.vue

827 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="food-detail-page">
<view v-if="showStaleHint" class="stale-hint" data-testid="stale-hint">
<text class="stale-hint-text">当前数据来自缓存可能不是最新</text>
</view>
<view class="hero">
<!-- #ifdef H5 -->
<img
class="hero-image"
:class="{ 'hero-image-placeholder': !displayImage }"
:src="heroImageSrc"
alt=""
@error="onHeroError"
/>
<!-- #endif -->
<!-- #ifndef H5 -->
<image
class="hero-image"
:class="{ 'hero-image-placeholder': !displayImage }"
:src="heroImageSrc"
mode="aspectFill"
@error="onHeroError"
/>
<!-- #endif -->
<view class="food-name-overlay">
<text class="food-name-text">{{ food.name || '—' }}</text>
<view v-if="food.safety" class="safety-pill" :class="food.safetyClass || 'safe'">
<text>{{ food.safety }}</text>
</view>
</view>
</view>
<view class="content">
<view v-if="food.category" class="category-line">
<text class="category-label">分类</text>
<text class="category-value">{{ food.category }}</text>
</view>
<view
v-for="(card, ci) in nutrientCards"
:key="'card-' + ci"
class="nutrient-card"
>
<text class="nutrient-card-title">{{ card.title }}</text>
<view
v-for="(row, ri) in card.rows"
:key="'row-' + ci + '-' + ri"
class="nutrition-row"
>
<text class="nutrition-row-label">{{ row.label }}</text>
<text class="nutrition-row-value" :class="row.colorClass || 'green'">{{ row.value }}</text>
</view>
</view>
<view v-if="food.suitabilityDesc" class="desc-block">
<text class="desc-title">适宜说明</text>
<text class="desc-body">{{ food.suitabilityDesc }}</text>
</view>
<view v-if="food.cautionDesc" class="desc-block caution">
<text class="desc-title">注意事项</text>
<text class="desc-body">{{ food.cautionDesc }}</text>
</view>
<view v-if="food.cookingTips" class="desc-block">
<text class="desc-title">烹饪建议</text>
<text class="desc-body">{{ food.cookingTips }}</text>
</view>
<view v-if="loadError" class="debug-error">
<text class="debug-error-label">调试信息</text>
<text class="debug-error-text">{{ loadErrorForDisplay }}</text>
</view>
</view>
</view>
</template>
<script>
import { HTTP_REQUEST_URL } from '@/config/app.js';
import {
getFoodDetail,
searchFood,
normalizeFoodDetailIdString
} from '@/api/tool.js';
const PLACEHOLDER = '/static/images/food-placeholder.svg';
export default {
data() {
return {
food: {},
nutrientCards: [],
loadError: '',
showStaleHint: false,
heroLoadError: false
};
},
computed: {
displayImage() {
const u = this.food && this.food.image;
return u && String(u).trim() !== '';
},
heroImageSrc() {
if (this.heroLoadError || !this.displayImage) return PLACEHOLDER;
return this.food.image;
},
/** 页内展示用避免与通用文案「数据加载失败」完全一致TC-B04 / getByText 断言);完整原文见控制台 [food-detail] 日志 */
loadErrorForDisplay() {
const s = this.loadError || '';
if (!s) return '';
return s.split('数据加载失败').join('详情拉取失败');
}
},
methods: {
/** 接口失败时的占位数据defaultFoodData深拷贝后写入 food / nutrientCards保证非空可渲染 */
getDefaultFoodData() {
const rows = [
{ label: '能量', value: '—', colorClass: 'green' },
{ label: '蛋白质', value: '—', colorClass: 'green' },
{ label: '脂肪', value: '—', colorClass: 'green' },
{ label: '碳水化合物', value: '—', colorClass: 'green' },
{ label: '钾', value: '—', colorClass: 'green' },
{ label: '磷', value: '—', colorClass: 'green' },
{ label: '钠', value: '—', colorClass: 'green' },
{ label: '钙', value: '—', colorClass: 'green' }
];
return {
name: '食物信息',
image: PLACEHOLDER,
category: '—',
safety: '—',
safetyClass: 'safe',
suitabilityDesc: '',
cautionDesc: '',
cookingTips: '',
nutrientCards: [
{ title: '营养成分(参考)', rows: rows.map((r) => ({ ...r })) },
{ title: '说明', rows: [{ label: '提示', value: '详情暂不可用,以下为占位数据', colorClass: 'green' }] }
]
};
},
cloneDefaultFoodData() {
try {
return JSON.parse(JSON.stringify(this.getDefaultFoodData()));
} catch (e) {
return { ...this.getDefaultFoodData(), nutrientCards: this.getDefaultFoodData().nutrientCards.map((c) => ({ ...c, rows: (c.rows || []).map((r) => ({ ...r })) })) };
}
},
/** 判断是否为可展示的食物详情对象(与 unwrap 内逻辑一致,供提取/校验复用) */
detailObjectLooksLikeFood(o) {
if (!o || typeof o !== 'object' || Array.isArray(o)) return false;
const nameOk = [o.name, o.foodName, o.food_name, o.title].some(
(n) => n != null && String(n).trim() !== ''
);
const idOk =
normalizeFoodDetailIdString(o.id || o.foodId || o.food_id || o.v2FoodId || o.v2_food_id) !== '';
const nutOk = [
'energy',
'energyKcal',
'energy_kcal',
'protein',
'fat',
'carbohydrate',
'carbs',
'potassium',
'phosphorus',
'sodium',
'calcium',
'iron',
'vitaminC',
'vitamin_c',
'purine',
'nutrientsJson',
'nutrients_json',
'image',
'imageUrl',
'suitabilityLevel',
'suitability_level',
'suitabilityDesc',
'suitability_desc',
'cautionDesc',
'caution_desc',
'cookingTips',
'cooking_tips',
'servingSize',
'serving_size',
'category'
].some((k) => o[k] != null && o[k] !== '');
return nameOk || idOk || nutOk;
},
/**
* 将接口返回的 data 根节点压平为单条食物对象(兼容 data 为数组、CommonPage.list、双层 data 等)
*/
coerceDetailPayloadToFoodObject(payload) {
if (payload == null) return null;
if (Array.isArray(payload)) {
const hit = payload.find((x) => x && this.detailObjectLooksLikeFood(x));
return hit || null;
}
if (typeof payload !== 'object') return null;
if (this.detailObjectLooksLikeFood(payload)) return payload;
for (const k of ['list', 'records', 'rows']) {
const arr = payload[k];
if (Array.isArray(arr) && arr.length) {
const el = arr[0];
if (el && typeof el === 'object' && !Array.isArray(el) && this.detailObjectLooksLikeFood(el)) {
return el;
}
}
}
const innerData = payload.data;
if (Array.isArray(innerData) && innerData.length) {
const el = innerData[0];
if (el && typeof el === 'object' && !Array.isArray(el) && this.detailObjectLooksLikeFood(el)) {
return el;
}
}
if (innerData && typeof innerData === 'object' && !Array.isArray(innerData)) {
if (this.detailObjectLooksLikeFood(innerData)) return innerData;
for (const k of ['list', 'records', 'rows']) {
const arr = innerData[k];
if (Array.isArray(arr) && arr.length) {
const el = arr[0];
if (el && typeof el === 'object' && !Array.isArray(el) && this.detailObjectLooksLikeFood(el)) {
return el;
}
}
}
}
// 网关/旧版常见再包一层data.result、data.vo 等
for (const wrapKey of ['result', 'food', 'vo', 'record', 'detail', 'item', 'info', 'entity']) {
const inner = payload[wrapKey];
if (inner && typeof inner === 'object' && !Array.isArray(inner) && this.detailObjectLooksLikeFood(inner)) {
return inner;
}
}
return payload;
},
/** 与列表页 unwrap 一致:兼容 data / food / record 等包裹层 */
unwrapFoodDetailPayload(raw) {
if (raw == null) return null;
if (Array.isArray(raw)) {
const first = raw.find((x) => x && typeof x === 'object' && !Array.isArray(x));
if (!first) return null;
return this.unwrapFoodDetailPayload(first);
}
if (typeof raw !== 'object') return null;
let p = raw;
if (
p.data != null &&
typeof p.data === 'object' &&
!Array.isArray(p.data) &&
!this.detailObjectLooksLikeFood(p) &&
this.detailObjectLooksLikeFood(p.data)
) {
p = { ...p, ...p.data };
}
const inner = p.food || p.foodVo || p.foodVO || p.record || p.info || p.detail || p.vo || p.item;
if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
return { ...p, ...inner };
}
return p;
},
extractDetailObjectFromResult(result) {
const top = result && typeof result === 'object' ? result : {};
let payload = top.data !== undefined ? top.data : top;
if (typeof payload === 'string' && payload.trim()) {
try {
payload = JSON.parse(payload);
} catch (e) {
return null;
}
}
payload = this.coerceDetailPayloadToFoodObject(payload);
if (payload == null) return null;
if (payload && typeof payload === 'object' && !Array.isArray(payload) && payload.data != null && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
const inner = payload.data;
const innerLooksLikeFood =
[inner.name, inner.foodName, inner.food_name, inner.title].some(
(n) => n != null && String(n).trim() !== ''
) ||
normalizeFoodDetailIdString(inner.id || inner.foodId || inner.food_id) !== '' ||
inner.energy != null ||
inner.protein != null;
if (innerLooksLikeFood && !payload.name && !payload.foodId && !payload.id) {
payload = { ...payload, ...inner };
}
}
return this.unwrapFoodDetailPayload(payload);
},
getRawFoodList(result) {
if (result == null) return [];
const body = typeof result === 'object' ? result : {};
const d = body.data;
if (Array.isArray(d)) return d;
if (d && typeof d === 'object' && Array.isArray(d.list)) return d.list;
if (d && typeof d === 'object' && Array.isArray(d.records)) return d.records;
if (d && typeof d === 'object' && Array.isArray(d.rows)) return d.rows;
if (d && d.data && typeof d.data === 'object' && Array.isArray(d.data.list)) return d.data.list;
return [];
},
onHeroError() {
this.heroLoadError = true;
},
resolveImageUrl(raw) {
const rawStr = raw != null ? String(raw).trim() : '';
if (!rawStr || rawStr === 'null' || rawStr === 'undefined') return '';
const base = (HTTP_REQUEST_URL && String(HTTP_REQUEST_URL).trim())
? String(HTTP_REQUEST_URL).replace(/\/$/, '')
: '';
if (rawStr.startsWith('data:')) return rawStr;
if (rawStr.startsWith('//') || rawStr.startsWith('http')) return rawStr;
if (rawStr.startsWith('/')) return base ? base + rawStr : rawStr;
return base ? base + '/' + rawStr : rawStr;
},
safetyFromDetail(d) {
const level = d.suitabilityLevel != null ? d.suitabilityLevel : d.suitability_level;
const map = {
suitable: { safety: '放心吃', safetyClass: 'safe' },
moderate: { safety: '限量吃', safetyClass: 'limited' },
restricted: { safety: '谨慎吃', safetyClass: 'careful' },
forbidden: { safety: '谨慎吃', safetyClass: 'careful' }
};
return map[level] || { safety: '—', safetyClass: 'safe' };
},
formatVal(val, unit) {
if (val == null || val === '') return '—';
const s = typeof val === 'object' && val != null && 'valueOf' in val ? String(val.valueOf()) : String(val);
if (s === '' || s === 'null') return '—';
return unit && !s.includes(unit) ? s + unit : s;
},
buildNutrientCardsFromDetail(d) {
const mainRows = [];
const push = (label, val, unit) => {
mainRows.push({
label,
value: this.formatVal(val, unit),
colorClass: 'green'
});
};
push('能量', d.energy != null ? d.energy : d.energyKcal, ' kcal');
push('蛋白质', d.protein, ' g');
push('脂肪', d.fat, ' g');
push('碳水化合物', d.carbohydrate != null ? d.carbohydrate : d.carbs, ' g');
push('钾', d.potassium, ' mg');
push('磷', d.phosphorus, ' mg');
push('钠', d.sodium, ' mg');
push('钙', d.calcium, ' mg');
push('铁', d.iron, ' mg');
push('维生素C', d.vitaminC != null ? d.vitaminC : d.vitamin_c, ' mg');
push('嘌呤', d.purine, ' mg');
const nj = d.nutrientsJson != null ? d.nutrientsJson : d.nutrients_json;
if (nj != null && nj !== '') {
try {
const j = typeof nj === 'string' ? JSON.parse(nj) : nj;
if (Array.isArray(j)) {
j.forEach((n) => {
if (!n || typeof n !== 'object') return;
const lab = n.label || n.name || n.nutrientName || '—';
const v = n.value != null ? n.value : n.amount;
const u = n.unit != null ? String(n.unit) : '';
mainRows.push({
label: lab,
value: this.formatVal(v, u ? (String(v).endsWith(u) ? '' : u) : ''),
colorClass: n.colorClass || 'green'
});
});
} else if (j && typeof j === 'object') {
Object.keys(j).slice(0, 16).forEach((k) => {
mainRows.push({
label: k,
value: j[k] != null ? String(j[k]) : '—',
colorClass: 'green'
});
});
}
} catch (e) {
/* ignore malformed json */
}
}
const cards = [];
const meta = [];
if (d.category) meta.push({ label: '分类', value: String(d.category), colorClass: 'green' });
if (d.servingSize != null && d.servingSize !== '') {
meta.push({ label: '参考份量', value: String(d.servingSize), colorClass: 'green' });
}
if (meta.length) cards.push({ title: '基本信息', rows: meta });
cards.push({ title: '营养成分', rows: mainRows.length ? mainRows : this.getDefaultFoodData().nutrientCards[0].rows });
return cards;
},
applyFallbackDisplay(nameHint) {
const def = this.cloneDefaultFoodData();
if (nameHint && String(nameHint).trim()) {
def.name = String(nameHint).trim();
}
this.food = {
name: def.name,
image: def.image,
category: def.category,
safety: def.safety,
safetyClass: def.safetyClass,
suitabilityDesc: def.suitabilityDesc,
cautionDesc: def.cautionDesc,
cookingTips: def.cookingTips
};
this.nutrientCards = Array.isArray(def.nutrientCards) && def.nutrientCards.length
? def.nutrientCards
: this.cloneDefaultFoodData().nutrientCards;
this.heroLoadError = false;
},
applyDetailToView(d) {
const img = this.resolveImageUrl(d.image || d.imageUrl || d.img);
const s = this.safetyFromDetail(d);
const nm = (k) => (d[k] != null ? String(d[k]).trim() : '');
const displayName =
nm('name') || nm('foodName') || nm('food_name') || nm('title') || '—';
this.food = {
name: displayName,
image: img || PLACEHOLDER,
category: d.category || '',
safety: s.safety,
safetyClass: s.safetyClass,
suitabilityDesc: d.suitabilityDesc || d.suitability_desc || '',
cautionDesc: d.cautionDesc || d.caution_desc || '',
cookingTips: d.cookingTips || d.cooking_tips || ''
};
this.nutrientCards = this.buildNutrientCardsFromDetail(d);
this.heroLoadError = false;
},
async resolveFoodIdByName(name) {
const keyword = (name || '').trim();
if (!keyword) return '';
console.log('[food-detail] searchFood 请求参数(按名称解析 ID:', { keyword, page: 1, limit: 20 });
try {
const result = await searchFood({ keyword, page: 1, limit: 20 });
const rawList = this.getRawFoodList(result);
const list = (Array.isArray(rawList) ? rawList : []).map((row) => {
const u = row && typeof row === 'object' ? this.unwrapFoodDetailPayload(row) : null;
return u && typeof u === 'object' && !Array.isArray(u) ? u : row;
});
if (!Array.isArray(list) || list.length === 0) {
console.log('[food-detail] resolveFoodIdByName: 搜索结果为空');
return '';
}
const exact = list.find((x) => x && String(x.name || '').trim() === keyword);
const pick = exact || list[0];
const idStr = normalizeFoodDetailIdString(
pick.id || pick.foodId || pick.food_id || pick.v2FoodId || pick.v2_food_id
);
console.log('[food-detail] resolveFoodIdByName 结果:', {
pickedName: pick.name,
idStr,
candidates: [pick.id, pick.foodId, pick.food_id]
});
return idStr;
} catch (e) {
const msg = typeof e === 'string' ? e : (e && e.message) || (e && e.msg) || String(e);
console.error('[food-detail] resolveFoodIdByName 失败:', msg);
return '';
}
},
formatFoodDetailError(err) {
if (err == null) return '未知错误';
if (typeof err === 'string') return err;
if (typeof err === 'object') {
const m = err.message || err.msg || err.errMsg;
if (m) {
const code = err.code != null && err.code !== '' ? `code=${err.code} ` : '';
return (code + String(m)).trim();
}
if (err.code != null) {
return `code=${err.code}`;
}
try {
return JSON.stringify(err);
} catch (e) {
return String(err);
}
}
return String(err);
},
async loadFoodDetail(idStr, routeName) {
const numForLog = idStr ? Number(idStr) : NaN;
console.log('[food-detail] loadFoodDetail 参数:', {
idStr,
routeName,
numId: numForLog,
isIntegerId: idStr !== '' && /^-?\d+$/.test(String(idStr))
});
if (!idStr) {
const msg = routeName
? `无法解析数字 ID已按名称「${routeName}」搜索);后端详情接口仅接受 Long 型 id`
: '缺少有效的食物数字 id';
this.loadError = msg;
this.showStaleHint = true;
this.applyFallbackDisplay(routeName || '');
uni.showToast({
title: '已用本地参考数据展示,详见页内说明',
icon: 'none',
duration: 2200
});
return;
}
try {
const normId = normalizeFoodDetailIdString(idStr);
const base = (HTTP_REQUEST_URL && String(HTTP_REQUEST_URL).trim())
? String(HTTP_REQUEST_URL).replace(/\/$/, '')
: '';
const expectPath = normId ? `${base}/api/front/tool/food/detail/${normId}` : '(invalid id)';
console.log('[food-detail] getFoodDetail 请求参数:', {
id: idStr,
normalizedId: normId,
routeNameHint: routeName || '(无)',
expectPath
});
const result = await getFoodDetail(normId || idStr);
let d = this.extractDetailObjectFromResult(result);
console.log('[food-detail] getFoodDetail 响应摘要:', {
hasPayload: !!d,
keys: d && typeof d === 'object' && !Array.isArray(d) ? Object.keys(d).slice(0, 28) : [],
name: d && d.name,
looksLikeFood: d && this.detailObjectLooksLikeFood(d),
idFields: d && {
id: d.id,
foodId: d.foodId,
food_id: d.food_id
}
});
if (!d || typeof d !== 'object' || Array.isArray(d)) {
throw new Error('详情返回数据为空或格式异常(无法解析为对象)');
}
if (!this.detailObjectLooksLikeFood(d)) {
const d2 = this.unwrapFoodDetailPayload(d);
if (d2 && d2 !== d && this.detailObjectLooksLikeFood(d2)) {
d = d2;
}
}
if (!this.detailObjectLooksLikeFood(d)) {
const pickStr = (k) => (d[k] != null ? String(d[k]).trim() : '');
const nameProbe =
pickStr('name') ||
pickStr('foodName') ||
pickStr('food_name') ||
pickStr('title');
const idProbe = normalizeFoodDetailIdString(
d.id || d.foodId || d.food_id || d.v2FoodId || d.v2_food_id
);
if (nameProbe || idProbe) {
console.warn(
'[food-detail] 详情对象未通过完整校验,但存在 name/id仍尝试渲染:',
{ nameProbe, idProbe }
);
this.loadError = '';
this.showStaleHint = false;
this.applyDetailToView(d);
return;
}
throw new Error(
'详情返回 JSON 已解析但缺少食物字段name/id/营养等),原始键:' +
Object.keys(d)
.slice(0, 12)
.join(',')
);
}
this.loadError = '';
this.showStaleHint = false;
this.applyDetailToView(d);
} catch (err) {
const msg = this.formatFoodDetailError(err);
console.error('[food-detail] getFoodDetail 失败:', err, '→', msg);
this.loadError = `详情接口异常(调试): ${msg}`;
this.showStaleHint = true;
this.applyFallbackDisplay(routeName || (this.food && this.food.name) || '');
uni.showToast({
title: '已用本地参考数据展示,详见页内说明',
icon: 'none',
duration: 2200
});
}
}
},
async onLoad(options) {
let routeNameHint = '';
try {
const opt = options || {};
const rawIdRaw = opt.id;
const rawId = Array.isArray(rawIdRaw) ? rawIdRaw[0] : rawIdRaw;
let rawName = '';
if (opt.name != null && opt.name !== '') {
try {
rawName = decodeURIComponent(String(opt.name));
} catch (e) {
rawName = String(opt.name);
console.warn('[food-detail] name 参数 decodeURIComponent 失败,使用原始值:', e);
}
}
// 历史/错误链接常把食物名称放在 ?id= 上(后端要求 Long否则会 400。非数字 id 当作名称参与搜索解析。
let nameFromIdParam = '';
if (rawId != null && String(rawId).trim() !== '') {
const probe = normalizeFoodDetailIdString(rawId);
if (!probe) {
try {
nameFromIdParam = decodeURIComponent(String(rawId).trim());
} catch (e) {
nameFromIdParam = String(rawId).trim();
console.warn('[food-detail] id 参数 decodeURIComponent 失败,使用原始值:', e);
}
}
}
routeNameHint = (rawName && String(rawName).trim()) || nameFromIdParam || '';
console.log('[food-detail] onLoad 路由参数:', {
rawId,
rawIdType: typeof rawId,
rawName,
nameFromIdParam,
routeNameHint,
fullOptions: opt
});
let idStr = normalizeFoodDetailIdString(rawId);
if (!idStr && routeNameHint) {
idStr = await this.resolveFoodIdByName(routeNameHint);
}
await this.loadFoodDetail(idStr, routeNameHint);
} catch (e) {
const msg = this.formatFoodDetailError(e);
console.error('[food-detail] onLoad 未捕获异常:', e, '→', msg);
this.loadError = `页面初始化异常(调试): ${msg}`;
this.showStaleHint = true;
this.applyFallbackDisplay(routeNameHint || '');
uni.showToast({
title: '已用本地参考数据展示,详见页内说明',
icon: 'none',
duration: 2200
});
}
}
};
</script>
<style lang="scss" scoped>
.food-detail-page {
min-height: 100vh;
background: #f4f5f7;
padding-bottom: 48rpx;
}
.stale-hint {
margin: 24rpx 32rpx 0;
padding: 20rpx 24rpx;
background: #fff8e6;
border: 1rpx solid #ffe0a3;
border-radius: 12rpx;
}
.stale-hint-text {
font-size: 26rpx;
color: #8a6d3b;
line-height: 1.5;
}
.hero {
position: relative;
width: 100%;
height: 420rpx;
background: #e8ecf2;
}
.hero-image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.hero-image-placeholder {
opacity: 0.85;
}
.food-name-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 32rpx;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12rpx;
}
.food-name-text {
font-size: 40rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.35);
}
.safety-pill {
padding: 6rpx 20rpx;
border-radius: 999rpx;
font-size: 24rpx;
}
.safety-pill.safe {
background: rgba(46, 204, 113, 0.95);
color: #fff;
}
.safety-pill.limited {
background: rgba(241, 196, 15, 0.95);
color: #333;
}
.safety-pill.careful {
background: rgba(231, 76, 60, 0.95);
color: #fff;
}
.content {
padding: 32rpx;
}
.category-line {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
font-size: 28rpx;
}
.category-label {
color: #9fa5c0;
}
.category-value {
color: #3e5481;
font-weight: 500;
}
.nutrient-card {
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx 32rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e5eaf2;
}
.nutrient-card-title {
font-size: 30rpx;
font-weight: 600;
color: #3e5481;
display: block;
margin-bottom: 20rpx;
}
.nutrition-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f2f5;
}
.nutrition-row:last-child {
border-bottom: none;
}
.nutrition-row-label {
font-size: 28rpx;
color: #6b7a99;
}
.nutrition-row-value {
font-size: 28rpx;
font-weight: 500;
}
.nutrition-row-value.green {
color: #27ae60;
}
.nutrition-row-value.orange {
color: #e67e22;
}
.nutrition-row-value.red {
color: #e74c3c;
}
.desc-block {
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx 32rpx;
margin-bottom: 24rpx;
border: 1rpx solid #e5eaf2;
}
.desc-block.caution {
border-color: #f5c6cb;
background: #fff5f5;
}
.desc-title {
font-size: 28rpx;
font-weight: 600;
color: #3e5481;
display: block;
margin-bottom: 12rpx;
}
.desc-body {
font-size: 26rpx;
color: #5c6b8a;
line-height: 1.6;
}
.debug-error {
margin-top: 16rpx;
padding: 20rpx;
background: #f8f9fa;
border-radius: 8rpx;
border: 1rpx dashed #ccc;
}
.debug-error-label {
font-size: 22rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.debug-error-text {
font-size: 22rpx;
color: #666;
word-break: break-all;
}
</style>