Initial commit: HealthCarePregnant project documentation and platform
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { api, ApiError } from '../api/client';
|
||||
import { useToast } from '../components/Toast';
|
||||
import { useAutoRefresh } from '../lib/useAutoRefresh';
|
||||
import type { PatientSummary, RiskLevel } from '../api/types';
|
||||
import { riskBadgeClass, riskLabel } from '../lib/format';
|
||||
|
||||
const RISK_ORDER: Record<RiskLevel, number> = { high: 0, medium: 1, low: 2 };
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export function WorklistPage(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { show } = useToast();
|
||||
const [patients, setPatients] = useState<PatientSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [query, setQuery] = useState('');
|
||||
const [riskFilter, setRiskFilter] = useState<'all' | RiskLevel>('all');
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const load = useCallback(
|
||||
(silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
api
|
||||
.listPatients()
|
||||
.then(setPatients)
|
||||
.catch((err) => {
|
||||
if (!silent) show(err instanceof ApiError ? err.message : '加载孕妇列表失败');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
// 多端一致:新建档/风险变化近实时反映到工作列表
|
||||
useAutoRefresh(() => load(true));
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const q = query.trim();
|
||||
return patients
|
||||
.filter((p) => (riskFilter === 'all' ? true : p.initialRiskLevel === riskFilter))
|
||||
.filter((p) => (q ? p.name.includes(q) || (p.patientNo ?? '').toUpperCase().includes(q.toUpperCase()) : true))
|
||||
.sort((a, b) => RISK_ORDER[a.initialRiskLevel] - RISK_ORDER[b.initialRiskLevel]);
|
||||
}, [patients, query, riskFilter]);
|
||||
|
||||
// 过滤条件变化时回到第一页
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [query, riskFilter]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const pageRows = rows.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
return {
|
||||
total: patients.length,
|
||||
high: patients.filter((p) => p.initialRiskLevel === 'high').length,
|
||||
medium: patients.filter((p) => p.initialRiskLevel === 'medium').length,
|
||||
};
|
||||
}, [patients]);
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<div className="spread">
|
||||
<div>
|
||||
<h1 style={{ fontSize: 'var(--font-xxl)' }}>工作列表</h1>
|
||||
<p className="muted">在管孕妇 {stats.total} 人 · 高风险 {stats.high} · 中风险 {stats.medium}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="row" style={{ marginBottom: 'var(--space-4)' }}>
|
||||
<input
|
||||
placeholder="搜索姓名 / 编号…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
style={{ maxWidth: 260, padding: '8px 12px', borderRadius: 'var(--radius-md)', border: '1px solid var(--color-border-strong)' }}
|
||||
/>
|
||||
<select
|
||||
value={riskFilter}
|
||||
onChange={(e) => setRiskFilter(e.target.value as 'all' | RiskLevel)}
|
||||
style={{ padding: '8px 12px', borderRadius: 'var(--radius-md)', border: '1px solid var(--color-border-strong)' }}
|
||||
>
|
||||
<option value="all">全部风险</option>
|
||||
<option value="high">高风险</option>
|
||||
<option value="medium">中风险</option>
|
||||
<option value="low">低风险</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="empty">加载中…</p>
|
||||
) : rows.length === 0 ? (
|
||||
<p className="empty">暂无匹配的孕妇。建档后将出现在此列表。</p>
|
||||
) : (
|
||||
<>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>编号</th>
|
||||
<th>姓名</th>
|
||||
<th>年龄</th>
|
||||
<th>孕周</th>
|
||||
<th>分期</th>
|
||||
<th>初始风险</th>
|
||||
<th>风险因素</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pageRows.map((p) => (
|
||||
<tr key={p.id} className="clickable" onClick={() => navigate(`/patients/${p.id}`)}>
|
||||
<td style={{ fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 'var(--font-sm)' }}>
|
||||
{p.patientNo || '—'}
|
||||
</td>
|
||||
<td style={{ fontWeight: 600 }}>{p.name}</td>
|
||||
<td>{p.age}</td>
|
||||
<td>
|
||||
{p.gestationalWeeks}周{p.gestationalDays}天
|
||||
</td>
|
||||
<td>{trimesterLabel(p.trimester)}</td>
|
||||
<td>
|
||||
<span className={`badge ${riskBadgeClass(p.initialRiskLevel)}`}>
|
||||
{riskLabel(p.initialRiskLevel)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="muted">{p.initialRiskFactors.join('、') || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="pager">
|
||||
<span className="muted">
|
||||
共 {rows.length} 条 · 第 {currentPage}/{totalPages} 页
|
||||
</span>
|
||||
<div className="pager__btns">
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage <= 1}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft size={16} strokeWidth={1.9} /> 上一页
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage >= totalPages}
|
||||
type="button"
|
||||
>
|
||||
下一页 <ChevronRight size={16} strokeWidth={1.9} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function trimesterLabel(t: string): string {
|
||||
return { first: '孕早期', second: '孕中期', third: '孕晚期' }[t] ?? t;
|
||||
}
|
||||
Reference in New Issue
Block a user