|
|
|
|
@ -0,0 +1,315 @@
|
|
|
|
|
<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
|
|
|
|
|
:expand-on-click-node="false"
|
|
|
|
|
:filter-node-method="filterTreeNode"
|
|
|
|
|
@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 :options="chartOption" height="520px" />
|
|
|
|
|
</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'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'DeviceParamAnalysis' })
|
|
|
|
|
|
|
|
|
|
type TreeNodeType = 'device' | 'param'
|
|
|
|
|
|
|
|
|
|
type DeviceTreeNode = {
|
|
|
|
|
id: string
|
|
|
|
|
label: string
|
|
|
|
|
type: TreeNodeType
|
|
|
|
|
children?: DeviceTreeNode[]
|
|
|
|
|
deviceId?: string
|
|
|
|
|
paramKey?: string
|
|
|
|
|
unit?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const message = useMessage()
|
|
|
|
|
|
|
|
|
|
const treeRef = ref()
|
|
|
|
|
const treeLoading = ref(false)
|
|
|
|
|
const keyword = ref('')
|
|
|
|
|
const treeProps = { children: 'children', label: 'label' }
|
|
|
|
|
const treeData = ref<DeviceTreeNode[]>([])
|
|
|
|
|
|
|
|
|
|
const selectedParam = ref<DeviceTreeNode | null>(null)
|
|
|
|
|
const chartLoading = ref(false)
|
|
|
|
|
const chartXAxis = ref<string[]>([])
|
|
|
|
|
const chartSeries = ref<number[]>([])
|
|
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
grid: { left: 30, right: 20, top: 20, bottom: 40, containLabel: true },
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
data: chartXAxis.value
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
name: unit || ''
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
type: 'line',
|
|
|
|
|
name: selectedParam.value?.label || '参数',
|
|
|
|
|
smooth: true,
|
|
|
|
|
showSymbol: false,
|
|
|
|
|
data: chartSeries.value
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 loadTree = async () => {
|
|
|
|
|
treeLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
treeData.value = await mockFetchTree()
|
|
|
|
|
} finally {
|
|
|
|
|
treeLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ensureDateRange = () => {
|
|
|
|
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
|
|
|
|
dateRange.value = buildDefaultDateRange()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchChart = async () => {
|
|
|
|
|
const param = selectedParam.value
|
|
|
|
|
if (!param?.deviceId || !param.paramKey) return
|
|
|
|
|
|
|
|
|
|
ensureDateRange()
|
|
|
|
|
const [start, end] = dateRange.value
|
|
|
|
|
if (!start || !end) return
|
|
|
|
|
|
|
|
|
|
chartLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await mockFetchSeries({ deviceId: param.deviceId, paramKey: param.paramKey, start, end })
|
|
|
|
|
chartXAxis.value = res.x
|
|
|
|
|
chartSeries.value = res.y
|
|
|
|
|
} catch {
|
|
|
|
|
message.error('获取图表数据失败')
|
|
|
|
|
} finally {
|
|
|
|
|
chartLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTreeNodeClick = async (data: DeviceTreeNode) => {
|
|
|
|
|
if (data.type !== 'param') return
|
|
|
|
|
selectedParam.value = data
|
|
|
|
|
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>
|