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

@@ -110,8 +110,8 @@
</view>
</view>
<!-- 营养统计卡片 -->
<view class="nutrition-stats-card" v-if="postData.nutritionStats && postData.nutritionStats.length > 0">
<!-- 营养统计卡片:仅根据是否有数据展示 -->
<view class="nutrition-stats-card" v-if="nutritionStatsLength > 0">
<view class="stats-header">
<view class="stats-title">
<text class="title-icon">📊</text>
@@ -233,6 +233,7 @@
<script>
import {
getCommunityDetail,
getCheckinDetail,
toggleLike as apiToggleLike,
toggleCollect,
toggleFollow as apiToggleFollow,
@@ -284,6 +285,13 @@ export default {
commentText: ''
}
},
computed: {
// 营养统计条数,用于卡片显示条件(避免 postData.nutritionStats 未定义时报错)
nutritionStatsLength() {
const stats = this.postData && this.postData.nutritionStats
return Array.isArray(stats) ? stats.length : 0
}
},
onLoad(options) {
if (options.id) {
this.postId = options.id
@@ -301,6 +309,11 @@ export default {
// 格式化帖子数据
this.formatPostData(data)
// 若详情接口未返回营养数据且有关联打卡记录,则根据打卡详情补充营养统计(等待完成后再结束加载)
if (this.postData.nutritionStats.length === 0 && (data.checkInRecordId != null)) {
await this.fillNutritionStatsFromCheckin(data.checkInRecordId)
}
// 同步状态
this.isLiked = data.isLiked || false
@@ -324,6 +337,95 @@ export default {
}
},
/**
* 从详情接口返回的数据中构建 nutritionStats 数组
* 支持nutritionStats/nutrition_stats 数组、nutritionDataJson/nutrition_data_json、dietaryData/mealData 等
*/
buildNutritionStatsFromDetailData(data) {
if (!data) return []
// 1) 后端直接返回的 stat 数组(兼容不同命名)
const rawStats = data.nutritionStats || data.nutrition_stats
if (Array.isArray(rawStats) && rawStats.length > 0) {
return rawStats.map(s => ({
label: s.label || s.name || '',
value: s.value != null ? String(s.value) : '-'
})).filter(s => s.label)
}
// 2) nutritionDataJson / nutrition_data_json
const jsonRaw = data.nutritionDataJson || data.nutrition_data_json
if (jsonRaw) {
try {
const nutritionData = typeof jsonRaw === 'string' ? JSON.parse(jsonRaw) : jsonRaw
if (nutritionData && typeof nutritionData === 'object') {
return [
{ label: '热量(kcal)', value: nutritionData.calories != null ? String(nutritionData.calories) : '-' },
{ label: '蛋白质', value: nutritionData.protein != null ? nutritionData.protein + 'g' : '-' },
{ label: '钾', value: nutritionData.potassium != null ? nutritionData.potassium + 'mg' : '-' },
{ label: '磷', value: nutritionData.phosphorus != null ? nutritionData.phosphorus + 'mg' : '-' }
]
}
} catch (e) {
// ignore
}
}
// 3) dietaryData / mealData / dietary_data / meal_data饮食打卡对象
const dietary = data.dietaryData || data.dietary_data || data.mealData || data.meal_data
if (dietary) {
const obj = typeof dietary === 'string' ? (() => { try { return JSON.parse(dietary) } catch (_) { return null } })() : dietary
if (obj && typeof obj === 'object') {
const cal = obj.calories ?? obj.energy ?? obj.calorie
const pro = obj.protein ?? obj.proteins
const pot = obj.potassium ?? obj.k
const pho = obj.phosphorus ?? obj.p
return [
{ label: '热量(kcal)', value: cal != null ? String(cal) : '-' },
{ label: '蛋白质', value: pro != null ? (typeof pro === 'number' ? pro + 'g' : String(pro)) : '-' },
{ label: '钾', value: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
]
}
}
return []
},
/**
* 根据打卡详情接口返回的数据构建 nutritionStats蛋白质、热量、钾、磷
*/
buildNutritionStatsFromCheckinDetail(detail) {
if (!detail || typeof detail !== 'object') return []
const items = []
// 热量
const energy = detail.actualEnergy ?? detail.energy
items.push({ label: '热量(kcal)', value: energy != null ? String(energy) : '-' })
// 蛋白质
const protein = detail.actualProtein ?? detail.protein
items.push({ label: '蛋白质', value: protein != null ? (typeof protein === 'number' ? protein + 'g' : String(protein)) : '-' })
// 钾、磷:打卡详情可能没有,用 -
items.push({ label: '钾', value: '-' })
items.push({ label: '磷', value: '-' })
return items
},
/**
* 根据打卡记录 ID 拉取打卡详情,并填充 postData.nutritionStats不阻塞主流程
*/
async fillNutritionStatsFromCheckin(checkInRecordId) {
try {
const res = await getCheckinDetail(checkInRecordId)
const detail = res.data || res
const stats = this.buildNutritionStatsFromCheckinDetail(detail)
if (stats.length > 0) {
this.postData.nutritionStats = stats
}
} catch (e) {
console.warn('拉取打卡详情补充营养数据失败:', e)
}
},
// 格式化帖子数据
formatPostData(data) {
// 解析图片
@@ -368,23 +470,8 @@ export default {
}
}
// 解析营养数据
let nutritionStats = []
if (data.nutritionDataJson) {
try {
const nutritionData = typeof data.nutritionDataJson === 'string'
? JSON.parse(data.nutritionDataJson)
: data.nutritionDataJson
nutritionStats = [
{ value: nutritionData.calories || '-', label: '热量(kcal)' },
{ value: nutritionData.protein ? nutritionData.protein + 'g' : '-', label: '蛋白质' },
{ value: nutritionData.potassium ? nutritionData.potassium + 'mg' : '-', label: '钾' },
{ value: nutritionData.phosphorus ? nutritionData.phosphorus + 'mg' : '-', label: '磷' }
]
} catch (e) {
nutritionStats = []
}
}
// 解析营养数据:支持多种后端字段名,以及从打卡数据计算
let nutritionStats = this.buildNutritionStatsFromDetailData(data)
// 提取纯文本内容
let description = ''