|
|
@@ -0,0 +1,330 @@
|
|
|
+<template>
|
|
|
+ <ContentWrap v-loading="formLoading">
|
|
|
+ <ContentWrap>
|
|
|
+ <el-form style="height: 89px; margin-left: 20px">
|
|
|
+ <el-row style="display: flex; flex-direction: row">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item prop="deviceCode">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">资产编码:</span>
|
|
|
+ </template>
|
|
|
+ <span class="custom-label">{{ formData.deviceCode }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item prop="deviceName">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">设备类别:</span>
|
|
|
+ </template>
|
|
|
+ <span class="custom-label">{{ formData.deviceName }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item prop="dept">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">所在部门:</span>
|
|
|
+ </template>
|
|
|
+ <span class="custom-label">{{ formData.dept }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item prop="ifInline">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">是否在线:</span>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="formData.ifInline" />
|
|
|
+ </template>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item prop="lastInlineTime">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">最后数据时间:</span>
|
|
|
+ </template>
|
|
|
+ <span class="custom-label">{{ formData.lastInlineTime }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item v-if="formData.vehicle" prop="vehicle">
|
|
|
+ <template #label>
|
|
|
+ <span class="custom-label">车牌号码:</span>
|
|
|
+ </template>
|
|
|
+ <span class="custom-label">{{ formData.vehicle }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ </ContentWrap>
|
|
|
+ <ContentWrap>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="24">
|
|
|
+ <TdDeviceLabel :tags="specs" @select="labelSelect" tag-width="24%" />
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </ContentWrap>
|
|
|
+ <ContentWrap>
|
|
|
+ <div class="chart-container">
|
|
|
+ <!-- 图表容器 -->
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ type="datetimerange"
|
|
|
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
|
|
+ start-placeholder="起始日期时间"
|
|
|
+ end-placeholder="结束日期时间"
|
|
|
+ format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ @change="handleDateChange"
|
|
|
+ />
|
|
|
+ <div v-loading="loading" style="height: 100%" ref="chartContainer"></div>
|
|
|
+ </div>
|
|
|
+ </ContentWrap>
|
|
|
+ </ContentWrap>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { DICT_TYPE } from '@/utils/dict'
|
|
|
+import TdDeviceLabel from '@/views/pms/device/monitor/TdDeviceLabel.vue'
|
|
|
+import { IotDeviceApi } from '@/api/pms/device'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { IotStatApi } from '@/api/pms/stat'
|
|
|
+import { IotAlarmSettingApi } from '@/api/pms/alarm'
|
|
|
+
|
|
|
+const { params, name } = useRoute() // 查询参数
|
|
|
+const info = ref({})
|
|
|
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
+const id = params.id
|
|
|
+defineOptions({ name: 'TdDeviceDetail' })
|
|
|
+const formData = ref({
|
|
|
+ deviceCode: '',
|
|
|
+ deviceName: '',
|
|
|
+ ifInline: undefined,
|
|
|
+ lastInlineTime: '',
|
|
|
+ dept: '',
|
|
|
+ vehicle: ''
|
|
|
+})
|
|
|
+const specs = ref([])
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const startTime = ref('')
|
|
|
+const endTime = ref('')
|
|
|
+const topicName = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const topic = ref('')
|
|
|
+// 设置固定阈值
|
|
|
+
|
|
|
+const handleDateChange = async (val) => {
|
|
|
+ if (val && val.length === 2) {
|
|
|
+ await getChart(val)
|
|
|
+ await renderChart()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const defaultEnd = dayjs()
|
|
|
+const defaultStart = defaultEnd.subtract(1, 'day')
|
|
|
+const dateRange = ref([
|
|
|
+ defaultStart.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ defaultEnd.format('YYYY-MM-DD HH:mm:ss')
|
|
|
+])
|
|
|
+const labelSelect = async (row) => {
|
|
|
+ topic.value = row.identifier
|
|
|
+ topicName.value = row.modelName
|
|
|
+ await getChart(dateRange.value)
|
|
|
+ await renderChart()
|
|
|
+}
|
|
|
+
|
|
|
+const chartContainer = ref(null)
|
|
|
+let chartInstance = null
|
|
|
+
|
|
|
+// 时间格式化(HH:mm)
|
|
|
+const formatTime = (timestamp) => {
|
|
|
+ return new Date(timestamp)
|
|
|
+ .toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
|
+ .slice(0, 5)
|
|
|
+}
|
|
|
+const result = ref([])
|
|
|
+const getChart = async (range) => {
|
|
|
+ loading.value = true
|
|
|
+ await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1]).then((res) => {
|
|
|
+ result.value = res
|
|
|
+ loading.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+// 初始化图表
|
|
|
+const renderChart = async () => {
|
|
|
+ if (!chartContainer.value) return
|
|
|
+ let upperLimit
|
|
|
+ let lowerLimit
|
|
|
+ await IotAlarmSettingApi.getDeviceRange(params.code, topic.value).then((res) => {
|
|
|
+ if (res) {
|
|
|
+ if (res.maxValue) {
|
|
|
+ upperLimit = res.maxValue
|
|
|
+ }
|
|
|
+ if (res.minValue) {
|
|
|
+ lowerLimit = res.minValue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 销毁旧实例
|
|
|
+ if (chartInstance) chartInstance.dispose()
|
|
|
+
|
|
|
+ chartInstance = markRaw(echarts.init(chartContainer.value))
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ title: {
|
|
|
+ text: topicName.value + '数据趋势',
|
|
|
+ left: 'center'
|
|
|
+ },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: result.value.map((d) => dayjs(d.timestamp).format('YYYY-MM-DD HH:mm:ss')),
|
|
|
+ axisLabel: { rotate: 45 },
|
|
|
+ inverse: true
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value'
|
|
|
+ // 根据固定阈值和实际数据调整Y轴范围,使阈值线更清晰
|
|
|
+ // min: Math.min(lowerLimit * 0.9, ...result.value.map(d => d.value || 0)),
|
|
|
+ // max: Math.max(upperLimit * 1.03, ...result.value.map(d => d.value || 0))
|
|
|
+ },
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: 'slider',
|
|
|
+ xAxisIndex: 0,
|
|
|
+ start: 0,
|
|
|
+ end: 100
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ // 原始数据曲线
|
|
|
+ {
|
|
|
+ data: result.value.map((d) => d.value),
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ name: '实时数据',
|
|
|
+ lineStyle: { color: '#409eff' }
|
|
|
+ },
|
|
|
+ // 上限阈值线(固定100)
|
|
|
+ {
|
|
|
+ data: result.value.map(() => upperLimit),
|
|
|
+ type: 'line',
|
|
|
+ name: '上限阈值',
|
|
|
+ lineStyle: {
|
|
|
+ color: '#f56c6c', // 红色虚线
|
|
|
+ type: 'dashed'
|
|
|
+ },
|
|
|
+ symbol: 'none', // 不显示数据点
|
|
|
+ emphasis: { disabled: true } // 禁用悬停高亮
|
|
|
+ },
|
|
|
+ // 下限阈值线(固定95)
|
|
|
+ {
|
|
|
+ data: result.value.map(() => lowerLimit),
|
|
|
+ type: 'line',
|
|
|
+ name: '下限阈值',
|
|
|
+ lineStyle: {
|
|
|
+ color: '#e6a23c', // 橙色虚线
|
|
|
+ type: 'dashed'
|
|
|
+ },
|
|
|
+ symbol: 'none',
|
|
|
+ emphasis: { disabled: true }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ // 添加图例显示各线条含义
|
|
|
+ legend: {
|
|
|
+ data: ['实时数据', '上限阈值', '下限阈值'],
|
|
|
+ top: 30
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ chartInstance.setOption(option)
|
|
|
+
|
|
|
+ // 窗口自适应
|
|
|
+ window.addEventListener('resize', () => chartInstance.resize())
|
|
|
+}
|
|
|
+onMounted(async () => {
|
|
|
+ formLoading.value = true
|
|
|
+ formData.value.deviceCode = params.code
|
|
|
+ formData.value.deviceName = params.name
|
|
|
+ formData.value.lastInlineTime = params.time
|
|
|
+ formData.value.ifInline = params.ifInline
|
|
|
+ formData.value.dept = params.dept
|
|
|
+ formData.value.vehicle = params.vehicle
|
|
|
+ await IotDeviceApi.getIotDeviceTds(id).then((res) => {
|
|
|
+ specs.value = res
|
|
|
+ specs.value = specs.value.sort((a, b) => {
|
|
|
+ return b.modelOrder - a.modelOrder
|
|
|
+ })
|
|
|
+ formLoading.value = false
|
|
|
+ topic.value = specs.value[0].identifier
|
|
|
+ topicName.value = specs.value[0].modelName
|
|
|
+ })
|
|
|
+ await getChart(dateRange.value)
|
|
|
+ await renderChart()
|
|
|
+})
|
|
|
+</script>
|
|
|
+<style scoped lang="scss">
|
|
|
+.container {
|
|
|
+ width: 100%;
|
|
|
+ margin: 20px auto;
|
|
|
+ padding: 24px;
|
|
|
+ //background: #f8f9fa;
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+.chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 600px;
|
|
|
+ padding: 20px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.date-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+input[type='datetime-local'] {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+input[type='datetime-local']:focus {
|
|
|
+ border-color: #409eff;
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+
|
|
|
+.separator {
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.query-btn {
|
|
|
+ padding: 8px 20px;
|
|
|
+ background: #409eff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.query-btn:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+//.chart {
|
|
|
+// width: 100%;
|
|
|
+// height: 500px;
|
|
|
+// margin-top: 20px;
|
|
|
+//}
|
|
|
+.custom-label {
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+</style>
|