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 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;
... ...
... ... @@ -43,7 +43,6 @@
43 43 <!-- 标题栏 -->
44 44 <div class="card-header">
45 45 <span class="device-name">{{ device.name }}</span>
46   - <el-icon class="menu-icon"><Menu /></el-icon>
47 46 </div>
48 47
49 48 <!-- 内容区:左侧状态灯 + 右侧信息 -->
... ...