Files
HealthCarePregnant/pcm-platform/admin-web/src/pages/WorklistPage.tsx
T
2026-06-18 09:48:05 +08:00

171 lines
6.3 KiB
TypeScript

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;
}