You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2276 lines
90 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tool</title>
<script src="./js/vue.global.min.js"></script>
<script src="./js/antd.min.js"></script>
<link rel="stylesheet" href="./css/antd.min.css">
<link rel="stylesheet" href="./css/element-plus.css">
<script src="./js/element-plus.js"></script>
<!-- 引入 Element Plus 中文语言包 -->
<script src="./js/zh-cn.min.js"></script>
<script src="./js/echarts.min.js"></script>
<script src="./js/echarts-gl.min.js"></script>
<link rel="stylesheet" href="./css/common.css">
<script type="text/javascript" src="./js/axios.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 10px;
background-color: #f5f5f5;
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<!-- 树菜单 -->
<div ref="treeMenu" v-if="treeMenuInfo.menuVisible" class="context-menu"
:style="{ left: `${treeMenuInfo.menuX}px`, top: `${treeMenuInfo.menuY}px` }">
<div v-if="treeMenuInfo.data.isRoot" @click="addMonitor(treeMenuInfo.data)">新增监测点</div>
<div @click="editNodeItem(treeMenuInfo.data)">编辑</div>
<a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteNodeItem(treeMenuInfo.data)">
<div>删除</div>
</a-popconfirm>
</div>
<div class="tree-content">
<div class="input-box">
<a-input placeholder="名称" v-model:value="searchValue" @change="search">
</a-input>
</div>
<div class="tree-box">
<el-tree ref="treeRef" node-key="key" check-strictly :check-on-click-node="false" :data="treeData"
:props="defaultProps" :load="onLoadData" lazy v-model:checked-keys="checkedKeys1"
:current-node-key="selectedKey" :show-checkbox="typeOfCheckable.includes(activeKey)"
:default-expanded-keys="defaultExpendKeys" @check="checkTree" @node-click="handleNodeClick"
:filter-node-method="filterNode" @node-contextmenu="handleRightClick">
<template #default="{ node, data }">
<div :class="[selectedKey===data.key?'hight-light-node':'','node-content']" :title="node.label"
@click.stop="handleNodeContentClick(data)">
{{ node.label }}
</div>
</template>
</el-tree>
</div>
<div class="bottom-btns">
<a-button type="primary" size="small" @click="isImportMonitorModalShow=true"> 导入监测点 </a-button>
<a-button style="margin-left: 4px;" type="primary" size="small" @click="addSite"> 新增站点 </a-button>
</div>
</div>
<div class="right-content">
<!-- tab切换 -->
<div class="top-box">
<a-tabs v-model:active-key="activeKey" @change="(key)=>activeKey=key">
<a-tab-pane v-for="item in tabsArr" :key="item.key" :tab="item.tab"></a-tab-pane>
</a-tabs>
<div class="top-timer" v-if="timeSelectArr.includes(activeKey)">
<el-date-picker v-model="dateRange" style="width: 260px" class="custom-date-picker"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" />
</div>
</div>
<div class="panel-views">
<div v-if="activeKey === 'alarmConfig'" key="alarmConfig" class="sub-content alarm-config">
<!-- 告警配置 -->
<div class="search-bar">
<div class="search-item">
<span>放电类型:</span>
<a-select v-model:value="alarmFilters.pdTypes" :options="pdTypeOps" mode="tags" placeholder="请选择"
style="width: 300px">
</div>
<div class="search-item">
<span>时间:</span>
<el-date-picker v-model="alarmFilters.times" style="width: 360px" class="custom-date-picker"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" />
</div>
<div class="break"></div>
<div class="search-item">
<span>是否复归:</span>
<a-radio-group :options="cancelOps" v-model:value="alarmFilters.isCancel" />
</div>
<div class="search-item">
<span>查询模式:</span>
<a-radio-group :options="searchModelOps" v-model:value="searchModel" />
</div>
<div class="search-item">
<a-button type="primary" size="small" @click="fetchAlarmGridData">查询</a-button>
</div>
<div class="search-item">
<a-button size="small" @click="initAlarmGridData">重置</a-button>
</div>
</div>
<div class="grid-content">
<a-table :columns="columns" :data-source="dataSource" :pagination="pagination" :loading="configLoading"
@change="handleTableChange" :scroll="{ y: 460 }" :custom-row="alarmRowClick">
<template #name1="{ text }">
{{pdTypeOps?.find(item=>item.value==text)?.label||'-'}}
</template>
<template #name2="{ text }">
{{ cancelOps?.find(item=>item.value==text)?.label||'-'}}
</template>
</a-table>
</div>
</div>
<div v-if="activeKey === 'historyTrend'" key="historyTrend" class="sub-content history-trend">
<div class="trend-graph-container">
<history-trend-graph v-for="(item,index) in trendGraphData" :key="item.key"
:ref="el => { if (el) trendGraphRefs[index] = el }" :data="item" @jump-to-prpd="jumpToPRPD" />
</div>
</div>
<div v-if="activeKey === 'prpdAndPrps'" key="prpdAndPrps" class="sub-content total-prpd-panel">
<div>
<history-trend-graph ref="trendRef" :data='trendGraphInfo' used-tab="prpdAndPrps"
@get-event-list="getEventList" />
</div>
<div class="prpd-box">
<!-- 累计PRPD组件 -->
<div class="totle-prpds">
<prpd-and-prps-comp class="total-prpd-item" v-for="(item,index) in totalPrpds" :key="index" :data="item"
:time="prpdAndPrpsTimes" :is-count-prpd="isTypeOfCountPrpd" :fiter-data="eventFilterForm" />
</div>
<div class="time-sider">
<div class="title">
<span>
事件:
</span>
<el-popover placement="right" :width="320" trigger="click" :visible="everntFilterVisible">
<template #reference>
<el-button @click="everntFilterVisible=!everntFilterVisible">筛选</el-button>
</template>
<el-form :inline="true" :model="eventFilterForm" label-width="auto" label-suffix=":"
class="demo-form-inline">
<el-form-item label="脉冲数">
<el-input v-model="eventFilterForm.plusCount" placeholder="请输入" type="number" min="0"
@input="validatePlusCount" />
</el-form-item>
<el-form-item label="幅值">
<el-input v-model="eventFilterForm.maxValue" placeholder="请输入" type="number" />
</el-form-item>
<el-form-item label="放电类型">
<el-select v-model="eventFilterForm.pdTypes" multiple collapse-tags collapse-tags-tooltip
style="width: 240px" placeholder="请选择">
<el-option v-for="item in allPdTypes" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onFilterEventList">确定</el-button>
<el-button @click="onResetEventList">重置</el-button>
</el-form-item>
</el-form>
</el-popover>
</div>
<div class="list" v-loading="eventListLoading">
<div :class="['event-item',activeEventTime==item.time?'active-event-item':'']"
v-for="(item,index) in eventList" :key="item.time" @click="handleEventClick(item)">
<div class="time">
<span class="label">时间:</span>
{{item.time}}
</div>
<div class="pd-type">
<span class="label">放电类型:</span>
{{allPdTypes.find(({value})=>value==item.pdType)?.label}}
</div>
<div class="num">
<span style="margin-right: 12px;">
<span class="label">最大值:</span>
{{item.maxValue}}</span>
<span>
<span class="label">脉冲数:</span>
{{item.plusCount}}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="activeKey === 'eventCount'" key="eventCount" class="sub-content">
<!-- 事件统计 -->
<event-count :time="dateRange" :selected-key="selectedKey" :tree-data="fullTreeData" />
</div>
<div v-if="activeKey === 'config'" key="config" class="sub-content config-page">
<!-- 配置页面 -->
<div class="station-config">
<div class="title">站点告警配置</div>
<div class="station">
<span>站点:</span>
<a-select v-model:value="stationName1" size="small" style="width: 200px">
<a-select-option v-for="item in treeData" :key="item.key" :value="item.stationName">
{{ item.stationName }}
</a-select-option>
</a-select>
</div>
<div class="delete-bar">
<span class="delete-time">
<span class="label">清除告警时间段:</span>
<el-date-picker v-model="deleteAlarmTimes" style="width: 360px" class="custom-date-picker"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" />
</span>
<a-popconfirm title="确定删除?" ok-text="是" cancel-text="否" @confirm="deleteAlarmConfirm">
<a href="#">删除</a>
</a-popconfirm>
</div>
<el-upload action="#" :http-request="alarmCustomUpload" :show-file-list="false" :limit="1"
accept=".csv,.xlsx,.xls" :before-upload="alarmBeforeUpload">
<a-button type="primary" size="small">导入告警</el-button>
</el-upload>
</div>
<div class="path-config">
<div class="title">路径配置</div>
<div class="path-item" v-for="item in allPathCongfigs" :key="item.key">
<div class="name">{{item.label}} :</div>
<div class="row">
<el-radio-group v-if="item.key==='ALARM_TYPE'" v-model="item.value">
<el-radio value="3">国网</el-radio>
<el-radio value="2">南网</el-radio>
</el-radio-group>
<el-input v-else v-model="item.value" style="width: 560px" placeholder="请输入路径" />
</div>
<a-button size="small" type="primary" style="margin-right: 4px;"
@click="savePathConfig(item)">保存</a-button>
<a-button v-if="item.key==='MERGIN_ROOT_PATH'" size="small" type="primary" style="margin-right: 4px;"
:loading="isCurrentInit" @click="initCatalogue">初始化</a-button>
<a-button size="small" @click="resetPathConfig(item.key)">重置</a-button>
<p v-if="item.key==='MERGIN_ROOT_PATH'&&isCurrentInit" class="init-warning-tips">
当前初始化进度:{{initTips}}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 导入监测点弹框 -->
<a-modal width="550px" :destroyOnClose="true" :visible="isImportMonitorModalShow" title="导入监测点"
@ok="onImportMonitor" @cancel="isImportMonitorModalShow=false" :confirmLoading="importMonitorModalloading"
ok-text="确定" cancel-text="取消">
<el-form :inline="true" ref="importMonitorFormRef" :model="importMonitorForm" label-width="auto"
label-suffix=":" class="demo-form-inline" :rules="importMonitorRules">
<el-form-item label="站点" prop="stationName">
<el-select v-model="importMonitorForm.stationName" style="width: 320px" placeholder="请选择">
<el-option v-for="item in fullTreeData" :key="item.title" :label="item.title" :value="item.title" />
</el-select>
</el-form-item>
<el-form-item label="选择文件" prop="file">
<el-upload ref="monitorImportRef" action="#" :on-change="handleFileChange"
:before-upload="beforImportMonitor" :show-file-list="true" :limit="1" accept=".csv,.xlsx,.xls"
:auto-upload="false" @remove="handleImportMonitorFileRemove">
<template #trigger>
<el-button size="small" type="primary">选择文件</el-button>
</template>
<template #tip>
<div class="el-upload__tip">
支持 CSV, XLSX, XLS 格式文件,单次仅导入一个文件。
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</a-modal>
<!-- 站点弹框 -->
<a-modal width="550px" :destroyOnClose="true" :visible="isSiteModalShow"
:title="isAddTypeOfSiteModal?'新增站点':'编辑站点'" @cancel="isSiteModalShow=false">
<a-form layout=" vertical" ref="siteModalRef" :model="siteModalForm">
<a-form-item label="站点名称" name="stationName" :rules="[
{
required: true,
message: '请输入站点名称',
},
{
max: 20,
message: '最多输入20个字符',
},
]">
<a-input v-model:value="siteModalForm.stationName" placeholder="请输入" />
</a-form-item>
<a-form-item label="背景图路径" name="img">
<a-input v-model:value="siteModalForm.img" placeholder="请输入" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="isSiteModalShow=false">关闭</a-button>
<a-button type="primary" @click="onSiteModalSubmit" :loading="siteModalloading">确定</a-button>
</template>
</a-modal>
<!-- 监测点弹框 -->
<a-modal width="550px" :destroyOnClose="true" :visible="isMonitorModalShow"
:title="isAddTypeOfMonitorModal?'新增监测点':'编辑监测点'" @cancel="isMonitorModalShow=false">
<a-form layout=" vertical" ref="monitorModalRef" :model="monitorModalForm">
<a-form-item label="站点名称" name="stationName" :rules="[
{
required: true,
message: '请输入监测点名称',
},
]" disabled>
<a-input v-model:value="monitorModalForm.stationName" disabled />
</a-form-item>
<a-form-item label="监测点Key" name="monitorKey" :rules="[
{
required: true,
message: '请输入监测点Key',
},
{
max: 20,
message: '最多输入20个字符',
},
]">
<a-input v-model:value="monitorModalForm.monitorKey" placeholder="请输入"
:disabled="!isAddTypeOfMonitorModal" />
</a-form-item>
<a-form-item label="监测点名称" name="name" :rules="[
{
required: true,
message: '请输入监测点名称',
},
{
max: 20,
message: '最多输入20个字符',
},
]">
<a-input v-model:value="monitorModalForm.name" placeholder="请输入" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="isMonitorModalShow=false">关闭</a-button>
<a-button type="primary" @click="onMonitorModalSubmit" :loading="monitorModalLoading">确定</a-button>
</template>
</a-modal>
</div>
<script>
const { createApp, ref, onMounted, unref, watch, reactive, computed, nextTick, onUnmounted, components, provide, inject } = Vue;
const { message } = antd;
axios.defaults.baseURL = 'http://192.168.1.198:9501'; // 临时服务地址
axios.defaults.timeout = 10000;
const HISTORY_TREND_LEGEND = [
{ id: 'plusAvg', name: '脉冲平均值' },
{ id: 'avg', name: '噪声' },
{ id: 'maxValue', name: '最大值' },
{ id: 'plusCount', name: '脉冲频次' },
{ id: 'eventCount', name: '事件数' },
]
const findParentByKey = (tree, targetKey) => {
for (const parent of tree) {
// 检查子节点
if (parent?.children?.length) {
for (const child of parent.children) {
if (child.key === targetKey) {
return parent.stationName; // 返回父节点
}
}
}
}
return null;
}
const formatTreeData = (node) => {
return {
...node.data,
children: node.childNodes?.map(child => formatTreeData(child)) || []
};
};
// 生成正弦波形
const generateSineWaveData = () => {
const dataPoints = [];
// 遍历0到360度每1度取一个点
for (let angle = 0; angle <= 360; angle++) {
// 将角度转换为弧度
const radian = angle * Math.PI / 180;
// 计算增益值
const gain = 40 * Math.sin(radian) - 40;
// 将 [相位, 增益] 对添加到数组中
// 使用 toFixed(2) 来保留两位小数,避免浮点数精度问题,并将其转换为数字类型
dataPoints.push([angle, parseFloat(gain.toFixed(2))]);
}
return dataPoints;
}
// 处理数据,生成 ECharts 所需的三维坐标格式
const deal = (arr, dbm) => {
for (let i = 0; i < 6400; i++) {
let j = parseInt(i / 128) + 1;
arr[i] = [j, parseInt((i - (j - 1) * 128 + 1) * 2.8125), parseInt(dbm[i]) + 80];
}
return arr;
}
const findMonitorNameByKey = (arr, key) => {
let result = [];
arr.forEach(item => {
if (item?.children?.length) {
result = [...result, ...item.children]
}
});
return result.find(item => item.key === key)?.name
}
// 获取24h前时间
const getPrevious24HourMark = (timeStr) => {
const date = new Date(timeStr);
date.setHours(date.getHours() - 24);
// 注意:分钟、秒、毫秒会保留原始值
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 获取15分钟前的时间索引
const getLastIndexWithinMaxTime = (times, maxTimeRange) => {
if (!times || times.length === 0) {
return -1; // 如果数组为空,返回 -1
}
const firstTimestamp = new Date(times[0]).getTime();
const timeLimit = firstTimestamp + maxTimeRange;
// --- 特殊逻辑检查:索引 1 是否就超过了 15 分钟 ---
if (times.length > 1) { // 确保有索引 1 存在
const secondTimestamp = new Date(times[1]).getTime();
if (secondTimestamp > timeLimit) {
return 1; // 如果索引 1 就超出了 15 分钟,直接返回 1
}
}
for (let i = 1; i < times.length; i++) {
const currentTimestamp = new Date(times[i]).getTime();
if (currentTimestamp > timeLimit) {
return i;
}
}
return times.length - 1;
}
// 历史趋势图组件
const trendGraphRefs = ref([]);
watch(trendGraphRefs, (arr) => {
// 此处定时器解决异步
setTimeout(() => {
arr.forEach((el) => {
el.getChart().group = 'trend-group';
});
}, 300);
}, {
immediate: true,
deep: true,
},);
// 历史趋势图谱
const historyTrendGraph = {
template: `<div class='graph-container'>
<div class="title">
<span> 监测点:{{ data?.label||'' }}</span>
<div v-if="usedTab==='prpdAndPrps'" class="time-range">
<span> 最大框选时间范围:</span>
<a-select v-model:value="maxInMs" size="small" style="width: 200px;margin-left: 10px;">
<a-select-option v-for="item in timeOps" :key="item.value" :value="item.value">
{{ item.label }}
</a-select-option>
</a-select>
<span style="margin-left:24px">框选时间段:{{lastBrushSelected?.minX}} ~ {{lastBrushSelected?.maxX}}</span>
</div>
</div>
<div ref='graphRef' class="line-graph" @dblclick='onDblclickGraph'></div>
</div>`,
data() {
return {
resizeObserver: null,
lastBrushSelected: null, // 用于存储上次成功的框选范围,防止重复触发
timeOps: [
{ label: '15分钟', value: 15 * 60 * 1000 },
{ label: '30分钟', value: 30 * 60 * 1000 },
{ label: '1小时', value: 60 * 60 * 1000 },
{ label: '6小时', value: 6 * 60 * 60 * 1000 },
{ label: '24小时', value: 24 * 60 * 60 * 1000 },
],
maxInMs: 15 * 60 * 1000 // 最大框选时间
}
},
props: {
data: {
type: Object,
default: () => { }
},
// 使用的场景
usedTab: {
type: String,
default: 'historyTrend'
}
},
// 定义常量不定义在data减少性能开销
created() {
this.chart = null
},
watch: {
data: {
async handler(val, oldVal) {
if (
val.key === oldVal?.key &&
val.label === oldVal?.label &&
val.startTime === oldVal?.startTime &&
val.endTime === oldVal?.endTime
) {
return
}
const { startTime, endTime, key } = val
if (!key || !startTime) return
this.chart?.showLoading()
const params = {
current: 1,
startTime,
endTime,
monitorKey: key,
pageSize: 99999
}
const { data: { result: { result: { result } } } } = await axios.get(`/ldpdtools/trendData/listByParam`, {
params,
}).finally(() => {
this.chart?.hideLoading()
})
this.initTrendGraph(result || [])
},
deep: true,
immediate: true
}
},
async mounted() {
this.resizeObserver = new ResizeObserver(() => {
this.chart?.resize();
});
this.resizeObserver.observe(this.$refs.graphRef);
},
methods: {
initTrendGraph(data) {
// 图数据
const series = HISTORY_TREND_LEGEND.map((item, index) => ({
name: item.name,
type: 'line',
yAxisIndex: index,
data: data.map(el => el[item.id]),
smooth: true,
showSymbol: false,
lineStyle: {
width: 2,
},
}));
const legend = {
data: HISTORY_TREND_LEGEND.map(s => s.name), // 图例数据
top: 0,
left: '18%',
};
const xAxis = {
type: 'category',
data: data.map(el => el.time.replace(' ', '\n')),
axisLine: {
onZero: false, // 禁止轴线对齐到0刻度强制固定在底部
},
name: '时间',
axisLabel: {
fontSize: 10,
},
};
const yAxis = HISTORY_TREND_LEGEND.map((item, index) => {
return {
type: 'value',
name: item.name,
nameTextStyle: {
color: '#666',
fontSize: 10,
padding: [0, 0, 0, 30], // 上、右、下、左左间距30px
},
triggerEvent: true, // 关键配置
axisLabel: {
formatter: '{value}',
fontSize: 10,
},
position: 'right',
offset: (index + 1) * 48, // 控制 Y 轴横向间距
splitLine: {
show: false, // 仅第一个 Y 轴显示网格线
},
splitNumber: 3, // Y轴仅显示3个刻度线
};
});
this.chart = echarts.init(this.$refs.graphRef);
const option = {
// 设置响应式
color: ['#1f8ffd', '#fd1f44', '#ff9933', "#660099", '#35C822'],
grid: {
top: 30,
right: '30%',
bottom: 30,
left: 30
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend,
xAxis,
yAxis,
series,
dataZoom: [
{
type: 'inside', // 内置型数据区域缩放
xAxisIndex: [0], // 作用于第一个或所有X轴
start: 0, // 数据窗口范围的起始百分比0%
end: 100, // 数据窗口范围的结束百分比100%
// 以下是 type: 'inside' 独有的配置项,控制交互
zoomOnMouseWheel: true, // 鼠标滚轮缩放
moveOnMouseMove: true, // 鼠标移动(拖拽)平移
moveOnMouseWheel: false, // 鼠标滚轮不平移,只缩放
filterMode: 'filter' // 数据过滤模式,'filter' 会过滤掉超出范围的数据
}]
,
brush: {
brushLink: 'all', // 刷选联动所有系列
xAxisIndex: 'all', // 允许在所有 X 轴上进行刷选 (这里只有一个 X 轴)
yAxisIndex: 'all', // 允许在所有 Y 轴上进行刷选
brushMode: 'rect', // **只允许矩形框选**
throttleType: 'debounce', // 内部刷选状态更新的节流模式
throttleDelay: 300, // 节流延迟
},
toolbox: {
show: this.usedTab === 'prpdAndPrps', // 只有prpd/prps展示
feature: {
// brush 工具箱,包含矩形框选和清除按钮
brush: {
type: ['lineX',] // 只显示矩形框选工具和清除刷选工具
},
},
left: 30, // 工具箱位置
top: '0'
}
}
// 3. 设置配置项并渲染图表
this.chart.setOption(option);
// 监听 'brushEnd' 事件,在用户松开鼠标完成框选时触发,用于获取最终结果
this.chart.on('brushEnd', this.handleBrushEnd);
if (this.usedTab === 'prpdAndPrps') {
this.loadInitTimeData()
}
},
// 加载初始化时间数据
loadInitTimeData() {
const xAxisData = this.chart.getOption().xAxis[0].data?.map(item => item.replace(/\n/g, ' ')) || [];
const index = getLastIndexWithinMaxTime(xAxisData, this.maxInMs)
if (index < 0) {
return message.error(`当前数据为空!`);
}
this.chart.dispatchAction({
type: 'brush', // 这是一个与 brush 组件相关的 action
command: 'brush', // 命令是 'brush',表示设置刷选区域
areas: [{ // areas 是一个数组,可以定义多个刷选区域
brushType: 'lineX', // 刷选类型,与你在 option.brush 中设置的一致
xAxisIndex: 0, // 对应你的第一个 X 轴(索引为 0
coordRange: [0, index] // 设置刷选区域的坐标范围(类目轴是索引)
}]
});
// 更新上次的选中状态
this.lastBrushSelected = { minX: xAxisData[0], maxX: xAxisData[index] };
this.$emit('getEventList', { startTime: xAxisData[0], endTime: xAxisData[index] })
},
// 双击跳转
onDblclickGraph() {
if (this.usedTab !== 'historyTrend') return
// 是否空数据
const isEmptyData = this.chart.getOption().series?.[0]?.data?.length === 0
if (isEmptyData) {
return message.error(`当前数据为空!`);
}
this.$emit('jumpToPrpd', this.data.key)
},
handleBrushEnd(params) {
const brushAreas = params.areas;
// 如果没有框选区域(例如用户只是点击了一下,或者点击了工具箱的清除按钮)
if (!brushAreas || brushAreas.length === 0) {
this.lastBrushSelected = null; // 清除上次的选中状态
this.loadInitTimeData()
return;
}
// 获取第一个(也是唯一一个)刷选区域的信息
// 由于你在 brushstart 中清除了旧框,这里通常只会有一个 areas 元素。
const brushArea = brushAreas[0];
let minXValue = null;
let maxXValue = null;
// `coordRanges` 针对 'category' 类型的 X 轴,会直接给出数据索引的范围
if (brushArea.coordRanges && brushArea.coordRanges.length > 0) {
const xAxisCoordRange = brushArea.coordRanges[0];
if (xAxisCoordRange && xAxisCoordRange.length > 0) {
const rawMinXIndex = xAxisCoordRange[0]; // 获取X轴的最小索引 (例如: 0)
const rawMaxXIndex = xAxisCoordRange[1]; // 获取X轴的最大索引 (例如: 6)
// 获取 ECharts 配置中 X 轴的所有数据(时间字符串数组)
const xAxisData = this.chart.getOption().xAxis[0].data;
// 根据索引从 xAxisData 中取出对应的实际时间值
if (xAxisData[rawMinXIndex] !== undefined && xAxisData[rawMaxXIndex] !== undefined) {
// 假设你的时间字符串可能包含换行符 '\n',这里将其替换为空格以统一格式
minXValue = String(xAxisData[rawMinXIndex]).replace(/\n/g, ' ');
maxXValue = String(xAxisData[rawMaxXIndex]).replace(/\n</g, ' ');
} else {
this.$emit('getEventList', { startTime: null, endTime: null })
}
}
}
// 如果未能从 `coordRanges` 中解析出有效的 X 轴时间范围
if (!minXValue || !maxXValue) {
message.warning(`未能从 brush 选中区域中解析出有效的 X 轴时间范围!`);
this.$emit('getEventList', { startTime: null, endTime: null })
this.lastBrushSelected = null;
return;
}
// 这在 brushEnd 事件中依然有用,例如用户连续两次选择完全相同的范围
if (this.lastBrushSelected &&
this.lastBrushSelected.minX === minXValue &&
this.lastBrushSelected.maxX === maxXValue) {
return;
}
const startTime = minXValue ? String(minXValue).replace(/\n/g, ' ') : '';
const endTime = maxXValue ? String(maxXValue).replace(/\n/g, ' ') : '';
const xAxisData = this.chart.getOption().xAxis[0].data;
const isOver15Min = new Date(endTime).getTime() - new Date(startTime).getTime() > this.maxInMs;
if (isOver15Min) {
this.chart.dispatchAction({
type: 'brush',
command: 'clear', // 'clear' 命令用于清除所有刷选区域
areas: [] // 空数组表示清除所有
});
this.$emit('getEventList', { startTime: null, endTime: null })
const warningLable = this.timeOps.find(item => item.value === this.maxInMs)?.label || ''
return message.error(`数据加载过多,请框选时间区域不超过${warningLable}`);
}
// 更新上次的选中状态
this.lastBrushSelected = { minX: startTime, maxX: endTime };
// 通过事件将框选的 X 轴范围传递给父组件
this.$emit('getEventList', { startTime, endTime })
},
getChart() {
return this.chart;
}
},
}
// prpd/prps组件
const prpdAndPrpsComp = {
template: `<div>
<div class="total-prpd-title" :style="{color:!!data.monitorName?'red':''}">
{{title}}</div>
<div v-show='currentKey==="PRPD"' ref="prpdBoxRef" class="prpd"></div>
<div v-show='currentKey==="PRPS"' ref="prpsBoxRef" class="prps"></div>
<div v-if='!isCountPrpd' class='switcher'>
<span v-for='item in switchBox' :key='item' @click='currentKey=item'
:class='[currentKey===item?"active-span":""]'>
<span v-if='item==="PRPS"'>/</span>
{{item}}</span>
</div>
</div>`,
props: ['data', 'time', 'isCountPrpd', 'fiterData'],
data() {
return {
currentKey: 'PRPD',
switchBox: ['PRPD', 'PRPS',],
timer: 0,
len: 0,
}
},
computed: {
// prpd所需参数
mixPRPDParams() {
return {
...this.data,
isCountPrpd: this.isCountPrpd,
time: this.time
}
},
title() {
return this.data.monitorName ? this.data.monitorName + (this.isCountPrpd ? '(累计PRPD)' : '') : '暂无监测点'
}
},
watch: {
mixPRPDParams: {
handler(newVal) {
if (newVal.monitorKey && newVal.time[0]) {
// 新增操作
this.$nextTick(() => {
if (newVal.isCountPrpd) {
// 加载累计prpd
this.currentKey = 'PRPD'
this.fetchPRPDData(newVal)
} else {
// 加载prpd / prps
this.fetchPRPDAndPrpsData(newVal)
}
})
}
if (!newVal.monitorKey || !newVal.time[0]) {
// 删除操作
this.myChart.setOption({
series: [{
name: 'PRPD数据',
data: []
}]
}, { notMerge: false });
}
},
deep: true
},
isCountPrpd: {
handler(bool) {
if (bool) {
this.currentKey = 'PRPD'
}
},
immediate: true,
}
},
created() {
this.myChart = null
this.resizeObserver = null
this.myPRPSChart = null // prps容器
this.myPRPSOps = null
this.resize3dObserver = null
},
mounted() {
this.initPrpd()
this.initPrps()
},
methods: {
initPrpd() {
const chartDom = this.$refs.prpdBoxRef;
this.myChart = echarts.init(chartDom);
const option = {
backgroundColor: '#ffffff', // 设置背景色为深色,与图片相似
tooltip: {
show: false
},
grid: {
left: '8%',
right: '10%',
top: '8%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'value',
name: '相位',
nameTextStyle: {
color: '#000000' // <-- 将轴名称“相位”的颜色设置为黑色
},
nameLocation: 'middle',
nameGap: 30, // 调整名称与轴线的距离
min: 0,
max: 360,
interval: 90, // 固定刻度为0, 90, 180, 270, 360
axisLabel: {
color: '#000' // X轴刻度文字颜色
},
axisLine: {
lineStyle: {
color: '#ccc' // X轴线颜色
}
},
splitLine: {
show: true,
lineStyle: {
color: '#ccc', // X轴网格线颜色
type: 'solid'
}
}
},
yAxis: {
type: 'value',
name: '幅值',
nameLocation: 'middle',
nameTextStyle: {
color: '#000000' // <-- 将轴名称“相位”的颜色设置为黑色
},
nameRotate: 90, // Y轴名称旋转
nameGap: 30, // 调整名称与轴线的距离
min: -80,
max: 0,
interval: 20, // 固定刻度为 -80, -60, -40, -20, 0
axisLabel: {
color: '#000', // Y轴刻度文字颜色
},
axisLine: {
lineStyle: {
color: '#ccc' // Y轴线颜色
}
},
splitLine: {
show: true,
lineStyle: {
color: '#ccc', // Y轴网格线颜色
type: 'solid'
}
}
},
// 颜色区分
visualMap: {
dimension: 2,
show: false,
inRange: {
color: ['#007ACC', '#FF4500'] // 例如:从浅蓝色到深蓝色
},
},
series: [
{
name: '正弦波形', // 系列名称
type: 'line', // 图表类型为折线图
smooth: true, // 开启平滑曲线
showSymbol: false, // 不显示数据点符号
lineStyle: {
width: 2 // 调整线条宽度
},
data: generateSineWaveData() // 调用函数生成数据
},
{
name: 'PRPD数据',
type: 'scatter', // 使用折线图来模拟曲线
symbolSize: 1.5,
data: [],
},
]
};
this.myChart.setOption(option);
this.resizeObserver = new ResizeObserver(() => {
this.myChart?.resize();
});
this.resizeObserver.observe(chartDom);
},
// 请求PRPD数据
async fetchPRPDData(data) {
this.myChart.showLoading()
const params = {
startTime: data.time[0],
endTime: data.time[1],
monitorKey: data.monitorKey,
...this.fiterData,
pdTypes: this.fiterData.pdTypes.join(','),
}
const { data: { result: res } } = await axios.get(`/ldpdtools/eventData/getPrpdByParam`, {
params,
}).catch(err => {
this.myChart.setOption({
series: [{
name: 'PRPD数据',
data: [],
symbolSize: 1.5,
}],
}, { notMerge: false });
}).finally(() => {
this.myChart.hideLoading()
})
const result = res || []
const countArr = result.map(item => item[2])
const minCount = Math.min(...countArr)
const maxCount = Math.max(...countArr)
this.myChart.setOption({
series: [{
name: 'PRPD数据',
data: result,
symbolSize: 1.5,
}],
visualMap: {
min: minCount,
max: maxCount
}
}, { notMerge: false });
},
// 请求prpd/prps的float数据
async fetchPRPDAndPrpsData({ monitorKey, time }) {
this.myChart.showLoading()
const params = {
monitorKey,
timeStr: time[0],
}
const { data: { result: res } } = await axios.get(`/ldpdtools/eventData/getInfoByTime`, {
params,
}).finally(() => {
this.myChart.hideLoading()
})
this.draw2dChart(res)
this.draw3dChart(res)
},
// prpd数据处理
draw2dChart(params) {
const res = params || []
let dbm = res.slice(6400, 12800);
if (dbm.length == 0) {
dbm = res.slice(0, 6400)
} else if (dbm.length < 6400) {
dbm = res.slice(6400, 6400 + dbm.length).concat(res.slice(0, 6400 - dbm.length))
}
let arr = [];
deal(arr, dbm)
const data = this.datting(arr)
const result = data.map(item => [item[1], item[2] - 80, item[0]])
const countArr = result.map(item => item[2])
const minCount = Math.min(...countArr)
const maxCount = Math.max(...countArr)
this.myChart.setOption({
series: [{
name: 'PRPD数据',
data: result,
symbolSize: 2.5,
}],
visualMap: {
min: minCount,
max: maxCount
}
}, { notMerge: false });
},
datting(arr) {
let dataArr = [];
let newArr = [];
for (let i = 1; i <= 128; i++) {
let obj = []
for (let j = 0; j < 50; j++) {
obj.push(arr[i + j * 128 - 1])
}
newArr.push(obj);
};
let lay1 = []
for (let i = 0; i < newArr.length; i++) {
let lay2 = [];
for (let k = 0; k < 80; k++) {
let lay3 = [];
for (let j = 0; j < newArr[i].length; j++) {
if (newArr[i][j][2] == k) {
lay3.push(newArr[i][j])
}
}
if (lay3 != '') {
lay2.push(lay3)
}
}
lay1.push(lay2)
}
for (let i = 0; i < lay1.length; i++) {
for (let j = 0; j < lay1[i].length; j++) {
for (let k = 0; k < lay1[i][j].length; k++) {
for (let m = 0; m < lay1[i][j][k].length; m++) {
lay1[i][j][k][0] = lay1[i][j].length;
}
}
dataArr.push(lay1[i][j][0]);
}
}
return dataArr;
},
// 初始化prps
initPrps() {
const dom3d = this.$refs.prpsBoxRef
this.myPRPSChart = echarts.init(dom3d);
// 初始化一次不变的 ECharts 配置
this.myPRPSOps = {
title: {
text: 'PRPS',
left: 'center',
top: 10,
textStyle: {
fontSize: 14,
color: '#111'
}
},
animation: false,
tooltip: {
show: false,
},
toolbox: {
show: false,
},
visualMap: {
min: -20,
max: 80,
show: false,
itemWidth: 5,
orient: "vertical",
inRange: {
color: ['transparent', '#00ee00', '#eeee00', '#ee0000', '#4e0211']
},
formatter: (value) => {
return parseInt(value - 80);
}
},
xAxis3D: {
type: 'value',
min: 0,
max: 50,
splitNumber: 5,
name: '周期',
nameGap: 24,
axisLine: {
lineStyle: {
color: '#000'
}
},
axisLabel: {
color: '#000',
fontSize: 14,
formatter: (value, index) => {
value = 50 - value
if (value >= 0) {
return value;
}
}
},
},
yAxis3D: {
type: 'value',
min: 0,
max: 360,
splitNumber: 4,
interval: 90,
name: '相位',
nameGap: 24,
splitArea: {
interval: 4,
},
axisLine: {
lineStyle: {
color: '#000'
}
},
axisLabel: {
color: '#000',
fontSize: 14,
},
},
zAxis3D: {
type: 'value',
min: 0,
max: 80,
scale: true,
splitNumber: 4,
name: '幅值',
nameGap: 24,
axisLine: {
lineStyle: {
color: '#000'
}
},
axisLabel: {
color: '#000',
fontSize: 14,
formatter: (value, index) => {
if (value >= 0) {
return value - 80;
}
}
},
},
grid3D: {
boxHeight: 100,
boxWidth: 120,
boxDepth: 100,
axisLine: {
lineStyle: {
color: '#fff',
opacity: 0.1
}
},
axisPointer: {
show: false,
lineStyle: {
color: '#fff'
},
},
viewControl: {
distance: 260,
minDistance: 40,
maxDistance: 400,
rotateSensitivity: [1, 1],
zoomSensitivity: 0,
panSensitivity: 0,
panMouseButton: "middle",
rotateMouseButton: "left",
orthographicSize: 150,
maxOrthographicSize: 400,
minOrthographicSize: 20,
center: [0, -10, 0],
minBeta: 30,
maxBeta: 180,
minAlpha: -90,
maxAlpha: 90,
projection: "perspective",
autoRotateDirection: "cw",
autoRotateAfterStill: 3,
damping: 0.8
},
light: {
main: { intensity: 1.2 },
ambient: { intensity: 0.3 }
}
},
roam: false,
series: [{
type: 'bar3D',
data: [],
shading: 'color',
label: {
show: false,
},
itemStyle: {
opacity: 0.8,
},
silent: true,
emphasis: {
label: { show: false },
}
}]
};
this.myPRPSChart.setOption(this.myPRPSOps);
this.resize3dObserver = new ResizeObserver(() => {
this.myPRPSChart?.resize();
});
this.resize3dObserver.observe(dom3d);
},
// prps数据处理
draw3dChart(data) {
let databox = []
for (let i = 0; i < 6400; i++) {
databox[i] = -128;
}
const fullData = databox.concat(data); // 直接使用导入的 JSON 数
this.dispose([...fullData]); // 启动动画
},
// 动画循环逻辑
dispose(data) {
clearTimeout(this.timer); // 清除上一个定时器
this.drawEcharts3d(data); // 绘制图表
let em = [];
em = data.splice(0, 1280); // 移除前1000个元素
this.len++; // 计数器加一
if (this.len > 5) { // 如果超过5次循环将移除的元素重新加回末尾
data = data.concat(em);
}
// 设置下一个定时器
this.timer = setTimeout(() => {
this.dispose(data); // 递归调用自身
}, 200);
},
drawEcharts3d(obj) {
if (!this.myPRPSChart) return; // 确保图表实例已存在
let arr = [];
deal(arr, obj); // 处理数据
// 只更新 series 的 data 部分
this.myPRPSChart.setOption({
series: [{
data: arr.map(function (item) {
return {
value: [item[0], item[1], item[2]]
}
})
}]
});
},
},
}
// 事件统计组件
const eventCount = {
components: {
historyTrendGraph,
},
template: `<div class='event-count-box'>
<history-trend-graph :data='graphInfo' used-tab="eventCount"/>
<div class="pd-type-grid">
<el-table :data="pdTypeData" border>
<el-table-column prop="typeCn" label="局放类型"/>
<el-table-column prop="max" label="最大值" />
<el-table-column prop="avg" label="平均值" />
<el-table-column prop="count" label="事件数" />
<el-table-column prop="plusCount" label="脉冲数" />
</el-table>
</div>
</div>`,
data() {
return {
pdTypeData: []
}
},
props: ['time', 'selectedKey', 'treeData'],
inject: ['provideAllPdTypes'],
computed: {
graphInfo() {
return {
key: this.selectedKey,
label: findMonitorNameByKey(this.treeData, this.selectedKey),
startTime: this.time[0],
endTime: this.time[1],
};
}
},
watch: {
graphInfo: {
handler(value) {
const { startTime, endTime, key } = value
if (!key || !startTime) {
return
}
this.feachGrid()
},
deep: true,
immediate: true
}
},
methods: {
// 获取表格数据
async feachGrid() {
this.loading = true
const params = {
startTime: this.time[0],
endTime: this.time[1],
monitorKey: this.selectedKey,
}
const { data: { result } } = await axios.get(`/ldpdtools/eventData/getEventStatistics`, {
params,
})
const res = result || {}
const data = [] // 展示的数据
Object.keys(res).forEach(key => {
const type = unref(this.provideAllPdTypes).find(el => el.value == key)
data.push({
typeCn: type.label,
typeEn: type.value,
...res[key]
})
})
this.pdTypeData = data || []
},
}
}
createApp({
setup() {
const typeOfCheckable = ['historyTrend', 'prpdAndPrps']
const timeSelectArr = ['historyTrend', 'prpdAndPrps', 'eventCount'] // 含有时间选择的组件
const treeData = ref([])
const fullTreeData = ref([]) // 懒加载后的完整菜单数据
const checkedKeys1 = ref([]); // 勾选
const selectedKey = ref('') // 选中
const defaultExpendKeys = ref([]); // 默认展开的节点
const treeMenuInfo = reactive({
menuVisible: false,
data: null,
menuX: 0,
menuY: 0
})
const treeMenu = ref()
const handleClickOutside = (e) => {
if (treeMenuInfo.menuVisible && !treeMenu.value?.contains(e.target)) {
treeMenuInfo.menuVisible = false;
}
};
const searchValue = ref('');
const treeRef = ref()
const activeKey = ref('alarmConfig')
const tabsArr = [
{ key: 'alarmConfig', tab: '告警列表' },
{ key: 'historyTrend', tab: '历史趋势' },
{ key: 'prpdAndPrps', tab: 'PRPD/PRPS' },
{ key: 'eventCount', tab: '事件统计' },
{ key: 'config', tab: '配置' }
]
const isImportMonitorModalShow = ref(false)
const importMonitorModalloading = ref(false)
const importMonitorFormRef = ref()
const monitorImportRef = ref()
const importMonitorRules = {
stationName: [
{ required: true, message: '请选择站点', trigger: 'change' } // 'change' 适合 select
],
file: [
{ required: true, message: '请选择文件', trigger: 'change' } // 'change' 适合 upload
]
}
// 上传校验
const beforImportMonitor = (file) => {
const allowedTypes = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
const isAllowedType = allowedTypes.includes(file.type) ||
['.csv', '.xlsx', '.xls'].some(ext => file.name.toLowerCase().endsWith(ext))
if (!isAllowedType) {
message.error('只能上传 CSV 或 Excel 文件!')
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error('文件大小不能超过 10MB!')
return false
}
return true
}
// 上传检测点
const handleFileChange = (file, fileList) => {
importMonitorForm.value.file = fileList.length > 0 ? fileList[0].raw : null;
}
const importMonitorForm = ref({
stationName: '',
file: null
})
watch(isImportMonitorModalShow, (bool) => {
if (bool) {
return
}
importMonitorForm.value.stationName = ''
importMonitorForm.value.file = null
monitorImportRef.value.clearFiles()
},)
// 清除文件
const handleImportMonitorFileRemove = () => {
importMonitorForm.value.file = null
}
// 导入监测点
const onImportMonitor = async () => {
importMonitorFormRef.value.validate(async (valid) => {
if (!valid) {
return
}
try {
const formData = new FormData();
formData.append('file', unref(importMonitorForm).file);
const { data: { code, message: message1 } } = await axios.post(`/ldpdtools/monitor/importMonitor`, formData, {
params: {
stationName: unref(importMonitorForm).stationName
}
})
if (code === 200) {
initTreeData()
isImportMonitorModalShow.value = false
message.success(message1)
} else {
message.error('导入失败')
}
} catch (error) {
console.error('上传错误:', error)
}
})
}
// const uploadMonitor = (options) => { }
const siteModalloading = ref(false)
const isSiteModalShow = ref(false)
const siteModalRef = ref()
const siteHistoryData = ref() // 站点历史数据
// 站点信息
const siteModalForm = reactive({
stationName: '',
img: ''
})
const isAddTypeOfSiteModal = ref(true) // true:新增站点 false:修改站点
const isMonitorModalShow = ref(false) // 监测点弹框
const isAddTypeOfMonitorModal = ref(true) // true:新增监测点 false:修改监测点
const monitorModalLoading = ref(false)
const monitorModalRef = ref()
const monitorModalForm = reactive({
stationName: '', // 站点名称
name: '',// 监测点名称
monitorKey: '' // 监测点key
})
// 查询树
const search = () => {
treeRef.value.filter(unref(searchValue)); // 触发过滤
};
const filterNode = (value, data) => {
if (!value) return true;
return data.name.includes(value); // 根据label匹配
};
const handleRightClick = (event, data) => {
event.preventDefault();
treeMenuInfo.menuVisible = true
treeMenuInfo.menuX = event.clientX - 8
treeMenuInfo.menuY = event.clientY - 12
treeMenuInfo.data = data
}
const checkTree = (checkedNode, { checkedKeys: currentKeys }) => {
if (currentKeys.length > 6) {
message.warning('最多只能勾选6个节点');
// 阻止勾选:回退到前一次的状态
checkedKeys1.value = checkedKeys1.value.slice(0, 6);
treeRef.value.setCheckedKeys(checkedKeys1.value);
} else {
if (unref(activeKey) === 'prpdAndPrps') {
// 处理累计prpd逻辑
handleCountPRPDSelect(checkedNode)
}
checkedKeys1.value = currentKeys
}
}
const handleNodeContentClick = (node) => {
if (node.isRoot) {
// 单击父节点
return
}
selectedKey.value = node.key
}
const defaultProps = {
children: 'children',
label: 'name',
isLeaf: 'leaf',
class: (node) => node.isRoot ? 'is-root' : ''
}
const onLoadData = (node, resolve) => {
if (node.level !== 1) {
resolve([]);
return
}
const stationName = node?.label ?? ''
axios.get(`/ldpdtools/monitor/monitorList?stationName=${stationName}`).then(({ data: { code, result } }) => {
const data = (result || []).map(item => {
return {
title: item.name,
key: item.monitorKey,
isRoot: false,
disabled: false,
leaf: true,
...item
}
})
resolve(data)
fullTreeData.value = treeRef.value?.store?.root.childNodes.map(node => formatTreeData(node));
})
}
// 初始化树
const initTreeData = async () => {
const { data: { code, result } } = await axios.get('/ldpdtools/monitor/getStations');
if (code !== 200) {
return
}
treeData.value = result.map((item, index) => {
return {
key: index,
title: item.stationName,
isRoot: true,
disabled: true,
leaf: false,
...item
}
})
// 初始化默认展开第一个父节点
if (unref(treeData)?.length) {
const firstParentNodeData = unref(treeData)[0].key;
defaultExpendKeys.value = [firstParentNodeData]
}
fullTreeData.value = treeRef.value?.store?.root.childNodes.map(node => formatTreeData(node));
}
// 编辑节点
const editNodeItem = (data) => {
const { isRoot, stationName, img, monitorKey, name } = data
if (isRoot) {
// 编辑站点
isSiteModalShow.value = true
isAddTypeOfSiteModal.value = false
siteModalForm.stationName = stationName
siteModalForm.img = img
siteHistoryData.value = JSON.parse(JSON.stringify({ stationName, img }))
} else {
// 编辑监测点
isMonitorModalShow.value = true
isAddTypeOfMonitorModal.value = false
monitorModalForm.stationName = stationName
monitorModalForm.name = name
monitorModalForm.monitorKey = monitorKey
}
}
// 删除节点
const deleteNodeItem = async (data) => {
const { isRoot, stationName, monitorKey } = data
let resultCode = 200
if (isRoot) {
// 删除站点
const { data: { code } } = await axios.delete(`/ldpdtools/monitor/deleteStation?stationName=${stationName}`);
resultCode = code
} else {
// 删除检测点
const { data: { code } } = await axios.delete(`/ldpdtools/monitor/deleteMonitor?stationName=${stationName}&monitorKey=${monitorKey}`);
resultCode = code
}
if (resultCode === 200) {
message.success(`删除${isRoot ? '站点' : '监测点'}成功`);
initTreeData()
} else {
message.error(`删除${isRoot ? '站点' : '监测点'}失败`);
}
}
// 新增监测点
const addMonitor = (data) => {
const { stationName } = data
monitorModalForm.stationName = stationName
isMonitorModalShow.value = true
}
const addSite = () => {
isSiteModalShow.value = true
}
// 新增、编辑站点
const onSiteModalSubmit = async () => {
await siteModalRef.value.validate()
siteModalloading.value = true
const { img, stationName } = unref(siteModalForm)
let resultCode = 200
if (unref(isAddTypeOfSiteModal)) {
// 新增
const { data: { code } } = await axios.put(`/ldpdtools/monitor/saveStateion?img=${img}&stationName=${stationName}`);
resultCode = code
} else {
// 编辑
const { data: { code } } = await axios.put(`/ldpdtools/monitor/editStateion?img=${img}&newStationName=${stationName}&stationName=${unref(siteHistoryData).stationName}`);
resultCode = code
}
siteModalloading.value = false
if (resultCode === 200) {
message.success(`${unref(isAddTypeOfSiteModal) ? '新增' : '编辑'}站点成功`);
isSiteModalShow.value = false
initTreeData()
} else {
message.error(`${unref(isAddTypeOfSiteModal) ? '新增' : '编辑'}站点失败`);
}
}
// 新增、编辑监测点
const onMonitorModalSubmit = async () => {
await monitorModalRef.value.validate()
monitorModalLoading.value = true
const { stationName, name, monitorKey } = unref(monitorModalForm)
let resultCode = 200
let resultMsg = ''
if (unref(isAddTypeOfMonitorModal)) {
// 新增
const { data: { code, message } } = await axios.put(`/ldpdtools/monitor/saveMonitor?stationName=${stationName}&name=${name}&monitorKey=${monitorKey}`);
resultCode = code
resultMsg = message
} else {
// 编辑
const { data: { code, message } } = await axios.put(`/ldpdtools/monitor/editMonitor?stationName=${stationName}&name=${name}&monitorKey=${monitorKey}`);
resultCode = code
resultMsg = message
}
monitorModalLoading.value = false
if (resultCode === 200) {
message.success(`${unref(isAddTypeOfMonitorModal) ? '新增' : '编辑'}监测点成功`);
isMonitorModalShow.value = false
initTreeData()
} else {
message.error(resultMsg);
}
}
// 监测站点弹框关闭
watch(isSiteModalShow, (bool) => {
if (bool) {
return
}
siteModalRef.value.resetFields()
isAddTypeOfSiteModal.value = true
siteModalForm.stationName = ''
siteModalForm.img = ''
siteHistoryData.value = {}
})
// 监测监测点弹框关闭
watch(isMonitorModalShow, (bool) => {
if (bool) {
return
}
monitorModalRef.value.resetFields()
isAddTypeOfMonitorModal.value = true
monitorModalForm.stationName = ''
monitorModalForm.name = ''
monitorModalForm.monitorKey = ''
})
// 历史趋势部分逻辑
const trendGraphData = ref([])
const dateRange = ref([]);
// tab在历史趋势监听勾选节点
watch([checkedKeys1, dateRange], async ([keys, time]) => {
const [startTime, endTime] = time || [];
if (unref(activeKey) !== 'historyTrend' || !startTime || !endTime) return;
echarts.connect('trend-group'); // 通过分组连接
trendGraphData.value = keys.map((key) => {
return {
key,
label: findMonitorNameByKey(unref(fullTreeData), key),
startTime,
endTime,
};
});
});
// 跳转到prpd/prps
const jumpToPRPD = (key) => {
activeKey.value = 'prpdAndPrps'
selectedKey.value = key
// 是否当前点击key存在
const isExist = unref(totalPrpds).some(item => item.monitorKey === key)
if (isExist) return
unref(totalPrpds).forEach(item => {
item.monitorKey = ''
item.monitorName = ''
})
const addItem = unref(totalPrpds).find(item => !item.monitorKey)
addItem.monitorKey = key
addItem.monitorName = findMonitorNameByKey(unref(fullTreeData), key)
}
// 告警信息
const configLoading = ref(false)
const columns = [
{
title: '监测点',
dataIndex: 'name',
align: 'center',
ellipsis: true,
width: 120
},
{
title: '告警信息',
dataIndex: 'detailsInfo',
align: 'center',
ellipsis: true,
},
{
title: '放电类型',
dataIndex: 'pdType',
align: 'center',
ellipsis: true,
slots: { customRender: 'name1' },
width: 120
},
{
title: '是否复归',
dataIndex: 'isCancel',
align: 'center',
ellipsis: true,
slots: { customRender: 'name2' },
width: 120
},
{
title: '告警时间',
dataIndex: 'time',
align: 'center',
ellipsis: true,
width: 180
},
]
const dataSource = ref([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['5', '10', '20'],
showTotal: (total) => `${total} 条数据`,
});
const cancelOps = [
{ label: '是', value: '0' },
{ label: '否', value: '1' },
]
// 查询模式
const searchModelOps = [
{ label: '站点', value: 'station' },
{ label: '监测点', value: 'monitor' },
]
const searchModel = ref('station')
const pdTypeOps = [
{
value: '1',
label: '尖刺放电',
},
{
value: '2',
label: '悬浮放电',
},
{
value: '3',
label: '沿面放电',
},
{
value: '4',
label: '内部放电',
},
{
value: '5',
label: '颗粒放电',
}
]
const alarmFilters = reactive({
times: [],
isCancel: '',
pdTypes: []
})
// 请求告警信息表格数据
const fetchAlarmGridData = async () => {
if (!unref(selectedKey)) {
message.warning(`请先选择左侧监测点`);
return
}
configLoading.value = true;
const params = {
current: pagination.current,
pageSize: pagination.pageSize,
startTime: alarmFilters.times?.[0] || '',
endTime: alarmFilters.times?.[1] || '',
isCancel: alarmFilters.isCancel,
pdTypes: alarmFilters.pdTypes,
monitorKey: unref(searchModel) === 'station' ? '' : unref(selectedKey),
stationName: findParentByKey(unref(fullTreeData), unref(selectedKey))
}
const { data: { result } } = await axios.post(`/ldpdtools/alarm/queryAlarmList`, params)
configLoading.value = false
dataSource.value = result?.result || []
pagination.total = result?.total || 0
}
// 重置表格
const initAlarmGridData = () => {
Object.assign(alarmFilters, {
times: [],
isCancel: '',
pdTypes: []
});
fetchAlarmGridData();
}
const handleTableChange = (pag) => {
Object.assign(pagination, pag);
fetchAlarmGridData();
};
// 单点击行
const alarmRowClick = (record, index) => {
return {
onDblclick: () => {
// 跳转到历史趋势
const pre24hTime = getPrevious24HourMark(record.time)
activeKey.value = 'historyTrend'
checkedKeys1.value = [record.monitorKey]
treeRef.value.setCheckedKeys(checkedKeys1.value);
dateRange.value = [pre24hTime, record.time]
},
};
}
// 告警管理监听
watch(selectedKey, (key) => {
if (!key || unref(activeKey) !== 'alarmConfig') return
// 初始化筛选数据
Object.assign(alarmFilters, {
times: [],
isCancel: '',
pdTypes: []
});
// 请求数据
fetchAlarmGridData();
});
// prpd/prps
const totalPrpds = ref([
{
monitorKey: '', // 监控点key
monitorName: '' // 监控点名称
},
{
monitorKey: '',
monitorName: ''
},
{
monitorKey: '',
monitorName: ''
},
{
monitorKey: '',
monitorName: ''
},
{
monitorKey: '',
monitorName: ''
},
{
monitorKey: '',
monitorName: ''
},
])
const prpdAndPrpsTimes = ref([]) // prpd/prps筛选时间
const eventListLoading = ref(false)
const eventList = ref([]) //事件列表
const isTypeOfCountPrpd = ref(false) //是否是累计prpd
const activeEventTime = ref('') // 当前选中的事件时间
const everntFilterVisible = ref(false)
const trendRef = ref()
// 事件筛选
const eventFilterForm = ref({
plusCount: '',
maxValue: '',
pdTypes: []
})
// 历史趋势所需数据
const trendGraphInfo = computed(() => {
return {
key: unref(selectedKey),
label: findMonitorNameByKey(unref(fullTreeData), unref(selectedKey)),
startTime: unref(dateRange)[0],
endTime: unref(dateRange)[1],
}
})
// 输入脉冲数校验函数
const validatePlusCount = (value) => {
let numValue = Number(value);
if (isNaN(numValue)) {
numValue = 0; // 如果不是有效数字设置为0
}
if (numValue < 0) {
numValue = 0; // 如果小于0设置为0
}
numValue = Math.floor(numValue);
eventFilterForm.value.plusCount = numValue;
};
// 获取事件列表
const getEventList = async ({ startTime, endTime }) => {
if (startTime === null && endTime === null) {
// 历史趋势没数据
isTypeOfCountPrpd.value = true
eventList.value = []
activeEventTime.value = ''
prpdAndPrpsTimes.value = [] // 子组件判断空清除
return
}
isTypeOfCountPrpd.value = true
prpdAndPrpsTimes.value = [startTime, endTime]
eventListLoading.value = true
const params = {
current: 1,
pageSize: 999999999,
startTime,
endTime,
monitorKey: unref(selectedKey),
...unref(eventFilterForm),
pdTypes: unref(eventFilterForm).pdTypes.join(','),
};
const { data: { code, result: { result: res } } } = await axios.get('/ldpdtools/eventData/listByParam', { params }).finally(() => {
eventListLoading.value = false
})
if (code !== 200) {
return
}
activeEventTime.value = ''
eventList.value = res?.map(item => {
return {
pdType: item.pdType,
time: item.time,
maxValue: item.maxFloat,
plusCount: item.plusCount,
}
}) || []
}
// 筛选
const onFilterEventList = () => {
trendRef.value.loadInitTimeData()
everntFilterVisible.value = false
}
// 重置筛选框
const onResetEventList = () => {
eventFilterForm.value = {
plusCount: '',
maxValue: '',
pdTypes: []
}
onFilterEventList()
}
// 点击事件
const handleEventClick = (data) => {
activeEventTime.value = data.time
isTypeOfCountPrpd.value = false
prpdAndPrpsTimes.value = [data.time, '']
}
const handleCountPRPDSelect = ({ key: currentKey }) => {
const index = unref(checkedKeys1).indexOf(currentKey);
if (index > -1) {
// 取消选中
const deleteItem = unref(totalPrpds).find(item => item.monitorKey === currentKey)
deleteItem.monitorKey = ''
deleteItem.monitorName = ''
} else {
// 选中
const addItem = unref(totalPrpds).find(item => !item.monitorKey)
addItem.monitorKey = currentKey
addItem.monitorName = findMonitorNameByKey(unref(fullTreeData), currentKey)
}
}
// 配置页面
const stationName1 = ref('')
const deleteAlarmTimes = ref([]) // 删除告警时间段
const deleteAlarmConfirm = async () => {
if (!unref(stationName1)) {
return message.warning(`请选择站点!`);
}
if (!unref(deleteAlarmTimes).length) {
return message.warning(`请选择需要清除告警时间段!`);
}
const res = await axios.get(`/ldpdtools/alarm/deleteAlarmByTime`, {
params: {
startTime: unref(deleteAlarmTimes)?.[0] || '',
endTime: unref(deleteAlarmTimes)?.[1] || '',
stationName: unref(stationName1)
},
})
}
// 上传前校验
const alarmBeforeUpload = (file) => {
if (!unref(stationName1)) {
message.warning(`请选择站点!`);
return false
}
const allowedTypes = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
const isAllowedType = allowedTypes.includes(file.type) ||
['.csv', '.xlsx', '.xls'].some(ext => file.name.toLowerCase().endsWith(ext))
if (!isAllowedType) {
message.error('只能上传 CSV 或 Excel 文件!')
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error('文件大小不能超过 10MB!')
return false
}
return true
}
// 自定义上传方法
const alarmCustomUpload = async (options) => {
const { file } = options
try {
const formData = new FormData()
formData.append('file', file)
const { data: { code, message: message1 } } = await axios.post(`/ldpdtools/alarm/importAlarm`, formData, {
params: {
stationName: unref(stationName1)
}
})
if (code === 200) {
message.success(message1)
} else {
message.error('导入失败')
}
} catch (error) {
console.error('上传错误:', error)
}
}
const allPathCongfigs = ref([]) // 所有路径配置
const isCurrentInit = ref(false) // 是否正在初始化
const initTips = ref('')
const allPdTypes = ref([])
provide('provideAllPdTypes', allPdTypes)
// 获取放电类型
const getPdTypes = async () => {
const types = []
const { data: { result } } = await axios.get('/ldpdtools/config/getPdTypes');
Object.keys(result).forEach(key => {
types.push({
value: key,
label: result[key]
})
})
allPdTypes.value = types
}
// 获取所有配置项
const getAllConfigs = async () => {
getPdTypes()
allPathCongfigs.value = []
const { data: { result } } = await axios.post('/ldpdtools/config/getConfigKey')
Object.keys(result).forEach(key => {
allPathCongfigs.value = [...unref(allPathCongfigs), {
key,
label: result[key],
value: ''
}]
})
const { data: { result: paths } } = await axios.get('/ldpdtools/config/getConfig')
// 初始化路径赋值
allPathCongfigs.value.forEach(item => {
item.value = paths[item.key] || ''
})
// 排序
const itemIdToMoves = ["ALARM_TYPE", "MERGIN_ROOT_PATH"];
const startOfItemToMove = unref(allPathCongfigs).find(item => item.key === itemIdToMoves[0]);
const endOfItemToMove = unref(allPathCongfigs).find(item => item.key === itemIdToMoves[1]);
const remainingItems = unref(allPathCongfigs).filter(item => !itemIdToMoves.includes(item.key));
if (startOfItemToMove && endOfItemToMove) {
allPathCongfigs.value = [startOfItemToMove, ...remainingItems, endOfItemToMove];
}
}
// 保存路径
const savePathConfig = async (data) => {
if (!data.value) {
return message.error(`请输入${data.label}!`);
}
const { label, ...params } = data
const { data: { code } } = await axios.get('/ldpdtools/config/editConfig', { params })
if (code !== 200) return message.error(`保存${data.label}失败`)
message.success('保存成功!')
getAllConfigs()
}
// 重置路径
const resetPathConfig = async (key) => {
const { data: { code } } = await axios.get('/ldpdtools/config/resetConfig', { params: { key } })
if (code !== 200) return message.error(`重置${data.label}失败`)
message.success('重置成功!')
getAllConfigs()
}
const progressTimer = ref(null)
// 获取初始化进度
const getInitPrgress = async () => {
try {
const { data: { message } } = await axios.get('/ldpdtools/config/getInitProgress')
isCurrentInit.value = message.includes('/')
initTips.value = message
if (!isCurrentInit.value) {
clearInterval(progressTimer.value); // 停止定时器
progressTimer.value = null; // 将定时器ID置空以防误操作
}
} catch (error) {
clearInterval(progressTimer.value);
progressTimer.value = null;
}
}
const initCatalogue = async () => {
// 在启动新的初始化前,确保清除任何正在运行的定时器
if (unref(progressTimer)) {
clearInterval(progressTimer.value);
progressTimer.value = null;
}
try {
const { data: { code } } = await axios.get('/ldpdtools/config/initConfig');
if (code === 200) {
// 立即执行一次进度获取,获取最新状态
await getInitPrgress();
// 如果初始化尚未完成 (isCurrentInit.value 仍为 true),则启动轮询
if (isCurrentInit.value) {
progressTimer.value = setInterval(getInitPrgress, 2000);
console.log('初始化配置成功,进度定时器已启动。');
} else {
console.log('初始化已完成,无需启动进度轮询。');
}
} else {
console.error('初始化配置请求失败code:', code);
}
} catch (error) {
console.error('初始化目录时发生网络或服务器错误:', error);
}
};
onMounted(() => {
initTreeData()
getAllConfigs()
progressTimer.value = setInterval(getInitPrgress, 2000);
document.addEventListener('click', handleClickOutside);
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
if (unref(progressTimer)) {
clearInterval(progressTimer.value);
progressTimer.value = null;
}
});
return {
treeRef,
treeData,
fullTreeData,
treeMenuInfo,
treeMenu,
filterNode,
handleRightClick,
checkedKeys1,
searchValue,
search,
activeKey,
tabsArr,
onLoadData,
editNodeItem,
deleteNodeItem,
isImportMonitorModalShow,
onImportMonitor,
importMonitorModalloading,
siteModalloading,
addSite,
isSiteModalShow,
siteModalRef,
siteModalForm,
onSiteModalSubmit,
isAddTypeOfSiteModal,
isMonitorModalShow,
isAddTypeOfMonitorModal,
monitorModalLoading,
addMonitor,
monitorModalRef,
monitorModalForm,
onMonitorModalSubmit,
dateRange,
checkTree,
trendGraphData,
typeOfCheckable,
timeSelectArr,
columns,
dataSource,
pagination,
configLoading,
handleTableChange,
selectedKey,
cancelOps,
searchModelOps,
searchModel,
alarmFilters,
pdTypeOps,
deleteAlarmTimes,
deleteAlarmConfirm,
fetchAlarmGridData,
initAlarmGridData,
stationName1,
alarmCustomUpload,
alarmBeforeUpload,
defaultProps,
totalPrpds,
allPathCongfigs,
savePathConfig,
resetPathConfig,
initCatalogue,
isCurrentInit,
initTips,
trendGraphRefs,
allPdTypes,
trendGraphInfo,
prpdAndPrpsTimes,
eventListLoading,
getEventList,
eventList,
isTypeOfCountPrpd,
handleEventClick,
activeEventTime,
alarmRowClick,
defaultExpendKeys,
jumpToPRPD,
everntFilterVisible,
eventFilterForm,
onFilterEventList,
onResetEventList,
trendRef,
handleNodeContentClick,
importMonitorForm,
handleFileChange,
beforImportMonitor,
importMonitorRules,
importMonitorFormRef,
handleImportMonitorFileRemove,
monitorImportRef,
validatePlusCount
};
},
components: {
prpdAndPrpsComp,
eventCount,
historyTrendGraph
}
})
.use(antd)
.use(ElementPlus, {
locale: ElementPlusLocaleZhCn // 使用中文语言包
})
.mount('#app');
</script>
</body>
</html>