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:
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 Gemini:POST /api/front/kieai/gemini/chat,回复仅来自 data.choices[0].message.content
|
||||
// 文本对话必须走 KieAI Gemini:POST /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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/integral(setSignIntegral)
|
||||
await setSignIntegral();
|
||||
this.todaySigned = true;
|
||||
// 子问题 B:打卡成功后用服务端最新积分刷新,优先 GET /api/front/user/info,禁止前端本地 +30
|
||||
// 子问题 B:仅用服务端返回的积分更新 currentPoints,禁止前端本地 +30
|
||||
// 先 GET /api/front/user(getUserInfo)刷新用户信息,取 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 时切换到科普文章 tab,switchTab 内会调用 loadKnowledgeList 加载列表
|
||||
this.switchTab('articles');
|
||||
} else {
|
||||
// 无 id 时保持当前 tab(nutrients);切换到「饮食指南」或「科普文章」时由 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 / article(v2_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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user