Commit ff47c5b7d323492e32c1c3b3f964d6b687467c04
1 parent
34ebee38
feat: 接入能耗设备列表接口并实现筛选分页
- 对接 /api/energy/list 和 /api/energy/stats 接口,替换本地硬编码数据 - 新增设备名称搜索及运行状态筛选功能,筛选标签支持动态计数与高亮 - 分页改为服务端分页,翻页及搜索时重新请求接口 - 设备卡片根据运行状态展示不同背景色与状态时长 - 移除未使用的 ParamSettingDialog 组件及相关入口
Showing
3 changed files
with
156 additions
and
376 deletions
src/components/ParamSettingDialog.vue
deleted
100644 → 0
| 1 | -<template> | ||
| 2 | - <el-dialog | ||
| 3 | - :model-value="visible" | ||
| 4 | - @update:model-value="$emit('update:visible', $event)" | ||
| 5 | - title="" | ||
| 6 | - width="calc(100vw - 40px)" | ||
| 7 | - :style="{ maxWidth: '1400px' }" | ||
| 8 | - top="3vh" | ||
| 9 | - destroy-on-close | ||
| 10 | - class="param-dialog" | ||
| 11 | - > | ||
| 12 | - <template #header> | ||
| 13 | - <div class="dialog-header"> | ||
| 14 | - <span class="title-text">{{ device?.name || '能耗设备1' }}</span> | ||
| 15 | - <div class="header-right"> | ||
| 16 | - <el-icon :size="16" style="cursor:pointer;color:#409eff;"><Refresh /></el-icon> | ||
| 17 | - <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;"><Grid /></el-icon> | ||
| 18 | - <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;"><List /></el-icon> | ||
| 19 | - <el-icon :size="16" style="cursor:pointer;color:#409eff;margin-left:6px;" @click="$emit('update:visible', false)"><Close /></el-icon> | ||
| 20 | - </div> | ||
| 21 | - </div> | ||
| 22 | - </template> | ||
| 23 | - | ||
| 24 | - <div class="param-body"> | ||
| 25 | - <!-- 相位标签 --> | ||
| 26 | - <div class="phase-tabs"> | ||
| 27 | - <div v-for="p in phases" :key="p" :class="['phase-tab', { active: activePhase === p }]" | ||
| 28 | - @click="activePhase = p">{{ p }}:0</div> | ||
| 29 | - </div> | ||
| 30 | - | ||
| 31 | - <!-- 主内容区:左侧数据表 + 右侧统计 --> | ||
| 32 | - <div class="main-grid"> | ||
| 33 | - <!-- 左侧:电压电流功率温度表格 + 曲线 --> | ||
| 34 | - <div class="left-panel"> | ||
| 35 | - <div class="data-table-area"> | ||
| 36 | - <table class="param-table"> | ||
| 37 | - <thead> | ||
| 38 | - <tr><th></th><th>电压</th><th>电流</th><th>功率</th><th>温度</th></tr> | ||
| 39 | - </thead> | ||
| 40 | - <tbody> | ||
| 41 | - <tr v-for="row in tableRows" :key="row.label"> | ||
| 42 | - <td class="row-label">{{ row.label }}</td> | ||
| 43 | - <td>{{ row.voltage }}</td> | ||
| 44 | - <td>{{ row.current }}</td> | ||
| 45 | - <td>{{ row.power }}</td> | ||
| 46 | - <td>{{ row.temp }}</td> | ||
| 47 | - </tr> | ||
| 48 | - </tbody> | ||
| 49 | - </table> | ||
| 50 | - <!-- 右侧额外信息 --> | ||
| 51 | - <div class="extra-info"> | ||
| 52 | - <div v-for="(info, i) in extraInfos" :key="i" class="ei-item">{{ info }}:0</div> | ||
| 53 | - </div> | ||
| 54 | - </div> | ||
| 55 | - | ||
| 56 | - <!-- 电流均值/峰值曲线 --> | ||
| 57 | - <div class="curve-section"> | ||
| 58 | - <div class="curve-header"> | ||
| 59 | - <span class="curve-title">电流均值/峰值曲线 ▼</span> | ||
| 60 | - <el-date-picker v-model="curveDate" type="date" size="small" placeholder="2026-04-28" /> | ||
| 61 | - </div> | ||
| 62 | - <div class="curve-chart"> | ||
| 63 | - <svg viewBox="0 0 700 180"> | ||
| 64 | - <g font-size="10" fill="#999" text-anchor="end"> | ||
| 65 | - <text x="24" y="18">5</text><text x="24" y="53">4</text><text x="24" y="88">3</text> | ||
| 66 | - <text x="24" y="123">2</text><text x="24" y="158">1</text> | ||
| 67 | - </g> | ||
| 68 | - <line x1="30" y1="154" x2="680" y2="154" stroke="#ddd"/> | ||
| 69 | - <polyline points="36,154 56,154 76,154 ... 660,154" fill="none" stroke="#f5a623" stroke-width="1.2"/> | ||
| 70 | - <g font-size="8" fill="#666" text-anchor="middle"> | ||
| 71 | - <text x="46" y="168">00:00</text><text x="130" y="168">04:00</text> | ||
| 72 | - <text x="220" y="168">08:00</text><text x="310" y="168">12:00</text> | ||
| 73 | - <text x="400" y="168">16:00</text><text x="490" y="168">20:00</text><text x="580" y="168">24:00</text> | ||
| 74 | - </g> | ||
| 75 | - </svg> | ||
| 76 | - </div> | ||
| 77 | - <div class="curve-legend"> | ||
| 78 | - <span class="leg"><i class="dot y"/>电流最大值</span> | ||
| 79 | - <span class="leg"><i class="dot g"/>电流最小值</span> | ||
| 80 | - <span class="leg"><i class="dot r"/>电流瞬时平均值</span> | ||
| 81 | - </div> | ||
| 82 | - </div> | ||
| 83 | - | ||
| 84 | - <!-- 电能 & 相位角 --> | ||
| 85 | - <div class="bottom-row"> | ||
| 86 | - <div class="power-card"> | ||
| 87 | - <div class="pc-title">电能</div> | ||
| 88 | - <div class="pc-list"> | ||
| 89 | - <div v-for="(p, i) in powerItems" :key="i" :class="['pc-item', p.color]"> | ||
| 90 | - <span class="pc-dot"></span> {{ p.label }} | ||
| 91 | - <span class="pc-val">{{ p.val }}</span> | ||
| 92 | - </div> | ||
| 93 | - </div> | ||
| 94 | - </div> | ||
| 95 | - <div class="angle-card"> | ||
| 96 | - <div class="ac-title">相位角</div> | ||
| 97 | - <div class="ac-charts"> | ||
| 98 | - <div class="ac-pie-wrap"> | ||
| 99 | - <svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="48" fill="#e8f4fd"/><text x="60" y="55" text-anchor="middle" font-size="11" fill="#333">电压相位角</text> | ||
| 100 | - <g font-size="10" fill="#666"><text x="45" y="72">A相 0</text><text x="75" y="72">B相 0</text><text x="60" y="86">C相 0</text></g> | ||
| 101 | - <circle cx="35" cy="90" r="3" fill="#e74c3c"/><circle cx="60" cy="93" r="3" fill="#67c23a"/><circle cx="85" cy="90" r="3" fill="#409eff"/> | ||
| 102 | - </svg> | ||
| 103 | - </div> | ||
| 104 | - <div class="ac-pie-wrap"> | ||
| 105 | - <svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="48" fill="#e8f4fd"/><text x="60" y="55" text-anchor="middle" font-size="11" fill="#333">电流相位角</text> | ||
| 106 | - <g font-size="10" fill="#666"><text x="42" y="70">A相 0</text><text x="78" y="70">B相 0</text><text x="60" y="84">C相 0</text></g> | ||
| 107 | - <circle cx="32" cy="92" r="3" fill="#e74c3c"/><circle cx="58" cy="95" r="3" fill="#67c23a"/><circle cx="84" cy="92" r="3" fill="#409eff"/> | ||
| 108 | - </svg> | ||
| 109 | - </div> | ||
| 110 | - </div> | ||
| 111 | - <div class="ac-ref">参考值: A相 120 B相 120 C相 240</div> | ||
| 112 | - </div> | ||
| 113 | - </div> | ||
| 114 | - </div> | ||
| 115 | - | ||
| 116 | - <!-- 右侧:功率统计 --> | ||
| 117 | - <div class="right-panel"> | ||
| 118 | - <div class="rp-title">功率</div> | ||
| 119 | - <div class="power-stats"> | ||
| 120 | - <div v-for="(ps, i) in powerStats" :key="i" :class="['ps-item', 'ps-'+ps.color]"> | ||
| 121 | - <div class="ps-circle"> | ||
| 122 | - <svg viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="none" stroke="#eee" stroke-width="5"/> | ||
| 123 | - <text x="25" y="28" text-anchor="middle" font-size="13" font-weight="bold" fill="#333">{{ ps.val }}</text> | ||
| 124 | - </svg> | ||
| 125 | - </div> | ||
| 126 | - <div class="ps-info"> | ||
| 127 | - <div class="ps-bars"> | ||
| 128 | - <span :class="'ps-bar '+pb.color" v-for="(pb, j) in ps.bars" :key="j"> | ||
| 129 | - {{ pb.label }}{{ pb.val }} | ||
| 130 | - </span> | ||
| 131 | - </div> | ||
| 132 | - <div class="ps-result">视在功率 {{ ps.result }}</div> | ||
| 133 | - </div> | ||
| 134 | - </div> | ||
| 135 | - </div> | ||
| 136 | - | ||
| 137 | - <!-- 温度图表 --> | ||
| 138 | - <div class="temp-card"> | ||
| 139 | - <div class="temp-header">温度 <b>▼</b></div> | ||
| 140 | - <div class="temp-chart"> | ||
| 141 | - <svg viewBox="0 0 280 140"> | ||
| 142 | - <g font-size="9" fill="#999" text-anchor="end"> | ||
| 143 | - <text x="22" y="15">1</text><text x="22" y="43">0.8</text><text x="22" y="71">0.6</text> | ||
| 144 | - <text x="22" y="99">0.4</text><text x="22" y="127">0.2</text> | ||
| 145 | - </g> | ||
| 146 | - <line x1="28" y1="124" x2="268" y2="124" stroke="#ddd"/> | ||
| 147 | - <g font-size="7" fill="#666" text-anchor="middle"> | ||
| 148 | - <text x="38" y="136">温度TA</text><text x="68" y="136">温度TB</text> | ||
| 149 | - <text x="98" y="136">温度TC</text><text x="128" y="136">温度TN</text> | ||
| 150 | - <text x="158" y="136">温度TE</text> | ||
| 151 | - </g> | ||
| 152 | - </svg> | ||
| 153 | - </div> | ||
| 154 | - </div> | ||
| 155 | - </div> | ||
| 156 | - </div> | ||
| 157 | - </div> | ||
| 158 | - </el-dialog> | ||
| 159 | -</template> | ||
| 160 | - | ||
| 161 | -<script setup> | ||
| 162 | -import { ref } from 'vue' | ||
| 163 | -import { Refresh, Grid, List, Close } from '@element-plus/icons-vue' | ||
| 164 | - | ||
| 165 | -defineProps({ visible: Boolean, device: Object }) | ||
| 166 | -defineEmits(['update:visible']) | ||
| 167 | - | ||
| 168 | -const phases = ['UA', 'UB', 'UC', 'UAB', 'UBC', 'UAC', 'UF'] | ||
| 169 | -const activePhase = ref('UA') | ||
| 170 | -const curveDate = ref('') | ||
| 171 | - | ||
| 172 | -const tableRows = [ | ||
| 173 | - { label: '极大值', voltage: 0, current: 0, power: 0, temp: 0 }, | ||
| 174 | - { label: '平均值', voltage: 0, current: 0, power: 0, temp: 0 }, | ||
| 175 | - { label: '极小值', voltage: 0, current: 0, power: 0, temp: 0 } | ||
| 176 | -] | ||
| 177 | -const extraInfos = ['IA', 'IB', 'IC'] | ||
| 178 | -const powerItems = [ | ||
| 179 | - { label: 'A相', val: 0, color: 'o' }, | ||
| 180 | - { label: 'B相', val: 0, color: 'g' }, | ||
| 181 | - { label: 'C相', val: 0, color: 'r' }, | ||
| 182 | - { label: '合相', val: 0, color: 'b' } | ||
| 183 | -] | ||
| 184 | -const powerStats = [ | ||
| 185 | - { | ||
| 186 | - color: 'o', val: 0, | ||
| 187 | - bars: [ | ||
| 188 | - { label: '有功功率', val: 0, color: '' }, | ||
| 189 | - { label: 'A相 ', val: '', color: 'o' }, { label: '无功功率', val: 0, color: '' } | ||
| 190 | - ], | ||
| 191 | - result: 0 | ||
| 192 | - }, | ||
| 193 | - { | ||
| 194 | - color: 'g', val: 0, | ||
| 195 | - bars: [ | ||
| 196 | - { label: '有功功率', val: 0, color: '' }, | ||
| 197 | - { label: 'B相 ', val: '', color: 'g' }, { label: '无功功率', val: 0, color: '' } | ||
| 198 | - ], | ||
| 199 | - result: 0 | ||
| 200 | - }, | ||
| 201 | - { | ||
| 202 | - color: 'r', val: 0, | ||
| 203 | - bars: [ | ||
| 204 | - { label: '有功功率', val: 0, color: '' }, | ||
| 205 | - { label: 'C相 ', val: '', color: 'r' }, { label: '无功功率', val: 0, color: '' } | ||
| 206 | - ], | ||
| 207 | - result: 0 | ||
| 208 | - }, | ||
| 209 | - { | ||
| 210 | - color: 'b', val: 0, | ||
| 211 | - bars: [ | ||
| 212 | - { label: '有功功率', val: 0, color: '' }, | ||
| 213 | - { label: '合相 ', val: '', color: 'b' }, { label: '无功功率', val: 0, color: '' } | ||
| 214 | - ], | ||
| 215 | - result: 0 | ||
| 216 | - } | ||
| 217 | -] | ||
| 218 | -</script> | ||
| 219 | - | ||
| 220 | -<style scoped> | ||
| 221 | -.param-dialog :deep(.el-dialog){max-height:92vh;display:flex;flex-direction:column;} | ||
| 222 | -.param-dialog :deep(.el-dialog__header){padding:10px 20px;border-bottom:1px solid #e8e8e8;margin:0;flex-shrink:0;} | ||
| 223 | -.param-dialog :deep(.el-dialog__body){overflow-y:auto;flex:1;padding:12px;} | ||
| 224 | -.dialog-header{display:flex;align-items:center;justify-content:space-between;} | ||
| 225 | -.title-text{font-size:15px;font-weight:bold;color:#333;} | ||
| 226 | -.header-right{display:flex;align-items:center;} | ||
| 227 | - | ||
| 228 | -.phase-tabs{display:flex;gap:4px;background:#fff;border:1px solid #eee;border-radius:6px 6px 0 0;padding:8px 14px;} | ||
| 229 | -.phase-tab{padding:6px 14px;font-size:12px;cursor:pointer;color:#666;border-radius:4px;} | ||
| 230 | -.phase-tab.active{background:#409eff;color:#fff;font-weight:bold;} | ||
| 231 | - | ||
| 232 | -.main-grid{display:flex;gap:14px;background:#fff;border:1px solid #eee;border-top:none;border-radius:0 0 6px 6px;padding:14px;} | ||
| 233 | - | ||
| 234 | -.left-panel{flex:1;min-width:0;} | ||
| 235 | -.data-table-area{background:#408aff;border-radius:6px;padding:14px;display:flex;gap:16px;margin-bottom:12px;color:#fff;} | ||
| 236 | -.param-table{border-collapse:collapse;font-size:13px;} | ||
| 237 | -.param-table th{padding:6px 16px;text-align:center;font-weight:normal;opacity:0.85;border-bottom:1px solid rgba(255,255,255,0.2);} | ||
| 238 | -.param-table td{padding:6px 16px;text-align:center;} | ||
| 239 | -.row-label{text-align:left!important;font-weight:bold;} | ||
| 240 | -.extra-info{font-size:12px;opacity:0.85;line-height:2;} | ||
| 241 | - | ||
| 242 | -.curve-section{border:1px solid #eee;border-radius:6px;padding:12px;margin-bottom:12px;} | ||
| 243 | -.curve-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;} | ||
| 244 | -.curve-title{font-size:13px;font-weight:bold;color:#333;} | ||
| 245 | -.curve-legend{display:flex;gap:14px;font-size:11px;color:#666;margin-top:6px;} | ||
| 246 | -.curve-chart svg{width:100%;height:auto;} | ||
| 247 | - | ||
| 248 | -.bottom-row{display:flex;gap:12px;} | ||
| 249 | -.power-card,.angle-card{flex:1;border:1px solid #eee;border-radius:6px;padding:12px;} | ||
| 250 | -.pc-title,.ac-title{font-size:13px;font-weight:bold;color:#333;margin-bottom:8px;} | ||
| 251 | -.pc-list{display:flex;flex-direction:column;gap:6px;} | ||
| 252 | -.pc-item{display:flex;align-items:center;gap:6px;font-size:12px;color:#666;} | ||
| 253 | -.pc-dot{width:10px;height:10px;border-radius:2px;} | ||
| 254 | -.pc-dot.o{background:#f56c6c;}.pc-dot.g{background:#67c23a;} | ||
| 255 | -.pc-dot.r{background:#e74c3c;}.pc-dot.b{background:#409eff;} | ||
| 256 | -.pc-val{margin-left:auto;font-weight:bold;color:#333;} | ||
| 257 | - | ||
| 258 | -.ac-charts{display:flex;gap:12px;justify-content:center;} | ||
| 259 | -.ac-pie-wrap svg{width:110px;height:110px;} | ||
| 260 | -.ac-ref{text-align:center;font-size:11px;color:#999;margin-top:6px;} | ||
| 261 | - | ||
| 262 | -.right-panel{width:260px;flex-shrink:0;} | ||
| 263 | -.rp-title{font-size:14px;font-weight:bold;color:#333;margin-bottom:10px;} | ||
| 264 | -.power-stats{display:flex;flex-direction:column;gap:10px;margin-bottom:14px;} | ||
| 265 | -.ps-item{display:flex;align-items:center;gap:8px;} | ||
| 266 | -.ps-circle svg{width:44px;height:44px;flex-shrink:0;} | ||
| 267 | -.ps-info{flex:1;font-size:10px;color:#666;} | ||
| 268 | -.ps-bar{display:inline-block;padding:2px 4px;border-radius:2px;margin-right:4px;} | ||
| 269 | -.ps-bar.o{background:#fff3e0;color:#e65100;}.ps-bar.g{background:#e8f5e9;color:#2e7d32;} | ||
| 270 | -.ps-bar.r{background:#ffebee;color:#c62828;}.ps-bar.b{background:#e3f2fd;color:#1565c0;} | ||
| 271 | -.ps-result{margin-top:2px;} | ||
| 272 | - | ||
| 273 | -.temp-card{border:1px solid #eee;border-radius:6px;padding:12px;} | ||
| 274 | -.temp-header{font-size:13px;font-weight:bold;color:#333;margin-bottom:8px;} | ||
| 275 | -.temp-chart svg{width:100%;height:auto;} | ||
| 276 | - | ||
| 277 | -.leg{display:flex;align-items:center;gap:3px;} | ||
| 278 | -.dot{display:inline-block;width:10px;height:10px;border-radius:2px;} | ||
| 279 | -.dot.y{background:#f5a623;}.dot.g{background:#67c23a;}.dot.r{background:#f56c6c;} | ||
| 280 | -</style> |
| @@ -16,76 +16,74 @@ | @@ -16,76 +16,74 @@ | ||
| 16 | 16 | ||
| 17 | <!-- 筛选栏(仅实时状态显示) --> | 17 | <!-- 筛选栏(仅实时状态显示) --> |
| 18 | <div v-if="currentStatus === 'realtime'" class="filter-bar"> | 18 | <div v-if="currentStatus === 'realtime'" class="filter-bar"> |
| 19 | - <span class="filter-label">请输入设备:</span> | 19 | + <el-input |
| 20 | + v-model="searchKeyword" | ||
| 21 | + placeholder="输入设备名称搜索" | ||
| 22 | + clearable | ||
| 23 | + size="default" | ||
| 24 | + style="width: 220px; margin-right: 16px;" | ||
| 25 | + @keyup.enter="doSearch" | ||
| 26 | + @clear="doSearch" | ||
| 27 | + /> | ||
| 20 | <div class="filter-tags"> | 28 | <div class="filter-tags"> |
| 21 | - <span class="tag-item black"><i></i>全量2台</span> | ||
| 22 | - <span class="tag-item red"><i></i>停机:0台</span> | ||
| 23 | - <span class="tag-item green"><i></i>待机:0台</span> | ||
| 24 | - <span class="tag-item blue"><i></i>运行:0台</span> | ||
| 25 | - <span class="tag-item gray"><i></i>离线:2台</span> | 29 | + <span :class="['tag-item', 'black', { active: !runStatusFilter }]" @click="filterByRunStatus('')"><i></i>全部{{ totalCounts.all }}台</span> |
| 30 | + <span :class="['tag-item', 'red', { active: runStatusFilter === '1' }]" @click="filterByRunStatus('1')"><i></i>停机:{{ totalCounts.stop }}台</span> | ||
| 31 | + <span :class="['tag-item', 'green', { active: runStatusFilter === '2' }]" @click="filterByRunStatus('2')"><i></i>待机:{{ totalCounts.standby }}台</span> | ||
| 32 | + <span :class="['tag-item', 'blue', { active: runStatusFilter === '3' }]" @click="filterByRunStatus('3')"><i></i>运行:{{ totalCounts.run }}台</span> | ||
| 33 | + <span :class="['tag-item', 'gray', { active: runStatusFilter === '0' }]" @click="filterByRunStatus('0')"><i></i>离线:{{ totalCounts.offline }}台</span> | ||
| 26 | </div> | 34 | </div> |
| 27 | </div> | 35 | </div> |
| 28 | 36 | ||
| 29 | <!-- ========== 实时状态:设备卡片 ========== --> | 37 | <!-- ========== 实时状态:设备卡片 ========== --> |
| 30 | <div v-if="currentStatus === 'realtime'" class="tab-content"> | 38 | <div v-if="currentStatus === 'realtime'" class="tab-content"> |
| 31 | <!-- 设备卡片网格 --> | 39 | <!-- 设备卡片网格 --> |
| 32 | - <div class="device-grid" :class="{ 'grid-full': totalDevices > PAGE_SIZE || (pagedDevices.length >= PAGE_SIZE && totalDevices > PAGE_SIZE), 'grid-normal': pagedDevices.length <= 6 }"> | ||
| 33 | - <div v-for="device in pagedDevices" :key="device.id" class="energy-card"> | 40 | + <div class="device-grid"> |
| 41 | + <div class="grid-inner"> | ||
| 42 | + <div v-for="device in pagedDevices" :key="device.id" :class="['energy-card', 'status-' + device.runStatus]"> | ||
| 34 | <div class="card-header"> | 43 | <div class="card-header"> |
| 35 | <span class="device-name">{{ device.name }}</span> | 44 | <span class="device-name">{{ device.name }}</span> |
| 36 | - <el-icon class="menu-icon"><Menu /></el-icon> | ||
| 37 | </div> | 45 | </div> |
| 38 | 46 | ||
| 39 | <div class="card-body"> | 47 | <div class="card-body"> |
| 40 | - <div class="energy-icon"> | ||
| 41 | - <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"> | ||
| 42 | - <path d="M32 8 L38 28 L48 20 L42 38 L58 38 L38 52 L44 62 L32 54 L20 62 L26 52 L6 38 L22 38 L16 20 L26 28 Z" | ||
| 43 | - fill="#f5a623" stroke="#d48806" stroke-width="2"/> | 48 | + <div class="energy-icon lightning-icon"> |
| 49 | + <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2003/2000/svg"> | ||
| 50 | + <path d="M35.5 4 L17 36 h14 l-6 24 22 -32 H33 l8 -24 Z" | ||
| 51 | + fill="#f5a623" stroke="#d48806" stroke-width="1.5" stroke-linejoin="round"/> | ||
| 44 | </svg> | 52 | </svg> |
| 45 | </div> | 53 | </div> |
| 46 | 54 | ||
| 47 | <div class="info-list"> | 55 | <div class="info-list"> |
| 48 | - <div class="info-item">电压: <span>{{ device.voltage }}V</span></div> | ||
| 49 | - <div class="info-item">电流: <span>{{ device.current }}A</span></div> | ||
| 50 | - <div class="info-item">昨日能耗: <span class="value-highlight">{{ device.yesterdayEnergy }}</span></div> | 56 | + <div class="info-item">用电量:<span>{{ device.evalue }} kw·h</span></div> |
| 57 | + <div class="info-item">{{ getRunStatusLabel(device.runStatus) }}:<span>{{ device.duration }}</span></div> | ||
| 51 | </div> | 58 | </div> |
| 52 | </div> | 59 | </div> |
| 53 | 60 | ||
| 54 | <div class="card-footer"> | 61 | <div class="card-footer"> |
| 55 | <button class="action-btn primary" @click="openDetail('report', device)"> | 62 | <button class="action-btn primary" @click="openDetail('report', device)"> |
| 56 | - <el-icon><Document /></el-icon>能耗报表 | 63 | + <el-icon><Document /></el-icon>运行状态 |
| 57 | </button> | 64 | </button> |
| 58 | <button class="action-btn danger" @click="openDetail('safety', device)"> | 65 | <button class="action-btn danger" @click="openDetail('safety', device)"> |
| 59 | - <el-icon><Lock /></el-icon>用电安全 | ||
| 60 | - </button> | ||
| 61 | - <button class="action-btn warning" @click="openDetail('param', device)"> | ||
| 62 | - <el-icon><Setting /></el-icon>参数设置 | ||
| 63 | - </button> | ||
| 64 | - <button v-if="false" class="action-btn info" @click="openDetail('warning', device)"> | ||
| 65 | - <el-icon><Warning /></el-icon>预警设置 | 66 | + <el-icon><Lock /></el-icon>用时用电 |
| 66 | </button> | 67 | </button> |
| 67 | </div> | 68 | </div> |
| 68 | </div> | 69 | </div> |
| 70 | + </div><!-- /grid-inner --> | ||
| 69 | </div> | 71 | </div> |
| 70 | 72 | ||
| 71 | <!-- 分页 --> | 73 | <!-- 分页 --> |
| 72 | <div v-if="totalDevices > 0" class="pagination-wrapper"> | 74 | <div v-if="totalDevices > 0" class="pagination-wrapper"> |
| 73 | - <!-- 分页控件 --> | ||
| 74 | <div class="pagination-controls"> | 75 | <div class="pagination-controls"> |
| 75 | - <!-- 每页条数选择 --> | ||
| 76 | <select class="page-size-select" :value="PAGE_SIZE"> | 76 | <select class="page-size-select" :value="PAGE_SIZE"> |
| 77 | <option value="12">12 条/页</option> | 77 | <option value="12">12 条/页</option> |
| 78 | </select> | 78 | </select> |
| 79 | - | ||
| 80 | - <!-- 导航按钮组 --> | ||
| 81 | - <button class="page-btn" :disabled="currentPage === 1" @click="currentPage = 1">«</button> | ||
| 82 | - <button class="page-btn" :disabled="currentPage === 1" @click="currentPage--"><</button> | 79 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage = 1; fetchDeviceList()">«</button> |
| 80 | + <button class="page-btn" :disabled="currentPage === 1" @click="currentPage--; fetchDeviceList()"><</button> | ||
| 83 | <template v-for="(p, i) in visiblePages" :key="i"> | 81 | <template v-for="(p, i) in visiblePages" :key="i"> |
| 84 | - <button v-if="typeof p === 'number'" :class="['page-btn', { active: currentPage === p }]" @click="currentPage = p">{{ p }}</button> | 82 | + <button v-if="typeof p === 'number'" :class="['page-btn', { active: currentPage === p }]" @click="currentPage = p; fetchDeviceList()">{{ p }}</button> |
| 85 | <span v-else class="page-dots">{{ p }}</span> | 83 | <span v-else class="page-dots">{{ p }}</span> |
| 86 | </template> | 84 | </template> |
| 87 | - <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage++">></button> | ||
| 88 | - <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage = totalPages">»</button> | 85 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage++; fetchDeviceList()">></button> |
| 86 | + <button class="page-btn" :disabled="currentPage === totalPages" @click="currentPage = totalPages; fetchDeviceList()">»</button> | ||
| 89 | </div> | 87 | </div> |
| 90 | </div> | 88 | </div> |
| 91 | </div><!-- /tab-content realtime --> | 89 | </div><!-- /tab-content realtime --> |
| @@ -287,11 +285,6 @@ | @@ -287,11 +285,6 @@ | ||
| 287 | v-model:visible="dialogVisible.safety" | 285 | v-model:visible="dialogVisible.safety" |
| 288 | :device="currentDevice" | 286 | :device="currentDevice" |
| 289 | /> | 287 | /> |
| 290 | - <!-- 参数设置弹窗 --> | ||
| 291 | - <ParamSettingDialog | ||
| 292 | - v-model:visible="dialogVisible.param" | ||
| 293 | - :device="currentDevice" | ||
| 294 | - /> | ||
| 295 | <!-- 预警设置弹窗 --> | 288 | <!-- 预警设置弹窗 --> |
| 296 | <WarningSettingDialog | 289 | <WarningSettingDialog |
| 297 | v-model:visible="dialogVisible.warning" | 290 | v-model:visible="dialogVisible.warning" |
| @@ -301,16 +294,33 @@ | @@ -301,16 +294,33 @@ | ||
| 301 | </template> | 294 | </template> |
| 302 | 295 | ||
| 303 | <script setup> | 296 | <script setup> |
| 304 | -import { ref, reactive, computed } from 'vue' | 297 | +import { ref, reactive, computed, onMounted } from 'vue' |
| 305 | import { Search, Menu, Document, Lock, Setting, Warning, Histogram } from '@element-plus/icons-vue' | 298 | import { Search, Menu, Document, Lock, Setting, Warning, Histogram } from '@element-plus/icons-vue' |
| 306 | import EnergyReportDialog from '../components/EnergyReportDialog.vue' | 299 | import EnergyReportDialog from '../components/EnergyReportDialog.vue' |
| 307 | import SafetyDialog from '../components/SafetyDialog.vue' | 300 | import SafetyDialog from '../components/SafetyDialog.vue' |
| 308 | -import ParamSettingDialog from '../components/ParamSettingDialog.vue' | ||
| 309 | import WarningSettingDialog from '../components/WarningSettingDialog.vue' | 301 | import WarningSettingDialog from '../components/WarningSettingDialog.vue' |
| 310 | 302 | ||
| 311 | const selectedFactory = ref('新建') | 303 | const selectedFactory = ref('新建') |
| 312 | const searchKeyword = ref('') | 304 | const searchKeyword = ref('') |
| 313 | const currentStatus = ref('realtime') | 305 | const currentStatus = ref('realtime') |
| 306 | +const runStatusFilter = ref('') // runStatus 筛选: ''=全部, '0'=离线, '1'=停机, '2'=待机, '3'=运行 | ||
| 307 | + | ||
| 308 | +// 各状态数量(接口返回后更新) | ||
| 309 | +const totalCounts = reactive({ all: 0, stop: 0, standby: 0, run: 0, offline: 0 }) | ||
| 310 | + | ||
| 311 | +// 点击状态筛选 | ||
| 312 | +function filterByRunStatus(runStatus) { | ||
| 313 | + if (runStatusFilter.value === runStatus) return | ||
| 314 | + runStatusFilter.value = runStatus | ||
| 315 | + currentPage.value = 1 | ||
| 316 | + fetchDeviceList() | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +// 搜索 | ||
| 320 | +function doSearch() { | ||
| 321 | + currentPage.value = 1 | ||
| 322 | + fetchDeviceList() | ||
| 323 | +} | ||
| 314 | 324 | ||
| 315 | // 能耗页面4个Tab(第4个是能耗效率,与智能灯不同) | 325 | // 能耗页面4个Tab(第4个是能耗效率,与智能灯不同) |
| 316 | const statusTabs = [ | 326 | const statusTabs = [ |
| @@ -320,14 +330,11 @@ const statusTabs = [ | @@ -320,14 +330,11 @@ const statusTabs = [ | ||
| 320 | { key: 'efficiency', label: '能耗效率' } | 330 | { key: 'efficiency', label: '能耗效率' } |
| 321 | ] | 331 | ] |
| 322 | 332 | ||
| 323 | -const deviceList = ref([ | ||
| 324 | - { id: 1, name: '磨粉设备1', voltage: 0, current: 0, yesterdayEnergy: 0 }, | ||
| 325 | - { id: 2, name: '磨粉设备2', voltage: 0, current: 0, yesterdayEnergy: 0 } | ||
| 326 | -]) | 333 | +const deviceList = ref([]) |
| 334 | +const totalDevices = ref(0) | ||
| 327 | 335 | ||
| 328 | const PAGE_SIZE = 12 | 336 | const PAGE_SIZE = 12 |
| 329 | const currentPage = ref(1) | 337 | const currentPage = ref(1) |
| 330 | -const totalDevices = computed(() => deviceList.value.length) | ||
| 331 | const totalPages = computed(() => Math.ceil(totalDevices.value / PAGE_SIZE) || 1) | 338 | const totalPages = computed(() => Math.ceil(totalDevices.value / PAGE_SIZE) || 1) |
| 332 | const visiblePages = computed(() => { | 339 | const visiblePages = computed(() => { |
| 333 | const pages = [] | 340 | const pages = [] |
| @@ -342,16 +349,71 @@ const visiblePages = computed(() => { | @@ -342,16 +349,71 @@ const visiblePages = computed(() => { | ||
| 342 | if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } | 349 | if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } |
| 343 | return pages | 350 | return pages |
| 344 | }) | 351 | }) |
| 345 | -const pagedDevices = computed(() => { | ||
| 346 | - const start = (currentPage.value - 1) * PAGE_SIZE | ||
| 347 | - return deviceList.value.slice(start, start + PAGE_SIZE) | 352 | +// 服务端分页,pagedDevices 直接使用接口返回的 list |
| 353 | +const pagedDevices = computed(() => deviceList.value) | ||
| 354 | + | ||
| 355 | +// 获取能耗设备列表 | ||
| 356 | +async function fetchDeviceList() { | ||
| 357 | + try { | ||
| 358 | + const params = new URLSearchParams({ | ||
| 359 | + pageNo: currentPage.value, | ||
| 360 | + pageSize: PAGE_SIZE, | ||
| 361 | + projectState: '1', | ||
| 362 | + }) | ||
| 363 | + if (searchKeyword.value) params.append('deviceName', searchKeyword.value) | ||
| 364 | + if (runStatusFilter.value !== '') params.append('runStatus', runStatusFilter.value) | ||
| 365 | + | ||
| 366 | + const res = await fetch(`/api/energy/list?${params}`) | ||
| 367 | + const data = await res.json() | ||
| 368 | + deviceList.value = (data.list || []).map(item => ({ | ||
| 369 | + id: item.id, | ||
| 370 | + name: item.deviceName || item.dtuSn, | ||
| 371 | + evalue: parseFloat(item.evalue) || 0, | ||
| 372 | + runStatus: String(item.runStatus ?? '0'), | ||
| 373 | + duration: item.duration || '0秒', | ||
| 374 | + _raw: item, | ||
| 375 | + })) | ||
| 376 | + totalDevices.value = data.total || 0 | ||
| 377 | + | ||
| 378 | + // 刷新统计数据 | ||
| 379 | + await fetchStats() | ||
| 380 | + } catch (err) { | ||
| 381 | + console.error('获取能耗设备列表失败:', err) | ||
| 382 | + } | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +// 获取运行状态统计 | ||
| 386 | +async function fetchStats() { | ||
| 387 | + try { | ||
| 388 | + const res = await fetch('/api/energy/stats') | ||
| 389 | + const data = await res.json() | ||
| 390 | + totalCounts.all = data.total || 0 | ||
| 391 | + totalCounts.offline = parseInt(data['0']) || 0 // runStatus=0 离线 | ||
| 392 | + totalCounts.stop = parseInt(data['1']) || 0 // runStatus=1 停机 | ||
| 393 | + totalCounts.standby = parseInt(data['2']) || 0 // runStatus=2 待机 | ||
| 394 | + totalCounts.run = parseInt(data['3']) || 0 // runStatus=3 运行 | ||
| 395 | + } catch (err) { | ||
| 396 | + console.error('获取能耗统计失败:', err) | ||
| 397 | + } | ||
| 398 | +} | ||
| 399 | + | ||
| 400 | +// 页面挂载时加载设备列表 | ||
| 401 | +onMounted(() => { | ||
| 402 | + fetchDeviceList() | ||
| 348 | }) | 403 | }) |
| 349 | 404 | ||
| 405 | +// runStatus 状态文字映射 | ||
| 406 | +const RUN_STATUS_LABELS = { '0': '离线', '1': '停机', '2': '待机', '3': '运行' } | ||
| 407 | +function getRunStatusLabel(runStatus) { | ||
| 408 | + return RUN_STATUS_LABELS[String(runStatus)] || '未知' | ||
| 409 | +} | ||
| 410 | + | ||
| 350 | const dialogVisible = reactive({ | 411 | const dialogVisible = reactive({ |
| 351 | report: false, | 412 | report: false, |
| 352 | safety: false, | 413 | safety: false, |
| 353 | param: false, | 414 | param: false, |
| 354 | - warning: false | 415 | + warning: false, |
| 416 | + setting: false | ||
| 355 | }) | 417 | }) |
| 356 | const currentDevice = ref(null) | 418 | const currentDevice = ref(null) |
| 357 | 419 | ||
| @@ -408,17 +470,14 @@ const effLine2Points = computed(() => { | @@ -408,17 +470,14 @@ const effLine2Points = computed(() => { | ||
| 408 | } | 470 | } |
| 409 | .device-grid { | 471 | .device-grid { |
| 410 | flex: 1; | 472 | flex: 1; |
| 473 | + overflow-x: auto; | ||
| 474 | + overflow-y: auto; | ||
| 411 | padding: 16px 20px; | 475 | padding: 16px 20px; |
| 476 | +} | ||
| 477 | +.device-grid .grid-inner { | ||
| 412 | display: grid; | 478 | display: grid; |
| 413 | - grid-template-columns: repeat(6, 1fr); | 479 | + grid-template-columns: repeat(6, 270px); |
| 414 | gap: 16px; | 480 | gap: 16px; |
| 415 | - align-content: start; | ||
| 416 | -} | ||
| 417 | -.device-grid.grid-full { | ||
| 418 | - align-content: stretch; | ||
| 419 | -} | ||
| 420 | -.device-grid.grid-normal { | ||
| 421 | - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| 422 | } | 481 | } |
| 423 | .top-toolbar { | 482 | .top-toolbar { |
| 424 | background: #fff; | 483 | background: #fff; |
| @@ -483,6 +542,14 @@ const effLine2Points = computed(() => { | @@ -483,6 +542,14 @@ const effLine2Points = computed(() => { | ||
| 483 | display: flex; | 542 | display: flex; |
| 484 | align-items: center; | 543 | align-items: center; |
| 485 | gap: 4px; | 544 | gap: 4px; |
| 545 | + cursor: pointer; | ||
| 546 | + transition: all 0.2s; | ||
| 547 | +} | ||
| 548 | +.tag-item:hover { | ||
| 549 | + opacity: 0.8; | ||
| 550 | +} | ||
| 551 | +.tag-item.active { | ||
| 552 | + font-weight: bold; | ||
| 486 | } | 553 | } |
| 487 | .tag-item i { | 554 | .tag-item i { |
| 488 | width: 10px; | 555 | width: 10px; |
| @@ -497,15 +564,20 @@ const effLine2Points = computed(() => { | @@ -497,15 +564,20 @@ const effLine2Points = computed(() => { | ||
| 497 | .tag-item.gray i { background: #909399; } | 564 | .tag-item.gray i { background: #909399; } |
| 498 | 565 | ||
| 499 | .energy-card { | 566 | .energy-card { |
| 567 | + min-width: 270px; | ||
| 500 | height: 340px; | 568 | height: 340px; |
| 501 | - background: linear-gradient(145deg, #3d3a4d 0%, #2d2a3a 100%); | ||
| 502 | border-radius: 12px; | 569 | border-radius: 12px; |
| 503 | overflow: hidden; | 570 | overflow: hidden; |
| 504 | - box-shadow: 0 4px 16px rgba(0,0,0,0.2); | 571 | + box-shadow: 0 4px 16px rgba(0,0,0,0.15); |
| 505 | transition: transform 0.2s; | 572 | transition: transform 0.2s; |
| 506 | display: flex; | 573 | display: flex; |
| 507 | flex-direction: column; | 574 | flex-direction: column; |
| 508 | } | 575 | } |
| 576 | +/* runStatus 状态背景色 */ | ||
| 577 | +.energy-card.status-0 { background: linear-gradient(145deg, #b8b8b8 0%, #999 100%); } | ||
| 578 | +.energy-card.status-1 { background: linear-gradient(145deg, #f56c6c 0%, #e74c3c 100%); } | ||
| 579 | +.energy-card.status-2 { background: linear-gradient(145deg, #7ec87e 0%, #5cb85c 100%); } | ||
| 580 | +.energy-card.status-3 { background: linear-gradient(145deg, #32a756 0%, #289048 100%); } | ||
| 509 | .energy-card:hover { | 581 | .energy-card:hover { |
| 510 | transform: translateY(-2px); | 582 | transform: translateY(-2px); |
| 511 | box-shadow: 0 6px 20px rgba(0,0,0,0.25); | 583 | box-shadow: 0 6px 20px rgba(0,0,0,0.25); |
| @@ -525,23 +597,27 @@ const effLine2Points = computed(() => { | @@ -525,23 +597,27 @@ const effLine2Points = computed(() => { | ||
| 525 | color: #aaa; | 597 | color: #aaa; |
| 526 | } | 598 | } |
| 527 | .card-body { | 599 | .card-body { |
| 528 | - padding: 14px 16px 10px; | 600 | + padding: 10px 16px; |
| 529 | text-align: center; | 601 | text-align: center; |
| 530 | flex: 1; | 602 | flex: 1; |
| 603 | + display: flex; | ||
| 604 | + flex-direction: column; | ||
| 605 | + align-items: center; | ||
| 606 | + justify-content: center; | ||
| 531 | } | 607 | } |
| 532 | .energy-icon { | 608 | .energy-icon { |
| 533 | - width: 64px; | ||
| 534 | - height: 64px; | ||
| 535 | - margin: 0 auto 12px; | 609 | + width: 56px; |
| 610 | + height: 56px; | ||
| 611 | + margin-bottom: 12px; | ||
| 536 | } | 612 | } |
| 537 | -.energy-icon svg { | 613 | +.lightning-icon svg { |
| 538 | width: 100%; | 614 | width: 100%; |
| 539 | height: 100%; | 615 | height: 100%; |
| 540 | - filter: drop-shadow(0 0 14px rgba(245,166,35,0.4)); | 616 | + filter: drop-shadow(0 0 12px rgba(255,200,0,0.5)); |
| 541 | } | 617 | } |
| 542 | .info-list { | 618 | .info-list { |
| 543 | - text-align: left; | ||
| 544 | - color: #ccc; | 619 | + text-align: center; |
| 620 | + color: #fff; | ||
| 545 | font-size: 13px; | 621 | font-size: 13px; |
| 546 | line-height: 2; | 622 | line-height: 2; |
| 547 | } | 623 | } |
| @@ -554,11 +630,11 @@ const effLine2Points = computed(() => { | @@ -554,11 +630,11 @@ const effLine2Points = computed(() => { | ||
| 554 | font-weight: bold; | 630 | font-weight: bold; |
| 555 | } | 631 | } |
| 556 | .card-footer { | 632 | .card-footer { |
| 557 | - padding: 10px 14px 12px; | 633 | + padding: 8px 12px; |
| 558 | display: grid; | 634 | display: grid; |
| 559 | grid-template-columns: 1fr 1fr; | 635 | grid-template-columns: 1fr 1fr; |
| 560 | - gap: 8px; | ||
| 561 | - border-top: 1px solid rgba(255,255,255,0.08); | 636 | + gap: 6px; |
| 637 | + border-top: 1px solid rgba(255,255,255,0.15); | ||
| 562 | flex-shrink: 0; | 638 | flex-shrink: 0; |
| 563 | } | 639 | } |
| 564 | .action-btn { | 640 | .action-btn { |
| @@ -566,38 +642,23 @@ const effLine2Points = computed(() => { | @@ -566,38 +642,23 @@ const effLine2Points = computed(() => { | ||
| 566 | align-items: center; | 642 | align-items: center; |
| 567 | justify-content: center; | 643 | justify-content: center; |
| 568 | gap: 4px; | 644 | gap: 4px; |
| 569 | - padding: 7px 8px; | ||
| 570 | - border: none; | ||
| 571 | - border-radius: 6px; | ||
| 572 | - font-size: 13px; | 645 | + padding: 6px 6px; |
| 646 | + border: 1px solid rgba(255,255,255,0.35); | ||
| 647 | + border-radius: 5px; | ||
| 648 | + font-size: 12px; | ||
| 573 | cursor: pointer; | 649 | cursor: pointer; |
| 574 | transition: all 0.2s; | 650 | transition: all 0.2s; |
| 575 | color: #fff; | 651 | color: #fff; |
| 652 | + background: rgba(0,0,0,0.15); | ||
| 576 | } | 653 | } |
| 577 | .action-btn:hover { | 654 | .action-btn:hover { |
| 578 | - opacity: 0.85; | 655 | + opacity: 0.8; |
| 579 | transform: scale(1.02); | 656 | transform: scale(1.02); |
| 580 | } | 657 | } |
| 581 | -.action-btn.primary { | ||
| 582 | - background: rgba(64,158,255,0.25); | ||
| 583 | - color: #409eff; | ||
| 584 | - border: 1px solid rgba(64,158,255,0.3); | ||
| 585 | -} | ||
| 586 | -.action-btn.danger { | ||
| 587 | - background: rgba(245,108,108,0.25); | ||
| 588 | - color: #f56c6c; | ||
| 589 | - border: 1px solid rgba(245,108,108,0.3); | ||
| 590 | -} | ||
| 591 | -.action-btn.warning { | ||
| 592 | - background: rgba(230,162,60,0.25); | ||
| 593 | - color: #e6a23c; | ||
| 594 | - border: 1px solid rgba(230,162,60,0.3); | ||
| 595 | -} | ||
| 596 | -.action-btn.info { | ||
| 597 | - background: rgba(144,147,153,0.25); | ||
| 598 | - color: #909399; | ||
| 599 | - border: 1px solid rgba(144,147,153,0.3); | ||
| 600 | -} | 658 | +.action-btn.primary { border-color: rgba(64,158,255,0.5); } |
| 659 | +.action-btn.danger { border-color: rgba(245,108,108,0.5); } | ||
| 660 | +.action-btn.info { border-color: rgba(144,147,153,0.45); } | ||
| 661 | +.action-btn.setting { border-color: rgba(144,147,153,0.45); } | ||
| 601 | /* ========== 自定义分页 ========== */ | 662 | /* ========== 自定义分页 ========== */ |
| 602 | .pagination-wrapper { | 663 | .pagination-wrapper { |
| 603 | display: flex; | 664 | display: flex; |
| @@ -43,7 +43,6 @@ | @@ -43,7 +43,6 @@ | ||
| 43 | <!-- 标题栏 --> | 43 | <!-- 标题栏 --> |
| 44 | <div class="card-header"> | 44 | <div class="card-header"> |
| 45 | <span class="device-name">{{ device.name }}</span> | 45 | <span class="device-name">{{ device.name }}</span> |
| 46 | - <el-icon class="menu-icon"><Menu /></el-icon> | ||
| 47 | </div> | 46 | </div> |
| 48 | 47 | ||
| 49 | <!-- 内容区:左侧状态灯 + 右侧信息 --> | 48 | <!-- 内容区:左侧状态灯 + 右侧信息 --> |