Files
msh-system/msh_single_uniapp/pages/tool/calculator-result.vue
scottpan 6f2dc27fbc 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
2026-03-04 12:21:29 +08:00

993 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.3s;
border-bottom: none;
box-sizing: border-box;
color: #9ca3af;
font-weight: 400;
.tab-icon {
font-size: 28rpx;
color: #9ca3af;
font-weight: 400;
}
.tab-text {
font-size: 28rpx;
color: #9ca3af;
font-weight: 400;
}
&.active {
background: transparent;
border-bottom: 3px solid #f97316;
color: #f97316;
font-weight: 700;
.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>