Files
intc-vue3/src/views/health/recordScreen/index.vue
2026-06-20 22:10:42 +08:00

2040 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="health-screen">
<div class="screen-bg screen-bg-a"></div>
<div class="screen-bg screen-bg-b"></div>
<div class="screen-grid-bg"></div>
<header class="cockpit-header">
<div class="title-block">
<div class="title-wing"></div>
<div>
<h1>健康档案态势大屏</h1>
<p>{{ recordInfo.personName || '全部成员' }} · {{ recordInfo.name || '全部健康档案' }}</p>
<div class="title-meta">
<span>当前时间 {{ nowParts.date }} {{ nowParts.time }}</span>
<span>健康档案全量态势监测</span>
</div>
</div>
<div class="title-wing title-wing-right"></div>
</div>
<div class="screen-actions">
<span class="filter-label">筛选</span>
<el-select
v-model="queryParams.id"
placeholder="选择人员"
clearable
filterable
class="filter-select person-select"
popper-class="health-screen-popper"
@change="handlePersonChange"
>
<el-option v-for="person in personList" :key="person.id" :label="person.name" :value="person.id" />
</el-select>
<el-select
v-model="queryParams.recordId"
placeholder="选择健康档案"
clearable
filterable
class="filter-select record-select"
popper-class="health-screen-popper"
@change="handleRecordChange"
>
<el-option v-for="record in healthRecordList" :key="record.id" :label="record.name" :value="record.id" />
</el-select>
<el-button type="primary" icon="Refresh" :loading="loading" class="screen-btn screen-btn-primary" @click="loadScreen">刷新</el-button>
<el-button icon="Download" :disabled="!queryParams.recordId" class="screen-btn screen-btn-plain" @click="handleFullExport">导出</el-button>
<el-button icon="RefreshLeft" class="screen-btn screen-btn-plain" @click="resetQuery">重置</el-button>
</div>
</header>
<main v-loading="loading" class="cockpit-main">
<section class="top-overview">
<div class="record-focus">
<div class="record-main">
<div>
<div class="record-kicker">档案画像</div>
<div class="record-name">{{ recordInfo.name || '请选择健康档案' }}</div>
</div>
<div class="record-meta">
<span v-for="item in recordMetaItems" :key="item.label">{{ item.label }}{{ item.value }}</span>
</div>
</div>
<div class="record-timeline">
<div>
<span>发生时间</span>
<strong>{{ formatDateTime(recordInfo.occurTime) }}</strong>
</div>
<div>
<span>康复时间</span>
<strong>{{ formatDateTime(recordInfo.rehabilitationTime) }}</strong>
</div>
</div>
</div>
<div class="metric-strip">
<div v-for="item in metricItems" :key="item.key" class="metric-card">
<span>{{ item.label }}</span>
<div class="metric-value">
<strong :class="item.className">{{ item.value }}</strong>
<em>{{ item.unit }}</em>
</div>
</div>
</div>
</section>
<section class="fact-strip-panel">
<div class="fact-strip-head">
<span>档案要点</span>
<em>症状 / 原因</em>
</div>
<div class="fact-strip">
<div v-for="item in recordFacts" :key="item.label" class="fact-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</section>
<section class="cockpit-grid">
<div class="screen-panel profile-panel grid-profile">
<div class="panel-head">
<span>健康风险雷达</span>
<em>{{ healthScoreText }}</em>
</div>
<div class="risk-score">
<div class="score-ring">
<strong>{{ healthScore }}</strong>
<span>健康指数</span>
</div>
<div class="risk-list">
<div v-for="item in riskItems" :key="item.label" class="risk-row" :class="item.level">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.desc }}</em>
</div>
</div>
</div>
</div>
<div class="screen-panel main-trend-panel grid-trend">
<div class="panel-head">
<span>体温趋势</span>
<em>{{ temperatureList.length }}条体温数据 · 预警线37.3</em>
</div>
<div ref="temperatureChartRef" class="chart chart-large"></div>
</div>
<div class="screen-panel medicine-panel grid-medicine">
<div class="panel-head">
<span>用药频次排行</span>
<em> {{ medicineRankList.length }} 种药品 / 图表Top5</em>
</div>
<div ref="medicineChartRef" class="chart chart-mid"></div>
<div class="rank-list">
<div v-for="(item, index) in medicineRankList" :key="`${item.name}-${index}`" class="rank-row" :style="{ '--rank-width': `${item.percent}%` }">
<span>{{ item.rankNo }}</span>
<strong>{{ item.name }}</strong>
<em>{{ item.count }}</em>
<i></i>
</div>
<el-empty v-if="!medicineRankList.length" description="暂无用药数据" :image-size="70" />
</div>
</div>
<div class="screen-panel grid-mix">
<div class="panel-head">
<span>数据结构</span>
<em>记录类型占比</em>
</div>
<div ref="recordMixChartRef" class="chart chart-mid"></div>
</div>
<div class="screen-panel timeline-panel grid-events">
<div class="panel-head">
<span>健康事件动态</span>
<em>最新 {{ eventItems.length }} </em>
</div>
<div class="event-stream">
<div v-for="item in eventItems" :key="`${item.type}-${item.time}-${item.title}`" class="event-row">
<span :class="['event-tag', item.type]">{{ item.label }}</span>
<div>
<strong>{{ item.title }}</strong>
<em>{{ item.time }}</em>
</div>
<b>{{ item.value }}</b>
</div>
<el-empty v-if="!eventItems.length" description="暂无健康事件" :image-size="70" />
</div>
</div>
<div class="screen-panel doctor-panel grid-doctor">
<div class="panel-head">
<span>就医费用趋势</span>
<em>{{ formatMoney(summary.doctorCost) }}</em>
</div>
<div ref="doctorChartRef" class="chart chart-mid"></div>
</div>
</section>
<section class="detail-grid">
<div class="screen-panel detail-panel">
<div class="panel-head detail-head">
<div>
<span>档案全量明细</span>
<strong>过程就医体温用药记录联动展示</strong>
</div>
<em>{{ detailCountText }}</em>
</div>
<el-tabs v-model="activeTab" class="screen-tabs">
<el-tab-pane label="过程记录" name="process">
<el-table :data="processRecordList" height="260">
<el-table-column label="记录时间" prop="recordingTime" width="180" />
<el-table-column label="记录内容" prop="content" show-overflow-tooltip />
<!-- <el-table-column label="备注" prop="remark" show-overflow-tooltip /> -->
</el-table>
</el-tab-pane>
<el-tab-pane label="就医记录" name="doctor">
<el-table :data="doctorRecordList" height="260">
<el-table-column label="医院" prop="hospitalName" width="270" />
<el-table-column label="科室" prop="departments" width="220" />
<el-table-column label="医生" prop="doctor" width="110" />
<el-table-column label="就诊时间" prop="visitingTime" width="170" />
<el-table-column label="费用" prop="totalCost" width="150" />
<el-table-column label="诊断结果" prop="diagnosis" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="体温记录" name="temperature">
<el-table :data="temperatureRecordList" height="260">
<el-table-column label="测量时间" prop="measureTime" width="180" />
<el-table-column label="体温" prop="temperature" width="100" />
<el-table-column label="人员" prop="personName" width="120" />
<el-table-column label="备注" prop="remark" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="用药记录" name="medicine">
<el-table :data="marRecordList" height="260">
<el-table-column label="用药时间" prop="dosingTime" width="180" />
<el-table-column label="药品" prop="name" min-width="170" show-overflow-tooltip />
<el-table-column label="剂量" prop="dosage" width="90" />
<el-table-column label="类型" prop="type" width="110">
<template #default="scope">
<dict-tag :options="mar_type" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-overflow-tooltip />
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</section>
</main>
</div>
</template>
<script setup name="HealthRecordScreen">
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import * as echarts from 'echarts'
import { listPerson } from '@/api/health/person'
import { listHealthRecord } from '@/api/health/healthRecord'
import { getRecordScreen } from '@/api/health/statisticAnalysis'
const { proxy } = getCurrentInstance()
const { mar_type, record_type, health_record_state, etiology } = proxy.useDict('mar_type', 'record_type', 'health_record_state', 'etiology')
const loading = ref(false)
const activeTab = ref('process')
const personList = ref([])
const healthRecordList = ref([])
const recordInfo = ref({})
const screenData = ref({})
const nowTime = ref('')
const nowParts = computed(() => {
const [date = '', time = ''] = nowTime.value.split(' ')
return { date, time }
})
const temperatureChartRef = ref(null)
const medicineChartRef = ref(null)
const doctorChartRef = ref(null)
const recordMixChartRef = ref(null)
let temperatureChart = null
let medicineChart = null
let doctorChart = null
let recordMixChart = null
let clockTimer = null
const queryParams = ref({
id: null,
recordId: null,
type: '1'
})
const summary = computed(() => screenData.value.summary || {})
const doctor = computed(() => screenData.value.doctor || {})
const temperature = computed(() => screenData.value.temperature || {})
const medicine = computed(() => screenData.value.medicine || {})
const processRecordList = computed(() => screenData.value.processRecordList || [])
const doctorRecordList = computed(() => screenData.value.doctorRecordList || [])
const temperatureRecordList = computed(() => screenData.value.temperatureRecordList || [])
const marRecordList = computed(() => screenData.value.marRecordList || [])
const temperatureList = computed(() => temperature.value.temperatureList || [])
const medicineItems = computed(() => medicine.value.marMapList || [])
const detailCountText = computed(() => {
const total = processRecordList.value.length + doctorRecordList.value.length + temperatureRecordList.value.length + marRecordList.value.length
return `${total} 条明细`
})
const recordMetaItems = computed(() => [
{ label: '类型', value: getDictLabel(record_type.value, recordInfo.value.type, recordInfo.value.typeName) },
{ label: '状态', value: getDictLabel(health_record_state.value, recordInfo.value.state, recordInfo.value.stateName) },
{ label: '周期', value: recordInfo.value.duration || '暂无' }
])
const metricItems = computed(() => [
{ key: 'process', label: '过程记录', value: formatNumber(summary.value.processRecordCount), unit: '条' },
{ key: 'doctor', label: '就医次数', value: formatNumber(summary.value.doctorCount), unit: '次' },
{ key: 'doctorCost', label: '就医费用', value: formatShortMoney(summary.value.doctorCost), unit: '元', className: 'value-gold' },
{ key: 'medicineDays', label: '用药天数', value: formatNumber(summary.value.marDayCount), unit: '天' },
{ key: 'medicineCount', label: '用药次数', value: formatNumber(summary.value.marCount), unit: '次' },
{
key: 'feverDays',
label: '发烧天数',
value: formatNumber(summary.value.feverDayCount),
unit: '天',
className: Number(summary.value.feverDayCount || 0) > 0 ? 'value-warning' : ''
},
{
key: 'maxTemp',
label: '最高体温',
value: formatTemperature(temperature.value.maxTemp),
unit: '℃',
className: Number(temperature.value.maxTemp || 0) >= 37.3 ? 'value-warning' : ''
},
{ key: 'avgTemp', label: '平均体温', value: formatTemperature(temperature.value.averageTemp), unit: '℃' }
])
const healthScore = computed(() => {
let score = 100
score -= Math.min(Number(summary.value.feverDayCount || 0) * 8, 32)
score -= Math.min(Number(summary.value.doctorCount || 0) * 3, 18)
if (Number(temperature.value.maxTemp || 0) >= 39) score -= 16
else if (Number(temperature.value.maxTemp || 0) >= 37.3) score -= 8
if (!queryParams.value.recordId) score -= 10
return Math.max(score, 35)
})
const healthScoreText = computed(() => {
if (healthScore.value >= 85) return '整体平稳'
if (healthScore.value >= 70) return '持续观察'
return '重点关注'
})
const riskItems = computed(() => [
{
label: '体温风险',
value: formatTemperature(temperature.value.maxTemp) + '℃',
desc: Number(temperature.value.maxTemp || 0) >= 37.3 ? '存在发热记录' : '未见明显异常',
level: Number(temperature.value.maxTemp || 0) >= 37.3 ? 'warning' : 'normal'
},
{
label: '就医压力',
value: `${formatNumber(summary.value.doctorCount)}`,
desc: `${formatMoney(summary.value.doctorCost)}累计费用`,
level: Number(summary.value.doctorCount || 0) > 3 ? 'warning' : 'normal'
},
{
label: '用药负荷',
value: `${formatNumber(summary.value.marCount)}`,
desc: `${formatNumber(summary.value.marDayCount)}天用药记录`,
level: Number(summary.value.marCount || 0) > 20 ? 'warning' : 'normal'
}
])
const recordFacts = computed(() => [
{ label: '发生原因', value: getDictLabel(etiology.value, recordInfo.value.etiology, recordInfo.value.etiologyName) },
{ label: '初期症状', value: recordInfo.value.initialSymptoms || '--' },
{ label: '中期症状', value: recordInfo.value.mediumTermSymptoms || '--' },
{ label: '后期症状', value: recordInfo.value.laterStageSymptoms || '--' }
])
const medicineRankList = computed(() => {
const data = medicineItems.value.map((item) => ({
name: item.medicalName || item.name || '未命名药品',
count: Number(item.count || item.value || 0)
}))
const max = Math.max(...data.map((item) => item.count), 1)
return data
.sort((a, b) => b.count - a.count)
.map((item, index) => ({
...item,
rankNo: String(index + 1).padStart(2, '0'),
percent: Math.round((item.count / max) * 100)
}))
})
const medicineChartList = computed(() => medicineRankList.value.slice(0, 5))
const eventItems = computed(() => {
const processEvents = processRecordList.value.map((item) => ({
type: 'process',
label: '过程',
title: item.content || item.remark || '过程记录',
time: formatDateTime(item.recordingTime),
value: item.healthRecordName || recordInfo.value.name || '--',
sortTime: getTimeValue(item.recordingTime)
}))
const doctorEvents = doctorRecordList.value.map((item) => ({
type: 'doctor',
label: '就医',
title: item.hospitalName || item.departments || '就医记录',
time: formatDateTime(item.visitingTime),
value: formatMoney(item.totalCost),
sortTime: getTimeValue(item.visitingTime)
}))
const tempEvents = temperatureRecordList.value.map((item) => ({
type: 'temperature',
label: '体温',
title: `${formatTemperature(item.temperature)}`,
time: formatDateTime(item.measureTime),
value: Number(item.temperature || 0) >= 37.3 ? '发热' : '正常',
sortTime: getTimeValue(item.measureTime)
}))
const medicineEvents = marRecordList.value.map((item) => ({
type: 'medicine',
label: '用药',
title: item.name || item.medicalName || '用药记录',
time: formatDateTime(item.dosingTime),
value: item.dosage ? `${item.dosage}` : '--',
sortTime: getTimeValue(item.dosingTime)
}))
return [...processEvents, ...doctorEvents, ...tempEvents, ...medicineEvents].sort((a, b) => b.sortTime - a.sortTime).slice(0, 10)
})
function handlePersonChange(personId) {
queryParams.value.recordId = null
recordInfo.value = {}
screenData.value = {}
renderCharts()
loadHealthRecords(personId, true)
}
function handleRecordChange(recordId) {
if (recordId) {
loadScreen()
return
}
recordInfo.value = {}
screenData.value = {}
renderCharts()
}
function resetQuery() {
queryParams.value = { id: null, recordId: null, type: '1' }
recordInfo.value = {}
screenData.value = {}
healthRecordList.value = []
renderCharts()
getPersonList()
}
function handleFullExport() {
if (!queryParams.value.recordId) {
proxy.$modal.msgWarning('请选择要导出的健康档案')
return
}
proxy.download(
'health/analysis/recordFullExport',
{
recordId: queryParams.value.recordId
},
`${recordInfo.value.name || '健康档案'}_完整信息_${new Date().getTime()}.xlsx`
)
}
function loadHealthRecords(personId, autoSelect = false) {
if (!personId) {
healthRecordList.value = []
return Promise.resolve([])
}
return listHealthRecord({ pageNum: 1, pageSize: 1000, personId }).then((response) => {
healthRecordList.value = response.rows || []
if (autoSelect) {
queryParams.value.recordId = healthRecordList.value.length ? healthRecordList.value[0].id : null
if (queryParams.value.recordId) {
return loadScreen()
}
recordInfo.value = {}
screenData.value = {}
renderCharts()
}
return healthRecordList.value
})
}
function getPersonList() {
return listPerson({ pageNum: 1, pageSize: 1000 }).then((response) => {
personList.value = response.rows || []
if (personList.value.length) {
queryParams.value.id = personList.value[0].id
return loadHealthRecords(queryParams.value.id, true)
}
queryParams.value.id = null
queryParams.value.recordId = null
healthRecordList.value = []
recordInfo.value = {}
screenData.value = {}
renderCharts()
return []
})
}
function loadScreen() {
if (!queryParams.value.recordId) {
proxy.$modal.msgWarning('请选择健康档案后再查询')
recordInfo.value = {}
screenData.value = {}
renderCharts()
return Promise.resolve()
}
loading.value = true
return getRecordScreen(queryParams.value)
.then((response) => {
screenData.value = response.data || {}
recordInfo.value = screenData.value.record || {}
nextTick(renderCharts)
})
.finally(() => {
loading.value = false
})
}
function renderCharts() {
renderTemperatureChart()
renderMedicineChart()
renderDoctorChart()
renderRecordMixChart()
}
function getChartTextStyle() {
return { color: '#9eb8d8', fontSize: 11 }
}
function renderTemperatureChart() {
if (!temperatureChartRef.value) return
temperatureChart && temperatureChart.dispose()
temperatureChart = echarts.init(temperatureChartRef.value)
const data = temperatureList.value
temperatureChart.setOption({
color: ['#27e0ff'],
grid: { left: 44, right: 24, top: 34, bottom: 34 },
tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,18,40,0.92)', borderColor: '#23d5ff', textStyle: { color: '#dff7ff' } },
xAxis: {
type: 'category',
data: data.map((item) => item.time),
axisLine: { lineStyle: { color: 'rgba(126,189,255,0.35)' } },
axisLabel: getChartTextStyle()
},
yAxis: {
type: 'value',
min: (value) => Math.max(34, Math.floor(value.min) - 1),
axisLine: { lineStyle: { color: 'rgba(126,189,255,0.35)' } },
splitLine: { lineStyle: { color: 'rgba(126,189,255,0.12)' } },
axisLabel: getChartTextStyle()
},
series: [
{
name: '体温',
type: 'line',
smooth: true,
data: data.map((item) => item.value),
symbolSize: 8,
lineStyle: { width: 3 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(39,224,255,0.38)' },
{ offset: 1, color: 'rgba(39,224,255,0.02)' }
])
},
markLine: {
symbol: 'none',
label: { color: '#ffcf5a', formatter: '发热线 37.3℃' },
lineStyle: { color: '#ffcf5a', type: 'dashed' },
data: [{ yAxis: 37.3 }]
}
}
]
})
}
function renderMedicineChart() {
if (!medicineChartRef.value) return
medicineChart && medicineChart.dispose()
medicineChart = echarts.init(medicineChartRef.value)
const data = medicineChartList.value.slice().reverse()
medicineChart.setOption({
color: ['#58ffb7'],
grid: { left: 34, right: 18, top: 18, bottom: 22, containLabel: false },
tooltip: {
trigger: 'item',
confine: true,
backgroundColor: 'rgba(4,16,36,0.96)',
borderColor: '#58ffb7',
borderWidth: 1,
extraCssText: 'box-shadow:0 8px 24px rgba(0,0,0,.35);border-radius:6px;padding:10px 12px;max-width:260px;',
formatter: (params) => {
const item = params.data || {}
const name = escapeHtml(item.name || '未命名药品')
const rankNo = escapeHtml(item.rankNo || '--')
const count = escapeHtml(item.count || 0)
return `
<div style="min-width:180px;color:#e8fff7;">
<div style="margin-bottom:8px;color:#58ffb7;font-weight:700;">TOP ${rankNo}</div>
<div style="line-height:1.45;white-space:normal;word-break:break-all;">${name}</div>
<div style="margin-top:8px;color:#ffcf5a;">用药次数:${count}次</div>
</div>
`
}
},
xAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(126,189,255,0.12)' } },
axisLabel: getChartTextStyle()
},
yAxis: {
type: 'category',
data: data.map((item) => item.rankNo),
axisLabel: { color: '#27e0ff', fontSize: 11, margin: 8 },
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(126,189,255,0.3)' } }
},
series: [
{
type: 'bar',
barWidth: 12,
data: data.map((item) => ({
value: item.count,
name: item.name,
count: item.count,
rankNo: item.rankNo
})),
itemStyle: {
borderRadius: [0, 8, 8, 0],
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#1d8fff' },
{ offset: 1, color: '#58ffb7' }
])
}
}
]
})
}
function renderDoctorChart() {
if (!doctorChartRef.value) return
doctorChart && doctorChart.dispose()
doctorChart = echarts.init(doctorChartRef.value)
const data = doctor.value.doctorCostList || []
doctorChart.setOption({
color: ['#ffcf5a'],
grid: { left: 46, right: 18, top: 24, bottom: 30 },
tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,18,40,0.92)', borderColor: '#ffcf5a', textStyle: { color: '#fff8df' } },
xAxis: {
type: 'category',
data: data.map((item) => item.time),
axisLabel: getChartTextStyle(),
axisLine: { lineStyle: { color: 'rgba(126,189,255,0.35)' } }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(126,189,255,0.12)' } },
axisLabel: getChartTextStyle()
},
series: [
{
name: '费用',
type: 'bar',
barWidth: 16,
data: data.map((item) => item.value),
itemStyle: {
borderRadius: [8, 8, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#ffcf5a' },
{ offset: 1, color: 'rgba(255,117,85,0.35)' }
])
}
}
]
})
}
function renderRecordMixChart() {
if (!recordMixChartRef.value) return
recordMixChart && recordMixChart.dispose()
recordMixChart = echarts.init(recordMixChartRef.value)
const data = [
{ name: '过程记录', value: Number(summary.value.processRecordCount || 0) },
{ name: '就医记录', value: Number(summary.value.doctorCount || 0) },
{ name: '体温记录', value: temperatureRecordList.value.length },
{ name: '用药记录', value: Number(summary.value.marCount || 0) }
]
recordMixChart.setOption({
color: ['#27e0ff', '#ffcf5a', '#ff6f91', '#58ffb7'],
tooltip: { trigger: 'item', backgroundColor: 'rgba(6,18,40,0.92)', borderColor: '#23d5ff', textStyle: { color: '#dff7ff' } },
legend: { bottom: 6, textStyle: getChartTextStyle() },
series: [
{
type: 'pie',
radius: ['52%', '74%'],
center: ['50%', '43%'],
avoidLabelOverlap: true,
label: { color: '#dff7ff', formatter: '{b}\n{c}' },
labelLine: { lineStyle: { color: 'rgba(188,225,255,0.45)' } },
data
}
]
})
}
function resizeCharts() {
temperatureChart && temperatureChart.resize()
medicineChart && medicineChart.resize()
doctorChart && doctorChart.resize()
recordMixChart && recordMixChart.resize()
}
function updateClock() {
nowTime.value = new Date().toLocaleString('zh-CN', {
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
function formatNumber(value) {
return Number(value || 0).toLocaleString('zh-CN')
}
function formatMoney(value) {
return `${Number(value || 0).toFixed(2)}`
}
function formatShortMoney(value) {
const num = Number(value || 0)
if (Math.abs(num) >= 10000) return (num / 10000).toFixed(1) + '万'
return num.toFixed(0)
}
function formatTemperature(value) {
return Number(value || 0).toFixed(1)
}
function formatDateTime(value) {
if (!value) return '--'
return String(value).replace('T', ' ').slice(0, 16)
}
function getTimeValue(value) {
const time = value ? new Date(value).getTime() : 0
return Number.isNaN(time) ? 0 : time
}
function getDictLabel(options, value, fallback) {
if (fallback) return fallback
const option = (options || []).find((item) => String(item.value) === String(value))
return option ? option.label : '--'
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (char) => {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}
return map[char]
})
}
onMounted(() => {
updateClock()
clockTimer = window.setInterval(updateClock, 1000)
getPersonList()
window.addEventListener('resize', resizeCharts)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeCharts)
window.clearInterval(clockTimer)
temperatureChart && temperatureChart.dispose()
medicineChart && medicineChart.dispose()
doctorChart && doctorChart.dispose()
recordMixChart && recordMixChart.dispose()
})
</script>
<style scoped lang="scss">
.health-screen {
position: relative;
height: 100vh;
padding: 16px 22px 24px;
overflow-x: hidden;
overflow-y: auto;
color: #dff7ff;
background: radial-gradient(circle at 50% -12%, rgba(39, 224, 255, 0.28), transparent 34%), linear-gradient(145deg, #04162f 0%, #071d3d 42%, #031022 100%);
box-sizing: border-box;
scrollbar-color: #27e0ff rgba(4, 19, 42, 0.82);
scrollbar-width: thin;
}
.health-screen::-webkit-scrollbar {
width: 8px;
}
.health-screen::-webkit-scrollbar-track {
background: rgba(4, 19, 42, 0.82);
}
.health-screen::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #27e0ff, #148dff);
border-radius: 8px;
}
.screen-bg,
.screen-grid-bg {
position: absolute;
inset: 0;
pointer-events: none;
}
.screen-bg-a {
background: linear-gradient(90deg, rgba(39, 224, 255, 0.08) 1px, transparent 1px), linear-gradient(0deg, rgba(39, 224, 255, 0.06) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.95), transparent 82%);
}
.screen-bg-b {
background: radial-gradient(circle at 10% 25%, rgba(88, 255, 183, 0.16), transparent 24%),
radial-gradient(circle at 90% 35%, rgba(255, 207, 90, 0.13), transparent 28%);
}
.screen-grid-bg {
border: 1px solid rgba(103, 205, 255, 0.16);
position: fixed;
margin: 12px 18px 12px 12px;
}
.cockpit-header,
.cockpit-main {
position: relative;
z-index: 1;
}
.cockpit-header {
display: grid;
grid-template-columns: minmax(420px, 1fr) minmax(560px, auto);
gap: 16px;
align-items: center;
min-height: 74px;
padding: 2px 2px 0;
}
.screen-actions,
.screen-panel,
.record-focus,
.metric-card {
background: linear-gradient(145deg, rgba(8, 36, 75, 0.88), rgba(4, 20, 44, 0.78));
border: 1px solid rgba(92, 205, 255, 0.26);
box-shadow: inset 0 0 28px rgba(39, 224, 255, 0.05), 0 12px 34px rgba(0, 7, 18, 0.34);
backdrop-filter: blur(10px);
}
.title-block {
grid-column: 1;
justify-self: start;
width: min(760px, 100%);
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
text-align: left;
pointer-events: none;
h1 {
margin: 0;
color: #effcff;
font-size: 32px;
font-weight: 800;
letter-spacing: 1px;
text-shadow: 0 0 22px rgba(39, 224, 255, 0.72);
}
p {
margin: 6px 0 0;
color: #9eb8d8;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.title-meta {
display: inline-flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 8px;
margin-top: 7px;
span {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 10px;
color: #bfefff;
background: linear-gradient(90deg, rgba(39, 224, 255, 0.16), rgba(88, 255, 183, 0.08));
border: 1px solid rgba(92, 205, 255, 0.24);
border-radius: 3px;
font-size: 12px;
}
}
.title-wing {
width: 74px;
height: 36px;
background: linear-gradient(90deg, #27e0ff, rgba(39, 224, 255, 0)), linear-gradient(180deg, rgba(39, 224, 255, 0.16), rgba(39, 224, 255, 0));
clip-path: polygon(0 0, 88% 0, 100% 50%, 88% 100%, 0 100%, 12% 50%);
}
.title-wing-right {
display: none;
}
.screen-actions {
grid-column: 2;
justify-self: end;
z-index: 2;
width: auto;
max-width: min(820px, 60vw);
display: grid;
grid-template-columns: 52px 168px minmax(260px, 320px) 82px 82px 82px;
align-items: center;
justify-content: flex-end;
gap: 9px;
padding: 8px 10px;
border-radius: 8px;
}
.filter-label {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 4px 0 0;
color: #8fffe0;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
white-space: nowrap;
&::before {
content: '';
width: 3px;
height: 14px;
margin-right: 7px;
background: linear-gradient(180deg, #27e0ff, #58ffb7);
border-radius: 2px;
box-shadow: 0 0 10px rgba(39, 224, 255, 0.6);
}
}
.filter-select {
flex: 0 0 auto;
}
.person-select {
width: 160px;
}
.record-select {
width: 260px;
}
.screen-actions :deep(.el-input__wrapper) {
min-height: 36px;
background: linear-gradient(180deg, rgba(7, 31, 65, 0.96), rgba(4, 19, 42, 0.92));
border: 1px solid rgba(92, 205, 255, 0.38);
border-radius: 4px;
box-shadow: inset 0 0 16px rgba(39, 224, 255, 0.08), 0 0 12px rgba(39, 224, 255, 0.06);
}
.screen-actions :deep(.el-input__wrapper:hover),
.screen-actions :deep(.el-input__wrapper.is-focus) {
border-color: rgba(39, 224, 255, 0.86);
box-shadow: inset 0 0 18px rgba(39, 224, 255, 0.14), 0 0 18px rgba(39, 224, 255, 0.18);
}
.screen-actions :deep(.el-input__inner) {
color: #dff7ff;
font-size: 13px;
}
.screen-actions :deep(.el-input__inner::placeholder) {
color: rgba(158, 184, 216, 0.7);
}
.screen-actions :deep(.el-select .el-input .el-select__caret) {
color: #27e0ff;
}
.screen-actions :deep(.el-input__suffix-inner .el-icon) {
color: #27e0ff;
}
.screen-btn {
height: 36px;
width: 82px;
padding: 0 10px;
border-radius: 4px;
font-weight: 600;
letter-spacing: 1px;
}
.screen-btn-primary {
border: none;
background: linear-gradient(90deg, #148dff, #27e0ff);
box-shadow: 0 0 16px rgba(39, 224, 255, 0.28);
}
.screen-btn-plain {
color: #bfefff;
background: linear-gradient(180deg, rgba(8, 35, 72, 0.9), rgba(4, 19, 42, 0.86));
border-color: rgba(92, 205, 255, 0.34);
&:hover,
&:focus {
color: #ffffff;
background: linear-gradient(180deg, rgba(12, 54, 108, 0.94), rgba(6, 31, 68, 0.9));
border-color: rgba(39, 224, 255, 0.72);
}
}
.cockpit-main {
margin-top: 10px;
}
.top-overview {
display: grid;
grid-template-columns: minmax(560px, 0.78fr) minmax(0, 1.62fr);
align-items: stretch;
gap: 12px;
}
.record-focus {
position: relative;
min-height: 172px;
padding: 16px 18px;
border-radius: 8px;
overflow: hidden;
&::after {
content: '';
position: absolute;
right: -48px;
top: -48px;
width: 150px;
height: 150px;
border: 26px solid rgba(39, 224, 255, 0.08);
border-radius: 50%;
}
}
.record-main {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: start;
}
.record-kicker,
.panel-head em,
.metric-card span {
color: #8fb8d8;
font-size: 13px;
}
.record-name {
margin-top: 7px;
overflow: hidden;
color: #ffffff;
font-size: 26px;
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
max-width: 230px;
margin-top: 2px;
span {
padding: 4px 10px;
color: #c8f7ff;
background: rgba(39, 224, 255, 0.12);
border: 1px solid rgba(39, 224, 255, 0.24);
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
}
}
.record-timeline {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 14px;
div {
padding: 10px;
background: rgba(2, 12, 28, 0.38);
border: 1px solid rgba(126, 189, 255, 0.16);
border-radius: 4px;
}
span {
display: block;
color: #8fb8d8;
font-size: 12px;
}
strong {
display: block;
margin-top: 6px;
overflow: hidden;
color: #e8fbff;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.metric-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
align-self: stretch;
gap: 9px;
min-height: 172px;
}
.metric-card {
position: relative;
display: grid;
grid-template-rows: 18px minmax(28px, 1fr);
align-content: center;
height: auto;
min-height: 0;
padding: 9px 12px 8px 14px;
border-radius: 8px;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 12px;
bottom: 12px;
width: 2px;
background: linear-gradient(180deg, #27e0ff, rgba(88, 255, 183, 0.45));
}
span {
display: block;
min-height: 18px;
overflow: hidden;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.metric-value {
display: flex;
align-items: baseline;
gap: 6px;
min-height: 28px;
margin-top: 3px;
overflow: visible;
strong {
display: block;
min-width: 0;
overflow: hidden;
color: #27e0ff;
font-family: DIN, 'Microsoft YaHei', sans-serif;
font-size: clamp(20px, 1.08vw, 25px);
line-height: 28px;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
display: block;
color: #9eb8d8;
font-style: normal;
font-size: 12px;
line-height: 18px;
}
.value-gold {
color: #ffcf5a;
}
.value-warning {
color: #ff6f91;
}
}
.fact-strip-panel {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
align-items: start;
gap: 12px;
min-height: 56px;
margin-top: 12px;
padding: 9px 14px;
overflow: hidden;
background: linear-gradient(145deg, rgba(8, 36, 75, 0.88), rgba(4, 20, 44, 0.78));
border: 1px solid rgba(92, 205, 255, 0.26);
border-radius: 8px;
box-shadow: inset 0 0 28px rgba(39, 224, 255, 0.05), 0 12px 34px rgba(0, 7, 18, 0.34);
backdrop-filter: blur(10px);
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 72px;
height: 2px;
background: linear-gradient(90deg, #27e0ff, transparent);
}
}
.fact-strip-head {
display: grid;
gap: 4px;
min-width: 0;
span {
color: #effcff;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}
em {
color: #8fb8d8;
font-size: 12px;
font-style: normal;
white-space: nowrap;
}
}
.fact-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: stretch;
gap: 10px;
min-width: 0;
}
.fact-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 8px;
min-width: 0;
min-height: 36px;
height: auto;
padding: 8px 10px;
background: rgba(3, 18, 40, 0.58);
border: 1px solid rgba(126, 189, 255, 0.12);
border-radius: 4px;
span {
padding-top: 1px;
color: #8fb8d8;
font-size: 12px;
white-space: nowrap;
}
strong {
min-width: 0;
overflow: visible;
color: #e8fbff;
font-size: 13px;
font-weight: 500;
line-height: 1.45;
text-overflow: initial;
white-space: normal;
word-break: break-all;
}
}
.cockpit-grid {
display: grid;
grid-template-columns: minmax(310px, 0.9fr) minmax(540px, 1.5fr) minmax(330px, 0.95fr);
grid-template-rows: 336px 292px;
grid-template-areas:
'profile trend medicine'
'mix events doctor';
gap: 12px;
margin-top: 12px;
}
.screen-panel {
position: relative;
display: flex;
min-width: 0;
min-height: 0;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 72px;
height: 2px;
background: linear-gradient(90deg, #27e0ff, transparent);
}
}
.panel-head {
flex: 0 0 42px;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0;
min-height: 42px;
padding: 0 14px;
border-bottom: 1px solid rgba(126, 189, 255, 0.14);
span {
min-width: 0;
overflow: hidden;
color: #effcff;
font-size: 16px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
flex: 0 0 auto;
max-width: 54%;
overflow: hidden;
font-style: normal;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.chart {
width: 100%;
min-height: 0;
}
.chart-mid {
flex: 1 1 auto;
height: auto;
min-height: 0;
}
.chart-large {
flex: 1 1 auto;
height: auto;
min-height: 0;
}
.grid-profile {
grid-area: profile;
}
.grid-trend {
grid-area: trend;
}
.grid-medicine {
grid-area: medicine;
}
.grid-mix {
grid-area: mix;
}
.grid-events {
grid-area: events;
}
.grid-doctor {
grid-area: doctor;
}
.risk-score {
flex: 1;
display: grid;
grid-template-columns: 108px minmax(0, 1fr);
align-items: center;
gap: 12px;
min-height: 0;
padding: 14px 14px 12px;
}
.score-ring {
align-self: center;
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 108px;
height: 108px;
border: 6px solid rgba(39, 224, 255, 0.24);
border-top-color: #27e0ff;
border-right-color: #58ffb7;
border-radius: 50%;
box-shadow: 0 0 28px rgba(39, 224, 255, 0.2);
strong {
color: #58ffb7;
font-size: 31px;
line-height: 1;
}
span {
margin-top: 8px;
color: #9eb8d8;
font-size: 12px;
}
}
.risk-list {
display: grid;
align-content: stretch;
gap: 6px;
min-width: 0;
}
.risk-row {
min-width: 0;
padding: 7px 10px;
background: rgba(3, 18, 40, 0.58);
border-left: 3px solid #58ffb7;
span,
em {
display: block;
color: #9eb8d8;
font-size: 12px;
font-style: normal;
}
strong {
display: block;
margin: 3px 0;
overflow: hidden;
color: #e8fbff;
font-size: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
&.warning {
border-left-color: #ff6f91;
strong {
color: #ffcf5a;
}
}
}
.main-trend-panel {
min-height: 0;
}
.timeline-panel {
min-height: 0;
}
.medicine-panel {
min-height: 0;
}
.medicine-panel .chart-mid {
flex: 0 0 132px;
height: 132px;
}
.doctor-panel {
min-height: 0;
}
.doctor-panel .chart-mid {
flex: 1;
min-height: 0;
}
.event-stream,
.rank-list {
padding: 10px 14px 12px;
}
.event-stream {
display: grid;
grid-template-columns: 1fr;
flex: 1;
align-content: start;
gap: 7px;
min-height: 0;
max-height: none;
overflow-x: hidden;
overflow-y: auto;
scrollbar-color: #27e0ff rgba(4, 19, 42, 0.82);
scrollbar-width: thin;
}
.event-stream::-webkit-scrollbar {
width: 6px;
}
.event-stream::-webkit-scrollbar-track {
background: rgba(4, 19, 42, 0.82);
}
.event-stream::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #27e0ff, #148dff);
border-radius: 6px;
}
.event-row {
position: relative;
display: grid;
grid-template-columns: 50px minmax(0, 1fr) minmax(68px, auto);
align-items: center;
gap: 10px;
min-height: 44px;
padding: 7px 10px 7px 13px;
overflow: hidden;
background: linear-gradient(90deg, rgba(3, 18, 40, 0.78), rgba(5, 28, 58, 0.48));
border: 1px solid rgba(126, 189, 255, 0.12);
border-radius: 4px;
&::before {
content: '';
position: absolute;
left: 0;
top: 9px;
bottom: 9px;
width: 2px;
background: linear-gradient(180deg, #27e0ff, rgba(88, 255, 183, 0.25));
}
strong {
display: block;
overflow: hidden;
color: #e8fbff;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
display: block;
margin-top: 5px;
color: #8fb8d8;
font-size: 12px;
font-style: normal;
}
b {
overflow: hidden;
color: #ffcf5a;
font-size: 12px;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.event-tag {
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
color: #00172c;
background: #27e0ff;
border-radius: 3px;
font-size: 12px;
font-weight: 700;
&.doctor {
background: #ffcf5a;
}
&.temperature {
background: #ff6f91;
}
&.medicine {
background: #58ffb7;
}
}
.rank-list {
display: grid;
align-content: start;
gap: 6px;
flex: 1;
min-height: 0;
overflow-y: auto;
padding-top: 8px;
scrollbar-color: #27e0ff rgba(4, 19, 42, 0.82);
scrollbar-width: thin;
}
.rank-list::-webkit-scrollbar {
width: 6px;
}
.rank-list::-webkit-scrollbar-track {
background: rgba(4, 19, 42, 0.82);
}
.rank-list::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #27e0ff, #148dff);
border-radius: 6px;
}
.rank-row {
position: relative;
display: grid;
grid-template-columns: 34px minmax(0, 1fr) 52px;
align-items: center;
gap: 8px;
min-height: 28px;
padding: 5px 8px;
overflow: hidden;
background: rgba(3, 18, 40, 0.6);
border: 1px solid rgba(126, 189, 255, 0.12);
border-radius: 4px;
span,
em,
strong {
position: relative;
z-index: 1;
}
span {
color: #27e0ff;
font-family: DIN, 'Microsoft YaHei', sans-serif;
}
strong {
overflow: hidden;
color: #e8fbff;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
color: #ffcf5a;
font-size: 12px;
font-style: normal;
text-align: right;
}
i {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: var(--rank-width);
background: linear-gradient(90deg, rgba(39, 224, 255, 0.25), rgba(88, 255, 183, 0.08));
}
}
.detail-grid {
margin-top: 12px;
}
.detail-panel {
min-height: 326px;
}
.detail-head {
min-height: 54px;
background: linear-gradient(90deg, rgba(39, 224, 255, 0.08), rgba(4, 22, 48, 0));
div {
display: grid;
gap: 5px;
min-width: 0;
}
strong {
overflow: hidden;
color: #8fb8d8;
font-size: 12px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
padding: 4px 10px;
color: #58ffb7;
background: rgba(88, 255, 183, 0.1);
border: 1px solid rgba(88, 255, 183, 0.2);
border-radius: 3px;
font-size: 12px;
}
}
.screen-tabs {
padding: 0 14px 16px;
}
.screen-tabs :deep(.el-tabs__header) {
margin: 0;
}
.screen-tabs :deep(.el-tabs__item) {
color: #8fb8d8;
}
.screen-tabs :deep(.el-tabs__item.is-active) {
color: #27e0ff;
}
.screen-tabs :deep(.el-tabs__active-bar) {
background-color: #27e0ff;
}
.screen-tabs :deep(.el-tabs__nav-wrap::after) {
background-color: rgba(126, 189, 255, 0.14);
}
.detail-panel :deep(.el-table) {
color: #dff7ff;
background: transparent !important;
--el-table-header-bg-color: rgba(4, 22, 48, 0.98);
--el-table-tr-bg-color: rgba(3, 18, 40, 0.38);
--el-table-row-hover-bg-color: rgba(39, 224, 255, 0.1);
}
.detail-panel :deep(.el-table__inner-wrapper),
.detail-panel :deep(.el-table__body-wrapper),
.detail-panel :deep(.el-table__header-wrapper),
.detail-panel :deep(.el-scrollbar__view) {
background: transparent !important;
}
.detail-panel :deep(.el-table__header-wrapper tr),
.detail-panel :deep(.el-table thead),
.detail-panel :deep(.el-table th.el-table__cell) {
color: #9ecfff !important;
background: rgba(4, 22, 48, 0.98) !important;
border-bottom-color: rgba(92, 205, 255, 0.22) !important;
font-weight: 700;
}
.detail-panel :deep(.el-table tr),
.detail-panel :deep(.el-table td.el-table__cell) {
color: #dff7ff;
background: rgba(3, 18, 40, 0.38) !important;
border-bottom-color: rgba(126, 189, 255, 0.12) !important;
}
.detail-panel :deep(.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell) {
background: rgba(39, 224, 255, 0.1) !important;
}
.detail-panel :deep(.el-table__inner-wrapper::before) {
background-color: rgba(126, 189, 255, 0.12);
}
.detail-panel :deep(.el-table__empty-block) {
background: rgba(3, 18, 40, 0.38);
}
.detail-panel :deep(.el-table__empty-text) {
color: #8fb8d8;
}
.health-screen :deep(.el-loading-mask) {
background-color: rgba(2, 10, 24, 0.66);
}
@media (max-width: 1680px) {
.cockpit-header {
grid-template-columns: minmax(360px, 1fr) minmax(520px, auto);
min-height: 72px;
}
.title-block {
width: min(640px, 100%);
h1 {
font-size: 29px;
}
}
.screen-actions {
max-width: min(760px, 62vw);
grid-template-columns: 48px 150px minmax(230px, 280px) 76px 76px 76px;
}
.top-overview {
grid-template-columns: minmax(520px, 0.82fr) minmax(0, 1.52fr);
}
.metric-card {
min-height: 0;
padding: 8px 10px 8px 12px;
}
.cockpit-grid {
grid-template-columns: minmax(300px, 0.9fr) minmax(480px, 1.46fr) minmax(310px, 0.92fr);
grid-template-rows: 318px 276px;
}
}
@media (max-width: 1500px) {
.cockpit-header {
grid-template-columns: minmax(320px, 1fr) minmax(0, auto);
}
.screen-actions {
max-width: 620px;
grid-template-columns: 48px 140px 210px;
}
.metric-strip {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
}
.cockpit-grid {
grid-template-columns: 292px minmax(0, 1fr) 306px;
grid-template-rows: 316px 272px;
}
.record-main {
grid-template-columns: 1fr;
gap: 8px;
}
.record-meta {
justify-content: flex-start;
max-width: none;
}
.risk-score {
grid-template-columns: 1fr;
align-content: center;
justify-items: center;
overflow-y: auto;
}
.score-ring {
justify-self: center;
width: 96px;
height: 96px;
}
.fact-strip-panel {
grid-template-columns: 100px minmax(0, 1fr);
}
.fact-strip {
grid-template-columns: repeat(4, minmax(180px, 1fr));
}
}
@media (max-width: 1280px) {
.cockpit-header,
.top-overview {
grid-template-columns: 1fr;
}
.cockpit-grid {
grid-template-columns: 1fr;
grid-template-rows: none;
grid-template-areas:
'profile'
'trend'
'medicine'
'mix'
'events'
'doctor';
}
.screen-panel {
min-height: 280px;
}
.cockpit-header {
display: grid;
gap: 14px;
min-height: auto;
}
.screen-actions,
.title-block {
position: relative;
left: auto;
top: auto;
z-index: 2;
width: 100%;
transform: none;
}
.title-block {
order: -1;
pointer-events: auto;
}
.screen-actions {
grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.4fr);
right: auto;
bottom: auto;
justify-self: stretch;
margin-top: 0;
justify-content: flex-start;
max-width: none;
}
.screen-btn {
min-width: 92px;
}
.metric-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(4, minmax(64px, auto));
}
.event-stream {
grid-template-columns: 1fr;
max-height: 340px;
}
.fact-strip-panel {
grid-template-columns: 1fr;
gap: 8px;
}
.fact-strip {
grid-template-columns: repeat(4, minmax(210px, 1fr));
overflow-x: auto;
padding-bottom: 2px;
}
}
@media (max-width: 900px) {
.health-screen {
padding: 12px;
}
.screen-actions {
grid-template-columns: 1fr;
}
.filter-label {
display: none;
}
.person-select,
.record-select {
width: 100%;
}
.record-timeline,
.metric-strip {
grid-template-columns: 1fr;
grid-template-rows: none;
}
.fact-strip {
grid-template-columns: repeat(4, minmax(190px, 1fr));
}
}
</style>
<style lang="scss">
.health-screen-popper {
background: rgba(4, 18, 42, 0.98) !important;
border: 1px solid rgba(39, 224, 255, 0.52) !important;
border-radius: 4px !important;
box-shadow: 0 12px 34px rgba(0, 7, 18, 0.48), 0 0 24px rgba(39, 224, 255, 0.18) !important;
.el-popper__arrow::before {
background: rgba(4, 18, 42, 0.98) !important;
border-color: rgba(39, 224, 255, 0.52) !important;
}
.el-select-dropdown {
background: transparent !important;
}
.el-select-dropdown__wrap {
max-height: 280px;
}
.el-select-dropdown__item {
height: 36px;
color: #bfefff;
line-height: 36px;
font-size: 13px;
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
color: #ffffff;
background: linear-gradient(90deg, rgba(39, 224, 255, 0.24), rgba(88, 255, 183, 0.08)) !important;
}
.el-select-dropdown__item.selected {
color: #58ffb7;
font-weight: 700;
background: rgba(88, 255, 183, 0.1);
}
.el-select-dropdown__empty {
color: #8fb8d8;
}
.el-scrollbar__bar.is-vertical > div {
background-color: #27e0ff;
}
}
</style>