This commit is contained in:
@@ -27,6 +27,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@dataview/datav-vue3": "0.0.0-test.1672506674342",
|
||||
"@jiaminghi/charts": "^0.2.18",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@tinymce/tinymce-vue": "^6.0.1",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
|
@@ -0,0 +1,48 @@
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 大屏接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 访客
|
||||
*/
|
||||
export function visitir() {
|
||||
return requestClient.get('/property/cockpit/visitor');
|
||||
}
|
||||
|
||||
/**
|
||||
*费用
|
||||
*/
|
||||
export function expenses() {
|
||||
return requestClient.get('/property/cockpit/expenses');
|
||||
}
|
||||
|
||||
/**
|
||||
* 物业人员配置
|
||||
*/
|
||||
export function propertyPerson() {
|
||||
return requestClient.get('/property/cockpit/propertyperson');
|
||||
}
|
||||
|
||||
/**
|
||||
* sos报警
|
||||
*/
|
||||
export function sos() {
|
||||
return requestClient.get('/property/cockpit/sos');
|
||||
}
|
||||
|
||||
/**
|
||||
* sos报警记录
|
||||
*/
|
||||
export function soslist() {
|
||||
return requestClient.get('/property/cockpit/soslist');
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单
|
||||
*/
|
||||
export function workcount() {
|
||||
return requestClient.get('/property/cockpit/workcount');
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
@@ -43,6 +45,7 @@ async function bootstrap(namespace: string) {
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
@@ -59,6 +62,7 @@ async function bootstrap(namespace: string) {
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
@@ -146,7 +146,7 @@ watch(
|
||||
:avatar
|
||||
:menus
|
||||
:text="userStore.userInfo?.realName"
|
||||
description="ann.vben@gmail.com"
|
||||
:description="userStore.userInfo?.roles[0]"
|
||||
tag-text="Pro"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
|
@@ -125,8 +125,18 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
title: '物业大屏',
|
||||
requiresAuth: true, // 如果需要登录验证
|
||||
},
|
||||
}, {
|
||||
component: () => import('#/views/screen/security/index.vue'),
|
||||
},
|
||||
// {
|
||||
// component: () => import('#/views/screen/security/index.vue'),
|
||||
// name: 'security',
|
||||
// path: '/security',
|
||||
// meta: {
|
||||
// title: '安防大屏',
|
||||
// requiresAuth: true, // 如果需要登录验证
|
||||
// },
|
||||
// },
|
||||
{
|
||||
component: () => import('#/views/cockpit/security/index.vue'),
|
||||
name: 'security',
|
||||
path: '/security',
|
||||
meta: {
|
||||
@@ -143,15 +153,15 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
requiresAuth: true, // 如果需要登录验证
|
||||
},
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/screen/security/index.vue'),
|
||||
name: 'security',
|
||||
path: '/security',
|
||||
meta: {
|
||||
title: '安防大屏',
|
||||
requiresAuth: true, // 如果需要登录验证
|
||||
},
|
||||
},
|
||||
// {
|
||||
// component: () => import('#/views/screen/security/index.vue'),
|
||||
// name: 'security',
|
||||
// path: '/security',
|
||||
// meta: {
|
||||
// title: '安防大屏',
|
||||
// requiresAuth: true, // 如果需要登录验证
|
||||
// },
|
||||
// },
|
||||
{
|
||||
component: () => import('#/views/screen/digitalIntelligence/index.vue'),
|
||||
name: 'digitalIntelligence',
|
||||
|
@@ -24,7 +24,7 @@ export const useNotifyStore = defineStore(
|
||||
* return才会被持久化 存储全部消息
|
||||
*/
|
||||
const notificationList = ref<NotificationItem[]>([]);
|
||||
|
||||
const sseList = ref<string[]>(["111"]);
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => {
|
||||
return userStore.userInfo?.userId || '0';
|
||||
@@ -65,24 +65,33 @@ export const useNotifyStore = defineStore(
|
||||
if (!message) return;
|
||||
console.log(`接收到消息: ${message}`);
|
||||
|
||||
notification.success({
|
||||
description: message,
|
||||
duration: 3,
|
||||
message: $t('component.notice.received'),
|
||||
});
|
||||
try {
|
||||
// 尝试解析JSON
|
||||
const obj = JSON.parse(message);
|
||||
// 检查解析结果是否为对象且不为null
|
||||
if (obj.getType() ==="yvjin"){
|
||||
sseList.value.join(message)
|
||||
}
|
||||
} catch (e) {
|
||||
notification.success({
|
||||
description: message,
|
||||
duration: 3,
|
||||
message: $t('component.notice.received'),
|
||||
});
|
||||
|
||||
notificationList.value.unshift({
|
||||
// avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像
|
||||
avatar: SvgMessageUrl,
|
||||
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
isRead: false,
|
||||
message,
|
||||
title: $t('component.notice.title'),
|
||||
userId: userId.value,
|
||||
});
|
||||
notificationList.value.unshift({
|
||||
// avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像
|
||||
avatar: SvgMessageUrl,
|
||||
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
isRead: false,
|
||||
message,
|
||||
title: $t('component.notice.title'),
|
||||
userId: userId.value,
|
||||
});
|
||||
|
||||
// 需要手动置空 vue3在值相同时不会触发watch
|
||||
data.value = null;
|
||||
// 需要手动置空 vue3在值相同时不会触发watch
|
||||
data.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,6 +105,10 @@ export const useNotifyStore = defineStore(
|
||||
item.isRead = true;
|
||||
});
|
||||
}
|
||||
function getsseList(){
|
||||
console.log(sseList.value)
|
||||
return sseList.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单条消息已读
|
||||
@@ -134,6 +147,8 @@ export const useNotifyStore = defineStore(
|
||||
$reset,
|
||||
clearAllMessage,
|
||||
notificationList,
|
||||
sseList,
|
||||
getsseList,
|
||||
notifications,
|
||||
setAllRead,
|
||||
setRead,
|
||||
|
@@ -1,11 +1,894 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import {useNotifyStore } from '#/store';
|
||||
// 处理备注
|
||||
const processingNote = ref('');
|
||||
|
||||
</style>
|
||||
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>
|
||||
|
@@ -750,23 +750,23 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.left {
|
||||
display: flex;
|
||||
width: 18.3125rem;
|
||||
.left-first {
|
||||
padding-left: 2.3125rem;
|
||||
font-size: 1.875rem;
|
||||
width: 10.5rem;
|
||||
color: #ffffff;
|
||||
.left{
|
||||
display: flex;
|
||||
width: 14.3125rem;
|
||||
.left-first{
|
||||
padding-left: 2.3125rem;
|
||||
padding-right: 3.5rem;
|
||||
font-size: 1.875rem;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.left-second{
|
||||
width: 6.5rem;
|
||||
font-family: ShiShangZhongHeiJianTi;
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
.left-second {
|
||||
width: 6.5rem;
|
||||
font-family: ShiShangZhongHeiJianTi;
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.center{
|
||||
font-size: 1.9rem;
|
||||
color: #fff;
|
||||
|
@@ -27,7 +27,8 @@ export default defineConfig(async () => {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://127.0.0.1:8080',
|
||||
// target: 'http://127.0.0.1:8080',
|
||||
target: 'http://183.230.235.66:11010/api',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"name": "vben-admin-monorepo",
|
||||
"version": "5.5.6",
|
||||
@@ -59,6 +58,7 @@
|
||||
"catalog": "pnpx codemod pnpm/catalog"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dataview/datav-vue3": "0.0.0-test.1672506674342",
|
||||
"@changesets/changelog-github": "catalog:",
|
||||
"@changesets/cli": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
@@ -118,6 +118,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@dataview/datav-vue3": "0.0.0-test.1672506674342",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"postcss-antd-fixes": "^0.2.0"
|
||||
}
|
||||
|
@@ -68,12 +68,12 @@ defineOptions({
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
avatar: '',
|
||||
description: '',
|
||||
enableShortcutKey: true,
|
||||
menus: () => [],
|
||||
showShortcutKey: true,
|
||||
tagText: '',
|
||||
text: '',
|
||||
description: '',
|
||||
trigger: 'click',
|
||||
hoverDelay: 500,
|
||||
});
|
||||
@@ -168,6 +168,7 @@ if (enableShortcutKey.value) {
|
||||
v-if="preferences.widget.lockScreen"
|
||||
:avatar="avatar"
|
||||
:text="text"
|
||||
:description="description"
|
||||
@submit="handleSubmitLock"
|
||||
/>
|
||||
|
||||
@@ -214,7 +215,7 @@ if (enableShortcutKey.value) {
|
||||
</Badge>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -26,6 +26,10 @@ interface BasicUserInfo {
|
||||
* 用户名
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface AccessState {
|
||||
|
@@ -47,8 +47,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
]);
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
|
Reference in New Issue
Block a user