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

537 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="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>