feat:设备运行参数分析-树状结构/图表对接接口

main
黄伟杰 4 weeks ago
parent b9607a5d03
commit 3dcd2e613e

@ -45,4 +45,13 @@ export const DeviceModelAttributeApi = {
exportDeviceModelAttribute: async (params) => {
return await request.download({ url: `/iot/device-model-attribute/export-excel`, params })
},
operationAnalysisDetails: async (params: {
deviceId: number
modelId: number
collectionStartTime?: string
collectionEndTime?: string
}) => {
return await request.get({ url: `/iot/device-model-attribute/operationAnalysisDetails`, params })
}
}

@ -16,6 +16,24 @@ export interface OrganizationVO {
orgType: string // 组织类型
}
export type DeviceParameterAnalysisNodeVO = {
id: number | string
name: string
equipments?: {
id: number | string
name: string
parameters?: {
id: number | string
name: string
code?: string
type?: string | null
dataType?: string | null
unit?: string | null
ratio?: number | null
}[]
}[]
}
// 产线工位 API
export const OrganizationApi = {
// 查询产线工位列表
@ -49,5 +67,9 @@ export const OrganizationApi = {
// 导出产线工位 Excel
exportOrganization: async (params) => {
return await request.download({ url: `/mes/organization/export-excel`, params })
},
deviceParameterAnalysis: async (params: { keyword?: string }) => {
return await request.get({ url: `/mes/organization/deviceParameterAnalysis`, params })
}
}

@ -17,8 +17,6 @@
:props="treeProps"
node-key="id"
highlight-current
:expand-on-click-node="false"
:filter-node-method="filterTreeNode"
@node-click="handleTreeNodeClick"
/>
</div>
@ -61,7 +59,7 @@
/>
<div v-loading="chartLoading">
<el-empty v-if="!selectedParam" description="请选择左侧参数" />
<EChart v-else :options="chartOption" height="520px" />
<EChart v-else :key="chartRenderKey" :options="chartOption" height="70vh" />
</div>
</ContentWrap>
</el-col>
@ -72,6 +70,8 @@
import type { EChartsOption } from 'echarts'
import dayjs from 'dayjs'
import { Echart as EChart } from '@/components/Echart'
import { OrganizationApi } from '@/api/mes/organization'
import { DeviceModelAttributeApi } from '@/api/iot/devicemodelattribute'
defineOptions({ name: 'DeviceParamAnalysis' })
@ -82,11 +82,31 @@ type DeviceTreeNode = {
label: string
type: TreeNodeType
children?: DeviceTreeNode[]
deviceId?: string
deviceId?: number
modelId?: number
paramKey?: string
unit?: string
}
type ApiTreeParameter = {
id: number
name: string
code?: string
unit?: string | null
}
type ApiTreeEquipment = {
id: number
name: string
parameters?: ApiTreeParameter[]
}
type ApiTreeOrg = {
id: number
name: string
equipments?: ApiTreeEquipment[]
}
const message = useMessage()
const treeRef = ref()
@ -95,10 +115,14 @@ const keyword = ref('')
const treeProps = { children: 'children', label: 'label' }
const treeData = ref<DeviceTreeNode[]>([])
const selectedDeviceId = ref<number | undefined>(undefined)
const selectedModelId = ref<number | undefined>(undefined)
const selectedParam = ref<DeviceTreeNode | null>(null)
const chartLoading = ref(false)
const chartXAxis = ref<string[]>([])
const chartSeries = ref<number[]>([])
const chartSeries = ref<{ name: string; data: Array<number | null> }[]>([])
const chartRenderKey = ref(0)
const buildDefaultDateRange = (): [string, string] => {
const end = dayjs().endOf('day')
@ -154,7 +178,8 @@ const chartOption = computed<EChartsOption>(() => {
const unit = selectedParam.value?.unit
return {
tooltip: { trigger: 'axis' },
grid: { left: 30, right: 20, top: 20, bottom: 40, containLabel: true },
legend: { top: 10, left: 'center', type: 'scroll' },
grid: { left: 50, right: 60, top: 80, bottom: 40, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
@ -164,96 +189,73 @@ const chartOption = computed<EChartsOption>(() => {
type: 'value',
name: unit || ''
},
series: [
{
type: 'line',
name: selectedParam.value?.label || '参数',
smooth: true,
showSymbol: false,
data: chartSeries.value
}
]
series: chartSeries.value.map((s) => ({
type: 'line',
name: s.name,
smooth: false,
showSymbol: false,
data: s.data
}))
}
})
let keywordTimer: number | undefined
const handleKeywordChange = () => {
treeRef.value?.filter(keyword.value)
}
const filterTreeNode = (value: string, data: DeviceTreeNode) => {
if (!value) return true
return data.label?.toLowerCase().includes(value.toLowerCase())
}
const mockFetchTree = async (): Promise<DeviceTreeNode[]> => {
const devices = [
{ id: 'D-1001', label: '压合机-01' },
{ id: 'D-1002', label: '烘干线-02' },
{ id: 'D-1003', label: '制浆机-03' },
{ id: 'D-1004', label: '包装机-04' }
]
const params = [
{ key: 'temp', label: '温度', unit: '℃' },
{ key: 'pressure', label: '压力', unit: 'kPa' },
{ key: 'speed', label: '速度', unit: 'm/s' },
{ key: 'power', label: '功率', unit: 'kW' },
{ key: 'current', label: '电流', unit: 'A' }
]
await new Promise((resolve) => setTimeout(resolve, 200))
return devices.map((d) => ({
id: d.id,
label: d.label,
type: 'device',
deviceId: d.id,
children: params.map((p) => ({
id: `${d.id}::${p.key}`,
label: p.label,
type: 'param',
deviceId: d.id,
paramKey: p.key,
unit: p.unit
}))
}))
}
const buildSeriesSeed = (key: string) => {
let hash = 0
for (let i = 0; i < key.length; i++) {
hash = (hash << 5) - hash + key.charCodeAt(i)
hash |= 0
}
return Math.abs(hash)
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
loadTree()
}, 300)
}
const mockFetchSeries = async (args: { deviceId: string; paramKey: string; start: string; end: string }) => {
await new Promise((resolve) => setTimeout(resolve, 250))
const start = dayjs(args.start)
const end = dayjs(args.end)
const diff = Math.max(end.startOf('day').diff(start.startOf('day'), 'day'), 0)
const count = Math.min(diff + 1, 366)
const x: string[] = []
const y: number[] = []
const seed = buildSeriesSeed(`${args.deviceId}::${args.paramKey}`)
const base = 20 + (seed % 30)
const amp = 5 + (seed % 10)
for (let i = 0; i < count; i++) {
const d = start.add(i, 'day')
x.push(d.format('YYYY-MM-DD'))
const v = base + Math.sin(i / 2) * amp + ((seed % 7) - 3) * 0.6
y.push(Number(v.toFixed(2)))
}
return { x, y }
const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
return (
orgs?.map((org) => {
const equipments = Array.isArray(org?.equipments) ? org.equipments : []
const equipmentNodes: DeviceTreeNode[] = equipments.map((eq) => {
const params = Array.isArray(eq?.parameters) ? eq.parameters : []
const paramNodes: DeviceTreeNode[] = params.map((p) => ({
id: `param-${eq.id}-${p.id}`,
label: p?.name ?? p?.code ?? String(p?.id ?? ''),
type: 'param',
deviceId: Number(eq.id),
modelId: Number(p.id),
paramKey: p?.code ? String(p.code) : undefined,
unit: p?.unit ? String(p.unit) : undefined
}))
return {
id: `equipment-${org.id}-${eq.id}`,
label: eq?.name ?? String(eq?.id ?? ''),
type: 'device',
children: paramNodes.length ? paramNodes : undefined
}
})
return {
id: `org-${org.id}`,
label: org?.name ?? String(org?.id ?? ''),
type: 'device',
children: equipmentNodes.length ? equipmentNodes : undefined
}
}) ?? []
)
}
const loadTree = async () => {
treeLoading.value = true
try {
treeData.value = await mockFetchTree()
const data = await OrganizationApi.deviceParameterAnalysis({ keyword: keyword.value || undefined })
treeData.value = buildTreeFromApi(Array.isArray(data) ? (data as ApiTreeOrg[]) : [])
if (keyword.value) {
treeRef.value?.setCurrentKey?.(undefined)
selectedParam.value = null
selectedDeviceId.value = undefined
selectedModelId.value = undefined
chartXAxis.value = []
chartSeries.value = []
}
} catch {
message.error('获取树数据失败')
} finally {
treeLoading.value = false
}
@ -265,19 +267,95 @@ const ensureDateRange = () => {
}
}
const resetChartData = () => {
chartXAxis.value = []
chartSeries.value = []
chartRenderKey.value++
}
const toNumber = (value: any) => {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const n = Number(value)
return Number.isFinite(n) ? n : 0
}
if (Array.isArray(value)) return 0
if (value && typeof value === 'object') {
const maybe = value.value ?? value.val ?? value.count ?? value.amount
const n = Number(maybe)
return Number.isFinite(n) ? n : 0
}
return 0
}
const fetchChart = async () => {
const param = selectedParam.value
if (!param?.deviceId || !param.paramKey) return
if (!selectedDeviceId.value || !selectedModelId.value) return
ensureDateRange()
const [start, end] = dateRange.value
if (!start || !end) return
const buildDateTime = (date: string, suffix: string) => {
return date.includes(':') ? date : `${date} ${suffix}`
}
chartLoading.value = true
resetChartData()
try {
const res = await mockFetchSeries({ deviceId: param.deviceId, paramKey: param.paramKey, start, end })
chartXAxis.value = res.x
chartSeries.value = res.y
const data = await DeviceModelAttributeApi.operationAnalysisDetails({
deviceId: selectedDeviceId.value,
modelId: selectedModelId.value,
collectionStartTime: buildDateTime(start, '00:00:00'),
collectionEndTime: buildDateTime(end, '23:59:59')
})
const rows: Record<string, any>[] = Array.isArray(data) ? data : []
if (!rows.length) {
chartXAxis.value = []
chartSeries.value = []
return
}
const firstRow = rows[0] || {}
const keys = Object.keys(firstRow)
const xKey = keys.find((k) => /time|date|时间|日期/i.test(k))
if (xKey) {
const sortedRows = [...rows].sort((a, b) =>
String(a?.[xKey] ?? '').localeCompare(String(b?.[xKey] ?? ''))
)
chartXAxis.value = sortedRows.map((r) => String(r?.[xKey] ?? ''))
const seriesKeys = keys.filter((k) => k !== xKey)
const seriesMap = new Map<string, Array<number | null>>()
const ensureSeries = (name: string) => {
if (!seriesMap.has(name)) {
seriesMap.set(name, Array(sortedRows.length).fill(null))
}
return seriesMap.get(name)!
}
sortedRows.forEach((row, rowIndex) => {
seriesKeys.forEach((k) => {
const v = row?.[k]
if (Array.isArray(v)) {
v.forEach((item) => {
const name = String(item?.attributeName ?? k)
const series = ensureSeries(name)
series[rowIndex] = toNumber(item?.addressValue ?? item?.value ?? item?.val)
})
return
}
const series = ensureSeries(k)
series[rowIndex] = toNumber(v)
})
})
chartSeries.value = Array.from(seriesMap.entries()).map(([name, data]) => ({ name, data }))
} else {
chartXAxis.value = keys
const values = keys.map((k) => toNumber(firstRow[k]))
chartSeries.value = [{ name: param?.label || '参数', data: values }]
}
} catch {
message.error('获取图表数据失败')
} finally {
@ -288,6 +366,8 @@ const fetchChart = async () => {
const handleTreeNodeClick = async (data: DeviceTreeNode) => {
if (data.type !== 'param') return
selectedParam.value = data
selectedDeviceId.value = data.deviceId
selectedModelId.value = data.modelId
dateRange.value = buildDefaultDateRange()
await fetchChart()
}

Loading…
Cancel
Save