feat:工具细节完成

master
guoyixuan 2 months ago
parent 49a2bedc31
commit fddf53520c

@ -1,23 +1,71 @@
{ {
// // ====== 1. ======
"editor.fontSize": 14, // "editor.fontSize": 14, //
"editor.wordWrap": "on", // "editor.lineHeight": 24, //
"editor.tabSize": 2, // 2Vue "editor.tabSize": 2, // Tab 2
"editor.insertSpaces": true, // Tab
"editor.detectIndentation": false, // 使 tabSize
"editor.renderWhitespace": "none", // 'all'/'boundary'/'none'
"editor.minimap.enabled": false, //
"editor.wordWrap": "off", // Prettier
"editor.cursorSmoothCaretAnimation": "on", //
"editor.smoothScrolling": true, //
// ====== 2. (Prettier) ======
"editor.defaultFormatter": "esbenp.prettier-vscode", // Prettier
"editor.formatOnSave": true, // "editor.formatOnSave": true, //
// Vue "editor.formatOnPaste": false, // false
"[vue]": { "editor.formatOnType": false, // false
"editor.defaultFormatter": "esbenp.prettier-vscode" // 使Vetur
// ====== 3. Prettier ======
// .prettierrc .prettierrc .prettierrc
"prettier.printWidth": 100, //
"prettier.tabWidth": 2, // 2
"prettier.useTabs": false, // 使 Tab 使
"prettier.singleQuote": true, // 使
"prettier.semi": false, //
"prettier.trailingComma": "es5", // ES5
"prettier.bracketSpacing": true, //
"prettier.jsxBracketSameLine": false, // JSX
"prettier.arrowParens": "avoid", // (e.g., x => x (x) => x)
"prettier.endOfLine": "lf", // LF (Linux Line Feed)
"prettier.vueIndentScriptAndStyle": true, // Vue <script> <style>
// ====== 4. ESLint ======
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue", // ESLint .vue
"html", // HTML
"json", // JSON
"jsonc" // JSON With Comments
],
// ESLint ESLint
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}, },
"vetur.format.defaultFormatter.html": "js-beautify-html", // HTML
"vetur.format.defaultFormatterOptions": { // ====== 5. Vue (Volar) ======
"js-beautify-html": { // Volar Vue 3
"wrap_line_length": 120, // "volar.autoCreateQuotes": true, //
"wrap_attributes": "auto", // auto "volar.completion.preferredTagCasing": "auto", //
"end_with_newline": false // "volar.jsx.autoComplete": true,
}
}, // ====== 6. ======
// Prettier //
"prettier.singleQuote": true, // // "files.associations": {
"prettier.semi": false, // // "*.vue": "vue"
"files.saveConflictResolution": "overwriteFileOnDisk" // },
// ====== 7. ======
"files.encoding": "utf8", //
"files.eol": "\n", // LF (Unix/Linux)
"files.trimTrailingWhitespace": true, //
"files.insertFinalNewline": true, //
"telemetry.telemetryLevel": "off" //
// ====== 8. ======
// "terminal.integrated.fontSize": 13
} }

@ -150,7 +150,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.graph-container .title {
display: flex;
align-items: center;
padding: 4px 12px;
}
.graph-container .title .time-range {
margin-left: 24px;
}
.graph-container .line-graph { .graph-container .line-graph {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@ -365,11 +372,6 @@
margin-left: 8px; margin-left: 8px;
} }
.total-prpd-panel .total-prpd-panel .prpd-box .totle-prpds .total-prpd-item .switcher .active-span {
.prpd-box
.totle-prpds
.total-prpd-item
.switcher
.active-span {
color: #1890ff; color: #1890ff;
} }

