feat: 集成 KieAI 服务,移除 models-integration 子项目

- 添加 Gemini 2.5 Flash 对话接口(流式+非流式)
- 添加 NanoBanana 图像生成/编辑接口
- 添加 Sora2 视频生成接口(文生视频、图生视频、去水印)
- 移除 models-integration 子项目(功能已迁移至主后端)
- 新增测试文档和 Playwright E2E 配置
- 更新前端页面和 API 接口
- 更新后端配置和日志处理
This commit is contained in:
2026-03-03 15:33:50 +08:00
parent 1ddb051977
commit 4be53dcd1b
586 changed files with 21142 additions and 25130 deletions

View File

@@ -5,6 +5,11 @@
<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>
<!-- 食物大图 -->
@@ -90,6 +95,10 @@ export default {
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: '',
@@ -124,13 +133,20 @@ export default {
}
},
onLoad(options) {
if (options.id) {
this.loadFoodData(options.id)
this.pageParams = { id: options.id || '', name: options.name || '' }
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id
console.log('[food-detail] onLoad params:', this.pageParams)
const rawId = options.id
const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId))
if (isNumericId) {
this.loadFoodData(Number(rawId))
} else if (options.name) {
this.loadFoodData(options.name)
// 无有效 id 仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException直接展示默认数据 + 当前名称
this.loadError = '暂无该食物详情数据,展示参考数据'
this.applyDefaultFoodData(false)
this.foodData.name = decodeURIComponent(String(options.name))
} else {
// 无参数时使用默认数据
this.foodData = { ...this.defaultFoodData }
this.applyDefaultFoodData()
}
},
methods: {
@@ -145,8 +161,29 @@ export default {
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 = ''
this.foodData = {
...this.defaultFoodData,
name: this.defaultFoodData.name || '—',
category: this.defaultFoodData.category || '—',
safetyTag: this.defaultFoodData.safetyTag || '—',
image: this.defaultFoodData.image || '',
keyNutrients: [...(this.defaultFoodData.keyNutrients || [])],
nutritionTable: [...(this.defaultFoodData.nutritionTable || [])]
}
},
/** 保证数组非空,空时返回 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)
const data = res.data || res
@@ -158,17 +195,26 @@ export default {
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.parseKeyNutrients(data),
nutritionTable: this.parseNutritionTable(data)
keyNutrients: this.ensureNonEmptyArray(this.parseKeyNutrients(data), this.defaultFoodData.keyNutrients),
nutritionTable: this.ensureNonEmptyArray(this.parseNutritionTable(data), this.defaultFoodData.nutritionTable)
}
} else {
// API 返回空数据,使用默认数据
this.foodData = { ...this.defaultFoodData }
this.applyDefaultFoodData()
}
} catch (error) {
console.error('加载食物数据失败:', error)
// 加载失败使用默认数据
this.foodData = { ...this.defaultFoodData }
const errMsg = (error && (error.message || error.msg || error)) ? String(error.message || error.msg || error) : '未知错误'
console.error('[food-detail] 加载食物数据失败:', error)
// a. 将 loadError 置为具体错误信息(用于调试)
this.loadError = errMsg
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面;不清空 loadError 以便展示「当前数据来自缓存」提示
this.applyDefaultFoodData(false)
// 若有入参 name用其覆盖展示名称避免显示默认「五谷香」
if (this.pageParams && this.pageParams.name) {
try {
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
} catch (e) {}
}
uni.showToast({
title: '数据加载失败',
icon: 'none'
@@ -237,6 +283,18 @@ export default {
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;
@@ -457,6 +515,10 @@ export default {
&.medium {
background: #ffa500;
}
&.high {
background: #e53935;
}
}
.nutrition-label {