修改全局列问题了,联调设备升级mqtt

This commit is contained in:
guanj
2026-07-23 11:32:42 +08:00
parent b4c706407f
commit 88a500a387
27 changed files with 2682 additions and 1359 deletions

View File

@@ -13,3 +13,23 @@ export const addDevModel = (file:any) => {
data: form,
})
}
/**
* 重启cld设备
**/
export const reboot = (data: any) => {
return request({
url: '/zl-event-boot/device/reboot',
method: 'get',
params: data
})
}
/**
* 重启Mqtt设备
**/
export const rebootDevice = (data: any) => {
return request({
url: '/access-boot/askDeviceData/rebootDevice',
method: 'post',
params: data
})
}

View File

@@ -102,7 +102,7 @@ export function findByDevTypeId(data: any) {
data
})
}
// 装置升级
// 装置升级 CLD
export function upgrade(params: any) {
return createAxios({
url: '/zl-event-boot/device/upgrade',
@@ -110,3 +110,19 @@ export function upgrade(params: any) {
params
})
}
// 装置升级 mqtt
export function uploadUpgradeFile(params: any) {
return createAxios({
url: '/cs-device-boot/EquipmentDelivery/uploadUpgradeFile',
method: 'post',
params
})
}
// 装置升级 升级 mqtt
export function commitUpgradeFile(params: any) {
return createAxios({
url: '/cs-device-boot/EquipmentDelivery/commitUpgradeFile',
method: 'post',
params
})
}

View File

@@ -2,8 +2,7 @@
<div class="device-control">
<!--治理效果报表 -->
<div v-show="fullscreen">
<!-- <PointTree :height="flag ? 106 : 50" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree> -->
<APFTree :height="flag ? 126 : 70" @node-click="handleNodeClick" template @init="handleNodeClick"></APFTree>
<APFTree :height="flag ? 128 : 72" @node-click="handleNodeClick" template @init="handleNodeClick"></APFTree>
</div>
<div>
<TableHeader :showReset="false" :timeKeyList="prop.timeKey" ref="TableHeaderRef" datePicker

View File

@@ -3,7 +3,7 @@
<!--指标越限时间分布
-->
<div v-show="fullscreen">
<PointTree :height="flag ? 106 : 50" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
<PointTree :height="flag ? 128 : 72" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
</div>
<div>
<TableHeader :showReset="false" :timeKeyList="prop.timeKey" ref="TableHeaderRef"

View File

@@ -2,7 +2,7 @@
<div class="device-control">
<!--指标越限概率分布 -->
<div v-show="fullscreen">
<PointTree :height="flag ? 106 : 50" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
<PointTree :height="flag ? 128 : 72" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
</div>
<div>
<TableHeader :showReset="false" :timeKeyList="prop.timeKey" ref="TableHeaderRef"

View File

@@ -2,7 +2,7 @@
<div class="device-control">
<!--指标拟合图 -->
<div v-show="fullscreen">
<PointTree :height="flag ? 106 : 50" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
<PointTree :height="flag ? 128 : 72" @node-click="nodeClick" @pointTypeChange="pointTypeChange"></PointTree>
</div>
<div>
<TableHeader datePicker @selectChange="selectChange" v-if="fullscreen" ref="TableHeaderRef"

View File

@@ -33,16 +33,28 @@
<Icon name="el-icon-Search" style="font-size: 16px" />
</template>
</el-input>
<!-- <Icon
@click="onMenuCollapse"
:name="menuCollapse ? 'el-icon-Expand' : 'el-icon-Fold'"
:class="menuCollapse ? 'unfold' : ''"
size="18px"
class="fold ml10 menu-collapse"
style="cursor: pointer"
v-if="props.canExpand"
/> -->
</div>
<div class="comm-tabs mb10">
<el-tooltip
v-for="tab in commTabs"
:key="tab.key"
:content="tab.label"
placement="top"
:show-after="200"
>
<button
type="button"
class="comm-tab"
:class="[
`is-${tab.key}`,
{ 'is-active': commFilter === tab.key }
]"
@click="onCommFilter(tab.key)"
>
<Icon :name="tab.icon" :size="12" class="comm-tab__icon" />
<span class="comm-tab__count">{{ tab.count }}</span>
</button>
</el-tooltip>
</div>
<el-collapse
@@ -67,9 +79,10 @@
:filter-node-method="filterNode"
node-key="id"
:default-expand-all="false"
v-bind="$attrs"
v-bind="treeAttrs"
:data="zlDevList"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
@@ -95,8 +108,9 @@
:filter-node-method="filterNode"
node-key="id"
:data="bxsDeviceData"
v-bind="$attrs"
v-bind="treeAttrs"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
@@ -122,8 +136,9 @@
:filter-node-method="filterNode"
node-key="id"
:data="frontDeviceData"
v-bind="$attrs"
v-bind="treeAttrs"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
@@ -147,10 +162,11 @@
highlight-current
:filter-node-method="filterNode"
node-key="id"
v-bind="$attrs"
v-bind="treeAttrs"
:data="props.data"
class="engineering-tree"
:default-expand-all="false"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
@@ -172,7 +188,7 @@
<script lang="ts" setup>
import useCurrentInstance from '@/utils/useCurrentInstance'
import { ElTree, type CollapseModelValue } from 'element-plus'
import { ref, watch, onMounted, nextTick } from 'vue'
import { ref, watch, onMounted, nextTick, computed, useAttrs } from 'vue'
import { collectDeviceLeaves } from './govern/lineTreeUtils'
import { collectDeviceApiLeaves } from './govern/deviceTreeUtils'
@@ -194,6 +210,8 @@ interface Props {
leafMode?: 'line' | 'device'
}
type CommFilter = 'all' | 'normal' | 'abnormal'
const props = withDefaults(defineProps<Props>(), {
width: '280px',
canExpand: true,
@@ -211,11 +229,20 @@ const options = [
]
const { proxy } = useCurrentInstance()
const attrs = useAttrs()
/** 去掉 node-click避免与本地处理重复/覆盖 */
const treeAttrs = computed(() => {
const { onNodeClick: _onNodeClick, ...rest } = attrs as Record<string, any>
return rest
})
const menuCollapse = ref(false)
const activeName = ref('0')
const filterText = ref('')
const process = ref('')
const loading = ref(false)
const commFilter = ref<CommFilter>('all')
const defaultProps = { label: 'name', value: 'id' }
@@ -231,6 +258,67 @@ const treeRef4 = ref<InstanceType<typeof ElTree>>()
defineExpose({ treeRef1, treeRef2, treeRef3, treeRef4 })
const isCommNormal = (node: any) => Number(node?.comFlag) === 2
/** 设备叶子Platform 且无子节点,或 type=device */
function isDeviceLeaf(data: any): boolean {
if (!data) return false
if (data.type === 'device') return true
if (data.icon === 'el-icon-Platform' && (!data.children || !data.children.length)) return true
if (data.level === 2 && data.icon === 'el-icon-Platform') return true
return false
}
function collectDevices(nodes: any[], out: any[] = []): any[] {
nodes?.forEach(n => {
if (isDeviceLeaf(n)) out.push(n)
if (n.children?.length) collectDevices(n.children, out)
})
return out
}
const deviceList = computed(() => {
if (treeType.value === '2') return collectDevices(props.data || [])
const collectLeaves = props.leafMode === 'device' ? collectDeviceApiLeaves : collectDeviceLeaves
const { govern, portable, monitor } = collectLeaves(
zlDevList.value,
bxsDeviceData.value,
frontDeviceData.value
)
return [...govern, ...portable, ...monitor]
})
const deviceStats = computed(() => {
const list = deviceList.value
const total = list.length
const normal = list.filter(isCommNormal).length
return { total, normal, abnormal: total - normal }
})
const commTabs = computed(() => [
{ key: 'all' as const, label: '全部设备', count: deviceStats.value.total, icon: 'el-icon-Platform' },
{ key: 'normal' as const, label: '通讯正常', count: deviceStats.value.normal, icon: 'el-icon-Platform' },
{ key: 'abnormal' as const, label: '通讯异常', count: deviceStats.value.abnormal, icon: 'el-icon-Platform' }
])
function applyTreeFilter() {
nextTick(() => {
const val = filterText.value
if (treeType.value === '1') {
treeRef1.value?.filter(val)
treeRef2.value?.filter(val)
treeRef3.value?.filter(val)
} else {
treeRef4.value?.filter(val)
}
})
}
const onCommFilter = (key: CommFilter) => {
commFilter.value = key
applyTreeFilter()
}
function splitTreeData(val: any[]) {
zlDeviceData.value = []
bxsDeviceData.value = []
@@ -270,6 +358,17 @@ function getActiveTreeRef() {
return treeRef3.value
}
function forwardNodeClick(data: any, node: any, ev?: any) {
const handler = (attrs as any).onNodeClick
if (!handler) return
if (Array.isArray(handler)) handler.forEach((fn: any) => fn?.(data, node, ev))
else handler(data, node, ev)
}
function onTreeNodeClick(data: any, node: any, ev?: any) {
forwardNodeClick(data, node, ev)
}
function resolveActiveName() {
if (zlDeviceData.value.length) return '0'
if (bxsDeviceData.value.length) return '1'
@@ -323,12 +422,13 @@ watch(
if (treeType.value === '1') {
nextTick(() => setActiveName())
}
applyTreeFilter()
},
{ immediate: true, deep: true }
)
watch(filterText, val => {
getActiveTreeRef()?.filter(val)
watch(filterText, () => {
applyTreeFilter()
})
watch(process, () => {
@@ -336,6 +436,11 @@ watch(process, () => {
if (activeName.value === '0') {
nextTick(() => changeDevice(activeName.value))
}
applyTreeFilter()
})
watch(treeType, () => {
applyTreeFilter()
})
const onMenuCollapse = () => {
@@ -343,10 +448,20 @@ const onMenuCollapse = () => {
proxy.eventBus.emit('cnTreeCollapse', menuCollapse)
}
const filterNode = (value: string, data: any, node: any): boolean => {
if (!value) return true
const filterNode = (_value: string, data: any, node: any): boolean => {
if (filterText.value) {
if (!data.name) return false
return chooseNode(value, data, node)
if (!chooseNode(filterText.value, data, node)) return false
}
if (commFilter.value === 'all') return true
if (isDeviceLeaf(data)) {
const normal = isCommNormal(data)
return commFilter.value === 'normal' ? normal : !normal
}
return !!filterText.value
}
const chooseNode = (value: string, data: any, node: any): boolean => {
@@ -413,6 +528,115 @@ onMounted(() => {
flex-shrink: 0;
}
.comm-tabs {
display: flex;
align-items: center;
// justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.comm-tab {
display: inline-flex;
align-items: center;
gap: 4px;
height: 22px;
padding: 0 4px 0 6px;
border-radius: 6px;
border: 1px solid #e5eaf3;
background: #fff;
color: #4e5969;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1;
&__icon {
font-size: 12px;
}
&.is-all .comm-tab__icon,
&.is-all :deep(.comm-tab__icon),
&.is-all :deep(svg),
&.is-all :deep(i) {
color: var(--el-color-primary) !important;
}
&.is-normal .comm-tab__icon,
&.is-normal :deep(.comm-tab__icon),
&.is-normal :deep(svg),
&.is-normal :deep(i) {
color: #2ab914 !important;
}
&.is-abnormal .comm-tab__icon,
&.is-abnormal :deep(.comm-tab__icon),
&.is-abnormal :deep(svg),
&.is-abnormal :deep(i) {
color: #e26257 !important;
}
&__count {
min-width: 20px;
height: 16px;
padding: 0 4px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #1d2129;
}
&:hover:not(.is-active) {
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
}
&.is-active {
color: #fff;
.comm-tab__icon,
:deep(.comm-tab__icon),
:deep(svg),
:deep(i) {
color: #fff !important;
}
.comm-tab__count {
background: #fff;
}
}
&.is-all.is-active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
.comm-tab__count {
color: var(--el-color-primary);
}
}
&.is-normal.is-active {
background: #2ab914;
border-color: #2ab914;
.comm-tab__count {
color: #2ab914;
}
}
&.is-abnormal.is-active {
background: #e26257;
border-color: #e26257;
.comm-tab__count {
color: #e26257;
}
}
}
.device-collapse {
flex: 1;
min-height: 0;

View File

@@ -13,7 +13,7 @@ export function decorateDeviceTree(
if (type === '2') {
applyMeta(item, { icon: 'el-icon-HomeFilled', color: primary() })
item.children?.forEach((child: any) => {
applyMeta(child, { icon: 'el-icon-List', color: primary() })
applyMeta(child, { icon: 'el-icon-Menu', color: primary() })
child.children?.forEach((grand: any) => {
applyMeta(grand, {
icon: 'el-icon-Platform',
@@ -29,7 +29,7 @@ export function decorateDeviceTree(
item.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary() })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary() })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary() })
l2.children?.forEach((l3: any) => {
l3.pName = '治理设备'
applyMeta(l3, {
@@ -63,7 +63,7 @@ export function decorateDeviceTree(
item.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary() })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary() })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary() })
l2.children?.forEach((l3: any) => {
l3.pName = '监测设备'
applyMeta(l3, {
@@ -88,7 +88,7 @@ export function decorateDeviceInfoTree(list: any[], decorators: LineTreeDecorato
list.forEach(item => {
applyMeta(item, { icon: 'el-icon-HomeFilled', color: primary() })
item.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary() })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary() })
l2.children?.forEach((l3: any) => {
applyMeta(l3, {
icon: 'el-icon-Platform',
@@ -107,11 +107,11 @@ export function decorateCloudTree(root: any, decorators: LineTreeDecorators): an
const { primary, statusColor, applyMeta } = decorators
const leaves: any[] = []
applyMeta(root, { icon: 'el-icon-Menu', color: primary() })
applyMeta(root, { icon: 'el-icon-ChromeFilled', color: primary() })
root.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary() })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary() })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary() })
l2.children?.forEach((l3: any) => {
applyMeta(l3, {
icon: 'el-icon-Platform',
@@ -140,7 +140,7 @@ export function decorateObjTree(data: any[], decorators: LineTreeDecorators): an
data.forEach(l1 => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary(), level: 1 })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary(), level: 2 })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary(), level: 2 })
l2.children?.forEach((l3: any) => {
applyMeta(l3, { icon: 'el-icon-Platform', color: primary(), level: 3 })
leaves.push(l3)
@@ -165,7 +165,7 @@ export function decorateLedgerLineTree(root: any, decorators: LineTreeDecorators
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary(), level: 1 })
if (!Array.isArray(l1.children)) l1.children = []
l1.children.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary(), level: 2 })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary(), level: 2 })
if (!Array.isArray(l2.children)) l2.children = []
l2.children.forEach((l3: any) => {
applyMeta(l3, {

View File

@@ -73,7 +73,7 @@ export function decorateLineTree(
if (type === '2') {
applyMeta(item, { icon: 'el-icon-HomeFilled', color: primary(), ...parentDisabled })
item.children?.forEach((child: any) => {
applyMeta(child, { icon: 'el-icon-List', color: primary(), ...parentDisabled })
applyMeta(child, { icon: 'el-icon-Menu', color: primary(), ...parentDisabled })
child.children?.forEach((grand: any) => {
applyMeta(grand, {
icon: 'el-icon-Platform',
@@ -99,7 +99,7 @@ export function decorateLineTree(
item.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary(), level: 1, ...parentDisabled })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary(), level: 1, ...parentDisabled })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary(), level: 1, ...parentDisabled })
l2.children?.forEach((l3: any) => {
applyMeta(l3, {
icon: 'el-icon-Platform',
@@ -139,7 +139,7 @@ export function decorateLineTree(
item.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-HomeFilled', color: primary(), level: 1, ...parentDisabled })
l1.children?.forEach((l2: any) => {
applyMeta(l2, { icon: 'el-icon-List', color: primary(), level: 1, ...parentDisabled })
applyMeta(l2, { icon: 'el-icon-Menu', color: primary(), level: 1, ...parentDisabled })
l2.children?.forEach((l3: any) => {
applyMeta(l3, {
icon: 'el-icon-Platform',

View File

@@ -1,5 +1,6 @@
<!-- 设备监控使用折叠面板渲染多个tree -->
<template>
<div :style="{ width: menuCollapse ? '40px' : props.width }" class="point-tree-root">
<!-- <Icon
v-show="menuCollapse"
@@ -11,7 +12,14 @@
style="cursor: pointer"
v-if="route.path != '/admin/govern/reportCore/statistics/index'"
/> -->
<div class="cn-tree" :style="{ opacity: menuCollapse ? 0 : 1, display: menuCollapse ? 'none' : '' }">
<div
class="cn-tree"
:style="{
opacity: menuCollapse ? 0 : 1,
display: menuCollapse ? 'none' : '',
height: `calc(100vh - 125px - ${props.height || 0}px)`
}"
>
<div class="tree-search mb10">
<el-input v-model.trim="filterText" placeholder="请输入内容" clearable>
<template #prepend>
@@ -24,15 +32,28 @@
<Icon name="el-icon-Search" style="font-size: 16px" />
</template>
</el-input>
<!-- <Icon
@click="onMenuCollapse"
:name="menuCollapse ? 'el-icon-Expand' : 'el-icon-Fold'"
:class="menuCollapse ? 'unfold' : ''"
size="18px"
class="fold ml10 menu-collapse"
style="cursor: pointer"
v-if="props.canExpand && route.path != '/admin/govern/reportCore/statistics/index'"
/> -->
</div>
<div class="comm-tabs mb10">
<el-tooltip
v-for="tab in commTabs"
:key="tab.key"
:content="tab.label"
placement="top"
:show-after="200"
>
<button
type="button"
class="comm-tab"
:class="[
`is-${tab.key}`,
{ 'is-active': commFilter === tab.key }
]"
@click="onCommFilter(tab.key)"
>
<Icon :name="tab.icon" :size="12" class="comm-tab__icon" />
<span class="comm-tab__count">{{ tab.count }}</span>
</button>
</el-tooltip>
</div>
<el-collapse :accordion="true" v-model="activeName" class="device-collapse" @change="changeDevice"
@@ -45,8 +66,8 @@
<el-option label="正式投运" value="4" />
</el-select>
<el-tree ref="treeRef1" :props="defaultProps" highlight-current :filter-node-method="filterNode"
node-key="id" v-bind="$attrs" :data="zlDevList" class="collapse-tree"
:default-expand-all="false">
node-key="id" v-bind="treeAttrs" :data="zlDevList" class="collapse-tree"
:default-expand-all="false" @node-click="onTreeNodeClick">
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon :name="nodeData.icon" style="font-size: 16px"
@@ -60,8 +81,8 @@
<el-collapse-item title="便携式设备" name="1" v-if="bxsDeviceData.length">
<div class="collapse-tree-panel">
<el-tree ref="treeRef2" :props="defaultProps" highlight-current :default-expand-all="false"
:filter-node-method="filterNode" node-key="id" :data="bxsDeviceData" v-bind="$attrs"
class="collapse-tree">
:filter-node-method="filterNode" node-key="id" :data="bxsDeviceData" v-bind="treeAttrs"
class="collapse-tree" @node-click="onTreeNodeClick">
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon :name="nodeData.icon" style="font-size: 16px"
@@ -75,8 +96,8 @@
<el-collapse-item title="监测设备" name="2" v-if="yqfDeviceData.length">
<div class="collapse-tree-panel">
<el-tree ref="treeRef3" :props="defaultProps" highlight-current :default-expand-all="false"
:filter-node-method="filterNode" node-key="id" :data="yqfDeviceData" v-bind="$attrs"
class="collapse-tree">
:filter-node-method="filterNode" node-key="id" :data="yqfDeviceData" v-bind="treeAttrs"
class="collapse-tree" @node-click="onTreeNodeClick">
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon :name="nodeData.icon" style="font-size: 16px"
@@ -90,8 +111,8 @@
</el-collapse>
<div v-if="treeType == '2'" class="engineering-tree-wrap" v-loading="loading">
<el-tree class="engineering-tree pt10 collapse-tree" ref="treeRef4" :props="defaultProps"
highlight-current :filter-node-method="filterNode" node-key="id" v-bind="$attrs" :data="props.data"
:default-expand-all="false">
highlight-current :filter-node-method="filterNode" node-key="id" v-bind="treeAttrs" :data="props.data"
:default-expand-all="false" @node-click="onTreeNodeClick">
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon :name="nodeData.icon" style="font-size: 16px" :style="{ color: nodeData.color }"
@@ -108,9 +129,9 @@
<script lang="ts" setup>
import useCurrentInstance from '@/utils/useCurrentInstance'
import { ElTree, type CollapseModelValue } from 'element-plus'
import { ref, watch, nextTick } from 'vue'
import { ref, watch, nextTick, computed, useAttrs } from 'vue'
import { useRoute } from 'vue-router'
import { collectDeviceLeaves } from './govern/lineTreeUtils'
import { collectDeviceLeaves, isLineTreeLeaf } from './govern/lineTreeUtils'
defineOptions({
name: 'govern/point',
@@ -127,6 +148,8 @@ interface Props {
height?: number
}
type CommFilter = 'all' | 'normal' | 'abnormal'
const props = withDefaults(defineProps<Props>(), {
width: '100%',
canExpand: true,
@@ -137,6 +160,13 @@ const props = withDefaults(defineProps<Props>(), {
const route = useRoute()
const { proxy } = useCurrentInstance()
const attrs = useAttrs()
/** 去掉 node-click避免与本地处理重复/覆盖 */
const treeAttrs = computed(() => {
const { onNodeClick: _onNodeClick, ...rest } = attrs as Record<string, any>
return rest
})
const treeType = ref('2')
const options = [
@@ -149,6 +179,7 @@ const activeName = ref('0')
const filterText = ref('')
const process = ref('')
const loading = ref(false)
const commFilter = ref<CommFilter>('all')
const defaultProps = { label: 'name', value: 'id' }
@@ -164,6 +195,57 @@ const treeRef4 = ref<InstanceType<typeof ElTree>>()
defineExpose({ treeRef1, treeRef2, treeRef3, treeRef4 })
const isCommNormal = (node: any) => Number(node?.comFlag) === 2
function collectPoints(nodes: any[], out: any[] = []): any[] {
nodes?.forEach(n => {
if (isLineTreeLeaf(n)) out.push(n)
if (n.children?.length) collectPoints(n.children, out)
})
return out
}
const pointList = computed(() => {
if (treeType.value === '2') return collectPoints(props.data || [])
const { govern, portable, monitor } = collectDeviceLeaves(
zlDevList.value,
bxsDeviceData.value,
yqfDeviceData.value
)
return [...govern, ...portable, ...monitor]
})
const pointStats = computed(() => {
const list = pointList.value
const total = list.length
const normal = list.filter(isCommNormal).length
return { total, normal, abnormal: total - normal }
})
const commTabs = computed(() => [
{ key: 'all' as const, label: '全部监测点', count: pointStats.value.total, icon: 'local-监测点' },
{ key: 'normal' as const, label: '通讯正常', count: pointStats.value.normal, icon: 'local-监测点' },
{ key: 'abnormal' as const, label: '通讯异常', count: pointStats.value.abnormal, icon: 'local-监测点' }
])
function applyTreeFilter() {
nextTick(() => {
const val = filterText.value
if (treeType.value === '1') {
treeRef1.value?.filter(val)
treeRef2.value?.filter(val)
treeRef3.value?.filter(val)
} else {
treeRef4.value?.filter(val)
}
})
}
const onCommFilter = (key: CommFilter) => {
commFilter.value = key
applyTreeFilter()
}
function splitTreeData(val: any[]) {
zlDeviceData.value = []
bxsDeviceData.value = []
@@ -213,6 +295,17 @@ function getActiveTreeRef() {
return treeRef3.value
}
function forwardNodeClick(data: any, node: any, ev?: any) {
const handler = (attrs as any).onNodeClick
if (!handler) return
if (Array.isArray(handler)) handler.forEach((fn: any) => fn?.(data, node, ev))
else handler(data, node, ev)
}
function onTreeNodeClick(data: any, node: any, ev?: any) {
forwardNodeClick(data, node, ev)
}
function resolveActiveName() {
if (zlDeviceData.value.length) return '0'
if (bxsDeviceData.value.length) return '1'
@@ -265,12 +358,13 @@ watch(
if (treeType.value === '1') {
nextTick(() => setActiveName())
}
applyTreeFilter()
},
{ immediate: true, deep: true }
)
watch(filterText, val => {
getActiveTreeRef()?.filter(val)
watch(filterText, () => {
applyTreeFilter()
})
watch(process, () => {
@@ -278,6 +372,11 @@ watch(process, () => {
if (activeName.value === '0') {
nextTick(() => changeDevice(activeName.value))
}
applyTreeFilter()
})
watch(treeType, () => {
applyTreeFilter()
})
const onMenuCollapse = () => {
@@ -285,10 +384,21 @@ const onMenuCollapse = () => {
proxy.eventBus.emit('cnTreeCollapse', menuCollapse)
}
const filterNode = (value: string, data: any, node: any): boolean => {
if (!value) return true
const filterNode = (_value: string, data: any, node: any): boolean => {
if (filterText.value) {
if (!data.name) return false
return chooseNode(value, data, node)
if (!chooseNode(filterText.value, data, node)) return false
}
if (commFilter.value === 'all') return true
if (isLineTreeLeaf(data)) {
const normal = isCommNormal(data)
return commFilter.value === 'normal' ? normal : !normal
}
// 非叶子:交由子节点可见性决定是否展示父级
return !!filterText.value
}
const chooseNode = (value: string, data: any, node: any): boolean => {
@@ -335,9 +445,7 @@ const changeTreeType = (val: string) => {
flex-direction: column;
box-sizing: border-box;
padding: 10px;
height: 100%;
min-height: 0;
max-height: 100%;
width: 280px;
background: #fff;
overflow: hidden;
@@ -356,6 +464,115 @@ const changeTreeType = (val: string) => {
flex-shrink: 0;
}
.comm-tabs {
display: flex;
align-items: center;
// justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.comm-tab {
display: inline-flex;
align-items: center;
gap: 4px;
height: 22px;
padding: 0 4px 0 6px;
border-radius: 6px;
border: 1px solid #e5eaf3;
background: #fff;
color: #4e5969;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1;
&__icon {
font-size: 12px;
}
&.is-all .comm-tab__icon,
&.is-all :deep(.comm-tab__icon),
&.is-all :deep(svg),
&.is-all :deep(i) {
color: var(--el-color-primary) !important;
}
&.is-normal .comm-tab__icon,
&.is-normal :deep(.comm-tab__icon),
&.is-normal :deep(svg),
&.is-normal :deep(i) {
color: #2ab914 !important;
}
&.is-abnormal .comm-tab__icon,
&.is-abnormal :deep(.comm-tab__icon),
&.is-abnormal :deep(svg),
&.is-abnormal :deep(i) {
color: #e26257 !important;
}
&__count {
min-width: 20px;
height: 16px;
padding: 0 4px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #1d2129;
}
&:hover:not(.is-active) {
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
}
&.is-active {
color: #fff;
.comm-tab__icon,
:deep(.comm-tab__icon),
:deep(svg),
:deep(i) {
color: #fff !important;
}
.comm-tab__count {
background: #fff;
}
}
&.is-all.is-active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
.comm-tab__count {
color: var(--el-color-primary);
}
}
&.is-normal.is-active {
background: #2ab914;
border-color: #2ab914;
.comm-tab__count {
color: #2ab914;
}
}
&.is-abnormal.is-active {
background: #e26257;
border-color: #e26257;
.comm-tab__count {
color: #e26257;
}
}
}
.device-collapse {
flex: 1;
min-height: 0;

200
src/utils/MQTT.ts Normal file
View File

@@ -0,0 +1,200 @@
import mqtt, { type MqttClient, type IClientOptions } from 'mqtt'
export type MqttMessageHandler = (topic: string, payload: any, raw: Uint8Array) => void
export interface MqttConnectOptions {
/** 覆盖 localStorage 中的 MQTTURL */
url?: string
username?: string
password?: string
clientId?: string
qos?: 0 | 1 | 2
connectTimeout?: number
}
const DEFAULT_OPTIONS: IClientOptions = {
protocolId: 'MQTT',
qos: 2,
clean: true,
connectTimeout: 30 * 1000,
username: 't_user',
password: 'njcnpqs'
}
/** 解析项目内常见的 `{key:value,key2:value2}` MQTT 字符串消息 */
export function parseMqttPayload(str: string) {
const content = String(str).replace(/^{|}$/g, '')
const result: Record<string, string | number> = {}
const regex = /([^,:]+):([^,]+)(?=,|$)/g
let match
while ((match = regex.exec(content)) !== null) {
const key = match[1].trim()
let value = match[2].trim()
// 去掉包裹引号 systemCrc:"3477" / version:"V1"
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1)
}
result[key] = isNaN(Number(value)) || value === '' ? value : Number(value)
}
return result
}
/** 解码 mqtt 二进制消息为对象(兼容 JSON 字符串 / 自定义 key:value 串) */
export function decodeMqttMessage(message: Uint8Array) {
const text = new TextDecoder().decode(message)
let parsed: any = text
try {
parsed = JSON.parse(text)
} catch {
// 非 JSON按原文字符串处理
}
// JSON.parse 后仍可能是内层字符串
if (typeof parsed === 'string') {
try {
const inner = JSON.parse(parsed)
parsed = typeof inner === 'string' ? parseMqttPayload(inner) : inner
} catch {
parsed = parseMqttPayload(parsed)
}
}
return parsed
}
/** 根据 nowStep / allStep 计算进度百分比 */
export function calcMqttProgress(data: { nowStep?: number | string; allStep?: number | string }) {
const allStep = Number(data?.allStep) || 0
const nowStep = Number(data?.nowStep) || 0
if (!allStep) return 0
let percentage = parseInt(String((nowStep / allStep) * 100), 10)
if (percentage > 100) percentage = 100
if (percentage < 0) percentage = 0
return percentage
}
export default class MqttService {
private client: MqttClient | null = null
private messageHandler: MqttMessageHandler | null = null
private subscribedTopics = new Set<string>()
get connected() {
return !!this.client?.connected
}
get instance() {
return this.client
}
/** 连接 MQTT默认读取 localStorage.MQTTURL */
connect(options: MqttConnectOptions = {}) {
const url = options.url || window.localStorage.getItem('MQTTURL')
if (!url) {
throw new Error('未配置 MQTT 地址')
}
if (this.client?.connected) {
return this.client
}
// 已有未连上的实例则先关掉
this.disconnect()
const clientOptions: IClientOptions = {
...DEFAULT_OPTIONS,
clientId: options.clientId || `mqttjs${Math.random()}`,
username: options.username ?? DEFAULT_OPTIONS.username,
password: options.password ?? DEFAULT_OPTIONS.password,
connectTimeout: options.connectTimeout ?? DEFAULT_OPTIONS.connectTimeout,
qos: options.qos ?? DEFAULT_OPTIONS.qos
}
this.client = mqtt.connect(url, clientOptions)
this.client.on('message', (topic, message) => {
if (!this.messageHandler) return
const payload = decodeMqttMessage(message)
this.messageHandler(topic, payload, message)
})
this.client.on('error', error => {
console.log('mqtt连接失败...', error)
this.disconnect()
})
this.client.on('close', () => {
console.log('mqtt客户端已断开连接.....')
})
return this.client
}
/** 连接成功后订阅;若尚未连接则等 connect 事件 */
subscribe(topic: string | string[], qos: 0 | 1 | 2 = 2) {
const topics = Array.isArray(topic) ? topic : [topic]
topics.forEach(t => this.subscribedTopics.add(t))
const doSubscribe = () => {
if (!this.client) return
topics.forEach(t => {
this.client!.subscribe(t, { qos })
console.log('mqtt已订阅:', t)
})
}
if (this.client?.connected) {
doSubscribe()
return
}
this.client?.once('connect', () => {
console.log('mqtt客户端已连接....')
doSubscribe()
})
}
/** 设置消息回调(覆盖式) */
onMessage(handler: MqttMessageHandler) {
this.messageHandler = handler
}
/** 取消订阅 */
unsubscribe(topic?: string | string[]) {
if (!this.client) return
const topics = topic
? Array.isArray(topic)
? topic
: [topic]
: [...this.subscribedTopics]
topics.forEach(t => {
this.client?.unsubscribe(t)
this.subscribedTopics.delete(t)
})
}
/** 断开并清理 */
disconnect() {
if (this.client) {
try {
this.client.end(true)
} catch (e) {}
this.client = null
}
this.subscribedTopics.clear()
this.messageHandler = null
}
}
/** 快捷创建:连接并订阅指定 topic */
export function createMqttClient(
topic: string | string[],
onMessage: MqttMessageHandler,
options?: MqttConnectOptions
) {
const service = new MqttService()
service.connect(options)
service.onMessage(onMessage)
service.subscribe(topic)
return service
}

View File

@@ -320,7 +320,7 @@ getCldTree().then(res => {
item.icon = 'el-icon-HomeFilled'
item.color = config.getColorVal('elementUiPrimary')
item.children.forEach((item: any) => {
item.icon = 'el-icon-List'
item.icon = 'el-icon-Menu'
item.color = config.getColorVal('elementUiPrimary')
item.children.forEach((item2: any) => {
item2.icon = 'el-icon-Platform'

View File

@@ -30,7 +30,7 @@
</TableHeader>
<Table v-if="view" ref="tableRef"></Table>
</div>
<el-empty v-else description="请选择设备" class="analyze-dvr-right" />
<el-empty v-else description="请选择数据" class="analyze-dvr-right" />
<MultiCondition v-model:visible="filterVisible" :params="tableStore.table.params" ref="multiConditionRef"
@confirm="onFilterConfirm" />

View File

@@ -410,7 +410,6 @@ import {
} from '@element-plus/icons-vue'
import analysisList from './analysisList/index.vue'
import mqtt from 'mqtt'
import { json } from 'node:stream/consumers'
defineOptions({
name: 'govern/device/control'
})

View File

@@ -75,12 +75,12 @@
style="width: 100%"
>
<vxe-column type="seq" title="序号" width="80"></vxe-column>
<vxe-column field="name" title="指标名称"></vxe-column>
<vxe-column field="phasic" title="相别"></vxe-column>
<vxe-column field="type" title="数据类型"></vxe-column>
<vxe-column field="unit" title="单位"></vxe-column>
<vxe-column field="startTimes" title="开始次数"></vxe-column>
<vxe-column field="endTimes" title="结束次数"></vxe-column>
<vxe-column field="name" title="指标名称" minWidth="200"></vxe-column>
<vxe-column field="phasic" title="相别" minWidth="150"></vxe-column>
<vxe-column field="type" title="数据类型" minWidth="100"></vxe-column>
<vxe-column field="unit" title="单位" minWidth="100"></vxe-column>
<vxe-column field="startTimes" title="开始次数"minWidth="100"></vxe-column>
<vxe-column field="endTimes" title="结束次数" minWidth="100"></vxe-column>
</vxe-table>
</div>
</el-tabs>

View File

@@ -86,7 +86,7 @@ export default {
// item2.color = config.getColorVal('elementUiPrimary')
item2.color = item.runStatus == 2 ? '#2ab914' : "#e26257"
item2.children.forEach(item3 => {
item3.icon = 'el-icon-List'
item3.icon = 'el-icon-Menu'
item3.color = config.getColorVal('elementUiPrimary')
// if (item3.runStatus === 1) {
// item3.color = '#e26257 !important'
@@ -110,7 +110,7 @@ export default {
// item2.color = config.getColorVal('elementUiPrimary')
item2.color = item.runStatus == 2 ? '#2ab914' : "#e26257"
item2.children.forEach(item3 => {
item3.icon = 'el-icon-List'
item3.icon = 'el-icon-Menu'
item3.color = config.getColorVal('elementUiPrimary')
// if (item3.runStatus === 1) {
// item3.color = '#e26257 !important'

View File

@@ -93,7 +93,7 @@ const tableStore = new TableStore({
}
},
{ title: '异常时间', field: 'evtTime', align: 'center', sortable: true },
{ title: '设备名称', field: 'ndid', align: 'center' },
{ title: '网络设备ID', field: 'ndid', align: 'center' },
{
title: '告警代码',
field: 'code',

View File

@@ -1,12 +1,21 @@
<template>
<el-dialog draggable class="cn-operate-dialog" width="800px" v-model.trim="dialogVisible" :title="title"
@close="emit('closePopup')">
<el-dialog
draggable
class="cn-operate-dialog"
width="800px"
v-model.trim="dialogVisible"
:title="title"
@close="emit('closePopup')"
>
<el-form class="form-two" :model="form" label-width="100px" ref="formRef" :rules="rules">
<el-form-item label="设备类型" prop="type">
<el-select v-model.trim="form.type" placeholder="请选择设备类型" @change="formDevTypeChange" clearable>
<el-option v-for="item in devTypeOptions" :key="item.value" :label="item.label"
:value="item.value"></el-option>
<el-option
v-for="item in devTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备型号" prop="devType">
@@ -14,43 +23,94 @@
<el-option v-for="item in DevTypeOptions" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select> -->
<el-select v-model.trim="form.devType" filterable placeholder="请选择设备型号" clearable
@change="devTypeChange">
<el-option v-for="item in formDevModelOptionsFilter" :key="item.value" :label="item.label"
:value="item.value"></el-option>
<el-select
v-model.trim="form.devType"
filterable
placeholder="请选择设备型号"
clearable
@change="devTypeChange"
>
<el-option
v-for="item in formDevModelOptionsFilter"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="版本号" prop="versionNo">
<el-input maxlength="32" show-word-limit v-model.trim="form.versionNo" autocomplete="off"
placeholder="请输入版本号"></el-input>
<el-input
maxlength="32"
show-word-limit
v-model.trim="form.versionNo"
autocomplete="off"
placeholder="请输入版本号"
></el-input>
</el-form-item>
<el-form-item label="版本类型" prop="versionType">
<el-input maxlength="32" show-word-limit v-model.trim="form.versionType" autocomplete="off"
placeholder="填写特殊类型(不填默认通用类型)"></el-input>
<el-input
maxlength="32"
show-word-limit
v-model.trim="form.versionType"
autocomplete="off"
placeholder="填写特殊类型(不填默认通用类型)"
></el-input>
</el-form-item>
<el-form-item label="协议版本" prop="versionAgreement">
<el-input maxlength="32" show-word-limit v-model.trim="form.versionAgreement" autocomplete="off"
placeholder="请输入协议版本"></el-input>
<el-input
maxlength="32"
show-word-limit
v-model.trim="form.versionAgreement"
autocomplete="off"
placeholder="请输入协议版本"
></el-input>
</el-form-item>
<el-form-item label="版本日期" prop="versionDate">
<el-date-picker v-model.trim="form.versionDate" type="date" value-format="YYYY-MM-DD"
placeholder="选择日期时间" style="width: 100%"></el-date-picker>
<el-date-picker
v-model.trim="form.versionDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item label="CRC校验" prop="crc">
<el-input maxlength="32" show-word-limit v-model.trim="form.crc" autocomplete="off"
placeholder="请输入CRC校验"></el-input>
<el-input
maxlength="32"
show-word-limit
v-model.trim="form.crc"
autocomplete="off"
placeholder="请输入CRC校验"
></el-input>
</el-form-item>
</el-form>
<el-form :model="form" label-width="100px" class="mt10 form-one">
<el-form-item label="描述" prop="description">
<el-input show-word-limit maxlength="300" :autosize="{ minRows: 2, maxRows: 4 }" type="textarea"
v-model.trim="form.description" autocomplete="off" placeholder="请输入描述"></el-input>
<el-input
show-word-limit
maxlength="300"
:autosize="{ minRows: 2, maxRows: 4 }"
type="textarea"
v-model.trim="form.description"
autocomplete="off"
placeholder="请输入描述"
></el-input>
</el-form-item>
<el-form-item label="升级文件" prop="file">
<el-upload :limit="1" accept=".bin" :auto-upload="false" :on-change="fileChange" :on-exceed="fileExceed"
:on-remove="fileRemove" :file-list="fileList">
<el-upload
:limit="1"
accept=".bin,.zip,.tar"
:auto-upload="false"
:on-change="fileChange"
:on-exceed="fileExceed"
:on-remove="fileRemove"
:file-list="fileList"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">支持 .bin 或压缩包.zip / .tar</div>
</template>
</el-upload>
</el-form-item>
</el-form>
@@ -86,7 +146,7 @@ const form = reactive<any>({
devTypeName: null,
versionAgreement: null,
versionNo: null,
versionType: null,
versionType: '通用',
crc: null,
description: null,
versionDate: null,
@@ -170,11 +230,13 @@ const fileExceed = (e: any) => {
ElMessage.error('只能上传一个文件')
}
// 处理上传前检查
const ALLOWED_EXTS = ['.bin', '.zip', '.tar']
const beforeUpload = (file: any) => {
const isWord = file.name.endsWith('.bin')
const name = (file?.name || '').toLowerCase()
const ok = ALLOWED_EXTS.some(ext => name.endsWith(ext))
if (!isWord) {
ElMessage.error('请上传(.bin)格式文件!')
if (!ok) {
ElMessage.error('请上传 .bin 或压缩包文件(.zip / .tar!')
fileList.value = []
return false
}
@@ -212,15 +274,16 @@ const open = async (text: string, data?: anyObj) => {
for (let key in form) {
form[key] = null
}
form.versionType = '通用'
form.file = []
fileList.value = []
}
}
const submit = () => {
// console.log(formRef)
formRef.value.validate(async (valid: boolean) => {
// console.log(valid)
if (valid) {
if (fileList.value.length == 0) {
ElMessage.warning('请上传升级文件')
return
@@ -231,15 +294,18 @@ const submit = () => {
submitForm.append(key, form[key])
}
if (form.id) {
await auditEdData(form)
await auditEdData(form).finally(() => {
loading.value = false
})
} else {
await addEdData(form)
await addEdData(form).finally(() => {
loading.value = false
})
}
ElMessage.success('操作成功')
tableStore.index()
dialogVisible.value = false
emit('closePopup')
loading.value = false
}
})
}

View File

@@ -1,7 +1,5 @@
<template>
<div class="default-main manage-realTime" :style="{ height: pageHeight.height }">
<DeviceTree @node-click="nodeClick" @init="nodeClick" @deviceTypeChange=""></DeviceTree>
<div class="manage-realTime-right">
<div class="og-realtime">
<div class="time-container">
<div>
<div>系统时间{{ realTime }}</div>
@@ -13,7 +11,7 @@
<MyEchart :options="echartsData" />
</div>
<div class="pl10 pr10">
<el-table :data="tableData" border stripe :show-header="false" class="custom-table">
<el-table :data="tableData" border stripe :show-header="false" size="small" class="custom-table">
<el-table-column prop="label1" />
<el-table-column prop="label2" />
<el-table-column prop="value1" />
@@ -24,27 +22,30 @@
</el-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { mainHeight } from '@/utils/layout'
import DeviceTree from '@/components/tree/govern/deviceTree.vue'
import MyEchart from '@/components/echarts/MyEchart.vue'
import { formatToDateTime } from '@/utils/dateUtil'
defineOptions({
name: 'manage/realTime'
name: 'operationalguidance/realTime'
})
//
const pageHeight = mainHeight(20)
const echartHeight = mainHeight(180)
const props = defineProps<{
device?: anyObj
}>()
const echartHeight = mainHeight(185)
const realTime = ref<string>('')
const deviceTime = ref<string>('')
const timer = ref<any>(null)
const timer = ref<ReturnType<typeof setInterval> | null>(null)
const echartsData = ref<any>({
title: {
text: '终端性能'
text: '设备性能'
},
tooltip: {
axisPointer: {
@@ -63,7 +64,6 @@ const echartsData = ref<any>({
backgroundColor: 'rgba(0,0,0,0.55)',
borderWidth: 0
},
legend: {
bottom: 0,
data: ['CPU1使用率', 'CPU2使用率', '内存使用率', '磁盘使用率']
@@ -181,23 +181,23 @@ const tableData = ref([
}
])
const nodeClick = async (e: anyObj) => {
console.log('点击设备树节点')
}
const handleClose = () => {
console.log('close')
watch(
() => props.device,
() => {
//
}
)
const synchronizeTime = async () => {
console.log('对时')
}
onMounted(() => {
timer.value = setInterval(() => {
realTime.value = formatToDateTime(new Date())
}, 1000)
})
//
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
@@ -205,23 +205,18 @@ onUnmounted(() => {
}
})
</script>
<style scoped lang="scss">
.manage-realTime {
display: flex;
&-right {
<style scoped lang="scss">
.og-realtime {
height: 100%;
overflow: hidden;
flex: 1;
padding: 10px 10px 10px 0;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5; // 便
.time-container {
display: flex;
justify-content: flex-end; //
align-items: center; //
justify-content: flex-end;
align-items: center;
* {
margin-right: 15px;
@@ -236,7 +231,6 @@ onUnmounted(() => {
text-align: center;
}
/* 1. 加深表格边框线条 */
:deep(.custom-table.el-table) {
border: 1px solid #dcdfe6;
width: auto;
@@ -246,18 +240,9 @@ onUnmounted(() => {
border: 0.5px solid #dcdfe6;
}
/* 2. 第 1、2、4、6 列显示深色背景 */
:deep(.custom-table .el-table__row .el-table__cell:nth-child(1)) {
background-color: #f5f7fa;
font-weight: bold;
}
/* 第 3、5、7 列显示白色背景 */
:deep(.custom-table .el-table__row .el-table__cell:nth-child(3)),
:deep(.custom-table .el-table__row .el-table__cell:nth-child(5)),
:deep(.custom-table .el-table__row .el-table__cell:nth-child(7)) {
// background-color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="og-setting">
<div class="og-setting__toolbar">
<el-button type="primary" icon="el-icon-Tickets" @click="readExternal">读取外部定值</el-button>
<el-button type="primary" icon="el-icon-Edit" @click="modifySetting">修改定值</el-button>
</div>
<el-tabs v-model="activeTab" type="border-card" class="og-setting__tabs" @tab-change="onTabChange">
<el-tab-pane
v-for="tab in tabs"
:key="tab.name"
:label="tab.label"
:name="tab.name"
>
<div class="og-setting__table" v-loading="loading">
<vxe-table
v-bind="defaultAttribute"
:data="tableMap[tab.name]"
height="auto"
>
<vxe-column type="seq" title="序号" width="90" sortable />
<vxe-column field="name" title="定值名称" min-width="160" />
<vxe-column field="value" title="当前定值" min-width="140" />
<vxe-column field="range" title="定值范围" min-width="160" />
</vxe-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { defaultAttribute } from '@/components/table/defaultAttribute'
defineOptions({
name: 'operationalguidance/settingManage'
})
const props = defineProps<{
device?: anyObj
}>()
type TabName = 'basic' | 'harmonicCurrent' | 'harmonicVoltage' | 'interharmonic'
const tabs: { label: string; name: TabName }[] = [
{ label: '基本定值', name: 'basic' },
{ label: '谐波电流', name: 'harmonicCurrent' },
{ label: '谐波电压', name: 'harmonicVoltage' },
{ label: '间谐波定值', name: 'interharmonic' }
]
const activeTab = ref<TabName>('basic')
const loading = ref(false)
const tableMap = reactive<Record<TabName, anyObj[]>>({
basic: [],
harmonicCurrent: [],
harmonicVoltage: [],
interharmonic: []
})
const loadData = async (tab: TabName = activeTab.value) => {
if (!props.device?.id) {
tableMap[tab] = []
return
}
loading.value = true
try {
// TODO: 按设备 + 定值类型请求接口
tableMap[tab] = []
} finally {
loading.value = false
}
}
const onTabChange = (name: string | number) => {
loadData(name as TabName)
}
const readExternal = () => {
if (!props.device?.id) {
ElMessage.warning('请先选择设备')
return
}
ElMessage.info('读取外部定值')
loadData()
}
const modifySetting = () => {
if (!props.device?.id) {
ElMessage.warning('请先选择设备')
return
}
ElMessage.info('修改定值')
}
watch(
() => props.device,
() => loadData(),
{ immediate: true, deep: true }
)
</script>
<style scoped lang="scss">
.og-setting {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
padding: 0 10px;
box-sizing: border-box;
&__toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 10px;
flex-shrink: 0;
}
&__tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:deep(.el-tabs__header) {
margin: 0;
flex-shrink: 0;
}
:deep(.el-tabs__content) {
flex: 1;
min-height: 0;
padding: 0;
}
:deep(.el-tab-pane) {
height: 100%;
}
:deep(.el-tabs__item) {
height: 36px;
line-height: 36px;
}
}
&__table {
height: 100%;
min-height: 0;
padding:0 10px;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="default-main manage-operationalguidance" :style="{ height: pageHeight.height }">
<DeviceTree @init="nodeClick" @deviceTypeChange="onDeviceTypeChange" />
<div class="manage-operationalguidance-right">
<el-tabs v-model="activeTab" type="border-card" class="og-tabs">
<el-tab-pane label="实时状态" name="realTime" lazy>
<RealTime :device="currentDevice" />
</el-tab-pane>
<el-tab-pane label="定值管理" name="setting" lazy>
<SettingManage :device="currentDevice" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { mainHeight } from '@/utils/layout'
import DeviceTree from '@/components/tree/govern/deviceTree.vue'
import RealTime from './comp/realTime.vue'
import SettingManage from './comp/settingManage.vue'
defineOptions({
name: 'operationalguidance'
})
const pageHeight = mainHeight(20)
const activeTab = ref('realTime')
const currentDevice = ref<anyObj>()
const nodeClick = (e?: anyObj) => {
if (e) currentDevice.value = e
}
const onDeviceTypeChange = (_type: any, device?: anyObj) => {
if (device) currentDevice.value = device
}
</script>
<style scoped lang="scss">
.manage-operationalguidance {
display: flex;
&-right {
height: 100%;
overflow: hidden;
flex: 1;
padding: 10px 10px 10px 0;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
min-width: 0;
}
.og-tabs {
height: 100%;
display: flex;
flex-direction: column;
:deep(.el-tabs__content) {
flex: 1;
overflow: hidden;
padding: 10px 0;
}
:deep(.el-tab-pane) {
height: 100%;
}
}
}
</style>

View File

@@ -1,68 +1,135 @@
<template>
<el-dialog draggable width="500px" v-model.trim="dialogVisible" :title="title">
<el-dialog
draggable
width="520px"
v-model.trim="dialogVisible"
:title="title"
:close-on-click-modal="!mqttProgressing"
@closed="onClosed"
>
<el-form :model="form" class="form-one" label-width="auto" ref="formRef" :rules="rules">
<el-form-item label="版本" prop="edDataId">
<el-select v-model.trim="form.edDataId" placeholder="请选择版本" @change="selectChange">
<el-option v-for="item in devList" :disabled="version == item.versionNo"
:label="`${item.devTypeName}_${item.versionNo}_${item.versionAgreement}_${item.versionDate}`"
:value="item.id"></el-option>
<el-select
v-model.trim="form.edDataId"
placeholder="请选择版本"
:disabled="mqttProgressing"
@change="selectChange"
>
<el-option
v-for="item in devList"
:key="item.id"
:disabled="version == item.versionNo"
:label="formatVersionLabel(item)"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input maxlength="300" show-word-limit v-model.trim="versionDesc" disabled type="textarea" :rows="3"></el-input>
<el-input
maxlength="300"
show-word-limit
v-model.trim="versionDesc"
disabled
type="textarea"
:rows="3"
></el-input>
</el-form-item>
</el-form>
<!-- MQTT上传成功后在输入框下方显示升级进度 -->
<div v-if="mqttVisible" class="mqtt-upgrade">
<div class="mqtt-upgrade__label">升级版本</div>
<el-progress
:percentage="mqttProgress"
:stroke-width="12"
striped
striped-flow
:status="mqttProgress >= 100 ? 'success' : undefined"
/>
<template v-if="mqttProgress >= 100">
<div class="mqtt-upgrade__codes">
<div>升级文件校验码{{ deviceCrc || '-' }}</div>
<div>归档文件校验码{{ verifyCode || '获取中...' }}</div>
</div>
<div v-if="codeReady" class="mqtt-upgrade__actions">
<template v-if="codeMatched">
<el-button type="primary" :loading="confirmLoading" @click="confirmUpgrade">确认升级</el-button>
<el-button @click="cancelUpgrade">取消升级</el-button>
</template>
<el-alert v-else type="error" show-icon :closable="false" title="校验码不一致不能升级" />
</div>
</template>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit" :loading="loading">确定</el-button>
<el-button @click="handleCancel" :disabled="confirmLoading">取消</el-button>
<el-button v-if="!mqttVisible" type="primary" @click="submit" :loading="loading">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, inject, reactive } from 'vue'
import { ref, reactive, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { findByDevTypeId, upgrade } from '@/api/system-boot/dictTree'
import { findByDevTypeId, upgrade, uploadUpgradeFile, commitUpgradeFile } from '@/api/system-boot/dictTree'
import MqttService, { createMqttClient, calcMqttProgress } from '@/utils/MQTT'
const formRef = ref()
const dialogVisible = ref(false)
const title = ref('程序版本')
const devList: any = ref([])
const versionDesc = ref('')
const version = ref('')
// 表单数据(新增了版本类型、版本描述字段)
const form = reactive<any>({
devId: [],
edDataId: '' // 新增:下拉选择版本
devId: [] as any[],
edDataId: ''
})
const loading = ref(false)
// 校验规则(新增两个字段的必填校验)
const rules = {
// 新增校验
edDataId: [{ required: true, message: '请选择版本', trigger: 'change' }]
}
const row = ref<any>({})
/** MQTT 升级进度 */
const mqttVisible = ref(false)
const mqttProgress = ref(0)
/** 升级文件校验码 = mqtt.devCrc */
const deviceCrc = ref('')
/** 归档文件校验码 = mqtt.systemCrc */
const verifyCode = ref('')
/** mqtt crcCheck 带回的 mid / version确认升级用 */
const mqttMid = ref<string | number>('')
const mqttVersion = ref('')
const confirmLoading = ref(false)
const mqttService = ref<MqttService | null>(null)
const mqttProgressing = computed(() => mqttVisible.value && mqttProgress.value < 100)
const codeReady = computed(() => !!(deviceCrc.value && verifyCode.value))
const codeMatched = computed(() => codeReady.value && String(deviceCrc.value) === String(verifyCode.value))
const getDevIdStr = () => (Array.isArray(form.devId) ? form.devId.join() : String(form.devId || ''))
// 打开弹窗
const open = (data: any) => {
resetMqttState()
loading.value = false
version.value = data.version
form.devId = []
form.edDataId = ''
versionDesc.value = ''
row.value = data
form.devId = Array.isArray(data.id) ? data.id : [data.id]
getFindByDev(data.devModel)
formRef.value?.resetFields()
dialogVisible.value = true
form.devId = data.id
}
// 获取版本
const getFindByDev = (devModel: string) => {
findByDevTypeId({
devType: devModel, //'45afe3ad9970785090cd7da27f489c37',
devType: devModel,
pageNum: 1,
pageSize: 10000
}).then((res: any) => {
@@ -73,42 +140,184 @@ const getFindByDev = (devModel: string) => {
} else if (devList.value.length == 1 && devList.value[0].versionNo == version.value) {
ElMessage.warning('该设备没有可升级的版本!')
}
})
}
// 提交
function disconnectMqtt() {
mqttService.value?.disconnect()
mqttService.value = null
}
function resetMqttState() {
disconnectMqtt()
mqttVisible.value = false
mqttProgress.value = 0
deviceCrc.value = ''
verifyCode.value = ''
mqttMid.value = ''
mqttVersion.value = ''
confirmLoading.value = false
}
function connectUpgradeMqtt(ndid: string) {
try {
const topic = `/Web/Progress/UpgradeFile/${ndid}`
mqttService.value = createMqttClient(topic, (_topic, data) => {
// 进度消息
if (data?.allStep != null || data?.nowStep != null) {
mqttProgress.value = calcMqttProgress(data)
}
// type=crcCheck 时取校验码及确认升级参数
if (data?.type === 'crcCheck') {
mqttProgress.value = 100
deviceCrc.value = data.devCrc != null ? String(data.devCrc) : ''
verifyCode.value = data.systemCrc != null ? String(data.systemCrc) : ''
mqttMid.value = data.mid ?? ''
mqttVersion.value = data.version != null ? String(data.version) : ''
}
})
} catch (e: any) {
ElMessage.error(e?.message || 'MQTT 连接失败')
}
}
const submit = () => {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
if (!valid) return
try {
ElMessage.info('升级中,请稍等...')
loading.value = true
if (row.value.accessMethod == 'CLD') {
ElMessage.info('升级中,请稍等...')
upgrade({
devId: form.devId.join(),
devId: getDevIdStr(),
edDataId: form.edDataId
})
.then(res => {
ElMessage.success(res.message)
loading.value = false
dialogVisible.value = false
})
.catch(error => {
.catch(() => {})
.finally(() => {
loading.value = false
})
} else if (row.value.accessMethod == 'MQTT') {
ElMessage.info('正在上传升级文件...')
uploadUpgradeFile({
id: getDevIdStr(),
ndid: row.value.ndid
})
.then(res => {
ElMessage.success(res.message || '上传成功')
loading.value = false
mqttVisible.value = true
mqttProgress.value = 0
deviceCrc.value = ''
verifyCode.value = ''
mqttMid.value = ''
mqttVersion.value = ''
connectUpgradeMqtt(row.value.ndid)
})
.catch(() => {
loading.value = false
})
} else {
ElMessage.warning('未知的接入方式')
loading.value = false
}
} catch (error) {
loading.value = false
// ElMessage.error(error.message)
}
}
})
}
const selectChange = e => {
devList.value.forEach(item => {
if (item.id === e) {
versionDesc.value = item.description
}
})
}
const confirmUpgrade = () => {
if (!codeMatched.value) {
ElMessage.warning('校验码不一致不能升级')
return
}
if (mqttMid.value === '' || mqttMid.value == null || !mqttVersion.value) {
ElMessage.warning('未获取到升级参数,请稍后重试')
return
}
confirmLoading.value = true
commitUpgradeFile({
mid: mqttMid.value,
version: mqttVersion.value,
nDid: row.value.ndid
})
.then((res: any) => {
ElMessage.success(res.message || '升级成功')
dialogVisible.value = false
})
.catch(() => {})
.finally(() => {
confirmLoading.value = false
})
}
const cancelUpgrade = () => {
resetMqttState()
ElMessage.info('已取消升级')
}
const handleCancel = () => {
if (mqttVisible.value) {
cancelUpgrade()
}
dialogVisible.value = false
}
const selectChange = (e: string) => {
const item = devList.value.find((v: any) => v.id === e)
versionDesc.value = item?.description || ''
}
const formatVersionLabel = (item: any) => {
const label = `${item.devTypeName}_${item.versionNo}_${item.versionAgreement}_${item.versionDate}`
return version.value == item.versionNo ? `${label}(当前版本)` : label
}
const onClosed = () => {
resetMqttState()
loading.value = false
}
onBeforeUnmount(() => {
disconnectMqtt()
})
defineExpose({ open })
</script>
<style scoped lang="scss">
.mqtt-upgrade {
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
&__label {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #1d2129;
}
&__codes {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
color: #4e5969;
line-height: 1.5;
}
&__actions {
margin-top: 14px;
display: flex;
align-items: center;
gap: 10px;
}
}
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div>
<TableHeader ref="tableHeaderRef" showExport showCustomColumn>
<template #select>
@@ -87,6 +87,8 @@ import { Share, Document } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import upgrade from './comp/upgrade.vue'
import deviceLog from './comp/deviceLog.vue'
import { reboot, rebootDevice } from '@/api/access-boot/analyzeModel'
defineOptions({
name: 'govern/manage/programVersion'
})
@@ -113,6 +115,7 @@ const tableStore: any = new TableStore({
},
{ title: '设备名称', field: 'name', minWidth: '150' },
{ title: '网络设备ID', field: 'ndid', minWidth: '150' },
{
title: '设备型号',
field: 'devModelName',
@@ -154,7 +157,6 @@ const tableStore: any = new TableStore({
}
},
{
title: 'icd模型',
field: 'icd',
@@ -167,7 +169,7 @@ const tableStore: any = new TableStore({
{
title: '所属工程',
field: 'associatedEngineering',
minWidth: '120',
minWidth: '150',
formatter: (row: any) => {
return row.cellValue || '/'
}
@@ -175,7 +177,7 @@ const tableStore: any = new TableStore({
{
title: '所属项目',
field: 'associatedProject',
minWidth: '120',
minWidth: '150',
formatter: (row: any) => {
return row.cellValue || '/'
}
@@ -215,7 +217,7 @@ const tableStore: any = new TableStore({
title: '操作',
fixed: 'right',
align: 'center',
width: '130',
width: '140',
render: 'buttons',
buttons: [
{
@@ -228,11 +230,7 @@ const tableStore: any = new TableStore({
},
render: 'basicButton',
click: (row: any) => {
upgradeRef.value.open({
id: [row.id],
devModel: row.devModel,
version: row.version
})
upgradeRef.value.open(row)
}
},
{
@@ -244,6 +242,38 @@ const tableStore: any = new TableStore({
click: (row: any) => {
deviceLogRef.value.open(row)
}
},
{
name: 'productSetting',
title: '重启',
type: 'primary',
icon: 'el-icon-EditPen',
loading: 'loading1',
render: 'basicButton',
click: (row: any) => {
row.loading1 = true
if (row.accessMethod == 'CLD') {
reboot({
devId: row.id
})
.then(res => {
ElMessage.success(res.message)
})
.finally(() => {
row.loading1 = false
})
} else if (row.accessMethod == 'MQTT') {
rebootDevice({
nDid: row.ndid
})
.then(res => {
ElMessage.success(res.message)
})
.finally(() => {
row.loading1 = false
})
}
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -366,7 +366,7 @@ const currentChangeEvent = () => {
nodeId.value = tableRef.value.getRef().getCurrentRecord().id
// 检查返回的数据是否存在且不为空
if (res.data && res.data.processDeviceList) {
dataSource.value = res.data.processDeviceList.filter(item => (item.name = item.processNo + ''))
dataSource.value = res.data.processDeviceList.filter(item => (item.name = item.processNo + '号进程'))
} else {
dataSource.value = []
}

View File

@@ -315,13 +315,13 @@ onUnmounted(() => {
<style lang="scss" scoped>
.vgl-layout {
background-color: #f8f9fa;
border-radius: 4px;
border-radius: 5px;
overflow: hidden;
}
:deep(.vgl-item:not(.vgl-item--placeholder)) {
background-color: #ffffff;
border-radius: 4px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}

View File

@@ -224,13 +224,13 @@ onUnmounted(() => {
<style lang="scss" scoped>
.vgl-layout {
background-color: #f8f9fa;
border-radius: 4px;
border-radius: 5px;
overflow: hidden;
}
:deep(.vgl-item:not(.vgl-item--placeholder)) {
background-color: #ffffff;
border-radius: 4px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}