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

597 lines
15 KiB
Vue
Raw Normal View History

<template>
<view class="food-page">
<!-- 搜索框 -->
<view class="search-container">
<view class="search-box">
<text class="search-icon">🔍</text>
<input
class="search-input"
v-model="searchText"
placeholder="搜索食物名称..."
placeholder-style="color: #9fa5c0"
@input="handleSearch"
/>
</view>
</view>
<!-- 分类标签 -->
<scroll-view class="category-scroll" scroll-x>
<view class="category-list">
<view
class="category-item"
:class="{ active: currentCategory === 'all' }"
@click="selectCategory('all')"
>
<text>全部</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'grain' }"
@click="selectCategory('grain')"
>
<text class="category-icon">🌾</text>
<text>谷薯类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'vegetable' }"
@click="selectCategory('vegetable')"
>
<text class="category-icon">🥬</text>
<text>蔬菜类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'fruit' }"
@click="selectCategory('fruit')"
>
<text class="category-icon">🍎</text>
<text>水果类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'meat' }"
@click="selectCategory('meat')"
>
<text class="category-icon">🍖</text>
<text>肉蛋类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'seafood' }"
@click="selectCategory('seafood')"
>
<text class="category-icon">🐟</text>
<text>水产类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'dairy' }"
@click="selectCategory('dairy')"
>
<text class="category-icon">🥛</text>
<text>奶类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'bean' }"
@click="selectCategory('bean')"
>
<text class="category-icon">🫘</text>
<text>豆类</text>
</view>
<view
class="category-item"
:class="{ active: currentCategory === 'nut' }"
@click="selectCategory('nut')"
>
<text class="category-icon">🥜</text>
<text>坚果类</text>
</view>
</view>
</scroll-view>
<!-- 食物列表 -->
<scroll-view class="food-scroll" scroll-y>
<view class="food-list-container">
<view class="food-count"> {{ filteredFoodList.length }} 种食物</view>
<view class="food-list">
<view
class="food-item"
v-for="(item, index) in filteredFoodList"
:key="item.id != null ? item.id : index"
@click="goToFoodDetail(item)"
>
<view class="food-image-wrapper">
<image class="food-image" :src="getFoodImage(item)" mode="aspectFill"></image>
<view v-if="item.warning" class="warning-badge"></view>
</view>
<view class="food-info">
<view class="food-header">
<view class="food-name-wrapper">
<text class="food-name">{{ item.name }}</text>
<view class="safety-tag" :class="item.safetyClass || 'safe'">
<text>{{ item.safety || '—' }}</text>
</view>
</view>
<view v-if="item.category" class="category-badge">
<text>{{ item.category }}</text>
</view>
</view>
<view class="nutrition-list">
<view
class="nutrition-item"
v-for="(nut, idx) in (item.nutrition || [])"
:key="idx"
>
<text class="nutrition-label">{{ nut.label }}</text>
<text class="nutrition-value" :class="nut.colorClass || 'green'">{{ nut.value }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { HTTP_REQUEST_URL } from '@/config/app.js';
export default {
data() {
// 无图时的占位图(灰色背景,与 .food-image-wrapper 背景一致)
const defaultPlaceholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZTRlNWU3Ii8+PC9zdmc+';
return {
searchText: '',
currentCategory: 'all',
searchTimer: null,
defaultPlaceholder,
foodList: [
{
name: '香蕉',
category: '水果类',
safety: '谨慎吃',
safetyClass: 'careful',
image: 'https://www.figma.com/api/mcp/asset/480782b7-5802-454c-9021-fad8cce6c195',
warning: true,
nutrition: [
{ label: '蛋白质', value: '1.4g', colorClass: 'green' },
{ label: '钾', value: '330mg', colorClass: 'red' },
{ label: '磷', value: '28mg', colorClass: 'orange' }
],
categoryType: 'fruit'
},
{
name: '玉米笋(罐头)',
category: '蔬菜类',
safety: '放心吃',
safetyClass: 'safe',
image: 'https://www.figma.com/api/mcp/asset/59db0f38-437e-4dfa-8fc6-d3eb6d2e9840',
warning: false,
nutrition: [
{ label: '钾', value: '36mg', colorClass: 'green' },
{ label: '钙', value: '6mg', colorClass: 'green' },
{ label: '磷', value: '4mg', colorClass: 'green' }
],
categoryType: 'vegetable'
},
{
name: '五谷香',
category: '谷薯类',
safety: '放心吃',
safetyClass: 'safe',
image: 'https://www.figma.com/api/mcp/asset/61347bd7-1ab4-485f-b8d0-09c7ede49fe6',
warning: false,
nutrition: [
{ label: '钾', value: '7mg', colorClass: 'green' },
{ label: '钙', value: '2mg', colorClass: 'green' },
{ label: '磷', value: '13mg', colorClass: 'green' }
],
categoryType: 'grain'
},
{
name: '糯米粥',
category: '谷薯类',
safety: '放心吃',
safetyClass: 'safe',
image: 'https://www.figma.com/api/mcp/asset/cf95c2ea-9fb0-4e40-b134-39873207f769',
warning: false,
nutrition: [
{ label: '钾', value: '13mg', colorClass: 'green' },
{ label: '钙', value: '7mg', colorClass: 'green' },
{ label: '磷', value: '20mg', colorClass: 'green' }
],
categoryType: 'grain'
},
{
name: '苹果',
category: '水果类',
safety: '限量吃',
safetyClass: 'limited',
image: 'https://www.figma.com/api/mcp/asset/4bc870ef-b16d-496b-b9ed-16f2cb9a53e1',
warning: false,
nutrition: [
{ label: '蛋白质', value: '0.4g', colorClass: 'green' },
{ label: '钾', value: '119mg', colorClass: 'orange' },
{ label: '磷', value: '12mg', colorClass: 'green' }
],
categoryType: 'fruit'
},
{
name: '西兰花',
category: '蔬菜类',
safety: '谨慎吃',
safetyClass: 'careful',
image: 'https://www.figma.com/api/mcp/asset/eb858cbc-78cb-46b1-a9a6-8cc9684dabba',
warning: false,
nutrition: [
{ label: '蛋白质', value: '4.1g', colorClass: 'orange' },
{ label: '钾', value: '316mg', colorClass: 'red' },
{ label: '磷', value: '72mg', colorClass: 'red' }
],
categoryType: 'vegetable'
}
]
}
},
computed: {
filteredFoodList() {
return this.foodList
},
},
onLoad(options) {
if (options && options.category) {
this.currentCategory = options.category;
}
this.loadFoodList();
},
methods: {
async loadFoodList() {
try {
const { getFoodList } = await import('@/api/tool.js');
const result = await getFoodList({
category: this.currentCategory === 'all' ? '' : this.currentCategory,
page: 1,
limit: 100
});
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
} catch (error) {
console.error('加载食物列表失败:', error);
}
},
getFoodImage(item) {
const raw = item.imageUrl || item.image || item.img || '';
const url = raw && (raw.startsWith('//') || raw.startsWith('http')) ? raw : (raw && raw.startsWith('/') ? (HTTP_REQUEST_URL || '') + raw : raw);
return (url && String(url).trim()) ? url : this.defaultPlaceholder;
},
normalizeFoodItem(item) {
const safetyMap = {
suitable: { safety: '放心吃', safetyClass: 'safe' },
moderate: { safety: '限量吃', safetyClass: 'limited' },
restricted: { safety: '谨慎吃', safetyClass: 'careful' },
forbidden: { safety: '谨慎吃', safetyClass: 'careful' }
};
const safety = item.safety != null ? { safety: item.safety, safetyClass: item.safetyClass || 'safe' } : (safetyMap[item.suitabilityLevel] || { safety: '—', safetyClass: 'safe' });
// 图片:统一为 image / imageUrl相对路径补全为完整 URL
const rawImg = item.imageUrl || item.image || item.img || '';
const imageUrl = (rawImg && (rawImg.startsWith('//') || rawImg.startsWith('http'))) ? rawImg : (rawImg && rawImg.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawImg : rawImg);
const image = imageUrl || '';
// 营养简介:优先 item.nutrition其次 item.nutrients兼容后端不同字段否则由扁平字段组装
let nutrition = item.nutrition;
if (Array.isArray(nutrition) && nutrition.length > 0) {
nutrition = nutrition.map(n => ({
label: n.label || n.name || n.labelName || '—',
value: n.value != null ? String(n.value) : '—',
colorClass: n.colorClass || 'green'
}));
} else if (Array.isArray(item.nutrients) && item.nutrients.length > 0) {
nutrition = item.nutrients.map(n => ({
label: n.label || n.name || n.labelName || '—',
value: n.value != null ? String(n.value) : '—',
colorClass: n.colorClass || 'green'
}));
} else {
nutrition = [];
const push = (label, val, unit) => { if (val != null && val !== '') nutrition.push({ label, value: String(val) + (unit || ''), colorClass: 'green' }); };
push('能量', item.energy, 'kcal');
push('蛋白质', item.protein, 'g');
push('钾', item.potassium, 'mg');
push('磷', item.phosphorus, 'mg');
push('钠', item.sodium, 'mg');
push('钙', item.calcium, 'mg');
}
return {
...item,
image,
imageUrl: image || undefined,
category: item.category || '',
safety: safety.safety,
safetyClass: safety.safetyClass,
nutrition
};
},
async selectCategory(category) {
this.currentCategory = category;
// 切换分类时清空搜索文本,避免搜索状态与分类状态冲突
this.searchText = '';
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
await this.loadFoodList();
},
handleSearch() {
// 防抖300ms 内不重复触发搜索请求
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(async () => {
if (this.searchText.trim()) {
try {
const { searchFood } = await import('@/api/tool.js');
const result = await searchFood({
keyword: this.searchText.trim(),
page: 1,
limit: 100
});
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
} catch (error) {
console.error('搜索失败:', error);
}
} else {
await this.loadFoodList();
}
}, 300);
},
goToFoodDetail(item) {
// 后端详情接口仅接受 Long 类型 id仅在有有效数字 id 时传 id始终传 name 供详情页失败时展示
const rawId = item.id != null ? item.id : ''
const numericId = (rawId !== '' && rawId !== undefined && !isNaN(Number(rawId))) ? Number(rawId) : null
const namePart = item.name ? `&name=${encodeURIComponent(item.name)}` : ''
const url = numericId !== null
? `/pages/tool/food-detail?id=${numericId}${namePart}`
: `/pages/tool/food-detail?name=${encodeURIComponent(item.name || '')}`
uni.navigateTo({ url })
}
}
}
</script>
<style lang="scss" scoped>
.food-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f4f5f7;
}
/* 搜索框 */
.search-container {
background: #ffffff;
padding: 32rpx;
border-bottom: 1rpx solid #d0dbea;
}
.search-box {
display: flex;
align-items: center;
height: 88rpx;
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 50rpx;
padding: 0 32rpx;
.search-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 32rpx;
color: #3e5481;
}
}
/* 分类标签 */
.category-scroll {
background: #ffffff;
border-bottom: 1rpx solid #d0dbea;
white-space: nowrap;
}
.category-list {
display: flex;
padding: 32rpx;
gap: 16rpx;
white-space: nowrap;
}
.category-item {
display: flex;
align-items: center;
gap: 12rpx;
height: 67rpx;
padding: 0 32rpx;
border-radius: 50rpx;
border: 1rpx solid #d0dbea;
background: #ffffff;
white-space: nowrap;
.category-icon {
font-size: 28rpx;
}
text {
font-size: 28rpx;
color: #9fa5c0;
}
&.active {
background: #ff6b35;
border-color: #ff6b35;
text {
color: #ffffff;
}
}
}
/* 食物列表 */
.food-scroll {
flex: 1;
overflow-y: auto;
}
.food-list-container {
padding: 32rpx;
}
.food-count {
font-size: 28rpx;
color: #9fa5c0;
margin-bottom: 32rpx;
}
.food-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.food-item {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 32rpx;
padding: 24rpx;
display: flex;
gap: 24rpx;
}
.food-image-wrapper {
position: relative;
width: 192rpx;
height: 192rpx;
border-radius: 24rpx;
overflow: hidden;
background: #f4f5f7;
flex-shrink: 0;
}
.food-image {
width: 100%;
height: 100%;
}
.warning-badge {
position: absolute;
top: 8rpx;
left: 8rpx;
background: #ff6464;
border-radius: 12rpx;
padding: 4rpx 12rpx;
font-size: 24rpx;
}
.food-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.food-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16rpx;
}
.food-name-wrapper {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.food-name {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
}
.safety-tag {
padding: 4rpx 16rpx;
border-radius: 50rpx;
font-size: 24rpx;
&.safe {
background: #e3fff1;
color: #1fcc79;
}
&.careful {
background: #ffe8e8;
color: #ff6464;
}
&.limited {
background: #fff8e1;
color: #ff9800;
}
}
.category-badge {
background: #fff5f0;
border: 1rpx solid #ff6b35;
border-radius: 12rpx;
padding: 4rpx 16rpx;
text {
font-size: 24rpx;
color: #ff6b35;
font-weight: 500;
}
}
.nutrition-list {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.nutrition-item {
display: flex;
justify-content: space-between;
align-items: center;
.nutrition-label {
font-size: 24rpx;
color: #9fa5c0;
}
.nutrition-value {
font-size: 24rpx;
&.green {
color: #1fcc79;
}
&.red {
color: #ff6464;
}
&.orange {
color: #ff9800;
}
}
}
</style>