|
|
|
|
@ -17,10 +17,10 @@
|
|
|
|
|
<Icon icon="ep:document-checked" />
|
|
|
|
|
保存分析方案
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="handleExport">
|
|
|
|
|
<!-- <el-button @click="handleExport">
|
|
|
|
|
<Icon icon="ep:download" />
|
|
|
|
|
导出分析结果
|
|
|
|
|
</el-button>
|
|
|
|
|
</el-button>-->
|
|
|
|
|
<el-button @click="handleRefresh">
|
|
|
|
|
<Icon icon="ep:refresh" />
|
|
|
|
|
刷新
|
|
|
|
|
@ -64,6 +64,7 @@
|
|
|
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@change="handleStartTimeChange"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
@ -77,6 +78,8 @@
|
|
|
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@change="handleEndTimeChange"
|
|
|
|
|
:picker-options="endTimeOptions"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
@ -101,8 +104,9 @@
|
|
|
|
|
placeholder="搜索点位名称或编码"
|
|
|
|
|
clearable
|
|
|
|
|
style="width: 200px; margin-right: 10px"
|
|
|
|
|
@input="handlePointSearch"
|
|
|
|
|
/>
|
|
|
|
|
<el-button @click="selectAllTemperature">选择温度类</el-button>
|
|
|
|
|
<el-button @click="selectAllTemperature">选择工艺参数</el-button>
|
|
|
|
|
<el-button @click="clearAllPoints">清空</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -123,9 +127,9 @@
|
|
|
|
|
<div class="point-list">
|
|
|
|
|
<el-collapse v-model="activePointGroup">
|
|
|
|
|
<el-collapse-item
|
|
|
|
|
v-for="group in pointGroups"
|
|
|
|
|
v-for="group in filteredPointGroups"
|
|
|
|
|
:key="group.group"
|
|
|
|
|
:title="group.group"
|
|
|
|
|
:title="`${group.group} (${group.points.length})`"
|
|
|
|
|
:name="group.group"
|
|
|
|
|
>
|
|
|
|
|
<el-checkbox-group v-model="selectedPointCodes">
|
|
|
|
|
@ -152,17 +156,17 @@
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
<el-form-item label="分析规则">
|
|
|
|
|
<!-- <el-form-item label="分析规则">
|
|
|
|
|
<el-checkbox-group v-model="queryParams.rules">
|
|
|
|
|
<el-checkbox label="max">最大值</el-checkbox>
|
|
|
|
|
<el-checkbox label="min">最小值</el-checkbox>
|
|
|
|
|
<el-checkbox label="avg">平均值</el-checkbox>
|
|
|
|
|
<el-checkbox label="range">波动值</el-checkbox>
|
|
|
|
|
<el-checkbox label="upperCount">超上限次数</el-checkbox>
|
|
|
|
|
<!– <el-checkbox label="upperCount">超上限次数</el-checkbox>
|
|
|
|
|
<el-checkbox label="lowerCount">超下限次数</el-checkbox>
|
|
|
|
|
<el-checkbox label="continuousAbnormal">连续异常时长</el-checkbox>
|
|
|
|
|
<el-checkbox label="continuousAbnormal">连续异常时长</el-checkbox>–>
|
|
|
|
|
</el-checkbox-group>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form-item>-->
|
|
|
|
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" @click="handleAnalyze">
|
|
|
|
|
@ -197,19 +201,14 @@
|
|
|
|
|
<span class="card-title">多点位历史趋势分析</span>
|
|
|
|
|
<div class="chart-toolbar">
|
|
|
|
|
<el-radio-group v-model="chartMode" size="small">
|
|
|
|
|
<el-radio-button label="all">全部显示</el-radio-button>
|
|
|
|
|
<el-radio-button label="abnormal">仅异常点位</el-radio-button>
|
|
|
|
|
<!-- <el-radio-button label="all">全部显示</el-radio-button>-->
|
|
|
|
|
<!-- <el-radio-button label="abnormal">仅异常点位</el-radio-button>-->
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
<el-tooltip content="鼠标悬浮在图表上查看详细数据" placement="top">
|
|
|
|
|
<el-icon><QuestionFilled /></el-icon>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- ECharts 图表容器 -->
|
|
|
|
|
<div ref="chartRef" class="chart-container"></div>
|
|
|
|
|
|
|
|
|
|
<el-descriptions :column="4" border class="chart-note">
|
|
|
|
|
<div id="historyTrendChart" class="chart-container"></div>
|
|
|
|
|
<!-- <el-descriptions :column="4" border class="chart-note">
|
|
|
|
|
<el-descriptions-item label="当前展示点位">
|
|
|
|
|
{{ chartNote.currentPoints }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
@ -224,11 +223,11 @@
|
|
|
|
|
<el-descriptions-item label="最大波动区间">
|
|
|
|
|
{{ chartNote.maxRange }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</el-descriptions>-->
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<!-- 分析结果表格 -->
|
|
|
|
|
<el-card class="result-card" shadow="never">
|
|
|
|
|
<el-card class="result-card" shadow="never" style="width: 44%; margin-right: 0.6%; display: inline-block; vertical-align: top;">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span class="card-title">分析结果</span>
|
|
|
|
|
@ -240,59 +239,59 @@
|
|
|
|
|
|
|
|
|
|
<el-table :data="resultData" style="width: 100%">
|
|
|
|
|
<el-table-column prop="name" label="点位名称" width="120" />
|
|
|
|
|
<el-table-column prop="code" label="点位编码" width="120" />
|
|
|
|
|
<el-table-column prop="unit" label="单位" width="80" />
|
|
|
|
|
<el-table-column prop="max" label="最大值" width="100">
|
|
|
|
|
<el-table-column prop="code" label="点位编码" width="140" />
|
|
|
|
|
<el-table-column prop="unit" label="单位" width="70" />
|
|
|
|
|
<el-table-column prop="max" label="最大值" width="110">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="row.maxClass">{{ formatValue(row.max, row.unit) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="min" label="最小值" width="100">
|
|
|
|
|
<el-table-column prop="min" label="最小值" width="110">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="row.minClass">{{ formatValue(row.min, row.unit) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="avg" label="平均值" width="100">
|
|
|
|
|
<el-table-column prop="avg" label="平均值" width="110">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
{{ formatValue(row.avg, row.unit) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="range" label="波动值" width="100">
|
|
|
|
|
<el-table-column prop="range" label="波动值" width="110">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="row.rangeClass">{{ formatValue(row.range, row.unit) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="upperCount" label="超上限次数" width="100">
|
|
|
|
|
<!-- <el-table-column prop="upperCount" label="超上限次数" width="100">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="row.upperCount > 0 ? 'text-danger' : ''">
|
|
|
|
|
{{ row.upperCount }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="lowerCount" label="超下限次数" width="100">
|
|
|
|
|
</el-table-column>-->
|
|
|
|
|
<!-- <el-table-column prop="lowerCount" label="超下限次数" width="100">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="row.lowerCount > 0 ? 'text-warning' : ''">
|
|
|
|
|
{{ row.lowerCount }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="continuousAbnormal" label="连续异常时长" width="120">
|
|
|
|
|
</el-table-column>-->
|
|
|
|
|
<!-- <el-table-column prop="continuousAbnormal" label="连续异常时长" width="120">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
{{ row.continuousAbnormal }}分钟
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="conclusion" label="分析结论" width="120">
|
|
|
|
|
</el-table-column>-->
|
|
|
|
|
<!-- <el-table-column prop="conclusion" label="分析结论" width="120">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.statusType" size="small">
|
|
|
|
|
{{ row.conclusion }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table-column>-->
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- 历史明细 -->
|
|
|
|
|
<el-card class="detail-card" shadow="never">
|
|
|
|
|
<el-card class="detail-card" shadow="never" style="width: 55.4%; display: inline-block; vertical-align: top;">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span class="card-title">历史明细</span>
|
|
|
|
|
@ -306,20 +305,20 @@
|
|
|
|
|
<el-table-column prop="time" label="采集时间" width="150" />
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="point in displayPoints"
|
|
|
|
|
:key="point.code"
|
|
|
|
|
:prop="point.code"
|
|
|
|
|
:key="point.code.toLowerCase()"
|
|
|
|
|
:prop="point.code.toLowerCase()"
|
|
|
|
|
:label="point.name"
|
|
|
|
|
width="120"
|
|
|
|
|
:min-width="100"
|
|
|
|
|
>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span :class="getValueClass(row[point.code], point)">
|
|
|
|
|
{{ formatValue(row[point.code], point.unit) }} {{ point.unit }}
|
|
|
|
|
<span :class="getValueClass(row[point.code.toLowerCase()], point)">
|
|
|
|
|
{{ formatValue(row[point.code.toLowerCase()], point.unit) }} {{ point.unit }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 侧边栏 -->
|
|
|
|
|
<!-- <el-aside width="320px" class="suggest-aside">
|
|
|
|
|
<el-card class="suggest-card" shadow="never">
|
|
|
|
|
@ -371,8 +370,7 @@ import {useRoute, useRouter} from 'vue-router'
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
|
|
|
|
import {CategoryApi} from "@/api/bpm/category";
|
|
|
|
|
import {DeviceApi} from "@/api/iot/device";
|
|
|
|
|
import {DeviceApi, DeviceVO} from "@/api/iot/device";
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
let query = route.query;
|
|
|
|
|
@ -423,13 +421,32 @@ const pointGroups = ref([
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const filteredPointGroups = computed(() => {
|
|
|
|
|
if (!pointKeyword.value.trim()) return pointGroups.value
|
|
|
|
|
|
|
|
|
|
const keyword = pointKeyword.value.toLowerCase()
|
|
|
|
|
return pointGroups.value
|
|
|
|
|
.map(group => {
|
|
|
|
|
const matchedPoints = group.points.filter(point =>
|
|
|
|
|
String(point.name || '').toLowerCase().includes(keyword) ||
|
|
|
|
|
String(point.code || '').toLowerCase().includes(keyword)
|
|
|
|
|
)
|
|
|
|
|
return matchedPoints.length > 0 ? { ...group, points: matchedPoints } : null
|
|
|
|
|
})
|
|
|
|
|
.filter((group): group is { group: string; points: any[] } => group !== null)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function handlePointSearch() {
|
|
|
|
|
// 搜索时自动展开所有分组
|
|
|
|
|
activePointGroup.value = filteredPointGroups.value.map(g => g.group)
|
|
|
|
|
}
|
|
|
|
|
// 图表相关
|
|
|
|
|
const chartRef = ref()
|
|
|
|
|
const chartInstance = ref<echarts.ECharts>()
|
|
|
|
|
const chartMode = ref('all')
|
|
|
|
|
const pointKeyword = ref('')
|
|
|
|
|
const activePointGroup = ref(['温度类', '工艺类', '能耗类'])
|
|
|
|
|
const selectedPointCodes = ref(['UpperTemp', 'MiddleTemp', 'LowerTemp', 'ReturnAirTemp', 'ActualTemp', 'SetTemp', 'ConveyorSpeed', 'MainTankLevel'])
|
|
|
|
|
const activePointGroup = ref([])
|
|
|
|
|
const selectedPointCodes = ref([])
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
const selectedPoints = computed(() => {
|
|
|
|
|
@ -443,6 +460,7 @@ const displayPoints = computed(() => {
|
|
|
|
|
|
|
|
|
|
// 历史数据
|
|
|
|
|
const historyData = ref([
|
|
|
|
|
/*
|
|
|
|
|
{time:"2026-04-22 08:00",UpperTemp:78.2,MiddleTemp:79.1,LowerTemp:76.8,ReturnAirTemp:68.4,ActualTemp:78.7,SetTemp:80,ConveyorSpeed:14.1,MainTankLevel:612,DryerPower:31.2},
|
|
|
|
|
{time:"2026-04-22 08:12",UpperTemp:78.8,MiddleTemp:80.3,LowerTemp:77.2,ReturnAirTemp:69,ActualTemp:79.4,SetTemp:80,ConveyorSpeed:14.3,MainTankLevel:615,DryerPower:32},
|
|
|
|
|
{time:"2026-04-22 08:24",UpperTemp:79.5,MiddleTemp:81.4,LowerTemp:77.9,ReturnAirTemp:69.5,ActualTemp:80.2,SetTemp:80,ConveyorSpeed:14.5,MainTankLevel:618,DryerPower:32.7},
|
|
|
|
|
@ -461,6 +479,7 @@ const historyData = ref([
|
|
|
|
|
{time:"2026-04-22 11:00",UpperTemp:78.7,MiddleTemp:80.2,LowerTemp:76.9,ReturnAirTemp:69.2,ActualTemp:79.1,SetTemp:80,ConveyorSpeed:16.1,MainTankLevel:611,DryerPower:31.9},
|
|
|
|
|
{time:"2026-04-22 11:12",UpperTemp:78.4,MiddleTemp:79.8,LowerTemp:76.4,ReturnAirTemp:68.7,ActualTemp:78.5,SetTemp:80,ConveyorSpeed:16.4,MainTankLevel:608,DryerPower:31.1},
|
|
|
|
|
{time:"2026-04-22 11:24",UpperTemp:78,MiddleTemp:79.2,LowerTemp:76.1,ReturnAirTemp:68.3,ActualTemp:78.1,SetTemp:80,ConveyorSpeed:16.2,MainTankLevel:605,DryerPower:30.8}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ... 其他数据
|
|
|
|
|
])
|
|
|
|
|
@ -487,7 +506,7 @@ function goBack() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectAllTemperature() {
|
|
|
|
|
const tempPoints = pointGroups.value.find(g => g.group === '温度类')?.points || []
|
|
|
|
|
const tempPoints = pointGroups.value.find(g => g.group === '工艺参数')?.points || []
|
|
|
|
|
selectedPointCodes.value = tempPoints.map(p => p.code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -517,116 +536,321 @@ function getValueClass(value, point) {
|
|
|
|
|
}
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAnalyze() {
|
|
|
|
|
const analyzing = ref(false)
|
|
|
|
|
async function handleAnalyze() {
|
|
|
|
|
if (selectedPointCodes.value.length === 0) {
|
|
|
|
|
ElMessage.warning('请至少选择一个点位')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// 验证时间范围
|
|
|
|
|
if (!validateTimeRange(queryParams.startTime, queryParams.endTime)) {
|
|
|
|
|
ElMessage.warning('时间范围不能超过24小时,且结束时间必须晚于开始时间')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
analyzing.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params: any = {
|
|
|
|
|
deviceId: query.id,
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10
|
|
|
|
|
}
|
|
|
|
|
params.collectionStartTime = queryParams.startTime
|
|
|
|
|
params.collectionEndTime = queryParams.endTime
|
|
|
|
|
if (Array.isArray(selectedPointCodes.value) && selectedPointCodes.value.length) {
|
|
|
|
|
params.attributeCodes = selectedPointCodes.value
|
|
|
|
|
}
|
|
|
|
|
const res: any = await DeviceApi.getHistoryAnalyse(params)
|
|
|
|
|
//oneHistoryData.value = res;
|
|
|
|
|
//historyData.value = res.data;
|
|
|
|
|
resultData.value = res.analyseData;
|
|
|
|
|
// 2. 重要:转换API返回数据的格式,适配图表
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
// 转换数据格式
|
|
|
|
|
transformApiDataToChartFormat(res.data)
|
|
|
|
|
}
|
|
|
|
|
// 模拟异步分析
|
|
|
|
|
//setTimeout(() => {
|
|
|
|
|
updateChart()
|
|
|
|
|
calculateAnalysis()
|
|
|
|
|
|
|
|
|
|
// 模拟分析计算
|
|
|
|
|
calculateAnalysis()
|
|
|
|
|
updateChart()
|
|
|
|
|
ElMessage.success('已重新分析,图表与统计结果已更新')
|
|
|
|
|
ElMessage.success('已重新分析,图表与统计结果已更新')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('分析失败:', error)
|
|
|
|
|
ElMessage.error('分析失败,请检查网络连接或参数设置')
|
|
|
|
|
} finally {
|
|
|
|
|
analyzing.value = false
|
|
|
|
|
}
|
|
|
|
|
//analyzing.value = false
|
|
|
|
|
//}, 800)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 新增函数:转换API返回数据为图表需要的格式
|
|
|
|
|
function transformApiDataToChartFormat(apiData) {
|
|
|
|
|
if (!apiData || apiData.length === 0) {
|
|
|
|
|
oneHistoryData.value = {
|
|
|
|
|
time: [],
|
|
|
|
|
series: []
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
historyData.value = apiData;
|
|
|
|
|
|
|
|
|
|
// 假设API返回的数据格式:[{time: '08:00', point1: 78.2, point2: 79.1, ...}, ...]
|
|
|
|
|
const timeArray = apiData.map(item => item.time || '')
|
|
|
|
|
|
|
|
|
|
// 获取所有点位名称(排除time字段)
|
|
|
|
|
const pointNames = []
|
|
|
|
|
if (apiData.length > 0) {
|
|
|
|
|
const firstItem = apiData[0]
|
|
|
|
|
Object.keys(firstItem).forEach(key => {
|
|
|
|
|
if (key !== 'time' && key !== 'timestamp') {
|
|
|
|
|
pointNames.push(key)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 为每个点位创建数据序列
|
|
|
|
|
const seriesArray = pointNames.map(pointName => {
|
|
|
|
|
const pointData = apiData.map(item => item[pointName] || 0)
|
|
|
|
|
|
|
|
|
|
// 查找对应的点位信息获取颜色
|
|
|
|
|
const pointInfo = pointGroups.value
|
|
|
|
|
.flatMap(g => g.points)
|
|
|
|
|
.find(p => p.code.toLowerCase() === pointName.toLowerCase())
|
|
|
|
|
|
|
|
|
|
// 颜色分配逻辑
|
|
|
|
|
const colors = ['#4888FF', '#00C853', '#FF6B6B', '#FFD93D', '#9C27B0', '#00BCD4', '#FF9800', '#795548']
|
|
|
|
|
const colorIndex = pointNames.indexOf(pointName) % colors.length
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: pointInfo?.name || pointName,
|
|
|
|
|
type: 'line',
|
|
|
|
|
data: pointData,
|
|
|
|
|
color: pointInfo?.color || colors[colorIndex]
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 更新oneHistoryData
|
|
|
|
|
oneHistoryData.value = {
|
|
|
|
|
time: timeArray,
|
|
|
|
|
series: seriesArray
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateAnalysis() {
|
|
|
|
|
async function calculateAnalysis() {
|
|
|
|
|
|
|
|
|
|
/* const params: any = {
|
|
|
|
|
deviceId: query.id,
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10
|
|
|
|
|
}
|
|
|
|
|
params.collectionStartTime = queryParams.startTime
|
|
|
|
|
params.collectionEndTime = queryParams.endTime
|
|
|
|
|
//const keyword = pointKeyword.value.toLowerCase()
|
|
|
|
|
if (Array.isArray(selectedPointCodes.value) && selectedPointCodes.value.length) {
|
|
|
|
|
params.attributeCodes = selectedPointCodes.value
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
const res: any = await DeviceApi.getHistoryAnalyse(params)
|
|
|
|
|
oneHistoryData.value = res;
|
|
|
|
|
historyData.value = res.data;*/
|
|
|
|
|
// 计算时间间隔
|
|
|
|
|
const timeInterval = calculateTimeInterval(queryParams.startTime, queryParams.endTime)
|
|
|
|
|
const intervalText = timeInterval > 0 ? `${timeInterval}小时` : '未设置'
|
|
|
|
|
|
|
|
|
|
// 查找最大和最小波动点位
|
|
|
|
|
let maxRangePoint = null
|
|
|
|
|
let minRangePoint = null
|
|
|
|
|
let maxValuePoint = null
|
|
|
|
|
let minValuePoint = null
|
|
|
|
|
if (resultData.value && resultData.value.length > 0) {
|
|
|
|
|
// 遍历所有点位,找出波动值和数值的极值
|
|
|
|
|
resultData.value.forEach(point => {
|
|
|
|
|
// 波动值比较
|
|
|
|
|
if (maxRangePoint === null || point.range > maxRangePoint.range) {
|
|
|
|
|
maxRangePoint = point
|
|
|
|
|
}
|
|
|
|
|
if (minRangePoint === null || point.range < minRangePoint.range) {
|
|
|
|
|
minRangePoint = point
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 数值比较
|
|
|
|
|
if (maxValuePoint === null || point.max > maxValuePoint.max) {
|
|
|
|
|
maxValuePoint = point
|
|
|
|
|
}
|
|
|
|
|
if (minValuePoint === null || point.min < minValuePoint.min) {
|
|
|
|
|
minValuePoint = point
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// 这里实现分析计算逻辑
|
|
|
|
|
// 模拟计算...
|
|
|
|
|
summaryData.value = [
|
|
|
|
|
{ title: '已选点位数', value: selectedPoints.value.length, desc: '当前参与趋势与统计分析' },
|
|
|
|
|
{ title: '分析时间跨度', value: '3.5h', desc: '2026-04-22 08:00 至 11:30' },
|
|
|
|
|
/* { title: '异常点位数', value: 1, desc: '存在超限或波动异常', warn: true },*/
|
|
|
|
|
{ title: '最大波动点位', value: '中层温度', desc: '波动 8.3℃' },
|
|
|
|
|
{ title: '最高值点位', value: '中层温度', desc: '最高 87.4℃' },
|
|
|
|
|
/* { title: '平均稳定性评分', value: 86, desc: '基于超限次数和波动幅度' }*/
|
|
|
|
|
{title: '已选点位数', value: selectedPoints.value.length, desc: '当前参与趋势与统计分析'},
|
|
|
|
|
{title: '分析时间跨度', value: intervalText, desc: queryParams.startTime && queryParams.endTime
|
|
|
|
|
? `${queryParams.startTime.substring(0, 16)} 至 ${queryParams.endTime.substring(0, 16)}`
|
|
|
|
|
: '未设置时间范围' },
|
|
|
|
|
/* { title: '异常点位数', value: 1, desc: '存在超限或波动异常', warn: true },*/
|
|
|
|
|
/*{title: '最大波动点位', value: '中层温度', desc: '波动 8.3℃'},*/
|
|
|
|
|
{
|
|
|
|
|
title: '最大波动点位',
|
|
|
|
|
value: maxRangePoint ? maxRangePoint.name : '-',
|
|
|
|
|
desc: maxRangePoint ? `波动 ${formatValue(maxRangePoint.range, maxRangePoint.unit)}` : '-',
|
|
|
|
|
warn: maxRangePoint && maxRangePoint.range > 0
|
|
|
|
|
},
|
|
|
|
|
/*{title: '最小波动点位', value: '中层温度', desc: '波动 8.3℃'},*/
|
|
|
|
|
{
|
|
|
|
|
title: '最小波动点位',
|
|
|
|
|
value: minRangePoint ? minRangePoint.name : '-',
|
|
|
|
|
desc: minRangePoint ? `波动 ${formatValue(minRangePoint.range, minRangePoint.unit)}` : '-'
|
|
|
|
|
},
|
|
|
|
|
/*{title: '最高值点位', value: '中层温度', desc: '最高 87.4℃'},
|
|
|
|
|
/!* { title: '平均稳定性评分', value: 86, desc: '基于超限次数和波动幅度' }*!/*/
|
|
|
|
|
{
|
|
|
|
|
title: '最高值点位',
|
|
|
|
|
value: maxValuePoint ? maxValuePoint.name : '-',
|
|
|
|
|
desc: maxValuePoint ? `最高 ${formatValue(maxValuePoint.max, maxValuePoint.unit)}` : '-',
|
|
|
|
|
warn: maxValuePoint && maxValuePoint.max > 85 // 示例:超过85显示警告
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
healthScore.value = 86
|
|
|
|
|
suggestions.value = [
|
|
|
|
|
{ content: '当前设备温度整体波动处于可控范围,建议保持现有采集频率。', type: 'info' },
|
|
|
|
|
{ content: '中层温度存在短时偏高,建议检查热风分布与风门开度。', type: 'warning' },
|
|
|
|
|
{ content: '实际温度与设定温度偏差较大,建议复核 PID 参数或温控探头校准状态。', type: 'error' },
|
|
|
|
|
{ content: '建议重点关注回风温度与输送速度联动关系,用于判断干燥效率变化。', type: 'warning' }
|
|
|
|
|
{content: '当前设备温度整体波动处于可控范围,建议保持现有采集频率。', type: 'info'},
|
|
|
|
|
{content: '中层温度存在短时偏高,建议检查热风分布与风门开度。', type: 'warning'},
|
|
|
|
|
{content: '实际温度与设定温度偏差较大,建议复核 PID 参数或温控探头校准状态。', type: 'error'},
|
|
|
|
|
{content: '建议重点关注回风温度与输送速度联动关系,用于判断干燥效率变化。', type: 'warning'}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 图表实例
|
|
|
|
|
let oneChartInstance = null
|
|
|
|
|
// 模拟历史数据(实际项目中从后端接口获取)
|
|
|
|
|
const oneHistoryData = ref({})
|
|
|
|
|
function updateChart() {
|
|
|
|
|
if (!chartRef.value) return
|
|
|
|
|
|
|
|
|
|
if (!chartInstance.value) {
|
|
|
|
|
chartInstance.value = echarts.init(chartRef.value)
|
|
|
|
|
const chartDom = document.getElementById('historyTrendChart')
|
|
|
|
|
// 1. 如果图表实例已存在,先销毁
|
|
|
|
|
if (oneChartInstance) {
|
|
|
|
|
oneChartInstance.dispose()
|
|
|
|
|
}
|
|
|
|
|
// 3. 检查是否有数据
|
|
|
|
|
if (!oneHistoryData.value || !oneHistoryData.value.series || oneHistoryData.value.series.length === 0) {
|
|
|
|
|
// 显示无数据提示
|
|
|
|
|
/* oneChartInstance.setOption({
|
|
|
|
|
title: {
|
|
|
|
|
text: '暂无数据',
|
|
|
|
|
left: 'center',
|
|
|
|
|
top: 'center',
|
|
|
|
|
textStyle: { color: '#999', fontSize: 16 }
|
|
|
|
|
},
|
|
|
|
|
xAxis: { show: false },
|
|
|
|
|
yAxis: { show: false },
|
|
|
|
|
series: []
|
|
|
|
|
})*/
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
oneChartInstance = echarts.init(chartDom)
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
color: ['#1f6feb', '#13a8a8', '#7c3aed', '#f59e0b', '#ef4444', '#10b981', '#6366f1', '#8b5cf6', '#64748b'],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
backgroundColor: 'rgba(17, 24, 39, 0.92)',
|
|
|
|
|
borderWidth: 0,
|
|
|
|
|
textStyle: { color: '#fff' }
|
|
|
|
|
trigger: 'axis', // 触发类型:坐标轴触发(支持多系列)
|
|
|
|
|
axisPointer: { type: 'cross' }, // 十字准星指示器
|
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.7)', // 深色背景
|
|
|
|
|
textStyle: { color: '#fff' }, // 文字白色
|
|
|
|
|
formatter: (params) => {
|
|
|
|
|
// 自定义 tooltip 内容,模仿截图样式
|
|
|
|
|
let html = `<div style="font-weight: bold; margin-bottom: 8px;">${params[0].name}</div>`
|
|
|
|
|
params.forEach(item => {
|
|
|
|
|
html += `
|
|
|
|
|
<div style="display: flex; align-items: center; margin: 4px 0;">
|
|
|
|
|
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: ${item.color}; margin-right: 6px;"></span>
|
|
|
|
|
<span style="flex: 1;">${item.seriesName}</span>
|
|
|
|
|
<span style="margin-left: 10px;">${item.value}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
return html
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
top: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
textStyle: { color: '#475467' }
|
|
|
|
|
},
|
|
|
|
|
grid: {
|
|
|
|
|
left: 42,
|
|
|
|
|
right: 28,
|
|
|
|
|
top: 58,
|
|
|
|
|
bottom: 42,
|
|
|
|
|
containLabel: true
|
|
|
|
|
data: oneHistoryData.value.series.map(s => s.name),
|
|
|
|
|
top: 30,
|
|
|
|
|
right: 20,
|
|
|
|
|
textStyle: { fontSize: 12 }
|
|
|
|
|
},
|
|
|
|
|
grid: { left: '3%', right: '4%', bottom: '3%', top: '60px', containLabel: true },
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
data: historyData.value.map(r => r.time.split(' ')[1]),
|
|
|
|
|
axisLine: { lineStyle: { color: '#d9e2ef' } },
|
|
|
|
|
axisTick: { show: false },
|
|
|
|
|
axisLabel: { color: '#667085' }
|
|
|
|
|
data: oneHistoryData.value.time,
|
|
|
|
|
axisLine: { lineStyle: { color: '#ccc' } },
|
|
|
|
|
axisLabel: { color: '#666' }
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
scale: true,
|
|
|
|
|
splitLine: { lineStyle: { color: '#edf1f6' } },
|
|
|
|
|
axisLabel: { color: '#667085' }
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
axisTick: { show: false },
|
|
|
|
|
splitLine: { lineStyle: { color: '#eee' } },
|
|
|
|
|
axisLabel: { color: '#666' }
|
|
|
|
|
},
|
|
|
|
|
dataZoom: [
|
|
|
|
|
{ type: 'inside', start: 0, end: 100 },
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
height: 20,
|
|
|
|
|
bottom: 6,
|
|
|
|
|
borderColor: '#dce3ee',
|
|
|
|
|
fillerColor: 'rgba(31, 111, 235, 0.14)'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
series: selectedPoints.value.map(point => ({
|
|
|
|
|
name: point.name,
|
|
|
|
|
series: oneHistoryData.value.series.map(s => ({
|
|
|
|
|
name: s.name,
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
symbol: 'circle',
|
|
|
|
|
symbolSize: 6,
|
|
|
|
|
showSymbol: false,
|
|
|
|
|
emphasis: { focus: 'series' },
|
|
|
|
|
lineStyle: { width: 2 },
|
|
|
|
|
data: historyData.value.map(r => r[point.code])
|
|
|
|
|
data: s.data,
|
|
|
|
|
smooth: true, // 平滑曲线
|
|
|
|
|
symbol: 'circle', // 标记点样式
|
|
|
|
|
symbolSize: 6, // 标记点大小
|
|
|
|
|
itemStyle: { color: s.color },
|
|
|
|
|
lineStyle: { color: s.color, width: 2 },
|
|
|
|
|
emphasis: { // 高亮时的样式
|
|
|
|
|
itemStyle: { color: s.color, borderColor: '#fff', borderWidth: 2 },
|
|
|
|
|
lineStyle: { width: 3 }
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chartInstance.value.setOption(option)
|
|
|
|
|
oneChartInstance.setOption(option)
|
|
|
|
|
|
|
|
|
|
// 窗口resize时自适应
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
oneChartInstance && oneChartInstance.resize()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetQuery() {
|
|
|
|
|
selectedPointCodes.value = ['UpperTemp', 'MiddleTemp', 'LowerTemp', 'ReturnAirTemp', 'ActualTemp', 'SetTemp', 'ConveyorSpeed', 'MainTankLevel']
|
|
|
|
|
selectedPointCodes.value = []
|
|
|
|
|
pointKeyword.value = ''
|
|
|
|
|
chartMode.value = 'all'
|
|
|
|
|
handleAnalyze()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSaveAnalysis() {
|
|
|
|
|
ElMessage.success('分析方案已保存')
|
|
|
|
|
const formLoading = ref(false)
|
|
|
|
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
|
|
|
|
const message = useMessage() // 消息弹窗
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
async function handleSaveAnalysis() {
|
|
|
|
|
// 提交请求
|
|
|
|
|
formLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const contactInfo={
|
|
|
|
|
startTime: queryParams.startTime,
|
|
|
|
|
endTime: queryParams.endTime,
|
|
|
|
|
codes: selectedPointCodes.value
|
|
|
|
|
}
|
|
|
|
|
const jsonString = JSON.stringify(contactInfo);
|
|
|
|
|
const data: any = {
|
|
|
|
|
id: route.query.id,
|
|
|
|
|
deviceCode:route.query.deviceCode,
|
|
|
|
|
deviceName:route.query.name,
|
|
|
|
|
isEnable: true,
|
|
|
|
|
contactInfo:jsonString.valueOf()
|
|
|
|
|
}
|
|
|
|
|
await DeviceApi.updateDevice(data)
|
|
|
|
|
message.success(t('common.updateSuccess'))
|
|
|
|
|
// 发送操作成功的事件
|
|
|
|
|
emit('success')
|
|
|
|
|
} finally {
|
|
|
|
|
formLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
//ElMessage.success('分析方案已保存')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleExport() {
|
|
|
|
|
@ -645,11 +869,94 @@ const getDiviceList = async () => {
|
|
|
|
|
pointGroups.value = data;
|
|
|
|
|
activePointGroup.value = data[0]?.typeNames;
|
|
|
|
|
selectedPointCodes.value = data[0]?.codes;
|
|
|
|
|
let deviceDO = data[0]?.deviceDO;
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证时间范围是否在1天内
|
|
|
|
|
function validateTimeRange(startTime, endTime) {
|
|
|
|
|
if (!startTime || !endTime) return true // 允许为空
|
|
|
|
|
|
|
|
|
|
const start = new Date(startTime)
|
|
|
|
|
const end = new Date(endTime)
|
|
|
|
|
const diffInHours = (end - start) / (1000 * 60 * 60)
|
|
|
|
|
|
|
|
|
|
// 检查是否超过24小时
|
|
|
|
|
if (diffInHours > 24) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查结束时间是否晚于开始时间
|
|
|
|
|
if (end < start) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算时间间隔(小时)
|
|
|
|
|
function calculateTimeInterval(startTime, endTime) {
|
|
|
|
|
if (!startTime || !endTime) return 0
|
|
|
|
|
|
|
|
|
|
const start = new Date(startTime)
|
|
|
|
|
const end = new Date(endTime)
|
|
|
|
|
const diffInMilliseconds = Math.abs(end - start)
|
|
|
|
|
const diffInHours = diffInMilliseconds / (1000 * 60 * 60)
|
|
|
|
|
|
|
|
|
|
return parseFloat(diffInHours.toFixed(1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化日期时间
|
|
|
|
|
function formatDateTime(date) {
|
|
|
|
|
const year = date.getFullYear()
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
|
|
|
const hours = String(date.getHours()).padStart(2, '0')
|
|
|
|
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
|
|
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始时间变化处理
|
|
|
|
|
function handleStartTimeChange(value) {
|
|
|
|
|
if (value && queryParams.endTime) {
|
|
|
|
|
if (!validateTimeRange(value, queryParams.endTime)) {
|
|
|
|
|
const start = new Date(value)
|
|
|
|
|
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000 - 1000) // 23:59:59
|
|
|
|
|
queryParams.endTime = formatDateTime(end)
|
|
|
|
|
ElMessage.warning('时间范围超过24小时,已自动调整结束时间')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 结束时间变化处理
|
|
|
|
|
function handleEndTimeChange(value) {
|
|
|
|
|
if (queryParams.startTime && value) {
|
|
|
|
|
if (!validateTimeRange(queryParams.startTime, value)) {
|
|
|
|
|
const end = new Date(value)
|
|
|
|
|
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000 + 1000) // 00:00:00
|
|
|
|
|
queryParams.startTime = formatDateTime(start)
|
|
|
|
|
ElMessage.warning('时间范围超过24小时,已自动调整开始时间')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 结束时间选择器选项
|
|
|
|
|
const endTimeOptions = computed(() => {
|
|
|
|
|
if (!queryParams.startTime) return {}
|
|
|
|
|
|
|
|
|
|
const start = new Date(queryParams.startTime)
|
|
|
|
|
const maxEndTime = new Date(start.getTime() + 24 * 60 * 60 * 1000 - 1000) // 23:59:59
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
disabledDate: (date) => {
|
|
|
|
|
return date.getTime() < start.getTime() || date.getTime() > maxEndTime.getTime()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 生命周期
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
getDiviceList()
|
|
|
|
|
@ -921,4 +1228,5 @@ window.addEventListener('resize', () => {
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|