安防大屏
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

This commit is contained in:
2025-08-16 17:58:59 +08:00
parent 7edbeca307
commit 97dc93f3aa
14 changed files with 1027 additions and 56 deletions

View File

@@ -27,6 +27,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@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", "@tinymce/tinymce-vue": "^6.0.1",
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",

View File

@@ -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');
}

View File

@@ -17,6 +17,8 @@ import { initSetupVbenForm } from './adapter/form';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
async function bootstrap(namespace: string) { async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
@@ -43,6 +45,7 @@ async function bootstrap(namespace: string) {
spinning: 'spinning', spinning: 'spinning',
}); });
// 国际化 i18n 配置 // 国际化 i18n 配置
await setupI18n(app); await setupI18n(app);
@@ -59,6 +62,7 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion'); const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); app.use(MotionPlugin);

View File

@@ -146,7 +146,7 @@ watch(
:avatar :avatar
:menus :menus
:text="userStore.userInfo?.realName" :text="userStore.userInfo?.realName"
description="ann.vben@gmail.com" :description="userStore.userInfo?.roles[0]"
tag-text="Pro" tag-text="Pro"
@logout="handleLogout" @logout="handleLogout"
/> />

View File

@@ -1,6 +1,7 @@
import { initPreferences } from '@vben/preferences'; import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils'; import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences'; import { overridesPreferences } from './preferences';
/** /**
* 应用初始化完成之后再进行页面加载渲染 * 应用初始化完成之后再进行页面加载渲染
*/ */

View File

@@ -125,8 +125,18 @@ const coreRoutes: RouteRecordRaw[] = [
title: '物业大屏', title: '物业大屏',
requiresAuth: true, // 如果需要登录验证 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', name: 'security',
path: '/security', path: '/security',
meta: { meta: {
@@ -143,15 +153,15 @@ const coreRoutes: RouteRecordRaw[] = [
requiresAuth: true, // 如果需要登录验证 requiresAuth: true, // 如果需要登录验证
}, },
}, },
{ // {
component: () => import('#/views/screen/security/index.vue'), // component: () => import('#/views/screen/security/index.vue'),
name: 'security', // name: 'security',
path: '/security', // path: '/security',
meta: { // meta: {
title: '安防大屏', // title: '安防大屏',
requiresAuth: true, // 如果需要登录验证 // requiresAuth: true, // 如果需要登录验证
}, // },
}, // },
{ {
component: () => import('#/views/screen/digitalIntelligence/index.vue'), component: () => import('#/views/screen/digitalIntelligence/index.vue'),
name: 'digitalIntelligence', name: 'digitalIntelligence',

View File

@@ -24,7 +24,7 @@ export const useNotifyStore = defineStore(
* return才会被持久化 存储全部消息 * return才会被持久化 存储全部消息
*/ */
const notificationList = ref<NotificationItem[]>([]); const notificationList = ref<NotificationItem[]>([]);
const sseList = ref<string[]>(["111"]);
const userStore = useUserStore(); const userStore = useUserStore();
const userId = computed(() => { const userId = computed(() => {
return userStore.userInfo?.userId || '0'; return userStore.userInfo?.userId || '0';
@@ -65,24 +65,33 @@ export const useNotifyStore = defineStore(
if (!message) return; if (!message) return;
console.log(`接收到消息: ${message}`); console.log(`接收到消息: ${message}`);
notification.success({ try {
description: message, // 尝试解析JSON
duration: 3, const obj = JSON.parse(message);
message: $t('component.notice.received'), // 检查解析结果是否为对象且不为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({ notificationList.value.unshift({
// avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像 // avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像
avatar: SvgMessageUrl, avatar: SvgMessageUrl,
date: dayjs().format('YYYY-MM-DD HH:mm:ss'), date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
isRead: false, isRead: false,
message, message,
title: $t('component.notice.title'), title: $t('component.notice.title'),
userId: userId.value, userId: userId.value,
}); });
// 需要手动置空 vue3在值相同时不会触发watch // 需要手动置空 vue3在值相同时不会触发watch
data.value = null; data.value = null;
}
}); });
} }
@@ -96,6 +105,10 @@ export const useNotifyStore = defineStore(
item.isRead = true; item.isRead = true;
}); });
} }
function getsseList(){
console.log(sseList.value)
return sseList.value
}
/** /**
* 设置单条消息已读 * 设置单条消息已读
@@ -134,6 +147,8 @@ export const useNotifyStore = defineStore(
$reset, $reset,
clearAllMessage, clearAllMessage,
notificationList, notificationList,
sseList,
getsseList,
notifications, notifications,
setAllRead, setAllRead,
setRead, setRead,

View File

@@ -1,11 +1,894 @@
<script setup lang="ts">
</script>
<template> <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> </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>

View File

@@ -750,23 +750,23 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.left { .left{
display: flex; display: flex;
width: 18.3125rem; width: 14.3125rem;
.left-first { .left-first{
padding-left: 2.3125rem; padding-left: 2.3125rem;
font-size: 1.875rem; padding-right: 3.5rem;
width: 10.5rem; font-size: 1.875rem;
color: #ffffff; 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{ .center{
font-size: 1.9rem; font-size: 1.9rem;
color: #fff; color: #fff;

View File

@@ -27,7 +27,8 @@ export default defineConfig(async () => {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址 // 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, ws: true,
}, },
}, },

View File

@@ -1,4 +1,3 @@
{ {
"name": "vben-admin-monorepo", "name": "vben-admin-monorepo",
"version": "5.5.6", "version": "5.5.6",
@@ -59,6 +58,7 @@
"catalog": "pnpx codemod pnpm/catalog" "catalog": "pnpx codemod pnpm/catalog"
}, },
"devDependencies": { "devDependencies": {
"@dataview/datav-vue3": "0.0.0-test.1672506674342",
"@changesets/changelog-github": "catalog:", "@changesets/changelog-github": "catalog:",
"@changesets/cli": "catalog:", "@changesets/cli": "catalog:",
"@playwright/test": "catalog:", "@playwright/test": "catalog:",
@@ -118,6 +118,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@dataview/datav-vue3": "0.0.0-test.1672506674342",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"postcss-antd-fixes": "^0.2.0" "postcss-antd-fixes": "^0.2.0"
} }

View File

@@ -68,12 +68,12 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
avatar: '', avatar: '',
description: '',
enableShortcutKey: true, enableShortcutKey: true,
menus: () => [], menus: () => [],
showShortcutKey: true, showShortcutKey: true,
tagText: '', tagText: '',
text: '', text: '',
description: '',
trigger: 'click', trigger: 'click',
hoverDelay: 500, hoverDelay: 500,
}); });
@@ -168,6 +168,7 @@ if (enableShortcutKey.value) {
v-if="preferences.widget.lockScreen" v-if="preferences.widget.lockScreen"
:avatar="avatar" :avatar="avatar"
:text="text" :text="text"
:description="description"
@submit="handleSubmitLock" @submit="handleSubmitLock"
/> />
@@ -214,7 +215,7 @@ if (enableShortcutKey.value) {
</Badge> </Badge>
</slot> </slot>
</div> </div>
<div class="text-muted-foreground text-xs font-normal"> <div class="text-muted-foreground text-xs font-normal">
{{ description }} {{ description }}
</div> </div>
</div> </div>

View File

@@ -26,6 +26,10 @@ interface BasicUserInfo {
* 用户名 * 用户名
*/ */
username: string; username: string;
/**
* 邮箱
*/
email: string;
} }
interface AccessState { interface AccessState {

View File

@@ -47,8 +47,8 @@ export const useAuthStore = defineStore('auth', () => {
]); ]);
userInfo = fetchUserInfoResult; userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes); accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) { if (accessStore.loginExpired) {