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 | 16 | |
| 17 | 17 | <!-- 筛选栏(仅实时状态显示) --> |
| 18 | 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 | 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 | 34 | </div> |
| 27 | 35 | </div> |
| 28 | 36 | |
| 29 | 37 | <!-- ========== 实时状态:设备卡片 ========== --> |
| 30 | 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 | 43 | <div class="card-header"> |
| 35 | 44 | <span class="device-name">{{ device.name }}</span> |
| 36 | - <el-icon class="menu-icon"><Menu /></el-icon> | |
| 37 | 45 | </div> |
| 38 | 46 | |
| 39 | 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 | 52 | </svg> |
| 45 | 53 | </div> |
| 46 | 54 | |
| 47 | 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 | 58 | </div> |
| 52 | 59 | </div> |
| 53 | 60 | |
| 54 | 61 | <div class="card-footer"> |
| 55 | 62 | <button class="action-btn primary" @click="openDetail('report', device)"> |
| 56 | - <el-icon><Document /></el-icon>能耗报表 | |
| 63 | + <el-icon><Document /></el-icon>运行状态 | |
| 57 | 64 | </button> |
| 58 | 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 | 67 | </button> |
| 67 | 68 | </div> |
| 68 | 69 | </div> |
| 70 | + </div><!-- /grid-inner --> | |
| 69 | 71 | </div> |
| 70 | 72 | |
| 71 | 73 | <!-- 分页 --> |
| 72 | 74 | <div v-if="totalDevices > 0" class="pagination-wrapper"> |
| 73 | - <!-- 分页控件 --> | |
| 74 | 75 | <div class="pagination-controls"> |
| 75 | - <!-- 每页条数选择 --> | |
| 76 | 76 | <select class="page-size-select" :value="PAGE_SIZE"> |
| 77 | 77 | <option value="12">12 条/页</option> |
| 78 | 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 | 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 | 83 | <span v-else class="page-dots">{{ p }}</span> |
| 86 | 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 | 87 | </div> |
| 90 | 88 | </div> |
| 91 | 89 | </div><!-- /tab-content realtime --> |
| ... | ... | @@ -287,11 +285,6 @@ |
| 287 | 285 | v-model:visible="dialogVisible.safety" |
| 288 | 286 | :device="currentDevice" |
| 289 | 287 | /> |
| 290 | - <!-- 参数设置弹窗 --> | |
| 291 | - <ParamSettingDialog | |
| 292 | - v-model:visible="dialogVisible.param" | |
| 293 | - :device="currentDevice" | |
| 294 | - /> | |
| 295 | 288 | <!-- 预警设置弹窗 --> |
| 296 | 289 | <WarningSettingDialog |
| 297 | 290 | v-model:visible="dialogVisible.warning" |
| ... | ... | @@ -301,16 +294,33 @@ |
| 301 | 294 | </template> |
| 302 | 295 | |
| 303 | 296 | <script setup> |
| 304 | -import { ref, reactive, computed } from 'vue' | |
| 297 | +import { ref, reactive, computed, onMounted } from 'vue' | |
| 305 | 298 | import { Search, Menu, Document, Lock, Setting, Warning, Histogram } from '@element-plus/icons-vue' |
| 306 | 299 | import EnergyReportDialog from '../components/EnergyReportDialog.vue' |
| 307 | 300 | import SafetyDialog from '../components/SafetyDialog.vue' |
| 308 | -import ParamSettingDialog from '../components/ParamSettingDialog.vue' | |
| 309 | 301 | import WarningSettingDialog from '../components/WarningSettingDialog.vue' |
| 310 | 302 | |
| 311 | 303 | const selectedFactory = ref('新建') |
| 312 | 304 | const searchKeyword = ref('') |
| 313 | 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 | 325 | // 能耗页面4个Tab(第4个是能耗效率,与智能灯不同) |
| 316 | 326 | const statusTabs = [ |
| ... | ... | @@ -320,14 +330,11 @@ const statusTabs = [ |
| 320 | 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 | 336 | const PAGE_SIZE = 12 |
| 329 | 337 | const currentPage = ref(1) |
| 330 | -const totalDevices = computed(() => deviceList.value.length) | |
| 331 | 338 | const totalPages = computed(() => Math.ceil(totalDevices.value / PAGE_SIZE) || 1) |
| 332 | 339 | const visiblePages = computed(() => { |
| 333 | 340 | const pages = [] |
| ... | ... | @@ -342,16 +349,71 @@ const visiblePages = computed(() => { |
| 342 | 349 | if (end < tp) { if (end < tp - 1) pages.push('...'); pages.push(tp) } |
| 343 | 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 | 411 | const dialogVisible = reactive({ |
| 351 | 412 | report: false, |
| 352 | 413 | safety: false, |
| 353 | 414 | param: false, |
| 354 | - warning: false | |
| 415 | + warning: false, | |
| 416 | + setting: false | |
| 355 | 417 | }) |
| 356 | 418 | const currentDevice = ref(null) |
| 357 | 419 | |
| ... | ... | @@ -408,17 +470,14 @@ const effLine2Points = computed(() => { |
| 408 | 470 | } |
| 409 | 471 | .device-grid { |
| 410 | 472 | flex: 1; |
| 473 | + overflow-x: auto; | |
| 474 | + overflow-y: auto; | |
| 411 | 475 | padding: 16px 20px; |
| 476 | +} | |
| 477 | +.device-grid .grid-inner { | |
| 412 | 478 | display: grid; |
| 413 | - grid-template-columns: repeat(6, 1fr); | |
| 479 | + grid-template-columns: repeat(6, 270px); | |
| 414 | 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 | 482 | .top-toolbar { |
| 424 | 483 | background: #fff; |
| ... | ... | @@ -483,6 +542,14 @@ const effLine2Points = computed(() => { |
| 483 | 542 | display: flex; |
| 484 | 543 | align-items: center; |
| 485 | 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 | 554 | .tag-item i { |
| 488 | 555 | width: 10px; |
| ... | ... | @@ -497,15 +564,20 @@ const effLine2Points = computed(() => { |
| 497 | 564 | .tag-item.gray i { background: #909399; } |
| 498 | 565 | |
| 499 | 566 | .energy-card { |
| 567 | + min-width: 270px; | |
| 500 | 568 | height: 340px; |
| 501 | - background: linear-gradient(145deg, #3d3a4d 0%, #2d2a3a 100%); | |
| 502 | 569 | border-radius: 12px; |
| 503 | 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 | 572 | transition: transform 0.2s; |
| 506 | 573 | display: flex; |
| 507 | 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 | 581 | .energy-card:hover { |
| 510 | 582 | transform: translateY(-2px); |
| 511 | 583 | box-shadow: 0 6px 20px rgba(0,0,0,0.25); |
| ... | ... | @@ -525,23 +597,27 @@ const effLine2Points = computed(() => { |
| 525 | 597 | color: #aaa; |
| 526 | 598 | } |
| 527 | 599 | .card-body { |
| 528 | - padding: 14px 16px 10px; | |
| 600 | + padding: 10px 16px; | |
| 529 | 601 | text-align: center; |
| 530 | 602 | flex: 1; |
| 603 | + display: flex; | |
| 604 | + flex-direction: column; | |
| 605 | + align-items: center; | |
| 606 | + justify-content: center; | |
| 531 | 607 | } |
| 532 | 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 | 614 | width: 100%; |
| 539 | 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 | 618 | .info-list { |
| 543 | - text-align: left; | |
| 544 | - color: #ccc; | |
| 619 | + text-align: center; | |
| 620 | + color: #fff; | |
| 545 | 621 | font-size: 13px; |
| 546 | 622 | line-height: 2; |
| 547 | 623 | } |
| ... | ... | @@ -554,11 +630,11 @@ const effLine2Points = computed(() => { |
| 554 | 630 | font-weight: bold; |
| 555 | 631 | } |
| 556 | 632 | .card-footer { |
| 557 | - padding: 10px 14px 12px; | |
| 633 | + padding: 8px 12px; | |
| 558 | 634 | display: grid; |
| 559 | 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 | 638 | flex-shrink: 0; |
| 563 | 639 | } |
| 564 | 640 | .action-btn { |
| ... | ... | @@ -566,38 +642,23 @@ const effLine2Points = computed(() => { |
| 566 | 642 | align-items: center; |
| 567 | 643 | justify-content: center; |
| 568 | 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 | 649 | cursor: pointer; |
| 574 | 650 | transition: all 0.2s; |
| 575 | 651 | color: #fff; |
| 652 | + background: rgba(0,0,0,0.15); | |
| 576 | 653 | } |
| 577 | 654 | .action-btn:hover { |
| 578 | - opacity: 0.85; | |
| 655 | + opacity: 0.8; | |
| 579 | 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 | 663 | .pagination-wrapper { |
| 603 | 664 | display: flex; | ... | ... |