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.

1700 lines
83 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 type="text/javascript" src="./three/three.min.js"></script>
<script type="text/javascript" src="./three/OrbitControls.js"></script>
<script type="text/javascript" src="./three/Lut.js"></script>
<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)"
@check="checkTree" @node-click="selectTree" highlight-current :filter-node-method="filterNode"
@node-contextmenu="handleRightClick" />
</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切换 -->
2 months ago
<div class="top-box">
<a-tabs v-model:activeKey="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>
2 months ago
<div class="panel-views">
<div v-if="activeKey === 'historyTrend'" key="historyTrend" class="sub-content history-trend">
<div class="trend-graph-container">
2 months ago
<history-trend-graph v-for="(item,index) in trendGraphData" :key="item.key"
:ref="el => { if (el) trendGraphRefs[index] = el }" :data="item" />
2 months ago
</div>
</div>
2 months ago
<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>
</div>
<div class="grid-content">
<a-table :columns="columns" :data-source="dataSource" :pagination="pagination"
:loading="configLoading" @change="handleTableChange" :scroll="{ y: 460 }">
<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 === 'countPRPD'" key="countPRPD" class="sub-content total-prpd">
<!-- 累计PRPD -->
<div class="trend-box"></div>
<div class="prpd-box">
<!-- 累计PRPD组件 -->
<total-prpd class="total-prpd-item" v-for="(item,index) in totalPrpds" :key="index"
:data="item" />
</div>
</div>
<div v-if="activeKey === 'prpdAndPrps'" key="prpdAndPrps" class="sub-content prpd-and-prps">
<!-- PRPD/PRPS -->
<div class="trend-box"></div>
<div class="prpd-and-prps-box">
<prpd-and-prps class="prpd-and-prps-item"
:ref="el => { if (el) prpdAndPrpsRefs[index] = el }" v-for="(item,index) in totalPrpds"
:key="index" :sort="index" />
</div>
</div>
<div v-if="activeKey === 'eventCount'" key="eventCount" class="sub-content">
<!-- 事件统计 -->
2 months ago
<event-count :time="dateRange" :selected-key="selectedKey" :tree-data="fullTreeData" />
2 months ago
</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-input v-model="item.value" style="width: 560px" placeholder="请输入路径" />
</div>
<a-button size="small" type="primary" style="margin-right: 4px;"
@click="savePathConfig(item)">保存</a-button>
2 months ago
<a-button v-if="item.key==='MERGIN_ROOT_PATH'" size="small" type="primary"
style="margin-right: 4px;" :loading="isCurrentInit"
@click="initCatalogue">初始化</a-button>
2 months ago
<a-button size="small" @click="resetPathConfig(item.key)">重置</a-button>
2 months ago
<p v-if="item.key==='MERGIN_ROOT_PATH'&&isCurrentInit" class="init-warning-tips">
当前初始化进度:{{initTips}}
</p>
2 months ago
</div>
</div>
</div>
</div>
</div>
<!-- 导入监测点弹框 -->
<a-modal width="550px" :destroyOnClose="true" :visible="isImportMonitorModalShow" title="导入监测点"
@ok="onImportMonitor" @cancel="isImportMonitorModalShow=false"
:confirmLoading="importMonitorModalloading"></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" :rules="[
{
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="[
{
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>
2 months ago
const { createApp, ref, onMounted, unref, watch, reactive, computed, nextTick, onUnmounted, components } = Vue;
const { message } = antd;
axios.defaults.baseURL = 'http://192.168.1.198:9501'; // 临时服务地址
2 months ago
axios.defaults.timeout = 10000;
2 months ago
const HISTORY_TREND_LEGEND = [
{ id: 'avg', name: '平均值' },
{ id: 'maxValue', name: '最大值' },
{ id: 'plusAvg', name: '脉冲平均值' },
{ id: 'plusCount', name: '脉冲次数' },
]
2 months ago
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 < 5000; i++) {
let j = parseInt(i / 100) + 1;
arr[i] = [j, parseInt((i - (j - 1) * 100 + 1) * 3.6), parseInt(dbm[i]) + 80];
}
return arr;
}
2 months ago
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
}
// 历史趋势图组件
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">
监测点:{{ data?.label||'' }}
</div>
<div ref='graphRef' class="line-graph"></div>
</div>`,
data() {
return {
resizeObserver: null
}
},
props: {
data: {
type: Object,
default: () => { }
}
},
// 定义常量不定义在data减少性能开销
created() {
this.chart = null
},
watch: {
data: {
async handler(val) {
const { startTime, endTime, key } = val
if (!key || !startTime) return
const params = {
current: 1,
startTime,
endTime,
monitorKey: key,
pageSize: 99999
}
const { data: { result: { result: { result } } } } = await axios.get(`/ldpdtools/trendData/listByParam`, {
params,
})
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,
},
};
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'
}
},
legend,
xAxis,
yAxis,
series,
}
// 3. 设置配置项并渲染图表
this.chart.setOption(option);
},
getChart() {
return this.chart;
}
},
}
2 months ago
// 累计prpd组件
const totalPrpd = {
template: `<div ref="prpdBoxRef" style="width: 100%; height: 100%;"></div>`,
props: ['data'], // 接收父组件传递的数据
data() {
return {
}
},
methods: {
initPrpd() {
const chartDom = this.$refs.prpdBoxRef;
this.myChart = echarts.init(chartDom);
const option = {
backgroundColor: '#ffffff', // 设置背景色为深色,与图片相似
title: {
text: 'PRPD',
left: 'center',
top: '2%',
textStyle: {
color: '#444444', // 标题文字颜色
fontSize: 20
}
},
tooltip: {
show: false
},
axisPointer: {
show: true,
snap: true,
lineStyle: {
type: 'dashed',
},
label: {
show: true,
margin: 6,
backgroundColor: '#556',
textStyle: {
color: '#fff'
}
},
link: [{
xAxisId: ['xAxisLeft-yAxisTop', 'xAxisLeft-yAxisBottom']
}, {
xAxisId: ['xAxisRight-yAxisTop', 'xAxisRight-yAxisBottom']
}, {
yAxisId: ['xAxisLeft-yAxisTop', 'xAxisRight-yAxisTop']
}, {
yAxisId: ['xAxisLeft-yAxisBottom', 'xAxisRight-yAxisBottom']
}]
},
grid: {
left: '10%',
right: '10%',
top: '15%',
bottom: '15%',
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: 2.5,
data: [],
},
]
};
this.myChart.setOption(option);
this.resizeObserver = new ResizeObserver(() => {
this.myChart?.resize();
});
this.resizeObserver.observe(chartDom);
},
datting(arr) {
let dataArr = [];
let newArr = [];
for (let i = 1; i <= 100; i++) {
let obj = []
for (let j = 0; j < 50; j++) {
obj.push(arr[i + j * 100 - 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)
// console.log(lay1)
}
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]);
}
}
// console.log(dataArr)
data = dataArr;
}
},
created() {
this.myChart = null
this.resizeObserver = null
},
mounted() {
this.$nextTick(() => {
this.initPrpd()
})
// fetch('./three/prpd.json') // JSON文件路径
// .then(response => response.json()) // 解析为JSON对象
// .then(data => {
// let index = 0;
// const intervalId = setInterval(() => {
// const curret = data[index].map(item => {
// const y = Number(item[0]) - 80
// return [item[1], y, item[2]]
// });
// const countArr = data[index].map(item => item[2])
// const minCount = Math.min(...countArr)
// const maxCount = Math.max(...countArr)
// console.log(curret);
// this.myChart.setOption({
// series: [{
// name: 'PRPD数据',
// data: curret
// }],
// visualMap: {
// min: minCount,
// max: maxCount
// }
// }, { notMerge: false });
// index = (index + 1) % data.length; // 循环索引
// }, 1000);
// })
// .catch(error => console.error('加载JSON失败:', error));
fetch('./three/prps copy.json')
.then(response => response.json())
.then(res => {
let dbm = res.slice(5000, 10000);
if (dbm.length == 0) {
dbm = res.slice(0, 5000)
} else if (dbm.length < 5000) {
dbm = res.slice(5000, 5000 + dbm.length).concat(res.slice(0, 5000 - dbm.length))
}
let arr = [];
deal(arr, dbm)
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
}],
visualMap: {
min: minCount,
max: maxCount
}
}, { notMerge: false });
})
.catch(error => console.error('加载JSON失败:', error));
},
}
// 放在vue中的data会卡顿
const prpsData = [
{
prpsChart: null,
prpsChartOps: null
},
{
prpsChart: null,
prpsChartOps: null
},
{
prpsChart: null,
prpsChartOps: null
},
{
prpsChart: null,
prpsChartOps: null
},
{
prpsChart: null,
prpsChartOps: null
},
{
prpsChart: null,
prpsChartOps: null
}
]
const prpdAndPrpsRefs = ref([]);
const prpdAndPrps = {
components: {
totalPrpd,
},
template: `<div class='prps-and-prpd-container'>
<div v-show='currentKey==="PRPS"' ref='prpsRef' style="width: 100%; height: 100%;"></div>
<total-prpd v-show='currentKey==="PRPD"' class="total-prpd-item" />
<div class='switcher'>
<span v-for='item in switchBox' :key='item' @click='currentKey=item'
:class='{"active-span":currentKey===item}'>
<span v-if='item==="PRPD"'>/</span>
{{item}}</span>
</div>
</div>`,
data() {
return {
len: 0,
timer: 0,
switchBox: ['PRPS', 'PRPD'],
currentKey: 'PRPS'
};
},
props: {
sort: {
type: Number,
default: 0
}
},
async mounted() {
this.initChart(); // 初始化 ECharts 实例和基础配置
await fetch('./three/prps copy.json')  // JSON文件路径
.then(response => response.json())  // 解析为JSON对象
.then(data => {
let databox = []
for (let i = 0; i < 5000; i++) {
databox[i] = -100;
}
const fullData = databox.concat(data); // 直接使用导入的 JSON 数
this.dispose([...fullData]); // 启动动画
})
.catch(error => console.error('加载JSON失败:', error));
},
beforeDestroy() {
// 在组件销毁前,清除定时器并销毁 ECharts 实例
if (this.timer) {
this.closeTimer()
}
},
methods: {
closeTimer() {
clearTimeout(this.timer);
},
initChart() {
if (this.$refs.prpsRef) {
prpsData[this.sort].prpsChart = echarts.init(this.$refs.prpsRef);
// 初始化一次不变的 ECharts 配置
prpsData[this.sort].prpsChartOps = {
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: function (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: function (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: function (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 },
}
}]
};
prpsData[this.sort].prpsChart.setOption(prpsData[this.sort].prpsChartOps);
}
},
// 绘制/更新 ECharts 3D 图表
drawEcharts3d(obj) {
if (!prpsData[this.sort].prpsChart) return; // 确保图表实例已存在
let arr = [];
deal(arr, obj); // 处理数据
// 只更新 series 的 data 部分
prpsData[this.sort].prpsChart.setOption({
series: [{
data: arr.map(function (item) {
return {
value: [item[0], item[1], item[2]]
}
})
}]
});
},
// 动画循环逻辑
dispose(data) {
clearTimeout(this.timer); // 清除上一个定时器
this.drawEcharts3d(data); // 绘制图表
let em = [];
em = data.splice(0, 1000); // 移除前1000个元素
this.len++; // 计数器加一
if (this.len > 5) { // 如果超过5次循环将移除的元素重新加回末尾
data = data.concat(em);
}
// 设置下一个定时器
this.timer = setTimeout(() => {
this.dispose(data); // 递归调用自身
}, 200);
}
}
}
2 months ago
// 事件统计组件
const eventCount = {
components: {
historyTrendGraph,
},
template: `<div class='event-count-box'>
<history-trend-graph :data='graphInfo' />
</div>`,
data() {
return {
}
},
props: ['time', 'selectedKey', 'treeData'],
computed: {
graphInfo() {
return {
key: this.selectedKey,
label: findMonitorNameByKey(this.treeData, this.selectedKey),
startTime: this.time[0],
endTime: this.time[1],
};
}
},
methods: {
}
}
2 months ago
createApp({
setup() {
const typeOfCheckable = ['historyTrend', 'countPRPD', 'prpdAndPrps']
2 months ago
const timeSelectArr = ['historyTrend', 'countPRPD', 'prpdAndPrps', 'eventCount'] // 含有时间选择的组件
2 months ago
const treeData = ref([])
const fullTreeData = ref([]) // 懒加载后的完整菜单数据
const checkedKeys1 = ref([]); // 勾选
const selectedKey = 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('historyTrend')
2 months ago
const tabsArr = [
{ key: 'historyTrend', tab: '历史趋势' },
{ key: 'alarmConfig', tab: '告警管理' },
{ key: 'countPRPD', tab: '累计PRPD' },
{ key: 'prpdAndPrps', tab: 'PRPD/PRPS' },
{ key: 'eventCount', tab: '事件统计' },
{ key: 'config', tab: '配置' }
]
const isImportMonitorModalShow = ref(false)
const importMonitorModalloading = ref(false)
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 = (checkedKeys, { checkedKeys: currentKeys }) => {
if (currentKeys.length > 6) {
message.warning('最多只能勾选6个节点');
// 阻止勾选:回退到前一次的状态
checkedKeys1.value = checkedKeys1.value.slice(0, 6);
treeRef.value.setCheckedKeys(checkedKeys1.value);
} else {
checkedKeys1.value = currentKeys
}
}
const selectTree = (node) => {
if (node.isRoot) {
selectedKey.value = ''
} else {
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,
...item
}
})
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 onImportMonitor = () => {
}
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]) => {
2 months ago
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,
};
2 months ago
});
});
// 告警信息
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: []
})
// 请求告警信息表格数据
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();
};
// 告警管理监听
watch(selectedKey, (key) => {
if (!key || unref(activeKey) !== 'alarmConfig') return
// 初始化筛选数据
Object.assign(alarmFilters, {
times: [],
isCancel: '',
pdTypes: []
});
// 请求数据
fetchAlarmGridData();
});
// 累计prpd
const totalPrpds = ref([
{
monitorKey: ''
},
{
monitorKey: ''
},
{
monitorKey: ''
},
{
monitorKey: ''
},
{
monitorKey: ''
},
{
monitorKey: ''
},
])
// 配置页面
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([]) // 所有路径配置
2 months ago
const isCurrentInit = ref(false) // 是否正在初始化
const initTips = ref('')
2 months ago
// 获取所有配置项
const getAllConfigs = async () => {
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] || ''
})
2 months ago
// 排序
const itemIdToMove = "MERGIN_ROOT_PATH";
const itemToMove = unref(allPathCongfigs).find(item => item.key === itemIdToMove);
const remainingItems = unref(allPathCongfigs).filter(item => item.key !== itemIdToMove);
if (itemToMove) {
allPathCongfigs.value = [...remainingItems, itemToMove];
}
2 months ago
}
// 保存路径
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 getInitPrgress = async () => {
const { data: { message } } = await axios.get('/ldpdtools/config/getInitProgress')
isCurrentInit.value = message.includes('/')
initTips.value = message
}
// 初始化目录
const initCatalogue = async () => {
const { data: { code } } = await axios.get('/ldpdtools/config/initConfig')
if (code === 200) {
setTimeout(() => {
getInitPrgress()
}, 1000) // 后台有一定延迟
}
}
2 months ago
// 监听tab切换
watch(activeKey, (tabKey, oldKey) => {
2 months ago
// 先注释切换tab不清空树选中状态
// checkedKeys1.value = []
// treeRef.value.setCheckedKeys([]);
// selectedKey.value = ''
// treeRef.value.setCurrentKey(null);
2 months ago
if (oldKey === 'historyTrend') {
// 销毁历史趋势数据
2 months ago
// trendGraphData.value = []
// dateRange.value = []
2 months ago
}
if (oldKey === 'alarmConfig') {
// 销毁告警管理
Object.assign(alarmFilters, {
times: [],
isCancel: '',
pdTypes: []
});
dataSource.value = []
}
if (oldKey === 'prpdAndPrps') {
// 销毁prpd/Prps
unref(prpdAndPrpsRefs).forEach(item => {
item.closeTimer()
})
prpsData.forEach(item => {
item.prpsChart?.dispose()
})
}
});
onMounted(() => {
initTreeData()
getAllConfigs()
2 months ago
getInitPrgress()
2 months ago
document.addEventListener('click', handleClickOutside);
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
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,
2 months ago
timeSelectArr,
2 months ago
columns,
dataSource,
pagination,
configLoading,
handleTableChange,
selectedKey,
selectTree,
cancelOps,
alarmFilters,
pdTypeOps,
deleteAlarmTimes,
deleteAlarmConfirm,
fetchAlarmGridData,
initAlarmGridData,
stationName1,
alarmCustomUpload,
alarmBeforeUpload,
defaultProps,
totalPrpds,
allPathCongfigs,
savePathConfig,
resetPathConfig,
2 months ago
prpdAndPrpsRefs,
initCatalogue,
isCurrentInit,
initTips,
trendGraphRefs
2 months ago
};
},
components: {
totalPrpd,
2 months ago
prpdAndPrps,
eventCount,
historyTrendGraph
2 months ago
}
})
.use(antd)
.use(ElementPlus, {
locale: ElementPlusLocaleZhCn // 使用中文语言包
})
.mount('#app');
</script>
</body>
</html>