diff --git a/src/api/iot/devicemodelattribute/index.ts b/src/api/iot/devicemodelattribute/index.ts
index c589c9cc..8694e034 100644
--- a/src/api/iot/devicemodelattribute/index.ts
+++ b/src/api/iot/devicemodelattribute/index.ts
@@ -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 })
+ }
}
diff --git a/src/api/mes/organization/index.ts b/src/api/mes/organization/index.ts
index 9036e87a..9510afbd 100644
--- a/src/api/mes/organization/index.ts
+++ b/src/api/mes/organization/index.ts
@@ -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 })
}
}
diff --git a/src/views/iot/deviceParamAnalysis/index.vue b/src/views/iot/deviceParamAnalysis/index.vue
index 731424a0..cccd34b3 100644
--- a/src/views/iot/deviceParamAnalysis/index.vue
+++ b/src/views/iot/deviceParamAnalysis/index.vue
@@ -17,8 +17,6 @@
:props="treeProps"
node-key="id"
highlight-current
- :expand-on-click-node="false"
- :filter-node-method="filterTreeNode"
@node-click="handleTreeNodeClick"
/>
@@ -61,7 +59,7 @@
/>
-
+
@@ -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([])
+const selectedDeviceId = ref(undefined)
+const selectedModelId = ref(undefined)
+
const selectedParam = ref(null)
const chartLoading = ref(false)
const chartXAxis = ref([])
-const chartSeries = ref([])
+const chartSeries = ref<{ name: string; data: Array }[]>([])
+const chartRenderKey = ref(0)
const buildDefaultDateRange = (): [string, string] => {
const end = dayjs().endOf('day')
@@ -154,7 +178,8 @@ const chartOption = computed(() => {
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(() => {
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 => {
- 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[] = 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>()
+ 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()
}