修改全局列问题了,联调设备升级mqtt
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
200
src/utils/MQTT.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
72
src/views/govern/manage/operationalguidance/index.vue
Normal file
72
src/views/govern/manage/operationalguidance/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 = []
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user