537 lines
12 KiB
Vue
537 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="calculator-page">
|
|||
|
|
<!-- 表单区域 -->
|
|||
|
|
<view class="form-container">
|
|||
|
|
<!-- 性别选择 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">性别:</view>
|
|||
|
|
<view class="radio-group">
|
|||
|
|
<view
|
|||
|
|
class="radio-item"
|
|||
|
|
:class="{ active: formData.gender === 'male' }"
|
|||
|
|
@click="selectGender('male')"
|
|||
|
|
>
|
|||
|
|
<view class="radio-dot" :class="{ checked: formData.gender === 'male' }"></view>
|
|||
|
|
<text class="radio-text" :class="{ active: formData.gender === 'male' }">男</text>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
class="radio-item"
|
|||
|
|
:class="{ active: formData.gender === 'female' }"
|
|||
|
|
@click="selectGender('female')"
|
|||
|
|
>
|
|||
|
|
<view class="radio-dot" :class="{ checked: formData.gender === 'female' }"></view>
|
|||
|
|
<text class="radio-text" :class="{ active: formData.gender === 'female' }">女</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 年龄输入 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">年龄:</view>
|
|||
|
|
<view class="input-wrapper" :class="{ 'input-focus': focusField === 'age', 'input-filled': formData.age }">
|
|||
|
|
<input
|
|||
|
|
class="form-input"
|
|||
|
|
type="number"
|
|||
|
|
v-model="formData.age"
|
|||
|
|
placeholder="请输入年龄"
|
|||
|
|
placeholder-style="color: #9fa5c0"
|
|||
|
|
@focus="focusField = 'age'"
|
|||
|
|
@blur="focusField = ''"
|
|||
|
|
/>
|
|||
|
|
<text class="input-unit">岁</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 身高输入 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">身高:</view>
|
|||
|
|
<view class="input-wrapper" :class="{ 'input-focus': focusField === 'height', 'input-filled': formData.height }">
|
|||
|
|
<input
|
|||
|
|
class="form-input"
|
|||
|
|
type="number"
|
|||
|
|
v-model="formData.height"
|
|||
|
|
placeholder="请输入身高"
|
|||
|
|
placeholder-style="color: #9fa5c0"
|
|||
|
|
@focus="focusField = 'height'"
|
|||
|
|
@blur="focusField = ''"
|
|||
|
|
/>
|
|||
|
|
<text class="input-unit">cm</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 是否透析 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">是否透析:</view>
|
|||
|
|
<view class="radio-group">
|
|||
|
|
<view
|
|||
|
|
class="radio-item"
|
|||
|
|
:class="{ active: formData.dialysis === false }"
|
|||
|
|
@click="selectDialysis(false)"
|
|||
|
|
>
|
|||
|
|
<view class="radio-dot" :class="{ checked: formData.dialysis === false }"></view>
|
|||
|
|
<text class="radio-text" :class="{ active: formData.dialysis === false }">否</text>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
class="radio-item"
|
|||
|
|
:class="{ active: formData.dialysis === true }"
|
|||
|
|
@click="selectDialysis(true)"
|
|||
|
|
>
|
|||
|
|
<view class="radio-dot" :class="{ checked: formData.dialysis === true }"></view>
|
|||
|
|
<text class="radio-text" :class="{ active: formData.dialysis === true }">是</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 干体重输入 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">干体重:</view>
|
|||
|
|
<view class="input-wrapper" :class="{ 'input-focus': focusField === 'dryWeight', 'input-filled': formData.dryWeight }">
|
|||
|
|
<input
|
|||
|
|
class="form-input"
|
|||
|
|
type="digit"
|
|||
|
|
v-model="formData.dryWeight"
|
|||
|
|
placeholder="请输入干体重"
|
|||
|
|
placeholder-style="color: #9fa5c0"
|
|||
|
|
@focus="focusField = 'dryWeight'"
|
|||
|
|
@blur="focusField = ''"
|
|||
|
|
/>
|
|||
|
|
<text class="input-unit">kg</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="form-hint">透析患者请输入透析后的干体重</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 血肌酐输入 -->
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">血肌酐:</view>
|
|||
|
|
<view class="input-wrapper" :class="{ 'input-focus': focusField === 'creatinine', 'input-filled': formData.creatinine }">
|
|||
|
|
<input
|
|||
|
|
class="form-input"
|
|||
|
|
type="digit"
|
|||
|
|
v-model="formData.creatinine"
|
|||
|
|
placeholder="请输入血肌酐值"
|
|||
|
|
placeholder-style="color: #9fa5c0"
|
|||
|
|
@focus="focusField = 'creatinine'"
|
|||
|
|
@blur="focusField = ''"
|
|||
|
|
/>
|
|||
|
|
<text class="input-unit">μmol/L</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 开始计算按钮 -->
|
|||
|
|
<view class="footer-btn">
|
|||
|
|
<view
|
|||
|
|
class="calculate-btn"
|
|||
|
|
:class="{ disabled: isCalculating || !isFormValid }"
|
|||
|
|
@click="handleCalculate"
|
|||
|
|
>
|
|||
|
|
<text>{{ isCalculating ? '计算中...' : '开始计算' }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 底部安全距离 -->
|
|||
|
|
<view class="safe-bottom"></view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { checkLogin, toLogin } from '@/libs/login.js'
|
|||
|
|
import { mapGetters } from 'vuex'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
formData: {
|
|||
|
|
gender: 'male', // male: 男, female: 女
|
|||
|
|
age: '',
|
|||
|
|
height: '',
|
|||
|
|
dialysis: false, // false: 否, true: 是
|
|||
|
|
dryWeight: '',
|
|||
|
|
creatinine: ''
|
|||
|
|
},
|
|||
|
|
isCalculating: false, // 计算中状态
|
|||
|
|
pendingCalculate: false, // 登录后待执行计算
|
|||
|
|
focusField: '' // 当前聚焦的输入框
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
...mapGetters(['userInfo', 'isLogin']),
|
|||
|
|
// 表单是否有效(用于按钮状态)
|
|||
|
|
isFormValid() {
|
|||
|
|
return this.formData.age &&
|
|||
|
|
this.formData.height &&
|
|||
|
|
this.formData.dryWeight &&
|
|||
|
|
this.formData.creatinine
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad() {
|
|||
|
|
// 尝试预填用户信息
|
|||
|
|
this.prefillUserInfo()
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
// 页面显示时检查是否需要自动执行计算(登录返回后)
|
|||
|
|
if (this.pendingCalculate && checkLogin()) {
|
|||
|
|
this.pendingCalculate = false
|
|||
|
|
// 延迟执行,确保页面完全显示
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.handleCalculate()
|
|||
|
|
}, 300)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
// 预填用户信息
|
|||
|
|
prefillUserInfo() {
|
|||
|
|
if (this.isLogin && this.userInfo) {
|
|||
|
|
// 预填性别
|
|||
|
|
if (this.userInfo.sex !== undefined) {
|
|||
|
|
// sex: 0-未知, 1-男, 2-女
|
|||
|
|
if (this.userInfo.sex === 1) {
|
|||
|
|
this.formData.gender = 'male'
|
|||
|
|
} else if (this.userInfo.sex === 2) {
|
|||
|
|
this.formData.gender = 'female'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 如果有年龄信息
|
|||
|
|
if (this.userInfo.age) {
|
|||
|
|
this.formData.age = String(this.userInfo.age)
|
|||
|
|
}
|
|||
|
|
// 如果有身高信息
|
|||
|
|
if (this.userInfo.height) {
|
|||
|
|
this.formData.height = String(this.userInfo.height)
|
|||
|
|
}
|
|||
|
|
// 如果有体重信息
|
|||
|
|
if (this.userInfo.weight) {
|
|||
|
|
this.formData.dryWeight = String(this.userInfo.weight)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
async handleCalculate() {
|
|||
|
|
// 防止重复点击
|
|||
|
|
if (this.isCalculating) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查登录状态
|
|||
|
|
if (!checkLogin()) {
|
|||
|
|
// 标记待执行,登录成功后自动触发
|
|||
|
|
if (this.isFormValid) {
|
|||
|
|
this.pendingCalculate = true
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请先登录后再计算',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 1500
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
setTimeout(() => {
|
|||
|
|
toLogin()
|
|||
|
|
}, 500)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 表单验证
|
|||
|
|
if (!this.validateForm()) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.isCalculating = true
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const { calculateNutrition } = await import('@/api/tool.js');
|
|||
|
|
const result = await calculateNutrition({
|
|||
|
|
gender: this.formData.gender,
|
|||
|
|
age: Number(this.formData.age),
|
|||
|
|
height: Number(this.formData.height),
|
|||
|
|
dialysis: this.formData.dialysis,
|
|||
|
|
dryWeight: Number(this.formData.dryWeight),
|
|||
|
|
creatinine: Number(this.formData.creatinine)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.isCalculating = false
|
|||
|
|
|
|||
|
|
// 跳转到计算结果页,传递计算结果ID
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: `/pages/tool/calculator-result?id=${result.data.id || result.data.resultId}`
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
this.isCalculating = false
|
|||
|
|
|
|||
|
|
// 获取错误信息(error 可能是字符串、对象或 Error 实例)
|
|||
|
|
const errorMsg = typeof error === 'string' ? error : (error.message || error.msg || '');
|
|||
|
|
|
|||
|
|
// 登录相关错误关键词
|
|||
|
|
const loginErrorMessages = ['请先登录', '未登录', '登录已过期', '登录失效'];
|
|||
|
|
const isLoginError = loginErrorMessages.some(msg => errorMsg.includes(msg)) ||
|
|||
|
|
(error.code && [410000, 410001, 410002, 401].includes(error.code));
|
|||
|
|
|
|||
|
|
// 如果是登录相关错误,不显示错误提示(已跳转到登录页)
|
|||
|
|
if (!isLoginError) {
|
|||
|
|
console.error('计算失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: errorMsg || '计算失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
selectGender(gender) {
|
|||
|
|
this.formData.gender = gender
|
|||
|
|
},
|
|||
|
|
selectDialysis(value) {
|
|||
|
|
this.formData.dialysis = value
|
|||
|
|
},
|
|||
|
|
validateForm() {
|
|||
|
|
const age = Number(this.formData.age)
|
|||
|
|
const height = Number(this.formData.height)
|
|||
|
|
const dryWeight = Number(this.formData.dryWeight)
|
|||
|
|
const creatinine = Number(this.formData.creatinine)
|
|||
|
|
|
|||
|
|
if (!this.formData.age) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入年龄',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if (!Number.isInteger(age) || age < 1 || age > 150) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入有效的年龄(1-150的整数)',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.formData.height) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入身高',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if (height < 50 || height > 250) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入有效的身高(50-250cm)',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.formData.dryWeight) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入干体重',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if (dryWeight < 20 || dryWeight > 300) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入有效的体重(20-300kg)',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.formData.creatinine) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入血肌酐值',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if (creatinine <= 0 || creatinine > 2000) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入有效的血肌酐值',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.calculator-page {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
background-color: #f4f5f7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 表单容器 */
|
|||
|
|
.form-container {
|
|||
|
|
padding: 32rpx;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-item {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-label {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #2e3e5c;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 单选按钮组 */
|
|||
|
|
.radio-group {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.radio-item {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 80rpx;
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
border: 2rpx solid #d0dbea;
|
|||
|
|
background: #ffffff;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 12rpx;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
|
|||
|
|
&.active {
|
|||
|
|
background: #fff5f0;
|
|||
|
|
border-color: #ff6b35;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.radio-dot {
|
|||
|
|
width: 32rpx;
|
|||
|
|
height: 32rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border: 2rpx solid #d0dbea;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
|
|||
|
|
&.checked {
|
|||
|
|
border-color: #ff6b35;
|
|||
|
|
background: #ff6b35;
|
|||
|
|
|
|||
|
|
&::after {
|
|||
|
|
content: '';
|
|||
|
|
width: 20rpx;
|
|||
|
|
height: 20rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #ffffff;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.radio-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
|
|||
|
|
&.active {
|
|||
|
|
color: #ff6b35;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 输入框 */
|
|||
|
|
.input-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
height: 96rpx;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
|
|||
|
|
&.input-focus .form-input {
|
|||
|
|
border-color: #ff6b35;
|
|||
|
|
box-shadow: 0 0 0 4rpx rgba(255, 107, 53, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.input-filled .form-input {
|
|||
|
|
border-color: #9fa5c0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.input-focus.input-filled .form-input {
|
|||
|
|
border-color: #ff6b35;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-input {
|
|||
|
|
//width: 100%;
|
|||
|
|
height: 96rpx;
|
|||
|
|
background: #ffffff;
|
|||
|
|
border: 2rpx solid #d0dbea;
|
|||
|
|
border-radius: 24rpx;
|
|||
|
|
padding: 0 24rpx;
|
|||
|
|
padding-right: 96rpx;
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #3e5481;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-unit {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 24rpx;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-hint {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #99a1af;
|
|||
|
|
margin-top: -16rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 底部按钮 */
|
|||
|
|
.footer-btn {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
background: #ffffff;
|
|||
|
|
border-top: 1rpx solid #d0dbea;
|
|||
|
|
padding: 32rpx;
|
|||
|
|
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calculate-btn {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 96rpx;
|
|||
|
|
background: #ff6b35;
|
|||
|
|
border-radius: 50rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
|
|||
|
|
text {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #ffffff;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
transform: scale(0.98);
|
|||
|
|
opacity: 0.9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.disabled {
|
|||
|
|
background: #d0dbea;
|
|||
|
|
box-shadow: none;
|
|||
|
|
pointer-events: none;
|
|||
|
|
|
|||
|
|
text {
|
|||
|
|
color: #9fa5c0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 底部安全距离 */
|
|||
|
|
.safe-bottom {
|
|||
|
|
height: calc(160rpx + env(safe-area-inset-bottom));
|
|||
|
|
}
|
|||
|
|
</style>
|