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.

2258 lines
90 KiB
HTML

2 months ago
<!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>
2 months ago
</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">
<a-button type="primary" size="small" @click="fetchAlarmGridData">查询</a-button>
</div>
<div class="search-item">
<a-button size="small" @click="initAlarmGridData">重置</a-button>
</div>
2 months ago
</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>
2 months ago
</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>
2 months ago
</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}}
2 months ago
</div>
<div class="pd-type">
<span class="label">放电类型:</span>
{{allPdTypes.find(({value})=>value==item.pdType)?.label}}
2 months ago
</div>
<div class="num">
<span style="margin-right: 12px;">
<span class="label">最大值:</span>
{{item.maxValue}}</span>
<span>
<span class="label">脉冲数:</span>
{{item.plusCount}}
</span>
2 months ago
</div>
</div>
2 months ago
</div>
</div>
2 months ago
</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="[
2 months ago
{
required: true,
message: '请输入站点名称',
},
{
max: 20,
message: '最多输入20个字符',
},
]">
<a-input v-model:value="siteModalForm.stationName" placeholder="请输入" />
</a-form-item>
<a-form-item label="背景图路径" name="img" :rules="[
2 months ago
{
required: true,
message: '请输入背景图路径',
},
]">
<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="[
2 months ago
{
required: true,
message: '请输入监测点名称',
},
]" disabled>
<a-input v-model:value="monitorModalForm.stationName" disabled />
</a-form-item>
<a-form-item label="监测点Key" name="monitorKey" :rules="[
{
2 months ago
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="[
2 months ago
{
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: 'avg', name: '平均值' },
{ id: 'maxValue', name: '最大值' },
{ id: 'plusAvg', name: '脉冲平均值' },
{ id: 'plusCount', 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; // 返回父节点
}
2 months ago
}
}
}
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>
</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,
};
const xAxis = {
type: 'category',
data: data.map(el => el.time.replace(' ', '\n')),
axisLine: {
onZero: false, // 禁止轴线对齐到0刻度强制固定在底部
},
name: '时间',
axisLabel: {
fontSize: 10,
},
2 months ago
};
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 = {
// 设置响应式
grid: {
top: 30,
right: 230,
bottom: 30,
left: 30
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
2 months ago
}
},
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'
}
2 months ago
}
// 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.$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 })
2 months ago
}
}
2 months ago
}
// 如果未能从 `coordRanges` 中解析出有效的 X 轴时间范围
if (!minXValue || !maxXValue) {
message.warning(`未能从 brush 选中区域中解析出有效的 X 轴时间范围!`);
this.$emit('getEventList', { startTime: null, endTime: null })
this.lastBrushSelected = null;
return;
2 months ago
}
// 这在 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}`);
2 months ago
}
// 更新上次的选中状态
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':''}">
2 months ago
{{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'
2 months ago
:class='[currentKey===item?"active-span":""]'>
<span v-if='item==="PRPS"'>/</span>
2 months ago
{{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' // <--
2 months ago
},
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,
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,
}],
}, { 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,
}],
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;
2 months ago
}
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++; // 计数器加一
2 months ago
if (this.len > 5) { // 如果超过5次循环将移除的元素重新加回末尾
data = data.concat(em);
2 months ago
}
// 设置下一个定时器
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>
2 months ago
</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,
2 months ago
}
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: '配置' }
2 months ago
]
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
}
2 months ago
}
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
const isExist = unref(totalPrpds).some(item => item.monitorKey === key)
if (isExist) return
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 pdTypeOps = [
{
value: '1',
label: '尖刺放电',
},
{
value: '2',
label: '悬浮放电',
},
{
value: '3',
label: '沿面放电',
},
{
value: '4',
label: '内部放电',
},
{
value: '5',
label: '颗粒放电',
}
]
const alarmFilters = reactive({
times: [],
isCancel: '',
pdTypes: []
})
2 months ago
// 请求告警信息表格数据
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,
pdType: alarmFilters.pdTypes,
monitorKey: 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
}
2 months ago
// 自定义上传方法
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()
}
2 months ago
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();
2 months ago
// 如果初始化尚未完成 (isCurrentInit.value 仍为 true),则启动轮询
if (isCurrentInit.value) {
progressTimer.value = setInterval(getInitPrgress, 2000);
console.log('初始化配置成功,进度定时器已启动。');
} else {
console.log('初始化已完成,无需启动进度轮询。');
2 months ago
}
} 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,
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>
2 months ago
</body>
</html>