Files
admin-vben5/apps/web-antd/src/views/cockpit/security/index.vue
mocheng 97dc93f3aa
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
安防大屏
2025-08-16 17:58:59 +08:00

895 lines
33 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="min-h-screen bg-slate-900 text-slate-100 flex flex-col overflow-hidden relative">
<!-- 背景动态效果 -->
<div class="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.15)_0,rgba(15,23,42,0)_70%)] pointer-events-none"></div>
<div class="absolute inset-0 bg-grid pattern-grid pointer-events-none"></div>
<!-- 顶部标题区域 -->
<header class="bg-slate-800/80 backdrop-blur-sm border-b border-slate-700 py-3 px-6 flex justify-between items-center z-10">
<div class="flex items-center gap-3">
<div class="bg-blue-600 rounded-lg w-10 h-10 flex items-center justify-center shadow-lg shadow-blue-600/20">
<i class="fa fa-shield text-xl"></i>
</div>
<h1 class="text-2xl font-bold tracking-wide text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-300">
预警监控指挥系统
</h1>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2 text-slate-300">
<i class="fa fa-clock-o"></i>
<span>{{ currentTime }}</span>
</div>
<div class="flex items-center gap-2 text-slate-300">
<i class="fa fa-refresh"></i>
<span>数据更新于: {{ currentTime }}</span>
</div>
<!-- <button class="bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 px-4 py-2 rounded-lg transition-all duration-300 flex items-center gap-2 transform hover:scale-105">-->
<!-- <i class="fa fa-download"></i>-->
<!-- <span>导出报告</span>-->
<!-- </button>-->
</div>
</header>
<!-- 主内容区域 -->
<main class="flex-1 flex overflow-hidden p-4 gap-4">
<!-- 左侧预警列表区域 -->
<section class="w-1/4 flex flex-col gap-4">
<!-- 预警分类统计 -->
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<h2 class="text-lg font-semibold mb-3 flex items-center">
<i class="fa fa-tags text-blue-400 mr-2"></i>
预警分类统计
</h2>
<div class="grid grid-cols-2 gap-3">
<div class="bg-red-600/10 border border-red-500/30 rounded-lg p-3 transform transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-red-500/10">
<div class="text-sm text-red-300">紧急预警</div>
<div class="text-2xl font-bold text-red-400 mt-1 counter-animation">{{ stats.emergency }}</div>
</div>
<div class="bg-orange-600/10 border border-orange-500/30 rounded-lg p-3 transform transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-orange-500/10">
<div class="text-sm text-orange-300">重要预警</div>
<div class="text-2xl font-bold text-orange-400 mt-1 counter-animation">{{ stats.important }}</div>
</div>
<div class="bg-yellow-600/10 border border-yellow-500/30 rounded-lg p-3 transform transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-yellow-500/10">
<div class="text-sm text-yellow-300">一般预警</div>
<div class="text-2xl font-bold text-yellow-400 mt-1 counter-animation">{{ stats.normal }}</div>
</div>
<div class="bg-green-600/10 border border-green-500/30 rounded-lg p-3 transform transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-green-500/10">
<div class="text-sm text-green-300">已处理</div>
<div class="text-2xl font-bold text-green-400 mt-1 counter-animation">{{ stats.resolved }}</div>
</div>
</div>
<!-- 预警类型饼图 -->
<!-- <div class="mt-4 h-40">-->
<!-- <canvas ref="eventTypePieChart"></canvas>-->
<!-- </div>-->
</div>
<!-- 预警列表 -->
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg flex-1 overflow-hidden flex flex-col transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<div class="flex justify-between items-center mb-3">
<h2 class="text-lg font-semibold flex items-center">
<i class="fa fa-list-alt text-blue-400 mr-2"></i>
预警列表
</h2>
<div class="relative">
<input
type="text"
placeholder="搜索预警..."
class="bg-slate-700/50 text-sm rounded-lg px-3 py-1.5 w-40 focus:outline-none focus:ring-1 focus:ring-blue-500 transition-all duration-300 focus:w-48"
>
<i class="fa fa-search absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm"></i>
</div>
</div>
<div class="overflow-y-auto flex-1 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent">
<div
v-for="event in events"
:key="event.id"
@click="selectEvent(event)"
class="p-3 rounded-lg border border-slate-700 mb-2 cursor-pointer transition-all duration-200 hover:border-blue-500/50 hover:bg-slate-700/30 flex items-center gap-3 transform hover:translate-x-1"
:class="{ 'bg-blue-600/20 border-blue-500/50': selectedEvent?.id === event.id }"
>
<div class="w-2 h-2 rounded-full" :class="eventStatusColor(event.status)"></div>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">{{ event.title }}</div>
<div class="text-xs text-slate-400 mt-0.5">{{ event.location }} · {{ formatTime(event.time) }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full" :class="eventStatusBadgeClass(event.status)">
{{ eventStatusText(event.status) }}
</span>
</div>
</div>
</div>
</section>
<!-- 中间地图区域 -->
<section class="flex-1 flex flex-col gap-4">
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg flex-1 relative overflow-hidden transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<h2 class="text-lg font-semibold mb-3 flex items-center relative z-10">
<i class="fa fa-map-marker text-blue-400 mr-2"></i>
预警分布地图
</h2>
<!-- 模拟地图 -->
<div class="absolute inset-0 bg-slate-900/50 rounded-lg overflow-hidden">
<div class="w-full h-full bg-[url('https://picsum.photos/id/1015/1200/800')] opacity-20 bg-cover bg-center"></div>
<!-- 网格线 -->
<div class="absolute inset-0 grid grid-cols-8 grid-rows-6">
<div v-for="i in 48" :key="i" class="border border-slate-700/30"></div>
</div>
<!-- 预警标记点 -->
<div
v-for="event in events"
:key="event.id"
:style="{ left: `${event.position.x}%`, top: `${event.position.y}%` }"
class="absolute transform -translate-x-1/2 -translate-y-1/2 cursor-pointer group"
@click="selectEvent(event)"
>
<div
class="w-3 h-3 rounded-full"
:class="eventStatusColor(event.status)"
></div>
<div class="absolute -top-10 left-1/2 transform -translate-x-1/2 bg-slate-800 px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-10">
{{ event.title }}
</div>
<div
class="absolute w-6 h-6 rounded-full animate-ping"
:class="eventStatusPingClass(event.status)"
style="animation-duration: 2s"
></div>
</div>
<!-- 选中预警的高亮 -->
<div
v-if="selectedEvent"
:style="{ left: `${selectedEvent.position.x}%`, top: `${selectedEvent.position.y}%` }"
class="absolute transform -translate-x-1/2 -translate-y-1/2"
>
<div class="w-4 h-4 rounded-full bg-blue-500 border-2 border-white shadow-lg"></div>
<div class="absolute w-8 h-8 rounded-full border-2 border-blue-500 animate-ping opacity-75" style="animation-duration: 1.5s"></div>
</div>
</div>
<!-- 地图控制 -->
<div class="absolute bottom-4 right-4 flex flex-col gap-2 z-10">
<button class="bg-slate-800/80 hover:bg-slate-700 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300 hover:scale-110">
<i class="fa fa-plus text-sm"></i>
</button>
<button class="bg-slate-800/80 hover:bg-slate-700 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300 hover:scale-110">
<i class="fa fa-minus text-sm"></i>
</button>
<button class="bg-slate-800/80 hover:bg-slate-700 w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-300 hover:scale-110">
<i class="fa fa-location-arrow text-sm"></i>
</button>
</div>
</div>
<!-- 处理时间和图表区域 -->
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg h-64 transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<h2 class="text-lg font-semibold mb-3 flex items-center">
<i class="fa fa-line-chart text-blue-400 mr-2"></i>
预警处理数据分析
</h2>
<div class="grid grid-cols-2 gap-4 h-48">
<!-- 处理时间趋势折线图 -->
<div>
<h3 class="text-sm text-slate-300 mb-2">平均处理时间(分钟)</h3>
<div class="h-36">
<canvas ref="processingTimeChart"></canvas>
</div>
</div>
<!-- 每日预警数量柱状图 -->
<div>
<h3 class="text-sm text-slate-300 mb-2">每日预警数量</h3>
<div class="h-36">
<canvas ref="dailyEventsChart"></canvas>
</div>
</div>
</div>
</div>
</section>
<!-- 右侧处理状态和操作 -->
<section class="w-1/4 flex flex-col gap-4">
<!-- 预警详情 -->
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<h2 class="text-lg font-semibold mb-3 flex items-center">
<i class="fa fa-info-circle text-blue-400 mr-2"></i>
预警详情
</h2>
<div v-if="selectedEvent" class="space-y-3 animate-fadeIn">
<div>
<div class="text-xs text-slate-400">预警标题</div>
<div class="font-medium">{{ selectedEvent.title }}</div>
</div>
<div>
<div class="text-xs text-slate-400">预警类型</div>
<div class="font-medium">{{ selectedEvent.type }}</div>
</div>
<div>
<div class="text-xs text-slate-400">发生时间</div>
<div class="font-medium">{{ formatDateTime(selectedEvent.time) }}</div>
</div>
<div>
<div class="text-xs text-slate-400">位置信息</div>
<div class="font-medium">{{ selectedEvent.location }}</div>
</div>
<div>
<div class="text-xs text-slate-400">预警描述</div>
<div class="text-sm text-slate-300 bg-slate-700/30 p-2 rounded-lg mt-1 hover:bg-slate-700/50 transition-colors duration-300">
{{ selectedEvent.description }}
</div>
</div>
<div>
<div class="text-xs text-slate-400">当前状态</div>
<div class="flex items-center mt-1">
<span class="text-sm px-2 py-0.5 rounded-full" :class="eventStatusBadgeClass(selectedEvent.status)">
{{ eventStatusText(selectedEvent.status) }}
</span>
<span class="ml-2 text-xs text-slate-400">
更新于: {{ formatTime(selectedEvent.updateTime) }}
</span>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center h-48 text-slate-500">
<div class="text-center">
<i class="fa fa-hand-pointer-o text-2xl mb-2 animate-pulse"></i>
<p>请从左侧列表或地图中选择一个预警</p>
</div>
</div>
</div>
<!-- 处理状态和操作 -->
<div class="bg-slate-800/60 backdrop-blur-sm rounded-xl p-4 border border-slate-700/50 shadow-lg flex-1 flex flex-col transform transition-all duration-300 hover:shadow-blue-500/10 hover:border-blue-500/20">
<h2 class="text-lg font-semibold mb-3 flex items-center">
<i class="fa fa-cogs text-blue-400 mr-2"></i>
处理操作
</h2>
<div v-if="selectedEvent" class="flex-1 flex flex-col animate-fadeIn">
<!-- 处理进度 -->
<div class="mb-4">
<div class="text-sm font-medium mb-2">处理进度</div>
<div class="relative pt-1">
<div class="flex mb-2 items-center justify-between">
<div>
<span class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-blue-200/10">
处理进度
</span>
</div>
<div class="text-right">
<span class="text-xs font-semibold inline-block text-blue-400">
{{ processingProgress }}%
</span>
</div>
</div>
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-slate-700/50">
<div
:style="{ width: `${processingProgress}%` }"
class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-gradient-to-r from-blue-500 to-cyan-400 transition-all duration-1000 ease-out"
></div>
</div>
</div>
</div>
<!-- 处理记录 -->
<div class="mb-4 flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-700">
<div class="text-sm font-medium mb-2">处理记录</div>
<div class="space-y-3">
<div v-for="record in selectedEvent.processingRecords" :key="record.id" class="flex gap-2 transform transition-all duration-300 hover:translate-x-1">
<div class="mt-0.5 w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center flex-shrink-0">
<i class="fa fa-check text-xs text-slate-300"></i>
</div>
<div>
<div class="text-sm">{{ record.action }}</div>
<div class="text-xs text-slate-400 mt-0.5">
{{ record.user }} · {{ formatTime(record.time) }}
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-2 pt-2 border-t border-slate-700/50">
<div class="flex gap-2">
<button
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-1 text-sm transform hover:scale-[1.02] active:scale-[0.98]"
:disabled="selectedEvent.status === 'resolved'"
@click="markAsResolved"
>
<i class="fa fa-check"></i>
<span>标记为已处理</span>
</button>
<button class="bg-slate-700 hover:bg-slate-600 text-white p-2 rounded-lg transition-all duration-300 transform hover:scale-110 active:scale-90">
<i class="fa fa-comments"></i>
</button>
</div>
<div class="flex gap-2">
<button class="flex-1 bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-1 text-sm transform hover:scale-[1.02] active:scale-[0.98]">
<i class="fa fa-user-plus"></i>
<span>指派处理人</span>
</button>
<button class="flex-1 bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-1 text-sm transform hover:scale-[1.02] active:scale-[0.98]">
<i class="fa fa-file-text-o"></i>
<span>生成报告</span>
</button>
</div>
<div>
<textarea
placeholder="输入处理备注..."
class="w-full bg-slate-700/50 border border-slate-600 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 resize-none h-16 transition-all duration-300 focus:border-blue-500/50"
v-model="processingNote"
></textarea>
</div>
</div>
</div>
<div v-else class="flex-1 flex items-center justify-center text-slate-500">
<div class="text-center">
<i class="fa fa-wrench text-2xl mb-2 animate-pulse"></i>
<p>选择预警后显示处理操作</p>
</div>
</div>
</div>
</section>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import {useNotifyStore } from '#/store';
// 处理备注
const processingNote = ref('');
const list=useNotifyStore().sseList;
list.map(item=>{
})
// 模拟预警数据
const events = ref([
{
id: 1,
title: "设备故障报警",
type: "设备故障",
status: "emergency",
time: "2023-10-15T08:23:45",
updateTime: "2023-10-15T08:45:12",
location: "一号厂房A区",
description: "流水线三号设备突然停机显示电机故障代码E109需要紧急处理以避免生产线中断。",
position: { x: 35, y: 40 },
processingRecords: [
{ id: 1, action: "接收到报警信息", user: "系统自动", time: "2023-10-15T08:23:45" },
{ id: 2, action: "指派给维修组", user: "张经理", time: "2023-10-15T08:25:10" }
]
},
{
id: 2,
title: "安全门异常开启",
type: "安全预警",
status: "important",
time: "2023-10-15T07:15:30",
updateTime: "2023-10-15T07:30:22",
location: "二号仓库入口",
description: "非工作时间安全门被异常开启,系统已自动记录并触发警报,需检查是否有异常进入。",
position: { x: 65, y: 30 },
processingRecords: [
{ id: 1, action: "接收到报警信息", user: "系统自动", time: "2023-10-15T07:15:30" },
{ id: 2, action: "安保人员已前往查看", user: "李主管", time: "2023-10-15T07:17:05" },
{ id: 3, action: "初步检查未发现异常", user: "王保安", time: "2023-10-15T07:30:22" }
]
},
{
id: 3,
title: "温湿度超标",
type: "环境异常",
status: "normal",
time: "2023-10-15T09:40:12",
updateTime: "2023-10-15T09:40:12",
location: "实验室B区",
description: "实验室B区温湿度超出正常范围当前温度26℃湿度65%,需调整空调系统。",
position: { x: 45, y: 60 },
processingRecords: [
{ id: 1, action: "接收到报警信息", user: "系统自动", time: "2023-10-15T09:40:12" }
]
},
{
id: 4,
title: "物料短缺预警",
type: "物料管理",
status: "normal",
time: "2023-10-15T10:15:22",
updateTime: "2023-10-15T10:15:22",
location: "原料仓库",
description: "A类原材料库存低于警戒线剩余数量约可维持2天生产请及时采购补充。",
position: { x: 25, y: 70 },
processingRecords: [
{ id: 1, action: "系统自动发出预警", user: "系统自动", time: "2023-10-15T10:15:22" }
]
},
{
id: 5,
title: "网络中断恢复",
type: "网络问题",
status: "resolved",
time: "2023-10-15T06:30:15",
updateTime: "2023-10-15T07:05:33",
location: "三号车间",
description: "三号车间网络中断,影响设备数据上传,技术人员已修复,网络恢复正常。",
position: { x: 75, y: 55 },
processingRecords: [
{ id: 1, action: "检测到网络中断", user: "系统自动", time: "2023-10-15T06:30:15" },
{ id: 2, action: "技术人员前往处理", user: "赵主管", time: "2023-10-15T06:35:40" },
{ id: 3, action: "网络已恢复正常", user: "孙工", time: "2023-10-15T07:05:33" }
]
}
]);
// 选中的预警
const selectedEvent = ref(null);
// 统计数据
const stats = ref({
emergency: 1,
important: 1,
normal: 2,
resolved: 1
});
// 当前时间和最后更新时间
const currentTime = ref("");
const lastUpdateTime = ref("2023-10-15 10:30:45");
// 图表引用
const eventTypePieChart = ref(null);
const processingTimeChart = ref(null);
const dailyEventsChart = ref(null);
// 处理进度(根据预警状态计算)
const processingProgress = computed(() => {
if (!selectedEvent.value) return 0;
switch(selectedEvent.value.status) {
case 'emergency': return 30;
case 'important': return 50;
case 'normal': return 20;
case 'resolved': return 100;
default: return 0;
}
});
// 选择预警
const selectEvent = (event) => {
selectedEvent.value = event;
};
// 标记为已处理
const markAsResolved = () => {
if (selectedEvent.value) {
// 更新预警状态
selectedEvent.value.status = 'resolved';
selectedEvent.value.updateTime = new Date().toISOString();
// 添加处理记录
if (processingNote.value.trim()) {
selectedEvent.value.processingRecords.push({
id: selectedEvent.value.processingRecords.length + 1,
action: `标记为已处理: ${processingNote.value.trim()}`,
user: "当前操作员",
time: new Date().toISOString()
});
processingNote.value = '';
} else {
selectedEvent.value.processingRecords.push({
id: selectedEvent.value.processingRecords.length + 1,
action: "标记为已处理",
user: "当前操作员",
time: new Date().toISOString()
});
}
// 更新统计数据
stats.value.resolved++;
if (stats.value.emergency > 0) stats.value.emergency--;
else if (stats.value.important > 0) stats.value.important--;
else if (stats.value.normal > 0) stats.value.normal--;
// 重新绘制图表
drawAllCharts();
}
};
// 格式化时间
const formatTime = (timeString) => {
const date = new Date(timeString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// 格式化日期时间
const formatDateTime = (timeString) => {
const date = new Date(timeString);
return date.toLocaleString();
};
// 预警状态文本
const eventStatusText = (status) => {
const statusMap = {
'emergency': '紧急',
'important': '重要',
'normal': '一般',
'resolved': '已处理'
};
return statusMap[status] || '未知';
};
// 预警状态颜色
const eventStatusColor = (status) => {
const colorMap = {
'emergency': 'bg-red-500',
'important': 'bg-orange-500',
'normal': 'bg-yellow-500',
'resolved': 'bg-green-500'
};
return colorMap[status] || 'bg-slate-500';
};
// 预警状态标记颜色(脉冲效果)
const eventStatusPingClass = (status) => {
const colorMap = {
'emergency': 'bg-red-500/30',
'important': 'bg-orange-500/30',
'normal': 'bg-yellow-500/30',
'resolved': 'bg-green-500/30'
};
return colorMap[status] || 'bg-slate-500/30';
};
// 预警状态徽章样式
const eventStatusBadgeClass = (status) => {
const classMap = {
'emergency': 'bg-red-500/20 text-red-400 border border-red-500/30',
'important': 'bg-orange-500/20 text-orange-400 border border-orange-500/30',
'normal': 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
'resolved': 'bg-green-500/20 text-green-400 border border-green-500/30'
};
return classMap[status] || 'bg-slate-500/20 text-slate-400 border border-slate-500/30';
};
// 更新当前时间
const updateCurrentTime = () => {
const now = new Date();
currentTime.value = now.toLocaleString();
};
// 绘制饼图 - 使用原生Canvas API
const drawPieChart = (canvas, data) => {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清除画布
ctx.clearRect(0, 0, width, height);
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 3;
const total = data.values.reduce((sum, value) => sum + value, 0);
let startAngle = 0;
data.values.forEach((value, index) => {
const sliceAngle = 2 * Math.PI * (value / total);
const color = data.colors[index];
// 绘制扇形
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
// 添加边框
ctx.strokeStyle = 'rgba(30, 41, 59, 0.8)';
ctx.lineWidth = 1;
ctx.stroke();
// 计算标签位置
const labelAngle = startAngle + sliceAngle / 2;
const labelRadius = radius + 15;
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
// 绘制标签
ctx.fillStyle = '#e2e8f0';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${data.labels[index]}: ${value}`, labelX, labelY);
startAngle += sliceAngle;
});
};
// 绘制折线图 - 使用原生Canvas API
const drawLineChart = (canvas, data) => {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 边距
const margin = { top: 10, right: 10, bottom: 20, left: 30 };
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// 找到数据范围
const maxValue = Math.max(...data.values);
const minValue = 0;
const valueRange = maxValue - minValue;
// 计算X轴和Y轴的比例
const xScale = chartWidth / (data.values.length - 1);
const yScale = chartHeight / valueRange;
// 绘制坐标轴
ctx.beginPath();
ctx.strokeStyle = 'rgba(148, 163, 184, 0.5)';
ctx.lineWidth = 1;
// X轴
ctx.moveTo(margin.left, margin.top + chartHeight);
ctx.lineTo(margin.left + chartWidth, margin.top + chartHeight);
ctx.stroke();
// Y轴
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + chartHeight);
ctx.stroke();
// 绘制网格线
ctx.strokeStyle = 'rgba(148, 163, 184, 0.1)';
// 水平网格线
const yGridCount = 5;
for (let i = 0; i <= yGridCount; i++) {
const y = margin.top + chartHeight - (i * chartHeight / yGridCount);
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + chartWidth, y);
ctx.stroke();
// Y轴刻度
ctx.fillStyle = 'rgba(148, 163, 184, 0.7)';
ctx.font = '8px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(Math.round(i * valueRange / yGridCount), margin.left - 5, y);
}
// 绘制数据线
ctx.beginPath();
data.values.forEach((value, index) => {
const x = margin.left + index * xScale;
const y = margin.top + chartHeight - (value - minValue) * yScale;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.fillStyle = data.lineColor;
ctx.beginPath();
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fill();
// 绘制白色边框
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
});
// 绘制线条
ctx.strokeStyle = data.lineColor;
ctx.lineWidth = 2;
ctx.stroke();
// 填充区域
ctx.lineTo(margin.left + (data.values.length - 1) * xScale, margin.top + chartHeight);
ctx.lineTo(margin.left, margin.top + chartHeight);
ctx.closePath();
ctx.fillStyle = data.areaColor;
ctx.fill();
// 绘制X轴标签
data.labels.forEach((label, index) => {
const x = margin.left + index * xScale;
ctx.fillStyle = 'rgba(148, 163, 184, 0.7)';
ctx.font = '8px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x, margin.top + chartHeight + 5);
});
};
// 绘制柱状图 - 使用原生Canvas API
const drawBarChart = (canvas, data) => {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 边距
const margin = { top: 10, right: 10, bottom: 20, left: 30 };
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// 找到数据范围
const maxValue = Math.max(...data.values) * 1.1; // 留10%的余量
const minValue = 0;
const valueRange = maxValue - minValue;
// 计算X轴和Y轴的比例
const barWidth = chartWidth / (data.values.length * 2);
const xScale = chartWidth / (data.values.length);
const yScale = chartHeight / valueRange;
// 绘制坐标轴
ctx.beginPath();
ctx.strokeStyle = 'rgba(148, 163, 184, 0.5)';
ctx.lineWidth = 1;
// X轴
ctx.moveTo(margin.left, margin.top + chartHeight);
ctx.lineTo(margin.left + chartWidth, margin.top + chartHeight);
ctx.stroke();
// Y轴
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + chartHeight);
ctx.stroke();
// 绘制网格线
ctx.strokeStyle = 'rgba(148, 163, 184, 0.1)';
// 水平网格线
const yGridCount = 5;
for (let i = 0; i <= yGridCount; i++) {
const y = margin.top + chartHeight - (i * chartHeight / yGridCount);
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + chartWidth, y);
ctx.stroke();
// Y轴刻度
ctx.fillStyle = 'rgba(148, 163, 184, 0.7)';
ctx.font = '8px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(Math.round(i * valueRange / yGridCount), margin.left - 5, y);
}
// 绘制柱子
data.values.forEach((value, index) => {
const x = margin.left + index * xScale + (xScale - barWidth) / 2;
const barHeight = (value - minValue) * yScale;
const y = margin.top + chartHeight - barHeight;
// 绘制柱子
ctx.fillStyle = data.barColor;
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制柱子顶部的值
ctx.fillStyle = 'rgba(226, 232, 240, 0.9)';
ctx.font = '8px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(value, x + barWidth / 2, y - 3);
});
// 绘制X轴标签
data.labels.forEach((label, index) => {
const x = margin.left + index * xScale + xScale / 2;
ctx.fillStyle = 'rgba(148, 163, 184, 0.7)';
ctx.font = '8px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x, margin.top + chartHeight + 5);
});
};
// 绘制所有图表
const drawAllCharts = () => {
// 设置Canvas尺寸考虑高DPI屏幕
const setupCanvas = (canvas) => {
if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
};
// 预警类型饼图数据
const pieData = {
labels: ['设备故障', '安全预警', '环境异常', '物料管理', '网络问题'],
values: [1, 1, 1, 1, 1],
colors: [
'#3b82f6', // 蓝色
'#f97316', // 橙色
'#eab308', // 黄色
'#10b981', // 绿色
'#8b5cf6' // 紫色
]
};
// 处理时间折线图数据
const lineData = {
labels: ['1日', '2日', '3日', '4日', '5日', '6日', '7日'],
values: [25, 32, 28, 45, 36, 22, 30],
lineColor: '#3b82f6',
areaColor: 'rgba(59, 130, 246, 0.1)'
};
// 每日预警数量柱状图数据
const barData = {
labels: ['1日', '2日', '3日', '4日', '5日', '6日', '7日'],
values: [8, 12, 5, 15, 7, 10, 5],
barColor: '#06b6d4'
};
// 设置并绘制图表
// setupCanvas(eventTypePieChart.value);
setupCanvas(processingTimeChart.value);
setupCanvas(dailyEventsChart.value);
// drawPieChart(eventTypePieChart.value, pieData);
drawLineChart(processingTimeChart.value, lineData);
drawBarChart(dailyEventsChart.value, barData);
};
// 页面加载时初始化
onMounted(() => {
// 默认选择第一个预警
if (events.value.length > 0) {
selectedEvent.value = events.value[0];
}
// 初始化时间
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 初始化图表
drawAllCharts();
// 监听窗口大小变化,重新绘制图表
window.addEventListener('resize', drawAllCharts);
});
</script>