feat: 集成 KieAI 服务,移除 models-integration 子项目
- 添加 Gemini 2.5 Flash 对话接口(流式+非流式) - 添加 NanoBanana 图像生成/编辑接口 - 添加 Sora2 视频生成接口(文生视频、图生视频、去水印) - 移除 models-integration 子项目(功能已迁移至主后端) - 新增测试文档和 Playwright E2E 配置 - 更新前端页面和 API 接口 - 更新后端配置和日志处理
This commit is contained in:
@@ -92,7 +92,8 @@
|
||||
if (!/^[a-zA-Z]\w{5,17}$/i.test(that.password)) return that.$util.Tips({
|
||||
title: '密码格式错误,密码必须以字母开头,长度在6~18之间,只能包含字符、数字和下划线'
|
||||
});
|
||||
this.$refs.verify.show();
|
||||
// this.$refs.verify.show();
|
||||
this.code();
|
||||
},
|
||||
//滑块验证成功后
|
||||
handlerOnVerSuccess(data) {
|
||||
|
||||
@@ -613,7 +613,7 @@ export default {
|
||||
async sendToAI(content, type) {
|
||||
this.isLoading = true;
|
||||
|
||||
// 纯文字 或 多模态(图+文) 均走 KieAI Gemini,一次请求
|
||||
// 纯文字 / 多模态均走 KieAI Gemini:POST /api/front/kieai/gemini/chat,回复仅来自 data.choices[0].message.content
|
||||
if (type === 'text' || type === 'multimodal') {
|
||||
try {
|
||||
const messages = [{ role: 'user', content: content }];
|
||||
@@ -622,8 +622,11 @@ export default {
|
||||
if (response && response.code === 200 && response.data) {
|
||||
const data = response.data;
|
||||
const choice = data.choices && data.choices[0];
|
||||
const text = choice && choice.message && (choice.message.content || choice.message.text);
|
||||
const reply = (typeof text === 'string' ? text : (text && String(text))) || '未能获取到有效回复。';
|
||||
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 });
|
||||
} else {
|
||||
const msg = (response && response.message) || '发起对话失败';
|
||||
@@ -804,25 +807,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getAIResponse(question) {
|
||||
// 这里可以根据问题返回不同的回复
|
||||
// 实际应该调用AI API
|
||||
const responses = {
|
||||
'香蕉': '香蕉含钾量较高(每100g约330mg),对于需要控制钾摄入的透析患者来说需要谨慎食用。\n\n建议:\n• 如果血钾控制良好,可以少量食用(如半根)\n• 透析后食用更安全\n• 建议咨询您的主治医生确认\n\n您最近的血钾指标如何呢?',
|
||||
'苹果': '苹果是相对安全的水果选择,含钾量中等。建议适量食用,每天1-2个即可。',
|
||||
'蛋白质': '对于肾病患者,蛋白质的摄入需要根据CKD分期和透析情况来调整。建议咨询专业营养师制定个性化方案。'
|
||||
}
|
||||
|
||||
// 简单的关键词匹配
|
||||
for (let key in responses) {
|
||||
if (question.includes(key)) {
|
||||
return responses[key]
|
||||
}
|
||||
}
|
||||
|
||||
// 默认回复
|
||||
return `感谢您的提问。关于"${question}",我建议您:\n\n• 咨询您的主治医生获取专业建议\n• 根据您的CKD分期和透析情况调整饮食\n• 定期监测相关指标\n\n如需更详细的指导,可以联系专业营养师。`
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
// 动态切换 scrollTop 值以触发滚动更新
|
||||
|
||||
@@ -480,29 +480,40 @@ export default {
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 75rpx;
|
||||
border-radius: 50rpx;
|
||||
height: 100%;
|
||||
min-height: 75rpx;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
border-bottom: 3px solid transparent;
|
||||
box-sizing: border-box;
|
||||
color: #9ca3af;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: #9fa5c0;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #ff6b35;
|
||||
background: transparent;
|
||||
border-bottom: 3px solid #f97316;
|
||||
color: #f97316;
|
||||
font-weight: 700;
|
||||
|
||||
.tab-text {
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
color: #f97316;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tab-icon {
|
||||
color: #f97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 立即打卡按钮 -->
|
||||
<view class="checkin-btn" @click="handleCheckin">
|
||||
<text>立即打卡</text>
|
||||
<!-- 立即打卡按钮:今日已打卡则显示灰色不可点 -->
|
||||
<view
|
||||
class="checkin-btn"
|
||||
:class="{ 'checkin-btn-disabled': todaySigned }"
|
||||
@click="handleCheckin"
|
||||
>
|
||||
<text>{{ todaySigned ? '今日已打卡' : '立即打卡' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -108,6 +112,7 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
todaySigned: false,
|
||||
currentPoints: 522,
|
||||
streakDays: [
|
||||
{ day: 1, completed: false, special: false },
|
||||
@@ -170,6 +175,11 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const { checkLogin, toLogin } = require('@/libs/login.js');
|
||||
if (!checkLogin()) {
|
||||
toLogin();
|
||||
return;
|
||||
}
|
||||
this.loadCheckinData();
|
||||
},
|
||||
onShow() {
|
||||
@@ -180,31 +190,32 @@ export default {
|
||||
async loadCheckinData() {
|
||||
try {
|
||||
const { getUserPoints, getCheckinStreak, getCheckinTasks } = await import('@/api/tool.js');
|
||||
|
||||
// 并行加载数据
|
||||
const [pointsRes, streakRes, tasksRes] = await Promise.all([
|
||||
const { getSignGet } = await import('@/api/user.js');
|
||||
|
||||
const [pointsRes, streakRes, tasksRes, signRes] = await Promise.all([
|
||||
getUserPoints().catch(() => ({ data: { points: 0 } })),
|
||||
getCheckinStreak().catch(() => ({ data: { streakDays: 7, currentStreak: 0 } })),
|
||||
getCheckinTasks().catch(() => ({ data: { tasks: [] } }))
|
||||
getCheckinTasks().catch(() => ({ data: { tasks: [] } })),
|
||||
getSignGet().catch(() => ({ data: { today: false } }))
|
||||
]);
|
||||
|
||||
// 更新积分
|
||||
if (pointsRes.data) {
|
||||
this.currentPoints = pointsRes.data.points || 0;
|
||||
|
||||
if (signRes && signRes.data && signRes.data.today) {
|
||||
this.todaySigned = true;
|
||||
}
|
||||
|
||||
// 更新连续打卡天数
|
||||
|
||||
if (pointsRes.data) {
|
||||
this.currentPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? 0;
|
||||
}
|
||||
|
||||
if (streakRes.data) {
|
||||
const streak = streakRes.data.currentStreak || 0;
|
||||
// 生成连续打卡天数数组
|
||||
this.streakDays = Array.from({ length: 7 }, (_, i) => ({
|
||||
day: i + 1,
|
||||
completed: i < streak,
|
||||
special: i === 6 // 第7天特殊标记
|
||||
special: i === 6
|
||||
}));
|
||||
}
|
||||
|
||||
// 更新任务列表
|
||||
|
||||
if (tasksRes.data && tasksRes.data.tasks && tasksRes.data.tasks.length > 0) {
|
||||
this.taskList = tasksRes.data.tasks.map(task => ({
|
||||
id: task.id || task.taskId,
|
||||
@@ -224,16 +235,39 @@ export default {
|
||||
url: '/pages/tool/points-rules'
|
||||
})
|
||||
},
|
||||
handleCheckin() {
|
||||
async handleCheckin() {
|
||||
if (this.todaySigned) {
|
||||
uni.showToast({ title: '今日已打卡', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { setSignIntegral, getUserInfo } = await import('@/api/user.js');
|
||||
const { getUserPoints } = await import('@/api/tool.js');
|
||||
// 子问题 A:不在 API 成功前修改 currentPoints,避免积分提前跳变
|
||||
await setSignIntegral();
|
||||
this.todaySigned = true;
|
||||
// 子问题 B:打卡成功后用服务端最新积分刷新,优先 GET /api/front/user/info,禁止前端本地 +30
|
||||
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;
|
||||
} else {
|
||||
const pointsRes = await getUserPoints();
|
||||
if (pointsRes && pointsRes.data) {
|
||||
const serverPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? pointsRes.data.availablePoints ?? 0;
|
||||
this.currentPoints = serverPoints;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败';
|
||||
if (msg.includes('今日已签到') || msg.includes('不可重复')) {
|
||||
this.todaySigned = true;
|
||||
}
|
||||
uni.showToast({ title: msg, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/checkin-publish'
|
||||
})
|
||||
// 更新打卡状态
|
||||
const todayIndex = this.getTodayIndex()
|
||||
if (todayIndex >= 0 && todayIndex < this.streakDays.length) {
|
||||
// this.streakDays[todayIndex].completed = true
|
||||
this.currentPoints += 30 // 每日打卡获得30积分
|
||||
}
|
||||
});
|
||||
},
|
||||
getTodayIndex() {
|
||||
// 根据连续打卡数据计算当前是第几天
|
||||
@@ -388,7 +422,7 @@ export default {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 16rpx 48rpx rgba(255, 107, 53, 0.35);
|
||||
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
@@ -396,6 +430,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-btn-disabled {
|
||||
background: linear-gradient(135deg, #c4c4c4 0%, #e0e0e0 100%);
|
||||
box-shadow: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 通用区块样式 */
|
||||
.section {
|
||||
margin: 48rpx 32rpx 0;
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<image class="share-icon" :src="iconShare" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 数据来自缓存时的提示 -->
|
||||
<view v-if="loadError" class="cache-notice">
|
||||
<text>当前数据来自缓存,可能不是最新</text>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<!-- 食物大图 -->
|
||||
@@ -90,6 +95,10 @@ export default {
|
||||
iconShare: 'https://www.figma.com/api/mcp/asset/f9f0d7b9-89c0-48d4-9e04-7140229e42f0',
|
||||
iconSearch: 'https://www.figma.com/api/mcp/asset/aa6bb75b-0a9d-43cb-aaa4-6a71993fbd4d',
|
||||
loading: false,
|
||||
/** 加载失败时的具体错误信息,用于调试;有值时页面会展示「当前数据来自缓存」提示 */
|
||||
loadError: '',
|
||||
/** 入参 id/name,用于 API 失败时用 name 填充展示 */
|
||||
pageParams: { id: '', name: '' },
|
||||
foodData: {
|
||||
name: '',
|
||||
category: '',
|
||||
@@ -124,13 +133,20 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.loadFoodData(options.id)
|
||||
this.pageParams = { id: options.id || '', name: options.name || '' }
|
||||
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id
|
||||
console.log('[food-detail] onLoad params:', this.pageParams)
|
||||
const rawId = options.id
|
||||
const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId))
|
||||
if (isNumericId) {
|
||||
this.loadFoodData(Number(rawId))
|
||||
} else if (options.name) {
|
||||
this.loadFoodData(options.name)
|
||||
// 无有效 id 仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException),直接展示默认数据 + 当前名称
|
||||
this.loadError = '暂无该食物详情数据,展示参考数据'
|
||||
this.applyDefaultFoodData(false)
|
||||
this.foodData.name = decodeURIComponent(String(options.name))
|
||||
} else {
|
||||
// 无参数时使用默认数据
|
||||
this.foodData = { ...this.defaultFoodData }
|
||||
this.applyDefaultFoodData()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -145,8 +161,29 @@ export default {
|
||||
url: `/pages/tool/food-encyclopedia?category=${this.foodData.categoryType || 'all'}`
|
||||
})
|
||||
},
|
||||
/** 用 defaultFoodData 填充页面,保证 keyNutrients/nutritionTable 为非空数组以便 .nutrient-card / .nutrition-row 正常渲染;clearError 为 true 时顺带清空 loadError */
|
||||
applyDefaultFoodData(clearError = true) {
|
||||
if (clearError) this.loadError = ''
|
||||
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 || [])]
|
||||
}
|
||||
},
|
||||
/** 保证数组非空,空时返回 fallback 的副本,用于 API 解析结果避免空数组导致列表不渲染 */
|
||||
ensureNonEmptyArray(arr, fallback) {
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr
|
||||
return Array.isArray(fallback) ? [...fallback] : []
|
||||
},
|
||||
async loadFoodData(id) {
|
||||
this.loading = true
|
||||
this.loadError = ''
|
||||
// 打印 API 请求参数便于确认(后端需要 Long 类型 id)
|
||||
console.log('[food-detail] getFoodDetail request param:', { id, type: typeof id })
|
||||
try {
|
||||
const res = await getFoodDetail(id)
|
||||
const data = res.data || res
|
||||
@@ -158,17 +195,26 @@ export default {
|
||||
categoryType: data.categoryType || data.categoryCode || 'all',
|
||||
safetyTag: data.safetyTag || data.safety || this.getSafetyTag(data),
|
||||
image: data.image || data.imageUrl || data.coverImage || this.defaultFoodData.image,
|
||||
keyNutrients: this.parseKeyNutrients(data),
|
||||
nutritionTable: this.parseNutritionTable(data)
|
||||
keyNutrients: this.ensureNonEmptyArray(this.parseKeyNutrients(data), this.defaultFoodData.keyNutrients),
|
||||
nutritionTable: this.ensureNonEmptyArray(this.parseNutritionTable(data), this.defaultFoodData.nutritionTable)
|
||||
}
|
||||
} else {
|
||||
// API 返回空数据,使用默认数据
|
||||
this.foodData = { ...this.defaultFoodData }
|
||||
this.applyDefaultFoodData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载食物数据失败:', error)
|
||||
// 加载失败使用默认数据
|
||||
this.foodData = { ...this.defaultFoodData }
|
||||
const errMsg = (error && (error.message || error.msg || error)) ? String(error.message || error.msg || error) : '未知错误'
|
||||
console.error('[food-detail] 加载食物数据失败:', error)
|
||||
// a. 将 loadError 置为具体错误信息(用于调试)
|
||||
this.loadError = errMsg
|
||||
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面;不清空 loadError 以便展示「当前数据来自缓存」提示
|
||||
this.applyDefaultFoodData(false)
|
||||
// 若有入参 name,用其覆盖展示名称,避免显示默认「五谷香」
|
||||
if (this.pageParams && this.pageParams.name) {
|
||||
try {
|
||||
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
|
||||
} catch (e) {}
|
||||
}
|
||||
uni.showToast({
|
||||
title: '数据加载失败',
|
||||
icon: 'none'
|
||||
@@ -237,6 +283,18 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 数据来自缓存提示 */
|
||||
.cache-notice {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #fff8e1;
|
||||
border-bottom: 1rpx solid #ffe082;
|
||||
text-align: center;
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #f57c00;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分享按钮 */
|
||||
.share-btn-top {
|
||||
position: fixed;
|
||||
@@ -457,6 +515,10 @@ export default {
|
||||
&.medium {
|
||||
background: #ffa500;
|
||||
}
|
||||
|
||||
&.high {
|
||||
background: #e53935;
|
||||
}
|
||||
}
|
||||
|
||||
.nutrition-label {
|
||||
|
||||
@@ -99,33 +99,33 @@
|
||||
<view
|
||||
class="food-item"
|
||||
v-for="(item, index) in filteredFoodList"
|
||||
:key="index"
|
||||
:key="item.id != null ? item.id : index"
|
||||
@click="goToFoodDetail(item)"
|
||||
>
|
||||
<view class="food-image-wrapper">
|
||||
<image class="food-image" :src="item.image" mode="aspectFill"></image>
|
||||
<image class="food-image" :src="getFoodImage(item)" mode="aspectFill"></image>
|
||||
<view v-if="item.warning" class="warning-badge">⚠️</view>
|
||||
</view>
|
||||
<view class="food-info">
|
||||
<view class="food-header">
|
||||
<view class="food-name-wrapper">
|
||||
<text class="food-name">{{ item.name }}</text>
|
||||
<view class="safety-tag" :class="item.safetyClass">
|
||||
<text>{{ item.safety }}</text>
|
||||
<view class="safety-tag" :class="item.safetyClass || 'safe'">
|
||||
<text>{{ item.safety || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-badge">
|
||||
<view v-if="item.category" class="category-badge">
|
||||
<text>{{ item.category }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nutrition-list">
|
||||
<view
|
||||
class="nutrition-item"
|
||||
v-for="(nut, idx) in item.nutrition"
|
||||
v-for="(nut, idx) in (item.nutrition || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<text class="nutrition-label">{{ nut.label }}</text>
|
||||
<text class="nutrition-value" :class="nut.colorClass">{{ nut.value }}</text>
|
||||
<text class="nutrition-value" :class="nut.colorClass || 'green'">{{ nut.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -137,12 +137,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { HTTP_REQUEST_URL } from '@/config/app.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
// 无图时的占位图(灰色背景,与 .food-image-wrapper 背景一致)
|
||||
const defaultPlaceholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZTRlNWU3Ii8+PC9zdmc+';
|
||||
return {
|
||||
searchText: '',
|
||||
currentCategory: 'all',
|
||||
searchTimer: null,
|
||||
defaultPlaceholder,
|
||||
foodList: [
|
||||
{
|
||||
name: '香蕉',
|
||||
@@ -251,13 +256,66 @@ export default {
|
||||
page: 1,
|
||||
limit: 100
|
||||
});
|
||||
if (result.data && result.data.list) {
|
||||
this.foodList = result.data.list;
|
||||
}
|
||||
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
|
||||
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
|
||||
} catch (error) {
|
||||
console.error('加载食物列表失败:', error);
|
||||
}
|
||||
},
|
||||
getFoodImage(item) {
|
||||
const raw = item.imageUrl || item.image || item.img || '';
|
||||
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;
|
||||
},
|
||||
normalizeFoodItem(item) {
|
||||
const safetyMap = {
|
||||
suitable: { safety: '放心吃', safetyClass: 'safe' },
|
||||
moderate: { safety: '限量吃', safetyClass: 'limited' },
|
||||
restricted: { safety: '谨慎吃', safetyClass: 'careful' },
|
||||
forbidden: { safety: '谨慎吃', safetyClass: 'careful' }
|
||||
};
|
||||
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 || '';
|
||||
const imageUrl = (rawImg && (rawImg.startsWith('//') || rawImg.startsWith('http'))) ? rawImg : (rawImg && rawImg.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawImg : rawImg);
|
||||
const image = imageUrl || '';
|
||||
|
||||
// 营养简介:优先 item.nutrition,其次 item.nutrients(兼容后端不同字段),否则由扁平字段组装
|
||||
let nutrition = item.nutrition;
|
||||
if (Array.isArray(nutrition) && nutrition.length > 0) {
|
||||
nutrition = nutrition.map(n => ({
|
||||
label: n.label || n.name || n.labelName || '—',
|
||||
value: n.value != null ? String(n.value) : '—',
|
||||
colorClass: n.colorClass || 'green'
|
||||
}));
|
||||
} else if (Array.isArray(item.nutrients) && item.nutrients.length > 0) {
|
||||
nutrition = item.nutrients.map(n => ({
|
||||
label: n.label || n.name || n.labelName || '—',
|
||||
value: n.value != null ? String(n.value) : '—',
|
||||
colorClass: n.colorClass || 'green'
|
||||
}));
|
||||
} else {
|
||||
nutrition = [];
|
||||
const push = (label, val, unit) => { if (val != null && val !== '') nutrition.push({ label, value: String(val) + (unit || ''), colorClass: 'green' }); };
|
||||
push('能量', item.energy, 'kcal');
|
||||
push('蛋白质', item.protein, 'g');
|
||||
push('钾', item.potassium, 'mg');
|
||||
push('磷', item.phosphorus, 'mg');
|
||||
push('钠', item.sodium, 'mg');
|
||||
push('钙', item.calcium, 'mg');
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
image,
|
||||
imageUrl: image || undefined,
|
||||
category: item.category || '',
|
||||
safety: safety.safety,
|
||||
safetyClass: safety.safetyClass,
|
||||
nutrition
|
||||
};
|
||||
},
|
||||
async selectCategory(category) {
|
||||
this.currentCategory = category;
|
||||
// 切换分类时清空搜索文本,避免搜索状态与分类状态冲突
|
||||
@@ -281,9 +339,8 @@ export default {
|
||||
page: 1,
|
||||
limit: 100
|
||||
});
|
||||
if (result.data && result.data.list) {
|
||||
this.foodList = result.data.list;
|
||||
}
|
||||
const rawList = result.data && (result.data.list || (Array.isArray(result.data) ? result.data : []));
|
||||
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item));
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
}
|
||||
@@ -293,9 +350,14 @@ export default {
|
||||
}, 300);
|
||||
},
|
||||
goToFoodDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/food-detail?id=${item.name}`
|
||||
})
|
||||
// 后端详情接口仅接受 Long 类型 id,仅在有有效数字 id 时传 id;始终传 name 供详情页失败时展示
|
||||
const rawId = item.id != null ? item.id : ''
|
||||
const numericId = (rawId !== '' && rawId !== undefined && !isNaN(Number(rawId))) ? Number(rawId) : null
|
||||
const namePart = item.name ? `&name=${encodeURIComponent(item.name)}` : ''
|
||||
const url = numericId !== null
|
||||
? `/pages/tool/food-detail?id=${numericId}${namePart}`
|
||||
: `/pages/tool/food-detail?name=${encodeURIComponent(item.name || '')}`
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
msh_single_uniapp/pages/tool/knowledge-detail.vue
Normal file
156
msh_single_uniapp/pages/tool/knowledge-detail.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<view class="knowledge-detail-page">
|
||||
<scroll-view class="content-scroll" scroll-y v-if="detail.title">
|
||||
<view class="detail-header">
|
||||
<view class="detail-title">{{ detail.title }}</view>
|
||||
<view class="detail-meta">
|
||||
<text class="meta-item">{{ detail.time }}</text>
|
||||
<text class="meta-dot">·</text>
|
||||
<text class="meta-item">{{ detail.views }} 阅读</text>
|
||||
</view>
|
||||
<image
|
||||
v-if="detail.coverImage || detail.cover_image"
|
||||
class="detail-cover"
|
||||
:src="detail.coverImage || detail.cover_image"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
<view class="detail-body">
|
||||
<jyf-parser :html="detail.content || ''" :tag-style="tagStyle" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else-if="loading" class="loading-wrap">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="error" class="error-wrap">
|
||||
<text>{{ error }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getKnowledgeDetail } from '@/api/tool.js';
|
||||
import parser from '@/components/jyf-parser/jyf-parser';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'jyf-parser': parser
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: '',
|
||||
detail: {},
|
||||
loading: true,
|
||||
error: '',
|
||||
tagStyle: {
|
||||
img: 'width:100%;display:block;',
|
||||
table: 'width:100%',
|
||||
video: 'width:100%'
|
||||
}
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.id = options.id;
|
||||
} else {
|
||||
this.loading = false;
|
||||
this.error = '缺少文章 ID';
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (this.id) {
|
||||
this.loadDetail();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatTime(val) {
|
||||
if (!val) return '';
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return String(val);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
},
|
||||
async loadDetail() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await getKnowledgeDetail(this.id);
|
||||
if (res && res.data) {
|
||||
const d = res.data;
|
||||
this.detail = {
|
||||
...d,
|
||||
time: this.formatTime(d.publishedAt || d.updatedAt || d.createdAt),
|
||||
views: d.viewCount != null ? d.viewCount : (d.views != null ? d.views : 0),
|
||||
coverImage: d.coverImage || d.cover_image || ''
|
||||
};
|
||||
uni.setNavigationBarTitle({
|
||||
title: (d.title || '知识详情').substring(0, 12) + (d.title && d.title.length > 12 ? '...' : '')
|
||||
});
|
||||
} else {
|
||||
this.error = '加载失败';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('知识详情加载失败:', e);
|
||||
this.error = (e && (e.message || e.msg)) || '加载失败,请重试';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.knowledge-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
.content-scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
.detail-header {
|
||||
padding: 24rpx 30rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.detail-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #282828;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.detail-meta {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.meta-dot {
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
.detail-cover {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.detail-body {
|
||||
padding: 24rpx 30rpx;
|
||||
background: #fff;
|
||||
min-height: 200rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.loading-wrap,
|
||||
.error-wrap {
|
||||
padding: 80rpx 30rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.error-wrap {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 饮食指南Tab内容 -->
|
||||
<!-- 饮食指南Tab内容(来自 v2_knowledge,type=guide) -->
|
||||
<view v-if="currentTab === 'guide'" class="tab-content">
|
||||
<view class="knowledge-list">
|
||||
<view
|
||||
@@ -67,7 +67,10 @@
|
||||
:key="index"
|
||||
@click="goToDetail(item)"
|
||||
>
|
||||
<view class="knowledge-icon">
|
||||
<view class="knowledge-cover" v-if="item.coverImage || item.cover_image">
|
||||
<image :src="item.coverImage || item.cover_image" mode="aspectFill" class="cover-img" />
|
||||
</view>
|
||||
<view class="knowledge-icon" v-else>
|
||||
<text>{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="knowledge-info">
|
||||
@@ -86,7 +89,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 科普文章Tab内容 -->
|
||||
<!-- 科普文章Tab内容(来自 v2_knowledge,type=article) -->
|
||||
<view v-if="currentTab === 'articles'" class="tab-content">
|
||||
<view class="knowledge-list">
|
||||
<view
|
||||
@@ -95,7 +98,10 @@
|
||||
:key="index"
|
||||
@click="goToDetail(item)"
|
||||
>
|
||||
<view class="knowledge-icon">
|
||||
<view class="knowledge-cover" v-if="item.coverImage || item.cover_image">
|
||||
<image :src="item.coverImage || item.cover_image" mode="aspectFill" class="cover-img" />
|
||||
</view>
|
||||
<view class="knowledge-icon" v-else>
|
||||
<text>{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="knowledge-info">
|
||||
@@ -177,37 +183,72 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 如果携带id参数,自动切换到对应tab并加载
|
||||
if (options && options.id) {
|
||||
// 有id时切换到文章tab展示详情
|
||||
// 有 id 时切换到科普文章 tab 并加载列表
|
||||
this.switchTab('articles');
|
||||
} else {
|
||||
this.loadKnowledgeList();
|
||||
// 无 id 时保持当前 tab(nutrients);切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
|
||||
this.currentTab = 'nutrients';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatKnowledgeTime(val) {
|
||||
if (!val) return '';
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return String(val);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
},
|
||||
async loadKnowledgeList() {
|
||||
// 营养素列表使用本地静态数据,不从API加载
|
||||
if (this.currentTab === 'nutrients') {
|
||||
return;
|
||||
}
|
||||
|
||||
// type 与后端一致:guide / article
|
||||
const typeParam = this.currentTab === 'guide' ? 'guide' : 'article';
|
||||
try {
|
||||
const { getKnowledgeList } = await import('@/api/tool.js');
|
||||
const result = await getKnowledgeList({
|
||||
type: this.currentTab === 'guide' ? 'guide' : 'article',
|
||||
type: typeParam,
|
||||
page: 1,
|
||||
limit: 50
|
||||
});
|
||||
if (result.data && result.data.list) {
|
||||
if (this.currentTab === 'guide') {
|
||||
this.guideList = result.data.list;
|
||||
} else {
|
||||
this.articleList = result.data.list;
|
||||
// 兼容 result.data.list 或 result.data 为数组
|
||||
let rawList = [];
|
||||
if (result && result.data) {
|
||||
if (Array.isArray(result.data.list)) {
|
||||
rawList = result.data.list;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
rawList = result.data;
|
||||
}
|
||||
}
|
||||
const list = rawList.map(item => ({
|
||||
...item,
|
||||
desc: item.desc || item.summary || '',
|
||||
time: item.time || (item.publishedAt || item.createdAt ? this.formatKnowledgeTime(item.publishedAt || item.createdAt) : ''),
|
||||
views: item.views != null ? item.views : (item.viewCount != null ? item.viewCount : 0),
|
||||
icon: item.icon || '📄',
|
||||
coverImage: item.coverImage || item.cover_image || ''
|
||||
}));
|
||||
if (this.currentTab === 'guide') {
|
||||
this.guideList = list;
|
||||
} else if (this.currentTab === 'articles') {
|
||||
this.articleList = list;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载知识列表失败:', error);
|
||||
uni.showToast({
|
||||
title: (error && (error.message || error.msg)) || '加载列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
// 确保列表始终为数组,不设为 undefined
|
||||
if (this.currentTab === 'guide') {
|
||||
this.guideList = this.guideList ?? [];
|
||||
} else if (this.currentTab === 'articles') {
|
||||
this.articleList = this.articleList ?? [];
|
||||
}
|
||||
}
|
||||
},
|
||||
async switchTab(tab) {
|
||||
@@ -220,16 +261,19 @@ export default {
|
||||
})
|
||||
},
|
||||
goToDetail(item) {
|
||||
if (!item) return
|
||||
// 饮食指南和科普文章使用知识详情页(富文本内容),而非营养素详情页
|
||||
if (item.knowledgeId || item.id) {
|
||||
const id = item.knowledgeId || item.id
|
||||
// 如果有content(富文本),使用内嵌webview或者新闻详情页
|
||||
// 跳转到文章详情(使用现有新闻详情页展示富文本内容)
|
||||
uni.navigateTo({
|
||||
url: `/pages/news/news_details/index?id=${id}`
|
||||
})
|
||||
if (!item) {
|
||||
uni.showToast({ title: '暂无详情', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const id = item.knowledgeId ?? item.id;
|
||||
if (id === undefined || id === null || id === '') {
|
||||
uni.showToast({ title: '暂无详情', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// 饮食指南、科普文章使用知识详情页(调用 tool/knowledge/detail 接口)
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/knowledge-detail?id=${id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,6 +484,18 @@ export default {
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.knowledge-cover {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.knowledge-icon {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营养统计卡片 -->
|
||||
<view class="nutrition-stats-card" v-if="postData.nutritionStats && postData.nutritionStats.length > 0">
|
||||
<!-- 营养统计卡片:仅根据是否有数据展示 -->
|
||||
<view class="nutrition-stats-card" v-if="nutritionStatsLength > 0">
|
||||
<view class="stats-header">
|
||||
<view class="stats-title">
|
||||
<text class="title-icon">📊</text>
|
||||
@@ -233,6 +233,7 @@
|
||||
<script>
|
||||
import {
|
||||
getCommunityDetail,
|
||||
getCheckinDetail,
|
||||
toggleLike as apiToggleLike,
|
||||
toggleCollect,
|
||||
toggleFollow as apiToggleFollow,
|
||||
@@ -284,6 +285,13 @@ export default {
|
||||
commentText: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 营养统计条数,用于卡片显示条件(避免 postData.nutritionStats 未定义时报错)
|
||||
nutritionStatsLength() {
|
||||
const stats = this.postData && this.postData.nutritionStats
|
||||
return Array.isArray(stats) ? stats.length : 0
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.postId = options.id
|
||||
@@ -301,6 +309,11 @@ export default {
|
||||
|
||||
// 格式化帖子数据
|
||||
this.formatPostData(data)
|
||||
|
||||
// 若详情接口未返回营养数据且有关联打卡记录,则根据打卡详情补充营养统计(等待完成后再结束加载)
|
||||
if (this.postData.nutritionStats.length === 0 && (data.checkInRecordId != null)) {
|
||||
await this.fillNutritionStatsFromCheckin(data.checkInRecordId)
|
||||
}
|
||||
|
||||
// 同步状态
|
||||
this.isLiked = data.isLiked || false
|
||||
@@ -324,6 +337,95 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从详情接口返回的数据中构建 nutritionStats 数组
|
||||
* 支持:nutritionStats/nutrition_stats 数组、nutritionDataJson/nutrition_data_json、dietaryData/mealData 等
|
||||
*/
|
||||
buildNutritionStatsFromDetailData(data) {
|
||||
if (!data) return []
|
||||
|
||||
// 1) 后端直接返回的 stat 数组(兼容不同命名)
|
||||
const rawStats = data.nutritionStats || data.nutrition_stats
|
||||
if (Array.isArray(rawStats) && rawStats.length > 0) {
|
||||
return rawStats.map(s => ({
|
||||
label: s.label || s.name || '',
|
||||
value: s.value != null ? String(s.value) : '-'
|
||||
})).filter(s => s.label)
|
||||
}
|
||||
|
||||
// 2) nutritionDataJson / nutrition_data_json
|
||||
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') {
|
||||
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' : '-' }
|
||||
]
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 3) dietaryData / mealData / dietary_data / meal_data(饮食打卡对象)
|
||||
const dietary = data.dietaryData || data.dietary_data || data.mealData || data.meal_data
|
||||
if (dietary) {
|
||||
const obj = typeof dietary === 'string' ? (() => { try { return JSON.parse(dietary) } catch (_) { return null } })() : dietary
|
||||
if (obj && typeof obj === 'object') {
|
||||
const cal = obj.calories ?? obj.energy ?? obj.calorie
|
||||
const pro = obj.protein ?? obj.proteins
|
||||
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: pot != null ? (typeof pot === 'number' ? pot + 'mg' : String(pot)) : '-' },
|
||||
{ label: '磷', value: pho != null ? (typeof pho === 'number' ? pho + 'mg' : String(pho)) : '-' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据打卡详情接口返回的数据构建 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
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据打卡记录 ID 拉取打卡详情,并填充 postData.nutritionStats(不阻塞主流程)
|
||||
*/
|
||||
async fillNutritionStatsFromCheckin(checkInRecordId) {
|
||||
try {
|
||||
const res = await getCheckinDetail(checkInRecordId)
|
||||
const detail = res.data || res
|
||||
const stats = this.buildNutritionStatsFromCheckinDetail(detail)
|
||||
if (stats.length > 0) {
|
||||
this.postData.nutritionStats = stats
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('拉取打卡详情补充营养数据失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化帖子数据
|
||||
formatPostData(data) {
|
||||
// 解析图片
|
||||
@@ -368,23 +470,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 解析营养数据
|
||||
let nutritionStats = []
|
||||
if (data.nutritionDataJson) {
|
||||
try {
|
||||
const nutritionData = typeof data.nutritionDataJson === 'string'
|
||||
? JSON.parse(data.nutritionDataJson)
|
||||
: data.nutritionDataJson
|
||||
nutritionStats = [
|
||||
{ value: nutritionData.calories || '-', label: '热量(kcal)' },
|
||||
{ value: nutritionData.protein ? nutritionData.protein + 'g' : '-', label: '蛋白质' },
|
||||
{ value: nutritionData.potassium ? nutritionData.potassium + 'mg' : '-', label: '钾' },
|
||||
{ value: nutritionData.phosphorus ? nutritionData.phosphorus + 'mg' : '-', label: '磷' }
|
||||
]
|
||||
} catch (e) {
|
||||
nutritionStats = []
|
||||
}
|
||||
}
|
||||
// 解析营养数据:支持多种后端字段名,以及从打卡数据计算
|
||||
let nutritionStats = this.buildNutritionStatsFromDetailData(data)
|
||||
|
||||
// 提取纯文本内容
|
||||
let description = ''
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<view class="post-image" v-if="item.image">
|
||||
<image :src="item.image" mode="aspectFill" lazy-load></image>
|
||||
<!-- 类型标签 -->
|
||||
<view class="meal-tag">{{ item.mealType }}</view>
|
||||
<view class="meal-tag">{{ getMealTypeLabel(item.mealType) }}</view>
|
||||
<!-- 视频标记 -->
|
||||
<view class="video-badge" v-if="item.hasVideo || item.videoUrl">
|
||||
<text class="badge-icon">🎬</text>
|
||||
@@ -73,7 +73,7 @@
|
||||
<!-- 内容区域 -->
|
||||
<view class="post-content">
|
||||
<!-- 无图片时显示类型标签 -->
|
||||
<view class="type-tag" v-if="!item.image">{{ item.mealType }}</view>
|
||||
<view class="type-tag" v-if="!item.image">{{ getMealTypeLabel(item.mealType) }}</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="post-title">{{ item.title }}</view>
|
||||
@@ -426,6 +426,21 @@ export default {
|
||||
return String(count)
|
||||
},
|
||||
|
||||
// 帖子类型英文转中文显示(仅用于展示,保证 label 均为中文)
|
||||
getMealTypeLabel(mealType) {
|
||||
if (!mealType) return '分享'
|
||||
const map = {
|
||||
breakfast: '早餐',
|
||||
lunch: '午餐',
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
share: '分享',
|
||||
checkin: '打卡'
|
||||
}
|
||||
const lower = String(mealType).toLowerCase()
|
||||
return map[lower] != null ? map[lower] : '分享'
|
||||
},
|
||||
|
||||
// 格式化标签显示
|
||||
formatTag(tag) {
|
||||
if (!tag) return ''
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四大功能入口 -->
|
||||
<view class="function-grid">
|
||||
<!-- 四大功能入口(根据系统配置 eb_system_config.field01=1 时显示) -->
|
||||
<view class="function-grid" v-if="showFunctionEntries">
|
||||
<view class="function-item calculator" @tap="goToCalculator">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
@@ -64,7 +64,7 @@
|
||||
<view class="function-item nutrition-knowledge" @tap="goToNutritionKnowledge">
|
||||
<view class="function-content">
|
||||
<view class="function-text">
|
||||
<view class="function-title">营养知识</view>
|
||||
<view class="function-title">健康知识</view>
|
||||
<view class="function-desc">专业营养指导</view>
|
||||
</view>
|
||||
<view class="function-icon">
|
||||
@@ -156,7 +156,8 @@
|
||||
import {
|
||||
getRecommendedRecipes,
|
||||
getRecommendedKnowledge,
|
||||
getUserHealthStatus
|
||||
getUserHealthStatus,
|
||||
getHomeDisplayConfig
|
||||
} from '@/api/tool.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { toLogin, checkLogin } from '@/libs/login.js';
|
||||
@@ -177,6 +178,7 @@ import {
|
||||
hasProfile: false,
|
||||
profileStatus: '尚未完成健康档案'
|
||||
},
|
||||
showFunctionEntries: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
@@ -198,16 +200,18 @@ import {
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 并行加载数据
|
||||
const [recipesRes, knowledgeRes, healthRes] = await Promise.all([
|
||||
// 并行加载数据(含首页展示配置:field01=1 时显示四大功能入口)
|
||||
const [recipesRes, knowledgeRes, healthRes, displayConfigRes] = await Promise.all([
|
||||
getRecommendedRecipes({ limit: 2 }).catch(() => ({ data: [] })),
|
||||
getRecommendedKnowledge({ limit: 2 }).catch(() => ({ data: [] })),
|
||||
getUserHealthStatus().catch(() => ({ data: { hasProfile: false, profileStatus: '尚未完成健康档案' } }))
|
||||
getUserHealthStatus().catch(() => ({ data: { hasProfile: false, profileStatus: '尚未完成健康档案' } })),
|
||||
getHomeDisplayConfig().catch(() => ({ data: { showFunctionEntries: false } }))
|
||||
]);
|
||||
|
||||
this.recipeList = recipesRes.data?.list || recipesRes.data || [];
|
||||
this.knowledgeList = knowledgeRes.data?.list || knowledgeRes.data || [];
|
||||
this.userHealthStatus = healthRes.data || { hasProfile: false, profileStatus: '尚未完成健康档案' };
|
||||
this.showFunctionEntries = !!(displayConfigRes.data && displayConfigRes.data.showFunctionEntries);
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
uni.showToast({
|
||||
@@ -226,9 +230,14 @@ import {
|
||||
},
|
||||
// 跳转到打卡页面
|
||||
handleCheckin() {
|
||||
if (!checkLogin()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => toLogin(), 500);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/tool/checkin'
|
||||
})
|
||||
});
|
||||
},
|
||||
// 跳转到食谱计算器
|
||||
goToCalculator() {
|
||||
|
||||
@@ -435,8 +435,9 @@
|
||||
if (!/^1(3|4|5|7|8|9|6)\d{9}$/i.test(that.account)) return that.$util.Tips({
|
||||
title: '请输入正确的手机号码'
|
||||
});
|
||||
if (that.formItem == 2) that.type = "register";
|
||||
that.$refs.verify.show();
|
||||
if (that.formItem == 2) that.type = "register";
|
||||
this.codeSend();
|
||||
// that.$refs.verify.show();
|
||||
},
|
||||
navTap: function(index) {
|
||||
this.current = index;
|
||||
|
||||
Reference in New Issue
Block a user