From 6f2dc27fbc4d3a2755e5322ae86e47ad343eef8d Mon Sep 17 00:00:00 2001 From: scottpan <43121650@qq.com> Date: Wed, 4 Mar 2026 12:21:29 +0800 Subject: [PATCH] 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 --- .../zbkj/common/model/article/Article.java | 41 +++++++++ .../zbkj/common/model/system/SystemAdmin.java | 8 ++ .../common/model/system/SystemConfig.java | 19 +++- .../zbkj/common/model/system/SystemMenu.java | 47 ++++++++++ .../model/system/SystemPermissions.java | 3 + .../java/com/zbkj/common/model/user/User.java | 4 + .../zbkj/front/controller/ToolController.java | 20 ++++- .../service/impl/SystemMenuServiceImpl.java | 2 +- msh_crmeb_22/deploy.conf | 6 ++ msh_crmeb_22/pom.xml | 7 ++ msh_single_uniapp/api/tool.js | 20 +++-- .../pages/tool/ai-nutritionist.vue | 20 +++-- .../pages/tool/calculator-result.vue | 6 +- .../pages/tool/checkin-publish.vue | 68 ++------------- msh_single_uniapp/pages/tool/checkin.vue | 6 +- msh_single_uniapp/pages/tool/food-detail.vue | 41 ++++++--- .../pages/tool/food-encyclopedia.vue | 42 +++++++-- .../pages/tool/nutrition-knowledge.vue | 30 ++++--- msh_single_uniapp/pages/tool/post-detail.vue | 87 +++++++++++++------ msh_single_uniapp/pages/tool_main/index.vue | 26 ++++-- 20 files changed, 352 insertions(+), 151 deletions(-) create mode 100644 msh_crmeb_22/deploy.conf diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/article/Article.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/article/Article.java index d60eaeb..7fd74ec 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/article/Article.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/article/Article.java @@ -1,6 +1,7 @@ package com.zbkj.common.model.article; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; @@ -55,6 +56,14 @@ public class Article implements Serializable { @ApiModelProperty(value = "浏览次数") private String visit; + @ApiModelProperty(value = "页面浏览量(表中暂无该列,仅内存使用)") + @TableField(exist = false) + private Integer pageViews; + + @ApiModelProperty(value = "收藏数(表中暂无该列,仅内存使用)") + @TableField(exist = false) + private Integer collect; + @ApiModelProperty(value = "排序") private Integer sort; @@ -141,4 +150,36 @@ public class Article implements Serializable { @ApiModelProperty(value = "关联的打卡记录ID") private Integer checkInRecordId; + + public Integer getId() { + return id; + } + + public Date getCreateTime() { + return createTime; + } + + public String getCid() { + return cid; + } + + public Boolean getHide() { + return hide; + } + + public Boolean getStatus() { + return status; + } + + public String getVisit() { + return visit; + } + + public Integer getPageViews() { + return pageViews; + } + + public Integer getCollect() { + return collect; + } } diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemAdmin.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemAdmin.java index 280f9cf..2324963 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemAdmin.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemAdmin.java @@ -69,4 +69,12 @@ public class SystemAdmin implements Serializable { @ApiModelProperty(value = "是否接收短信") private Boolean isSms; + + public Integer getId() { + return id; + } + + public String getRealName() { + return realName; + } } diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemConfig.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemConfig.java index 896933e..ec5ace8 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemConfig.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemConfig.java @@ -8,7 +8,6 @@ import java.util.Date; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; @@ -18,7 +17,6 @@ import lombok.experimental.Accessors; * | Author:ScottPan * +---------------------------------------------------------------------- */ -@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("eb_system_config") @@ -51,4 +49,21 @@ public class SystemConfig implements Serializable { @ApiModelProperty(value = "更新时间") private Date updateTime; + + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public Integer getFormId() { return formId; } + public void setFormId(Integer formId) { this.formId = formId; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + public Boolean getStatus() { return status; } + public void setStatus(Boolean status) { this.status = status; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } } diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemMenu.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemMenu.java index 91b6279..414e928 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemMenu.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemMenu.java @@ -66,5 +66,52 @@ public class SystemMenu implements Serializable { @JsonIgnore private Date updateTime; + public Integer getId() { + return id; + } + + public Integer getPid() { + return pid; + } + + public String getName() { + return name; + } + + public String getIcon() { + return icon; + } + + public String getPerms() { + return perms; + } + + public String getComponent() { + return component; + } + + public String getMenuType() { + return menuType; + } + + public Integer getSort() { + return sort; + } + + public Boolean getIsShow() { + return isShow; + } + + public Boolean getIsDelte() { + return isDelte; + } + + public Date getCreateTime() { + return createTime; + } + + public Date getUpdateTime() { + return updateTime; + } } diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemPermissions.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemPermissions.java index 650f1a8..2ec9478 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemPermissions.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/system/SystemPermissions.java @@ -47,5 +47,8 @@ public class SystemPermissions implements Serializable { @ApiModelProperty(value = "是否删除") private Boolean isDelte; + public String getPath() { + return path; + } } diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java index f254716..3daf63b 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java @@ -150,4 +150,8 @@ public class User implements Serializable { @ApiModelProperty(value = "成为分销员时间") private Date promoterTime; + + public Integer getUid() { + return uid; + } } diff --git a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java index 528d4fd..d069868 100644 --- a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java +++ b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java @@ -358,7 +358,15 @@ public class ToolController { @ApiOperation(value = "点赞/取消点赞") @PostMapping("/community/like") public CommonResult toggleLike(@RequestBody Map data) { - toolCommunityService.toggleLike((Long) data.get("postId"), (Boolean) data.get("isLike")); + Object postIdObj = data.get("postId"); + Long postId = postIdObj instanceof Number + ? ((Number) postIdObj).longValue() + : (postIdObj != null ? Long.parseLong(postIdObj.toString()) : null); + Object isLikeObj = data.get("isLike"); + Boolean isLike = isLikeObj instanceof Boolean + ? (Boolean) isLikeObj + : (isLikeObj != null && Boolean.parseBoolean(isLikeObj.toString())); + toolCommunityService.toggleLike(postId, isLike); return CommonResult.success("操作成功"); } @@ -368,7 +376,15 @@ public class ToolController { @ApiOperation(value = "收藏/取消收藏") @PostMapping("/community/collect") public CommonResult toggleCollect(@RequestBody Map data) { - toolCommunityService.toggleCollect((Long) data.get("postId"), (Boolean) data.get("isCollect")); + Object postIdObj = data.get("postId"); + Long postId = postIdObj instanceof Number + ? ((Number) postIdObj).longValue() + : (postIdObj != null ? Long.parseLong(postIdObj.toString()) : null); + Object isCollectObj = data.get("isCollect"); + Boolean isCollect = isCollectObj instanceof Boolean + ? (Boolean) isCollectObj + : (isCollectObj != null && Boolean.parseBoolean(isCollectObj.toString())); + toolCommunityService.toggleCollect(postId, isCollect); return CommonResult.success("操作成功"); } diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/SystemMenuServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/SystemMenuServiceImpl.java index 8a22bb9..b759f04 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/SystemMenuServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/SystemMenuServiceImpl.java @@ -79,7 +79,7 @@ public class SystemMenuServiceImpl extends ServiceImpl1.8 1.8 UTF-8 + + + org.projectlombok + lombok + 1.18.34 + + -parameters diff --git a/msh_single_uniapp/api/tool.js b/msh_single_uniapp/api/tool.js index 39c90e9..eb796f7 100644 --- a/msh_single_uniapp/api/tool.js +++ b/msh_single_uniapp/api/tool.js @@ -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 }); } /** diff --git a/msh_single_uniapp/pages/tool/ai-nutritionist.vue b/msh_single_uniapp/pages/tool/ai-nutritionist.vue index 2598729..02a37c7 100644 --- a/msh_single_uniapp/pages/tool/ai-nutritionist.vue +++ b/msh_single_uniapp/pages/tool/ai-nutritionist.vue @@ -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 }); diff --git a/msh_single_uniapp/pages/tool/calculator-result.vue b/msh_single_uniapp/pages/tool/calculator-result.vue index 6c7eb0a..50378ba 100644 --- a/msh_single_uniapp/pages/tool/calculator-result.vue +++ b/msh_single_uniapp/pages/tool/calculator-result.vue @@ -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; } } } diff --git a/msh_single_uniapp/pages/tool/checkin-publish.vue b/msh_single_uniapp/pages/tool/checkin-publish.vue index dde71e1..735fc5f 100644 --- a/msh_single_uniapp/pages/tool/checkin-publish.vue +++ b/msh_single_uniapp/pages/tool/checkin-publish.vue @@ -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, diff --git a/msh_single_uniapp/pages/tool/checkin.vue b/msh_single_uniapp/pages/tool/checkin.vue index 12caf30..eb1c75d 100644 --- a/msh_single_uniapp/pages/tool/checkin.vue +++ b/msh_single_uniapp/pages/tool/checkin.vue @@ -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; diff --git a/msh_single_uniapp/pages/tool/food-detail.vue b/msh_single_uniapp/pages/tool/food-detail.vue index 7e6d886..71dfe5b 100644 --- a/msh_single_uniapp/pages/tool/food-detail.vue +++ b/msh_single_uniapp/pages/tool/food-detail.vue @@ -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 diff --git a/msh_single_uniapp/pages/tool/food-encyclopedia.vue b/msh_single_uniapp/pages/tool/food-encyclopedia.vue index ea306cf..ea552d7 100644 --- a/msh_single_uniapp/pages/tool/food-encyclopedia.vue +++ b/msh_single_uniapp/pages/tool/food-encyclopedia.vue @@ -103,7 +103,12 @@ @click="goToFoodDetail(item)" > - + ⚠️ @@ -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); diff --git a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue index 508bf44..524996e 100644 --- a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue +++ b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue @@ -63,8 +63,8 @@ @@ -84,7 +84,7 @@ - + 暂无饮食指南数据 @@ -94,8 +94,8 @@ @@ -115,7 +115,7 @@ - + 暂无科普文章数据 @@ -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; diff --git a/msh_single_uniapp/pages/tool/post-detail.vue b/msh_single_uniapp/pages/tool/post-detail.vue index 4cc1c1a..111cd33 100644 --- a/msh_single_uniapp/pages/tool/post-detail.vue +++ b/msh_single_uniapp/pages/tool/post-detail.vue @@ -110,8 +110,8 @@ - - + + 📊 @@ -122,7 +122,7 @@ - + {{ stat.value }} {{ stat.label }} @@ -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 diff --git a/msh_single_uniapp/pages/tool_main/index.vue b/msh_single_uniapp/pages/tool_main/index.vue index 160f351..798c342 100644 --- a/msh_single_uniapp/pages/tool_main/index.vue +++ b/msh_single_uniapp/pages/tool_main/index.vue @@ -132,15 +132,16 @@ - {{ item.icon }} + + {{ item.icon || '💡' }} {{ item.title }} - {{ item.desc }} + {{ item.desc || item.summary || '' }} - {{ item.time }} - · - {{ item.views }} + {{ item.time || '' }} + · + {{ item.views != null ? item.views : '' }} @@ -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;