Files
msh-system/msh_single_uniapp/pages/tool/nutrition-knowledge.vue
scottpan d8d2025543 feat: T10 回归测试 Bug 修复与功能完善
修复 BUG-001 至 BUG-009 及 T10-1 至 T10-6 相关问题:
- 打卡积分显示与累加逻辑优化
- 食谱计算器 Tab 选中样式修复
- 食物百科列表图片与简介展示修复
- 食物详情页数据加载修复
- AI营养师差异化回复优化
- 健康知识/营养知识名称统一
- 饮食指南/科普文章详情页内容展示修复
- 帖子营养统计数据展示修复
- 社区帖子类型中文命名统一
- 帖子详情标签中文显示修复
- 食谱营养AI填充功能完善
- 食谱收藏/点赞功能修复

新增:
- ToolNutritionFillService 营养填充服务
- T10 回归测试用例 (Playwright)
- 知识文章数据 SQL 脚本

涉及模块:
- crmeb-common: VO/Request/Response 优化
- crmeb-service: 业务逻辑完善
- crmeb-front: API 接口扩展
- msh_single_uniapp: 前端页面修复
- tests/e2e: 回归测试用例
2026-03-05 09:35:00 +08:00

560 lines
12 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 class="content-scroll" scroll-y>
<!-- 营养素百科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(item)"
>
<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(item)"
>
<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(item)"
>
<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 {
data() {
return {
currentTab: 'nutrients',
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) {
if (options && options.id) {
// 有 id 时切换到科普文章 tabswitchTab 内会调用 loadKnowledgeList 加载列表
this.switchTab('articles');
} else {
// 无 id 时默认当前 tab 为「营养素」,不请求接口;用户点击「饮食指南」或「科普文章」时由 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 / articlev2_knowledge 表 type 字段)
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
});
// 兼容 CommonPageresult.data.list或 result.data/result.data.records 为数组
let rawList = [];
if (result && result.data) {
if (Array.isArray(result.data.list)) {
rawList = result.data.list;
} else if (Array.isArray(result.data.records)) {
rawList = result.data.records;
} else if (Array.isArray(result.data)) {
rawList = result.data;
}
}
// 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,
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);
const msg = (error && (typeof error === 'string' ? error : (error.message || error.msg))) || '加载列表失败';
uni.showToast({
title: String(msg),
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(item) {
uni.navigateTo({
url: `/pages/tool/nutrient-detail?name=${item.name}`
})
},
goToDetail(item) {
if (!item) {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
// 确保 knowledgeId 或 id 存在才跳转,否则提示暂无详情
const id = item.knowledgeId ?? item.id ?? item.knowledge_id;
if (id === undefined || id === null || id === '' || (typeof id === 'number' && isNaN(id))) {
uni.showToast({ title: '暂无详情', icon: 'none' });
return;
}
uni.navigateTo({
url: `/pages/tool/knowledge-detail?id=${id}`
});
}
}
}
</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>