feat(ui): 仪表盘实时数据推送优化 - WebSocket连接+预警卡片+趋势展示
This commit is contained in:
@@ -3,26 +3,19 @@
|
||||
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="font-size:18px;font-weight:bold">系统仪表盘</span>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="loadData"
|
||||
>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
@click="showSystemInfo = true"
|
||||
>
|
||||
系统信息
|
||||
</el-button>
|
||||
<el-tag v-if="wsConnected" type="success" size="small" style="margin-right:8px">
|
||||
● 实时连接
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger" size="small" style="margin-right:8px">
|
||||
○ 离线
|
||||
</el-tag>
|
||||
<el-button type="primary" @click="loadData">刷新</el-button>
|
||||
<el-button type="info" @click="showSystemInfo = true">系统信息</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统概览 -->
|
||||
<el-card
|
||||
shadow="never"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<el-card shadow="never" style="margin-bottom:16px">
|
||||
<div style="text-align:center;padding:20px">
|
||||
<h2 style="color:#409eff;margin:0;font-size:24px">
|
||||
{{ overview.systemName || 'HealthLink-HIS' }}
|
||||
@@ -33,35 +26,45 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 核心指标 -->
|
||||
<el-row
|
||||
:gutter="16"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<el-col
|
||||
v-for="item in statCards"
|
||||
:key="item.label"
|
||||
:span="4"
|
||||
>
|
||||
<el-card
|
||||
shadow="hover"
|
||||
:body-style="{padding:'12px'}"
|
||||
>
|
||||
<!-- 核心指标 - 实时更新 -->
|
||||
<el-row :gutter="16" style="margin-bottom:16px">
|
||||
<el-col v-for="item in statCards" :key="item.label" :span="4">
|
||||
<el-card shadow="hover" :body-style="{padding:'12px'}">
|
||||
<div style="text-align:center">
|
||||
<div
|
||||
style="font-size:22px;font-weight:bold"
|
||||
:style="{color:item.color}"
|
||||
>
|
||||
<div style="font-size:22px;font-weight:bold" :style="{color:item.color}">
|
||||
{{ item.value }}
|
||||
<el-icon v-if="item.trend > 0" style="color:#67C23A;margin-left:4px"><Top /></el-icon>
|
||||
<el-icon v-else-if="item.trend < 0" style="color:#F56C6C;margin-left:4px"><Bottom /></el-icon>
|
||||
</div>
|
||||
<div style="font-size:12px;color:#999">
|
||||
{{ item.label }}
|
||||
<div style="font-size:12px;color:#999">{{ item.label }}</div>
|
||||
<div v-if="item.alert" style="margin-top:4px">
|
||||
<el-tag type="danger" size="small">{{ item.alert }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 实时预警 -->
|
||||
<el-row :gutter="16" style="margin-bottom:16px" v-if="alerts.length > 0">
|
||||
<el-col :span="24">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span style="color:#F56C6C">⚠️ 实时预警</span>
|
||||
</template>
|
||||
<el-alert
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
:title="alert.title"
|
||||
:description="alert.description"
|
||||
:type="alert.type"
|
||||
show-icon
|
||||
style="margin-bottom:8px"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据仪表盘 -->
|
||||
<el-row :gutter="16" style="margin-bottom:16px" v-if="dashData.totalRecords > 0">
|
||||
<el-col :span="6">
|
||||
@@ -99,55 +102,26 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 功能模块 -->
|
||||
<el-card
|
||||
shadow="never"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<template #header>
|
||||
功能模块 ({{ modules.length }})
|
||||
</template>
|
||||
<el-card shadow="never" style="margin-bottom:16px">
|
||||
<template #header>功能模块 ({{ modules.length }})</template>
|
||||
<el-row :gutter="12">
|
||||
<el-col
|
||||
v-for="mod in modules"
|
||||
:key="mod.name"
|
||||
:span="4"
|
||||
>
|
||||
<el-card
|
||||
shadow="hover"
|
||||
style="text-align:center;margin-bottom:12px;cursor:pointer"
|
||||
@click="handleModuleClick(mod)"
|
||||
>
|
||||
<div style="font-size:16px;font-weight:bold;color:#409eff;margin-bottom:4px">
|
||||
{{ mod.icon || '📦' }}
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:500">
|
||||
{{ mod.name }}
|
||||
</div>
|
||||
<div style="font-size:11px;color:#999;margin-top:4px">
|
||||
{{ mod.desc }}
|
||||
</div>
|
||||
<el-col v-for="mod in modules" :key="mod.name" :span="4">
|
||||
<el-card shadow="hover" style="text-align:center;margin-bottom:12px;cursor:pointer" @click="handleModuleClick(mod)">
|
||||
<div style="font-size:16px;font-weight:bold;color:#409eff;margin-bottom:4px">{{ mod.icon }}</div>
|
||||
<div style="font-size:13px;font-weight:500">{{ mod.name }}</div>
|
||||
<div style="font-size:11px;color:#999;margin-top:4px">{{ mod.desc }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<el-row
|
||||
:gutter="16"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<el-row :gutter="16" style="margin-bottom:16px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
快捷操作
|
||||
</template>
|
||||
<template #header>快捷操作</template>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px">
|
||||
<el-button
|
||||
v-for="action in quickActions"
|
||||
:key="action.label"
|
||||
:type="action.type"
|
||||
@click="handleAction(action)"
|
||||
>
|
||||
<el-button v-for="action in quickActions" :key="action.label" :type="action.type" @click="handleAction(action)">
|
||||
{{ action.icon }} {{ action.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -155,17 +129,9 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
最近操作
|
||||
</template>
|
||||
<template #header>最近操作</template>
|
||||
<el-timeline style="padding-left:10px">
|
||||
<el-timeline-item
|
||||
v-for="(log, index) in recentLogs"
|
||||
:key="index"
|
||||
:timestamp="log.time"
|
||||
placement="top"
|
||||
:type="log.type"
|
||||
>
|
||||
<el-timeline-item v-for="(log, index) in recentLogs" :key="index" :timestamp="log.time" placement="top" :type="log.type">
|
||||
{{ log.content }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
@@ -180,60 +146,43 @@
|
||||
width="600px"
|
||||
append-to-body
|
||||
>
|
||||
<el-descriptions
|
||||
:column="2"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="系统名称">
|
||||
{{ overview.systemName || 'HealthLink-HIS' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本号">
|
||||
{{ overview.version || 'v2.0' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行环境">
|
||||
{{ overview.env || '开发环境' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="后端端口">
|
||||
{{ overview.backendPort || '18082' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库表">
|
||||
{{ overview.totalTables || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="API接口">
|
||||
{{ overview.totalApis || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="功能模块">
|
||||
{{ modules.length }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">
|
||||
{{ overview.startTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="系统名称">{{ overview.systemName || 'HealthLink-HIS' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="版本号">{{ overview.version || 'v2.0' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运行环境">{{ overview.env || '开发环境' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="后端端口">{{ overview.backendPort || '18082' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库表">{{ overview.totalTables || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="API接口">{{ overview.totalApis || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="功能模块">{{ modules.length }}</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">{{ overview.startTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="showSystemInfo = false">
|
||||
关闭
|
||||
</el-button>
|
||||
<el-button @click="showSystemInfo = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {ref, onMounted, onUnmounted} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {getDashboardOverview, getDashboardData, getDashboardCharts} from './api'
|
||||
import {Top, Bottom} from '@element-plus/icons-vue'
|
||||
import {getDashboardOverview, getDashboardData} from './api'
|
||||
|
||||
const overview = ref({})
|
||||
const dashData = ref({})
|
||||
const showSystemInfo = ref(false)
|
||||
const wsConnected = ref(false)
|
||||
const alerts = ref([])
|
||||
let ws = null
|
||||
|
||||
const statCards = ref([
|
||||
{label:'数据库表', value:0, color:'#409eff'},
|
||||
{label:'API接口', value:0, color:'#67c23a'},
|
||||
{label:'功能模块', value:0, color:'#e6a23c'},
|
||||
{label:'菜单数', value:0, color:'#f56c6c'},
|
||||
{label:'在线用户', value:0, color:'#909399'},
|
||||
{label:'今日操作', value:0, color:'#409eff'}
|
||||
{label:'数据库表', value:0, color:'#409eff', trend:0, alert:''},
|
||||
{label:'API接口', value:0, color:'#67c23a', trend:0, alert:''},
|
||||
{label:'功能模块', value:0, color:'#e6a23c', trend:0, alert:''},
|
||||
{label:'菜单数', value:0, color:'#f56c6c', trend:0, alert:''},
|
||||
{label:'在线用户', value:0, color:'#909399', trend:0, alert:''},
|
||||
{label:'今日操作', value:0, color:'#409eff', trend:0, alert:''}
|
||||
])
|
||||
|
||||
const modules = ref([
|
||||
@@ -260,13 +209,7 @@ const quickActions = ref([
|
||||
{label:'报表', icon:'📈', type:'', path:'/businessanalytics'}
|
||||
])
|
||||
|
||||
const recentLogs = ref([
|
||||
{content:'管理员登录系统', time:'刚刚', type:'primary'},
|
||||
{content:'新增处方点评计划', time:'5分钟前', type:'success'},
|
||||
{content:'更新抗菌药物规则', time:'10分钟前', type:'warning'},
|
||||
{content:'护理评估提交', time:'15分钟前', type:'info'},
|
||||
{content:'危急值处理完成', time:'20分钟前', type:'danger'}
|
||||
])
|
||||
const recentLogs = ref([])
|
||||
|
||||
function formatMoney(val) {
|
||||
if (!val) return '0.00'
|
||||
@@ -287,6 +230,67 @@ async function loadData() {
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
try {
|
||||
const wsUrl = `ws://${window.location.hostname}:18082/ws/dashboard`
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
wsConnected.value = true
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
handleRealtimeUpdate(data)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
wsConnected.value = false
|
||||
setTimeout(initWebSocket, 5000)
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
wsConnected.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('WebSocket连接失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRealtimeUpdate(data) {
|
||||
if (data.type === 'STATISTICS') {
|
||||
if (data.onlineUsers !== undefined) {
|
||||
const prev = statCards.value[4].value
|
||||
statCards.value[4].value = data.onlineUsers
|
||||
statCards.value[4].trend = data.onlineUsers - prev
|
||||
}
|
||||
if (data.todayOperations !== undefined) {
|
||||
const prev = statCards.value[5].value
|
||||
statCards.value[5].value = data.todayOperations
|
||||
statCards.value[5].trend = data.todayOperations - prev
|
||||
}
|
||||
} else if (data.type === 'ALERT') {
|
||||
alerts.value.unshift({
|
||||
id: Date.now(),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
type: data.level || 'warning'
|
||||
})
|
||||
if (alerts.value.length > 5) {
|
||||
alerts.value.pop()
|
||||
}
|
||||
} else if (data.type === 'RECENT_LOG') {
|
||||
recentLogs.value.unshift({
|
||||
content: data.content,
|
||||
time: data.time || '刚刚',
|
||||
type: data.logType || 'primary'
|
||||
})
|
||||
if (recentLogs.value.length > 10) {
|
||||
recentLogs.value.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModuleClick(mod) {
|
||||
ElMessage.info('进入模块: ' + mod.name)
|
||||
}
|
||||
@@ -295,5 +299,12 @@ function handleAction(action) {
|
||||
ElMessage.info('跳转: ' + action.label)
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
initWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) ws.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user