楼宇能效监测控制系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1546 lines
50 KiB

<template>
<div class="device-manage">
<!-- 搜索筛选区域 -->
<div class="search-filter-area">
<div class="search-group">
<div class="search-item">
<label>设备名称</label>
<el-input
v-model="searchForm.deviceName"
placeholder="请输入水泵/仪表/网关名称或编号"
clearable
class="custom-input"
@clear="handleSearch"
>
<template #prefix>
<svg class="search-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M909.6 854.5L649.9 594.8c41.3-54.3 65.9-121.4 65.9-194.1C715.8 262.2 560.6 107 370.4 107S25 262.2 25 452.4s155.2 345.4 345.4 345.4c72.7 0 139.8-24.6 194.1-65.9l259.7 259.7c11.1 11.1 29.1 11.1 40.2 0l45.2-45.2c11.1-11.1 11.1-29.1 0-40.2zM370.4 705c-139.1 0-252.6-113.5-252.6-252.6s113.5-252.6 252.6-252.6 252.6 113.5 252.6 252.6-113.5 252.6-252.6 252.6z" fill="currentColor"/>
</svg>
</template>
</el-input>
</div>
<div class="search-item">
<label>设备状态</label>
<el-select
v-model="searchForm.deviceStatus"
placeholder="全部"
clearable
class="custom-select"
@change="handleSearch"
>
<el-option label="全部" value=""></el-option>
<el-option label="运行中" value="running"></el-option>
<el-option label="离线" value="offline"></el-option>
<el-option label="故障" value="fault"></el-option>
</el-select>
</div>
<div class="search-item">
<label>设备子类型</label>
<el-select
v-model="searchForm.deviceType"
placeholder="全部"
clearable
class="custom-select"
@change="handleSearch"
>
<el-option label="全部" value=""></el-option>
<el-option
v-for="type in getSubDeviceTypes()"
:key="type.value"
:label="type.label"
:value="type.value"
></el-option>
</el-select>
</div>
</div>
<div class="action-group">
<div class="primary-btn"><el-button type="primary" @click="handleSearch" class="custom-btn">查询</el-button></div>
<el-button @click="handleReset" class="custom-btn">重置</el-button>
<div class="success-btn"><el-button type="success" @click="handleExport" class="custom-btn">
<svg class="btn-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M384 704h256v-192h192l-320-320-320 320h192v192zM128 768h768v64H128v-64z" fill="currentColor"/>
</svg>
导出列表
</el-button></div>
</div>
</div>
<!-- 设备统计卡片 -->
<div class="device-stats">
<div class="stat-card total">
<div class="stat-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"/>
<path d="M688 464H336c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h352c8.8 0 16-7.2 16-16v-64c0-8.8-7.2-16-16-16z" fill="currentColor"/>
</svg>
</div>
<div class="stat-info">
<div class="stat-value">{{ deviceStats.total }}</div>
<div class="stat-label">设备总数</div>
</div>
</div>
<div class="stat-card running">
<div class="stat-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"/>
<path d="M672 464H352c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h320c8.8 0 16-7.2 16-16v-64c0-8.8-7.2-16-16-16z" fill="currentColor"/>
<path d="M512 592c-4.4 0-8 3.6-8 8v256c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V600c0-4.4-3.6-8-8-8h-48z" fill="currentColor"/>
</svg>
</div>
<div class="stat-info">
<div class="stat-value">{{ deviceStats.running }}</div>
<div class="stat-label">运行中</div>
</div>
</div>
<div class="stat-card offline">
<div class="stat-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"/>
<path d="M512 352c-8.8 0-16 7.2-16 16v224c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16V368c0-8.8-7.2-16-16-16h-48z" fill="currentColor"/>
</svg>
</div>
<div class="stat-info">
<div class="stat-value">{{ deviceStats.offline }}</div>
<div class="stat-label">离线</div>
</div>
</div>
<div class="stat-card fault">
<div class="stat-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"/>
<path d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z" fill="currentColor"/>
</svg>
</div>
<div class="stat-info">
<div class="stat-value">{{ deviceStats.fault }}</div>
<div class="stat-label">故障</div>
</div>
</div>
</div>
<!-- 设备分类标签页 -->
<div class="category-tabs">
<div
v-for="(category, index) in deviceCategories"
:key="index"
:class="['category-tab', { active: activeCategory === index }]"
@click="handleCategoryChange(index)"
>
<span class="tab-icon">{{ getCategoryIcon(category.value) }}</span>
<span class="tab-label">{{ category.label }}</span>
<span class="tab-count">({{ category.count }})</span>
</div>
</div>
<!-- 设备列表 -->
<div class="device-list-container">
<div class="list-header">
<div class="list-title">设备列表</div>
<div class="list-info">
共 {{ filteredDevices.length }} 台设备
</div>
</div>
<div class="device-grid">
<div
v-for="(device, index) in paginatedDevices"
:key="index"
:class="['device-card', device.status]"
@click="viewDeviceDetail(device)"
>
<div class="card-header">
<div class="device-icon" :style="{ background: device.iconBg }">
<span class="icon-emoji">{{ getDeviceIconEmoji(device.type) }}</span>
</div>
<div class="device-header-info">
<div class="device-name">{{ device.name }}</div>
<div class="device-code">{{ device.code }}</div>
</div>
<div class="status-badge" :class="device.status">
{{ getStatusText(device.status) }}
</div>
</div>
<div class="card-body">
<div class="device-info-row">
<span class="info-label">型号:</span>
<span class="info-value">{{ device.model }}</span>
</div>
<div class="device-info-row">
<span class="info-label">安装日期:</span>
<span class="info-value">{{ device.installDate }}</span>
</div>
<div class="device-info-row" v-if="device.area">
<span class="info-label">所在区域:</span>
<span class="info-value">{{ device.area }}</span>
</div>
<div class="device-info-row" v-if="device.status === 'running' && device.currentPressure !== undefined">
<span class="info-label">当前压力:</span>
<span class="info-value highlight">{{ device.currentPressure }} MPa</span>
</div>
</div>
<div class="card-footer">
<div class="device-params" v-if="device.parameters && device.parameters.length > 0">
<div
v-for="(param, pIndex) in device.parameters.slice(0, 3)"
:key="pIndex"
class="param-item"
>
<span class="param-label">{{ param.label }}:</span>
<span class="param-value">{{ param.value }}{{ param.unit }}</span>
</div>
<div v-if="device.parameters.length > 3" class="param-more">
+{{ device.parameters.length - 3 }}项
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[12, 24, 48, 96]"
:total="filteredDevices.length"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="custom-pagination"
>
</el-pagination>
</div>
</div>
<!-- 设备详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
:title="selectedDevice ? selectedDevice.name + ' - 设备详情' : '设备详情'"
width="900px"
class="custom-dialog"
@close="closeDetailDialog"
>
<div class="detail-content" v-if="selectedDevice">
<div class="detail-section">
<div class="section-title">基本信息</div>
<div class="info-grid">
<div class="info-item">
<span class="item-label">设备编号:</span>
<span class="item-value">{{ selectedDevice.code }}</span>
</div>
<div class="info-item">
<span class="item-label">设备名称:</span>
<span class="item-value">{{ selectedDevice.name }}</span>
</div>
<div class="info-item">
<span class="item-label">设备型号:</span>
<span class="item-value">{{ selectedDevice.model }}</span>
</div>
<div class="info-item">
<span class="item-label">设备类型:</span>
<span class="item-value">{{ getDeviceTypeName(selectedDevice.type) }}</span>
</div>
<div class="info-item">
<span class="item-label">安装日期:</span>
<span class="item-value">{{ selectedDevice.installDate }}</span>
</div>
<div class="info-item">
<span class="item-label">设备状态:</span>
<span class="item-value status-badge" :class="selectedDevice.status">
{{ getStatusText(selectedDevice.status) }}
</span>
</div>
<div class="info-item" v-if="selectedDevice.area">
<span class="item-label">所在区域:</span>
<span class="item-value">{{ selectedDevice.area }}</span>
</div>
<div class="info-item" v-if="selectedDevice.location">
<span class="item-label">具体位置:</span>
<span class="item-value">{{ selectedDevice.location }}</span>
</div>
<div class="info-item full-width" v-if="selectedDevice.spec">
<span class="item-label">设备参数:</span>
<span class="item-value">{{ selectedDevice.spec }}</span>
</div>
<div class="info-item full-width" v-if="selectedDevice.description">
<span class="item-label">设备描述:</span>
<span class="item-value">{{ selectedDevice.description }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="selectedDevice.parameters && selectedDevice.parameters.length > 0">
<div class="section-title">运行参数</div>
<div class="params-grid">
<div
v-for="(param, index) in selectedDevice.parameters"
:key="index"
class="param-card"
>
<div class="param-label">{{ param.label }}</div>
<div class="param-value">{{ param.value }}</div>
<div class="param-unit">{{ param.unit }}</div>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-title">运行状态</div>
<div class="status-info">
<div class="status-item">
<span class="status-label">当前状态:</span>
<span class="status-text" :class="selectedDevice.status">
{{ getStatusText(selectedDevice.status) }}
</span>
</div>
<div class="status-item" v-if="selectedDevice.status === 'running' && selectedDevice.runtime">
<span class="status-label">运行时长:</span>
<span class="status-text">{{ selectedDevice.runtime }}</span>
</div>
<div class="status-item" v-if="selectedDevice.status === 'fault'">
<span class="status-label">故障信息:</span>
<span class="status-text fault">{{ selectedDevice.faultInfo || '未知故障' }}</span>
</div>
<div class="status-item" v-if="selectedDevice.lastUpdate">
<span class="status-label">最后更新:</span>
<span class="status-text">{{ selectedDevice.lastUpdate }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="selectedDevice.type === 'pump'">
<div class="section-title">控制信息</div>
<div class="control-grid">
<div class="control-item">
<span class="control-label">设定压力:</span>
<el-slider v-model="selectedDevice.setPressure" :min="0.2" :max="0.8" :step="0.05" :disabled="selectedDevice.status !== 'running'"></el-slider>
<span class="control-value">{{ selectedDevice.setPressure }} MPa</span>
</div>
<div class="control-item" v-if="selectedDevice.frequency">
<span class="control-label">运行频率:</span>
<el-slider v-model="selectedDevice.frequency" :min="0" :max="50" :step="0.5" :disabled="selectedDevice.status !== 'running'"></el-slider>
<span class="control-value">{{ selectedDevice.frequency }} Hz</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-title">
<span>历史运行数据趋势</span>
<div class="section-actions">
<el-select v-model="historyTimeRange" @change="updateHistoryChart" size="small" class="custom-select" style="width: 1.2rem;">
<el-option label="近24小时" value="24h"></el-option>
<el-option label="近7天" value="7d"></el-option>
<el-option label="近30天" value="30d"></el-option>
</el-select>
<el-button size="small" @click="exportHistoryData" class="custom-btn">导出数据</el-button>
</div>
</div>
<div class="chart-container">
<div ref="historyChart" class="chart"></div>
</div>
</div>
<div class="detail-section">
<div class="section-title">
<span>维护记录</span>
<div class="section-actions">
<el-button size="small" type="primary" @click="showMaintenanceDialog" class="custom-btn">新增记录</el-button>
<el-button size="small" @click="exportMaintenanceRecords" class="custom-btn">导出记录</el-button>
</div>
</div>
<div class="maintenance-list">
<div
v-for="(record, index) in maintenanceRecords"
:key="index"
class="maintenance-item"
>
<div class="maintenance-header">
<div class="maintenance-type" :class="record.type">{{ getMaintenanceTypeText(record.type) }}</div>
<div class="maintenance-date">{{ record.date }}</div>
</div>
<div class="maintenance-content">
<div class="maintenance-title">{{ record.title }}</div>
<div class="maintenance-desc">{{ record.description }}</div>
<div class="maintenance-operator">
<span class="operator-label">维护人:</span>
<span class="operator-name">{{ record.operator }}</span>
</div>
</div>
</div>
<div class="no-maintenance" v-if="maintenanceRecords.length === 0">
<div class="no-data-icon">📋</div>
<div class="no-data-text">暂无维护记录</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDetailDialog" class="custom-btn">关闭</el-button>
<el-button type="primary" @click="saveControl" class="custom-btn" v-if="selectedDevice && selectedDevice.type === 'pump'">保存控制</el-button>
</div>
</template>
</el-dialog>
<!-- 维护记录弹窗 -->
<el-dialog
v-model="maintenanceDialogVisible"
title="新增维护记录"
width="600px"
class="custom-dialog"
>
<el-form :model="maintenanceForm" :rules="maintenanceRules" ref="maintenanceFormRef" label-width="100px" class="custom-form">
<el-form-item label="维护类型" prop="type">
<el-select v-model="maintenanceForm.type" placeholder="请选择维护类型" class="custom-select">
<el-option label="定期保养" value="regular"></el-option>
<el-option label="故障维修" value="repair"></el-option>
<el-option label="预防性维护" value="preventive"></el-option>
<el-option label="升级改造" value="upgrade"></el-option>
</el-select>
</el-form-item>
<el-form-item label="维护标题" prop="title">
<el-input v-model="maintenanceForm.title" placeholder="请输入维护标题" class="custom-input"></el-input>
</el-form-item>
<el-form-item label="维护描述" prop="description">
<el-input
v-model="maintenanceForm.description"
type="textarea"
:rows="3"
placeholder="请输入维护描述"
class="custom-input"
></el-input>
</el-form-item>
<el-form-item label="维护人员">
<el-input v-model="maintenanceForm.operator" placeholder="请输入维护人员姓名" class="custom-input"></el-input>
</el-form-item>
<el-form-item label="维护时间">
<el-date-picker
v-model="maintenanceForm.date"
type="datetime"
placeholder="选择维护时间"
value-format="yyyy-MM-dd HH:mm:ss"
class="custom-picker"
></el-date-picker>
</el-form-item>
<el-form-item label="维护结果">
<el-radio-group v-model="maintenanceForm.result" class="custom-radio">
<el-radio label="success">正常完成</el-radio>
<el-radio label="partial">部分完成</el-radio>
<el-radio label="failed">未完成</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="maintenanceDialogVisible = false" class="custom-btn">取消</el-button>
<el-button type="primary" @click="saveMaintenanceRecord" class="custom-btn">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "DeviceManage",
data() {
return {
searchForm: {
deviceName: "",
deviceStatus: "",
deviceType: ""
},
deviceCategories: [
{ label: "全部设备", value: "all", count: 26 },
{ label: "水泵组", value: "pump", count: 16 },
{ label: "仪表", value: "meter", count: 4 },
{ label: "网关", value: "gateway", count: 3 },
{ label: "传感器", value: "sensor", count: 3 }
],
activeCategory: 0,
currentPage: 1,
pageSize: 12,
deviceList: [],
deviceStats: {
total: 26,
running: 22,
offline: 3,
fault: 1
},
detailDialogVisible: false,
selectedDevice: null,
historyTimeRange: "24h",
historyChart: null,
maintenanceDialogVisible: false,
maintenanceForm: {
type: "regular",
title: "",
description: "",
operator: "",
date: "",
result: "success"
},
maintenanceRules: {
type: [{ required: true, message: "请选择维护类型", trigger: "change" }],
title: [{ required: true, message: "请输入维护标题", trigger: "blur" }],
description: [{ required: true, message: "请输入维护描述", trigger: "blur" }]
},
maintenanceRecords: []
};
},
computed: {
filteredDevices() {
let devices = [...this.deviceList];
const category = this.deviceCategories[this.activeCategory];
if (category.value !== "all") {
devices = devices.filter(d => d.type === category.value);
}
if (this.searchForm.deviceName) {
const keyword = this.searchForm.deviceName.toLowerCase();
devices = devices.filter(d =>
d.name.toLowerCase().includes(keyword) ||
d.code.toLowerCase().includes(keyword)
);
}
if (this.searchForm.deviceStatus) {
devices = devices.filter(d => d.status === this.searchForm.deviceStatus);
}
if (this.searchForm.deviceType) {
devices = devices.filter(d => d.subType === this.searchForm.deviceType);
}
return devices;
},
paginatedDevices() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.filteredDevices.slice(start, end);
}
},
mounted() {
this.initDeviceList();
},
methods: {
initDeviceList() {
const pumpIcons = ["#FFD700", "#FFA500", "#FF6B6B", "#4ECDC4"];
const gatewayIcons = ["#667eea", "#764ba2"];
const meterIcons = ["#f093fb", "#f5576c"];
const sensorIcons = ["#30cfd0", "#330867"];
// 水泵设备
const pumpTypes = [
{ label: "高区水泵", value: "highZonePump", zone: "高区" },
{ label: "中区水泵", value: "middleZonePump", zone: "中区" },
{ label: "低区水泵", value: "lowZonePump", zone: "低区" },
{ label: "消防水泵", value: "fireZonePump", zone: "消防区" }
];
const pumpModels = [
{ model: "SBP-45-30", power: 45, pressure: 0.6, flow: 180 },
{ model: "SBP-37-25", power: 37, pressure: 0.5, flow: 150 },
{ model: "SBP-22-18", power: 22, pressure: 0.4, flow: 120 },
{ model: "SBP-55-40", power: 55, pressure: 0.7, flow: 200 }
];
for (let i = 1; i <= 16; i++) {
const typeIndex = i % pumpTypes.length;
const modelIndex = i % pumpModels.length;
const iconIndex = i % pumpIcons.length;
this.deviceList.push({
code: `WP-${pumpTypes[typeIndex].zone === '高区' ? 'H' : pumpTypes[typeIndex].zone === '中区' ? 'M' : pumpTypes[typeIndex].zone === '低区' ? 'L' : 'F'}-${String(i).padStart(3, '0')}`,
name: `${pumpTypes[typeIndex].zone}${i}#${pumpTypes[typeIndex].label.replace('区', '').replace('消防', '')}`,
type: "pump",
subType: pumpTypes[typeIndex].value,
model: pumpModels[modelIndex].model,
spec: `额定功率: ${pumpModels[modelIndex].power}kW, 额定压力: ${pumpModels[modelIndex].pressure}MPa, 额定流量: ${pumpModels[modelIndex].flow}m³/h`,
installDate: this.getRandomDate(2020, 2023),
status: i <= 14 ? "running" : i <= 15 ? "offline" : "fault",
faultInfo: i > 15 ? "水泵过载" : undefined,
area: pumpTypes[typeIndex].zone,
location: `设备房-${Math.ceil(i / 4)}`,
description: `${pumpTypes[typeIndex].label},用于${pumpTypes[typeIndex].zone}供水系统`,
iconBg: `linear-gradient(135deg, ${pumpIcons[iconIndex]} 0%, ${pumpIcons[(iconIndex + 1) % pumpIcons.length]} 100%)`,
currentPressure: (pumpModels[modelIndex].pressure * (0.85 + Math.random() * 0.2)).toFixed(2),
currentFlow: (pumpModels[modelIndex].flow * (0.8 + Math.random() * 0.3)).toFixed(1),
currentPower: (pumpModels[modelIndex].power * (0.75 + Math.random() * 0.2)).toFixed(1),
setPressure: pumpModels[modelIndex].pressure,
frequency: (35 + Math.random() * 15).toFixed(1),
runtime: `${Math.floor(1000 + Math.random() * 2000)}小时`,
lastUpdate: this.getRecentTime(),
parameters: [
{ label: "出口压力", value: (pumpModels[modelIndex].pressure * (0.85 + Math.random() * 0.2)).toFixed(2), unit: "MPa" },
{ label: "流量", value: (pumpModels[modelIndex].flow * (0.8 + Math.random() * 0.3)).toFixed(1), unit: "m³/h" },
{ label: "功率", value: (pumpModels[modelIndex].power * (0.75 + Math.random() * 0.2)).toFixed(1), unit: "kW" },
{ label: "频率", value: (35 + Math.random() * 15).toFixed(1), unit: "Hz" },
{ label: "电流", value: (pumpModels[modelIndex].power / 380 * (2 + Math.random() * 0.5)).toFixed(2), unit: "A" },
{ label: "电压", value: (380 + Math.random() * 10).toFixed(1), unit: "V" }
]
});
}
// 网关设备
for (let i = 1; i <= 3; i++) {
const iconIndex = i % gatewayIcons.length;
const areaIndex = i % 4;
const areas = ["高区", "中区", "低区", "消防区"];
this.deviceList.push({
code: `GW-00${i}`,
name: `${i}号水泵网关`,
type: "gateway",
subType: "pumpGateway",
model: "LGT-GW1000",
spec: "支持50个节点, Modbus/RS485, 供电: 24V/2A",
installDate: this.getRandomDate(2020, 2021),
status: i <= 2 ? "running" : "offline",
area: areas[areaIndex],
location: `${areas[areaIndex]}-机房`,
description: "水泵系统智能网关,负责数据采集和控制指令下发",
iconBg: `linear-gradient(135deg, #${gatewayIcons[iconIndex]} 0%, #${gatewayIcons[(iconIndex + 1) % gatewayIcons.length]} 100%)`,
runtime: `${Math.floor(2000 + Math.random() * 3000)}小时`,
lastUpdate: this.getRecentTime(),
parameters: [
{ label: "在线设备数", value: `${5 + Math.floor(Math.random() * 6)}`, unit: "台" },
{ label: "网络延迟", value: `${Math.floor(10 + Math.random() * 20)}`, unit: "ms" },
{ label: "信号强度", value: `-40 - ${Math.floor(Math.random() * 20)}`, unit: "dBm" },
{ label: "CPU使用率", value: `${Math.floor(20 + Math.random() * 30)}`, unit: "%" }
]
});
}
// 仪表设备(流量计、压力表)
for (let i = 1; i <= 4; i++) {
const iconIndex = i % meterIcons.length;
const areaIndex = i % 4;
const meterTypes = ["流量计", "压力表", "电表", "液位计"];
const areas = ["高区", "中区", "低区", "消防区"];
this.deviceList.push({
code: `MT-${String(i).padStart(3, '0')}`,
name: `${areas[areaIndex]}${meterTypes[i-1]}-${i}`,
type: "meter",
subType: `pump${meterTypes[i-1]}`,
model: i === 3 ? "DTS666" : "LGT-MT100",
spec: i === 3 ? "三相四线, 3(6)A, 0.5S级, RS485通信" : "精度±0.5%, 4-20mA/RS485",
installDate: this.getRandomDate(2020, 2021),
status: i <= 3 ? "running" : "offline",
area: areas[areaIndex],
location: `${areas[areaIndex]}-设备房`,
description: `${meterTypes[i-1]},用于${areas[areaIndex]}数据监测`,
iconBg: `linear-gradient(135deg, #${meterIcons[iconIndex]} 0%, #${meterIcons[(iconIndex + 1) % meterIcons.length]} 100%)`,
runtime: `${Math.floor(2000 + Math.random() * 3000)}小时`,
lastUpdate: this.getRecentTime(),
parameters: this.getMeterParameters(i)
});
}
// 传感器设备
const sensorTypes = [
{ label: "压力传感器", value: "pressureSensor", spec: "量程: 0-1.6MPa, 精度: ±0.25%FS" },
{ label: "液位传感器", value: "levelSensor", spec: "量程: 0-10m, 精度: ±1%FS" },
{ label: "流量传感器", value: "flowSensor", spec: "量程: 0-500m³/h, 精度: ±0.5%" }
];
for (let i = 1; i <= 3; i++) {
const typeIndex = (i - 1) % sensorTypes.length;
const iconIndex = i % sensorIcons.length;
const areaIndex = i % 4;
const areas = ["高区", "中区", "低区", "消防区"];
this.deviceList.push({
code: `SN-${String(i).padStart(3, '0')}`,
name: `${areas[areaIndex]}${sensorTypes[typeIndex].label}-${i}`,
type: "sensor",
subType: sensorTypes[typeIndex].value,
model: "LGT-SN200",
spec: sensorTypes[typeIndex].spec,
installDate: this.getRandomDate(2020, 2022),
status: i <= 2 ? "running" : "offline",
area: areas[areaIndex],
location: `${areas[areaIndex]}-水箱房`,
description: `${sensorTypes[typeIndex].label},用于${areas[areaIndex]}监测`,
iconBg: `linear-gradient(135deg, #${sensorIcons[iconIndex]} 0%, #${sensorIcons[(iconIndex + 1) % sensorIcons.length]} 100%)`,
runtime: `${Math.floor(1000 + Math.random() * 2000)}小时`,
lastUpdate: this.getRecentTime(),
parameters: this.getSensorParameters(sensorTypes[typeIndex].value)
});
}
},
getSensorParameters(type) {
switch (type) {
case "pressureSensor":
return [
{ label: "当前压力", value: `${(0.3 + Math.random() * 0.5).toFixed(2)}`, unit: "MPa" },
{ label: "信号强度", value: `-45 - ${Math.floor(Math.random() * 10)}`, unit: "dBm" }
];
case "levelSensor":
return [
{ label: "当前液位", value: `${Math.floor(3 + Math.random() * 5)}`, unit: "m" },
{ label: "信号强度", value: `-48 - ${Math.floor(Math.random() * 10)}`, unit: "dBm" }
];
case "flowSensor":
return [
{ label: "瞬时流量", value: `${(100 + Math.random() * 200).toFixed(1)}`, unit: "m³/h" },
{ label: "累计流量", value: `${Math.floor(10000 + Math.random() * 50000)}`, unit: "m³" },
{ label: "信号强度", value: `-50 - ${Math.floor(Math.random() * 10)}`, unit: "dBm" }
];
default:
return [];
}
},
getMeterParameters(index) {
// TODO: 根据区域(area)参数返回不同区域的仪表数据
switch (index) {
case 1: // 流量计
return [
{ label: "瞬时流量", value: `${(120 + Math.random() * 80).toFixed(1)}`, unit: "m³/h" },
{ label: "累计流量", value: `${Math.floor(50000 + Math.random() * 100000)}`, unit: "m³" },
{ label: "信号强度", value: `-42 - ${Math.floor(Math.random() * 12)}`, unit: "dBm" }
];
case 2: // 压力表
return [
{ label: "当前压力", value: `${(0.35 + Math.random() * 0.35).toFixed(2)}`, unit: "MPa" },
{ label: "信号强度", value: `-45 - ${Math.floor(Math.random() * 10)}`, unit: "dBm" }
];
case 3: // 电表
return [
{ label: "总有功电能", value: `${Math.floor(20000 + Math.random() * 80000)}`, unit: "kWh" },
{ label: "总功率", value: `${Math.floor(80 + Math.random() * 150)}`, unit: "kW" },
{ label: "A相电压", value: `${(218 + Math.random() * 5).toFixed(1)}`, unit: "V" },
{ label: "B相电压", value: `${(218 + Math.random() * 5).toFixed(1)}`, unit: "V" },
{ label: "C相电压", value: `${(218 + Math.random() * 5).toFixed(1)}`, unit: "V" }
];
case 4: // 液位计
return [
{ label: "当前液位", value: `${Math.floor(4 + Math.random() * 4)}`, unit: "m" },
{ label: "水箱容积", value: `${Math.floor(80 + Math.random() * 40)}`, unit: "%" },
{ label: "信号强度", value: `-48 - ${Math.floor(Math.random() * 10)}`, unit: "dBm" }
];
default:
return [];
}
},
getRandomDate(startYear, endYear) {
const start = new Date(startYear, 0, 1);
const end = new Date(endYear, 11, 31);
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
return date.toISOString().split('T')[0];
},
getRecentTime() {
const now = new Date();
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
},
getSubDeviceTypes() {
const category = this.deviceCategories[this.activeCategory];
switch (category.value) {
case "pump":
return [
{ label: "高区水泵", value: "highZonePump" },
{ label: "中区水泵", value: "middleZonePump" },
{ label: "低区水泵", value: "lowZonePump" },
{ label: "消防水泵", value: "fireZonePump" }
];
case "gateway":
return [
{ label: "水泵网关", value: "pumpGateway" }
];
case "meter":
return [
{ label: "流量计", value: "pumpFlowMeter" },
{ label: "压力表", value: "pumpPressureMeter" },
{ label: "电表", value: "pumpEnergyMeter" },
{ label: "液位计", value: "pumpLevelMeter" }
];
case "sensor":
return [
{ label: "压力传感器", value: "pressureSensor" },
{ label: "液位传感器", value: "levelSensor" },
{ label: "流量传感器", value: "flowSensor" }
];
default:
return [];
}
},
getStatusText(status) {
const statusMap = {
running: "运行中",
offline: "离线",
fault: "故障"
};
return statusMap[status] || status;
},
getDeviceIconEmoji(type) {
const iconMap = {
pump: "🔧",
gateway: "📡",
meter: "⚡",
sensor: "📊"
};
return iconMap[type] || "📱";
},
getDeviceTypeName(type) {
const typeMap = {
pump: "水泵",
gateway: "网关",
meter: "仪表",
sensor: "传感器"
};
return typeMap[type] || type;
},
getCategoryIcon(value) {
const iconMap = {
all: "📋",
pump: "🔧",
gateway: "📡",
meter: "⚡",
sensor: "📊"
};
return iconMap[value] || "📱";
},
handleCategoryChange(index) {
this.activeCategory = index;
this.currentPage = 1;
this.searchForm.deviceType = "";
},
handleSearch() {
this.currentPage = 1;
},
handleReset() {
this.searchForm = { deviceName: "", deviceStatus: "", deviceType: "" };
this.activeCategory = 0;
this.currentPage = 1;
},
handleExport() {
this.$message.success("设备列表导出成功");
},
handleSizeChange(size) {
this.pageSize = size;
this.currentPage = 1;
},
handleCurrentChange(page) {
this.currentPage = page;
},
viewDeviceDetail(device) {
this.selectedDevice = { ...device };
this.detailDialogVisible = true;
},
closeDetailDialog() {
this.detailDialogVisible = false;
this.selectedDevice = null;
},
saveControl() {
this.$message.success("控制参数保存成功");
this.closeDetailDialog();
}
}
};
</script>
<style lang="scss" scoped>
.device-manage {
padding: 0.16rem;
background: rgba(11, 30, 48, 0.6);
}
.search-filter-area {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0.2rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.08rem;
margin-bottom: 0.16rem;
flex-wrap: wrap;
gap: 0.15rem;
border: 1px solid rgba(255, 255, 255, 0.1);
.search-group {
display: flex;
gap: 0.15rem;
flex-wrap: wrap;
flex: 1;
min-width: 0;
.search-item {
display: flex;
flex-direction: column;
gap: 0.08rem;
min-width: 2rem;
label {
font-size: 0.13rem;
color: #ffffff;
opacity: 0.7;
}
::v-deep .custom-input,
::v-deep .custom-select {
.el-input__wrapper,
.el-select__wrapper {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: none;
&:hover {
border-color: rgba(21, 225, 253, 0.5);
}
.el-input__inner {
color: #ffffff;
}
}
.search-icon {
width: 0.16rem;
height: 0.16rem;
fill: rgba(255, 255, 255, 0.5);
}
}
}
}
.action-group {
display: flex;
gap: 0.1rem;
::v-deep .custom-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
&.el-button--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
}
}
&.el-button--success {
background: linear-gradient(135deg, #91CC75 0%, #66BB6A 100%);
border: none;
.btn-icon {
width: 0.14rem;
height: 0.14rem;
fill: #ffffff;
margin-right: 0.04rem;
}
&:hover {
background: linear-gradient(135deg, #7CB342 0%, #558B2F 100%);
}
}
}
}
}
.device-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.15rem;
margin-bottom: 0.16rem;
.stat-card {
display: flex;
align-items: center;
padding: 0.2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
border-radius: 0.08rem;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
&:hover {
transform: translateY(-0.02rem);
box-shadow: 0 0.08rem 0.2rem rgba(21, 225, 253, 0.15);
}
&.total {
border-left: 0.04rem solid #5470C6;
}
&.running {
border-left: 0.04rem solid #91CC75;
}
&.offline {
border-left: 0.04rem solid #EE6666;
}
&.fault {
border-left: 0.04rem solid #FAC858;
}
.stat-icon {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.15rem;
background: rgba(255, 255, 255, 0.1);
svg {
width: 0.26rem;
height: 0.26rem;
fill: #ffffff;
}
}
.stat-info {
flex: 1;
.stat-value {
font-size: 0.28rem;
color: #15e1fd;
font-weight: bold;
margin-bottom: 0.03rem;
}
.stat-label {
font-size: 0.13rem;
color: #ffffff;
opacity: 0.7;
}
}
}
}
.category-tabs {
display: flex;
gap: 0.1rem;
padding: 0.2rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.08rem;
margin-bottom: 0.16rem;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
.category-tab {
display: flex;
align-items: center;
gap: 0.08rem;
padding: 0.1rem 0.2rem;
font-size: 0.14rem;
color: #ffffff;
opacity: 0.7;
cursor: pointer;
border-radius: 0.06rem;
transition: all 0.3s;
white-space: nowrap;
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 1;
}
.tab-icon {
font-size: 0.16rem;
}
.tab-label {
flex: 1;
}
.tab-count {
font-size: 0.12rem;
opacity: 0.8;
}
}
}
.device-list-container {
background: rgba(255, 255, 255, 0.03);
border-radius: 0.08rem;
padding: 0.2rem;
border: 1px solid rgba(255, 255, 255, 0.05);
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.2rem;
.list-title {
font-size: 0.16rem;
color: #ffffff;
font-weight: bold;
padding-left: 0.12rem;
border-left: 0.04rem solid #15e1fd;
}
.list-info {
font-size: 0.14rem;
color: #ffffff;
opacity: 0.7;
}
}
.device-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(3.5rem, 1fr));
gap: 0.15rem;
margin-bottom: 0.2rem;
.device-card {
padding: 0.2rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.08rem;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(255, 255, 255, 0.05);
&:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-0.03rem);
box-shadow: 0 0.1rem 0.25rem rgba(0, 0, 0, 0.2);
}
&.running {
border-left: 3px solid #91CC75;
}
&.offline {
border-left: 3px solid #EE6666;
}
&.fault {
border-left: 3px solid #FAC858;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 0.15rem;
.device-icon {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.12rem;
font-size: 0.24rem;
color: #ffffff;
}
.device-header-info {
flex: 1;
.device-name {
font-size: 0.15rem;
color: #ffffff;
font-weight: bold;
margin-bottom: 0.04rem;
}
.device-code {
font-size: 0.12rem;
color: #ffffff;
opacity: 0.6;
}
}
.status-badge {
padding: 0.04rem 0.12rem;
border-radius: 0.04rem;
font-size: 0.11rem;
&.running {
background: rgba(145, 204, 117, 0.2);
color: #91CC75;
border: 1px solid rgba(145, 204, 117, 0.3);
}
&.offline {
background: rgba(238, 102, 102, 0.2);
color: #EE6666;
border: 1px solid rgba(238, 102, 102, 0.3);
}
&.fault {
background: rgba(250, 200, 88, 0.2);
color: #FAC858;
border: 1px solid rgba(250, 200, 88, 0.3);
}
}
}
.card-body {
margin-bottom: 0.15rem;
.device-info-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.08rem;
font-size: 0.13rem;
.info-label {
color: #ffffff;
opacity: 0.6;
}
.info-value {
color: #ffffff;
opacity: 0.9;
&.highlight {
color: #15e1fd;
font-weight: bold;
}
}
}
}
.card-footer {
.device-params {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
.param-item {
padding: 0.06rem 0.1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.04rem;
font-size: 0.11rem;
color: #ffffff;
opacity: 0.8;
.param-label {
opacity: 0.7;
}
.param-value {
color: #15e1fd;
font-weight: bold;
margin: 0 0.04rem;
}
}
.param-more {
padding: 0.06rem 0.1rem;
background: rgba(21, 225, 253, 0.1);
border-radius: 0.04rem;
font-size: 0.11rem;
color: #15e1fd;
opacity: 0.8;
}
}
}
}
}
.pagination-container {
display: flex;
justify-content: center;
padding-top: 0.2rem;
::v-deep .custom-pagination {
.el-pagination__total,
.el-pagination__jump,
.el-pager li {
color: #ffffff;
}
.el-pager li {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
background: rgba(255, 255, 255, 0.1);
}
&.is-active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: transparent;
}
}
.btn-prev,
.btn-next {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}
}
}
::v-deep .custom-dialog {
.el-dialog {
background: linear-gradient(135deg, rgba(11, 30, 48, 0.98) 0%, rgba(20, 45, 65, 0.98) 100%);
border: 1px solid rgba(21, 225, 253, 0.3);
box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.4);
}
.el-dialog__header {
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.el-dialog__title {
color: #ffffff;
}
.el-dialog__headerbtn .el-dialog__close {
color: #ffffff;
}
.detail-content {
.detail-section {
margin-bottom: 0.3rem;
.section-title {
font-size: 0.15rem;
color: #15e1fd;
font-weight: bold;
margin-bottom: 0.15rem;
padding-left: 0.12rem;
border-left: 0.04rem solid #15e1fd;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.15rem;
.info-item {
display: flex;
justify-content: space-between;
padding: 0.12rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 0.06rem;
&.full-width {
grid-column: 1 / -1;
}
.item-label {
font-size: 0.14rem;
color: #ffffff;
opacity: 0.7;
}
.item-value {
font-size: 0.14rem;
color: #ffffff;
&.status-badge {
padding: 0.04rem 0.12rem;
border-radius: 0.04rem;
font-size: 0.12rem;
&.running {
background: rgba(145, 204, 117, 0.2);
color: #91CC75;
}
&.offline {
background: rgba(238, 102, 102, 0.2);
color: #EE6666;
}
&.fault {
background: rgba(250, 200, 88, 0.2);
color: #FAC858;
}
}
}
}
}
.params-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(1.5rem, 1fr));
gap: 0.15rem;
.param-card {
padding: 0.2rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.06rem;
text-align: center;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-0.02rem);
}
.param-label {
font-size: 0.12rem;
color: #ffffff;
opacity: 0.6;
margin-bottom: 0.08rem;
}
.param-value {
font-size: 0.2rem;
color: #15e1fd;
font-weight: bold;
margin-bottom: 0.04rem;
}
.param-unit {
font-size: 0.11rem;
color: #ffffff;
opacity: 0.5;
}
}
}
.status-info {
.status-item {
display: flex;
justify-content: space-between;
padding: 0.15rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 0.06rem;
margin-bottom: 0.1rem;
.status-label {
font-size: 0.14rem;
color: #ffffff;
opacity: 0.7;
}
.status-text {
font-size: 0.14rem;
color: #ffffff;
&.running {
color: #91CC75;
}
&.offline {
color: #EE6666;
}
&.fault {
color: #FAC858;
}
}
}
}
.control-grid {
.control-item {
display: flex;
align-items: center;
gap: 0.15rem;
padding: 0.15rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 0.06rem;
margin-bottom: 0.1rem;
.control-label {
min-width: 1rem;
font-size: 0.14rem;
color: #ffffff;
opacity: 0.7;
}
.el-slider {
flex: 1;
}
.control-value {
min-width: 0.8rem;
text-align: right;
font-size: 0.14rem;
color: #15e1fd;
font-weight: bold;
}
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 0.1rem;
}
}
::v-deep .el-slider {
.el-slider__runway {
background: rgba(255, 255, 255, 0.2);
}
.el-slider__bar {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.el-slider__button {
border-color: #667eea;
}
}
@media (max-width: 1485px) {
.device-stats {
grid-template-columns: repeat(2, 1fr);
}
.device-grid {
grid-template-columns: repeat(auto-fill, minmax(3rem, 1fr));
}
}
@media (max-width: 768px) {
.search-filter-area {
flex-direction: column;
.search-group {
width: 100%;
flex-direction: column;
.search-item {
width: 100%;
::v-deep .custom-input,
::v-deep .custom-select {
width: 100%;
}
}
}
.action-group {
width: 100%;
justify-content: flex-end;
}
}
.device-stats {
grid-template-columns: repeat(2, 1fr);
}
.category-tabs {
flex-wrap: wrap;
}
.detail-content {
.info-grid {
grid-template-columns: 1fr;
}
}
}
</style>