feat: 集成 KieAI 服务,移除 models-integration 子项目
- 添加 Gemini 2.5 Flash 对话接口(流式+非流式) - 添加 NanoBanana 图像生成/编辑接口 - 添加 Sora2 视频生成接口(文生视频、图生视频、去水印) - 移除 models-integration 子项目(功能已迁移至主后端) - 新增测试文档和 Playwright E2E 配置 - 更新前端页面和 API 接口 - 更新后端配置和日志处理
This commit is contained in:
@@ -99,33 +99,33 @@
|
||||
<view
|
||||
class="food-item"
|
||||
v-for="(item, index) in filteredFoodList"
|
||||
:key="index"
|
||||
:key="item.id != null ? item.id : index"
|
||||
@click="goToFoodDetail(item)"
|
||||
>
|
||||
<view class="food-image-wrapper">
|
||||
<image class="food-image" :src="item.image" mode="aspectFill"></image>
|
||||
<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">
|
||||
<text>{{ item.safety }}</text>
|
||||
<view class="safety-tag" :class="item.safetyClass || 'safe'">
|
||||
<text>{{ item.safety || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-badge">
|
||||
<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"
|
||||
v-for="(nut, idx) in (item.nutrition || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<text class="nutrition-label">{{ nut.label }}</text>
|
||||
<text class="nutrition-value" :class="nut.colorClass">{{ nut.value }}</text>
|
||||
<text class="nutrition-value" :class="nut.colorClass || 'green'">{{ nut.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -137,12 +137,17 @@
|
||||
</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: '香蕉',
|
||||
@@ -251,13 +256,66 @@ export default {
|
||||
page: 1,
|
||||
limit: 100
|
||||
});
|
||||
if (result.data && result.data.list) {
|
||||
this.foodList = result.data.list;
|
||||
}
|
||||
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;
|
||||
// 切换分类时清空搜索文本,避免搜索状态与分类状态冲突
|
||||
@@ -281,9 +339,8 @@ export default {
|
||||
page: 1,
|
||||
limit: 100
|
||||
});
|
||||
if (result.data && result.data.list) {
|
||||
this.foodList = result.data.list;
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -293,9 +350,14 @@ export default {
|
||||
}, 300);
|
||||
},
|
||||
goToFoodDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/food-detail?id=${item.name}`
|
||||
})
|
||||
// 后端详情接口仅接受 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user