2040 lines
51 KiB
Vue
2040 lines
51 KiB
Vue
<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 = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}
|
||
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>
|