新增 useSafeTimeout hook,页面隐藏时自动清理所有定时器。 10 个页面接入:daily-monitoring、exchange、family-add、 health/input、prescription detail/create、dialysis detail/create、 appointment detail/create。所有 fire-and-forget setTimeout 替换为 safeSetTimeout,避免页面切走后定时器回调在错误上下文执行。
169 lines
6.2 KiB
TypeScript
169 lines
6.2 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { View, Text, ScrollView } from '@tarojs/components';
|
|
import Taro, { useRouter } from '@tarojs/taro';
|
|
import {
|
|
getDialysisPrescription, updateDialysisPrescription, deleteDialysisPrescription,
|
|
type DialysisPrescription,
|
|
} from '@/services/doctor/dialysis';
|
|
import Loading from '@/components/Loading';
|
|
import { useElderClass } from '../../../../hooks/useElderClass';
|
|
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
|
import './index.scss';
|
|
|
|
export default function PrescriptionDetail() {
|
|
const router = useRouter();
|
|
const id = router.params.id || '';
|
|
const modeClass = useElderClass();
|
|
const [rx, setRx] = useState<DialysisPrescription | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const { safeSetTimeout } = useSafeTimeout();
|
|
|
|
useEffect(() => {
|
|
if (id) loadRx();
|
|
}, [id]);
|
|
|
|
const loadRx = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getDialysisPrescription(id);
|
|
setRx(data);
|
|
} catch {
|
|
Taro.showToast({ title: '加载失败', icon: 'none' });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeactivate = async () => {
|
|
if (!rx) return;
|
|
const { confirm } = await Taro.showModal({
|
|
title: '确认停用',
|
|
content: '停用后该处方将不再生效,确定停用吗?',
|
|
});
|
|
if (!confirm) return;
|
|
setSubmitting(true);
|
|
try {
|
|
const updated = await updateDialysisPrescription(id, { status: 'inactive' }, rx.version);
|
|
setRx(updated);
|
|
Taro.showToast({ title: '已停用', icon: 'success' });
|
|
} catch {
|
|
Taro.showToast({ title: '操作失败', icon: 'none' });
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!rx) return;
|
|
const { confirm } = await Taro.showModal({
|
|
title: '确认删除',
|
|
content: '删除后不可恢复,确定要删除这条处方吗?',
|
|
});
|
|
if (!confirm) return;
|
|
setSubmitting(true);
|
|
try {
|
|
await deleteDialysisPrescription(id, rx.version);
|
|
Taro.showToast({ title: '已删除', icon: 'success' });
|
|
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
|
} catch {
|
|
Taro.showToast({ title: '删除失败', icon: 'none' });
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const Row = ({ label, value, unit }: { label: string; value?: string | number | null; unit?: string }) => {
|
|
if (value == null) return null;
|
|
return (
|
|
<View className='detail-row'>
|
|
<Text className='detail-label'>{label}</Text>
|
|
<Text className='detail-value'>{value}{unit || ''}</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
if (loading) return <Loading />;
|
|
if (!rx) return <View className={`error-text ${modeClass}`}><Text>处方加载失败</Text></View>;
|
|
|
|
return (
|
|
<ScrollView scrollY className={`prescription-detail ${modeClass}`}>
|
|
{/* 状态头部 */}
|
|
<View className='section'>
|
|
<View className='rx-header'>
|
|
<Text className='rx-header__title'>{rx.dialyzer_model || '透析处方'}</Text>
|
|
<Text className={`rx-header__status rx-header__status--${rx.status}`}>
|
|
{rx.status === 'active' ? '生效中' : rx.status === 'inactive' ? '已停用' : rx.status}
|
|
</Text>
|
|
</View>
|
|
{(rx.effective_from || rx.effective_to) && (
|
|
<Text className='rx-sub'>{rx.effective_from || '...'} ~ {rx.effective_to || '...'}</Text>
|
|
)}
|
|
</View>
|
|
|
|
{/* 基本参数 */}
|
|
<View className='section'>
|
|
<Text className='section-title'>基本参数</Text>
|
|
<Row label='透析器型号' value={rx.dialyzer_model} />
|
|
<Row label='膜面积' value={rx.membrane_area != null ? `${rx.membrane_area}` : null} unit=' m²' />
|
|
<Row label='血流速' value={rx.blood_flow_rate} unit=' ml/min' />
|
|
<Row label='透析液流量' value={rx.dialysate_flow_rate} unit=' ml/min' />
|
|
<Row label='频率' value={rx.frequency_per_week != null ? `${rx.frequency_per_week} 次/周` : null} />
|
|
<Row label='每次时长' value={rx.duration_minutes} unit=' 分钟' />
|
|
</View>
|
|
|
|
{/* 透析液配比 */}
|
|
<View className='section'>
|
|
<Text className='section-title'>透析液配比</Text>
|
|
<Row label='钾浓度' value={rx.dialysate_potassium} unit=' mmol/L' />
|
|
<Row label='钙浓度' value={rx.dialysate_calcium} unit=' mmol/L' />
|
|
<Row label='碳酸氢盐' value={rx.dialysate_bicarbonate} unit=' mmol/L' />
|
|
</View>
|
|
|
|
{/* 抗凝方案 */}
|
|
<View className='section'>
|
|
<Text className='section-title'>抗凝方案</Text>
|
|
<Row label='抗凝类型' value={rx.anticoagulation_type} />
|
|
<Row label='抗凝剂量' value={rx.anticoagulation_dose} />
|
|
</View>
|
|
|
|
{/* 血管通路 */}
|
|
{(rx.vascular_access_type || rx.vascular_access_location) && (
|
|
<View className='section'>
|
|
<Text className='section-title'>血管通路</Text>
|
|
<Row label='通路类型' value={rx.vascular_access_type} />
|
|
<Row label='通路位置' value={rx.vascular_access_location} />
|
|
</View>
|
|
)}
|
|
|
|
{/* 超滤目标 */}
|
|
{(rx.target_ultrafiltration_ml != null || rx.target_dry_weight != null) && (
|
|
<View className='section'>
|
|
<Text className='section-title'>超滤目标</Text>
|
|
<Row label='目标超滤量' value={rx.target_ultrafiltration_ml} unit=' ml' />
|
|
<Row label='目标干体重' value={rx.target_dry_weight} unit=' kg' />
|
|
</View>
|
|
)}
|
|
|
|
{/* 备注 */}
|
|
{rx.notes && (
|
|
<View className='section'>
|
|
<Text className='section-title'>备注</Text>
|
|
<Text className='notes-text'>{rx.notes}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* 操作按钮 */}
|
|
<View className='actions'>
|
|
{rx.status === 'active' && (
|
|
<View className={`action-btn action-btn--secondary ${submitting ? 'action-btn--disabled' : ''}`} onClick={handleDeactivate}>
|
|
<Text className='action-btn__text'>停用处方</Text>
|
|
</View>
|
|
)}
|
|
<View className='action-btn action-btn--danger' onClick={handleDelete}>
|
|
<Text className='action-btn__text'>删除</Text>
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|