chore: update pom.xml Lombok config and deploy settings

- Update Maven compiler plugin to support Lombok annotation processing
- Add deploy.conf for automated deployment
- Update backend models and controllers
- Update frontend pages and API
This commit is contained in:
2026-03-04 12:21:29 +08:00
parent 4646fbc9b5
commit 6f2dc27fbc
20 changed files with 352 additions and 151 deletions

View File

@@ -203,11 +203,15 @@ export function getFoodList(data) {
}
/**
* 获取食物详情
* @param {String} id - 食物ID名称
* 获取食物详情(后端仅接受 Long 类型 id传 name 会 400
* @param {Number|String} id - 食物ID(必须为数字,不能传名称
*/
export function getFoodDetail(id) {
return request.get('tool/food/detail/' + id);
const numId = typeof id === 'number' && !isNaN(id) ? id : parseInt(String(id), 10);
if (isNaN(numId)) {
return Promise.reject(new Error('食物详情接口需要数字ID当前传入: ' + id));
}
return request.get('tool/food/detail/' + numId);
}
/**
@@ -284,20 +288,22 @@ export function publishCommunityPost(data) {
/**
* 点赞/取消点赞
* @param {Number} postId - 内容ID
* @param {Number} postId - 内容ID(会被转为数字以匹配后端 Long
* @param {Boolean} isLike - 是否点赞true/false
*/
export function toggleLike(postId, isLike) {
return request.post('tool/community/like', { postId, isLike });
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/like', { postId: id, isLike: !!isLike });
}
/**
* 收藏/取消收藏
* @param {Number} postId - 内容ID
* @param {Number} postId - 内容ID(会被转为数字以匹配后端 Long
* @param {Boolean} isCollect - 是否收藏true/false
*/
export function toggleCollect(postId, isCollect) {
return request.post('tool/community/collect', { postId, isCollect });
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/collect', { postId: id, isCollect: !!isCollect });
}
/**

View File

@@ -610,10 +610,20 @@ export default {
this.scrollToBottom();
},
/** 从 Gemini 响应 message.content 提取展示文本(支持 string 或 parts 数组) */
extractReplyContent(content) {
if (content == null) return '';
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content.map(part => (part && part.text) ? part.text : '').filter(Boolean).join('');
}
return String(content);
},
async sendToAI(content, type) {
this.isLoading = true;
// 纯文字 / 多模态均走 KieAI GeminiPOST /api/front/kieai/gemini/chat回复仅来自 data.choices[0].message.content
// 文本对话必须走 KieAI GeminiPOST /api/front/kieai/gemini/chat请求体 { messages: [{ role: 'user', content }], stream: false }
if (type === 'text' || type === 'multimodal') {
try {
const messages = [{ role: 'user', content: content }];
@@ -623,11 +633,9 @@ export default {
const data = response.data;
const choice = data.choices && data.choices[0];
const msgObj = choice && choice.message;
// 仅使用接口返回的 content禁止固定话术
const reply = (msgObj && msgObj.content != null)
? (typeof msgObj.content === 'string' ? msgObj.content : String(msgObj.content))
: '未能获取到有效回复。';
this.messageList.push({ role: 'ai', content: reply });
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) || '发起对话失败';
this.messageList.push({ role: 'ai', content: '请求失败:' + msg });

View File

@@ -488,18 +488,21 @@ export default {
justify-content: center;
gap: 16rpx;
transition: all 0.3s;
border-bottom: 3px solid transparent;
border-bottom: none;
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 {
@@ -514,6 +517,7 @@ export default {
}
.tab-icon {
color: #f97316;
font-weight: 700;
}
}
}

View File

@@ -568,80 +568,26 @@ export default {
if (this._publishing) return
this._publishing = true
uni.showLoading({
title: this.enableAIVideo ? '正在创建视频任务...' : '发布中...'
});
uni.showLoading({ title: '发布中...' });
try {
const imageUrls = this.selectedImages;
// 如果开启了AI视频生成创建视频任务
let videoTaskId = '';
if (this.enableAIVideo) {
try {
// 构建视频生成提示词
const mealLabel = this.mealTypes.find(m => m.value === this.selectedMealType)?.label || '饮食';
const videoPrompt = this.remark || `健康${mealLabel}打卡`;
const taskParams = {
uid: String(this.uid || ''),
prompt: videoPrompt,
image_urls: imageUrls,
remove_watermark: true
};
let videoTaskRes;
if (imageUrls.length > 0) {
// 图生视频:使用第一张图片作为参考图
videoTaskRes = await api.createImageToVideoTask({
...taskParams,
imageUrl: imageUrls[0]
});
} else {
// 文生视频
videoTaskRes = await api.createTextToVideoTask(taskParams);
}
if (videoTaskRes && videoTaskRes.code === 200 && videoTaskRes.data) {
videoTaskId = videoTaskRes.data;
console.log('视频生成任务已提交taskId:', videoTaskId);
} else {
console.warn('视频任务创建返回异常:', videoTaskRes);
}
} catch (videoError) {
console.error('创建视频任务失败:', videoError);
// 视频任务失败不阻断打卡提交,只提示
uni.showToast({
title: '视频任务创建失败,打卡将继续提交',
icon: 'none',
duration: 2000
});
// 等待toast显示
await new Promise(resolve => setTimeout(resolve, 1500));
}
}
// 更新loading提示
uni.showLoading({ title: '提交打卡中...' });
// 提交打卡
const today = new Date().toISOString().split('T')[0];
const result = await submitCheckin({
mealType: this.selectedMealType,
date: today,
photosJson: JSON.stringify(imageUrls),
notes: this.remark,
taskId: videoTaskId,
enableAIVideo: this.enableAIVideo,
enableAIAnalysis: false
});
uni.hideLoading();
// 显示成功提示
const points = result.data?.points || 0;
const successMsg = videoTaskId
? `打卡成功!视频生成中...`
const taskId = result.data?.taskId;
const successMsg = (this.enableAIVideo && taskId)
? '打卡成功!视频生成中...'
: `打卡成功!获得${points}积分`;
uni.showToast({
title: successMsg,

View File

@@ -243,10 +243,12 @@ export default {
try {
const { setSignIntegral, getUserInfo } = await import('@/api/user.js');
const { getUserPoints } = await import('@/api/tool.js');
// 子问题 A不在 API 成功前修改 currentPoints避免积分提前跳变
// 子问题 A在 API 返回成功前修改 currentPoints避免打卡前积分提前跳变
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral
await setSignIntegral();
this.todaySigned = true;
// 子问题 B打卡成功后用服务端最新积分刷新,优先 GET /api/front/user/info,禁止前端本地 +30
// 子问题 B仅用服务端返回的积分更新 currentPoints,禁止前端本地 +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

@@ -134,8 +134,9 @@ export default {
},
onLoad(options) {
this.pageParams = { id: options.id || '', name: options.name || '' }
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id
console.log('[food-detail] onLoad params:', this.pageParams)
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id,传 name 会导致 400
console.log('[food-detail] onLoad options:', options)
console.log('[food-detail] onLoad pageParams (id/name):', this.pageParams)
const rawId = options.id
const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId))
if (isNumericId) {
@@ -144,7 +145,9 @@ export default {
// 无有效 id 仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException直接展示默认数据 + 当前名称
this.loadError = '暂无该食物详情数据,展示参考数据'
this.applyDefaultFoodData(false)
this.foodData.name = decodeURIComponent(String(options.name))
try {
this.foodData.name = decodeURIComponent(String(options.name))
} catch (e) {}
} else {
this.applyDefaultFoodData()
}
@@ -164,14 +167,17 @@ export default {
/** 用 defaultFoodData 填充页面,保证 keyNutrients/nutritionTable 为非空数组以便 .nutrient-card / .nutrition-row 正常渲染clearError 为 true 时顺带清空 loadError */
applyDefaultFoodData(clearError = true) {
if (clearError) this.loadError = ''
const def = this.defaultFoodData
const fallbackKey = (def.keyNutrients && def.keyNutrients.length) ? def.keyNutrients : [{ name: '—', value: '—', unit: '', status: '—' }]
const fallbackTable = (def.nutritionTable && def.nutritionTable.length) ? def.nutritionTable : [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }]
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 || [])]
...def,
name: (def && def.name) ? def.name : '—',
category: (def && def.category) ? def.category : '—',
safetyTag: (def && def.safetyTag) ? def.safetyTag : '—',
image: (def && def.image) ? def.image : '',
keyNutrients: Array.isArray(def.keyNutrients) && def.keyNutrients.length > 0 ? [...def.keyNutrients] : [...fallbackKey],
nutritionTable: Array.isArray(def.nutritionTable) && def.nutritionTable.length > 0 ? [...def.nutritionTable] : [...fallbackTable]
}
},
/** 保证数组非空,空时返回 fallback 的副本,用于 API 解析结果避免空数组导致列表不渲染 */
@@ -199,11 +205,20 @@ export default {
nutritionTable: this.ensureNonEmptyArray(this.parseNutritionTable(data), this.defaultFoodData.nutritionTable)
}
} else {
// API 返回空数据,使用默认数据
this.applyDefaultFoodData()
// API 返回空数据,按“失败”处理:展示默认数据 + 缓存提示
const emptyMsg = (data == null || !data.name) ? '接口返回数据为空' : '缺少食物名称'
console.warn('[food-detail] 接口返回无效数据:', data)
this.loadError = emptyMsg
this.applyDefaultFoodData(false)
if (this.pageParams && this.pageParams.name) {
try {
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
} catch (e) {}
}
uni.showToast({ title: '数据加载失败', icon: 'none' })
}
} catch (error) {
const errMsg = (error && (error.message || error.msg || error)) ? String(error.message || error.msg || error) : '未知错误'
const errMsg = (error && (error.message || error.msg || error.errMsg || error)) ? String(error.message || error.msg || error.errMsg || error) : '未知错误'
console.error('[food-detail] 加载食物数据失败:', error)
// a. 将 loadError 置为具体错误信息(用于调试)
this.loadError = errMsg

View File

@@ -103,7 +103,12 @@
@click="goToFoodDetail(item)"
>
<view class="food-image-wrapper">
<image class="food-image" :src="getFoodImage(item)" mode="aspectFill"></image>
<image
class="food-image"
:src="getFoodImage(item)"
mode="aspectFill"
@error="onFoodImageError(item)"
></image>
<view v-if="item.warning" class="warning-badge"></view>
</view>
<view class="food-info">
@@ -148,6 +153,7 @@ export default {
currentCategory: 'all',
searchTimer: null,
defaultPlaceholder,
imageErrorIds: {}, // 图片加载失败时用占位图key 为 item.id
foodList: [
{
name: '香蕉',
@@ -256,17 +262,35 @@ export default {
page: 1,
limit: 100
});
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
const rawList = this.getRawFoodList(result);
this.imageErrorIds = {};
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
} catch (error) {
console.error('加载食物列表失败:', error);
}
},
// 兼容 result.data.list / result.list / result.data 为数组 等响应结构
getRawFoodList(result) {
if (!result) return [];
const page = result.data !== undefined && result.data !== null ? result.data : result;
if (page && Array.isArray(page.list)) return page.list;
if (Array.isArray(page)) return page;
return [];
},
getFoodImage(item) {
const raw = item.imageUrl || item.image || item.img || '';
const id = item.id != null ? item.id : item.foodId;
if (id != null && this.imageErrorIds[String(id)]) return this.defaultPlaceholder;
const raw = item.imageUrl || item.image || item.img || item.pic || item.coverImage || '';
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;
},
onFoodImageError(item) {
const id = item.id != null ? item.id : item.foodId;
if (id != null && !this.imageErrorIds[String(id)]) {
this.imageErrorIds[String(id)] = true;
this.imageErrorIds = { ...this.imageErrorIds };
}
},
normalizeFoodItem(item) {
const safetyMap = {
suitable: { safety: '放心吃', safetyClass: 'safe' },
@@ -276,12 +300,12 @@ export default {
};
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 || '';
// 图片:兼容 image/imageUrl/img/pic/coverImage相对路径补全为完整 URL空则留空由 getFoodImage 用占位图
const rawImg = item.imageUrl || item.image || item.img || item.pic || item.coverImage || '';
const imageUrl = (rawImg && (rawImg.startsWith('//') || rawImg.startsWith('http'))) ? rawImg : (rawImg && rawImg.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawImg : rawImg);
const image = imageUrl || '';
// 营养简介:优先 item.nutrition其次 item.nutrients兼容后端不同字段),否则由扁平字段组装
// 营养简介:优先 item.nutrition其次 item.nutrients兼容后端否则由扁平字段 energy/protein/potassium 等组装
let nutrition = item.nutrition;
if (Array.isArray(nutrition) && nutrition.length > 0) {
nutrition = nutrition.map(n => ({
@@ -308,12 +332,13 @@ export default {
return {
...item,
id: item.id != null ? item.id : item.foodId,
image,
imageUrl: image || undefined,
category: item.category || '',
safety: safety.safety,
safetyClass: safety.safetyClass,
nutrition
nutrition: Array.isArray(nutrition) ? nutrition : []
};
},
async selectCategory(category) {
@@ -339,7 +364,8 @@ export default {
page: 1,
limit: 100
});
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
const rawList = this.getRawFoodList(result);
this.imageErrorIds = {};
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
} catch (error) {
console.error('搜索失败:', error);

View File

@@ -63,8 +63,8 @@
<view class="knowledge-list">
<view
class="knowledge-item"
v-for="(item, index) in guideList"
:key="index"
v-for="(item, index) in (guideList || [])"
:key="item.knowledgeId || item.id || index"
@click="goToDetail(item)"
>
<view class="knowledge-cover" v-if="item.coverImage || item.cover_image">
@@ -84,7 +84,7 @@
</view>
</view>
</view>
<view v-if="guideList.length === 0" class="empty-placeholder">
<view v-if="(guideList || []).length === 0" class="empty-placeholder">
<text>暂无饮食指南数据</text>
</view>
</view>
@@ -94,8 +94,8 @@
<view class="knowledge-list">
<view
class="knowledge-item"
v-for="(item, index) in articleList"
:key="index"
v-for="(item, index) in (articleList || [])"
:key="item.knowledgeId || item.id || index"
@click="goToDetail(item)"
>
<view class="knowledge-cover" v-if="item.coverImage || item.cover_image">
@@ -115,7 +115,7 @@
</view>
</view>
</view>
<view v-if="articleList.length === 0" class="empty-placeholder">
<view v-if="(articleList || []).length === 0" class="empty-placeholder">
<text>暂无科普文章数据</text>
</view>
</view>
@@ -184,10 +184,10 @@ export default {
},
onLoad(options) {
if (options && options.id) {
// 有 id 时切换到科普文章 tab 加载列表
// 有 id 时切换到科普文章 tabswitchTab 内会调用 loadKnowledgeList 加载列表
this.switchTab('articles');
} else {
// 无 id 时保持当前 tabnutrients切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
// 无 id 时默认当前 tab 为「营养素」;用户切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
this.currentTab = 'nutrients';
}
},
@@ -206,7 +206,7 @@ export default {
if (this.currentTab === 'nutrients') {
return;
}
// type 与后端一致guide / article
// type 与后端一致guide / articlev2_knowledge 表 type 字段)
const typeParam = this.currentTab === 'guide' ? 'guide' : 'article';
try {
const { getKnowledgeList } = await import('@/api/tool.js');
@@ -224,7 +224,7 @@ export default {
rawList = result.data;
}
}
const list = rawList.map(item => ({
const list = (rawList || []).map(item => ({
...item,
desc: item.desc || item.summary || '',
time: item.time || (item.publishedAt || item.createdAt ? this.formatKnowledgeTime(item.publishedAt || item.createdAt) : ''),
@@ -239,15 +239,16 @@ export default {
}
} catch (error) {
console.error('加载知识列表失败:', error);
const msg = (error && (error.message || error.msg)) || '加载列表失败';
uni.showToast({
title: (error && (error.message || error.msg)) || '加载列表失败',
title: String(msg),
icon: 'none'
});
// 确保列表始终为数组,不设为 undefined
if (this.currentTab === 'guide') {
this.guideList = this.guideList ?? [];
this.guideList = Array.isArray(this.guideList) ? this.guideList : [];
} else if (this.currentTab === 'articles') {
this.articleList = this.articleList ?? [];
this.articleList = Array.isArray(this.articleList) ? this.articleList : [];
}
}
},
@@ -265,7 +266,8 @@ export default {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
const id = item.knowledgeId ?? item.id;
// 兼容后端 knowledgeId / id / knowledge_id
const id = item.knowledgeId ?? item.id ?? item.knowledge_id;
if (id === undefined || id === null || id === '') {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;

View File

@@ -110,8 +110,8 @@
</view>
</view>
<!-- 营养统计卡片:仅根据是否有数据展示 -->
<view class="nutrition-stats-card" v-if="nutritionStatsLength > 0">
<!-- 营养统计卡片:仅当 nutritionStats.length > 0 时展示,不依赖后端字段存在性 -->
<view class="nutrition-stats-card" v-if="nutritionStats.length > 0">
<view class="stats-header">
<view class="stats-title">
<text class="title-icon">📊</text>
@@ -122,7 +122,7 @@
</view>
</view>
<view class="stats-grid">
<view class="stat-item" v-for="(stat, index) in postData.nutritionStats" :key="index">
<view class="stat-item" v-for="(stat, index) in nutritionStats" :key="index">
<view class="stat-value">{{ stat.value }}</view>
<view class="stat-label">{{ stat.label }}</view>
</view>
@@ -286,16 +286,17 @@ export default {
}
},
computed: {
// 营养统计数,用于卡片显示条件(避免 postData.nutritionStats 未定义时报错
nutritionStatsLength() {
// 营养统计数,用于卡片显示与列表渲染(单一数据源,避免未定义
nutritionStats() {
const stats = this.postData && this.postData.nutritionStats
return Array.isArray(stats) ? stats.length : 0
return Array.isArray(stats) ? stats : []
}
},
onLoad(options) {
if (options.id) {
this.postId = options.id
this.loadPostData(options.id)
// Ensure postId is number for API calls (URL params are strings)
this.postId = parseInt(options.id, 10) || options.id
this.loadPostData(this.postId)
}
},
methods: {
@@ -343,6 +344,14 @@ 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
@@ -353,17 +362,21 @@ export default {
})).filter(s => s.label)
}
// 2) nutritionDataJson / nutrition_data_json
// 2) nutritionDataJson / nutrition_data_json(兼容后端驼峰与下划线;含打卡字段 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: 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' : '-' }
{ 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)) : '-' }
]
}
} catch (e) {
@@ -376,13 +389,13 @@ export default {
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 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: pro != null ? (typeof pro === 'number' ? pro + 'g' : String(pro)) : '-' },
{ 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)) : '-' }
]
@@ -392,22 +405,26 @@ export default {
return []
},
/** 格式化营养素显示值(兼容 number/string/BigDecimal 等) */
formatNutritionValue(val, unit) {
if (val == null || val === '') return '-'
const str = typeof val === 'number' ? String(val) : String(val)
return str === '' || str === 'undefined' || str === 'null' ? '-' : (unit ? str + unit : str)
},
/**
* 根据打卡详情接口返回的数据构建 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
return [
{ label: '热量(kcal)', value: energy != null ? String(energy) : '-' },
{ label: '蛋白质', value: this.formatNutritionValue(protein, 'g') },
{ label: '', value: '-' },
{ label: '磷', value: '-' }
]
},
/**
@@ -715,6 +732,13 @@ export default {
return
}
const postIdNum = typeof this.postId === 'number' ? this.postId : parseInt(this.postId, 10)
if (!postIdNum || isNaN(postIdNum)) {
console.error('[post-detail] toggleLike: invalid postId', this.postId)
uni.showToast({ title: '操作失败,请重试', icon: 'none' })
return
}
const newLikeState = !this.isLiked
const newLikeCount = newLikeState
? this.postData.likeCount + 1
@@ -725,8 +749,9 @@ export default {
this.postData.likeCount = newLikeCount
try {
await apiToggleLike(this.postId, newLikeState)
await apiToggleLike(postIdNum, newLikeState)
} catch (error) {
console.error('[post-detail] toggleLike failed:', error)
// 回滚
this.isLiked = !newLikeState
this.postData.likeCount = newLikeState
@@ -747,6 +772,13 @@ export default {
return
}
const postIdNum = typeof this.postId === 'number' ? this.postId : parseInt(this.postId, 10)
if (!postIdNum || isNaN(postIdNum)) {
console.error('[post-detail] toggleFavorite: invalid postId', this.postId)
uni.showToast({ title: '操作失败,请重试', icon: 'none' })
return
}
const newCollectState = !this.isCollected
const newCollectCount = newCollectState
? this.postData.favoriteCount + 1
@@ -757,12 +789,13 @@ export default {
this.postData.favoriteCount = newCollectCount
try {
await toggleCollect(this.postId, newCollectState)
await toggleCollect(postIdNum, newCollectState)
uni.showToast({
title: newCollectState ? '已收藏' : '取消收藏',
icon: 'none'
})
} catch (error) {
console.error('[post-detail] toggleFavorite failed:', error)
// 回滚
this.isCollected = !newCollectState
this.postData.favoriteCount = newCollectState

View File

@@ -132,15 +132,16 @@
<view class="knowledge-list">
<view class="knowledge-item" v-for="(item, index) in knowledgeList" :key="index" @tap="goToKnowledgeDetail(item)">
<view class="knowledge-icon">
<text>{{ item.icon }}</text>
<image v-if="item.coverImage" class="knowledge-cover" :src="item.coverImage" mode="aspectFill"></image>
<text v-else>{{ item.icon || '💡' }}</text>
</view>
<view class="knowledge-info">
<view class="knowledge-title">{{ item.title }}</view>
<view class="knowledge-desc">{{ item.desc }}</view>
<view class="knowledge-desc">{{ item.desc || item.summary || '' }}</view>
<view class="knowledge-meta">
<text class="meta-text">{{ item.time }}</text>
<text class="meta-dot">·</text>
<text class="meta-text">{{ item.views }}</text>
<text class="meta-text">{{ item.time || '' }}</text>
<text class="meta-dot" v-if="item.time && item.views">·</text>
<text class="meta-text">{{ item.views != null ? item.views : '' }}</text>
</view>
</view>
</view>
@@ -209,7 +210,12 @@ import {
]);
this.recipeList = recipesRes.data?.list || recipesRes.data || [];
this.knowledgeList = knowledgeRes.data?.list || knowledgeRes.data || [];
const rawKnowledge = knowledgeRes.data?.list || knowledgeRes.data || [];
this.knowledgeList = rawKnowledge.map(item => ({
...item,
desc: item.summary ?? item.desc ?? '',
icon: item.icon || '💡'
}));
this.userHealthStatus = healthRes.data || { hasProfile: false, profileStatus: '尚未完成健康档案' };
this.showFunctionEntries = !!(displayConfigRes.data && displayConfigRes.data.showFunctionEntries);
} catch (error) {
@@ -303,7 +309,7 @@ import {
goToKnowledgeDetail(item) {
if (!item) return
uni.navigateTo({
url: `/pages/tool/nutrition-knowledge?id=${item.id || 1}`
url: `/pages/tool/knowledge-detail?id=${item.id || 1}`
})
}
}
@@ -683,6 +689,12 @@ import {
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
.knowledge-cover {
width: 100%;
height: 100%;
}
text {
font-size: 60rpx;