Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿
This commit is contained in:
536
msh_single_uniapp/pages/tool/calculator.vue
Normal file
536
msh_single_uniapp/pages/tool/calculator.vue
Normal file
@@ -0,0 +1,536 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user