feat(miniprogram): 实现小程序透析模块 — 患者端查看 + 医护端录入/审阅
审计后续 H1: 补齐小程序端透析功能,对接后端 12 个 API 路由。 新增内容: - 患者端: 透析记录列表/详情 + 透析处方列表/详情(只读,4 页面) - 医护端: 透析记录列表/详情/创建 + 处方列表/详情/创建(6 页面) - Service 层: dialysis.ts(患者端只读)+ doctor/dialysis.ts(医护端 CRUD) - 集成入口: 医生工作台快捷操作 + 患者"我的"菜单 + 路由注册 - 基础设施: api.delete 扩展支持 data 参数(后端 delete 需要 version)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
@mixin serif-number {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background: $bd-l;
|
||||
color: $tx3;
|
||||
|
||||
&.active {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
&.expired {
|
||||
background: $tx3;
|
||||
color: $bg;
|
||||
}
|
||||
}
|
||||
|
||||
.header-sub {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.notes-text {
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120px 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28px;
|
||||
color: $tx3;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { getDialysisPrescription } from '@/services/dialysis';
|
||||
import type { DialysisPrescription } from '@/services/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
active: { label: '生效中', cls: 'active' },
|
||||
inactive: { label: '已停用', cls: 'inactive' },
|
||||
expired: { label: '已过期', cls: 'expired' },
|
||||
};
|
||||
|
||||
export default function DialysisPrescriptionDetail() {
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const [rx, setRx] = useState<DialysisPrescription | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
getDialysisPrescription(id)
|
||||
.then((data) => setRx(data))
|
||||
.catch(() => Taro.showToast({ title: '加载失败', icon: 'none' }))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <View className='detail-page'><Loading /></View>;
|
||||
if (!rx) return <View className='detail-page'><View className='empty-state'><Text className='empty-text'>处方不存在</Text></View></View>;
|
||||
|
||||
const si = STATUS_MAP[rx.status] || { label: rx.status, cls: '' };
|
||||
|
||||
const Row = ({ label, value }: { label: string; value?: string | number | null }) => {
|
||||
if (value == null) return null;
|
||||
return (
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>{label}</Text>
|
||||
<Text className='detail-value'>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='detail-page'>
|
||||
{/* 状态头部 */}
|
||||
<View className='detail-card header-card'>
|
||||
<View className='header-row'>
|
||||
<Text className='detail-title'>{rx.dialyzer_model || '透析处方'}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
{(rx.effective_from || rx.effective_to) && (
|
||||
<Text className='header-sub'>{rx.effective_from || '...'} ~ {rx.effective_to || '...'}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 基本参数 */}
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>基本参数</Text>
|
||||
<Row label='透析器型号' value={rx.dialyzer_model} />
|
||||
<Row label='膜面积' value={rx.membrane_area != null ? `${rx.membrane_area} m²` : null} />
|
||||
<Row label='血流速' value={rx.blood_flow_rate != null ? `${rx.blood_flow_rate} ml/min` : null} />
|
||||
<Row label='透析液流量' value={rx.dialysate_flow_rate != null ? `${rx.dialysate_flow_rate} ml/min` : null} />
|
||||
<Row label='频率' value={rx.frequency_per_week != null ? `${rx.frequency_per_week} 次/周` : null} />
|
||||
<Row label='每次时长' value={rx.duration_minutes != null ? `${rx.duration_minutes} 分钟` : null} />
|
||||
</View>
|
||||
|
||||
{/* 透析液配比 */}
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>透析液配比</Text>
|
||||
<Row label='钾浓度' value={rx.dialysate_potassium != null ? `${rx.dialysate_potassium} mmol/L` : null} />
|
||||
<Row label='钙浓度' value={rx.dialysate_calcium != null ? `${rx.dialysate_calcium} mmol/L` : null} />
|
||||
<Row label='碳酸氢盐浓度' value={rx.dialysate_bicarbonate != null ? `${rx.dialysate_bicarbonate} mmol/L` : null} />
|
||||
</View>
|
||||
|
||||
{/* 抗凝方案 */}
|
||||
<View className='detail-card'>
|
||||
<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='detail-card'>
|
||||
<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='detail-card'>
|
||||
<Text className='section-title'>超滤目标</Text>
|
||||
<Row label='目标超滤量' value={rx.target_ultrafiltration_ml != null ? `${rx.target_ultrafiltration_ml} ml` : null} />
|
||||
<Row label='目标干体重' value={rx.target_dry_weight != null ? `${rx.target_dry_weight} kg` : null} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
{rx.notes && (
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>备注</Text>
|
||||
<Text className='notes-text'>{rx.notes}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
@mixin section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@mixin tag($bg, $color) {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background: $bg;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
@mixin serif-number {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dialysis-prescriptions-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 32px 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@include section-title;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.prescription-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.prescription-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:active {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
}
|
||||
|
||||
.prescription-card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.prescription-model {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
@include tag($bd-l, $tx3);
|
||||
|
||||
&.active {
|
||||
@include tag($acc-l, $acc);
|
||||
}
|
||||
|
||||
&.expired {
|
||||
@include tag($tx3, $bg);
|
||||
}
|
||||
}
|
||||
|
||||
.prescription-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.prescription-date {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
@include serif-number;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { listDialysisPrescriptions } from '@/services/dialysis';
|
||||
import type { DialysisPrescription } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
active: { label: '生效中', cls: 'active' },
|
||||
inactive: { label: '已停用', cls: 'inactive' },
|
||||
expired: { label: '已过期', cls: 'expired' },
|
||||
};
|
||||
|
||||
export default function DialysisPrescriptionList() {
|
||||
const [prescriptions, setPrescriptions] = useState<DialysisPrescription[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setPrescriptions([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisPrescriptions({ patient_id: patientId, page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setPrescriptions(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => { fetchData(1); });
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => Taro.stopPullDownRefresh());
|
||||
});
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && prescriptions.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
const statusInfo = (s: string) => STATUS_MAP[s] || { label: s, cls: '' };
|
||||
|
||||
return (
|
||||
<View className='dialysis-prescriptions-page'>
|
||||
<Text className='page-title'>透析处方</Text>
|
||||
|
||||
<View className='prescription-list'>
|
||||
{prescriptions.map((p) => {
|
||||
const si = statusInfo(p.status);
|
||||
return (
|
||||
<View
|
||||
className='prescription-card'
|
||||
key={p.id}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/pkg-profile/dialysis-prescriptions/detail/index?id=${p.id}` })}
|
||||
>
|
||||
<View className='prescription-card-top'>
|
||||
<Text className='prescription-model'>{p.dialyzer_model || '未指定型号'}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<View className='prescription-meta'>
|
||||
{p.frequency_per_week != null && (
|
||||
<Text className='meta-item'>{p.frequency_per_week}次/周</Text>
|
||||
)}
|
||||
{p.duration_minutes != null && (
|
||||
<Text className='meta-item'>每次{p.duration_minutes}分钟</Text>
|
||||
)}
|
||||
</View>
|
||||
{(p.effective_from || p.effective_to) && (
|
||||
<Text className='prescription-date'>
|
||||
{p.effective_from || '...'} ~ {p.effective_to || '...'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{prescriptions.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无透析处方' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
@mixin serif-number {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background: $bd-l;
|
||||
color: $tx3;
|
||||
|
||||
&.completed {
|
||||
background: $pri-l;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
&.reviewed {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
}
|
||||
}
|
||||
|
||||
.header-sub {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.review-info {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120px 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28px;
|
||||
color: $tx3;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { getDialysisRecord } from '@/services/dialysis';
|
||||
import type { DialysisRecord } from '@/services/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: '草稿', cls: 'draft' },
|
||||
completed: { label: '已完成', cls: 'completed' },
|
||||
reviewed: { label: '已审核', cls: 'reviewed' },
|
||||
};
|
||||
|
||||
const TYPE_MAP: Record<string, string> = {
|
||||
HD: '血液透析',
|
||||
HDF: '血液透析滤过',
|
||||
HF: '血液滤过',
|
||||
};
|
||||
|
||||
export default function DialysisRecordDetail() {
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const [record, setRecord] = useState<DialysisRecord | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
getDialysisRecord(id)
|
||||
.then((data) => setRecord(data))
|
||||
.catch(() => Taro.showToast({ title: '加载失败', icon: 'none' }))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <View className='detail-page'><Loading /></View>;
|
||||
if (!record) return <View className='detail-page'><View className='empty-state'><Text className='empty-text'>记录不存在</Text></View></View>;
|
||||
|
||||
const si = STATUS_MAP[record.status] || { label: record.status, cls: '' };
|
||||
|
||||
return (
|
||||
<View className='detail-page'>
|
||||
{/* 状态头部 */}
|
||||
<View className='detail-card header-card'>
|
||||
<View className='header-row'>
|
||||
<Text className='detail-title'>{record.dialysis_date}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<Text className='header-sub'>{TYPE_MAP[record.dialysis_type] || record.dialysis_type}</Text>
|
||||
{record.reviewed_at && <Text className='review-info'>审核时间 {record.reviewed_at}</Text>}
|
||||
</View>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>基本信息</Text>
|
||||
{record.start_time && (
|
||||
<View className='detail-row'><Text className='detail-label'>开始时间</Text><Text className='detail-value'>{record.start_time}</Text></View>
|
||||
)}
|
||||
{record.end_time && (
|
||||
<View className='detail-row'><Text className='detail-label'>结束时间</Text><Text className='detail-value'>{record.end_time}</Text></View>
|
||||
)}
|
||||
{record.dialysis_duration != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透析时长</Text><Text className='detail-value'>{record.dialysis_duration} 分钟</Text></View>
|
||||
)}
|
||||
{record.blood_flow_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>血流速</Text><Text className='detail-value'>{record.blood_flow_rate} ml/min</Text></View>
|
||||
)}
|
||||
{record.ultrafiltration_volume != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>超滤量</Text><Text className='detail-value'>{record.ultrafiltration_volume} ml</Text></View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 体重与血压 */}
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>体重与血压</Text>
|
||||
{record.dry_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>干体重</Text><Text className='detail-value'>{record.dry_weight} kg</Text></View>
|
||||
)}
|
||||
{record.pre_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前体重</Text><Text className='detail-value'>{record.pre_weight} kg</Text></View>
|
||||
)}
|
||||
{record.post_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后体重</Text><Text className='detail-value'>{record.post_weight} kg</Text></View>
|
||||
)}
|
||||
{record.pre_bp_systolic != null && record.pre_bp_diastolic != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前血压</Text><Text className='detail-value'>{record.pre_bp_systolic}/{record.pre_bp_diastolic} mmHg</Text></View>
|
||||
)}
|
||||
{record.post_bp_systolic != null && record.post_bp_diastolic != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后血压</Text><Text className='detail-value'>{record.post_bp_systolic}/{record.post_bp_diastolic} mmHg</Text></View>
|
||||
)}
|
||||
{record.pre_heart_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前心率</Text><Text className='detail-value'>{record.pre_heart_rate} bpm</Text></View>
|
||||
)}
|
||||
{record.post_heart_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后心率</Text><Text className='detail-value'>{record.post_heart_rate} bpm</Text></View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 症状与并发症 */}
|
||||
{(record.symptoms || record.complication_notes) && (
|
||||
<View className='detail-card'>
|
||||
<Text className='section-title'>症状与并发症</Text>
|
||||
{record.symptoms && Object.keys(record.symptoms).length > 0 && (
|
||||
<View className='detail-row'><Text className='detail-label'>症状</Text><Text className='detail-value'>{JSON.stringify(record.symptoms)}</Text></View>
|
||||
)}
|
||||
{record.complication_notes && (
|
||||
<View className='detail-row'><Text className='detail-label'>并发症备注</Text><Text className='detail-value'>{record.complication_notes}</Text></View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
@mixin section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@mixin tag($bg, $color) {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background: $bg;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
@mixin serif-number {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dialysis-records-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 32px 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@include section-title;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:active {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
}
|
||||
|
||||
.record-card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
@include tag($pri-l, $pri-d);
|
||||
|
||||
&.hdf {
|
||||
@include tag($acc-l, $acc);
|
||||
}
|
||||
|
||||
&.hf {
|
||||
@include tag($wrn-l, $wrn);
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
@include tag($bd-l, $tx3);
|
||||
|
||||
&.completed {
|
||||
@include tag($pri-l, $pri);
|
||||
}
|
||||
|
||||
&.reviewed {
|
||||
@include tag($acc-l, $acc);
|
||||
}
|
||||
}
|
||||
|
||||
.record-date {
|
||||
@include serif-number;
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.weight-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.weight-item {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
@include serif-number;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { listDialysisRecords } from '@/services/dialysis';
|
||||
import type { DialysisRecord } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const TYPE_MAP: Record<string, { label: string; cls: string }> = {
|
||||
HD: { label: 'HD', cls: 'hd' },
|
||||
HDF: { label: 'HDF', cls: 'hdf' },
|
||||
HF: { label: 'HF', cls: 'hf' },
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: '草稿', cls: 'draft' },
|
||||
completed: { label: '已完成', cls: 'completed' },
|
||||
reviewed: { label: '已审核', cls: 'reviewed' },
|
||||
};
|
||||
|
||||
export default function DialysisRecordList() {
|
||||
const [records, setRecords] = useState<DialysisRecord[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisRecords(patientId, { page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setRecords(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => { fetchData(1); });
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => Taro.stopPullDownRefresh());
|
||||
});
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && records.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
const typeInfo = (t: string) => TYPE_MAP[t] || { label: t, cls: '' };
|
||||
const statusInfo = (s: string) => STATUS_MAP[s] || { label: s, cls: '' };
|
||||
|
||||
return (
|
||||
<View className='dialysis-records-page'>
|
||||
<Text className='page-title'>透析记录</Text>
|
||||
|
||||
<View className='record-list'>
|
||||
{records.map((r) => {
|
||||
const ti = typeInfo(r.dialysis_type);
|
||||
const si = statusInfo(r.status);
|
||||
return (
|
||||
<View
|
||||
className='record-card'
|
||||
key={r.id}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/pkg-profile/dialysis-records/detail/index?id=${r.id}` })}
|
||||
>
|
||||
<View className='record-card-top'>
|
||||
<Text className={`type-tag ${ti.cls}`}>{ti.label}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<Text className='record-date'>{r.dialysis_date}</Text>
|
||||
{(r.pre_weight || r.post_weight) && (
|
||||
<View className='weight-row'>
|
||||
{r.pre_weight && <Text className='weight-item'>透前 {r.pre_weight}kg</Text>}
|
||||
{r.post_weight && <Text className='weight-item'>透后 {r.post_weight}kg</Text>}
|
||||
</View>
|
||||
)}
|
||||
{r.dialysis_duration && (
|
||||
<Text className='record-meta'>时长 {r.dialysis_duration}分钟</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无透析记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user