Commit ff47c5b7d323492e32c1c3b3f964d6b687467c04

Authored by 杨鸣坤
1 parent 34ebee38

feat: 接入能耗设备列表接口并实现筛选分页

- 对接 /api/energy/list 和 /api/energy/stats 接口,替换本地硬编码数据
- 新增设备名称搜索及运行状态筛选功能,筛选标签支持动态计数与高亮
- 分页改为服务端分页,翻页及搜索时重新请求接口
- 设备卡片根据运行状态展示不同背景色与状态时长
- 移除未使用的 ParamSettingDialog 组件及相关入口
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 &nbsp; B相 120 &nbsp; 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">视在功率 &nbsp;&nbsp; {{ 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 <!-- 内容区:左侧状态灯 + 右侧信息 -->