温度看板搭建示例
This commit is contained in:
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{r as e,a as r,o as t,c as o,b as s,w as n,F as i,d as l,e as a,t as c,f as u,g as d}from"./vendor.3763ac43.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver((e=>{for(const t of e)if("childList"===t.type)for(const e of t.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)})).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),"use-credentials"===e.crossorigin?r.credentials="include":"anonymous"===e.crossorigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();const f=l("img",{alt:"Vue logo",src:"/assets/logo.03d6d6da.png"},null,-1),p=u({setup(l){const u=e("Hello Element Plus via Vite");return(e,l)=>{const d=r("el-button");return t(),o(i,null,[f,s(d,{type:"primary"},{default:n((()=>[a(c(u.value),1)])),_:1})],64)}}});p.use(d),p.mount("#app");
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
Vendored
-39
File diff suppressed because one or more lines are too long
Vendored
+2
-20
@@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -6,7 +5,8 @@
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>设备管理系统</title>
|
||||
<script type="module" crossorigin src="/assets/index-BhGvW7tT.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-1sWe50d-.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BpBnxyAz.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -14,21 +14,3 @@
|
||||
<!-- Vite 入口,指向 src/main.js,不要删除 -->␍
|
||||
</body>
|
||||
</html>
|
||||
=======
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Element Plus Demo</title>
|
||||
<script type="module" crossorigin src="/assets/index.a712a2e4.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.3763ac43.js">
|
||||
<link rel="stylesheet" href="/assets/index.a7868d2c.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- 引入 Vite 的核心模块入口 -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
>>>>>>> ef029340f2f8ddaab8f06229c6a867fadacd9a3c
|
||||
|
||||
+983
File diff suppressed because it is too large
Load Diff
+15
-9
@@ -1,43 +1,49 @@
|
||||
{
|
||||
"hash": "57bb8c15",
|
||||
"hash": "a6f0f194",
|
||||
"configHash": "147f2811",
|
||||
"lockfileHash": "fb7e8ff9",
|
||||
"browserHash": "fba38d4b",
|
||||
"lockfileHash": "77198382",
|
||||
"browserHash": "0cd29ca6",
|
||||
"optimized": {
|
||||
"@element-plus/icons-vue": {
|
||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||
"file": "@element-plus_icons-vue.js",
|
||||
"fileHash": "d1f71359",
|
||||
"fileHash": "9553abbf",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "3e018dd2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"echarts": {
|
||||
"src": "../../echarts/index.js",
|
||||
"file": "echarts.js",
|
||||
"fileHash": "34e45839",
|
||||
"fileHash": "275a5510",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus": {
|
||||
"src": "../../element-plus/es/index.mjs",
|
||||
"file": "element-plus.js",
|
||||
"fileHash": "8047ae48",
|
||||
"fileHash": "cc57a904",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../pinia/dist/pinia.js",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "fa8e7c2a",
|
||||
"fileHash": "0a5304b8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../vue-router/dist/vue-router.js",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "25bd4614",
|
||||
"fileHash": "0ef5b6fe",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "9a318764",
|
||||
"fileHash": "52e89e85",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 设备看板(温湿度)相关接口
|
||||
|
||||
// 获取所有环境箱最新温湿度(卡片轮询用)
|
||||
export function getRealtime() {
|
||||
return request.get('/inspection/dashboard/realtime')
|
||||
}
|
||||
|
||||
// 获取指定环境箱最近 N 分钟的温湿度曲线
|
||||
export function getCurve(deviceCode, minutes = 5) {
|
||||
return request.get('/inspection/dashboard/curve', {
|
||||
params: { deviceCode, minutes }
|
||||
})
|
||||
}
|
||||
@@ -18,13 +18,14 @@ request.interceptors.request.use(config => {
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
// 假设后端统一返回 { code, data, message }
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message))
|
||||
// 后端统一返回 { Code, Msg, Data }(兼容小写字段)
|
||||
const code = res.Code ?? res.code
|
||||
if (code !== 0) {
|
||||
ElMessage.error(res.Msg || res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.Msg || res.message))
|
||||
}
|
||||
// 直接返回 data,页面拿到的就是干净数据,不用每次再 .data
|
||||
return res.data
|
||||
// 直接返回 Data,页面拿到的就是干净数据,不用每次再 .data
|
||||
return res.Data ?? res.data
|
||||
},
|
||||
error => {
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
|
||||
@@ -1,14 +1,255 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<div class="page-container temp-box-page">
|
||||
<!-- 顶部:选中环境箱的实时温湿度曲线 -->
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>温度箱看板</span>
|
||||
<div class="chart-header">
|
||||
<span class="title">
|
||||
实时曲线
|
||||
<el-tag v-if="selected" size="small" type="primary">{{ selected.DeviceCode }}</el-tag>
|
||||
<el-tag v-if="selected && isAlarm(selected)" size="small" type="danger">报警</el-tag>
|
||||
</span>
|
||||
<span class="update-time">卡片数据更新:{{ updateTime || '--' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty description="温度箱看板 - 功能开发中" />
|
||||
<div ref="chartRef" class="chart-box"></div>
|
||||
<el-empty v-if="!selected" description="点击下方卡片选择一台环境箱查看曲线" :image-size="60" />
|
||||
</el-card>
|
||||
|
||||
<!-- 下方:100 台环境箱卡片 -->
|
||||
<el-card class="grid-card">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span class="title">环境箱状态({{ devices.length }} 台)</span>
|
||||
<span class="update-time">
|
||||
报警 {{ alarmCount }} 台
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="loading && devices.length === 0" class="device-grid">
|
||||
<div
|
||||
v-for="d in devices"
|
||||
:key="d.DeviceCode"
|
||||
class="device-card"
|
||||
:class="{ 'is-alarm': isAlarm(d), 'is-selected': d.DeviceCode === selectedCode }"
|
||||
@click="selectDevice(d)"
|
||||
>
|
||||
<div class="device-card__head">
|
||||
<span class="device-code">{{ d.DeviceCode }}</span>
|
||||
<el-tag :type="isAlarm(d) ? 'danger' : 'success'" size="small">
|
||||
{{ d.Status }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="device-card__body">
|
||||
<div class="value temp">{{ d.DeviceTemperature }}<small>℃</small></div>
|
||||
<div class="value hum">{{ d.DeviceHumidity }}<small>%</small></div>
|
||||
</div>
|
||||
<div class="device-card__foot">
|
||||
<span v-if="isAlarm(d)" class="alarm-text">{{ d.AlarmInfo }}</span>
|
||||
<span class="data-time">{{ fmtTime(d.CreateTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TODO: 实现温度箱看板功能
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getRealtime, getCurve } from '@/api/dashboard'
|
||||
|
||||
// ---------- 状态 ----------
|
||||
const devices = ref([]) // 100 台设备最新值
|
||||
const selectedCode = ref('') // 当前选中的设备
|
||||
const updateTime = ref('')
|
||||
const loading = ref(false)
|
||||
const chartRef = ref(null)
|
||||
let chart = null
|
||||
let realtimeTimer = null
|
||||
let curveTimer = null
|
||||
|
||||
const selected = computed(() => devices.value.find(d => d.DeviceCode === selectedCode.value) || null)
|
||||
const alarmCount = computed(() => devices.value.filter(isAlarm).length)
|
||||
|
||||
// ---------- 工具 ----------
|
||||
function isAlarm(d) {
|
||||
return d.Status === '报警' || !!d.AlarmInfo
|
||||
}
|
||||
|
||||
function fmtTime(str) {
|
||||
if (!str) return '--'
|
||||
// 后端返回 ISO 时间,截取 时:分:秒
|
||||
return str.indexOf('T') > -1 ? str.slice(11, 19) : str
|
||||
}
|
||||
|
||||
// ---------- 曲线 ----------
|
||||
function initChart() {
|
||||
if (!chartRef.value) return
|
||||
chart = echarts.init(chartRef.value)
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['温度(℃)', '湿度(%)'] },
|
||||
grid: { left: 50, right: 50, top: 40, bottom: 30 },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||
yAxis: [
|
||||
{ type: 'value', name: '℃' },
|
||||
{ type: 'value', name: '%' }
|
||||
],
|
||||
series: [
|
||||
{ name: '温度(℃)', type: 'line', showSymbol: false, smooth: true, data: [], itemStyle: { color: '#f56c6c' } },
|
||||
{ name: '湿度(%)', type: 'line', showSymbol: false, smooth: true, yAxisIndex: 1, data: [], itemStyle: { color: '#409eff' } }
|
||||
]
|
||||
})
|
||||
window.addEventListener('resize', resizeChart)
|
||||
}
|
||||
|
||||
function resizeChart() {
|
||||
chart && chart.resize()
|
||||
}
|
||||
|
||||
async function fetchCurve() {
|
||||
if (!selectedCode.value) return
|
||||
try {
|
||||
const list = await getCurve(selectedCode.value, 5)
|
||||
const times = list.map(d => fmtTime(d.CreateTime))
|
||||
chart && chart.setOption({
|
||||
xAxis: { data: times },
|
||||
series: [
|
||||
{ data: list.map(d => d.DeviceTemperature) },
|
||||
{ data: list.map(d => d.DeviceHumidity) }
|
||||
]
|
||||
})
|
||||
} catch (e) { /* 轮询失败忽略,等下一轮 */ }
|
||||
}
|
||||
|
||||
// ---------- 卡片 ----------
|
||||
async function fetchRealtime() {
|
||||
try {
|
||||
loading.value = true
|
||||
const list = await getRealtime()
|
||||
devices.value = list || []
|
||||
updateTime.value = new Date().toLocaleTimeString()
|
||||
// 默认选中第一台
|
||||
if (!selectedCode.value && devices.value.length) {
|
||||
selectedCode.value = devices.value[0].DeviceCode
|
||||
}
|
||||
} catch (e) { /* 后端未启动时静默等待下一轮 */ }
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectDevice(d) {
|
||||
if (selectedCode.value === d.DeviceCode) return
|
||||
selectedCode.value = d.DeviceCode
|
||||
fetchCurve() // 切换后立刻刷一次曲线
|
||||
}
|
||||
|
||||
// ---------- 生命周期 ----------
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
initChart()
|
||||
fetchRealtime()
|
||||
fetchCurve()
|
||||
realtimeTimer = setInterval(fetchRealtime, 5000) // 卡片每 5 秒刷新
|
||||
curveTimer = setInterval(fetchCurve, 2000) // 曲线每 2 秒刷新
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(realtimeTimer)
|
||||
clearInterval(curveTimer)
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
chart && chart.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.temp-box-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.update-time {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.chart-box {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.device-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.device-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
}
|
||||
.device-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.device-card.is-selected {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
.device-card.is-alarm {
|
||||
border-color: #f56c6c;
|
||||
background: #fef0f0;
|
||||
}
|
||||
.device-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.device-code {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.device-card__body {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.value small {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 2px;
|
||||
color: #909399;
|
||||
}
|
||||
.value.temp { color: #f56c6c; }
|
||||
.value.hum { color: #409eff; }
|
||||
.device-card__foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.alarm-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
+8
-1
@@ -11,6 +11,13 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
open: true,
|
||||
// 开发环境代理:/api 请求转发到后端服务,避免跨域
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5287',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user