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; 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 { &__hint {
margin: var(--tk-gap-xl) 0; margin: var(--tk-gap-xl) 0;
padding: var(--tk-gap-md); padding: var(--tk-gap-md);

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,4 +83,30 @@
justify-content: space-between; justify-content: space-between;
margin-bottom: 16px; 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 modeClass = useDoctorClass();
const [dashboard, setDashboard] = useState<DoctorDashboard | null>(null); const [dashboard, setDashboard] = useState<DoctorDashboard | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);
const hasRole = (allowed: string[] | undefined) => { const hasRole = (allowed: string[] | undefined) => {
if (!allowed) return true; if (!allowed) return true;
@@ -57,8 +58,10 @@ export default function DoctorHome() {
try { try {
const data = await getDashboard(); const data = await getDashboard();
setDashboard(data); setDashboard(data);
setLoadError(false);
} catch (err) { } catch (err) {
console.warn('[doctor] 加载工作台数据失败:', err); console.warn('[doctor] 加载工作台数据失败:', err);
setLoadError(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -78,6 +81,19 @@ export default function DoctorHome() {
if (loading) return <Loading />; 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 ( return (
<View className={`doctor-home ${modeClass}`}> <View className={`doctor-home ${modeClass}`}>
<ScrollView scrollY className="doctor-home__scroll"> <ScrollView scrollY className="doctor-home__scroll">

View File

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