feat: remove cost/费用 statistics from clinical analytics
Per request — drop all monetary statistics (住院费用 is sensitive). - Backend statistics.py: remove mean_cost KPI + cost_histogram / cost_by_disease / cost_vs_los from /inpatient-clinical (models, computation, response) - Frontend: drop 人均费用 KPI card (now 4 KPIs), 住院费用分布, 各病种平均费用, 费用×住院天数散点; delete CostByDiseaseChart + CostVsLosScatter components; trim statsApi type + e2e fixture + chartColors Clinical page now: KPI(总人次/中位住院日/治愈好转率/急诊占比) + LOS dist + LOS-by-disease box + outcome donut + admission-route donut + age-band BMI box. Gates: backend 106 pytest · tsc 0 · build ok · clinical+user-flows e2e 19/19 · live endpoint confirmed cost-free Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { Users, CalendarDays, Wallet, HeartPulse, Siren } from 'lucide-react';
|
||||
import { Users, CalendarDays, HeartPulse, Siren } from 'lucide-react';
|
||||
import { StatCard } from '@/components/StatCard';
|
||||
import { TESTIDS } from '@/utils/testids';
|
||||
import type { InpatientClinicalResponse } from '@/services/api';
|
||||
@@ -8,12 +8,12 @@ interface ClinicalKpiRowProps {
|
||||
kpis: InpatientClinicalResponse['kpis'];
|
||||
}
|
||||
|
||||
/** 住院临床 5 项核心指标。375px 下 2 列,sm 起 5 列。 */
|
||||
/** 住院临床 4 项核心指标。375px 下 2 列,sm 起 4 列。 */
|
||||
export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpiRowProps) {
|
||||
return (
|
||||
<div
|
||||
data-testid={TESTIDS.clinicalKpis}
|
||||
className="grid grid-cols-2 sm:grid-cols-5 gap-3"
|
||||
className="grid grid-cols-2 sm:grid-cols-4 gap-3"
|
||||
>
|
||||
<StatCard
|
||||
icon={<Users className="w-4 h-4 text-primary" />}
|
||||
@@ -25,11 +25,6 @@ export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpi
|
||||
label="中位住院日"
|
||||
value={`${kpis.median_los_days} 天`}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Wallet className="w-4 h-4 text-primary" />}
|
||||
label="人均费用"
|
||||
value={`¥${Math.round(kpis.mean_cost).toLocaleString()}`}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<HeartPulse className="w-4 h-4 text-success" />}
|
||||
label="治愈好转率"
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
|
||||
|
||||
interface CostByDiseaseChartProps {
|
||||
data: { diagnosis: string; mean_cost: number; n: number }[];
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s.length > max ? s.slice(0, max) + '…' : s;
|
||||
}
|
||||
|
||||
/** 各病种平均费用横向柱状图。 */
|
||||
export const CostByDiseaseChart = memo(function CostByDiseaseChart({
|
||||
data,
|
||||
}: CostByDiseaseChartProps) {
|
||||
if (!data || data.length === 0) {
|
||||
return <div className="text-center py-8 text-text-muted text-sm">暂无数据</div>;
|
||||
}
|
||||
|
||||
const chartData = [...data]
|
||||
.sort((a, b) => a.mean_cost - b.mean_cost)
|
||||
.map((d) => ({ ...d, displayName: truncate(d.diagnosis, 8) }));
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={Math.max(240, chartData.length * 34)}>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 20, left: 12, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CLINICAL_COLORS.grid} horizontal={false} />
|
||||
<XAxis
|
||||
type="number"
|
||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
||||
tickFormatter={(v: number) => `¥${(v / 1000).toFixed(0)}k`}
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="displayName"
|
||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axisLabel }}
|
||||
width={72}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
formatter={(v: number) => [`¥${Math.round(v).toLocaleString()}`, '人均费用']}
|
||||
/>
|
||||
<Bar dataKey="mean_cost" fill={CLINICAL_COLORS.cost} barSize={16} radius={[0, 3, 3, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
ScatterChart,
|
||||
Scatter,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
|
||||
|
||||
interface CostVsLosScatterProps {
|
||||
data: { los: number; cost: number }[];
|
||||
}
|
||||
|
||||
/** 费用 vs 住院天数散点。 */
|
||||
export const CostVsLosScatter = memo(function CostVsLosScatter({ data }: CostVsLosScatterProps) {
|
||||
if (!data || data.length === 0) {
|
||||
return <div className="text-center py-8 text-text-muted text-sm">暂无数据</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ScatterChart margin={{ top: 10, right: 16, left: 6, bottom: 16 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CLINICAL_COLORS.grid} />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="los"
|
||||
name="住院天数"
|
||||
unit="天"
|
||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="cost"
|
||||
name="费用"
|
||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
||||
width={52}
|
||||
tickFormatter={(v: number) => `¥${(v / 1000).toFixed(0)}k`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
cursor={{ strokeDasharray: '3 3' }}
|
||||
formatter={(value: number, name: string) =>
|
||||
name === '费用'
|
||||
? [`¥${value.toLocaleString()}`, name]
|
||||
: [`${value} 天`, name]
|
||||
}
|
||||
/>
|
||||
<Scatter data={data} fill={CLINICAL_COLORS.scatter} fillOpacity={0.5} />
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
@@ -13,11 +13,11 @@ import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
|
||||
interface HistogramChartProps {
|
||||
data: { bin_label: string; count: number }[];
|
||||
color?: string;
|
||||
/** tooltip 中数量的标签,如 "住院天数" / "费用区间"。 */
|
||||
/** tooltip 中数量的标签,如 "住院天数"。 */
|
||||
countLabel?: string;
|
||||
}
|
||||
|
||||
/** 通用直方图。复用于「住院天数分布」与「住院费用分布」。 */
|
||||
/** 通用直方图。用于「住院天数分布」。 */
|
||||
export const HistogramChart = memo(function HistogramChart({
|
||||
data,
|
||||
color = CLINICAL_COLORS.los,
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
export const CLINICAL_COLORS = {
|
||||
primary: '#2563EB', // primary
|
||||
los: '#2563EB',
|
||||
cost: '#0891B2', // cyan — 费用维度
|
||||
scatter: '#7C3AED', // violet — 散点
|
||||
box: '#3B82F6', // 箱体填充
|
||||
boxMedian: '#1D4ED8', // 中位刻度
|
||||
grid: '#E2E8F0',
|
||||
|
||||
Reference in New Issue
Block a user