refactor(mp): 分包策略优化 — 合并单页分包 + doctor 拆包 + consultation 移出主包

- 合并 4 个单页分包:report→pkg-profile/reports, followup→pkg-profile/followups,
  events→pkg-profile/events, device-sync→pkg-health
- consultation/detail 移出主包到 pkg-consultation 分包(减少主包体积)
- doctor 18 页拆分为 pkg-doctor-core(8页) + pkg-doctor-clinical(10页)
- 全部导航路径和 import 路径同步更新
- 分包 10→8 个,主包页面 13→12
This commit is contained in:
iven
2026-05-15 07:53:00 +08:00
parent 5baa518516
commit 4c38fcd89d
58 changed files with 71 additions and 78 deletions

View File

@@ -0,0 +1,98 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.create-page {
min-height: 100vh;
background: $bg;
padding: 24px;
padding-bottom: 200px;
}
.section {
background: $card;
border-radius: $r;
padding: 24px;
margin-bottom: 16px;
box-shadow: $shadow-sm;
}
.section-title {
font-size: var(--tk-font-body-lg);
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 16px;
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid $bd-l;
&:last-child {
border-bottom: none;
}
&--textarea {
flex-direction: column;
align-items: flex-start;
}
}
.form-label {
font-size: var(--tk-font-h1);
color: $tx2;
flex-shrink: 0;
min-width: 140px;
}
.form-input {
flex: 1;
text-align: right;
font-size: var(--tk-font-h1);
color: $tx;
}
.form-value {
font-size: var(--tk-font-h1);
color: $tx;
&.placeholder {
color: $tx3;
}
}
.form-textarea {
width: 100%;
margin-top: 12px;
font-size: var(--tk-font-h1);
color: $tx;
min-height: 120px;
background: $bg;
border-radius: $r-sm;
padding: 16px;
}
.submit-btn {
background: $pri;
border-radius: $r-sm;
padding: 24px;
text-align: center;
margin-top: 24px;
&:active {
background: $pri-d;
}
&--disabled {
opacity: 0.5;
}
}
.submit-btn__text {
font-size: var(--tk-font-num);
font-weight: bold;
color: $white;
}

View File

