Files
msh-system/msh_single_uniapp/pages/tool/nutrition-knowledge.vue
msh-agent c1857ce852 fix: 修复关注按钮相关问题
- 食谱详情页: 修复 applyDefaultData 中未定义变量 id 的问题
- 帖子详情页: 优化 toggleFollow 方法,提前校验 author.id,兼容多种后端字段
- 为帖子详情页已关注状态添加灰色样式
2026-03-09 18:56:53 +08:00

597 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="nutrition-page">
<!-- Tab切换 -->
<view class="tab-container">
<view
class="tab-item"
:class="{ active: currentTab === 'nutrients' }"
@click="switchTab('nutrients')"
>
<text>营养素</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'guide' }"
@click="switchTab('guide')"
>
<text>饮食指南</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'articles' }"
@click="switchTab('articles')"
>
<text>科普文章</text>
</view>
</view>
<!-- 内容区域微信小程序要求 scroll-view 有明确高度 -->
<scroll-view class="content-scroll" scroll-y :style="{ height: scrollViewHeight }">
<!-- 营养素百科Tab内容 -->
<view v-if="currentTab === 'nutrients'" class="tab-content">
<view class="intro-text">了解关键营养素科学管理慢性肾病饮食</view>
<view class="nutrient-list">
<view
class="nutrient-card"
v-for="(item, index) in nutrientList"
:key="index"
@click="goToNutrientDetail" :data-nutrient-index="index"
>
<view class="nutrient-icon-wrapper">
<text class="nutrient-icon">{{ item.icon }}</text>
</view>
<view class="nutrient-info">
<view class="nutrient-header">
<text class="nutrient-name">{{ item.name }}</text>
<text class="nutrient-english">{{ item.english }}</text>
</view>
<text class="nutrient-desc">{{ item.description }}</text>
<view class="nutrient-tag" :class="item.tagClass">
<text>{{ item.tag }}</text>
</view>
</view>
<view class="arrow-icon">
<text></text>
</view>
</view>
</view>
</view>
<!-- 饮食指南Tab内容来自 v2_knowledgetype=guide -->
<view v-if="currentTab === 'guide'" class="tab-content">
<view class="knowledge-list">
<view
class="knowledge-item"
v-for="(item, index) in (guideList || [])"
:key="item.knowledgeId || item.id || index"
@click="goToDetail($event, item, index, 'guide')"
>
<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">
<view class="knowledge-title">{{ item.title }}</view>
<view class="knowledge-desc">{{ item.desc }}</view>
<view class="knowledge-meta">
<text class="meta-text">{{ item.time }}</text>
<text class="meta-dot">·</text>
<text class="meta-text">{{ item.views }}</text>
</view>
</view>
</view>
</view>
<view v-if="(guideList || []).length === 0" class="empty-placeholder">
<text>暂无饮食指南数据</text>
</view>
</view>
<!-- 科普文章Tab内容来自 v2_knowledgetype=article -->
<view v-if="currentTab === 'articles'" class="tab-content">
<view class="knowledge-list">
<view
class="knowledge-item"
v-for="(item, index) in (articleList || [])"
:key="item.knowledgeId || item.id || index"
@click="goToDetail($event, item, index, 'articles')"
>
<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">
<view class="knowledge-title">{{ item.title }}</view>
<view class="knowledge-desc">{{ item.desc }}</view>
<view class="knowledge-meta">
<text class="meta-text">{{ item.time }}</text>
<text class="meta-dot">·</text>
<text class="meta-text">{{ item.views }}</text>
</view>
</view>
</view>
</view>
<view v-if="(articleList || []).length === 0" class="empty-placeholder">
<text>暂无科普文章数据</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
navigationBarTitleText: '健康知识',
data() {
return {
currentTab: 'nutrients',
scrollViewHeight: '100vh', // 默认值onReady 中按机型计算为 px保证微信小程序 scroll-view 有明确高度
guideList: [],
articleList: [],
nutrientList: [
{
name: '蛋白质',
english: 'Protein',
icon: '🥩',
description: '构成人体组织的重要营养素',
tag: '需控制',
tagClass: 'control'
},
{
name: '钾',
english: 'Potassium (K)',
icon: '🍌',
description: '维持神经肌肉功能的重要元素',
tag: '严格控制',
tagClass: 'strict'
},
{
name: '磷',
english: 'Phosphorus (P)',
icon: '🥜',
description: '骨骼健康的重要矿物质',
tag: '严格控制',
tagClass: 'strict'
},
{
name: '钠',
english: 'Sodium (Na)',
icon: '🧂',
description: '调节体液平衡的电解质',
tag: '适量控制',
tagClass: 'moderate'
},
{
name: '钙',
english: 'Calcium (Ca)',
icon: '🥛',
description: '骨骼和牙齿的主要成分',
tag: '适量补充',
tagClass: 'supplement'
},
{
name: '水分',
english: 'Water',
icon: '💧',
description: '生命活动必需的基础物质',
tag: '需控制',
tagClass: 'control'
}
]
}
},
onLoad(options) {
// 确保列表初始为数组,避免未加载时为 undefined
this.guideList = Array.isArray(this.guideList) ? this.guideList : [];
this.articleList = Array.isArray(this.articleList) ? this.articleList : [];
if (options && options.id) {
// 有 id 时切换到科普文章 tabswitchTab 内会调用 loadKnowledgeList 加载列表
this.switchTab('articles');
} else {
// 无 id 时默认显示「营养素」tab本地静态数据用户切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList 加载对应列表
this.currentTab = 'nutrients';
}
},
onReady() {
// 微信小程序 scroll-view 必须使用明确高度,且 calc(vh - rpx) 兼容性差,改为用 px 计算
try {
const systemInfo = uni.getSystemInfoSync();
const statusBarHeight = systemInfo.statusBarHeight || 0;
const navHeight = 44;
const tabBarRpx = 120;
const windowWidth = systemInfo.windowWidth || 375;
const tabBarPx = Math.ceil((windowWidth / 750) * tabBarRpx);
this.scrollViewHeight = `calc(100vh - ${statusBarHeight + navHeight + tabBarPx}px)`;
} catch (e) {
this.scrollViewHeight = 'calc(100vh - 140px)';
}
},
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 与后端 v2_knowledge 表一致guide=饮食指南article=科普文章
const typeParam = this.currentTab === 'guide' ? 'guide' : 'article';
try {
const { getKnowledgeList } = await import('@/api/tool.js');
const result = await getKnowledgeList({
type: typeParam,
page: 1,
limit: 50
});
// 兼容多种返回结构result.data.list / result.data.records / result.data 为数组 / result.list
let rawList = [];
if (result && result.data) {
const d = result.data;
if (Array.isArray(d.list)) {
rawList = d.list;
} else if (Array.isArray(d.records)) {
rawList = d.records;
} else if (Array.isArray(d)) {
rawList = d;
} else if (d && Array.isArray(d.data)) {
rawList = d.data;
}
} else if (result && Array.isArray(result.list)) {
rawList = result.list;
}
// Normalize id: backend may return knowledgeId, id, or knowledge_id (BeanUtil/JSON)
const list = (rawList || []).map(item => {
const id = item.knowledgeId ?? item.id ?? item.knowledge_id;
return {
...item,
id,
knowledgeId: item.knowledgeId ?? id,
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 || ''
};
});
// 始终赋值为数组,绝不设为 undefined
const safeList = Array.isArray(list) ? list : [];
if (this.currentTab === 'guide') {
this.guideList = safeList;
} else if (this.currentTab === 'articles') {
this.articleList = safeList;
}
} catch (error) {
console.error('加载知识列表失败:', error);
const msg = (error && (typeof error === 'string' ? error : (error.message || error.msg))) || '加载列表失败';
uni.showToast({
title: String(msg).substring(0, 20),
icon: 'none'
});
// 失败时清空当前 tab 列表,确保始终为数组,绝不设为 undefined
if (this.currentTab === 'guide') {
this.guideList = [];
} else if (this.currentTab === 'articles') {
this.articleList = [];
}
}
},
async switchTab(tab) {
this.currentTab = tab;
await this.loadKnowledgeList();
},
goToNutrientDetail(event) {
const index = event.currentTarget.dataset.nutrientIndex;
const item = this.nutrientList[index];
if (!item) return;
uni.navigateTo({
url: `/pages/tool/nutrient-detail?name=${encodeURIComponent(item.name)}`
});
},
goToDetail(event, item, index, tab) {
// 优先从传入的 item 取 knowledgeId 或 id避免 dataset 序列化丢失
const fromItem = item != null ? (item.knowledgeId ?? item.id) : undefined;
const fromDataset = event && event.currentTarget && event.currentTarget.dataset;
const id = fromDataset ? (fromDataset.itemId ?? fromDataset.item_id) : undefined;
const knowledgeId = fromDataset ? (fromDataset.itemKid ?? fromDataset.item_kid) : undefined;
let finalId = fromItem ?? knowledgeId ?? id;
if (finalId == null && tab != null && index != null) {
const list = tab === 'guide' ? this.guideList : this.articleList;
const listItem = Array.isArray(list) ? list[index] : null;
finalId = listItem != null ? (listItem.knowledgeId ?? listItem.id) : undefined;
}
// 仅当 knowledgeId 或 id 存在且有效时才跳转,否则提示暂无详情
if (finalId == null || finalId === '' || String(finalId).trim() === '' || String(finalId) === 'undefined') {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
const idStr = String(finalId).trim();
uni.navigateTo({
url: `/pages/tool/knowledge-detail?id=${encodeURIComponent(idStr)}`
});
}
}
}
</script>
<style lang="scss" scoped>
.nutrition-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f4f5f7;
}
/* Tab切换 */
.tab-container {
background: #ffffff;
border-bottom: 1rpx solid #d0dbea;
display: flex;
padding: 12rpx 32rpx;
gap: 16rpx;
}
.tab-item {
flex: 1;
height: 75rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
border: 1rpx solid #d0dbea;
text {
font-size: 28rpx;
color: #9fa5c0;
}
&.active {
background: #ff6b35;
border-color: #ff6b35;
text {
color: #ffffff;
}
}
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
overflow-y: auto;
}
.tab-content {
padding: 32rpx;
}
.intro-text {
font-size: 28rpx;
color: #9fa5c0;
margin-bottom: 32rpx;
line-height: 1.2;
}
/* 营养素列表 */
.nutrient-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.nutrient-card {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 32rpx;
display: flex;
align-items: center;
gap: 32rpx;
}
.nutrient-icon-wrapper {
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: #e3fff1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.nutrient-icon {
font-size: 60rpx;
}
.nutrient-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.nutrient-header {
display: flex;
align-items: baseline;
gap: 16rpx;
}
.nutrient-name {
font-size: 32rpx;
color: #101828;
font-weight: 500;
}
.nutrient-english {
font-size: 24rpx;
color: #99a1af;
}
.nutrient-desc {
font-size: 28rpx;
color: #4a5565;
line-height: 1.5;
}
.nutrient-tag {
padding: 8rpx 24rpx;
border-radius: 50rpx;
display: inline-block;
align-self: flex-start;
text {
font-size: 24rpx;
}
&.control {
background: #fef2f2;
border: 1rpx solid #ffc9c9;
text {
color: #e7000b;
}
}
&.strict {
background: #fef2f2;
border: 1rpx solid #ffc9c9;
text {
color: #e7000b;
}
}
&.moderate {
background: #fff8e1;
border: 1rpx solid #ffe0b2;
text {
color: #ff9800;
}
}
&.supplement {
background: #f0fdf4;
border: 1rpx solid #b9f8cf;
text {
color: #00a63e;
}
}
}
.arrow-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 40rpx;
color: #9fa5c0;
}
}
/* 空状态 */
.empty-placeholder {
padding: 200rpx 0;
text-align: center;
color: #9fa5c0;
font-size: 28rpx;
}
/* 知识列表 */
.knowledge-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.knowledge-item {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1);
display: flex;
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;
border-radius: 32rpx;
background: #f4f5f7;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text {
font-size: 60rpx;
}
}
.knowledge-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.knowledge-title {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
margin-bottom: 12rpx;
}
.knowledge-desc {
font-size: 24rpx;
color: #9fa5c0;
line-height: 1.6;
margin-bottom: 16rpx;
}
.knowledge-meta {
display: flex;
align-items: center;
gap: 24rpx;
.meta-text {
font-size: 24rpx;
color: #9fa5c0;
}
.meta-dot {
font-size: 24rpx;
color: #d0dbea;
}
}
}
</style>