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

@@ -1,6 +1,7 @@
package com.zbkj.common.model.article; package com.zbkj.common.model.article;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
@@ -55,6 +56,14 @@ public class Article implements Serializable {
@ApiModelProperty(value = "浏览次数") @ApiModelProperty(value = "浏览次数")
private String visit; private String visit;
@ApiModelProperty(value = "页面浏览量(表中暂无该列,仅内存使用)")
@TableField(exist = false)
private Integer pageViews;
@ApiModelProperty(value = "收藏数(表中暂无该列,仅内存使用)")
@TableField(exist = false)
private Integer collect;
@ApiModelProperty(value = "排序") @ApiModelProperty(value = "排序")
private Integer sort; private Integer sort;
@@ -141,4 +150,36 @@ public class Article implements Serializable {
@ApiModelProperty(value = "关联的打卡记录ID") @ApiModelProperty(value = "关联的打卡记录ID")
private Integer checkInRecordId; 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;
}
} }

View File

@@ -69,4 +69,12 @@ public class SystemAdmin implements Serializable {
@ApiModelProperty(value = "是否接收短信") @ApiModelProperty(value = "是否接收短信")
private Boolean isSms; private Boolean isSms;
public Integer getId() {
return id;
}
public String getRealName() {
return realName;
}
} }

View File

@@ -8,7 +8,6 @@ import java.util.Date;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@@ -18,7 +17,6 @@ import lombok.experimental.Accessors;
* | Author:ScottPan * | Author:ScottPan
* +---------------------------------------------------------------------- * +----------------------------------------------------------------------
*/ */
@Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Accessors(chain = true) @Accessors(chain = true)
@TableName("eb_system_config") @TableName("eb_system_config")
@@ -51,4 +49,21 @@ public class SystemConfig implements Serializable {
@ApiModelProperty(value = "更新时间") @ApiModelProperty(value = "更新时间")
private Date updateTime; 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; }
} }

View File

@@ -66,5 +66,52 @@ public class SystemMenu implements Serializable {
@JsonIgnore @JsonIgnore
private Date updateTime; 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;
}
} }

View File

@@ -47,5 +47,8 @@ public class SystemPermissions implements Serializable {
@ApiModelProperty(value = "是否删除") @ApiModelProperty(value = "是否删除")
private Boolean isDelte; private Boolean isDelte;
public String getPath() {
return path;
}
} }

View File

@@ -150,4 +150,8 @@ public class User implements Serializable {
@ApiModelProperty(value = "成为分销员时间") @ApiModelProperty(value = "成为分销员时间")
private Date promoterTime; private Date promoterTime;
public Integer getUid() {
return uid;
}
} }

View File

@@ -358,7 +358,15 @@ public class ToolController {
@ApiOperation(value = "点赞/取消点赞") @ApiOperation(value = "点赞/取消点赞")
@PostMapping("/community/like") @PostMapping("/community/like")
public CommonResult<String> toggleLike(@RequestBody Map<String, Object> data) { public CommonResult<String> toggleLike(@RequestBody Map<String, Object> 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("操作成功"); return CommonResult.success("操作成功");
} }
@@ -368,7 +376,15 @@ public class ToolController {
@ApiOperation(value = "收藏/取消收藏") @ApiOperation(value = "收藏/取消收藏")
@PostMapping("/community/collect") @PostMapping("/community/collect")
public CommonResult<String> toggleCollect(@RequestBody Map<String, Object> data) { public CommonResult<String> toggleCollect(@RequestBody Map<String, Object> 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("操作成功"); return CommonResult.success("操作成功");
} }

View File

@@ -79,7 +79,7 @@ public class SystemMenuServiceImpl extends ServiceImpl<SystemMenuDao, SystemMenu
lqw.like(SystemMenu::getName, request.getName()); lqw.like(SystemMenu::getName, request.getName());
} }
if (StrUtil.isNotEmpty(request.getMenuType())) { if (StrUtil.isNotEmpty(request.getMenuType())) {
lqw.eq(SystemMenu::getName, request.getMenuType()); lqw.eq(SystemMenu::getMenuType, request.getMenuType());
} }
lqw.eq(SystemMenu::getIsDelte, false); lqw.eq(SystemMenu::getIsDelte, false);
lqw.orderByDesc(SystemMenu::getSort); lqw.orderByDesc(SystemMenu::getSort);

6
msh_crmeb_22/deploy.conf Normal file
View File

@@ -0,0 +1,6 @@
SERVER_HOST=49.235.131.69
SERVER_USER=root
SERVER_PORT=22
REMOTE_DIR=/www/crmeb
LOCAL_PORT=20822
JAR_NAME=sophia-front-2.2.jar

View File