@@ -0,0 +1,250 @@
import { useState, useEffect } from 'react';
import { View, Text, Input, Textarea, Picker, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import {
getDialysisRecord, updateDialysisRecord, createDialysisRecord,
} from '@/services/doctor/dialysis';
import Loading from '@/components/Loading';
import { useElderClass } from '../../../../hooks/useElderClass';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import './index.scss';
const DIALYSIS_TYPES = ['HD', 'HDF', 'HF'];
interface FormState {
patient_id: string;
dialysis_date: string;
start_time: string;
end_time: string;
dialysis_type: string;
dialysis_duration: string;
blood_flow_rate: string;
dry_weight: string;
pre_weight: string;
post_weight: string;
pre_bp_systolic: string;
pre_bp_diastolic: string;
post_bp_systolic: string;
post_bp_diastolic: string;
pre_heart_rate: string;
post_heart_rate: string;
ultrafiltration_volume: string;
complication_notes: string;
}
const initialForm: FormState = {
patient_id: '',
dialysis_date: '',
start_time: '',
end_time: '',
dialysis_type: 'HD',
dialysis_duration: '',
blood_flow_rate: '',
dry_weight: '',
pre_weight: '',
post_weight: '',
pre_bp_systolic: '',
pre_bp_diastolic: '',
post_bp_systolic: '',
post_bp_diastolic: '',
pre_heart_rate: '',
post_heart_rate: '',
ultrafiltration_volume: '',
complication_notes: '',
};
export default function DialysisCreate() {
const router = useRouter();
const id = router.params.id || '';
const version = router.params.version ? Number(router.params.version) : 0;
const patientIdFromRoute = router.params.patientId || '';
const isEdit = !!id;
const modeClass = useElderClass();
const [form, setForm] = useState<FormState>({ ...initialForm, patient_id: patientIdFromRoute });
const [loading, setLoading] = useState(isEdit);
const [submitting, setSubmitting] = useState(false);
const { safeSetTimeout } = useSafeTimeout();
useEffect(() => {
if (isEdit && id) loadRecord();
}, [id]);
const loadRecord = async () => {
setLoading(true);
try {
const r = await getDialysisRecord(id);
setForm({
patient_id: r.patient_id,
dialysis_date: r.dialysis_date || '',
start_time: r.start_time || '',
end_time: r.end_time || '',
dialysis_type: r.dialysis_type || 'HD',
dialysis_duration: r.dialysis_duration != null ? String(r.dialysis_duration) : '',
blood_flow_rate: r.blood_flow_rate != null ? String(r.blood_flow_rate) : '',
dry_weight: r.dry_weight != null ? String(r.dry_weight) : '',
pre_weight: r.pre_weight != null ? String(r.pre_weight) : '',
post_weight: r.post_weight != null ? String(r.post_weight) : '',
pre_bp_systolic: r.pre_bp_systolic != null ? String(r.pre_bp_systolic) : '',
pre_bp_diastolic: r.pre_bp_diastolic != null ? String(r.pre_bp_diastolic) : '',
post_bp_systolic: r.post_bp_systolic != null ? String(r.post_bp_systolic) : '',
post_bp_diastolic: r.post_bp_diastolic != null ? String(r.post_bp_diastolic) : '',
pre_heart_rate: r.pre_heart_rate != null ? String(r.pre_heart_rate) : '',
post_heart_rate: r.post_heart_rate != null ? String(r.post_heart_rate) : '',
ultrafiltration_volume: r.ultrafiltration_volume != null ? String(r.ultrafiltration_volume) : '',
complication_notes: r.complication_notes || '',
});
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
};
const updateField = (key: keyof FormState, value: string) => {
setForm((prev) => ({ ...prev, [key]: value }));
};
const handleSubmit = async () => {
if (!form.dialysis_date) {
Taro.showToast({ title: '请选择透析日期', icon: 'none' });
return;
}
if (!form.patient_id) {
Taro.showToast({ title: '缺少患者信息', icon: 'none' });
return;
}
setSubmitting(true);
const num = (v: string) => v ? Number(v) : undefined;
const payload = {
patient_id: form.patient_id,
dialysis_date: form.dialysis_date,
start_time: form.start_time || undefined,
end_time: form.end_time || undefined,
dialysis_type: form.dialysis_type,
dialysis_duration: num(form.dialysis_duration),
blood_flow_rate: num(form.blood_flow_rate),
dry_weight: num(form.dry_weight),
pre_weight: num(form.pre_weight),
post_weight: num(form.post_weight),
pre_bp_systolic: num(form.pre_bp_systolic),
pre_bp_diastolic: num(form.pre_bp_diastolic),
post_bp_systolic: num(form.post_bp_systolic),
post_bp_diastolic: num(form.post_bp_diastolic),
pre_heart_rate: num(form.pre_heart_rate),
post_heart_rate: num(form.post_heart_rate),
ultrafiltration_volume: num(form.ultrafiltration_volume),
complication_notes: form.complication_notes || undefined,
};
try {
if (isEdit) {
const { patient_id, ...updateData } = payload;
await updateDialysisRecord(id, updateData, version);
Taro.showToast({ title: '更新成功', icon: 'success' });
} else {
await createDialysisRecord(payload);
Taro.showToast({ title: '创建成功', icon: 'success' });
}
safeSetTimeout(() => Taro.navigateBack(), 1000);
} catch {
Taro.showToast({ title: isEdit ? '更新失败' : '创建失败', icon: 'none' });
} finally {
setSubmitting(false);
}
};
if (loading) return <Loading />;
const InputField = ({ label, field, placeholder, type = 'digit' }: {
label: string; field: keyof FormState; placeholder: string; type?: string;
}) => (
<View className='form-row'>
<Text className='form-label'>{label}</Text>
<Input
className='form-input'
type={type as any}
placeholder={placeholder}
value={form[field]}
onInput={(e) => updateField(field, e.detail.value)}
/>
</View>
);
return (
<ScrollView scrollY className={`create-page ${modeClass}`}>
<View className='section'>
<Text className='section-title'></Text>
<View className='form-row'>
<Text className='form-label'></Text>
<Picker mode='date' value={form.dialysis_date} onChange={(e) => updateField('dialysis_date', e.detail.value)}>
<Text className={`form-value ${!form.dialysis_date ? 'placeholder' : ''}`}>
{form.dialysis_date || '请选择日期'}
</Text>
</Picker>
</View>
<View className='form-row'>
<Text className='form-label'></Text>
<Picker mode='time' value={form.start_time} onChange={(e) => updateField('start_time', e.detail.value)}>
<Text className={`form-value ${!form.start_time ? 'placeholder' : ''}`}>
{form.start_time || '请选择时间'}
</Text>
</Picker>
</View>
<View className='form-row'>
<Text className='form-label'></Text>
<Picker mode='time' value={form.end_time} onChange={(e) => updateField('end_time', e.detail.value)}>
<Text className={`form-value ${!form.end_time ? 'placeholder' : ''}`}>
{form.end_time || '请选择时间'}
</Text>
</Picker>
</View>
<View className='form-row'>
<Text className='form-label'></Text>
<Picker mode='selector' range={DIALYSIS_TYPES} value={DIALYSIS_TYPES.indexOf(form.dialysis_type)} onChange={(e) => updateField('dialysis_type', DIALYSIS_TYPES[Number(e.detail.value)])}>
<Text className='form-value'>{form.dialysis_type}</Text>
</Picker>
</View>
<InputField label='透析时长' field='dialysis_duration' placeholder='分钟' type='number' />
<InputField label='血流速' field='blood_flow_rate' placeholder='ml/min' type='number' />
</View>
<View className='section'>
<Text className='section-title'></Text>
<InputField label='干体重' field='dry_weight' placeholder='kg' />
<InputField label='透前体重' field='pre_weight' placeholder='kg' />
<InputField label='透后体重' field='post_weight' placeholder='kg' />
</View>
<View className='section'>
<Text className='section-title'></Text>
<InputField label='透前收缩压' field='pre_bp_systolic' placeholder='mmHg' type='number' />
<InputField label='透前舒张压' field='pre_bp_diastolic' placeholder='mmHg' type='number' />
<InputField label='透后收缩压' field='post_bp_systolic' placeholder='mmHg' type='number' />
<InputField label='透后舒张压' field='post_bp_diastolic' placeholder='mmHg' type='number' />
<InputField label='透前心率' field='pre_heart_rate' placeholder='bpm' type='number' />
<InputField label='透后心率' field='post_heart_rate' placeholder='bpm' type='number' />
</View>
<View className='section'>
<Text className='section-title'></Text>
<InputField label='超滤量' field='ultrafiltration_volume' placeholder='ml' type='number' />
<View className='form-row form-row--textarea'>
<Text className='form-label'></Text>
<Textarea
className='form-textarea'
placeholder='请输入...'
value={form.complication_notes}
onInput={(e) => updateField('complication_notes', e.detail.value)}
maxlength={500}
/>
</View>
</View>
<View className={`submit-btn ${submitting ? 'submit-btn--disabled' : ''}`} onClick={handleSubmit}>
<Text className='submit-btn__text'>{submitting ? '提交中...' : isEdit ? '更新记录' : '创建记录'}</Text>
</View>
</ScrollView>
);
}

View File

@@ -0,0 +1,156 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.dialysis-detail {
min-height: 100vh;
background: $bg;
padding: 24px;
padding-bottom: 200px;
}
.section {
background: $card;
border-radius: $r;
padding: 24px;
margin-bottom: 16px;
box-shadow: $shadow-sm;
}
.section-title {
font-size: var(--tk-font-body-lg);
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 12px;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.record-header__title {
font-size: var(--tk-font-num-lg);
font-weight: bold;
color: $tx;
font-variant-numeric: tabular-nums;
}
.record-header__status {
display: inline-block;
padding: 4px 12px;
border-radius: $r-xs;
font-size: var(--tk-font-body);
background: $bd-l;
color: $tx3;
&--completed {
background: $pri-l;
color: $pri;
}
&--reviewed {
background: $acc-l;
color: $acc;
}
}
.record-sub {
font-size: var(--tk-font-h1);
color: $tx2;
display: block;
}
.review-info {
font-size: var(--tk-font-h2);
color: $tx3;
display: block;
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid $bd-l;
&:last-child {
border-bottom: none;
}
}
.detail-label {
font-size: var(--tk-font-h1);
color: $tx2;
}
.detail-value {
font-size: var(--tk-font-h1);
color: $tx;
text-align: right;
flex: 1;
margin-left: 24px;
font-variant-numeric: tabular-nums;
}
.error-text {
text-align: center;
padding: 120px 0;
font-size: var(--tk-font-body-lg);
color: $tx3;
}
.actions {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.action-btn {
border-radius: $r-sm;
padding: 20px;
text-align: center;
&--primary {
background: $pri;
.action-btn__text {
color: $white;
}
&:active {
background: $pri-d;
}
}
&--secondary {
background: $card;
border: 1px solid $bd;
.action-btn__text {
color: $pri;
}
}
&--danger {
background: $card;
border: 1px solid $dan;
.action-btn__text {
color: $dan;
}
}
&--disabled {
opacity: 0.5;
}
}
.action-btn__text {
font-size: var(--tk-font-body-lg);
font-weight: 500;
}

View File

@@ -0,0 +1,180 @@
import { useState, useCallback } from 'react';
import { View, Text, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import {
getDialysisRecord, reviewDialysisRecord,
updateDialysisRecord, deleteDialysisRecord,
type DialysisRecord,
} 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 DialysisDetail() {
const router = useRouter();
const id = router.params.id || '';
const modeClass = useElderClass();
const [record, setRecord] = useState<DialysisRecord | null>(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const { safeSetTimeout } = useSafeTimeout();
const loadRecord = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const r = await getDialysisRecord(id);
setRecord(r);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, [id]);
usePageData(loadRecord, { throttleMs: 60000, enablePullDown: false, enabled: !!id });
const handleReview = async () => {
if (!record) return;
setSubmitting(true);
try {
const updated = await reviewDialysisRecord(id, record.version);
setRecord(updated);
Taro.showToast({ title: '审核完成', icon: 'success' });
} catch {
Taro.showToast({ title: '审核失败', icon: 'none' });
} finally {
setSubmitting(false);
}
};
const handleComplete = async () => {
if (!record) return;
setSubmitting(true);
try {
const updated = await updateDialysisRecord(id, { status: 'completed' }, record.version);
setRecord(updated);
Taro.showToast({ title: '已标记完成', icon: 'success' });
} catch {
Taro.showToast({ title: '操作失败', icon: 'none' });
} finally {
setSubmitting(false);
}
};
const handleDelete = async () => {
if (!record) return;
const { confirm } = await Taro.showModal({
title: '确认删除',
content: '删除后不可恢复,确定要删除这条记录吗?',
});
if (!confirm) return;
setSubmitting(true);
try {
await deleteDialysisRecord(id, record.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 (!record) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
const canComplete = record.status === 'draft';
const canReview = record.status === 'completed';
return (
<ScrollView scrollY className={`dialysis-detail ${modeClass}`}>
{/* 状态头部 */}
<View className='section'>
<View className='record-header'>
<Text className='record-header__title'>{record.dialysis_date}</Text>
<Text className={`record-header__status record-header__status--${record.status}`}>
{record.status === 'draft' ? '草稿' : record.status === 'completed' ? '已完成' : '已审核'}
</Text>
</View>
<Text className='record-sub'>
{(record.dialysis_type === 'HD' ? '血液透析' : record.dialysis_type === 'HDF' ? '血液透析滤过' : record.dialysis_type === 'HF' ? '血液滤过' : record.dialysis_type)}
</Text>
{record.reviewed_at && <Text className='review-info'> {record.reviewed_at}</Text>}
</View>
{/* 基本信息 */}
<View className='section'>
<Text className='section-title'></Text>
<Row label='透析日期' value={record.dialysis_date} />
<Row label='开始时间' value={record.start_time} />
<Row label='结束时间' value={record.end_time} />
<Row label='透析时长' value={record.dialysis_duration} unit=' 分钟' />
<Row label='血流速' value={record.blood_flow_rate} unit=' ml/min' />
<Row label='超滤量' value={record.ultrafiltration_volume} unit=' ml' />
</View>
{/* 体重与血压 */}
<View className='section'>
<Text className='section-title'></Text>
<Row label='干体重' value={record.dry_weight} unit=' kg' />
<Row label='透前体重' value={record.pre_weight} unit=' kg' />
<Row label='透后体重' value={record.post_weight} unit=' kg' />
{record.pre_bp_systolic != null && record.pre_bp_diastolic != null && (
<Row label='透前血压' value={`${record.pre_bp_systolic}/${record.pre_bp_diastolic}`} unit=' mmHg' />
)}
{record.post_bp_systolic != null && record.post_bp_diastolic != null && (
<Row label='透后血压' value={`${record.post_bp_systolic}/${record.post_bp_diastolic}`} unit=' mmHg' />
)}
<Row label='透前心率' value={record.pre_heart_rate} unit=' bpm' />
<Row label='透后心率' value={record.post_heart_rate} unit=' bpm' />
</View>
{/* 症状与并发症 */}
{(record.symptoms || record.complication_notes) && (
<View className='section'>
<Text className='section-title'></Text>
{record.symptoms && (
<Row label='症状' value={JSON.stringify(record.symptoms)} />
)}
<Row label='并发症备注' value={record.complication_notes} />
</View>
)}
{/* 操作按钮 */}
<View className='actions'>
{canComplete && (
<View className={`action-btn action-btn--primary ${submitting ? 'action-btn--disabled' : ''}`} onClick={handleComplete}>
<Text className='action-btn__text'>{submitting ? '处理中...' : '标记完成'}</Text>
</View>
)}
{canReview && (
<View className={`action-btn action-btn--primary ${submitting ? 'action-btn--disabled' : ''}`} onClick={handleReview}>
<Text className='action-btn__text'>{submitting ? '审核中...' : '确认审核'}</Text>
</View>
)}
{record.status === 'draft' && (
<View className='action-btn action-btn--secondary' onClick={() => Taro.navigateTo({
url: `/pages/pkg-doctor-clinical/dialysis/create/index?id=${id}&version=${record.version}`,
})}>
<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>
);
}

View File

@@ -0,0 +1,194 @@
@import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
.dialysis-page {
min-height: 100vh;
background: $bg;
padding-bottom: 120px;
}
.search-bar {
padding: 16px 24px;
background: $card;
}
.search-input {
background: $bg;
border-radius: $r-sm;
padding: 16px 20px;
font-size: var(--tk-font-body-lg);
color: $tx;
}
.tabs {
display: flex;
padding: 0 24px;
background: $card;
border-bottom: 1px solid $bd-l;
}
.tab {
flex: 1;
text-align: center;
padding: 20px 0;
position: relative;
&--active {
.tab-text {
color: $pri;
font-weight: bold;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 25%;
right: 25%;
height: 4px;
background: $pri;
border-radius: $r-xs;
}
}
}
.tab-text {
font-size: var(--tk-font-h1);
color: $tx2;
}
.record-list {
padding: 16px 24px;
}
.record-count {
font-size: var(--tk-font-h2);
color: $tx3;
padding: 8px 0 16px;
}
.record-card {
background: $card;
border-radius: $r;
padding: 24px;
margin-bottom: 16px;
box-shadow: $shadow-sm;
&:active {
box-shadow: $shadow-md;
}
}
.record-card__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.type-tag {
display: inline-block;
padding: 4px 12px;
border-radius: $r-xs;
font-size: var(--tk-font-body);
font-weight: 600;
background: $pri-l;
color: $pri-d;
&--hdf {
background: $acc-l;
color: $acc;
}
&--hf {
background: $wrn-l;
color: $wrn;
}
}
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: $r-xs;
font-size: var(--tk-font-body);
background: $bd-l;
color: $tx3;
&--completed {
background: $pri-l;
color: $pri;
}
&--reviewed {
background: $acc-l;
color: $acc;
}
}
.record-card__body {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.record-card__date {
font-size: var(--tk-font-h1);
color: $tx;
font-variant-numeric: tabular-nums;
}
.record-card__meta {
font-size: var(--tk-font-h2);
color: $tx2;
font-variant-numeric: tabular-nums;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 0;
gap: 24px;
}
.page-btn {
padding: 12px 24px;
background: $card;
border-radius: $r-sm;
font-size: var(--tk-font-h1);
color: $pri;
&--disabled {
opacity: 0.4;
}
}
.page-info {
font-size: var(--tk-font-h2);
color: $tx2;
}
.fab {
position: fixed;
right: 32px;
bottom: 120px;
width: 96px;
height: 96px;
border-radius: $r-pill;
background: $pri;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-md;
z-index: 10;
&:active {
background: $pri-d;
}
}
.fab-text {
font-size: var(--tk-font-hero);
color: $white;
font-weight: bold;
}

View File

@@ -0,0 +1,185 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { View, Text, Input, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listDialysisRecords, type DialysisRecord } from '@/services/doctor/dialysis';
import { listPatients } from '@/services/doctor/patient';
import Loading from '@/components/Loading';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { safeNavigateTo } from '@/utils/navigate';
import './index.scss';
const TABS = [
{ key: '', label: '全部' },
{ key: 'draft', label: '草稿' },
{ key: 'completed', label: '已完成' },
{ key: 'reviewed', label: '已审核' },
];
const TYPE_MAP: Record<string, string> = { HD: 'HD', HDF: 'HDF', HF: 'HF' };
export default function DialysisList() {
const router = useRouter();
const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [searchPatient, setSearchPatient] = useState('');
const [currentPatientId, setCurrentPatientId] = useState(patientId);
const [activeTab, setActiveTab] = useState('');
const [records, setRecords] = useState<DialysisRecord[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
const loadRecords = useCallback(async (p: number) => {
if (!currentPatientId) return;
setLoading(true);
try {
const params: { page: number; page_size: number; status?: string } = { page: p, page_size: 20 };
if (activeTab) params.status = activeTab;
const res = await listDialysisRecords(currentPatientId, params);
setRecords(res.data || []);
setTotal(res.total || 0);
setPage(p);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, [currentPatientId, activeTab]);
usePageData(
useCallback(() => loadRecords(1), [loadRecords]),
{ enabled: !!currentPatientId },
);
// tab/patientId 变化时重新加载(跳过首次 mount由 usePageData 的 useDidShow 处理)
useEffect(() => {
if (mountedRef.current && currentPatientId) {
loadRecords(1);
}
mountedRef.current = true;
}, [currentPatientId, activeTab, loadRecords]);
const handleSearch = async () => {
if (!searchPatient.trim()) return;
setLoading(true);
try {
const res = await listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
if (res.data && res.data.length > 0) {
setCurrentPatientId(res.data[0].id);
Taro.setNavigationBarTitle({ title: res.data[0].name + '的透析记录' });
} else {
Taro.showToast({ title: '未找到患者', icon: 'none' });
}
} catch {
Taro.showToast({ title: '搜索失败', icon: 'none' });
} finally {
setLoading(false);
}
};
const handleTab = (key: string) => {
setActiveTab(key);
setPage(1);
};
// 服务端已按 activeTab 过滤,无需客户端二次筛选
if (loading && records.length === 0) return <Loading />;
return (
<ScrollView scrollY className={`dialysis-page ${modeClass}`}>
{!patientId && (
<View className='search-bar'>
<Input
className='search-input'
placeholder='搜索患者姓名'
value={searchPatient}
onInput={(e) => setSearchPatient(e.detail.value)}
confirmType='search'
onConfirm={handleSearch}
/>
</View>
)}
<View className='tabs'>
{TABS.map((t) => (
<View
key={t.key}
className={`tab ${activeTab === t.key ? 'tab--active' : ''}`}
onClick={() => handleTab(t.key)}
>
<Text className='tab-text'>{t.label}</Text>
</View>
))}
</View>
{!currentPatientId ? (
<EmptyState text='请搜索并选择患者' />
) : records.length === 0 ? (
<EmptyState text='暂无透析记录' />
) : (
<View className='record-list'>
<View className='record-count'><Text> {total} </Text></View>
{records.map((r) => (
<View
key={r.id}
className='record-card'
onClick={() => safeNavigateTo(`/pages/pkg-doctor-clinical/dialysis/detail/index?id=${r.id}`)}
>
<View className='record-card__header'>
<Text className={`type-tag type-tag--${(r.dialysis_type || 'hd').toLowerCase()}`}>
{TYPE_MAP[r.dialysis_type] || r.dialysis_type}
</Text>
<Text className={`status-tag status-tag--${r.status}`}>
{r.status === 'draft' ? '草稿' : r.status === 'completed' ? '已完成' : '已审核'}
</Text>
</View>
<View className='record-card__body'>
<Text className='record-card__date'>{r.dialysis_date}</Text>
{r.dialysis_duration != null && (
<Text className='record-card__meta'> {r.dialysis_duration}</Text>
)}
{r.ultrafiltration_volume != null && (
<Text className='record-card__meta'> {r.ultrafiltration_volume}ml</Text>
)}
</View>
</View>
))}
{total > 20 && (
<View className='pagination'>
<View
className={`page-btn ${page <= 1 ? 'page-btn--disabled' : ''}`}
onClick={() => page > 1 && loadRecords(page - 1)}
>
<Text></Text>
</View>
<Text className='page-info'>{page} / {Math.ceil(total / 20)}</Text>
<View
className={`page-btn ${page * 20 >= total ? 'page-btn--disabled' : ''}`}
onClick={() => page * 20 < total && loadRecords(page + 1)}
>
<Text></Text>
</View>
</View>
)}
</View>
)}
<View
className='fab'
onClick={() => {
if (!currentPatientId) {
Taro.showToast({ title: '请先选择患者', icon: 'none' });
return;
}
safeNavigateTo(`/pages/pkg-doctor-clinical/dialysis/create/index?patientId=${currentPatientId}`);
}}
>
<Text className='fab-text'>+</Text>
</View>
</ScrollView>
);
}