Merge branch 'master' of http://47.109.37.87:3000/by2025/admin-vben5
Some checks failed
/ Explore-Gitea-Actions (push) Failing after 3m58s
Some checks failed
/ Explore-Gitea-Actions (push) Failing after 3m58s
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:*",
|
||||
|
48
apps/web-antd/src/api/cockpit/cockpit.ts
Normal file
48
apps/web-antd/src/api/cockpit/cockpit.ts
Normal 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');
|
||||
}
|
@@ -1,4 +1,7 @@
|
||||
import type { PageQuery, BaseEntity } from '#/api/common';
|
||||
import type {
|
||||
QuestionItemForm
|
||||
} from "#/api/property/customerService/questionnaire/questionItem/model";
|
||||
|
||||
export interface QuestionVO {
|
||||
/**
|
||||
@@ -57,7 +60,7 @@ export interface QuestionForm extends BaseEntity {
|
||||
/**
|
||||
* 问题类型(1单行文本2多行文本3单选题4多选题5评分题6日期选择)
|
||||
*/
|
||||
type?: string;
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 是否必填(1不必填2必填)
|
||||
@@ -74,6 +77,21 @@ export interface QuestionForm extends BaseEntity {
|
||||
*/
|
||||
sort?: number;
|
||||
|
||||
/**
|
||||
* 选项
|
||||
*/
|
||||
questionnaireQuestionItems:QuestionItemForm[]
|
||||
|
||||
/**
|
||||
* 评分预览
|
||||
*/
|
||||
rate?:number;
|
||||
|
||||
/**
|
||||
* 时间预览
|
||||
*/
|
||||
dateTime?:string;
|
||||
|
||||
}
|
||||
|
||||
export interface QuestionQuery extends PageQuery {
|
@@ -1,4 +1,5 @@
|
||||
import type { PageQuery, BaseEntity } from '#/api/common';
|
||||
import type {QuestionForm} from "#/api/property/customerService/questionnaire/question/model";
|
||||
|
||||
export interface QuestionnaireVO {
|
||||
/**
|
||||
@@ -74,6 +75,11 @@ export interface QuestionnaireForm extends BaseEntity {
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 问题
|
||||
*/
|
||||
questionnaireQuestions:QuestionForm[];
|
||||
|
||||
}
|
||||
|
||||
export interface QuestionnaireQuery extends PageQuery {
|
@@ -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,
|
||||
|
116
apps/web-antd/src/views/cockpit/security/index.css
Normal file
116
apps/web-antd/src/views/cockpit/security/index.css
Normal file
@@ -0,0 +1,116 @@
|
||||
/* 自定义滚动条样式 */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.scrollbar-thumb-slate-700::-webkit-scrollbar-thumb {
|
||||
background-color: #334155;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollbar-track-transparent::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes ping {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
/* 淡入动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* 数字计数器动画 */
|
||||
@keyframes countUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.counter-animation {
|
||||
animation: countUp 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* 网格背景图案 */
|
||||
.pattern-grid {
|
||||
background-image: linear-gradient(rgba(148, 163, 184, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.05) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
/* 确保Canvas元素占满容器 */
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 自定义按钮反馈效果 */
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 卡片悬停效果增强 */
|
||||
div[class*="bg-slate-800/60"] {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
div[class*="bg-slate-800/60"]:hover {
|
||||
box-shadow: 0 0 15px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 输入框焦点效果 */
|
||||
input:focus, textarea:focus {
|
||||
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
/* 列表项悬停效果 */
|
||||
div[class*="cursor-pointer"] {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
div[class*="cursor-pointer"]:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 状态徽章样式优化 */
|
||||
span[class*="rounded-full"][class*="text-xs"] {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
span[class*="rounded-full"][class*="text-xs"]:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
894
apps/web-antd/src/views/cockpit/security/index.vue
Normal file
894
apps/web-antd/src/views/cockpit/security/index.vue
Normal file
@@ -0,0 +1,894 @@
|
||||
<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>
|
@@ -0,0 +1,32 @@
|
||||
import type { FormSchemaGetter } from "#/adapter/form";
|
||||
import type { VxeGridProps } from "@vben/plugins/vxe-table";
|
||||
|
||||
export const querySchema:FormSchemaGetter=()=>[
|
||||
{
|
||||
component:'Input',
|
||||
fieldName:'',
|
||||
label:'回复者'
|
||||
}
|
||||
];
|
||||
export const columns:VxeGridProps['columns'] = [
|
||||
{
|
||||
title:'序号',
|
||||
field:'',
|
||||
width:'auto',
|
||||
},
|
||||
{
|
||||
title:'回复者',
|
||||
field:'',
|
||||
|
||||
},
|
||||
{
|
||||
title:'提交时间',
|
||||
field:''
|
||||
},
|
||||
{
|
||||
title:'操作',
|
||||
field:'action',
|
||||
slots:{default:'action'},
|
||||
|
||||
}
|
||||
]
|
@@ -8,13 +8,23 @@ import {
|
||||
CalendarTwoTone,
|
||||
ApiTwoTone,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import questionnaireTableModal from './questionnaire-table-modal.vue';
|
||||
|
||||
const [QuestionnaireTableModal,modalApi] = useVbenModal({
|
||||
connectedComponent: questionnaireTableModal
|
||||
}
|
||||
)
|
||||
function handleTable(){
|
||||
modalApi.setData({});
|
||||
modalApi.open();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="px-16">
|
||||
<div class="flex justify-end pb-4">
|
||||
<Button type="primary">查看所有回复</Button>
|
||||
<Button type="primary" @click="handleTable()">查看所有回复</Button>
|
||||
</div>
|
||||
<div class="bg-white p-2">
|
||||
<!-- 总览 -->
|
||||
@@ -271,5 +281,6 @@ import { Page } from '@vben/common-ui';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<QuestionnaireTableModal/>
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -1,4 +1,81 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { querySchema,columns } from './data';
|
||||
|
||||
const [BasicModal,modalApi] = useVbenModal({
|
||||
fullscreenButton:false,
|
||||
fullscreen:true,
|
||||
onClosed:handleClose,
|
||||
onConfirm:handleComfirm,
|
||||
onOpenChange:async(isOpen)=>{
|
||||
if(!isOpen){
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
})
|
||||
const formOptions:VbenFormProps={
|
||||
commonConfig:{
|
||||
labelWidth:80,
|
||||
componentProps:{
|
||||
allowClear:true,
|
||||
}
|
||||
},
|
||||
schema:querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
|
||||
}
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
// trigger: 'row',
|
||||
},
|
||||
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
|
||||
// columns: columns(),
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'questionnaire-table-modal'
|
||||
};
|
||||
|
||||
const [BasicTable,tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
async function handleClose() {
|
||||
|
||||
}
|
||||
async function handleComfirm() {
|
||||
|
||||
}
|
||||
async function handleEdit() {
|
||||
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<BasicModal>
|
||||
<BasicTable table-title="回复列表">
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-access:code="['property:attendanceArea:edit']"
|
||||
@click.stop="handleEdit()"
|
||||
>
|
||||
查看
|
||||
</ghost-button>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user