You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
11 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<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"
/>
<div class="mt-12px">
<el-tree
ref="treeRef"
v-loading="treeLoading"
:data="treeData"
:props="treeProps"
node-key="id"
highlight-current
@node-click="handleTreeNodeClick"
/>
</div>
</ContentWrap>
</el-col>
<el-col :span="18" :xs="24">
<ContentWrap>
<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"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="!selectedParam" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button :disabled="!selectedParam" @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-alert
v-if="selectedParam"
:title="selectedParamTitle"
type="info"
:closable="false"
class="mb-12px"
/>
<div v-loading="chartLoading">
<el-empty v-if="!selectedParam" description="请选择左侧参数" />
<EChart v-else :key="chartRenderKey" :options="chartOption" height="70vh" />
</div>
</ContentWrap>
</el-col>
</el-row>
</template>
<script setup lang="ts">
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' })
type TreeNodeType = 'device' | 'param'
type DeviceTreeNode = {
id: string
label: string
type: TreeNodeType
children?: DeviceTreeNode[]
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()
const treeLoading = ref(false)
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<{ name: string; data: Array<number | null> }[]>([])
const chartRenderKey = ref(0)
const buildDefaultDateRange = (): [string, string] => {
const end = dayjs().endOf('day')
const start = end.subtract(6, 'day').startOf('day')
return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')]
}
const dateRange = ref<[string, string]>(buildDefaultDateRange())
const dateShortcuts = [
{
text: '最近 7 天',
value: () => {
const end = dayjs().endOf('day').toDate()
const start = dayjs().subtract(6, 'day').startOf('day').toDate()
return [start, end]
}
},
{
text: '上周',
value: () => {
const start = dayjs().subtract(1, 'week').startOf('week').toDate()
const end = dayjs().subtract(1, 'week').endOf('week').toDate()
return [start, end]
}
},
{
text: '上个月',
value: () => {
const start = dayjs().subtract(1, 'month').startOf('month').toDate()
const end = dayjs().subtract(1, 'month').endOf('month').toDate()
return [start, end]
}
},
{
text: '三个月内',
value: () => {
const end = dayjs().endOf('day').toDate()
const start = dayjs().subtract(3, 'month').startOf('day').toDate()
return [start, end]
}
}
]
const selectedParamTitle = computed(() => {
const param = selectedParam.value
if (!param) return ''
const unitText = param.unit ? `${param.unit}` : ''
return `参数:${param.label}${unitText}`
})
const chartOption = computed<EChartsOption>(() => {
const unit = selectedParam.value?.unit
return {
tooltip: { trigger: 'axis' },
legend: { top: 10, left: 'center', type: 'scroll' },
grid: { left: 50, right: 60, top: 80, bottom: 40, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: chartXAxis.value
},
yAxis: {
type: 'value',
name: unit || ''
},
series: chartSeries.value.map((s) => ({
type: 'line',
name: s.name,
smooth: false,
showSymbol: false,
data: s.data
}))
}
})
let keywordTimer: number | undefined
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
loadTree()
}, 300)
}
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 {
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
}
}
const ensureDateRange = () => {
if (!dateRange.value || dateRange.value.length !== 2) {
dateRange.value = buildDefaultDateRange()
}
}
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 (!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 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 {
chartLoading.value = false
}
}
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()
}
const handleQuery = async () => {
if (!selectedParam.value) return
await fetchChart()
}
const resetQuery = async () => {
dateRange.value = buildDefaultDateRange()
await fetchChart()
}
onMounted(async () => {
await loadTree()
})
</script>
<style scoped lang="scss">
:deep(.el-tree) {
max-height: calc(100vh - 280px);
overflow: auto;
}
</style>