Files
admin-sjzx/src/views/pqs/harmonicMonitoring/area/getIntegrityData/index.vue
2026-05-22 09:58:07 +08:00

629 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="default-main online">
<div class="online_header">
<TableHeader date-picker ref="tableHeaderRef">
<template #select>
<el-form-item label="统计类型">
<el-select v-model="formData.statisticalType" placeholder="请选择统计类型" value-key="id"
style="width: 100%">
<el-option v-for="item in classificationData" :key="item.id" :label="item.name"
:value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="区域选择">
<!-- <el-select ref="select1" v-model="deptName" placeholder="请选择所属部门区域" style="width: 100%">
<el-option :value="formData.deptIndex" style="height: auto"> -->
<!-- {{ formData.deptIndex }} -->
<el-cascader v-model="formData.deptIndex" :props="defaultProps" :options="treeData" filterable
collapse-tags placeholder="请选择区域" />
<!-- <el-tree
ref="tree"
v-model="formData.deptName"
:data="treeData"
node-key="id"
accordion
:default-expanded-keys="idArr"
:props="defaultProps"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span :title="data.name">{{ data?.name }}</span>
</template>
</el-tree> -->
<!-- </el-option>
</el-select> -->
</el-form-item>
<el-form-item label="电压等级">
<el-select v-model="formData.scale" multiple collapse-tags clearable placeholder="请选择电压等级"
style="width: 100%" value-key="id">
<el-option v-for="item in voltageleveloption" :key="item.id" :label="item.name"
:value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="终端厂家">
<el-select v-model="formData.manufacturer" multiple collapse-tags clearable
placeholder="请选择终端厂家" style="width: 100%" value-key="id">
<el-option v-for="(item, index) in terminaloption" :key="index" :label="item.name"
:value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="干扰源类型">
<el-select v-model="formData.loadType" multiple collapse-tags clearable placeholder="请选择干扰源类型"
style="width: 100%" value-key="id">
<el-option v-for="(item, index) in interfereoption" :key="index" :label="item.name"
:value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="筛选数据">
<el-input v-model="tableStore.table.params.searchValue" clearable placeholder="请输入关键字"></el-input>
</el-form-item>
</template>
<template #operation>
<el-button type="primary" icon="el-icon-Tickets" @click="makeUp" v-if="!VITE_FLAG">补招</el-button>
</template>
</TableHeader>
</div>
<div class="online_main">
<el-tabs v-model="activeName" type="border-card" @tab-click="handleClick">
<el-tab-pane :name="0" :lazy="true" label="数据完整性列表">
<Table ref="tableRef" :tree-config="{ transform: true, parentField: 'uPid', rowField: 'uId' }"
:checkbox-config="{ labelField: 'name', checkMethod: ({ row }) => true }"
:scroll-y="{ enabled: true }" v-if="activeName == 0" />
</el-tab-pane>
<el-tab-pane :name="1" :lazy="true" label="数据完整性图表">
<charts v-if="activeName == 1" ref="chartsRef" />
</el-tab-pane>
</el-tabs>
</div>
<el-dialog v-model="timePopUp" draggable title="补招" width="500">
补招时间:
<el-date-picker v-model="timeData" type="datetimerange" format="YYYY-MM-DD HH:mm:00"
value-format="YYYY-MM-DD HH:mm:00" range-separator="" date-format="YYYY-MM-DD" time-format="HH:mm:00"
start-placeholder="开始日期" end-placeholder="结束日期" style="width: 400px" :disabledDate="disabledDate" />
<template #footer>
<div class="dialog-footer">
<el-button @click="timePopUp = false">取消</el-button>
<el-button type="primary" @click="makeUpSubmit">确认</el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="logPopUp" draggable title="补招日志" width="800" @close="close">
<div class="logList" ref="logRef">
<p v-for="item in logList" :style="{
color: item.type === 'error' ? '#F56C6C' : ''
}">
<div style="width: 150px;"> {{ item.time }}</div>
<div style="flex: 1;">{{ item.name }}</div>
</p>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, nextTick } from 'vue'
import { useDictData } from '@/stores/dictData'
import DatePicker from '@/components/form/datePicker/index.vue'
import { getAreaDept, FullRecall } from '@/api/harmonic-boot/area'
import TableHeader from '@/components/table/header/index.vue'
import TableStore from '@/utils/tableStore'
import Table from '@/components/table/index.vue'
import charts from './components/charts.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import socketClient from '@/utils/webSocketClient'
import { formatDate } from '@/utils/formatTime'
import { useAdminInfo } from '@/stores/adminInfo'
const adminInfo = useAdminInfo()
defineOptions({
name: 'harmonic-boot/harmonic/getIntegrityData'
})
const VITE_FLAG = import.meta.env.VITE_NAME == 'hainan'
const logRef = ref()
const tableRef = ref()
const chartsRef = ref()
const dictData = useDictData()
const timePopUp = ref(false)
const logPopUp = ref(false)
//开始创建webSocket客户端
const dataSocket = reactive({
socketServe: socketClient.Instance
})
//字典获取电压等级
const voltageleveloption = dictData.getBasicData('Dev_Voltage_Stand')
//字典获取终端厂家
const terminaloption = dictData.getBasicData('Dev_Manufacturers')
//字典获取干扰源类型
const interfereoption = dictData.getBasicData('Interference_Source')
//字典获取统计类型
const classificationData = dictData.getBasicData('Statistical_Type', ['Report_Type'])
//调用区域接口获取区域
const treeData = ref([])
const idArr = ref([])
const timeData = ref([])
const logList: any = ref([])
const activeName = ref(0)
const getTreeData = async () => {
await getAreaDept().then(res => {
var data = res.data
data.forEach(element => {
idArr.value.push(element.id)
})
treeData.value = JSON.parse(JSON.stringify(res.data))
})
}
getTreeData()
const formData = ref({
statisticalType: classificationData[0], //统计类型
deptIndex: treeData.value[0]?.id, //区域选择
scale: voltageleveloption, //电压等级
manufacturer: terminaloption, //终端厂家
loadType: interfereoption //干扰源类型
// searchBeginTime: '',
// searchEndTime: ''
})
formData.value.deptIndex = treeData.value[0]?.id
const defaultProps = ref({
label: 'name',
value: 'id',
checkStrictly: true,
emitPath: false,
expandTrigger: 'click' as const
})
const handleClick = (tab: any, e: any) => {
// if(activeName.value===1){
tableStore.index()
// }
}
const tableHeaderRef = ref()
const tableStore = new TableStore({
publicHeight: 60,
showPage: false,
url: '/device-boot/LineIntegrityData/getLineIntegrityData',
method: 'POST',
column: [
{
title: formData.value.statisticalType.name,
field: 'name',
align: 'left',
type: 'checkbox',
treeNode: true,
width: 350,
formatter: function (row) {
return row.cellValue ? row.cellValue : '/'
}
},
{
title: '网络参数',
field: 'ip',
align: 'center',width:'120px',
formatter: function (row) {
return row.cellValue ? row.cellValue : '/'
}
},
{
title: '厂家',
field: 'manufacturer',
align: 'center',
formatter: function (row) {
return row.cellValue ? row.cellValue : '/'
}
},
{
title: '终端名称',
field: 'deviceName',
align: 'center',
formatter: function (row) {
return row.cellValue ? row.cellValue : '/'
}
},
{
title: '通讯状态',
field: 'comFlag',
align: 'center',
render: 'tag',
custom: {
null: 'primary',
0: 'danger',
1: 'success'
},
replaceValue: {
null: '/',
0: '中断',
1: '正常'
}
},
{
title: '最新数据时间',
field: 'updateTime',
align: 'center',
formatter: function (row) {
return row.cellValue ? row.cellValue : '/'
}
},
{
title: '完整性(%)',
field: 'integrityData',
align: 'center',
formatter: function (row) {
return row.cellValue == 3.14159 ? '暂无数据' : row.cellValue.toFixed(2)
}
},
{
title: '评估',
field: 'valueOver',
align: 'center',
render: 'tag',
custom: {
null: 'danger',
0: 'danger',
1: 'success',
2: 'primary',
3: 'danger'
},
replaceValue: {
null: '暂无评估',
0: '暂无评估',
1: '优秀',
2: '合格',
3: '不合格'
}
}
],
beforeSearchFun: () => {
tableStore.table.params.deptIndex = formData.value.deptIndex
tableStore.table.params.statisticalType = formData.value.statisticalType
tableStore.table.params.scale = formData.value.scale
tableStore.table.params.manufacturer = formData.value.manufacturer
tableStore.table.params.loadType = formData.value.loadType
tableStore.table.params.serverName = 'harmonicBoot'
delete tableStore.table.params.timeFlag
delete tableStore.table.params.startTime
delete tableStore.table.params.endTime
delete tableStore.table.params.pageNum
delete tableStore.table.params.pageSize
// tableStore.table.params.searchBeginTime = tableHeaderRef.value.datePickerRef.timeValue[0]
// tableStore.table.params.searchEndTime = tableHeaderRef.value.datePickerRef.timeValue[1]
},
loadCallback: () => {
tableStore.table.data = tree2List(filterTreeByKeyword( tableStore.table.data,tableStore.table.params.searchValue), Math.random() * 1000)
tableStore.table.column[0].title = formData.value.statisticalType.name
chartsRef.value && chartsRef.value.getTableStoreParams(tableStore.table.params)
setTimeout(() => {
activeName.value == 0 && tableRef.value && tableRef.value.getRef().setAllTreeExpand(true)
}, 0)
},
resetCallback: () => {
// 重置表单数据到默认值
formData.value.statisticalType = classificationData[0]
formData.value.deptIndex = treeData.value[0]?.id
formData.value.scale = voltageleveloption
formData.value.manufacturer = terminaloption
formData.value.loadType = interfereoption
}
})
tableStore.table.params.deptIndex = ''
tableStore.table.params.filterName = ''
tableStore.table.params.searchValue = ''
tableStore.table.params.statisticalType = []
tableStore.table.params.scale = []
tableStore.table.params.manufacturer = []
tableStore.table.params.loadType = []
provide('tableStore', tableStore)
const tree2List = (list: any, id?: string) => {
//存储结果的数组
let arr: any = []
// 遍历 tree 数组
list.forEach((item: any) => {
item.uPid = id
item.uId = Math.random() * 1000
item.valueOver =
item.integrityData == 3.14159
? 0
: item.integrityData >= 90
? 1
: item.integrityData >= 60 && item.integrityData < 90
? 2
: 3
// 判断item是否存在children
if (!item.children) return arr.push(item)
// 函数递归对children数组进行tree2List的转换
const children = tree2List(item.children, item.uId)
// 删除item的children属性
delete item.children
// 把item和children数组添加至结果数组
//..children: 意思是把children数组展开
arr.push(item, ...children)
})
// 返回结果数组
return arr
}
/**
* 树形结构按名称筛选:保留匹配节点+所有上级+所有下级,保持原树形层级
* @param {Array} treeData - 原始嵌套树形数据(根节点数组)
* @param {string} keyword - 筛选关键词name包含该关键词即匹配
* @returns {Array} 筛选后的嵌套树形数据,保持原层级
*/
function filterTreeByKeyword(treeData, keyword) {
// 关键词为空,直接返回原树(深拷贝,避免修改原数据)
if (!keyword || keyword.trim() === '') {
return JSON.parse(JSON.stringify(treeData));
}
const targetKey = keyword.trim();
// 存储需要保留的节点ID匹配节点+所有上级+所有下级)
const keepIdSet = new Set();
// 第一步递归遍历树形标记所有需要保留的节点ID
const markKeepNodes = (node, parentNodes = []) => {
// 1. 若当前节点名称包含关键词,标记自身+所有上级+所有下级
const isMatch = node.name && node.name.includes(targetKey);
if (isMatch) {
// 标记自身
keepIdSet.add(node.id);
// 标记所有上级父节点
parentNodes.forEach(pNode => keepIdSet.add(pNode.id));
// 标记所有下级子节点(递归)
const markChildren = (childNode) => {
keepIdSet.add(childNode.id);
if (childNode.children && childNode.children.length) {
childNode.children.forEach(markChildren);
}
};
if (node.children && node.children.length) {
node.children.forEach(markChildren);
}
}
// 2. 递归遍历子节点传递当前节点的上级链parentNodes + 当前节点)
if (node.children && node.children.length) {
node.children.forEach(child => markKeepNodes(child, [...parentNodes, node]));
}
};
// 遍历根节点,开始标记
treeData.forEach(rootNode => markKeepNodes(rootNode));
// 第二步:递归重构树形,只保留标记过的节点,保持层级
const rebuildTree = (node) => {
// 若当前节点无需保留直接返回null
if (!keepIdSet.has(node.id)) {
return null;
}
// 深拷贝当前节点,避免修改原数据
const newNode = { ...node };
// 递归处理子节点,过滤掉无需保留的,只保留有效子节点
if (newNode.children && newNode.children.length) {
const newChildren = newNode.children.map(child => rebuildTree(child)).filter(Boolean);
newNode.children = newChildren;
} else {
newNode.children = [];
}
return newNode;
};
// 重构根节点过滤掉null的根节点
const filteredTree = treeData.map(rootNode => rebuildTree(rootNode)).filter(Boolean);
return filteredTree;
}
// 禁用超过当前日期的选择
const disabledDate = (date: Date) => {
return date > new Date() // 如果日期大于当前日期,则禁用
}
onMounted(() => { })
// 补招
const makeUp = () => {
// tableRef.value && tableRef.value.getRef().getCheckboxRecords()
let list =
tableRef.value
.getRef()
.getCheckboxRecords()
.filter((item: any) => item.level == '6') || []
if (list.length == 0) {
return ElMessage({
message: '请选择监测点',
type: 'warning'
})
}
// 检查是否有 comFlag 等于 0 的项
const hasOfflineDevice = list.some((item: any) => item.comFlag === 0)
if (hasOfflineDevice) {
return ElMessage({
message: '请选择在线监测点进行补招',
type: 'warning'
})
}
timeData.value = []
timePopUp.value = true
}
// 补招提交
const makeUpSubmit = () => {
if (timeData.value.length == 0) {
return ElMessage({
message: '请选择补招时间',
type: 'warning'
})
}
let form = {
monitorId: tableRef.value
.getRef()
.getCheckboxRecords()
.filter(item => item.level == 6)
.map(item => item.id),
reCallEndTime: timeData.value[1],
reCallStartTime: timeData.value[0]
}
socket(form)
timePopUp.value = false
logPopUp.value = true
}
const socket = async (form: any) => {
const url = (localStorage.getItem('WebSocketUrl2') || 'null')//'ws://192.168.1.67:10405/api/recell/')
logList.value = []
await dataSocket.socketServe.connect(`${url}${adminInfo.id}`)
await dataSocket.socketServe.send(form)
logList.value.push({
type: '',
time: formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'),
name: '开始补召,请稍等...',
})
await dataSocket.socketServe.registerCallBack('message', (res: any) => {
logList.value.push({
type: res.code == 500 ? 'error' : '',
time: formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'),
name: res.message
})
setTimeout(() => {
logRef.value && (logRef.value.scrollTop = (logRef.value.scrollHeight + 30))
}, 10)
})
}
const close = () => {
dataSocket.socketServe?.closeWs()
}
watch(
() => treeData.value,
(val, oldVal) => {
if (val && val.length != 0) {
formData.value.deptIndex = val[0].id
tableStore.index()
}
},
{
immediate: true,
deep: true
}
)
</script>
<style lang="scss" scoped>
// .online {
// width: 100%;
// height: 100%;
// .online_header {
// width: 100%;
// max-height: 140px;
// padding: 10px;
// box-sizing: border-box;
// }
// .online_main {
// padding: 0 10px;
// }
// }
// 修复VXE表格树表checkbox的样式问题
:deep(.vxe-table--main-wrapper) {
.vxe-table--body-wrapper {
.vxe-body--row {
.vxe-cell--checkbox {
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3);
}
}
}
}
}
// 确保空数据状态下没有灰色遮罩层
:deep(.vxe-table--empty-block) {
background: transparent;
.vxe-table--empty-content {
color: #909399;
}
}
// 修复树表header的样式
:deep(.vxe-table--header-wrapper) {
.vxe-header--row {
.vxe-cell--checkbox {
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3);
}
}
}
}
// 精确修复checkbox的disabled状态样式
:deep(.vxe-checkbox.is--disabled) {
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3) !important;
}
}
// 确保树表节点没有灰色背景,但保留边框
:deep(.vxe-tree-cell) {
background: transparent;
}
// 精确修复空数据状态下的灰色背景
:deep(.vxe-table--empty-block) {
background: transparent;
}
// 修复checkbox在空数据状态下的样式
:deep(.vxe-table--body-wrapper:empty) {
.vxe-cell--checkbox {
background: transparent;
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3);
}
}
}
// 专门修复checkbox和treeNode组合的样式问题
:deep(.vxe-cell--checkbox) {
background: transparent !important;
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3) !important;
}
}
// 修复树表checkbox的特殊样式
:deep(.vxe-tree-cell .vxe-cell--checkbox) {
background: transparent !important;
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3) !important;
}
}
// 确保checkbox在树表中的正确显示
:deep(.vxe-body--row .vxe-cell--checkbox) {
background: transparent !important;
.vxe-checkbox--icon {
color: var(--el-color-primary-light-3) !important;
}
}
.logList {
height: 300px;
overflow-y: auto;
p {
margin-bottom: 10px;
display: flex;
}
}
</style>