From 3dcd2e613ee3204c7195224c9f232cb3af61cf20 Mon Sep 17 00:00:00 2001 From: hwj Date: Tue, 6 Jan 2026 16:34:12 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=AE=BE=E5=A4=87=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E5=8F=82=E6=95=B0=E5=88=86=E6=9E=90-=E6=A0=91?= =?UTF-8?q?=E7=8A=B6=E7=BB=93=E6=9E=84/=E5=9B=BE=E8=A1=A8=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/devicemodelattribute/index.ts | 9 + src/api/mes/organization/index.ts | 22 ++ src/views/iot/deviceParamAnalysis/index.vue | 258 +++++++++++++------- 3 files changed, 200 insertions(+), 89 deletions(-) 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() }