feature: Clinical Review Mode: Charts, Replay Controls & Alert Reasoning

This commit is contained in:
voltsrage
2026-06-20 14:17:58 +08:00
parent 2ca0078223
commit ebd53f2df6
19 changed files with 939 additions and 9 deletions
@@ -0,0 +1,50 @@
<script setup>
import { computed, shallowRef, markRaw } from 'vue'
import { Line } from 'vue-chartjs'
import { Chart, registerables } from 'chart.js'
import { formatTime } from '@/composables/chartFormat'
Chart.register(...registerables)
const props = defineProps({ history: { type: Array, required: true } })
const chartData = computed(() => ({
labels: props.history.map(h => formatTime(h.calculatedAt)),
datasets: [{
label: 'NEWS2',
data: props.history.map(h => h.totalScore),
borderColor: '#3b82f6',
backgroundColor: props.history.map(h => {
if (h.totalScore >= 7) return 'rgba(220, 38, 38, 0.3)'
if (h.totalScore >= 5) return 'rgba(245, 158, 11, 0.3)'
return 'rgba(34, 197, 94, 0.3)'
}),
fill: true,
tension: 0.3,
pointRadius: 4,
}],
}))
const chartOptions = shallowRef(markRaw({
responsive: true,
maintainAspectRatio: true,
animation: {
duration: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 400,
},
scales: {
y: { min: 0, max: 20, title: { display: true, text: 'NEWS2 Score' } },
},
plugins: {
legend: { display: false },
},
}))
</script>
<template>
<div class="w-full min-w-0 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
<h3 class="mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">NEWS2 Score Over Time</h3>
<div class="aspect-video w-full min-h-0 overflow-hidden">
<Line :data="chartData" :options="chartOptions" />
</div>
</div>
</template>
@@ -0,0 +1,27 @@
<script setup>
import VitalTrendChart from './VitalTrendChart.vue'
defineProps({ observations: { type: Array, required: true } })
const charts = [
{ code: 'HEART_RATE', title: 'Heart Rate (bpm)', yMin: 30, yMax: 180 },
{ code: 'RESP_RATE', title: 'Respiratory Rate (/min)', yMin: 0, yMax: 40 },
{ code: 'SYSTOLIC_BP', title: 'Systolic BP (mmHg)', yMin: 60, yMax: 250 },
{ code: 'SPO2', title: 'SpO₂ (%)', yMin: 80, yMax: 100 },
{ code: 'TEMP_C', title: 'Temperature (°C)', yMin: 34, yMax: 42 },
]
</script>
<template>
<div class="grid w-full min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
<VitalTrendChart
v-for="chart in charts"
:key="chart.code"
:observations="observations"
:code="chart.code"
:title="chart.title"
:y-min="chart.yMin"
:y-max="chart.yMax"
/>
</div>
</template>
@@ -0,0 +1,43 @@
<script setup>
import { computed, toValue, shallowRef, markRaw } from 'vue'
import { Line } from 'vue-chartjs'
import { Chart, registerables } from 'chart.js'
Chart.register(...registerables)
const props = defineProps({
chartData: { type: Object, required: true },
title: { type: String, required: true },
yMin: { type: Number, default: undefined },
yMax: { type: Number, default: undefined },
})
const lineData = computed(() => {
const data = toValue(props.chartData)
return data?.datasets ? data : { labels: [], datasets: [] }
})
const chartOptions = shallowRef(markRaw({
responsive: true,
maintainAspectRatio: true,
animation: {
duration: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 400,
},
scales: {
y: { min: props.yMin, max: props.yMax },
x: { ticks: { maxTicksAuto: true, maxRotation: 45 } },
},
plugins: {
legend: { display: false },
},
}))
</script>
<template>
<div class="w-full min-w-0 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
<h3 class="mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">{{ title }}</h3>
<div class="aspect-video w-full min-h-0 overflow-hidden">
<Line :data="lineData" :options="chartOptions" />
</div>
</div>
</template>
@@ -0,0 +1,23 @@
<script setup>
import { useChartData } from '@/composables/useChartData'
import VitalChart from './VitalChart.vue'
const props = defineProps({
observations: { type: Array, required: true },
code: { type: String, required: true },
title: { type: String, required: true },
yMin: { type: Number, default: undefined },
yMax: { type: Number, default: undefined },
})
const { chartData } = useChartData(() => props.observations, props.code)
</script>
<template>
<VitalChart
:chart-data="chartData"
:title="title"
:y-min="yMin"
:y-max="yMax"
/>
</template>