Files
msh-system/msh_single_uniapp/pages/tool/calculator.vue

537 lines
12 KiB
Vue
Raw Normal View History

<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>