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

674 lines
20 KiB
Vue
Raw Normal View History

<template>
<view class="food-detail-page">
<!-- 分享按钮 -->
<view class="share-btn-top" @tap="handleShare">
<image class="share-icon" :src="iconShare" mode="aspectFit"></image>
</view>
<!-- 数据来自缓存时的提示 -->
<view v-if="loadError" class="cache-notice">
<text>当前数据来自缓存可能不是最新</text>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 食物大图 -->
<view class="food-image-section">
<image class="food-image" :src="displayImage" mode="aspectFill"></image>
<view class="image-overlay"></view>
<view class="food-info-overlay">
<view class="food-name-overlay">{{ displayName }}</view>
<view class="food-tags">
<view class="food-tag category">{{ displayCategory }}</view>
<view class="food-tag safe">{{ displaySafetyTag }}</view>
</view>
</view>
</view>
<!-- 关键营养成分 -->
<view class="key-nutrients-section">
<view class="section-header">
<text class="section-title">关键营养成分</text>
<view class="unit-badge">每100g</view>
</view>
<view class="key-nutrients-grid">
<view
class="nutrient-card"
v-for="(nutrient, index) in displayKeyNutrients"
:key="index"
>
<text class="nutrient-name">{{ nutrient.name }}</text>
<view class="nutrient-value">
<text class="value-number">{{ nutrient.value }}</text>
<text class="value-unit">{{ nutrient.unit }}</text>
</view>
<text class="nutrient-status">{{ nutrient.status }}</text>
</view>
</view>
</view>
<!-- 营养成分表 -->
<view class="nutrition-table-section">
<view class="section-header">
<text class="section-title">营养成分表</text>
<text class="unit-text">每100g</text>
</view>
<view class="nutrition-table">
<view
class="nutrition-row"
v-for="(item, index) in displayNutritionTable"
:key="index"
>
<view class="nutrition-left">
<view class="nutrition-dot" :class="item.level"></view>
<text class="nutrition-label">{{ item.name }}</text>
<view class="nutrition-badge" :class="item.level">
<text>{{ item.levelText }}</text>
</view>
</view>
<view class="nutrition-right">
<text class="nutrition-value" :class="item.level">{{ item.value }}</text>
<text class="nutrition-unit">{{ item.unit }}</text>
</view>
</view>
</view>
</view>
<!-- 查看相似食物按钮 -->
<view class="similar-foods-btn" @click="viewSimilarFoods">
<image class="btn-icon" :src="iconSearch" mode="aspectFit"></image>
<text>查看相似食物</text>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</scroll-view>
</view>
</template>
<script>
import { getFoodDetail } from '@/api/tool.js';
export default {
data() {
return {
iconShare: 'https://www.figma.com/api/mcp/asset/f9f0d7b9-89c0-48d4-9e04-7140229e42f0',
iconSearch: 'https://www.figma.com/api/mcp/asset/aa6bb75b-0a9d-43cb-aaa4-6a71993fbd4d',
loading: false,
/** 加载失败时的具体错误信息,用于调试;有值时页面会展示「当前数据来自缓存」提示 */
loadError: '',
/** 入参 id/name用于 API 失败时用 name 填充展示 */
pageParams: { id: '', name: '' },
foodData: {
name: '',
category: '',
categoryType: '',
safetyTag: '',
image: '',
keyNutrients: [],
nutritionTable: []
},
// 默认展示数据API 未返回时使用)
defaultFoodData: {
name: '五谷香',
category: '谷薯类',
categoryType: 'grain',
safetyTag: '放心吃',
image: 'https://www.figma.com/api/mcp/asset/bf4ff04c-1322-474a-bd03-5b8a49fe33ad',
keyNutrients: [
{ name: '磷', value: '13', unit: 'mg', status: '正常' },
{ name: '钾', value: '7', unit: 'mg', status: '正常' },
{ name: '钠', value: '2', unit: 'mg', status: '正常' },
{ name: '嘌呤', value: '15', unit: 'mg', status: '正常' }
],
nutritionTable: [
{ name: '钾', value: '7', unit: 'mg', level: 'low', levelText: '低' },
{ name: '钙', value: '2', unit: 'mg', level: 'low', levelText: '低' },
{ name: '磷', value: '13', unit: 'mg', level: 'low', levelText: '低' },
{ name: '蛋白质', value: '9.9', unit: 'g', level: 'medium', levelText: '中' },
{ name: '钠', value: '2', unit: 'mg', level: 'low', levelText: '低' },
{ name: '嘌呤', value: '15', unit: 'mg', level: 'low', levelText: '低' }
]
}
}
},
computed: {
// 保证 .food-name-overlay / .nutrient-card / .nutrition-row 在 defaultFoodData 状态下也有非空数据可渲染
displayName() {
return (this.foodData && this.foodData.name) ? this.foodData.name : (this.defaultFoodData.name || '—')
},
displayCategory() {
return (this.foodData && this.foodData.category) ? this.foodData.category : (this.defaultFoodData.category || '—')
},
displaySafetyTag() {
return (this.foodData && this.foodData.safetyTag) ? this.foodData.safetyTag : (this.defaultFoodData.safetyTag || '—')
},
displayImage() {
return (this.foodData && this.foodData.image) ? this.foodData.image : (this.defaultFoodData.image || '')
},
displayKeyNutrients() {
const arr = this.foodData && this.foodData.keyNutrients
if (Array.isArray(arr) && arr.length > 0) return arr
const def = this.defaultFoodData.keyNutrients
return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', status: '—' }]
},
displayNutritionTable() {
const arr = this.foodData && this.foodData.nutritionTable
if (Array.isArray(arr) && arr.length > 0) return arr
const def = this.defaultFoodData.nutritionTable
return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }]
}
},
onLoad(options) {
options = options || {}
// 若列表误将 name 传成 id如 id=羊肉(熟)),用 id 作为展示用 name避免请求接口 400
const rawId = options.id
const rawName = options.name
const hasNonNumericId = rawId !== undefined && rawId !== '' && isNaN(Number(rawId))
const displayName = rawName || (hasNonNumericId ? rawId : '')
this.pageParams = {
id: (rawId !== undefined && rawId !== '') ? String(rawId) : '',
name: (displayName !== undefined && displayName !== '') ? String(displayName) : ''
}
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id传 name 会导致 400
console.log('[food-detail] onLoad options:', options)
console.log('[food-detail] onLoad pageParams (id/name):', this.pageParams)
const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId))
if (isNumericId) {
const numId = Number(rawId)
console.log('[food-detail] 使用数字 id 请求详情,不传 name 字段:', numId)
this.loadFoodData(numId)
} else if (this.pageParams.name) {
// 无有效 id、仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException直接展示默认数据 + 当前名称
this.loadError = '暂无该食物详情数据,展示参考数据'
this.applyDefaultFoodData(false)
try {
this.foodData.name = decodeURIComponent(this.pageParams.name)
} catch (e) {}
} else {
this.applyDefaultFoodData()
}
},
methods: {
handleShare() {
uni.showToast({
title: '分享功能开发中',
icon: 'none'
})
},
viewSimilarFoods() {
uni.navigateTo({
url: `/pages/tool/food-encyclopedia?category=${this.foodData.categoryType || 'all'}`
})
},
/** 用 defaultFoodData 填充页面,保证 keyNutrients/nutritionTable 为非空数组以便 .nutrient-card / .nutrition-row 正常渲染clearError 为 true 时顺带清空 loadError */
applyDefaultFoodData(clearError = true) {
if (clearError) this.loadError = ''
const def = this.defaultFoodData
const fallbackKey = (def.keyNutrients && def.keyNutrients.length) ? def.keyNutrients : [{ name: '—', value: '—', unit: '', status: '—' }]
const fallbackTable = (def.nutritionTable && def.nutritionTable.length) ? def.nutritionTable : [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }]
this.foodData = {
...def,
name: (def && def.name) ? def.name : '—',
category: (def && def.category) ? def.category : '—',
safetyTag: (def && def.safetyTag) ? def.safetyTag : '—',
image: (def && def.image) ? def.image : '',
keyNutrients: Array.isArray(def.keyNutrients) && def.keyNutrients.length > 0 ? [...def.keyNutrients] : [...fallbackKey],
nutritionTable: Array.isArray(def.nutritionTable) && def.nutritionTable.length > 0 ? [...def.nutritionTable] : [...fallbackTable]
}
},
/** 保证数组非空,空时返回 fallback 的副本,用于 API 解析结果避免空数组导致列表不渲染 */
ensureNonEmptyArray(arr, fallback) {
if (Array.isArray(arr) && arr.length > 0) return arr
return Array.isArray(fallback) ? [...fallback] : []
},
async loadFoodData(id) {
this.loading = true
this.loadError = ''
// 打印 API 请求参数便于确认(后端需要 Long 类型 id
console.log('[food-detail] getFoodDetail request param:', { id, type: typeof id })
try {
const res = await getFoodDetail(id)
// 打印响应结构便于确认request 成功时 resolve 的是 res.data即 { code: 200, data: {...} }
console.log('[food-detail] getFoodDetail 响应结构:', res ? { hasData: !!res.data, code: res.code, keys: Object.keys(res || {}) } : null)
const data = res.data != null ? res.data : res
console.log('[food-detail] getFoodDetail 解析后 data:', data ? { hasName: !!data.name, hasImage: !!data.image, keys: Object.keys(data) } : null)
if (data && data.name) {
// 解析API返回的食物数据
this.foodData = {
name: data.name || '',
category: data.category || data.categoryName || '',
categoryType: data.categoryType || data.categoryCode || 'all',
safetyTag: data.safetyTag || data.safety || this.getSafetyTag(data),
image: data.image || data.imageUrl || data.coverImage || this.defaultFoodData.image,
keyNutrients: this.ensureNonEmptyArray(this.parseKeyNutrients(data), this.defaultFoodData.keyNutrients),
nutritionTable: this.ensureNonEmptyArray(this.parseNutritionTable(data), this.defaultFoodData.nutritionTable)
}
} else {
// API 返回空数据,按“失败”处理:展示默认数据 + 缓存提示
const emptyMsg = (data == null || !data.name) ? '接口返回数据为空' : '缺少食物名称'
console.warn('[food-detail] 接口返回无效数据:', data)
this.loadError = emptyMsg
this.applyDefaultFoodData(false)
if (this.pageParams && this.pageParams.name) {
try {
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
} catch (e) {}
}
uni.showToast({ title: '数据加载失败', icon: 'none' })
}
} catch (error) {
// a. 将 loadError 置为具体错误信息(用于调试):兼容 Error、{ message/msg }、字符串
const errMsg = (error && (error.message || error.msg || error.errMsg))
? String(error.message || error.msg || error.errMsg)
: (typeof error === 'string' ? error : (error ? String(error) : '未知错误'))
console.error('[food-detail] 加载食物数据失败:', error)
console.error('[food-detail] loadError(用于调试):', errMsg)
this.loadError = errMsg
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面而不是空白
this.applyDefaultFoodData(false)
// 若有入参 name用其覆盖展示名称避免显示默认「五谷香」
if (this.pageParams && this.pageParams.name) {
try {
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
} catch (e) {}
}
// c. 页面通过 v-if="loadError" 显示「当前数据来自缓存,可能不是最新」;再弹出轻提示
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
getSafetyTag(data) {
// 根据营养数据判断安全标签
if (data.safetyLevel === 'safe' || data.safetyLevel === 1) return '放心吃'
if (data.safetyLevel === 'caution' || data.safetyLevel === 2) return '少量吃'
if (data.safetyLevel === 'danger' || data.safetyLevel === 3) return '不宜吃'
return '放心吃'
},
parseKeyNutrients(data) {
if (data.keyNutrients && Array.isArray(data.keyNutrients)) {
return data.keyNutrients
}
// 从详细营养数据中提取关键营养素
const nutrients = data.nutrients || data.nutritionData || {}
const keyList = []
if (nutrients.phosphorus !== undefined) keyList.push({ name: '磷', value: String(nutrients.phosphorus), unit: 'mg', status: this.getStatus(nutrients.phosphorus, 'phosphorus') })
if (nutrients.potassium !== undefined) keyList.push({ name: '钾', value: String(nutrients.potassium), unit: 'mg', status: this.getStatus(nutrients.potassium, 'potassium') })
if (nutrients.sodium !== undefined) keyList.push({ name: '钠', value: String(nutrients.sodium), unit: 'mg', status: this.getStatus(nutrients.sodium, 'sodium') })
if (nutrients.purine !== undefined) keyList.push({ name: '嘌呤', value: String(nutrients.purine), unit: 'mg', status: this.getStatus(nutrients.purine, 'purine') })
return keyList.length > 0 ? keyList : this.defaultFoodData.keyNutrients
},
parseNutritionTable(data) {
if (data.nutritionTable && Array.isArray(data.nutritionTable)) {
return data.nutritionTable
}
const nutrients = data.nutrients || data.nutritionData || {}
const table = []
const items = [
{ key: 'potassium', name: '钾', unit: 'mg', thresholds: [200, 500] },
{ key: 'calcium', name: '钙', unit: 'mg', thresholds: [100, 300] },
{ key: 'phosphorus', name: '磷', unit: 'mg', thresholds: [100, 300] },
{ key: 'protein', name: '蛋白质', unit: 'g', thresholds: [5, 15] },
{ key: 'sodium', name: '钠', unit: 'mg', thresholds: [100, 500] },
{ key: 'purine', name: '嘌呤', unit: 'mg', thresholds: [50, 150] }
]
items.forEach(item => {
const val = nutrients[item.key]
if (val !== undefined) {
const level = val <= item.thresholds[0] ? 'low' : (val <= item.thresholds[1] ? 'medium' : 'high')
const levelText = level === 'low' ? '低' : (level === 'medium' ? '中' : '高')
table.push({ name: item.name, value: String(val), unit: item.unit, level, levelText })
}
})
return table.length > 0 ? table : this.defaultFoodData.nutritionTable
},
getStatus(value, type) {
// 简易的营养素状态判断
return '正常'
}
}
}
</script>
<style lang="scss" scoped>
.food-detail-page {
min-height: 100vh;
background: #f4f5f7;
display: flex;
flex-direction: column;
}
/* 数据来自缓存提示 */
.cache-notice {
padding: 16rpx 32rpx;
background: #fff8e1;
border-bottom: 1rpx solid #ffe082;
text-align: center;
text {
font-size: 24rpx;
color: #f57c00;
}
}
/* 分享按钮 */
.share-btn-top {
position: fixed;
top: calc(var(--status-bar-height, 0) + 20rpx);
right: 32rpx;
z-index: 100;
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.share-icon {
width: 40rpx;
height: 40rpx;
}
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
padding: 0;
}
/* 食物大图区域 */
.food-image-section {
position: relative;
width: 100%;
height: 576rpx;
background: linear-gradient(180deg, #f4f5f7 0%, #ffffff 100%);
overflow: hidden;
.food-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 200rpx;
background: linear-gradient(to top, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0) 100%);
}
.food-info-overlay {
position: absolute;
bottom: 32rpx;
left: 32rpx;
right: 32rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.food-name-overlay {
font-size: 48rpx;
color: #ffffff;
font-weight: 500;
text-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.15);
}
.food-tags {
display: flex;
gap: 16rpx;
}
.food-tag {
height: 48rpx;
border-radius: 12rpx;
padding: 8rpx 24rpx;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
&.category {
background: rgba(255, 255, 255, 0.95);
color: #ff6b35;
}
&.safe {
background: rgba(255, 107, 53, 0.9);
color: #ffffff;
}
}
}
/* 关键营养成分 */
.key-nutrients-section {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
.section-title {
font-size: 32rpx;
color: #2e3e5c;
font-weight: 500;
}
.unit-badge {
background: #f4f5f7;
border-radius: 16rpx;
padding: 8rpx 24rpx;
font-size: 24rpx;
color: #9fa5c0;
}
.unit-text {
font-size: 24rpx;
color: #9fa5c0;
}
}
.key-nutrients-grid {
display: flex;
gap: 16rpx;
justify-content: space-between;
}
.nutrient-card {
background: #ffffff;
border: 2rpx solid #d0dbea;
border-radius: 32rpx;
padding: 34rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
flex: 1;
min-width: 0;
.nutrient-name {
font-size: 40rpx;
color: #ff6b35;
font-weight: 500;
}
.nutrient-value {
display: flex;
align-items: baseline;
gap: 4rpx;
.value-number {
font-size: 24rpx;
color: #9fa5c0;
}
.value-unit {
font-size: 20rpx;
color: #9fa5c0;
}
}
.nutrient-status {
font-size: 20rpx;
color: #ff6b35;
}
}
/* 营养成分表 */
.nutrition-table-section {
padding: 0 32rpx 32rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.nutrition-table {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 32rpx;
overflow: hidden;
}
.nutrition-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 26rpx 32rpx;
border-bottom: 1rpx solid #d0dbea;
&:last-child {
border-bottom: none;
}
}
.nutrition-left {
display: flex;
align-items: center;
gap: 24rpx;
flex: 1;
min-width: 0;
}
.nutrition-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
flex-shrink: 0;
&.low {
background: #ff6b35;
}
&.medium {
background: #ffa500;
}
&.high {
background: #e53935;
}
}
.nutrition-label {
font-size: 28rpx;
color: #2e3e5c;
flex-shrink: 0;
}
.nutrition-badge {
height: 34rpx;
border-radius: 12rpx;
padding: 2rpx 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.low {
background: linear-gradient(180deg, #fff5f0 0%, #ffe8dc 100%);
border: 1rpx solid rgba(255, 107, 53, 0.3);
text {
font-size: 24rpx;
color: #ff6b35;
font-weight: 500;
}
}
&.medium {
background: linear-gradient(180deg, #fff8e1 0%, #fff4e6 100%);
border: 1rpx solid rgba(255, 165, 0, 0.3);
text {
font-size: 24rpx;
color: #ffa500;
font-weight: 500;
}
}
}
.nutrition-right {
display: flex;
align-items: baseline;
gap: 8rpx;
flex-shrink: 0;
}
.nutrition-value {
font-size: 32rpx;
color: #ff6b35;
font-weight: 500;
&.medium {
color: #ffa500;
}
}
.nutrition-unit {
font-size: 24rpx;
color: #9fa5c0;
}
/* 查看相似食物按钮 */
.similar-foods-btn {
background: #ffffff;
border: 2rpx solid #ff6b35;
border-radius: 50rpx;
height: 104rpx;
margin: 0 32rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
.btn-icon {
width: 32rpx;
height: 32rpx;
}
text {
font-size: 28rpx;
color: #ff6b35;
font-weight: 500;
}
}
/* 底部安全距离 */
.safe-bottom {
height: 40rpx;
}
</style>