feat: T10 回归测试 Bug 修复与功能完善

修复 BUG-001 至 BUG-009 及 T10-1 至 T10-6 相关问题:
- 打卡积分显示与累加逻辑优化
- 食谱计算器 Tab 选中样式修复
- 食物百科列表图片与简介展示修复
- 食物详情页数据加载修复
- AI营养师差异化回复优化
- 健康知识/营养知识名称统一
- 饮食指南/科普文章详情页内容展示修复
- 帖子营养统计数据展示修复
- 社区帖子类型中文命名统一
- 帖子详情标签中文显示修复
- 食谱营养AI填充功能完善
- 食谱收藏/点赞功能修复

新增:
- ToolNutritionFillService 营养填充服务
- T10 回归测试用例 (Playwright)
- 知识文章数据 SQL 脚本

涉及模块:
- crmeb-common: VO/Request/Response 优化
- crmeb-service: 业务逻辑完善
- crmeb-front: API 接口扩展
- msh_single_uniapp: 前端页面修复
- tests/e2e: 回归测试用例
This commit is contained in:
2026-03-05 09:35:00 +08:00
parent 6f2dc27fbc
commit d8d2025543
44 changed files with 1536 additions and 165 deletions

View File

@@ -208,6 +208,8 @@ export function getFoodList(data) {
*/
export function getFoodDetail(id) {
const numId = typeof id === 'number' && !isNaN(id) ? id : parseInt(String(id), 10);
// 打印请求参数便于确认(后端仅接受 Long 类型 id传 name 会 400
console.log('[api/tool] getFoodDetail 请求参数:', { id, numId, type: typeof id });
if (isNaN(numId)) {
return Promise.reject(new Error('食物详情接口需要数字ID当前传入: ' + id));
}
@@ -252,6 +254,15 @@ export function getNutrientDetail(name) {
return request.get('tool/knowledge/nutrient/' + name);
}
/**
* AI 营养估算根据饮食描述文本返回估算的热量、蛋白质、钾、磷T06-T09
* @param {String} text - 饮食描述
* @returns {Promise<{data: {energyKcal?, proteinG?, potassiumMg?, phosphorusMg?}}>}
*/
export function getAiNutritionFill(text) {
return request.post('tool/nutrition/fill-ai', { text: text || '' });
}
// ==================== 打卡社区相关 ====================
/**
@@ -346,6 +357,16 @@ export function sharePost(postId) {
return request.post('tool/community/share', { postId });
}
/**
* 填充帖子营养数据(服务端根据帖子内容/打卡补充营养并回写)
* @param {Number|String} postId - 帖子ID
* @returns {Promise<{data?: object}>} 返回更新后的帖子或营养数据
*/
export function fillPostNutrition(postId) {
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/post/' + id + '/fill-nutrition');
}
// ==================== 积分系统相关 ====================
/**
@@ -458,6 +479,10 @@ export function getRecipeDetail(id) {
export function toggleRecipeFavorite(recipeId, isFavorite) {
return request.post('tool/recipe/favorite', { recipeId, isFavorite });
}
// T09: 食谱营养 AI 回填
export function fillRecipeNutrition(recipeId) {
return request.post("tool/recipe/" + recipeId + "/fill-nutrition")
}
// ==================== 文件上传相关 ====================

View File

@@ -623,7 +623,8 @@ export default {
async sendToAI(content, type) {
this.isLoading = true;
// 文本对话必须走 KieAI GeminiPOST /api/front/kieai/gemini/chat请求体 { messages: [{ role: 'user', content }], stream: false }
// BUG-005文本/多模态必须走 KieAI Gemini,回复仅从 data.choices[0].message.content 取,不得使用固定话术
// 请求体: { messages: [{ role: 'user', content }], stream: false }
if (type === 'text' || type === 'multimodal') {
try {
const messages = [{ role: 'user', content: content }];
@@ -635,6 +636,7 @@ export default {
const msgObj = choice && choice.message;
const rawContent = msgObj && msgObj.content;
const reply = rawContent != null ? this.extractReplyContent(rawContent) : '';
// 成功时仅展示模型返回内容
this.messageList.push({ role: 'ai', content: reply || '未能获取到有效回复。' });
} else {
const msg = (response && response.message) || '发起对话失败';

View File

@@ -492,25 +492,23 @@ export default {
box-sizing: border-box;
color: #9ca3af;
font-weight: 400;
/* 未激活:明显变灰,无下划线 */
.tab-icon {
font-size: 28rpx;
color: #9ca3af;
font-weight: 400;
}
.tab-text {
font-size: 28rpx;
color: #9ca3af;
font-weight: 400;
}
/* 激活:加粗、主色、橙色底部下划线 */
&.active {
background: transparent;
border-bottom: 3px solid #f97316;
color: #f97316;
font-weight: 700;
.tab-text {
color: #f97316;
font-weight: 700;

View File

@@ -243,12 +243,14 @@ export default {
try {
const { setSignIntegral, getUserInfo } = await import('@/api/user.js');
const { getUserPoints } = await import('@/api/tool.js');
// 子问题 A不得在 API 返回成功前修改 currentPoints避免打卡前积分提前跳变
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral
// 子问题 B-1打卡接口GET /api/front/user/sign/integralsetSignIntegral,与 /api/front/user/checkin 同属签到类接口
await setSignIntegral();
this.todaySigned = true;
// 子问题 B仅用服务端返回的积分更新 currentPoints禁止前端本地 +30
// GET /api/front/usergetUserInfo刷新用户信息 integral 赋给 currentPoints
// 子问题 B-2/B-3仅用服务端返回值更新积分禁止前端本地 +30。调用 GET /api/front/usergetUserInfo刷新用户信息将返回的 integral 赋给 currentPoints
const userRes = await getUserInfo();
if (userRes && userRes.data && (userRes.data.integral != null || userRes.data.points != null)) {
this.currentPoints = userRes.data.integral ?? userRes.data.points ?? 0;

View File

@@ -14,13 +14,13 @@
<scroll-view class="content-scroll" scroll-y>
<!-- 食物大图 -->
<view class="food-image-section">
<image class="food-image" :src="foodData.image" mode="aspectFill"></image>
<image class="food-image" :src="foodData.image || defaultFoodData.image" mode="aspectFill"></image>
<view class="image-overlay"></view>
<view class="food-info-overlay">
<view class="food-name-overlay">{{ foodData.name }}</view>
<view class="food-name-overlay">{{ foodData.name || '—' }}</view>
<view class="food-tags">
<view class="food-tag category">{{ foodData.category }}</view>
<view class="food-tag safe">{{ foodData.safetyTag }}</view>
<view class="food-tag category">{{ foodData.category || '—' }}</view>
<view class="food-tag safe">{{ foodData.safetyTag || '—' }}</view>
</view>
</view>
</view>
@@ -193,6 +193,7 @@ export default {
try {
const res = await getFoodDetail(id)
const data = res.data || res
console.log('[food-detail] getFoodDetail 响应:', data ? { hasName: !!data.name, hasImage: !!data.image, keys: Object.keys(data) } : null)
if (data && data.name) {
// 解析API返回的食物数据
this.foodData = {
@@ -220,6 +221,7 @@ export default {
} catch (error) {
const errMsg = (error && (error.message || error.msg || error.errMsg || error)) ? String(error.message || error.msg || error.errMsg || error) : '未知错误'
console.error('[food-detail] 加载食物数据失败:', error)
console.error('[food-detail] loadError(用于调试):', errMsg)
// a. 将 loadError 置为具体错误信息(用于调试)
this.loadError = errMsg
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面;不清空 loadError 以便展示「当前数据来自缓存」提示
@@ -230,6 +232,7 @@ export default {
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
} catch (e) {}
}
// c. 页面已通过 v-if="loadError" 显示「当前数据来自缓存,可能不是最新」;再弹出轻提示
uni.showToast({
title: '数据加载失败',
icon: 'none'

View File

@@ -105,7 +105,7 @@
<view class="food-image-wrapper">
<image
class="food-image"
:src="getFoodImage(item)"
:src="getFoodImage(item) || defaultPlaceholder"
mode="aspectFill"
@error="onFoodImageError(item)"
></image>
@@ -126,11 +126,11 @@
<view class="nutrition-list">
<view
class="nutrition-item"
v-for="(nut, idx) in (item.nutrition || [])"
v-for="(nut, idx) in (item.nutrition || item.nutrients || [])"
:key="idx"
>
<text class="nutrition-label">{{ nut.label }}</text>
<text class="nutrition-value" :class="nut.colorClass || 'green'">{{ nut.value }}</text>
<text class="nutrition-label">{{ nut.label || '—' }}</text>
<text class="nutrition-value" :class="nut.colorClass || 'green'">{{ nut.value != null ? nut.value : '—' }}</text>
</view>
</view>
</view>
@@ -320,8 +320,12 @@ export default {
colorClass: n.colorClass || 'green'
}));
} else {
// 后端列表仅返回扁平字段,无 nutrition/nutrients 数组,此处组装并始终展示主要项(空值显示 —)
nutrition = [];
const push = (label, val, unit) => { if (val != null && val !== '') nutrition.push({ label, value: String(val) + (unit || ''), colorClass: 'green' }); };
const push = (label, val, unit) => {
const value = (val != null && val !== '') ? String(val) + (unit || '') : '—';
nutrition.push({ label, value, colorClass: 'green' });
};
push('能量', item.energy, 'kcal');
push('蛋白质', item.protein, 'g');
push('钾', item.potassium, 'mg');
@@ -330,9 +334,14 @@ export default {
push('钙', item.calcium, 'mg');
}
// 后端详情接口仅接受 Long 类型 id若列表返回的 id 为非数字(如名称),不传 id避免详情页请求 400
const rawId = item.id != null ? item.id : item.foodId;
const numericId = (rawId !== undefined && rawId !== null && rawId !== '' && !isNaN(Number(rawId)))
? (typeof rawId === 'number' ? rawId : Number(rawId))
: undefined;
return {
...item,
id: item.id != null ? item.id : item.foodId,
id: numericId,
image,
imageUrl: image || undefined,
category: item.category || '',

View File

@@ -187,7 +187,7 @@ export default {
// 有 id 时切换到科普文章 tabswitchTab 内会调用 loadKnowledgeList 加载列表
this.switchTab('articles');
} else {
// 无 id 时默认当前 tab 为「营养素」;用户切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
// 无 id 时默认当前 tab 为「营养素」,不请求接口;用户点击「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
this.currentTab = 'nutrients';
}
},
@@ -215,23 +215,30 @@ export default {
page: 1,
limit: 50
});
// 兼容 result.data.list 或 result.data 为数组
// 兼容 CommonPageresult.data.list或 result.data/result.data.records 为数组
let rawList = [];
if (result && result.data) {
if (Array.isArray(result.data.list)) {
rawList = result.data.list;
} else if (Array.isArray(result.data.records)) {
rawList = result.data.records;
} else if (Array.isArray(result.data)) {
rawList = result.data;
}
}
const list = (rawList || []).map(item => ({
...item,
desc: item.desc || item.summary || '',
time: item.time || (item.publishedAt || item.createdAt ? this.formatKnowledgeTime(item.publishedAt || item.createdAt) : ''),
views: item.views != null ? item.views : (item.viewCount != null ? item.viewCount : 0),
icon: item.icon || '📄',
coverImage: item.coverImage || item.cover_image || ''
}));
// Normalize id: backend may return knowledgeId, id, or knowledge_id (BeanUtil/JSON)
const list = (rawList || []).map(item => {
const id = item.knowledgeId ?? item.id ?? item.knowledge_id;
return {
...item,
id,
desc: item.desc || item.summary || '',
time: item.time || (item.publishedAt || item.createdAt ? this.formatKnowledgeTime(item.publishedAt || item.createdAt) : ''),
views: item.views != null ? item.views : (item.viewCount != null ? item.viewCount : 0),
icon: item.icon || '📄',
coverImage: item.coverImage || item.cover_image || ''
};
});
if (this.currentTab === 'guide') {
this.guideList = list;
} else if (this.currentTab === 'articles') {
@@ -239,16 +246,16 @@ export default {
}
} catch (error) {
console.error('加载知识列表失败:', error);
const msg = (error && (error.message || error.msg)) || '加载列表失败';
const msg = (error && (typeof error === 'string' ? error : (error.message || error.msg))) || '加载列表失败';
uni.showToast({
title: String(msg),
icon: 'none'
});
// 确保列表始终为数组,不设为 undefined
// 失败时清空当前 tab 列表并确保始终为数组,不设为 undefined
if (this.currentTab === 'guide') {
this.guideList = Array.isArray(this.guideList) ? this.guideList : [];
this.guideList = [];
} else if (this.currentTab === 'articles') {
this.articleList = Array.isArray(this.articleList) ? this.articleList : [];
this.articleList = [];
}
}
},
@@ -266,13 +273,12 @@ export default {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
// 兼容后端 knowledgeId / id / knowledge_id
// 确保 knowledgeId id 存在才跳转,否则提示暂无详情
const id = item.knowledgeId ?? item.id ?? item.knowledge_id;
if (id === undefined || id === null || id === '') {
if (id === undefined || id === null || id === '' || (typeof id === 'number' && isNaN(id))) {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
// 饮食指南、科普文章使用知识详情页(调用 tool/knowledge/detail 接口)
uni.navigateTo({
url: `/pages/tool/knowledge-detail?id=${id}`
});

View File

@@ -22,7 +22,7 @@
<!-- 餐次标签 -->
<view class="meal-tag" v-if="postData.mealType">
<text class="meal-icon">{{ getMealIcon(postData.mealType) }}</text>
<text class="meal-text">{{ postData.mealType }}</text>
<text class="meal-text">{{ getMealTypeLabel(postData.mealType) }}</text>
</view>
</view>
@@ -44,7 +44,7 @@
<!-- 餐次标签 -->
<view class="meal-tag" v-if="postData.mealType">
<text class="meal-icon">{{ getMealIcon(postData.mealType) }}</text>
<text class="meal-text">{{ postData.mealType }}</text>
<text class="meal-text">{{ getMealTypeLabel(postData.mealType) }}</text>
</view>
<!-- 图片页码 -->
<view class="image-counter" v-if="postData.images.length > 1">
@@ -56,7 +56,7 @@
<view class="post-content-section">
<!-- 无图片时显示类型标签 -->
<view class="type-tag-row" v-if="!postData.images || postData.images.length === 0">
<view class="type-tag">{{ postData.mealType || '分享' }}</view>
<view class="type-tag">{{ getMealTypeLabel(postData.mealType) }}</view>
</view>
<!-- 标题和分数 -->
@@ -129,6 +129,18 @@
</view>
</view>
<!-- T08/T09: 无营养数据时展示「AI 补充营养」按钮 -->
<view class="nutrition-fill-row" v-else-if="!isLoading && (postData.description || postData.title)">
<button
class="ai-fill-btn"
:disabled="aiNutritionFilling"
@click="fillNutritionByAi"
>
<text v-if="!aiNutritionFilling">🤖 AI 补充营养</text>
<text v-else>估算中...</text>
</button>
</view>
<!-- AI营养师点评 -->
<view class="ai-comment-card" v-if="postData.aiComment">
<view class="ai-header">
@@ -234,6 +246,8 @@
import {
getCommunityDetail,
getCheckinDetail,
getAiNutritionFill,
fillPostNutrition,
toggleLike as apiToggleLike,
toggleCollect,
toggleFollow as apiToggleFollow,
@@ -282,7 +296,8 @@ export default {
hasMoreComments: true,
isLoadingComments: false,
showCommentInput: false,
commentText: ''
commentText: '',
aiNutritionFilling: false
}
},
computed: {
@@ -300,6 +315,13 @@ export default {
}
},
methods: {
// 获取餐次类型中文标签
getMealTypeLabel(mealType) {
if (!mealType) return '分享'
const map = { breakfast:'早餐', lunch:'午餐', dinner:'晚餐', snack:'加餐', share:'分享', checkin:'打卡' }
return map[String(mealType).toLowerCase()] ?? '分享'
},
// 加载帖子详情
async loadPostData(id) {
this.isLoading = true
@@ -312,8 +334,13 @@ export default {
this.formatPostData(data)
// 若详情接口未返回营养数据且有关联打卡记录,则根据打卡详情补充营养统计(等待完成后再结束加载)
if (this.postData.nutritionStats.length === 0 && (data.checkInRecordId != null)) {
await this.fillNutritionStatsFromCheckin(data.checkInRecordId)
const checkInId = data.checkInRecordId != null ? (Number(data.checkInRecordId) || data.checkInRecordId) : null
if (this.postData.nutritionStats.length === 0 && checkInId != null) {
await this.fillNutritionStatsFromCheckin(checkInId)
}
// T07: 营养仍为空时调用服务端填充接口
if (this.postData.nutritionStats.length === 0) {
await this.fillNutritionFromServer()
}
// 同步状态
@@ -344,14 +371,6 @@ export default {
*/
buildNutritionStatsFromDetailData(data) {
if (!data) return []
console.log('[post-detail] buildNutritionStatsFromDetailData API response data:', JSON.stringify({
nutritionStats: data.nutritionStats,
nutrition_stats: data.nutrition_stats,
nutritionDataJson: data.nutritionDataJson,
nutrition_data_json: data.nutrition_data_json,
dietaryData: data.dietaryData,
mealData: data.mealData
}))
// 1) 后端直接返回的 stat 数组(兼容不同命名)
const rawStats = data.nutritionStats || data.nutrition_stats
@@ -362,23 +381,21 @@ export default {
})).filter(s => s.label)
}
// 2) nutritionDataJson / nutrition_data_json兼容后端驼峰与下划线含打卡字段 actualEnergy/actualProtein
// 2) nutritionDataJson / nutrition_data_json兼容后端驼峰与下划线 fill-nutrition 的 energyKcal/proteinG/potassiumMg/phosphorusMg、打卡字段 actualEnergy/actualProtein
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') {
const cal = nutritionData.calories ?? nutritionData.energy ?? nutritionData.calorie ?? nutritionData.actualEnergy
const pro = nutritionData.protein ?? nutritionData.proteins ?? nutritionData.actualProtein
const pot = nutritionData.potassium ?? nutritionData.k
const pho = nutritionData.phosphorus ?? nutritionData.p
return [
{ label: '热量(kcal)', value: cal != null ? String(cal) : '-' },
{ label: '蛋白质', value: this.formatNutritionValue(pro, 'g') },
{ label: '钾', value: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
]
if (!nutritionData || typeof nutritionData !== 'object') return []
// 2a) 若为数组格式 [{label, value}, ...],直接使用
if (Array.isArray(nutritionData) && nutritionData.length > 0) {
return nutritionData.map(s => ({
label: s.label || s.name || '',
value: s.value != null ? String(s.value) : '-'
})).filter(s => s.label)
}
// 2b) 对象格式:兼容后端 fill-nutrition 的 energyKcal/proteinG/potassiumMg/phosphorusMg 及常见命名
return this.buildNutritionStatsFromNutritionObject(nutritionData)
} catch (e) {
// ignore
}
@@ -388,23 +405,36 @@ export default {
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 ?? obj.actualEnergy
const pro = obj.protein ?? obj.proteins ?? obj.actualProtein
const pot = obj.potassium ?? obj.k
const pho = obj.phosphorus ?? obj.p
return [
{ label: '热量(kcal)', value: cal != null ? String(cal) : '-' },
{ label: '蛋白质', value: this.formatNutritionValue(pro, 'g') },
{ label: '钾', value: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
]
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
const statsFromObj = this.buildNutritionStatsFromNutritionObject(obj)
if (statsFromObj.length > 0) return statsFromObj
}
}
// 4) data 本身为营养对象(如 fill-nutrition 接口直接返回的 { energyKcal, proteinG, potassiumMg, phosphorusMg }
if (data && typeof data === 'object' && !Array.isArray(data) && (data.energyKcal != null || data.proteinG != null || data.actualEnergy != null || data.actualProtein != null || data.calories != null || data.protein != null)) {
const statsFromData = this.buildNutritionStatsFromNutritionObject(data)
if (statsFromData.length > 0) return statsFromData
}
return []
},
/** 从单一营养对象构建 [{label, value}, ...],统一兼容 energyKcal/proteinG/potassiumMg/phosphorusMg 及 calories/protein 等命名 */
buildNutritionStatsFromNutritionObject(obj) {
if (!obj || typeof obj !== 'object') return []
const cal = obj.energyKcal ?? obj.calories ?? obj.energy ?? obj.calorie ?? obj.actualEnergy
const pro = obj.proteinG ?? obj.protein ?? obj.proteins ?? obj.actualProtein
const pot = obj.potassiumMg ?? obj.potassium ?? obj.k
const pho = obj.phosphorusMg ?? obj.phosphorus ?? obj.p
return [
{ label: '热量(kcal)', value: cal != null ? String(cal) : '-' },
{ label: '蛋白质', value: this.formatNutritionValue(pro, 'g') },
{ label: '钾', value: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
]
},
/** 格式化营养素显示值(兼容 number/string/BigDecimal 等) */
formatNutritionValue(val, unit) {
if (val == null || val === '') return '-'
@@ -443,6 +473,60 @@ export default {
}
},
/**
* T07: 调用服务端 fill-nutrition 接口,根据帖子内容补充营养并更新本地展示
*/
async fillNutritionFromServer() {
if (!this.postId) return
try {
const res = await fillPostNutrition(this.postId)
const data = res.data || res
if (!data) return
const stats = this.buildNutritionStatsFromDetailData(data)
if (stats.length > 0) {
this.postData.nutritionStats = stats
}
} catch (e) {
console.warn('服务端填充帖子营养失败:', e)
}
},
/**
* T08/T09: 用 AI 根据帖子描述估算营养并填充 nutritionStats
*/
async fillNutritionByAi() {
const text = (this.postData.description || this.postData.title || '').trim()
if (!text) {
uni.showToast({ title: '暂无描述可估算', icon: 'none' })
return
}
this.aiNutritionFilling = true
try {
const res = await getAiNutritionFill(text)
const data = (res && res.data) ? res.data : res
if (!data || typeof data !== 'object') {
uni.showToast({ title: 'AI 估算暂无结果', icon: 'none' })
return
}
const energy = data.energyKcal != null ? String(data.energyKcal) : '-'
const protein = this.formatNutritionValue(data.proteinG, 'g')
const potassium = data.potassiumMg != null ? String(data.potassiumMg) + 'mg' : '-'
const phosphorus = data.phosphorusMg != null ? String(data.phosphorusMg) + 'mg' : '-'
this.postData.nutritionStats = [
{ label: '热量(kcal)', value: energy },
{ label: '蛋白质', value: protein },
{ label: '钾', value: potassium },
{ label: '磷', value: phosphorus }
]
uni.showToast({ title: '已补充营养估算', icon: 'success' })
} catch (e) {
console.warn('AI 营养估算失败:', e)
uni.showToast({ title: '估算失败,请重试', icon: 'none' })
} finally {
this.aiNutritionFilling = false
}
},
// 格式化帖子数据
formatPostData(data) {
// 解析图片

View File

@@ -214,7 +214,7 @@
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="action-btn-small" @click="toggleLike">
<text class="action-icon-small" :class="{ active: isLiked }"></text>
<text class="action-icon-small" :class="{ active: isLiked }">{{ isLiked ? '❤️' : '🤍' }}</text>
</view>
<view class="action-btn-small" @click="toggleFavorite">
<text class="action-icon-small" :class="{ active: isFavorite }"></text>
@@ -227,7 +227,7 @@
</template>
<script>
import { getRecipeDetail, toggleRecipeFavorite } from '@/api/tool.js';
import { getRecipeDetail, toggleRecipeFavorite, fillRecipeNutrition } from '@/api/tool.js';
export default {
data() {
@@ -321,9 +321,41 @@ export default {
}
},
methods: {
// T09: AI 回填食谱营养
async fillNutritionFromAI(recipeId) {
try {
uni.showLoading({ title: 'AI分析中...' })
const res = await fillRecipeNutrition(recipeId)
const data = res.data || res
if (data) {
// 更新营养数据
this.nutritionData = {
main: [
{ value: String(data.energyKcal || '--'), label: '热量(kcal)' },
{ value: (data.proteinG || '--') + 'g', label: '蛋白质' },
{ value: '--', label: '脂肪' },
{ value: '--', label: '碳水' }
],
minor: [
{ value: data.sodiumMg ? data.sodiumMg + 'mg' : '--', label: '钠' },
{ value: data.potassiumMg ? data.potassiumMg + 'mg' : '--', label: '钾' },
{ value: data.phosphorusMg ? data.phosphorusMg + 'mg' : '--', label: '磷' }
]
}
}
} catch (e) {
console.error('AI营养回填失败', e)
} finally {
uni.hideLoading()
}
},
applyDefaultData() {
this.recipeData = { ...this.defaultRecipeData }
this.nutritionData = JSON.parse(JSON.stringify(this.defaultNutritionData))
// T09: 若无营养数据,调用 AI 回填
this.fillNutritionFromAI(id)
this.mealPlan = JSON.parse(JSON.stringify(this.defaultMealPlan))
this.warningList = [...this.defaultWarningList]
},
@@ -382,6 +414,9 @@ export default {
}
} else {
this.nutritionData = JSON.parse(JSON.stringify(this.defaultNutritionData))
// T09: 若无营养数据,调用 AI 回填
this.fillNutritionFromAI(id)
}
// 解析三餐配餐:

View File

@@ -437,8 +437,12 @@ export default {
share: '分享',
checkin: '打卡'
}
const lower = String(mealType).toLowerCase()
return map[lower] != null ? map[lower] : '分享'
const str = String(mealType).trim()
const lower = str.toLowerCase()
if (map[lower] != null) return map[lower]
// 后端可能直接返回中文,避免误显示为「分享」
if (/[\u4e00-\u9fa5]/.test(str)) return str
return '分享'
},
// 格式化标签显示