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

827 lines
24 KiB
Vue
Raw Normal View History

<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>