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

991 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="result-page">
<!-- 加载中 -->
<view v-if="loading" class="loading-wrap">
<view class="loading-inner">
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 加载失败 -->
<view v-else-if="loadError" class="error-wrap">
<text class="error-text">{{ loadError }}</text>
<view class="retry-btn" v-if="resultId" @click="loadResult">重新加载</view>
<view class="retry-btn" v-else @click="goBackToCalculator">返回重新计算</view>
</view>
<!-- 正常内容 -->
<template v-else>
<!-- Tab切换 -->
<view class="tab-container">
<view
class="tab-item"
:class="{ active: currentTab === 'overview' }"
@click="switchTab('overview')"
>
<text class="tab-icon">📊</text>
<text class="tab-text">健康概览</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'meal' }"
@click="switchTab('meal')"
>
<text class="tab-icon">🍽</text>
<text class="tab-text">营养配餐</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 健康概览Tab内容 -->
<view v-if="currentTab === 'overview'" class="tab-content">
<!-- 您的健康数据卡片 -->
<view class="data-card">
<view class="card-header">
<text class="card-icon">💪</text>
<text class="card-title">您的健康数据</text>
</view>
<view class="data-grid">
<view class="data-item">
<text class="data-label">eGFR数值</text>
<text class="data-value">{{ healthData.eGFR }}</text>
<text class="data-unit">ml/min/1.73²m</text>
</view>
<view class="data-item">
<text class="data-label">标准体重</text>
<text class="data-value">{{ healthData.standardWeight }}</text>
<text class="data-unit">东方人标准</text>
</view>
<view class="data-item">
<text class="data-label">体重指数</text>
<text class="data-value">{{ healthData.bmi }}</text>
<text class="data-unit">{{ healthData.bmiStatus }}</text>
</view>
<view class="data-item">
<text class="data-label">CKD分期</text>
<text class="data-value ckd-stage">{{ healthData.ckdStage }}</text>
</view>
</view>
</view>
<!-- 每日营养目标卡片 -->
<view class="data-card">
<view class="card-header">
<text class="card-icon">🎯</text>
<text class="card-title">每日营养目标</text>
</view>
<view class="nutrition-goals">
<view class="goal-item protein">
<text class="goal-label">蛋白质</text>
<text class="goal-value">{{ nutritionGoals.protein }}</text>
<text class="goal-unit">/</text>
</view>
<view class="goal-item energy">
<text class="goal-label">能量</text>
<text class="goal-value">{{ nutritionGoals.energy }}</text>
<text class="goal-unit">千卡/</text>
</view>
</view>
<view class="nutrition-hint">
<text class="hint-icon">💡</text>
<text class="hint-text">相当于下方表中食物的推荐摄入量</text>
</view>
</view>
<!-- 食物份数建议卡片 -->
<view class="data-card">
<view class="card-header">
<text class="card-icon">🥗</text>
<text class="card-title">食物份数建议</text>
</view>
<view class="food-list">
<view
class="food-item"
v-for="(item, index) in foodList"
:key="index"
>
<view class="food-info">
<view class="food-number">{{ item.number }}</view>
<text class="food-name">{{ item.name }}</text>
</view>
<text class="food-portion">{{ item.portion }} </text>
</view>
</view>
</view>
<!-- 温馨提示 -->
<view class="tip-box">
<text class="tip-icon">💡</text>
<view class="tip-content">
<text class="tip-title">温馨提示</text>
<text class="tip-text">以上计算结果仅供参考具体饮食方案请咨询专业营养师或医生</text>
</view>
</view>
<!-- 联系专业营养师按钮 -->
<view class="contact-btn" @click="contactNutritionist">
<text>联系专业营养师</text>
</view>
</view>
<!-- 营养配餐Tab内容 -->
<view v-if="currentTab === 'meal'" class="tab-content meal-content">
<!-- 早餐 -->
<view class="meal-section">
<view class="meal-header">
<view class="meal-icon">🌅</view>
<text class="meal-title">早餐</text>
</view>
<view class="meal-items">
<view
class="meal-item"
v-for="(item, index) in mealPlan.breakfast"
:key="index"
>
<view class="meal-image">
<image :src="item.image" mode="aspectFill"></image>
<view class="meal-number">{{ index + 1 }}</view>
</view>
<view class="meal-info">
<text class="meal-name">{{ item.name }}</text>
<view class="ingredient-tags">
<view
class="ingredient-tag"
v-for="(ingredient, i) in item.ingredients"
:key="i"
>
{{ ingredient }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 午餐 -->
<view class="meal-section">
<view class="meal-header">
<view class="meal-icon"></view>
<text class="meal-title">午餐</text>
</view>
<view class="meal-items">
<view
class="meal-item"
v-for="(item, index) in mealPlan.lunch"
:key="index"
>
<view class="meal-image">
<image :src="item.image" mode="aspectFill"></image>
<view class="meal-number">{{ index + 1 }}</view>
</view>
<view class="meal-info">
<text class="meal-name">{{ item.name }}</text>
<view class="ingredient-tags">
<view
class="ingredient-tag"
v-for="(ingredient, i) in item.ingredients"
:key="i"
>
{{ ingredient }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 晚餐 -->
<view class="meal-section">
<view class="meal-header">
<view class="meal-icon">🌙</view>
<text class="meal-title">晚餐</text>
</view>
<view class="meal-items">
<view
class="meal-item"
v-for="(item, index) in mealPlan.dinner"
:key="index"
>
<view class="meal-image">
<image :src="item.image" mode="aspectFill"></image>
<view class="meal-number">{{ index + 1 }}</view>
</view>
<view class="meal-info">
<text class="meal-name">{{ item.name }}</text>
<view class="ingredient-tags">
<view
class="ingredient-tag"
v-for="(ingredient, i) in item.ingredients"
:key="i"
>
{{ ingredient }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 重要提示 -->
<view class="tips-card">
<view class="tips-header">
<text class="tips-icon"></text>
<text class="tips-title">重要提示</text>
</view>
<view class="tips-list">
<view
class="tip-item"
v-for="(tip, index) in importantTips"
:key="index"
>
<text class="tip-dot"></text>
<text class="tip-text">{{ tip }}</text>
</view>
</view>
</view>
<!-- 采纳计划按钮 -->
<view class="adopt-btn" @click="adoptPlan">
<text class="adopt-icon"></text>
<text class="adopt-text">采纳计划开始打卡</text>
</view>
<!-- 联系专业营养师按钮 -->
<view class="contact-btn" @click="contactNutritionist">
<text>联系专业营养师</text>
</view>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</scroll-view>
</template>
</view>
</template>
<script>
import { getCalculatorResult } from '@/api/tool.js'
export default {
data() {
return {
currentTab: 'overview',
loading: true,
loadError: '',
resultId: null,
healthData: {
eGFR: '--',
standardWeight: '--',
bmi: '--',
bmiStatus: '--',
ckdStage: '--'
},
nutritionGoals: {
protein: '--',
energy: '--'
},
foodList: [],
mealPlan: {
breakfast: [],
lunch: [],
dinner: []
},
importantTips: []
}
},
onLoad(options) {
const id = options.id || options.resultId
if (id) {
this.resultId = Number(id)
this.loadResult()
} else {
this.loading = false
this.loadError = '暂无计算结果,请先完成营养计算'
}
},
methods: {
async loadResult() {
if (!this.resultId) return
this.loading = true
this.loadError = ''
try {
const res = await getCalculatorResult(this.resultId)
const data = res.data || res
this.applyResult(data)
} catch (e) {
const msg = (e && (e.message || e.msg)) || '加载失败,请重试'
this.loadError = msg
}
this.loading = false
},
applyResult(data) {
if (!data) return
if (data.healthData) {
this.healthData = {
eGFR: data.healthData.eGFR ?? '--',
standardWeight: data.healthData.standardWeight ?? '--',
bmi: data.healthData.bmi ?? '--',
bmiStatus: data.healthData.bmiStatus ?? '--',
ckdStage: data.healthData.ckdStage ?? '--'
}
}
if (data.nutritionGoals) {
this.nutritionGoals = {
protein: data.nutritionGoals.protein ?? '--',
energy: data.nutritionGoals.energy ?? '--'
}
}
if (Array.isArray(data.foodList)) {
this.foodList = data.foodList
}
if (data.mealPlan) {
this.mealPlan = {
breakfast: Array.isArray(data.mealPlan.breakfast) ? data.mealPlan.breakfast : [],
lunch: Array.isArray(data.mealPlan.lunch) ? data.mealPlan.lunch : [],
dinner: Array.isArray(data.mealPlan.dinner) ? data.mealPlan.dinner : []
}
}
if (Array.isArray(data.importantTips)) {
this.importantTips = data.importantTips
}
},
goBackToCalculator() {
uni.navigateBack({
delta: 1,
fail: () => {
uni.redirectTo({
url: '/pages/tool/calculator'
})
}
})
},
switchTab(tab) {
this.currentTab = tab
},
contactNutritionist() {
// #ifdef MP-WEIXIN
const openConversation =
typeof uni.openCustomerServiceConversation === 'function'
? uni.openCustomerServiceConversation
: (typeof wx !== 'undefined' && typeof wx.openCustomerServiceConversation === 'function'
? wx.openCustomerServiceConversation
: null);
if (!openConversation) {
uni.showToast({
title: '当前微信版本暂不支持客服会话,请到「我的-联系客服」',
icon: 'none'
});
return;
}
openConversation({
showMessageCard: true,
success: () => {},
fail: () => {
uni.showToast({
title: '打开客服失败,请到「我的-联系客服」',
icon: 'none'
});
}
});
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: '请在微信小程序中联系客服',
icon: 'none'
});
// #endif
},
async adoptPlan() {
if (!this.resultId) {
uni.showToast({ title: '暂无计算结果', icon: 'none' })
return
}
// 防止重复点击
if (this._adoptingPlan) return
this._adoptingPlan = true
try {
const { adoptNutritionPlan } = await import('@/api/tool.js')
await adoptNutritionPlan(this.resultId)
uni.showToast({ title: '采纳成功', icon: 'success' })
uni.navigateTo({
url: `/pages/tool/checkin-publish?planId=${this.resultId}`
})
} catch (e) {
const msg = (e && (e.message || e.msg)) || '采纳失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
this._adoptingPlan = false
}
}
}
}
</script>
<style lang="scss" scoped>
.result-page {
min-height: 100vh;
background-color: #f4f5f7;
}
/* 加载中 / 错误态 */
.loading-wrap,
.error-wrap {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
}
.loading-inner {
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #9fa5c0;
}
.error-wrap {
flex-direction: column;
gap: 24rpx;
}
.error-text {
font-size: 28rpx;
color: #666;
text-align: center;
}
.retry-btn {
padding: 20rpx 48rpx;
background: #ff6b35;
color: #fff;
font-size: 28rpx;
border-radius: 48rpx;
}
/* Tab切换 */
.tab-container {
background: #ffffff;
border-bottom: 1rpx solid #d0dbea;
display: flex;
padding: 0 32rpx;
height: 96rpx;
align-items: center;
position: sticky;
// top: 88rpx;
z-index: 99;
}
.tab-item {
flex: 1;
height: 100%;
min-height: 75rpx;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
transition: all 0.2s ease;
box-sizing: border-box;
/* 未激活明显变灰无下划线BUG-002 */
color: #9ca3af;
font-weight: 400;
border-bottom: 3rpx solid transparent;
.tab-icon {
font-size: 28rpx;
color: inherit;
font-weight: inherit;
}
.tab-text {
font-size: 28rpx;
color: inherit;
font-weight: inherit;
}
/* 激活加粗、主色、橙色底部下划线BUG-002 */
&.active {
background: transparent;
color: #f97316;
font-weight: 700;
border-bottom: 3px solid #f97316;
.tab-text {
color: #f97316;
font-weight: 700;
}
.tab-icon {
color: #f97316;
font-weight: 700;
}
}
}
/* 内容滚动区域 */
.content-scroll {
height: calc(100vh - 184rpx);
}
.tab-content {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
/* 数据卡片 */
.data-card {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 40rpx;
}
.card-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
.card-icon {
font-size: 48rpx;
}
.card-title {
font-size: 32rpx;
color: #2e3e5c;
font-weight: 500;
}
}
/* 健康数据网格 */
.data-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.data-item {
background: #f4f5f7;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 30rpx 24rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
.data-label {
font-size: 24rpx;
color: #9fa5c0;
}
.data-value {
font-size: 40rpx;
color: #3e5481;
font-weight: 500;
&.ckd-stage {
font-size: 32rpx;
}
}
.data-unit {
font-size: 24rpx;
color: #9fa5c0;
}
}
/* 营养目标 */
.nutrition-goals {
display: flex;
gap: 24rpx;
margin-bottom: 24rpx;
}
.goal-item {
flex: 1;
border-radius: 24rpx;
padding: 32rpx 24rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
&.protein {
background: #fff5f0;
border: 1rpx solid rgba(255, 107, 53, 0.3);
.goal-label {
color: #ff6b35;
}
.goal-value {
color: #ff6b35;
}
.goal-unit {
color: rgba(255, 107, 53, 0.7);
}
}
&.energy {
background: #fff4e6;
border: 1rpx solid rgba(255, 165, 0, 0.3);
.goal-label {
color: #ff9800;
}
.goal-value {
color: #ff9800;
}
.goal-unit {
color: rgba(255, 152, 0, 0.7);
}
}
.goal-label {
font-size: 24rpx;
}
.goal-value {
font-size: 48rpx;
font-weight: 500;
}
.goal-unit {
font-size: 24rpx;
}
}
.nutrition-hint {
background: #f4f5f7;
border: 1rpx solid #d0dbea;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
.hint-icon {
font-size: 32rpx;
}
.hint-text {
font-size: 24rpx;
color: #9fa5c0;
flex: 1;
}
}
/* 食物列表 */
.food-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.food-item {
background: #f4f5f7;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.food-info {
display: flex;
align-items: center;
gap: 24rpx;
.food-number {
width: 56rpx;
height: 56rpx;
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #3e5481;
flex-shrink: 0;
}
.food-name {
font-size: 28rpx;
color: #3e5481;
}
}
.food-portion {
font-size: 28rpx;
color: #ff6b35;
font-weight: 500;
}
/* 温馨提示 */
.tip-box {
background: #e3f2fd;
border: 1rpx solid #64b5f6;
border-radius: 24rpx;
padding: 32rpx;
display: flex;
gap: 20rpx;
align-items: flex-start;
.tip-icon {
font-size: 40rpx;
flex-shrink: 0;
}
.tip-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.tip-title {
font-size: 28rpx;
color: #1976d2;
font-weight: 500;
}
.tip-text {
font-size: 24rpx;
color: #1565c0;
line-height: 1.6;
}
}
}
/* 联系营养师按钮 */
.contact-btn {
width: calc(100% - 64rpx);
height: 96rpx;
background: #ffffff;
border: 2rpx solid #ff6b35;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 32rpx 24rpx;
text {
font-size: 28rpx;
color: #ff6b35;
font-weight: 500;
}
}
/* 空状态 */
.empty-placeholder {
padding: 200rpx 0;
text-align: center;
color: #9fa5c0;
font-size: 28rpx;
}
/* 营养配餐内容 */
.meal-content {
padding: 0;
gap: 0;
}
/* 餐次区块 */
.meal-section {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
margin: 0 32rpx 32rpx;
overflow: hidden;
}
.meal-header {
background: linear-gradient(180deg, #f8f9fa 0%, #f4f5f7 100%);
border-bottom: 1rpx solid #d0dbea;
padding: 32rpx;
display: flex;
align-items: center;
gap: 24rpx;
}
.meal-icon {
width: 80rpx;
height: 80rpx;
background: #ffffff;
border: 2rpx solid rgba(255, 136, 68, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
}
.meal-title {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
}
/* 菜品列表 */
.meal-items {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.meal-item {
background: #ffffff;
border: 1rpx solid #d0dbea;
border-radius: 24rpx;
padding: 16rpx;
display: flex;
gap: 24rpx;
}
.meal-image {
width: 160rpx;
height: 160rpx;
border-radius: 24rpx;
overflow: hidden;
position: relative;
background: #f4f5f7;
flex-shrink: 0;
image {
width: 100%;
height: 100%;
}
.meal-number {
position: absolute;
left: 12rpx;
top: 12rpx;
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #ff8844 0%, #ff7722 100%);
border: 2rpx solid #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
}
.meal-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 8rpx 0;
}
.meal-name {
font-size: 28rpx;
color: #2e3e5c;
font-weight: 500;
}
.ingredient-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.ingredient-tag {
background: linear-gradient(135deg, #fff8f0 0%, #fff5eb 100%);
border: 1rpx solid rgba(255, 136, 68, 0.3);
border-radius: 12rpx;
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #ff7722;
}
/* 重要提示卡片 */
.tips-card {
background: #fff3e0;
border: 1rpx solid #ffb74d;
border-radius: 24rpx;
margin: 0 32rpx 32rpx;
padding: 32rpx;
}
.tips-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
.tips-icon {
font-size: 40rpx;
}
.tips-title {
font-size: 28rpx;
color: #e65100;
font-weight: 500;
}
}
.tips-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.tip-item {
display: flex;
gap: 16rpx;
align-items: flex-start;
.tip-dot {
font-size: 28rpx;
color: #e65100;
line-height: 1;
margin-top: 4rpx;
}
.tip-text {
flex: 1;
font-size: 28rpx;
color: #f57c00;
line-height: 1.6;
}
}
/* 采纳计划按钮 */
.adopt-btn {
width: calc(100% - 64rpx);
height: 96rpx;
background: #ff6b35;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin: 0 32rpx 24rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
.adopt-icon {
font-size: 28rpx;
}
.adopt-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
/* 底部安全距离 */
.safe-bottom {
height: 40rpx;
}
</style>