fix(mp): Phase 1 核心体验修复 — 咨询描述+体征校验+商城+医生端+跳转

- consultation: 添加 description 字段 + 症状描述输入 + 建议填写提醒
- health/index: 使用 validateNum 添加体征范围校验(血压/心率/血糖/体重)
- mall: 隐藏未实现的积分任务空壳入口
- pkg-doctor-core: 工作台加载失败添加重试按钮和错误状态
- index: 医护人员跳转返回 null 替代 Loading 避免无用渲染
This commit is contained in:
iven
2026-05-21 16:18:20 +08:00
parent 23f7bcb8ce
commit 6338cd7428
8 changed files with 97 additions and 13 deletions

View File

@@ -34,6 +34,17 @@
color: $tx3;
}
&__textarea {
width: 100%;
min-height: 120px;
padding: var(--tk-gap-md);
font-size: var(--tk-font-body);
background: $card;
border: 1px solid $bd;
border-radius: $r-sm;
box-sizing: border-box;
}
&__hint {
margin: var(--tk-gap-xl) 0;
padding: var(--tk-gap-md);

View File

@@ -27,6 +27,7 @@ export default function ConsultationCreate() {
const [typeIdx, setTypeIdx] = useState(0);
const [submitting, setSubmitting] = useState(false);
const [doctorsLoaded, setDoctorsLoaded] = useState(false);
const [description, setDescription] = useState('');
const modeClass = useElderClass();
const loadDoctors = async () => {
@@ -48,12 +49,24 @@ export default function ConsultationCreate() {
return;
}
if (submitting) return;
if (!description.trim()) {
const { confirm } = await Taro.showModal({
title: '提示',
content: '建议描述症状以便医生更快响应,是否继续?',
confirmText: '继续提交',
cancelText: '去填写',
});
if (!confirm) return;
}
setSubmitting(true);
try {
const session = await createSession({
patient_id: currentPatient.id,
doctor_id: selectedDoctorIdx >= 0 ? doctorList[selectedDoctorIdx]?.id : undefined,
consultation_type: CONSULTATION_TYPES[typeIdx],
description: description.trim() || undefined,
});
Taro.showToast({ title: '创建成功', icon: 'success' });
setTimeout(() => {
@@ -109,6 +122,18 @@ export default function ConsultationCreate() {
</Picker>
</View>
<View className='consult-create__section'>
<Text className='consult-create__label'></Text>
<Input
className='consult-create__textarea'
type='text'
placeholder='请描述您的症状或问题'
value={description}
onInput={(e) => setDescription(e.detail.value)}
maxlength={500}
/>
</View>
<View className='consult-create__hint'>
<Text className='consult-create__hint-text'>

View File

@@ -5,6 +5,7 @@ import { safeNavigateTo } from '@/utils/navigate';
import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import { findThreshold, inputVitalSign, type HealthThreshold } from '../../services/health';
import { validateNum } from '../../utils/validate';
import Loading from '../../components/Loading';
import ErrorState from '../../components/ErrorState';
import GuestGuard from '../../components/GuestGuard';
@@ -113,6 +114,10 @@ export default function Health() {
const sys = parseFloat(systolic);
const dia = parseFloat(diastolic);
if (!sys || !dia) { Taro.showToast({ title: '请填写完整', icon: 'none' }); return; }
const sysErr = validateNum(sys, '收缩压', { min: 60, max: 250 });
if (sysErr) { Taro.showToast({ title: sysErr, icon: 'none' }); return; }
const diaErr = validateNum(dia, '舒张压', { min: 40, max: 150 });
if (diaErr) { Taro.showToast({ title: diaErr, icon: 'none' }); return; }
await inputVitalSign(patientId, {
indicator_type: 'blood_pressure',
value: sys,
@@ -125,6 +130,8 @@ export default function Health() {
case 'heart_rate': {
const val = parseFloat(heartRateVal);
if (!val) { Taro.showToast({ title: '请填写心率', icon: 'none' }); return; }
const err = validateNum(val, '心率', { min: 30, max: 220 });
if (err) { Taro.showToast({ title: err, icon: 'none' }); return; }
await inputVitalSign(patientId, { indicator_type: 'heart_rate', value: val });
setHeartRateVal('');
break;
@@ -132,6 +139,8 @@ export default function Health() {
case 'blood_sugar': {
const val = parseFloat(sugarVal);
if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; }
const err = validateNum(val, '血糖', { min: 1.0, max: 33.3 });
if (err) { Taro.showToast({ title: err, icon: 'none' }); return; }
const bsType = sugarPeriod === 'fasting' ? 'blood_sugar_fasting' : 'blood_sugar_postprandial';
await inputVitalSign(patientId, { indicator_type: bsType, value: val });
setSugarVal('');
@@ -140,6 +149,8 @@ export default function Health() {
case 'weight': {
const val = parseFloat(weightVal);
if (!val) { Taro.showToast({ title: '请填写体重', icon: 'none' }); return; }
const err = validateNum(val, '体重', { min: 20, max: 300 });
if (err) { Taro.showToast({ title: err, icon: 'none' }); return; }
await inputVitalSign(patientId, { indicator_type: 'weight', value: val });
setWeightVal('');
break;

View File

@@ -316,7 +316,7 @@ export default function Index() {
// 医护人员访问患者首页时,自动跳转到医生端
// 不渲染 HomeDashboard避免触发患者首页的 API 请求(并发叠加问题)
const shouldRedirect = user && isMedicalStaff();
const shouldRedirect = !!(user && isMedicalStaff());
useDidShow(() => {
if (shouldRedirect) {
@@ -329,11 +329,10 @@ export default function Index() {
}
});
if (!user) {
return <GuestHome modeClass={modeClass} />;
}
if (shouldRedirect) {
return <Loading />;
}
// 未登录 → 访客首页
if (!user) return <GuestHome modeClass={modeClass} />;
// 医护人员 → 等待跳转(返回 null 避免无用渲染)
if (shouldRedirect) return null;
// 患者用户 → 正常首页
return <HomeDashboard modeClass={modeClass} />;
}

View File

@@ -184,12 +184,7 @@ export default function Mall() {
</View>
<Text className='mall-action-label'></Text>
</View>
<View className='mall-action'>
<View className='mall-action-icon mall-action-icon--task'>
<Text className='mall-action-icon-text'></Text>
</View>
<Text className='mall-action-label'></Text>
</View>
{/* TODO: 积分任务功能待实现后恢复 */}
<View className='mall-action' onClick={() => safeNavigateTo('/pages/pkg-mall/orders/index')}>
<View className='mall-action-icon mall-action-icon--history'>
<Text className='mall-action-icon-text'></Text>

View File

@@ -83,4 +83,30 @@
justify-content: space-between;
margin-bottom: 16px;
}
// ── 错误状态 ──
&__error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 32px;
gap: 24px;
}
&__error-text {
font-size: var(--tk-body-lg);
color: var(--tk-text-secondary);
}
&__error-retry {
padding: 16px 48px;
background: var(--tk-primary);
border-radius: 12px;
}
&__error-retry-text {
font-size: var(--tk-body);
color: #fff;
}
}

View File

@@ -45,6 +45,7 @@ export default function DoctorHome() {
const modeClass = useDoctorClass();
const [dashboard, setDashboard] = useState<DoctorDashboard | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);
const hasRole = (allowed: string[] | undefined) => {
if (!allowed) return true;
@@ -57,8 +58,10 @@ export default function DoctorHome() {
try {
const data = await getDashboard();
setDashboard(data);
setLoadError(false);
} catch (err) {
console.warn('[doctor] 加载工作台数据失败:', err);
setLoadError(true);
} finally {
setLoading(false);
}
@@ -78,6 +81,19 @@ export default function DoctorHome() {
if (loading) return <Loading />;
if (loadError && !dashboard) {
return (
<View className={`doctor-home ${modeClass}`}>
<View className='doctor-home__error'>
<Text className='doctor-home__error-text'></Text>
<View className='doctor-home__error-retry' onClick={() => { setLoading(true); loadDashboard(); }}>
<Text className='doctor-home__error-retry-text'></Text>
</View>
</View>
</View>
);
}
return (
<View className={`doctor-home ${modeClass}`}>
<ScrollView scrollY className="doctor-home__scroll">

View File

@@ -79,6 +79,7 @@ export async function createSession(params: {
patient_id: string;
doctor_id?: string;
consultation_type?: string;
description?: string;
}) {
return api.post<ConsultationSession>('/health/consultation-sessions', params);
}