@@ -347,6 +347,13 @@
<source>1.8</source> <source>1.8</source>
<target>1.8</target> <target>1.8</target>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</path>
</annotationProcessorPaths>
<!-- java8 保留参数名编译参数 --> <!-- java8 保留参数名编译参数 -->
<compilerArgument>-parameters</compilerArgument> <compilerArgument>-parameters</compilerArgument>
<compilerArguments> <compilerArguments>

View File

@@ -203,11 +203,15 @@ export function getFoodList(data) {
} }
/** /**
* 获取食物详情 * 获取食物详情(后端仅接受 Long 类型 id传 name 会 400
* @param {String} id - 食物ID名称 * @param {Number|String} id - 食物ID(必须为数字,不能传名称
*/ */
export function getFoodDetail(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 * @param {Boolean} isLike - 是否点赞true/false
*/ */
export function toggleLike(postId, isLike) { 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 * @param {Boolean} isCollect - 是否收藏true/false
*/ */
export function toggleCollect(postId, isCollect) { 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(); 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) { async sendToAI(content, type) {
this.isLoading = true; 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') { if (type === 'text' || type === 'multimodal') {
try { try {
const messages = [{ role: 'user', content: content }]; const messages = [{ role: 'user', content: content }];
@@ -623,11 +633,9 @@ export default {
const data = response.data; const data = response.data;
const choice = data.choices && data.choices[0]; const choice = data.choices && data.choices[0];
const msgObj = choice && choice.message; const msgObj = choice && choice.message;
// 仅使用接口返回的 content禁止固定话术 const rawContent = msgObj && msgObj.content;
const reply = (msgObj && msgObj.content != null) const reply = rawContent != null ? this.extractReplyContent(rawContent) : '';
? (typeof msgObj.content === 'string' ? msgObj.content : String(msgObj.content)) this.messageList.push({ role: 'ai', content: reply || '未能获取到有效回复。' });
: '未能获取到有效回复。';
this.messageList.push({ role: 'ai', content: reply });
} else { } else {
const msg = (response && response.message) || '发起对话失败'; const msg = (response && response.message) || '发起对话失败';
this.messageList.push({ role: 'ai', content: '请求失败:' + msg }); this.messageList.push({ role: 'ai', content: '请求失败:' + msg });

View File

@@ -488,18 +488,21 @@ export default {
justify-content: center; justify-content: center;
gap: 16rpx; gap: 16rpx;
transition: all 0.3s; transition: all 0.3s;
border-bottom: 3px solid transparent; border-bottom: none;
box-sizing: border-box; box-sizing: border-box;
color: #9ca3af; color: #9ca3af;
font-weight: 400;
.tab-icon { .tab-icon {
font-size: 28rpx; font-size: 28rpx;
color: #9ca3af; color: #9ca3af;
font-weight: 400;
} }
.tab-text { .tab-text {
font-size: 28rpx; font-size: 28rpx;
color: #9ca3af; color: #9ca3af;
font-weight: 400;
} }
&.active { &.active {
@@ -514,6 +517,7 @@ export default {
} }
.tab-icon { .tab-icon {
color: #f97316; color: #f97316;
font-weight: 700;
} }
} }
} }

View File

@@ -568,80 +568,26 @@ export default {
if (this._publishing) return if (this._publishing) return
this._publishing = true this._publishing = true
uni.showLoading({ uni.showLoading({ title: '发布中...' });
title: this.enableAIVideo ? '正在创建视频任务...' : '发布中...'
});
try { try {
const imageUrls = this.selectedImages; 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 today = new Date().toISOString().split('T')[0];
const result = await submitCheckin({ const result = await submitCheckin({
mealType: this.selectedMealType, mealType: this.selectedMealType,
date: today, date: today,
photosJson: JSON.stringify(imageUrls), photosJson: JSON.stringify(imageUrls),
notes: this.remark, notes: this.remark,
taskId: videoTaskId,
enableAIVideo: this.enableAIVideo, enableAIVideo: this.enableAIVideo,
enableAIAnalysis: false enableAIAnalysis: false
}); });
uni.hideLoading(); uni.hideLoading();
// 显示成功提示
const points = result.data?.points || 0; const points = result.data?.points || 0;
const successMsg = videoTaskId const taskId = result.data?.taskId;
? `打卡成功!视频生成中...` const successMsg = (this.enableAIVideo && taskId)
? '打卡成功!视频生成中...'
: `打卡成功!获得${points}积分`; : `打卡成功!获得${points}积分`;
uni.showToast({ uni.showToast({
title: successMsg, title: successMsg,

View File

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

View File

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

View File

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

View File

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

View File

@@ -110,8 +110,8 @@
</view> </view>
</view> </view>
<!-- 营养统计卡片:仅根据是否有数据展示 --> <!-- 营养统计卡片:仅当 nutritionStats.length > 0 时展示,不依赖后端字段存在性 -->
<view class="nutrition-stats-card" v-if="nutritionStatsLength > 0"> <view class="nutrition-stats-card" v-if="nutritionStats.length > 0">
<view class="stats-header"> <view class="stats-header">
<view class="stats-title"> <view class="stats-title">
<text class="title-icon">📊</text> <text class="title-icon">📊</text>
@@ -122,7 +122,7 @@
</view> </view>
</view> </view>
<view class="stats-grid"> <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-value">{{ stat.value }}</view>
<view class="stat-label">{{ stat.label }}</view> <view class="stat-label">{{ stat.label }}</view>
</view> </view>
@@ -286,16 +286,17 @@ export default {
} }
}, },
computed: { computed: {
// 营养统计数,用于卡片显示条件(避免 postData.nutritionStats 未定义时报错 // 营养统计数,用于卡片显示与列表渲染(单一数据源,避免未定义
nutritionStatsLength() { nutritionStats() {
const stats = this.postData && this.postData.nutritionStats const stats = this.postData && this.postData.nutritionStats
return Array.isArray(stats) ? stats.length : 0 return Array.isArray(stats) ? stats : []
} }
}, },
onLoad(options) { onLoad(options) {
if (options.id) { if (options.id) {
this.postId = options.id // Ensure postId is number for API calls (URL params are strings)
this.loadPostData(options.id) this.postId = parseInt(options.id, 10) || options.id
this.loadPostData(this.postId)
} }
}, },
methods: { methods: {
@@ -343,6 +344,14 @@ export default {
*/ */
buildNutritionStatsFromDetailData(data) { buildNutritionStatsFromDetailData(data) {
if (!data) return [] 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 数组(兼容不同命名) // 1) 后端直接返回的 stat 数组(兼容不同命名)
const rawStats = data.nutritionStats || data.nutrition_stats const rawStats = data.nutritionStats || data.nutrition_stats
@@ -353,17 +362,21 @@ export default {
})).filter(s => s.label) })).filter(s => s.label)
} }
// 2) nutritionDataJson / nutrition_data_json // 2) nutritionDataJson / nutrition_data_json(兼容后端驼峰与下划线;含打卡字段 actualEnergy/actualProtein
const jsonRaw = data.nutritionDataJson || data.nutrition_data_json const jsonRaw = data.nutritionDataJson || data.nutrition_data_json
if (jsonRaw) { if (jsonRaw) {
try { try {
const nutritionData = typeof jsonRaw === 'string' ? JSON.parse(jsonRaw) : jsonRaw const nutritionData = typeof jsonRaw === 'string' ? JSON.parse(jsonRaw) : jsonRaw
if (nutritionData && typeof nutritionData === 'object') { 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 [ return [
{ label: '热量(kcal)', value: nutritionData.calories != null ? String(nutritionData.calories) : '-' }, { label: '热量(kcal)', value: cal != null ? String(cal) : '-' },
{ label: '蛋白质', value: nutritionData.protein != null ? nutritionData.protein + 'g' : '-' }, { label: '蛋白质', value: this.formatNutritionValue(pro, 'g') },
{ label: '钾', value: nutritionData.potassium != null ? nutritionData.potassium + 'mg' : '-' }, { label: '钾', value: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: nutritionData.phosphorus != null ? nutritionData.phosphorus + 'mg' : '-' } { label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
] ]
} }
} catch (e) { } catch (e) {
@@ -376,13 +389,13 @@ export default {
if (dietary) { if (dietary) {
const obj = typeof dietary === 'string' ? (() => { try { return JSON.parse(dietary) } catch (_) { return null } })() : dietary const obj = typeof dietary === 'string' ? (() => { try { return JSON.parse(dietary) } catch (_) { return null } })() : dietary
if (obj && typeof obj === 'object') { if (obj && typeof obj === 'object') {
const cal = obj.calories ?? obj.energy ?? obj.calorie const cal = obj.calories ?? obj.energy ?? obj.calorie ?? obj.actualEnergy
const pro = obj.protein ?? obj.proteins const pro = obj.protein ?? obj.proteins ?? obj.actualProtein
const pot = obj.potassium ?? obj.k const pot = obj.potassium ?? obj.k
const pho = obj.phosphorus ?? obj.p const pho = obj.phosphorus ?? obj.p
return [ return [
{ label: '热量(kcal)', value: cal != null ? String(cal) : '-' }, { 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: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' } { label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
] ]
@@ -392,22 +405,26 @@ export default {
return [] 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蛋白质、热量、钾、磷 * 根据打卡详情接口返回的数据构建 nutritionStats蛋白质、热量、钾、磷
*/ */
buildNutritionStatsFromCheckinDetail(detail) { buildNutritionStatsFromCheckinDetail(detail) {
if (!detail || typeof detail !== 'object') return [] if (!detail || typeof detail !== 'object') return []
const items = []
// 热量
const energy = detail.actualEnergy ?? detail.energy const energy = detail.actualEnergy ?? detail.energy
items.push({ label: '热量(kcal)', value: energy != null ? String(energy) : '-' })
// 蛋白质
const protein = detail.actualProtein ?? detail.protein const protein = detail.actualProtein ?? detail.protein
items.push({ label: '蛋白质', value: protein != null ? (typeof protein === 'number' ? protein + 'g' : String(protein)) : '-' }) return [
// 钾、磷:打卡详情可能没有,用 - { label: '热量(kcal)', value: energy != null ? String(energy) : '-' },
items.push({ label: '', value: '-' }) { label: '蛋白质', value: this.formatNutritionValue(protein, 'g') },
items.push({ label: '', value: '-' }) { label: '', value: '-' },
return items { label: '磷', value: '-' }
]
}, },
/** /**
@@ -715,6 +732,13 @@ export default {
return 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 newLikeState = !this.isLiked
const newLikeCount = newLikeState const newLikeCount = newLikeState
? this.postData.likeCount + 1 ? this.postData.likeCount + 1
@@ -725,8 +749,9 @@ export default {
this.postData.likeCount = newLikeCount this.postData.likeCount = newLikeCount
try { try {
await apiToggleLike(this.postId, newLikeState) await apiToggleLike(postIdNum, newLikeState)
} catch (error) { } catch (error) {
console.error('[post-detail] toggleLike failed:', error)
// 回滚 // 回滚
this.isLiked = !newLikeState this.isLiked = !newLikeState
this.postData.likeCount = newLikeState this.postData.likeCount = newLikeState
@@ -747,6 +772,13 @@ export default {
return 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 newCollectState = !this.isCollected
const newCollectCount = newCollectState const newCollectCount = newCollectState
? this.postData.favoriteCount + 1 ? this.postData.favoriteCount + 1
@@ -757,12 +789,13 @@ export default {
this.postData.favoriteCount = newCollectCount this.postData.favoriteCount = newCollectCount
try { try {
await toggleCollect(this.postId, newCollectState) await toggleCollect(postIdNum, newCollectState)
uni.showToast({ uni.showToast({
title: newCollectState ? '已收藏' : '取消收藏', title: newCollectState ? '已收藏' : '取消收藏',
icon: 'none' icon: 'none'
}) })
} catch (error) { } catch (error) {
console.error('[post-detail] toggleFavorite failed:', error)
// 回滚 // 回滚
this.isCollected = !newCollectState this.isCollected = !newCollectState
this.postData.favoriteCount = newCollectState this.postData.favoriteCount = newCollectState

View File

@@ -132,15 +132,16 @@
<view class="knowledge-list"> <view class="knowledge-list">
<view class="knowledge-item" v-for="(item, index) in knowledgeList" :key="index" @tap="goToKnowledgeDetail(item)"> <view class="knowledge-item" v-for="(item, index) in knowledgeList" :key="index" @tap="goToKnowledgeDetail(item)">
<view class="knowledge-icon"> <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>
<view class="knowledge-info"> <view class="knowledge-info">
<view class="knowledge-title">{{ item.title }}</view> <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"> <view class="knowledge-meta">
<text class="meta-text">{{ item.time }}</text> <text class="meta-text">{{ item.time || '' }}</text>
<text class="meta-dot">·</text> <text class="meta-dot" v-if="item.time && item.views">·</text>
<text class="meta-text">{{ item.views }}</text> <text class="meta-text">{{ item.views != null ? item.views : '' }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -209,7 +210,12 @@ import {
]); ]);
this.recipeList = recipesRes.data?.list || recipesRes.data || []; 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.userHealthStatus = healthRes.data || { hasProfile: false, profileStatus: '尚未完成健康档案' };
this.showFunctionEntries = !!(displayConfigRes.data && displayConfigRes.data.showFunctionEntries); this.showFunctionEntries = !!(displayConfigRes.data && displayConfigRes.data.showFunctionEntries);
} catch (error) { } catch (error) {
@@ -303,7 +309,7 @@ import {
goToKnowledgeDetail(item) { goToKnowledgeDetail(item) {
if (!item) return if (!item) return
uni.navigateTo({ 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; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
overflow: hidden;
.knowledge-cover {
width: 100%;
height: 100%;
}
text { text {
font-size: 60rpx; font-size: 60rpx;