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.

1334 lines
43 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>
<div class="app-container">
<!-- 顶部设备信息栏 -->
<el-card class="top-card" shadow="never">
<el-page-header :content="pageTitle" @back="goBack" />
<el-descriptions class="device-info" :column="5" border>
<el-descriptions-item label="设备名称">{{ deviceData.name }}</el-descriptions-item>
<el-descriptions-item label="设备编码">{{ deviceData.code }}</el-descriptions-item>
<!-- <el-descriptions-item label="设备类型">{{ deviceData.type }}</el-descriptions-item>-->
<el-descriptions-item label="所属产线">{{ deviceData.productionLine }}</el-descriptions-item>
<el-descriptions-item label="最新采集时间">{{ deviceData.lastCollectTime }}</el-descriptions-item>
</el-descriptions>
<div class="action-buttons">
<el-button type="primary" @click="handleSaveAnalysis">
<Icon icon="ep:document-checked" />
保存分析方案
</el-button>
<!-- <el-button @click="handleExport">
<Icon icon="ep:download" />
导出分析结果
</el-button>-->
<el-button @click="handleRefresh">
<Icon icon="ep:refresh" />
刷新
</el-button>
</div>
</el-card>
<!-- 分析条件 -->
<el-card class="filter-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">分析条件</span>
</div>
</template>
<!-- <el-row :gutter="20">-->
<!-- <el-col :span="6">
<el-card class="device-info-card">
<h3>当前设备</h3>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="设备名称">{{ deviceData.name }}</el-descriptions-item>
<el-descriptions-item label="设备编码">{{ deviceData.code }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ deviceData.type }}</el-descriptions-item>
<el-descriptions-item label="所属产线">{{ deviceData.productionLine }}</el-descriptions-item>
<el-descriptions-item label="采集状态">
<el-tag type="success" size="small">在线</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>-->
<!-- <el-col :span="18">-->
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="开始时间">
<el-date-picker
v-model="queryParams.startTime"
type="datetime"
placeholder="选择开始时间"
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>
<el-col :span="6">
<el-form-item label="结束时间">
<el-date-picker
v-model="queryParams.endTime"
type="datetime"
placeholder="选择结束时间"
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>
<el-col :span="6">
<!-- <el-form-item label="时间粒度">
<el-radio-group v-model="queryParams.granularity">
<el-radio-button label="original">原始值</el-radio-button>
<el-radio-button label="1min">1分钟</el-radio-button>
<el-radio-button label="5min">5分钟</el-radio-button>
<el-radio-button label="1hour">1小时</el-radio-button>
</el-radio-group>
</el-form-item>-->
</el-col>
</el-row>
<el-form-item label="点位选择">
<div class="point-selector-card">
<div class="selector-header">
<el-input
v-model="pointKeyword"
placeholder="搜索点位名称或编码"
clearable
style="width: 200px; margin-right: 10px"
@input="handlePointSearch"
/>
<el-button @click="selectAllTemperature">选择工艺参数</el-button>
<el-button @click="clearAllPoints">清空</el-button>
</div>
<div class="selected-tags">
<el-tag
v-for="point in selectedPoints"
:key="point.code"
closable
@close="removePoint(point.code)"
>
{{ point.name }}
</el-tag>
<span v-if="selectedPoints.length === 0" style="color: #98a2b3">
未选择点位
</span>
</div>
<div class="point-list">
<el-collapse v-model="activePointGroup">
<el-collapse-item
v-for="group in filteredPointGroups"
:key="group.group"
:title="`${group.group} (${group.points.length})`"
:name="group.group"
>
<el-checkbox-group v-model="selectedPointCodes">
<el-space wrap>
<el-checkbox
v-for="point in group.points"
:key="point.code"
:label="point.code"
>
<div class="point-info">
<div class="point-name">{{ point.name }}</div>
<div class="point-detail">
<span class="point-code">{{ point.code }}</span>
<span class="point-unit">{{ point.unit }}</span>
<span class="point-range">({{ point.lower }} ~ {{ point.upper }})</span>
</div>
</div>
</el-checkbox>
</el-space>
</el-checkbox-group>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-form-item>
<!-- <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>
&lt;!&ndash; <el-checkbox label="upperCount">超上限次数</el-checkbox>
<el-checkbox label="lowerCount">超下限次数</el-checkbox>
<el-checkbox label="continuousAbnormal">连续异常时长</el-checkbox>&ndash;&gt;
</el-checkbox-group>
</el-form-item>-->
<el-form-item>
<el-button type="primary" @click="handleAnalyze">
<Icon icon="ep:search" />
开始分析
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" />
重置条件
</el-button>
</el-form-item>
</el-form>
<!-- </el-col>
</el-row>-->
</el-card>
<!-- 概览卡片 -->
<el-row :gutter="20" class="summary-row">
<el-col v-for="item in summaryData" :key="item.title" :span="4">
<el-card :class="['summary-card', item.warn ? 'summary-warn' : '']">
<div class="summary-title">{{ item.title }}</div>
<div class="summary-value">{{ item.value }}</div>
<div class="summary-desc">{{ item.desc }}</div>
</el-card>
</el-col>
</el-row>
<!-- 多点位历史趋势分析 -->
<el-card class="chart-card" shadow="never">
<template #header>
<div class="card-header">
<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-group>
</div>
</div>
</template>
<div id="historyTrendChart" class="chart-container"></div>
<!-- <el-descriptions :column="4" border class="chart-note">
<el-descriptions-item label="当前展示点位">
{{ chartNote.currentPoints }}
</el-descriptions-item>
<el-descriptions-item label="是否有异常">
<el-tag :type="chartNote.hasAbnormal ? 'warning' : 'success'">
{{ chartNote.hasAbnormal ? `发现 ${chartNote.abnormalCount} 个异常点位` : '未发现明显异常' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最高值出现时间">
{{ chartNote.maxTime }}
</el-descriptions-item>
<el-descriptions-item label="最大波动区间">
{{ chartNote.maxRange }}
</el-descriptions-item>
</el-descriptions>-->
</el-card>
<div class="table-container">
<!-- 分析结果表格 -->
<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>
<span style="color: #667085; font-size: 13px">
按点位聚合统计,异常结果已高亮
</span>
</div>
</template>
<el-table :data="resultData" style="width: 100%">
<el-table-column prop="name" label="点位名称" width="120" />
<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="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="110">
<template #default="{ row }">
{{ formatValue(row.avg, row.unit) }}
</template>
</el-table-column>
<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">
<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">
<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">
<template #default="{ row }">
{{ row.continuousAbnormal }}分钟
</template>
</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>
</el-card>
<!-- 历史明细 -->
<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>
<span style="color: #667085; font-size: 13px">
展示部分原始采集数据
</span>
</div>
</template>
<el-table :data="historyData" style="width: 100%" height="360">
<el-table-column prop="time" label="采集时间" width="150" />
<el-table-column
v-for="point in displayPoints"
:key="point.code.toLowerCase()"
:prop="point.code.toLowerCase()"
:label="point.name"
:min-width="100"
>
<template #default="{ row }">
<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">
<template #header>
<div class="card-header">
<span class="card-title">分析说明/建议</span>
</div>
</template>
<div class="health-score">
<div class="score-ring">
<strong>{{ healthScore }}</strong>
<span>稳定性评分</span>
</div>
</div>
<div class="suggest-list">
<el-alert
v-for="(suggest, index) in suggestions"
:key="index"
:title="suggest.content"
:type="suggest.type"
:closable="false"
show-icon
class="suggest-item"
/>
</div>
<div class="event-list">
<h4>关键事件记录</h4>
<el-timeline>
<el-timeline-item
v-for="event in keyEvents"
:key="event.time"
:timestamp="event.time"
>
{{ event.text }}
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</el-aside>-->
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import {useRoute, useRouter} from 'vue-router'
import * as echarts from 'echarts'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useTagsViewStore } from '@/store/modules/tagsView'
import {DeviceApi, DeviceVO} from "@/api/iot/device";
const router = useRouter()
const route = useRoute();
let query = route.query;
// 页面数据
const pageTitle = '单设备多点位历史数据分析'
// 设备信息
const deviceData = reactive({
name: query.name as string,
code: query.deviceCode as string,
productionLine: query.lineName as string,
lastCollectTime: query.collectionTime as string,
})
// 查询参数
const queryParams = reactive({
startTime: '2026-04-22 08:00:00',
endTime: '2026-04-22 11:30:00',
granularity: 'original',
rules: ['max', 'min', 'avg', 'range', 'upperCount', 'lowerCount', 'continuousAbnormal']
})
// 点位数据
const pointGroups = ref([
{
group: '温度类',
points: [
{ name: '上层温度', code: 'UpperTemp', unit: '℃', upper: 86, lower: 72 },
{ name: '中层温度', code: 'MiddleTemp', unit: '℃', upper: 86, lower: 72 },
{ name: '下层温度', code: 'LowerTemp', unit: '℃', upper: 84, lower: 70 },
{ name: '回风温度', code: 'ReturnAirTemp', unit: '℃', upper: 78, lower: 62 },
{ name: '实际温度', code: 'ActualTemp', unit: '℃', upper: 86, lower: 72 },
]
},
{
group: '工艺类',
points: [
{ name: '输送速度', code: 'ConveyorSpeed', unit: 'm/min', upper: 18, lower: 10 },
{ name: '主缸水位', code: 'MainTankLevel', unit: 'mm', upper: 680, lower: 520 }
]
},
{
group: '能耗类',
points: [
{ name: '干燥柜用电量', code: 'DryerPower', unit: 'kWh', upper: 48, lower: 20 }
]
}
])
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([])
// 计算属性
const selectedPoints = computed(() => {
const allPoints = pointGroups.value.flatMap(g => g.points)
return allPoints.filter(p => selectedPointCodes.value.includes(p.code))
})
const displayPoints = computed(() => {
return selectedPoints.value
})
// 历史数据
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},
{time:"2026-04-22 08:36",UpperTemp:80.1,MiddleTemp:82.6,LowerTemp:78.4,ReturnAirTemp:70.3,ActualTemp:81,SetTemp:80,ConveyorSpeed:14.2,MainTankLevel:621,DryerPower:33.3},
{time:"2026-04-22 08:48",UpperTemp:80.8,MiddleTemp:83.2,LowerTemp:79,ReturnAirTemp:70.8,ActualTemp:81.8,SetTemp:80,ConveyorSpeed:13.8,MainTankLevel:624,DryerPower:34.1},
{time:"2026-04-22 09:00",UpperTemp:81.4,MiddleTemp:84.5,LowerTemp:79.7,ReturnAirTemp:71.6,ActualTemp:82.5,SetTemp:80,ConveyorSpeed:13.4,MainTankLevel:628,DryerPower:35.8},
{time:"2026-04-22 09:12",UpperTemp:82.1,MiddleTemp:85.8,LowerTemp:80.2,ReturnAirTemp:72.4,ActualTemp:83.2,SetTemp:80,ConveyorSpeed:13.2,MainTankLevel:631,DryerPower:37.4},
{time:"2026-04-22 09:24",UpperTemp:82.8,MiddleTemp:87.4,LowerTemp:80.8,ReturnAirTemp:73.2,ActualTemp:84.7,SetTemp:80,ConveyorSpeed:12.8,MainTankLevel:636,DryerPower:39.1},
{time:"2026-04-22 09:36",UpperTemp:83,MiddleTemp:86.9,LowerTemp:81.2,ReturnAirTemp:74.1,ActualTemp:85.6,SetTemp:80,ConveyorSpeed:12.5,MainTankLevel:640,DryerPower:40.5},
{time:"2026-04-22 09:48",UpperTemp:82.4,MiddleTemp:85.6,LowerTemp:80.5,ReturnAirTemp:73.6,ActualTemp:84.9,SetTemp:80,ConveyorSpeed:12.9,MainTankLevel:637,DryerPower:39.3},
{time:"2026-04-22 10:00",UpperTemp:81.7,MiddleTemp:84.4,LowerTemp:79.8,ReturnAirTemp:72.8,ActualTemp:83.8,SetTemp:80,ConveyorSpeed:13.5,MainTankLevel:633,DryerPower:37.8},
{time:"2026-04-22 10:12",UpperTemp:80.9,MiddleTemp:83.5,LowerTemp:79.1,ReturnAirTemp:72,ActualTemp:82.4,SetTemp:80,ConveyorSpeed:14,MainTankLevel:629,DryerPower:36.2},
{time:"2026-04-22 10:24",UpperTemp:80.2,MiddleTemp:82.6,LowerTemp:78.3,ReturnAirTemp:71.4,ActualTemp:81.3,SetTemp:80,ConveyorSpeed:14.6,MainTankLevel:625,DryerPower:34.9},
{time:"2026-04-22 10:36",UpperTemp:79.8,MiddleTemp:81.8,LowerTemp:77.8,ReturnAirTemp:70.7,ActualTemp:80.6,SetTemp:80,ConveyorSpeed:15.2,MainTankLevel:620,DryerPower:33.5},
{time:"2026-04-22 10:48",UpperTemp:79.3,MiddleTemp:81,LowerTemp:77.4,ReturnAirTemp:70,ActualTemp:79.8,SetTemp:80,ConveyorSpeed:15.7,MainTankLevel:616,DryerPower:32.6},
{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}
*/
// ... 其他数据
])
// 分析结果
const resultData = ref([])
const summaryData = ref([])
const chartNote = ref({})
const healthScore = ref(86)
const suggestions = ref([])
const keyEvents = ref([
{ time: '09:26', text: '中层温度出现短时偏高' },
{ time: '10:14', text: '实际温度与设定温度偏差扩大' },
{ time: '10:58', text: '回风温度与输送速度联动变化明显' }
])
const tagsViewStore = useTagsViewStore()
// 方法
function goBack() {
tagsViewStore.delView(router.currentRoute.value)
router.push('/iot/historyData')
}
function selectAllTemperature() {
const tempPoints = pointGroups.value.find(g => g.group === '工艺参数')?.points || []
selectedPointCodes.value = tempPoints.map(p => p.code)
}
function clearAllPoints() {
selectedPointCodes.value = []
}
function removePoint(code) {
selectedPointCodes.value = selectedPointCodes.value.filter(c => c !== code)
}
function formatValue(value, unit) {
if (unit === 'mm') {
return Math.round(value)
} else if (unit === 'kWh') {
return value.toFixed(1)
} else {
return Number(value).toFixed(1)
}
}
function getValueClass(value, point) {
if (value > point.upper) {
return 'text-danger'
} else if (value < point.lower) {
return 'text-warning'
}
return ''
}
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;
if (res && res.analyseData) {
resultData.value = res.analyseData;
// 2. 重要转换API返回数据的格式适配图表
if (res && res.data) {
// 转换数据格式
transformApiDataToChartFormat(res.data)
}
// 模拟异步分析
//setTimeout(() => {
updateChart()
calculateAnalysis()
ElMessage.success('已重新分析,图表与统计结果已更新')
} else {
// 如果接口返回空数据
ElMessage.warning('未查询到相关数据')
initializeEmptyCharts()
}
} catch (error) {
console.error('分析失败:', error)
ElMessage.error('分析失败,请检查网络连接或参数设置')
// 出错时显示空状态
initializeEmptyCharts()
} finally {
analyzing.value = false
}
//analyzing.value = false
//}, 800)
}
// 新增函数转换API返回数据为图表需要的格式
function transformApiDataToChartFormat(apiData) {
if (!apiData || apiData.length === 0) {
historyData.value = []
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
}
}
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: 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'}
]
}
// 图表实例
let oneChartInstance = null
// 模拟历史数据(实际项目中从后端接口获取)
const oneHistoryData = ref({})
function updateChart() {
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 = {
tooltip: {
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: {
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: oneHistoryData.value.time,
axisLine: { lineStyle: { color: '#ccc' } },
axisLabel: { color: '#666' }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#eee' } },
axisLabel: { color: '#666' }
},
series: oneHistoryData.value.series.map(s => ({
name: s.name,
type: 'line',
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 }
}
}))
}
oneChartInstance.setOption(option)
// 窗口resize时自适应
window.addEventListener('resize', () => {
oneChartInstance && oneChartInstance.resize()
})
}
function resetQuery() {
selectedPointCodes.value = []
pointKeyword.value = ''
chartMode.value = 'all'
}
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() {
ElMessage.success('分析结果已生成导出任务')
}
function handleRefresh() {
handleAnalyze()
}
const loading = ref(true) // 列表的加载中
/** 查询列表 */
const getDiviceList = async () => {
loading.value = true
try {
const data = await DeviceApi.getDeviceAttributeGroupList({deviceId: route.query.id})
pointGroups.value = data;
activePointGroup.value = data[0]?.typeNames;
let deviceDO = data[0]?.deviceDO;
if (deviceDO && deviceDO.contactInfo) {
selectedPointCodes.value = data[0]?.codes;
const contactInfo = JSON.parse(deviceDO.contactInfo)
// 设置查询参数
if (contactInfo.startTime) {
queryParams.startTime = contactInfo.startTime
}
if (contactInfo.endTime) {
queryParams.endTime = contactInfo.endTime
}
if (contactInfo.codes && Array.isArray(contactInfo.codes)) {
selectedPointCodes.value = contactInfo.codes
await handleAnalyze() // 自动调用分析
}
}
} 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 pageLoaded = ref(false)
// 结束时间选择器选项
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()
}
}
})
// 新增:初始化空图表
function initializeEmptyCharts() {
const chartDom = document.getElementById('historyTrendChart')
if (chartDom && !oneChartInstance) {
oneChartInstance = echarts.init(chartDom)
}
// 显示无数据提示
if (oneChartInstance) {
oneChartInstance.setOption({
title: {
text: '请选择分析条件后点击"开始分析"',
left: 'center',
top: 'center',
textStyle: {
color: '#909399',
fontSize: 16,
fontWeight: 'normal'
}
},
xAxis: { show: false },
yAxis: { show: false },
series: []
})
}
// 清空表格数据
resultData.value = []
historyData.value = []
summaryData.value = [
{
title: '已选点位数',
value: 0,
desc: '请先选择点位'
},
{
title: '分析时间跨度',
value: '未设置',
desc: '请设置开始和结束时间'
},
{
title: '最大波动点位',
value: '-',
desc: '无数据'
},
{
title: '最小波动点位',
value: '-',
desc: '无数据'
},
{
title: '最高值点位',
value: '-',
desc: '无数据'
}
]
}
// 生命周期
onMounted(async () => {
getDiviceList()
// 2. 检查是否有保存的查询参数
const hasContactInfo = selectedPointCodes.value && selectedPointCodes.value.length > 0
if (hasContactInfo) {
// 3. 如果有保存的参数,自动调用接口
console.log('检测到保存的分析条件,自动查询...')
await handleAnalyze() // 自动调用分析
} else {
// 4. 如果没有保存的参数,只初始化图表(显示无数据状态)
console.log('无保存的分析条件,显示空状态')
initializeEmptyCharts()
calculateAnalysis() // 计算空的统计结果
}
// 设置页面加载完成标志
pageLoaded.value = true
/* calculateAnalysis()
nextTick(() => {
updateChart()
})*/
})
// 监听窗口变化
window.addEventListener('resize', () => {
if (chartInstance.value) {
chartInstance.value.resize()
}
})
</script>
<style scoped lang="scss">
.app-container {
min-width: 1180px;
}
.top-card {
margin-bottom: 10px;
background: linear-gradient(180deg, #fff 0%, #f9fbff 100%);
border: 1px solid var(--el-border-color);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
.device-info {
margin-top: 20px;
}
.action-buttons {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
}
.filter-card {
margin-bottom: 10px;
.card-header {
display: flex;
align-items: center;
gap: 9px;
font-size: 16px;
font-weight: 700;
color: #1d2939;
&::before {
content: '';
width: 4px;
height: 16px;
border-radius: 2px;
background: var(--el-color-primary);
}
}
.device-info-card {
height: 100%;
padding: 14px;
background: #f8fbff;
border: 1px solid #e0e9f6;
border-radius: 8px;
h3 {
margin: 0 0 12px;
font-size: 15px;
}
}
.point-selector-card {
border: 1px solid var(--el-border-color);
border-radius: 8px;
background: #fff;
overflow: hidden;
width: 100%;
.selector-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid var(--el-border-color-lighter);
background: #fbfcfe;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 30px;
padding: 10px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.point-list {
max-height: 230px;
overflow: auto;
padding: 12px;
}
}
}
.summary-row {
margin-bottom: 10px;
.summary-card {
padding: 16px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
transition: all 0.18s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
&.summary-warn {
border-color: #ffd89b;
background: linear-gradient(180deg, #fff 0%, #fff9ef 100%);
}
.summary-title {
color: var(--el-text-color-secondary);
font-size: 13px;
font-weight: 600;
}
.summary-value {
margin-top: 9px;
color: #142033;
font-size: 25px;
font-weight: 800;
line-height: 1.1;
}
.summary-desc {
margin-top: 8px;
color: var(--el-text-color-placeholder);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.chart-card {
margin-bottom: 10px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-container {
width: 100%;
height: 430px;
}
.chart-note {
margin-top: 20px;
}
}
.result-card, .detail-card {
margin-bottom: 10px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.suggest-aside {
position: sticky;
top: 20px;
height: fit-content;
.suggest-card {
.health-score {
display: flex;
align-items: center;
justify-content: center;
height: 148px;
margin-bottom: 16px;
background: radial-gradient(circle at center, #fff 0%, #eef6ff 100%);
border: 1px solid #dce9fb;
border-radius: 8px;
.score-ring {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 112px;
height: 112px;
border-radius: 50%;
background: radial-gradient(circle at center, #fff 58%, transparent 59%),
conic-gradient(var(--el-color-primary) 0 86%, #dbe7f6 86% 100%);
color: #15375f;
strong {
font-size: 30px;
line-height: 1;
}
span {
margin-top: 5px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
}
.suggest-list {
display: grid;
gap: 10px;
margin-bottom: 10px;
.suggest-item {
padding: 12px;
border-left: 4px solid var(--el-color-primary);
border-radius: 7px;
background: #fbfdff;
font-size: 13px;
line-height: 1.55;
&.el-alert--warning {
border-left-color: var(--el-color-warning);
background: #fffaf0;
}
&.el-alert--error {
border-left-color: var(--el-color-error);
background: #fff5f5;
}
}
}
.event-list {
h4 {
margin: 0 0 15px;
font-size: 14px;
font-weight: 600;
}
}
}
}
.text-danger {
color: var(--el-color-danger);
font-weight: 700;
background: var(--el-color-danger-light-9);
padding: 2px 4px;
border-radius: 4px;
}
.text-warning {
color: var(--el-color-warning);
font-weight: 700;
background: var(--el-color-warning-light-9);
padding: 2px 4px;
border-radius: 4px;
}
</style>