Files
msh-system/msh_single_uniapp/pages/tool/calculator-result.vue
msh-agent 560d4de275 fix(ui): 我的页继续瘦身 + 计算结果页客服按钮统一 + 历史页标题运行时回写
my-profile.vue:
- 整段移除「工具与服务」(邀请有礼当前未上线,剩它一项无意义)
- 同步清理 iconGift / goToInvite 残留

calculator-result.vue:
- 「联系专业营养师」按钮 MP-WEIXIN 端改用 <button open-type='contact'>
- 与 welcome-gift / customer-service 统一行为,零 JS API 依赖
- 非小程序端保留原 contactNutritionist 兜底
- contact-btn-mp 重置 button 默认 line-height/border 视觉一致

calculator-history.vue:
- onLoad 中 uni.setNavigationBarTitle 强制回写「我的计算记录」
- 兜底部分开发者工具/旧编译缓存把 UTF-8 标题渲染成乱码的场景

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:44:04 +08:00

1013 lines
21 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.gram || 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>
<!-- 联系专业营养师按钮小程序使用原生 button open-type='contact' 直接唤起客服 -->
<!-- #ifdef MP-WEIXIN -->
<button class="contact-btn contact-btn-mp" open-type="contact" hover-class="none">
<text>联系专业营养师</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="contact-btn" @click="contactNutritionist">
<text>联系专业营养师</text>
</view>
<!-- #endif -->
</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>
<!-- 联系专业营养师按钮小程序使用原生 button open-type='contact' 直接唤起客服 -->
<!-- #ifdef MP-WEIXIN -->
<button class="contact-btn contact-btn-mp" open-type="contact" hover-class="none">
<text>联系专业营养师</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="contact-btn" @click="contactNutritionist">
<text>联系专业营养师</text>
</view>
<!-- #endif -->
</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)) {
// 份数→克数适配:优先使用 gram 字段,兜底通过 portion * gramPerServing 换算
this.foodList = data.foodList.map(item => ({
...item,
gram: item.gram || (item.portion && item.gramPerServing ? Math.round(item.portion * item.gramPerServing) : null)
}))
}
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;
/* stretchTab 占满栏高,激活态 border-bottom 才能贴底显示BUG-002 */
align-items: stretch;
position: sticky;
// top: 88rpx;
z-index: 99;
}
/* Tab未激活 #9ca3af、无橙色下划线激活加粗 + 主色字 + 3px 橙色底边BUG-002 */
.tab-item {
flex: 1;
min-height: 0;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-sizing: border-box;
background: transparent;
/* 与激活态同厚底边占位,避免切换跳动;透明即无可见下划线 */
border-bottom: 3px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease, font-weight 0.15s ease;
/* 字色与字重落在 text 节点上,避免部分端不继承 view 的 color */
.tab-icon,
.tab-text {
font-size: 28rpx;
color: #9ca3af;
font-weight: 400;
transition: color 0.2s ease, font-weight 0.15s ease;
}
&.active {
color: #f97316;
font-weight: 700;
border-bottom: 3px solid #f97316;
.tab-icon,
.tab-text {
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;
}
}
/* 小程序原生 button 样式重置(保证视觉与原 view 版按钮一致) */
.contact-btn-mp {
line-height: 96rpx;
padding: 0;
}
.contact-btn-mp::after {
border: none;
}
/* 空状态 */
.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>