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