feat(web): 透析 API + 积分账户组件 + 工作台 store + 统计页修复

- dialysis.ts: 新增透析管理 API 模块
- PointsAccountTab.tsx: 积分账户标签页组件
- workbenchStore.ts: 工作台状态管理
- StatisticsDashboard.tsx: 统计页空列表修复
- auth.test.ts: 修复权限码拼写 health.alert → health.alerts
- api.test.ts: API 契约测试
This commit is contained in:
iven
2026-05-03 19:32:00 +08:00
parent 70322e4132
commit 3e4baa38a6
6 changed files with 485 additions and 5 deletions

View File

@@ -4,12 +4,13 @@ import { NurseDashboard } from './StatisticsDashboard/NurseDashboard';
import { AdminDashboard } from './StatisticsDashboard/AdminDashboard';
import { OperatorDashboard } from './StatisticsDashboard/OperatorDashboard';
const DASHBOARD_MAP = {
const DASHBOARD_MAP: Record<string, React.FC> = {
doctor: DoctorDashboard,
health_manager: NurseDashboard,
nurse: NurseDashboard,
admin: AdminDashboard,
operator: OperatorDashboard,
} as const;
};
export default function StatisticsDashboard() {
const role = useDashboardRole();

View File

@@ -0,0 +1,141 @@
import { useCallback, useMemo, useState } from 'react';
import { Table, Tag, Statistic, Row, Col, Card, Empty } from 'antd';
import {
pointsAdminApi,
type PointsAccountDetail,
type PointsTransactionDetail,
} from '../../../api/health/points';
import { usePaginatedData } from '../../../hooks/usePaginatedData';
import { handleApiError } from '../../../api/client';
interface Props {
patientId: string;
}
const TYPE_MAP: Record<string, { color: string; label: string }> = {
earn: { color: 'green', label: '获得' },
spend: { color: 'orange', label: '消费' },
expire: { color: 'default', label: '过期' },
adjust: { color: 'blue', label: '调整' },
checkin: { color: 'cyan', label: '签到' },
};
export function PointsAccountTab({ patientId }: Props) {
const [account, setAccount] = useState<PointsAccountDetail | null>(null);
const [accountLoading, setAccountLoading] = useState(true);
const fetchTransactions = useCallback(
async (page: number, pageSize: number) => {
if (!account) {
try {
const acc = await pointsAdminApi.getPatientAccount(patientId);
setAccount(acc);
} catch (err) {
handleApiError(err, '加载积分账户失败');
} finally {
setAccountLoading(false);
}
}
return pointsAdminApi.listPatientTransactions(patientId, { page, page_size: pageSize });
},
[patientId, account],
);
const { data, total, page, loading, refresh } = usePaginatedData<PointsTransactionDetail>(
fetchTransactions,
10,
);
const columns = useMemo(
() => [
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 170,
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
},
{
title: '类型',
dataIndex: 'transaction_type',
key: 'transaction_type',
width: 100,
render: (v: string) => {
const m = TYPE_MAP[v];
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
},
},
{
title: '变动',
dataIndex: 'amount',
key: 'amount',
width: 100,
render: (v: number) => (
<span style={{ color: v > 0 ? '#52c41a' : v < 0 ? '#ff4d4f' : undefined }}>
{v > 0 ? `+${v}` : v}
</span>
),
},
{
title: '余额',
dataIndex: 'balance_after',
key: 'balance_after',
width: 80,
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
],
[],
);
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="当前余额" value={account?.balance ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计获得" value={account?.total_earned ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计消费" value={account?.total_spent ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计过期" value={account?.total_expired ?? 0} suffix="分" />
</Card>
</Col>
</Row>
{!account && !accountLoading ? (
<Empty description="暂无积分记录" />
) : (
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: page,
total,
pageSize: 10,
onChange: (p) => refresh(p),
showTotal: (t) => `${t}`,
style: { margin: 0 },
}}
/>
)}
</div>
);
}