@ -5,9 +5,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tool</title> <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/vue.global.min.js"></script>
<script src="./js/antd.min.js"></script> <script src="./js/antd.min.js"></script>
<link rel="stylesheet" href="./css/antd.min.css"> <link rel="stylesheet" href="./css/antd.min.css">
@ -36,8 +33,7 @@
:style="{ left: `${treeMenuInfo.menuX}px`, top: `${treeMenuInfo.menuY}px` }"> :style="{ left: `${treeMenuInfo.menuX}px`, top: `${treeMenuInfo.menuY}px` }">
<div v-if="treeMenuInfo.data.isRoot" @click="addMonitor(treeMenuInfo.data)">新增监测点</div> <div v-if="treeMenuInfo.data.isRoot" @click="addMonitor(treeMenuInfo.data)">新增监测点</div>
<div @click="editNodeItem(treeMenuInfo.data)">编辑</div> <div @click="editNodeItem(treeMenuInfo.data)">编辑</div>
<a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消" <a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteNodeItem(treeMenuInfo.data)">
@confirm="deleteNodeItem(treeMenuInfo.data)">
<div>删除</div> <div>删除</div>
</a-popconfirm> </a-popconfirm>
</div> </div>
@ -53,8 +49,8 @@
:default-expanded-keys="defaultExpendKeys" @check="checkTree" @node-click="handleNodeClick" :default-expanded-keys="defaultExpendKeys" @check="checkTree" @node-click="handleNodeClick"
:filter-node-method="filterNode" @node-contextmenu="handleRightClick"> :filter-node-method="filterNode" @node-contextmenu="handleRightClick">
<template #default="{ node, data }"> <template #default="{ node, data }">
<div :class="[selectedKey===data.key?'hight-light-node':'','node-content']" <div :class="[selectedKey===data.key?'hight-light-node':'','node-content']" :title="node.label"
:title="node.label" @click.stop="handleNodeContentClick(data)"> @click.stop="handleNodeContentClick(data)">
{{ node.label }} {{ node.label }}
</div> </div>
</template> </template>
@ -73,8 +69,8 @@
</a-tabs> </a-tabs>
<div class="top-timer" v-if="timeSelectArr.includes(activeKey)"> <div class="top-timer" v-if="timeSelectArr.includes(activeKey)">
<el-date-picker v-model="dateRange" style="width: 260px" class="custom-date-picker" <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="至" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
start-placeholder="开始日期" end-placeholder="结束日期" /> end-placeholder="结束日期" />
</div> </div>
</div> </div>
<div class="panel-views"> <div class="panel-views">
@ -83,14 +79,14 @@
<div class="search-bar"> <div class="search-bar">
<div class="search-item"> <div class="search-item">
<span>放电类型:</span> <span>放电类型:</span>
<a-select v-model:value="alarmFilters.pdTypes" :options="pdTypeOps" mode="tags" <a-select v-model:value="alarmFilters.pdTypes" :options="pdTypeOps" mode="tags" placeholder="请选择"
placeholder="请选择" style="width: 300px"> style="width: 300px">
</div> </div>
<div class="search-item"> <div class="search-item">
<span>时间:</span> <span>时间:</span>
<el-date-picker v-model="alarmFilters.times" style="width: 360px" <el-date-picker v-model="alarmFilters.times" style="width: 360px" class="custom-date-picker"
class="custom-date-picker" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /> end-placeholder="结束日期" />
</div> </div>
<div class="break"></div> <div class="break"></div>
<div class="search-item"> <div class="search-item">
@ -105,9 +101,8 @@
</div> </div>
</div> </div>
<div class="grid-content"> <div class="grid-content">
<a-table :columns="columns" :data-source="dataSource" :pagination="pagination" <a-table :columns="columns" :data-source="dataSource" :pagination="pagination" :loading="configLoading"
:loading="configLoading" @change="handleTableChange" :scroll="{ y: 460 }" @change="handleTableChange" :scroll="{ y: 460 }" :custom-row="alarmRowClick">
:custom-row="alarmRowClick">
<template #name1="{ text }"> <template #name1="{ text }">
{{pdTypeOps.find(item=>item.value==text).label}} {{pdTypeOps.find(item=>item.value==text).label}}
</template> </template>
@ -120,8 +115,7 @@
<div v-if="activeKey === 'historyTrend'" key="historyTrend" class="sub-content history-trend"> <div v-if="activeKey === 'historyTrend'" key="historyTrend" class="sub-content history-trend">
<div class="trend-graph-container"> <div class="trend-graph-container">
<history-trend-graph v-for="(item,index) in trendGraphData" :key="item.key" <history-trend-graph v-for="(item,index) in trendGraphData" :key="item.key"
:ref="el => { if (el) trendGraphRefs[index] = el }" :data="item" :ref="el => { if (el) trendGraphRefs[index] = el }" :data="item" @jump-to-prpd="jumpToPRPD" />
@jump-to-prpd="jumpToPRPD" />
</div> </div>
</div> </div>
<div v-if="activeKey === 'prpdAndPrps'" key="prpdAndPrps" class="sub-content total-prpd-panel"> <div v-if="activeKey === 'prpdAndPrps'" key="prpdAndPrps" class="sub-content total-prpd-panel">
@ -132,33 +126,32 @@
<div class="prpd-box"> <div class="prpd-box">
<!-- 累计PRPD组件 --> <!-- 累计PRPD组件 -->
<div class="totle-prpds"> <div class="totle-prpds">
<prpd-and-prps-comp class="total-prpd-item" v-for="(item,index) in totalPrpds" <prpd-and-prps-comp class="total-prpd-item" v-for="(item,index) in totalPrpds" :key="index" :data="item"
:key="index" :data="item" :time="prpdAndPrpsTimes" :time="prpdAndPrpsTimes" :is-count-prpd="isTypeOfCountPrpd" :fiter-data="eventFilterForm" />
:is-count-prpd="isTypeOfCountPrpd" />
</div> </div>
<div class="time-sider"> <div class="time-sider">
<div class="title"> <div class="title">
<span> <span>
事件: 事件:
</span> </span>
<el-popover placement="right" :width="320" trigger="click" <el-popover placement="right" :width="320" trigger="click" :visible="everntFilterVisible">
:visible="everntFilterVisible">
<template #reference> <template #reference>
<el-button @click="everntFilterVisible=!everntFilterVisible">筛选</el-button> <el-button @click="everntFilterVisible=!everntFilterVisible">筛选</el-button>
</template> </template>
<el-form :inline="true" :model="eventFilterForm" label-width="auto" <el-form :inline="true" :model="eventFilterForm" label-width="auto" label-suffix=":"
label-suffix=":" class="demo-form-inline"> class="demo-form-inline">
<el-form-item label="脉冲数"> <el-form-item label="脉冲数">
<el-input v-model="eventFilterForm.plusCount" placeholder="请输入" /> <el-input v-model="eventFilterForm.plusCount" placeholder="请输入" type="number" min="0"
@input="validatePlusCount" />
</el-form-item> </el-form-item>
<el-form-item label="幅值"> <el-form-item label="幅值">
<el-input v-model="eventFilterForm.maxValue" placeholder="请输入" /> <el-input v-model="eventFilterForm.maxValue" placeholder="请输入" type="number" />
</el-form-item> </el-form-item>
<el-form-item label="放电类型"> <el-form-item label="放电类型">
<el-select v-model="eventFilterForm.pdTypes" multiple collapse-tags <el-select v-model="eventFilterForm.pdTypes" multiple collapse-tags collapse-tags-tooltip
collapse-tags-tooltip style="width: 240px" placeholder="请选择"> style="width: 240px" placeholder="请选择">
<el-option v-for="item in allPdTypes" :key="item.value" <el-option v-for="item in allPdTypes" :key="item.value" :label="item.label"
:label="item.label" :value="item.value" /> :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -170,8 +163,7 @@
</div> </div>
<div class="list" v-loading="eventListLoading"> <div class="list" v-loading="eventListLoading">
<div :class="['event-item',activeEventTime==item.time?'active-event-item':'']" <div :class="['event-item',activeEventTime==item.time?'active-event-item':'']"
v-for="(item,index) in eventList" :key="item.time" v-for="(item,index) in eventList" :key="item.time" @click="handleEventClick(item)">
@click="handleEventClick(item)">
<div class="time"> <div class="time">
<span class="label">时间:</span> <span class="label">时间:</span>
{{item.time}} {{item.time}}
@ -213,9 +205,8 @@
<div class="delete-bar"> <div class="delete-bar">
<span class="delete-time"> <span class="delete-time">
<span class="label">清除告警时间段:</span> <span class="label">清除告警时间段:</span>
<el-date-picker v-model="deleteAlarmTimes" style="width: 360px" <el-date-picker v-model="deleteAlarmTimes" style="width: 360px" class="custom-date-picker"
class="custom-date-picker" value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="至" start-placeholder="开始日期"
type="datetimerange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" /> end-placeholder="结束日期" />
</span> </span>
<a-popconfirm title="确定删除?" ok-text="是" cancel-text="否" @confirm="deleteAlarmConfirm"> <a-popconfirm title="确定删除?" ok-text="是" cancel-text="否" @confirm="deleteAlarmConfirm">
@ -240,9 +231,8 @@
</div> </div>
<a-button size="small" type="primary" style="margin-right: 4px;" <a-button size="small" type="primary" style="margin-right: 4px;"
@click="savePathConfig(item)">保存</a-button> @click="savePathConfig(item)">保存</a-button>
<a-button v-if="item.key==='MERGIN_ROOT_PATH'" size="small" type="primary" <a-button v-if="item.key==='MERGIN_ROOT_PATH'" size="small" type="primary" style="margin-right: 4px;"
style="margin-right: 4px;" :loading="isCurrentInit" :loading="isCurrentInit" @click="initCatalogue">初始化</a-button>
@click="initCatalogue">初始化</a-button>
<a-button size="small" @click="resetPathConfig(item.key)">重置</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"> <p v-if="item.key==='MERGIN_ROOT_PATH'&&isCurrentInit" class="init-warning-tips">
当前初始化进度:{{initTips}} 当前初始化进度:{{initTips}}
@ -254,8 +244,31 @@
</div> </div>
<!-- 导入监测点弹框 --> <!-- 导入监测点弹框 -->
<a-modal width="550px" :destroyOnClose="true" :visible="isImportMonitorModalShow" title="导入监测点" <a-modal width="550px" :destroyOnClose="true" :visible="isImportMonitorModalShow" title="导入监测点"
@ok="onImportMonitor" @cancel="isImportMonitorModalShow=false" @ok="onImportMonitor" @cancel="isImportMonitorModalShow=false" :confirmLoading="importMonitorModalloading"
:confirmLoading="importMonitorModalloading"></a-modal> 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" <a-modal width="550px" :destroyOnClose="true" :visible="isSiteModalShow"
:title="isAddTypeOfSiteModal?'新增站点':'编辑站点'" @cancel="isSiteModalShow=false"> :title="isAddTypeOfSiteModal?'新增站点':'编辑站点'" @cancel="isSiteModalShow=false">
@ -405,6 +418,28 @@
const seconds = String(date.getSeconds()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 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([]); const trendGraphRefs = ref([]);
watch(trendGraphRefs, (arr) => { watch(trendGraphRefs, (arr) => {
@ -422,13 +457,30 @@
const historyTrendGraph = { const historyTrendGraph = {
template: `<div class='graph-container'> template: `<div class='graph-container'>
<div class="title"> <div class="title">
监测点:{{ data?.label||'' }} <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>
<div ref='graphRef' class="line-graph" @dblclick='onDblclickGraph'></div> <div ref='graphRef' class="line-graph" @dblclick='onDblclickGraph'></div>
</div>`, </div>`,
data() { data() {
return { return {
resizeObserver: null 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: { props: {
@ -556,60 +608,62 @@
series, series,
dataZoom: [ dataZoom: [
{ {
type: 'slider', // 滑动条型数据区域缩放 type: 'inside', // 内置型数据区域缩放
xAxisIndex: [0], xAxisIndex: [0], // 作用于第一个或所有X轴
start: 0, start: 0, // 数据窗口范围的起始百分比0%
end: 100, end: 100, // 数据窗口范围的结束百分比100%
top: 20, // 距离底部的距离 // 以下是 type: 'inside' 独有的配置项,控制交互
height: 10, // 滑动条高度 zoomOnMouseWheel: true, // 鼠标滚轮缩放
throttle: 500, moveOnMouseMove: true, // 鼠标移动(拖拽)平移
showDetail: false // 不显示详细的起止数值,根据需求设置 moveOnMouseWheel: false, // 鼠标滚轮不平移,只缩放
filterMode: 'filter' // 数据过滤模式,'filter' 会过滤掉超出范围的数据
}]
,
brush: {
brushLink: 'all', // 刷选联动所有系列
xAxisIndex: 'all', // 允许在所有 X 轴上进行刷选 (这里只有一个 X 轴)
yAxisIndex: 'all', // 允许在所有 Y 轴上进行刷选
brushMode: 'rect', // **只允许矩形框选**
throttleType: 'debounce', // 内部刷选状态更新的节流模式
throttleDelay: 300, // 节流延迟
},
toolbox: {
show: this.usedTab === 'prpdAndPrps', // 只有prpd/prps展示
feature: {
// brush 工具箱,包含矩形框选和清除按钮
brush: {
type: ['lineX',] // 只显示矩形框选工具和清除刷选工具
},
},
left: 30, // 工具箱位置
top: '0'
} }
]
} }
// 3. 设置配置项并渲染图表 // 3. 设置配置项并渲染图表
this.chart.setOption(option); this.chart.setOption(option);
this.addEchartsEvents(); // 监听 'brushEnd' 事件,在用户松开鼠标完成框选时触发,用于获取最终结果
this.chart.on('brushEnd', this.handleBrushEnd);
if (this.usedTab === 'prpdAndPrps') { if (this.usedTab === 'prpdAndPrps') {
this.handleDataZoom({ start: 0, end: 100 }) this.loadInitTimeData()
} }
}, },
addEchartsEvents() { // 加载初始化时间数据
if (!this.chart || this.usedTab !== 'prpdAndPrps') return; loadInitTimeData() {
this.chart.on('datazoom', (params) => { const xAxisData = this.chart.getOption().xAxis[0].data?.map(item => item.replace(/\n/g, ' ')) || [];
this.handleDataZoom(params) const index = getLastIndexWithinMaxTime(xAxisData, this.maxInMs)
}); if (index < 0) {
}, return message.error(`当前数据为空!`);
// 外界触发筛选逻辑
emitFilterEvent() {
const { start, end } = this.chart.getOption()?.dataZoom?.[0] || {}
this.handleDataZoom({ start, end })
},
// 获取时间段
handleDataZoom(params) {
const startPercent = params.start;
const endPercent = params.end;
if (startPercent === undefined || endPercent === undefined) {
return;
}
// 获取当前的 ECharts option特别是 xAxis 的 data
const currentOption = this.chart.getOption();
const xAxisData = currentOption.xAxis[0].data; // 假设你的X轴是第一个 (index 0)
if (!xAxisData || xAxisData.length === 0) {
this.$emit('getEventList', { startTime: null, endTime: null })
return message.warning(`当前监测点历史趋势为空,请重新选择监测点!`);
}
const dataLength = xAxisData.length;
const startIndex = Math.floor(dataLength * (startPercent / 100));
const endIndex = Math.min(Math.ceil(dataLength * (endPercent / 100)) - 1, dataLength - 1); // -1 是因为索引从0开始
const startCategory = xAxisData[startIndex];
const endCategory = xAxisData[endIndex];
const startTime = startCategory ? String(startCategory).replace(/\n/g, ' ') : '';
const endTime = endCategory ? String(endCategory).replace(/\n/g, ' ') : '';
if (!startTime || !endTime) {
return
} }
this.$emit('getEventList', { startTime, endTime }) 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() { onDblclickGraph() {
@ -621,6 +675,69 @@
} }
this.$emit('jumpToPrpd', this.data.key) this.$emit('jumpToPrpd', this.data.key)
}, },
handleBrushEnd(params) {
const brushAreas = params.areas;
// 如果没有框选区域(例如用户只是点击了一下,或者点击了工具箱的清除按钮)
if (!brushAreas || brushAreas.length === 0) {
this.lastBrushSelected = null; // 清除上次的选中状态
this.loadInitTimeData()
return;
}
// 获取第一个(也是唯一一个)刷选区域的信息
// 由于你在 brushstart 中清除了旧框,这里通常只会有一个 areas 元素。
const brushArea = brushAreas[0];
let minXValue = null;
let maxXValue = null;
// `coordRanges` 针对 'category' 类型的 X 轴,会直接给出数据索引的范围
if (brushArea.coordRanges && brushArea.coordRanges.length > 0) {
const xAxisCoordRange = brushArea.coordRanges[0];
if (xAxisCoordRange && xAxisCoordRange.length > 0) {
const rawMinXIndex = xAxisCoordRange[0]; // 获取X轴的最小索引 (例如: 0)
const rawMaxXIndex = xAxisCoordRange[1]; // 获取X轴的最大索引 (例如: 6)
// 获取 ECharts 配置中 X 轴的所有数据(时间字符串数组)
const xAxisData = this.chart.getOption().xAxis[0].data;
// 根据索引从 xAxisData 中取出对应的实际时间值
if (xAxisData[rawMinXIndex] !== undefined && xAxisData[rawMaxXIndex] !== undefined) {
// 假设你的时间字符串可能包含换行符 '\n',这里将其替换为空格以统一格式
minXValue = String(xAxisData[rawMinXIndex]).replace(/\n/g, ' ');
maxXValue = String(xAxisData[rawMaxXIndex]).replace(/\n</g, ' ');
} else {
this.$emit('getEventList', { startTime: null, endTime: null })
}
}
}
// 如果未能从 `coordRanges` 中解析出有效的 X 轴时间范围
if (!minXValue || !maxXValue) {
message.warning(`未能从 brush 选中区域中解析出有效的 X 轴时间范围!`);
this.$emit('getEventList', { startTime: null, endTime: null })
this.lastBrushSelected = null;
return;
}
// 这在 brushEnd 事件中依然有用,例如用户连续两次选择完全相同的范围
if (this.lastBrushSelected &&
this.lastBrushSelected.minX === minXValue &&
this.lastBrushSelected.maxX === maxXValue) {
return;
}
const startTime = minXValue ? String(minXValue).replace(/\n/g, ' ') : '';
const endTime = maxXValue ? String(maxXValue).replace(/\n/g, ' ') : '';
const xAxisData = this.chart.getOption().xAxis[0].data;
const isOver15Min = new Date(endTime).getTime() - new Date(startTime).getTime() > this.maxInMs;
if (isOver15Min) {
this.chart.dispatchAction({
type: 'brush',
command: 'clear', // 'clear' 命令用于清除所有刷选区域
areas: [] // 空数组表示清除所有
});
this.$emit('getEventList', { startTime: null, endTime: null })
const warningLable = this.timeOps.find(item => item.value === this.maxInMs)?.label || ''
return message.error(`数据加载过多,请框选时间区域不超过${warningLable}`);
}
// 更新上次的选中状态
this.lastBrushSelected = { minX: startTime, maxX: endTime };
// 通过事件将框选的 X 轴范围传递给父组件
this.$emit('getEventList', { startTime, endTime })
},
getChart() { getChart() {
return this.chart; return this.chart;
} }
@ -641,7 +758,7 @@
{{item}}</span> {{item}}</span>
</div> </div>
</div>`, </div>`,
props: ['data', 'time', 'isCountPrpd'], props: ['data', 'time', 'isCountPrpd', 'fiterData'],
data() { data() {
return { return {
currentKey: 'PRPD', currentKey: 'PRPD',
@ -822,6 +939,8 @@
startTime: data.time[0], startTime: data.time[0],
endTime: data.time[1], endTime: data.time[1],
monitorKey: data.monitorKey, monitorKey: data.monitorKey,
...this.fiterData,
pdTypes: this.fiterData.pdTypes.join(','),
} }
const { data: { result: res } } = await axios.get(`/ldpdtools/eventData/getPrpdByParam`, { const { data: { result: res } } = await axios.get(`/ldpdtools/eventData/getPrpdByParam`, {
params, params,
@ -921,7 +1040,6 @@
} }
} }
lay1.push(lay2) lay1.push(lay2)
// console.log(lay1)
} }
for (let i = 0; i < lay1.length; i++) { for (let i = 0; i < lay1.length; i++) {
for (let j = 0; j < lay1[i].length; j++) { for (let j = 0; j < lay1[i].length; j++) {
@ -933,7 +1051,6 @@
dataArr.push(lay1[i][j][0]); dataArr.push(lay1[i][j][0]);
} }
} }
// console.log(dataArr)
return dataArr; return dataArr;
}, },
// 初始化prps // 初始化prps
@ -1254,6 +1371,79 @@
] ]
const isImportMonitorModalShow = ref(false) const isImportMonitorModalShow = ref(false)
const importMonitorModalloading = 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 siteModalloading = ref(false)
const isSiteModalShow = ref(false) const isSiteModalShow = ref(false)
const siteModalRef = ref() const siteModalRef = ref()
@ -1404,10 +1594,6 @@
const { stationName } = data const { stationName } = data
monitorModalForm.stationName = stationName monitorModalForm.stationName = stationName
isMonitorModalShow.value = true isMonitorModalShow.value = true
}
// 导入监测点
const onImportMonitor = () => {
} }
const addSite = () => { const addSite = () => {
isSiteModalShow.value = true isSiteModalShow.value = true
@ -1703,6 +1889,18 @@
endTime: unref(dateRange)[1], 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 }) => { const getEventList = async ({ startTime, endTime }) => {
if (startTime === null && endTime === null) { if (startTime === null && endTime === null) {
@ -1743,7 +1941,7 @@
} }
// 筛选 // 筛选
const onFilterEventList = () => { const onFilterEventList = () => {
trendRef.value.emitFilterEvent() trendRef.value.loadInitTimeData()
everntFilterVisible.value = false everntFilterVisible.value = false
} }
// 重置筛选框 // 重置筛选框
@ -1895,30 +2093,61 @@
message.success('重置成功!') message.success('重置成功!')
getAllConfigs() getAllConfigs()
} }
const progressTimer = ref(null)
// 获取初始化进度 // 获取初始化进度
const getInitPrgress = async () => { const getInitPrgress = async () => {
try {
const { data: { message } } = await axios.get('/ldpdtools/config/getInitProgress') const { data: { message } } = await axios.get('/ldpdtools/config/getInitProgress')
isCurrentInit.value = message.includes('/') isCurrentInit.value = message.includes('/')
initTips.value = message initTips.value = message
if (!isCurrentInit.value) {
clearInterval(progressTimer.value); // 停止定时器
progressTimer.value = null; // 将定时器ID置空以防误操作
}
} catch (error) {
clearInterval(progressTimer.value);
progressTimer.value = null;
}
} }
// 初始化目录
const initCatalogue = async () => { const initCatalogue = async () => {
const { data: { code } } = await axios.get('/ldpdtools/config/initConfig') // 在启动新的初始化前,确保清除任何正在运行的定时器
if (code === 200) { if (unref(progressTimer)) {
setTimeout(() => { clearInterval(progressTimer.value);
getInitPrgress() progressTimer.value = null;
}, 1000) // 后台有一定延迟
} }
try {
const { data: { code } } = await axios.get('/ldpdtools/config/initConfig');
if (code === 200) {
// 立即执行一次进度获取,获取最新状态
await getInitPrgress();
// 如果初始化尚未完成 (isCurrentInit.value 仍为 true),则启动轮询
if (isCurrentInit.value) {
progressTimer.value = setInterval(getInitPrgress, 2000);
console.log('初始化配置成功,进度定时器已启动。');
} else {
console.log('初始化已完成,无需启动进度轮询。');
}
} else {
console.error('初始化配置请求失败code:', code);
} }
} catch (error) {
console.error('初始化目录时发生网络或服务器错误:', error);
}
};
onMounted(() => { onMounted(() => {
initTreeData() initTreeData()
getAllConfigs() getAllConfigs()
getInitPrgress() progressTimer.value = setInterval(getInitPrgress, 2000);
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
if (unref(progressTimer)) {
clearInterval(progressTimer.value);
progressTimer.value = null;
}
}); });
return { return {
treeRef, treeRef,
@ -2000,7 +2229,15 @@
onFilterEventList, onFilterEventList,
onResetEventList, onResetEventList,
trendRef, trendRef,
handleNodeContentClick handleNodeContentClick,
importMonitorForm,
handleFileChange,
beforImportMonitor,
importMonitorRules,
importMonitorFormRef,
handleImportMonitorFileRemove,
monitorImportRef,
validatePlusCount
}; };
}, },
components: { components: {

Loading…
Cancel
Save