style:设备运行参数分析/表管理树状结构重构

main
黄伟杰 3 weeks ago
parent 5a01b37a02
commit 8741d714cf

@ -74,6 +74,7 @@ export interface DeviceContactModelVO {
dataUnit?: string
}
// 物联设备 API
export const DeviceApi = {
// 查询物联设备分页
@ -130,6 +131,10 @@ export const DeviceApi = {
return await request.get({ url: `/iot/device/historyRecord`, params })
},
devicePointList: async () => {
return await request.get({ url: `/iot/device/devicePointList` })
},
// ==================== 子表(设备属性) ====================
// 获得设备属性分页

@ -48,7 +48,7 @@ export const DeviceModelAttributeApi = {
operationAnalysisDetails: async (params: {
deviceId: number
modelId: number
modelId?: number
collectionStartTime?: string
collectionEndTime?: string
}) => {

@ -19,7 +19,9 @@ export interface OrganizationVO {
export type DeviceParameterAnalysisNodeVO = {
id: number | string
name: string
orgClass?: string
parentId?: number | string
children?: DeviceParameterAnalysisNodeVO[]
equipments?: {
id: number | string
name: string
@ -70,7 +72,7 @@ export const OrganizationApi = {
return await request.download({ url: `/mes/organization/export-excel`, params })
},
deviceParameterAnalysis: async (params: { keyword?: string }) => {
deviceParameterAnalysis: async (params: { keyword?: string, showDevices?: number }) => {
return await request.get({ url: `/mes/organization/deviceParameterAnalysis`, params })
}
}

@ -2,23 +2,11 @@
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<ContentWrap class="h-1/1">
<el-input
v-model="keyword"
clearable
placeholder="搜索设备或参数"
class="!w-1/1"
@input="handleKeywordChange"
/>
<el-input v-model="keyword" clearable placeholder="搜索设备或参数" class="!w-1/1" @input="handleKeywordChange" />
<div class="mt-12px">
<el-tree
ref="treeRef"
v-loading="treeLoading"
:data="treeData"
:props="treeProps"
node-key="id"
highlight-current
@node-click="handleTreeNodeClick"
/>
ref="treeRef" v-loading="treeLoading" :data="treeData" :props="treeProps" node-key="id"
highlight-current @node-click="handleTreeNodeClick" />
</div>
</ContentWrap>
</el-col>
@ -28,15 +16,9 @@
<el-form class="-mb-15px" :inline="true">
<el-form-item label="时间">
<el-date-picker
v-model="dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-360px"
/>
v-model="dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
value-format="YYYY-MM-DD" :shortcuts="dateShortcuts"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-360px" />
</el-form-item>
<el-form-item>
<el-button :disabled="!selectedParam" @click="handleQuery">
@ -50,22 +32,11 @@
</ContentWrap>
<ContentWrap>
<el-alert
v-if="selectedParam"
:title="selectedParamTitle"
type="info"
:closable="false"
class="mb-12px"
/>
<el-alert v-if="selectedParam" :title="selectedParamTitle" type="info" :closable="false" class="mb-12px" />
<div v-loading="chartLoading">
<el-empty v-if="!selectedParam" description="请选择左侧参数" />
<el-empty v-else-if="chartState === 'empty'" description="暂无数据" />
<EChart
v-else-if="chartState === 'ready'"
:key="chartRenderKey"
:options="chartOption"
height="70vh"
/>
<el-empty v-if="chartState === 'empty'" description="暂无数据" />
<el-empty v-else-if="!selectedParam" description="请选择左侧节点" />
<EChart v-else-if="chartState === 'ready'" :key="chartRenderKey" :options="chartOption" height="70vh" />
</div>
</ContentWrap>
</el-col>
@ -93,6 +64,7 @@ type DeviceTreeNode = {
modelId?: number
paramKey?: string
unit?: string
paramCount?: number
}
type ApiTreeParameter = {
@ -111,6 +83,7 @@ type ApiTreeEquipment = {
type ApiTreeOrg = {
id: number
name: string
orgClass?: string
parentId?: number | null
equipments?: ApiTreeEquipment[]
children?: ApiTreeOrg[]
@ -181,7 +154,7 @@ const selectedParamTitle = computed(() => {
const param = selectedParam.value
if (!param) return ''
const unitText = param.unit ? `${param.unit}` : ''
return `参数${param.label}${unitText}`
return `节点${param.label}${unitText}`
})
const chartOption = computed<EChartsOption>(() => {
@ -213,17 +186,39 @@ let keywordTimer: number | undefined
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
keywordTimer = undefined
loadTree()
}, 300)
}
const toFiniteId = (value: any): number | undefined => {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
if (typeof value === 'string') {
if (/^\d+$/.test(value.trim())) return Number(value)
const n = Number(value)
return Number.isFinite(n) ? n : undefined
}
return undefined
}
const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
const normalizedOrgs: ApiTreeOrg[] = (Array.isArray(orgs) ? orgs : []).map((o) => ({
...o,
parentId: typeof o?.parentId === 'number' ? o.parentId : Number(o?.parentId ?? 0) || 0
}))
const normalizeOrg = (org: any): ApiTreeOrg => {
const children = Array.isArray(org?.children) ? org.children.map(normalizeOrg) : undefined
const equipments = Array.isArray(org?.equipments) ? org.equipments : undefined
const parentId = typeof org?.parentId === 'number' ? org.parentId : Number(org?.parentId ?? 0) || 0
return {
id: Number(org?.id ?? 0) || 0,
name: String(org?.name ?? ''),
orgClass: org?.orgClass ? String(org.orgClass) : undefined,
parentId,
equipments,
children
}
}
const orgTree = handleTree(normalizedOrgs, 'id', 'parentId', 'children') as ApiTreeOrg[]
const normalizedOrgs: ApiTreeOrg[] = Array.isArray(orgs) ? orgs.map(normalizeOrg) : []
const isNestedTree = normalizedOrgs.some((o) => typeof (o as any)?.children !== 'undefined')
const orgTree = isNestedTree ? normalizedOrgs : (handleTree(normalizedOrgs, 'id', 'parentId', 'children') as ApiTreeOrg[])
const toOrgNode = (org: ApiTreeOrg): DeviceTreeNode => {
const orgChildren = Array.isArray(org?.children) ? org.children.map(toOrgNode) : []
@ -235,8 +230,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
id: `param-${eq.id}-${p.id}`,
label: p?.name ?? p?.code ?? String(p?.id ?? ''),
type: 'param',
deviceId: Number(eq.id),
modelId: Number(p.id),
deviceId: toFiniteId(eq.id),
modelId: toFiniteId(p.id),
paramKey: p?.code ? String(p.code) : undefined,
unit: p?.unit ? String(p.unit) : undefined
}))
@ -245,6 +240,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
id: `equipment-${org.id}-${eq.id}`,
label: eq?.name ?? String(eq?.id ?? ''),
type: 'device',
deviceId: toFiniteId(eq.id),
paramCount: params.length,
children: paramNodes.length ? paramNodes : undefined
}
})
@ -261,11 +258,19 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
return Array.isArray(orgTree) ? orgTree.map(toOrgNode) : []
}
const extractApiOrgs = (res: any): ApiTreeOrg[] => {
if (Array.isArray(res)) return res as ApiTreeOrg[]
if (Array.isArray(res?.data)) return res.data as ApiTreeOrg[]
if (Array.isArray(res?.result)) return res.result as ApiTreeOrg[]
if (Array.isArray(res?.list)) return res.list as ApiTreeOrg[]
return []
}
const loadTree = async () => {
treeLoading.value = true
try {
const data = await OrganizationApi.deviceParameterAnalysis({ keyword: keyword.value || undefined })
treeData.value = buildTreeFromApi(Array.isArray(data) ? (data as ApiTreeOrg[]) : [])
const res = await OrganizationApi.deviceParameterAnalysis({ keyword: keyword.value || undefined, showDevices: 1 })
treeData.value = buildTreeFromApi(extractApiOrgs(res))
if (keyword.value) {
treeRef.value?.setCurrentKey?.(undefined)
selectedParam.value = null
@ -311,7 +316,7 @@ const toNumber = (value: any) => {
const fetchChart = async () => {
const param = selectedParam.value
if (!selectedDeviceId.value || !selectedModelId.value) return
if (typeof selectedDeviceId.value !== 'number' || !Number.isFinite(selectedDeviceId.value)) return
ensureDateRange()
const [start, end] = dateRange.value
@ -325,17 +330,21 @@ const fetchChart = async () => {
chartLoading.value = true
resetChartData()
try {
const data = await DeviceModelAttributeApi.operationAnalysisDetails({
const req: Record<string, any> = {
deviceId: selectedDeviceId.value,
modelId: selectedModelId.value,
collectionStartTime: buildDateTime(start, '00:00:00'),
collectionEndTime: buildDateTime(end, '23:59:59')
})
}
if (typeof selectedModelId.value === 'number' && Number.isFinite(selectedModelId.value)) {
req.modelId = selectedModelId.value
}
const data = await DeviceModelAttributeApi.operationAnalysisDetails(req as any)
const rows: Record<string, any>[] = Array.isArray(data) ? data : []
if (!rows.length) {
chartXAxis.value = []
chartSeries.value = []
chartState.value = 'empty'
message.warning('该节点下暂无参数')
return
}
@ -390,16 +399,50 @@ const fetchChart = async () => {
}
const handleTreeNodeClick = async (data: DeviceTreeNode) => {
if (data.type !== 'param') return
if (keywordTimer) {
window.clearTimeout(keywordTimer)
keywordTimer = undefined
}
const hasChildren = Array.isArray(data?.children) && data.children.length > 0
if (hasChildren) {
return
}
const isEquipmentNode = typeof data?.id === 'string' && data.id.startsWith('equipment-')
if (isEquipmentNode && (data.paramCount ?? 0) <= 0) {
selectedParam.value = data
selectedDeviceId.value = undefined
selectedModelId.value = undefined
chartState.value = 'empty'
resetChartData()
message.warning('该设备下没有参数')
return
}
const toNodeIds = (node: DeviceTreeNode): { deviceId?: number; modelId?: number } => {
const modelId = typeof node.modelId === 'number' && Number.isFinite(node.modelId) ? node.modelId : undefined
const deviceId = typeof node.deviceId === 'number' && Number.isFinite(node.deviceId) ? node.deviceId : undefined
if (typeof deviceId === 'number') return { deviceId, modelId }
const parts = String(node.id ?? '').split('-').filter(Boolean)
const last = parts.length ? parts[parts.length - 1] : undefined
const parsed = toFiniteId(last)
if (typeof parsed !== 'number' || !Number.isFinite(parsed) || parsed <= 0) {
return { modelId }
}
return { deviceId: parsed, modelId }
}
const { deviceId, modelId } = toNodeIds(data)
selectedParam.value = data
selectedDeviceId.value = data.deviceId
selectedModelId.value = data.modelId
selectedDeviceId.value = deviceId
selectedModelId.value = modelId
dateRange.value = buildDefaultDateRange()
await fetchChart()
}
const handleQuery = async () => {
if (!selectedParam.value) return
await fetchChart()
}

@ -1,12 +1,6 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="表编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入表编码" :disabled="formType === 'update'" />
</el-form-item>
@ -16,60 +10,29 @@
</el-form-item>
<el-form-item label="能源类型" prop="deviceTypeId">
<el-select
v-model="formData.deviceTypeId"
@change="handleDeviceTypeChange"
placeholder="请选择能源类型"
clearable
filterable
class="!w-full"
>
<el-select v-model="formData.deviceTypeId" @change="handleDeviceTypeChange" placeholder="请选择能源类型" clearable
filterable class="!w-full">
<el-option v-for="item in typeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属区域" prop="orgId">
<el-tree-select
v-model="formData.orgId"
:data="orgSelectTree"
:props="orgTreeSelectProps"
filterable
check-strictly
clearable
class="!w-full"
placeholder="根据区域结构筛选列表"
:loading="analysisLoading"
:render-after-expand="false"
@change="handleOrgChange"
/>
<el-tree-select v-model="formData.orgId" :data="orgSelectTree" :props="orgTreeSelectProps" filterable
check-strictly clearable class="!w-full" placeholder="根据区域结构筛选列表" :loading="analysisLoading"
:render-after-expand="false" @change="handleOrgChange" />
</el-form-item>
<el-form-item label="计算规则" prop="operationRulesVOList">
<div class="w-full flex flex-col gap-8px">
<div
v-for="(rule, index) in formData.operationRulesVOList"
:key="index"
class="w-full flex items-center gap-8px"
>
<el-tree-select
v-model="rule.pointValue"
:data="equipmentTree"
:props="treeSelectProps"
filterable
clearable
class="!w-full"
placeholder="根据设备选择点位"
:disabled="!formData.orgId"
@change="(val) => handlePointSelected(index, val)"
/>
<div v-for="(rule, index) in formData.operationRulesVOList" :key="index"
class="w-full flex items-center gap-8px">
<el-tree-select v-model="rule.pointValue" :data="equipmentTree" :props="treeSelectProps" filterable
clearable class="!w-full" placeholder="根据设备选择点位" :loading="devicePointLoading"
@change="(val) => handlePointSelected(index, val)" />
<template v-if="index < formData.operationRulesVOList.length - 1">
<el-select
v-model="formData.operationRulesVOList[index].operator"
placeholder="运算符"
class="!w-110px"
clearable
>
<el-select v-model="formData.operationRulesVOList[index].operator" placeholder="运算符" class="!w-110px"
clearable>
<el-option v-for="op in operatorOptions" :key="op" :label="op" :value="op" />
</el-select>
</template>
@ -78,13 +41,10 @@
<el-button link type="primary" @click="addRule">
<Icon icon="ep:plus" />
</el-button>
<el-button
link
type="danger"
:disabled="formData.operationRulesVOList.length <= 1"
@click="removeRule"
>
<el-icon><Remove /></el-icon>
<el-button link type="danger" :disabled="formData.operationRulesVOList.length <= 1" @click="removeRule">
<el-icon>
<Remove />
</el-icon>
</el-button>
</div>
</template>
@ -107,8 +67,9 @@
</template>
<script setup lang="ts">
import { EnergyDeviceApi, EnergyDeviceVO } from '@/api/mes/energydevice'
import {EnergyTypeApi, EnergyTypeVO} from "@/api/mes/energytype";
import { EnergyTypeApi, EnergyTypeVO } from "@/api/mes/energytype";
import { OrganizationApi, DeviceParameterAnalysisNodeVO } from '@/api/mes/organization'
import { DeviceApi } from '@/api/iot/device'
import { Remove } from '@element-plus/icons-vue'
import { handleTree } from '@/utils/tree'
@ -158,6 +119,11 @@ const analysisTree = computed(() => {
const list = analysisList.value ?? []
if (!list.length) return [] as OrgAnalysisNode[]
const hasChildren = list.some((n) => Array.isArray((n as any).children) && (n as any).children.length)
if (hasChildren) {
return list.map((n) => ({ ...n })) as OrgAnalysisNode[]
}
const hasParentId = list.some((n) => (n as any).parentId !== undefined && (n as any).parentId !== null)
if (!hasParentId) {
return list.map((n) => ({ ...n })) as OrgAnalysisNode[]
@ -181,8 +147,6 @@ const orgSelectTree = computed(() => {
const kept: OrgSelectNode[] = []
nodes.forEach((node) => {
const childNodes = Array.isArray(node.children) ? pruneAndAttach(node.children) : []
const hasValidEquipment = (node?.equipments ?? []).some((eq) => (eq?.parameters ?? []).length > 0)
if (!hasValidEquipment && !childNodes.length) return
kept.push({ id: node.id, name: node.name, children: childNodes.length ? childNodes : undefined })
})
return kept
@ -206,27 +170,100 @@ const treeSelectProps = {
const operatorOptions = ['+', '-', '*', '/']
const currentOrgNode = computed(() => {
return analysisList.value.find((o) => o.id === formData.value.orgId)
})
const isSameId = (a: any, b: any) => {
return String(a ?? '') === String(b ?? '')
}
const equipmentTree = computed(() => {
const equipments = currentOrgNode.value?.equipments ?? []
return equipments
.map((eq) => {
const params = eq.parameters ?? []
if (!params.length) return null
return {
id: `device:${eq.id}`,
name: eq.name,
disabled: true,
children: params.map((p) => ({
id: `${eq.id}:${p.id}`,
name: `${eq.name}: ${p.name}`
}))
const findOrgNode = (nodes: OrgAnalysisNode[], id: any): OrgAnalysisNode | undefined => {
for (const node of nodes) {
if (isSameId(node.id, id)) return node
const children = Array.isArray(node.children) ? node.children : []
if (children.length) {
const found = findOrgNode(children, id)
if (found) return found
}
}
return undefined
}
const devicePointLoading = ref(false)
const devicePointTree = ref<any[]>([])
const loadDevicePointTree = async () => {
if (devicePointTree.value.length > 0) {
return
}
devicePointLoading.value = true
try {
const res: any = await DeviceApi.devicePointList()
const list = Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : []
const normalizeToString = (v: any) => String(v ?? '')
const isNonEmptyArray = (v: any) => Array.isArray(v) && v.length > 0
const deviceGroups = new Map<string, { deviceId: string; deviceName: string; points: any[] }>()
const pushPoint = (deviceId: any, deviceName: any, pointId: any, pointName: any) => {
const dId = normalizeToString(deviceId)
if (!dId) return
const group = deviceGroups.get(dId) ?? { deviceId: dId, deviceName: String(deviceName ?? ''), points: [] }
if (!group.deviceName) group.deviceName = String(deviceName ?? '')
const pId = normalizeToString(pointId)
if (!pId) {
deviceGroups.set(dId, group)
return
}
})
.filter(Boolean) as any[]
group.points.push({ id: `${dId}:${pId}`, name: `${group.deviceName}: ${String(pointName ?? '')}`.trim() })
deviceGroups.set(dId, group)
}
if (isNonEmptyArray(list) && ((list[0] as any)?.deviceId !== undefined || (list[0] as any)?.deviceName !== undefined)) {
; (list as any[]).forEach((row) => {
const deviceId = row?.deviceId ?? row?.devId ?? row?.device_id
const deviceName = row?.deviceName ?? row?.devName ?? row?.device_name
const points =
row?.contactModelDOList ??
row?.points ??
row?.pointList ??
row?.devicePoints ??
row?.devicePointList ??
row?.parameters
if (Array.isArray(points)) {
points.forEach((p) => {
const pointId = p?.pointId ?? p?.id
const pointName = p?.attributeName ?? p?.pointName ?? p?.name
pushPoint(deviceId, deviceName, pointId, pointName)
})
} else {
const pointId = row?.pointId ?? row?.attributeId ?? row?.devicePointId
const pointName = row?.attributeName ?? row?.pointName ?? row?.attributeName
pushPoint(deviceId, deviceName, pointId, pointName)
}
})
} else if (isNonEmptyArray(list)) {
; (list as any[]).forEach((row) => {
const deviceId = row?.deviceId ?? row?.devId
const deviceName = row?.deviceName ?? row?.devName
const pointId = row?.pointId ?? row?.id
const pointName = row?.attributeName ?? row?.pointName ?? row?.name
pushPoint(deviceId, deviceName, pointId, pointName)
})
}
devicePointTree.value = Array.from(deviceGroups.values())
.map((g) => {
const children = (g.points ?? []).filter((p) => p?.id)
if (!children.length) return null
return { id: `device:${g.deviceId}`, name: g.deviceName, disabled: true, children }
})
.filter(Boolean) as any[]
} finally {
devicePointLoading.value = false
}
}
const equipmentTree = computed(() => {
return devicePointTree.value
})
/** 打开弹窗 */
@ -237,13 +274,14 @@ const open = async (type: string, id?: number) => {
resetForm()
await loadAnalysis()
await loadDevicePointTree()
//
if (id) {
formLoading.value = true
try {
formData.value = await EnergyDeviceApi.getEnergyDevice(id)
if (!Array.isArray((formData.value as any).operationRulesVOList)) {
;(formData.value as any).operationRulesVOList = [
; (formData.value as any).operationRulesVOList = [
{ deviceId: undefined, pointId: undefined, operator: undefined, pointValue: undefined }
]
}
@ -263,21 +301,23 @@ const submitForm = async () => {
//
formLoading.value = true
try {
const data = formData.value as unknown as EnergyDeviceVO
data.deviceTypeName = typeList.value.find(item => item.id === data.deviceTypeId)?.name
const base = formData.value as unknown as EnergyDeviceVO
const payload: any = {
...(base as any),
deviceTypeName: typeList.value.find(item => item.id === base.deviceTypeId)?.name,
operationRulesVOList: buildOperationRulesPayload()
}
const org = analysisList.value.find((o) => o.id === data.orgId)
const org = findOrgNode(analysisTree.value ?? [], base.orgId)
if (org) {
;(data as any).orgName = org.name
payload.orgName = org.name
}
;(data as any).operationRulesVOList = buildOperationRulesPayload()
if (formType.value === 'create') {
await EnergyDeviceApi.createEnergyDevice(data)
await EnergyDeviceApi.createEnergyDevice(payload)
message.success(t('common.createSuccess'))
} else {
await EnergyDeviceApi.updateEnergyDevice(data)
await EnergyDeviceApi.updateEnergyDevice(payload)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
@ -321,7 +361,9 @@ const loadAnalysis = async () => {
}
analysisLoading.value = true
try {
analysisList.value = await OrganizationApi.deviceParameterAnalysis({})
const res: any = await OrganizationApi.deviceParameterAnalysis({ showDevices: 2 })
const list = Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : []
analysisList.value = list as OrgAnalysisNode[]
} finally {
analysisLoading.value = false
}
@ -332,11 +374,8 @@ const handleDeviceTypeChange = () => {
}
const handleOrgChange = () => {
const org = analysisList.value.find((o) => o.id === formData.value.orgId)
const org = findOrgNode(analysisTree.value ?? [], formData.value.orgId)
formData.value.orgName = org?.name
formData.value.operationRulesVOList = [
{ deviceId: undefined, pointId: undefined, operator: undefined, pointValue: undefined }
]
}
const handlePointSelected = (index: number, val: any) => {

Loading…
Cancel
Save