feat(ui): 仪表盘实时数据推送优化 - WebSocket连接+预警卡片+趋势展示

This commit is contained in:
2026-06-20 22:23:43 +08:00
parent 109abc122a
commit 715209d099

View File

@@ -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>