|
|
@@ -1,771 +1,514 @@
|
|
|
-<!--终端设备远程测控-->
|
|
|
+<!-- 终端设备远程测控 -->
|
|
|
<template>
|
|
|
- <div class="prototype-container">
|
|
|
-
|
|
|
-
|
|
|
- <div class="main-layout">
|
|
|
- <!-- 左侧:区域控制器配置 -->
|
|
|
- <div class="left-panel">
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 🗺️ 区域控制器配置
|
|
|
- <button @click="openRegionModal" class="btn-outline btn-sm">+ 新增区域</button>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="region-list">
|
|
|
- <div
|
|
|
- v-for="region in regions"
|
|
|
- :key="region.id"
|
|
|
- class="region-item"
|
|
|
- :class="{ active: selectedRegionId === region.id }"
|
|
|
- @click="selectRegion(region.id)"
|
|
|
- >
|
|
|
- <div class="region-name">{{ region.name }}</div>
|
|
|
- <div class="region-status">
|
|
|
- <span class="status-dot" :class="region.status === 'auto' ? 'dot-auto' : 'dot-manual'"></span>
|
|
|
- {{ region.status === 'auto' ? '自动控制' : '手动控制' }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div class="app-container remote-page" v-loading="loading">
|
|
|
+ <div class="hero-panel">
|
|
|
+ <div>
|
|
|
+ <div class="hero-title">终端设备远程测控</div>
|
|
|
+ <div class="hero-subtitle">基于区域控制、手动控制和事故联动预案,模拟值班中心对终端设备的远程控制管理</div>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" icon="Refresh" @click="loadDashboard">刷新区域</el-button>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 区域控制器参数设置(仅自动模式显示) -->
|
|
|
- <div class="card" v-if="currentRegion && currentRegion.controlMode === 'auto'">
|
|
|
- <div class="card-header">⚙️ 自动控制参数设置</div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="param-group">
|
|
|
- <label class="param-label">上限阈值</label>
|
|
|
- <input type="number" v-model="autoParams.upperThreshold" class="param-input" step="0.1">
|
|
|
- <span class="param-unit">{{ currentRegion.unit || '' }}</span>
|
|
|
- </div>
|
|
|
- <div class="param-group">
|
|
|
- <label class="param-label">下限阈值</label>
|
|
|
- <input type="number" v-model="autoParams.lowerThreshold" class="param-input" step="0.1">
|
|
|
- <span class="param-unit">{{ currentRegion.unit || '' }}</span>
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :xs="24" :lg="6">
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header-row">
|
|
|
+ <div class="card-title">区域控制器</div>
|
|
|
+ <el-tag>{{ regions.length }} 个区域</el-tag>
|
|
|
</div>
|
|
|
- <div class="param-group">
|
|
|
- <label class="param-label">目标值</label>
|
|
|
- <input type="number" v-model="autoParams.targetValue" class="param-input" step="0.1">
|
|
|
- <span class="param-unit">{{ currentRegion.unit || '' }}</span>
|
|
|
- </div>
|
|
|
- <div class="param-group">
|
|
|
- <label class="param-label">采样周期(秒)</label>
|
|
|
- <input type="number" v-model="autoParams.samplePeriod" class="param-input" step="1">
|
|
|
- <span class="param-unit">秒</span>
|
|
|
- </div>
|
|
|
- <button @click="saveAutoParams" class="btn-primary btn-sm" style="width:100%;">保存参数</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 设备运行方案预设 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 📋 设备运行方案预设
|
|
|
- <button @click="openSchemeModal" class="btn-outline btn-sm">+ 新增方案</button>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="scheme-list">
|
|
|
- <div
|
|
|
- v-for="scheme in operationSchemes"
|
|
|
- :key="scheme.id"
|
|
|
- class="scheme-item"
|
|
|
- :class="{ active: selectedSchemeId === scheme.id }"
|
|
|
- @click="selectScheme(scheme.id)"
|
|
|
- >
|
|
|
- <div class="scheme-name">{{ scheme.name }}</div>
|
|
|
- <div class="scheme-desc">{{ scheme.description }}</div>
|
|
|
- <div class="scheme-actions">
|
|
|
- <button @click.stop="applyScheme(scheme)" class="btn-success btn-sm">应用</button>
|
|
|
- <button @click.stop="editScheme(scheme)" class="btn-outline btn-sm">编辑</button>
|
|
|
- <button @click.stop="deleteScheme(scheme)" class="btn-danger-outline btn-sm">删除</button>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="region-list">
|
|
|
+ <div
|
|
|
+ v-for="item in regions"
|
|
|
+ :key="item.id"
|
|
|
+ class="region-item"
|
|
|
+ :class="{ active: selectedRegionId === item.id }"
|
|
|
+ @click="selectRegion(item.id)"
|
|
|
+ >
|
|
|
+ <div class="region-name">{{ item.name }}</div>
|
|
|
+ <div class="region-meta">{{ item.location }}</div>
|
|
|
+ <div class="region-status">
|
|
|
+ <span class="status-dot" :class="item.controlMode === 'auto' ? 'dot-auto' : 'dot-manual'"></span>
|
|
|
+ {{ item.controlMode === "auto" ? "自动控制" : "手动控制" }}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 中间:设备列表与控制 -->
|
|
|
- <div class="middle-panel">
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 📟 区域设备列表
|
|
|
- <span class="region-hint">{{ currentRegion?.name || '请选择区域' }}</span>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="device-control-list">
|
|
|
- <div v-for="device in currentDevices" :key="device.id" class="device-control-item">
|
|
|
- <div class="device-basic">
|
|
|
- <div class="device-name">{{ device.name }}</div>
|
|
|
- <div class="device-meta">{{ device.type }} | ID: {{ device.deviceId }}</div>
|
|
|
- <div class="device-status">
|
|
|
- <span class="status-dot" :class="device.status === 'running' ? 'dot-running' : 'dot-stopped'"></span>
|
|
|
- {{ device.status === 'running' ? '运行中' : '已停止' }}
|
|
|
- </div>
|
|
|
- <div class="device-value" v-if="device.currentValue !== undefined">
|
|
|
- 当前值: {{ device.currentValue }} {{ device.unit }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="device-control">
|
|
|
- <button
|
|
|
- @click="sendControlCommand(device, 'start')"
|
|
|
- class="btn-success btn-sm"
|
|
|
- :disabled="device.status === 'running'"
|
|
|
- >▶ 启动</button>
|
|
|
- <button
|
|
|
- @click="sendControlCommand(device, 'stop')"
|
|
|
- class="btn-danger btn-sm"
|
|
|
- :disabled="device.status === 'stopped'"
|
|
|
- >⏹️ 停止</button>
|
|
|
- <button
|
|
|
- @click="openSetValueModal(device)"
|
|
|
- class="btn-outline btn-sm"
|
|
|
- >⚙️ 设置参数</button>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card v-if="currentRegion" shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">自动控制参数</div>
|
|
|
+ </template>
|
|
|
+ <el-form label-width="80px" size="small">
|
|
|
+ <el-form-item label="上限阈值">
|
|
|
+ <el-input-number v-model="autoParams.upperThreshold" :step="0.1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="下限阈值">
|
|
|
+ <el-input-number v-model="autoParams.lowerThreshold" :step="0.1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="目标值">
|
|
|
+ <el-input-number v-model="autoParams.targetValue" :step="0.1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="采样周期">
|
|
|
+ <el-input-number v-model="autoParams.samplePeriod" :step="1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-button type="primary" style="width: 100%" @click="saveAutoParams">保存参数</el-button>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :xs="24" :lg="11">
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header-row">
|
|
|
+ <div>
|
|
|
+ <div class="card-title">中控远程手动控制</div>
|
|
|
+ <div class="card-subtitle">{{ currentRegion?.name || "请选择区域" }}</div>
|
|
|
</div>
|
|
|
+ <el-switch
|
|
|
+ v-model="manualMode"
|
|
|
+ active-text="手动模式"
|
|
|
+ inactive-text="自动模式"
|
|
|
+ @change="toggleControlMode"
|
|
|
+ />
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 中控手动控制面板 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 🎛️ 中控远程手动控制
|
|
|
- <label class="mode-switch">
|
|
|
- <input type="checkbox" v-model="isManualMode" @change="toggleControlMode">
|
|
|
- <span class="mode-slider"></span>
|
|
|
- <span class="mode-label">{{ isManualMode ? '手动模式' : '自动模式' }}</span>
|
|
|
- </label>
|
|
|
- </div>
|
|
|
- <div class="card-body" v-if="isManualMode">
|
|
|
- <div class="manual-control-panel">
|
|
|
- <div class="control-log" ref="controlLog">
|
|
|
- <div v-for="log in controlLogs.slice(-8)" :key="log.id" class="log-item">
|
|
|
- <span class="log-time">{{ log.time }}</span>
|
|
|
- <span class="log-action">{{ log.action }}</span>
|
|
|
- <span class="log-target">{{ log.target }}</span>
|
|
|
- <span class="log-result" :class="log.result">{{ log.result === 'success' ? '✓ 成功' : '✗ 失败' }}</span>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-if="currentDevices.length" class="device-list">
|
|
|
+ <div v-for="item in currentDevices" :key="item.id" class="device-item">
|
|
|
+ <div class="device-main">
|
|
|
+ <div class="device-name">{{ item.name }}</div>
|
|
|
+ <div class="device-meta">{{ item.type }} · {{ item.deviceId }}</div>
|
|
|
+ <div class="device-value">目标值:{{ item.currentValue }} {{ item.unit }}</div>
|
|
|
</div>
|
|
|
- <div class="quick-commands">
|
|
|
- <div class="quick-title">快捷指令</div>
|
|
|
- <div class="quick-buttons">
|
|
|
- <button @click="quickCommand('全部启动')" class="btn-primary btn-sm">全部启动</button>
|
|
|
- <button @click="quickCommand('全部停止')" class="btn-danger btn-sm">全部停止</button>
|
|
|
- <button @click="quickCommand('紧急停止')" class="btn-warning btn-sm">紧急停止</button>
|
|
|
- </div>
|
|
|
+ <div class="device-actions">
|
|
|
+ <el-tag :type="item.status === 'running' ? 'success' : 'info'">
|
|
|
+ {{ item.status === "running" ? "运行中" : "已停止" }}
|
|
|
+ </el-tag>
|
|
|
+ <el-button type="success" plain size="small" :disabled="!manualMode || item.status === 'running'" @click="sendControlCommand(item, 'start')">
|
|
|
+ 启动
|
|
|
+ </el-button>
|
|
|
+ <el-button type="danger" plain size="small" :disabled="!manualMode || item.status === 'stopped'" @click="sendControlCommand(item, 'stop')">
|
|
|
+ 停止
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" :disabled="!manualMode" @click="openSetValueDialog(item)">设置参数</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="card-body" v-else>
|
|
|
- <div class="auto-mode-hint">
|
|
|
- <span class="hint-icon">🤖</span>
|
|
|
- <span>当前为区域自动控制模式,设备根据预设阈值自动运行</span>
|
|
|
- <span class="hint-sub">切换到手动模式可进行远程单设备控制</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <el-empty v-else description="当前区域暂无可控设备" />
|
|
|
|
|
|
- <!-- 右侧:事故联动控制预案 -->
|
|
|
- <div class="right-panel">
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 🚨 事故联动控制预案
|
|
|
- <button @click="openIncidentModal" class="btn-primary btn-sm">+ 新增预案</button>
|
|
|
+ <div class="quick-actions">
|
|
|
+ <el-button type="primary" plain :disabled="!manualMode" @click="quickCommand('全部启动')">全部启动</el-button>
|
|
|
+ <el-button type="danger" plain :disabled="!manualMode" @click="quickCommand('全部停止')">全部停止</el-button>
|
|
|
+ <el-button type="warning" plain :disabled="!manualMode" @click="quickCommand('紧急停止')">紧急停止</el-button>
|
|
|
</div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="incident-list">
|
|
|
- <div v-for="incident in incidentPlans" :key="incident.id" class="incident-item" :class="incident.type">
|
|
|
- <div class="incident-header">
|
|
|
- <span class="incident-name">{{ incident.name }}</span>
|
|
|
- <span class="incident-type">{{ getIncidentTypeText(incident.type) }}</span>
|
|
|
- </div>
|
|
|
- <div class="incident-desc">{{ incident.description }}</div>
|
|
|
- <div class="incident-devices">
|
|
|
- <span class="devices-label">联动设备:</span>
|
|
|
- <span class="devices-list">{{ incident.devices.map(d => d.name).join(', ') }}</span>
|
|
|
- </div>
|
|
|
- <div class="incident-actions">
|
|
|
- <button @click="triggerIncident(incident)" class="btn-warning btn-sm">▶ 快速启动</button>
|
|
|
- <button @click="editIncident(incident)" class="btn-outline btn-sm">编辑</button>
|
|
|
- <button @click="deleteIncident(incident)" class="btn-danger-outline btn-sm">删除</button>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">设备运行方案</div>
|
|
|
+ </template>
|
|
|
+ <div class="scheme-list">
|
|
|
+ <div v-for="item in operationSchemes" :key="item.id" class="scheme-item">
|
|
|
+ <div class="scheme-name">{{ item.name }}</div>
|
|
|
+ <div class="scheme-desc">{{ item.description }}</div>
|
|
|
+ <div class="scheme-footer">
|
|
|
+ <span>设备数量:{{ item.deviceIds.length }}</span>
|
|
|
+ <el-button type="primary" link @click="applyScheme(item)">应用方案</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 预案执行记录 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">📋 预案执行记录</div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="execution-list">
|
|
|
- <div v-for="record in executionRecords.slice(-5)" :key="record.id" class="execution-item">
|
|
|
- <div class="exec-time">{{ record.time }}</div>
|
|
|
- <div class="exec-name">{{ record.incidentName }}</div>
|
|
|
- <div class="exec-status" :class="record.status">{{ record.status === 'success' ? '执行成功' : '执行中' }}</div>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :xs="24" :lg="7">
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">事故联动控制预案</div>
|
|
|
+ </template>
|
|
|
+ <div class="plan-list">
|
|
|
+ <div v-for="item in incidentPlans" :key="item.id" class="plan-item">
|
|
|
+ <div class="plan-name">{{ item.name }}</div>
|
|
|
+ <div class="plan-desc">{{ item.description }}</div>
|
|
|
+ <div class="plan-devices">联动设备:{{ item.devices.map((d) => d.name).join("、") }}</div>
|
|
|
+ <el-button type="warning" plain size="small" @click="triggerIncident(item)">快速启动</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 新增区域模态框 -->
|
|
|
- <div v-if="regionModalVisible" class="modal-overlay" @click.self="closeRegionModal">
|
|
|
- <div class="modal-content">
|
|
|
- <h3>{{ isEditRegion ? '编辑区域' : '新增区域' }}</h3>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">区域名称</label>
|
|
|
- <input v-model="regionForm.name" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">区域位置</label>
|
|
|
- <input v-model="regionForm.location" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">控制模式</label>
|
|
|
- <select v-model="regionForm.controlMode" class="form-select">
|
|
|
- <option value="auto">自动控制</option>
|
|
|
- <option value="manual">手动控制</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">监测类型/单位</label>
|
|
|
- <input v-model="regionForm.unit" class="form-input" placeholder="如 MPa、m³/h">
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeRegionModal" class="btn-outline">取消</button>
|
|
|
- <button @click="saveRegion" class="btn-primary">保存</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 新增/编辑方案模态框 -->
|
|
|
- <div v-if="schemeModalVisible" class="modal-overlay" @click.self="closeSchemeModal">
|
|
|
- <div class="modal-content" style="width:500px;">
|
|
|
- <h3>{{ isEditScheme ? '编辑方案' : '新增运行方案' }}</h3>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">方案名称</label>
|
|
|
- <input v-model="schemeForm.name" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">方案描述</label>
|
|
|
- <input v-model="schemeForm.description" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">控制设备</label>
|
|
|
- <div class="device-check-list">
|
|
|
- <label v-for="device in allDevices" :key="device.id" class="device-check">
|
|
|
- <input type="checkbox" :value="device.id" v-model="schemeForm.deviceIds">
|
|
|
- {{ device.name }} ({{ device.type }})
|
|
|
- </label>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">执行动作</label>
|
|
|
- <select v-model="schemeForm.action" class="form-select">
|
|
|
- <option value="start">全部启动</option>
|
|
|
- <option value="stop">全部停止</option>
|
|
|
- <option value="custom">自定义</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeSchemeModal" class="btn-outline">取消</button>
|
|
|
- <button @click="saveScheme" class="btn-primary">保存</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 新增/编辑事故预案模态框 -->
|
|
|
- <div v-if="incidentModalVisible" class="modal-overlay" @click.self="closeIncidentModal">
|
|
|
- <div class="modal-content" style="width:550px;">
|
|
|
- <h3>{{ isEditIncident ? '编辑预案' : '新增事故联动预案' }}</h3>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">预案名称</label>
|
|
|
- <input v-model="incidentForm.name" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">预案类型</label>
|
|
|
- <select v-model="incidentForm.type" class="form-select">
|
|
|
- <option value="leak">泄漏事故</option>
|
|
|
- <option value="fire">火灾事故</option>
|
|
|
- <option value="pressure">压力异常</option>
|
|
|
- <option value="maintenance">养护维护</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">预案描述</label>
|
|
|
- <textarea v-model="incidentForm.description" rows="2" class="form-textarea"></textarea>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">联动设备</label>
|
|
|
- <div class="device-check-list">
|
|
|
- <label v-for="device in allDevices" :key="device.id" class="device-check">
|
|
|
- <input type="checkbox" :value="device.id" v-model="incidentForm.deviceIds">
|
|
|
- {{ device.name }}
|
|
|
- </label>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="panel-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">控制日志</div>
|
|
|
+ </template>
|
|
|
+ <div class="log-list">
|
|
|
+ <div v-for="item in controlLogs" :key="item.id" class="log-item">
|
|
|
+ <div class="log-time">{{ item.time }}</div>
|
|
|
+ <div class="log-action">{{ item.action }}</div>
|
|
|
+ <div class="log-target">{{ item.target }}</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-dialog title="设置设备参数" v-model="setValueOpen" width="420px" append-to-body>
|
|
|
+ <el-form label-width="90px">
|
|
|
+ <el-form-item label="设备名称">
|
|
|
+ <span>{{ currentSetDevice?.name || "-" }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="目标值">
|
|
|
+ <el-input-number v-model="setValue" :step="0.1" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitSetValue">下发指令</el-button>
|
|
|
+ <el-button @click="setValueOpen = false">取 消</el-button>
|
|
|
</div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">联动动作</label>
|
|
|
- <select v-model="incidentForm.action" class="form-select">
|
|
|
- <option value="all_start">启动所有联动设备</option>
|
|
|
- <option value="all_stop">停止所有联动设备</option>
|
|
|
- <option value="emergency_stop">紧急停止+报警</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeIncidentModal" class="btn-outline">取消</button>
|
|
|
- <button @click="saveIncident" class="btn-primary">保存</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 设置设备参数模态框 -->
|
|
|
- <div v-if="setValueModalVisible" class="modal-overlay" @click.self="closeSetValueModal">
|
|
|
- <div class="modal-content" style="width:400px;">
|
|
|
- <h3>设置设备参数</h3>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">设备: {{ currentSetDevice?.name }}</label>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">目标值 ({{ currentSetDevice?.unit }})</label>
|
|
|
- <input type="number" v-model="setValue" class="form-input" step="0.1">
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeSetValueModal" class="btn-outline">取消</button>
|
|
|
- <button @click="submitSetValue" class="btn-primary">下发指令</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed } from 'vue'
|
|
|
-
|
|
|
-// ==================== 区域数据 ====================
|
|
|
-const regions = ref([
|
|
|
- { id: 1, name: "城东泵站区域", location: "城东区", controlMode: "auto", unit: "MPa", status: "auto", devices: [1, 2] },
|
|
|
- { id: 2, name: "城南调压站区域", location: "城南新区", controlMode: "manual", unit: "kPa", status: "manual", devices: [3, 4] },
|
|
|
- { id: 3, name: "城西污水处理区域", location: "城西工业区", controlMode: "auto", unit: "m³/h", status: "auto", devices: [5, 6] }
|
|
|
-])
|
|
|
-
|
|
|
-const selectedRegionId = ref(1)
|
|
|
-const currentRegion = computed(() => regions.value.find(r => r.id === selectedRegionId.value))
|
|
|
-
|
|
|
-// 区域控制器自动参数
|
|
|
-const autoParams = ref({
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
+import { getMonitorPointDetail, listMonitorPointAll, listPipeNetworkAll } from "@/api/pipeNetwork/pdeviceInform";
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const networkOptions = ref([]);
|
|
|
+const regions = ref([]);
|
|
|
+const selectedRegionId = ref();
|
|
|
+const manualMode = ref(false);
|
|
|
+const controlLogs = ref([]);
|
|
|
+const setValueOpen = ref(false);
|
|
|
+const currentSetDevice = ref();
|
|
|
+const setValue = ref(0);
|
|
|
+
|
|
|
+const autoParams = reactive({
|
|
|
upperThreshold: 1.2,
|
|
|
lowerThreshold: 0.3,
|
|
|
targetValue: 0.8,
|
|
|
samplePeriod: 10
|
|
|
-})
|
|
|
-
|
|
|
-// ==================== 设备数据 ====================
|
|
|
-const allDevices = ref([
|
|
|
- { id: 1, name: "进水提升泵", deviceId: "PMP-001", type: "水泵", status: "running", regionId: 1, currentValue: 0.85, unit: "MPa" },
|
|
|
- { id: 2, name: "出水调节阀", deviceId: "VLV-002", type: "阀门", status: "stopped", regionId: 1, currentValue: 0, unit: "%" },
|
|
|
- { id: 3, name: "天然气压缩机", deviceId: "CMP-003", type: "压缩机", status: "running", regionId: 2, currentValue: 450, unit: "kPa" },
|
|
|
- { id: 4, name: "放散阀", deviceId: "VLV-004", type: "阀门", status: "stopped", regionId: 2, currentValue: 0, unit: "%" },
|
|
|
- { id: 5, name: "搅拌器", deviceId: "MIX-005", type: "搅拌设备", status: "running", regionId: 3, currentValue: 120, unit: "m³/h" },
|
|
|
- { id: 6, name: "排泥泵", deviceId: "PMP-006", type: "水泵", status: "stopped", regionId: 3, currentValue: 0, unit: "m³/h" }
|
|
|
-])
|
|
|
-
|
|
|
-// 当前区域设备
|
|
|
-const currentDevices = computed(() => {
|
|
|
- if (!currentRegion.value) return []
|
|
|
- return allDevices.value.filter(d => d.regionId === currentRegion.value.id)
|
|
|
-})
|
|
|
+});
|
|
|
|
|
|
-// ==================== 运行方案 ====================
|
|
|
const operationSchemes = ref([
|
|
|
- { id: 1, name: "高峰供水方案", description: "早晚高峰时段,启动全部水泵", deviceIds: [1, 2], action: "start" },
|
|
|
- { id: 2, name: "低谷节能方案", description: "低峰时段,停止部分设备", deviceIds: [1, 2], action: "stop" },
|
|
|
- { id: 3, name: "夜间模式", description: "夜间低功耗运行", deviceIds: [1], action: "start" }
|
|
|
-])
|
|
|
+ { id: 1, name: "高峰供给方案", description: "高峰时段优先启用区域内主要设备", deviceIds: [] },
|
|
|
+ { id: 2, name: "低谷节能方案", description: "低谷时段保留关键设备,其余设备停机待命", deviceIds: [] },
|
|
|
+ { id: 3, name: "夜间值守方案", description: "仅维持核心设备运行,降低夜间能耗", deviceIds: [] }
|
|
|
+]);
|
|
|
|
|
|
-const selectedSchemeId = ref(null)
|
|
|
-
|
|
|
-// ==================== 事故预案 ====================
|
|
|
const incidentPlans = ref([
|
|
|
- {
|
|
|
- id: 1, name: "燃气泄漏应急联动", type: "leak", description: "检测到燃气泄漏时,自动关闭阀门、启动排风",
|
|
|
- devices: [{ id: 3, name: "天然气压缩机" }, { id: 4, name: "放散阀" }],
|
|
|
- deviceIds: [3, 4], action: "emergency_stop"
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2, name: "压力超标联动", type: "pressure", description: "压力超标时自动调节阀门",
|
|
|
- devices: [{ id: 1, name: "进水提升泵" }, { id: 2, name: "出水调节阀" }],
|
|
|
- deviceIds: [1, 2], action: "all_stop"
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3, name: "年度养护维护方案", type: "maintenance", description: "定期维护时停止相关设备",
|
|
|
- devices: [{ id: 5, name: "搅拌器" }, { id: 6, name: "排泥泵" }],
|
|
|
- deviceIds: [5, 6], action: "all_stop"
|
|
|
- }
|
|
|
-])
|
|
|
-
|
|
|
-// 执行记录
|
|
|
-const executionRecords = ref([
|
|
|
- { id: 1, time: "2025-05-08 10:23:15", incidentName: "燃气泄漏应急联动", status: "success" },
|
|
|
- { id: 2, time: "2025-05-08 08:45:22", incidentName: "压力超标联动", status: "success" }
|
|
|
-])
|
|
|
-
|
|
|
-// ==================== 控制模式 ====================
|
|
|
-const isManualMode = ref(false)
|
|
|
-const controlMode = computed(() => isManualMode.value ? 'manual' : 'auto')
|
|
|
-
|
|
|
-// 控制日志
|
|
|
-const controlLogs = ref([
|
|
|
- { id: 1, time: "14:23:15", action: "启动设备", target: "进水提升泵", result: "success" },
|
|
|
- { id: 2, time: "14:20:30", action: "停止设备", target: "出水调节阀", result: "success" }
|
|
|
-])
|
|
|
-
|
|
|
-// ==================== 模态框状态 ====================
|
|
|
-const regionModalVisible = ref(false)
|
|
|
-const schemeModalVisible = ref(false)
|
|
|
-const incidentModalVisible = ref(false)
|
|
|
-const setValueModalVisible = ref(false)
|
|
|
-
|
|
|
-const isEditRegion = ref(false)
|
|
|
-const isEditScheme = ref(false)
|
|
|
-const isEditIncident = ref(false)
|
|
|
-
|
|
|
-const regionForm = ref({ name: '', location: '', controlMode: 'auto', unit: '' })
|
|
|
-const schemeForm = ref({ name: '', description: '', deviceIds: [], action: 'start' })
|
|
|
-const incidentForm = ref({ name: '', type: 'leak', description: '', deviceIds: [], action: 'all_stop' })
|
|
|
-
|
|
|
-const currentSetDevice = ref(null)
|
|
|
-const setValue = ref(0)
|
|
|
-
|
|
|
-// ==================== 方法 ====================
|
|
|
-// 选择区域
|
|
|
-const selectRegion = (id) => {
|
|
|
- selectedRegionId.value = id
|
|
|
- // 加载区域自动参数(模拟)
|
|
|
- if (currentRegion.value && currentRegion.value.id === 1) {
|
|
|
- autoParams.value = { upperThreshold: 1.2, lowerThreshold: 0.3, targetValue: 0.8, samplePeriod: 10 }
|
|
|
- }
|
|
|
+ { id: 1, name: "泄漏事故联动", description: "快速停机并通知值班中心介入处理", devices: [] },
|
|
|
+ { id: 2, name: "压力异常联动", description: "立即停止相关设备,降低区域运行风险", devices: [] },
|
|
|
+ { id: 3, name: "养护维护预案", description: "进入维护模式,按计划逐项停机确认", devices: [] }
|
|
|
+]);
|
|
|
+
|
|
|
+const currentRegion = computed(() => {
|
|
|
+ return regions.value.find((item) => item.id === selectedRegionId.value);
|
|
|
+});
|
|
|
+
|
|
|
+const currentDevices = computed(() => {
|
|
|
+ return currentRegion.value?.devices || [];
|
|
|
+});
|
|
|
+
|
|
|
+function addLog(action, target) {
|
|
|
+ controlLogs.value.unshift({
|
|
|
+ id: Date.now() + Math.random(),
|
|
|
+ time: new Date().toLocaleTimeString(),
|
|
|
+ action,
|
|
|
+ target
|
|
|
+ });
|
|
|
+ controlLogs.value = controlLogs.value.slice(0, 10);
|
|
|
}
|
|
|
|
|
|
-// 保存自动参数
|
|
|
-const saveAutoParams = () => {
|
|
|
- alert(`自动控制参数已保存\n上限:${autoParams.value.upperThreshold} 下限:${autoParams.value.lowerThreshold}\n目标值:${autoParams.value.targetValue}`)
|
|
|
- addControlLog('系统', '更新自动参数', `${currentRegion.value?.name}`, 'success')
|
|
|
-}
|
|
|
-
|
|
|
-// 方案相关
|
|
|
-const selectScheme = (id) => { selectedSchemeId.value = id }
|
|
|
-const applyScheme = (scheme) => {
|
|
|
- const devices = allDevices.value.filter(d => scheme.deviceIds.includes(d.id))
|
|
|
- devices.forEach(device => {
|
|
|
- if (scheme.action === 'start') device.status = 'running'
|
|
|
- else if (scheme.action === 'stop') device.status = 'stopped'
|
|
|
- })
|
|
|
- addControlLog('系统', `应用方案:${scheme.name}`, `${devices.length}台设备`, 'success')
|
|
|
- alert(`已应用方案"${scheme.name}",${devices.length}台设备已响应`)
|
|
|
-}
|
|
|
-
|
|
|
-const openSchemeModal = () => { isEditScheme.value = false; schemeForm.value = { name: '', description: '', deviceIds: [], action: 'start' }; schemeModalVisible.value = true }
|
|
|
-const editScheme = (scheme) => { isEditScheme.value = true; schemeForm.value = { ...scheme, deviceIds: [...scheme.deviceIds] }; schemeModalVisible.value = true }
|
|
|
-const saveScheme = () => {
|
|
|
- if (isEditScheme.value) {
|
|
|
- const index = operationSchemes.value.findIndex(s => s.id === schemeForm.value.id)
|
|
|
- if (index !== -1) operationSchemes.value[index] = { ...schemeForm.value }
|
|
|
- } else {
|
|
|
- const newId = Math.max(...operationSchemes.value.map(s => s.id), 0) + 1
|
|
|
- operationSchemes.value.push({ ...schemeForm.value, id: newId })
|
|
|
- }
|
|
|
- closeSchemeModal()
|
|
|
- alert('方案已保存')
|
|
|
+function buildRegions(networks, points, detailMap) {
|
|
|
+ return networks.map((network, index) => {
|
|
|
+ const relatedPoints = points.filter((item) => item.networkId === network.networkId);
|
|
|
+ const devices = relatedPoints.flatMap((point) => {
|
|
|
+ const detail = detailMap[point.pointId] || {};
|
|
|
+ return (detail.equipmentList || []).map((equipment, equipmentIndex) => ({
|
|
|
+ id: `${network.networkId}-${equipment.equipmentId}-${equipmentIndex}`,
|
|
|
+ equipmentId: equipment.equipmentId,
|
|
|
+ name: equipment.equipmentName,
|
|
|
+ deviceId: equipment.equipmentCode,
|
|
|
+ type: equipment.equipmentModel || "设备",
|
|
|
+ status: ["正常", "启用"].includes(point.status) ? "running" : "stopped",
|
|
|
+ unit: point.pointType?.includes("压力") ? "MPa" : point.pointType?.includes("流量") ? "m3/h" : "值",
|
|
|
+ currentValue: equipmentIndex + 1,
|
|
|
+ pointName: point.pointName
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ id: index + 1,
|
|
|
+ networkId: network.networkId,
|
|
|
+ name: network.networkName,
|
|
|
+ location: network.location || "未维护位置",
|
|
|
+ controlMode: index % 2 === 0 ? "auto" : "manual",
|
|
|
+ devices
|
|
|
+ };
|
|
|
+ });
|
|
|
}
|
|
|
-const deleteScheme = (scheme) => {
|
|
|
- if (confirm(`删除方案"${scheme.name}"?`)) {
|
|
|
- operationSchemes.value = operationSchemes.value.filter(s => s.id !== scheme.id)
|
|
|
- }
|
|
|
+
|
|
|
+function loadDashboard() {
|
|
|
+ loading.value = true;
|
|
|
+ return Promise.all([listPipeNetworkAll(), listMonitorPointAll()])
|
|
|
+ .then(async ([networkRes, pointRes]) => {
|
|
|
+ const networks = networkRes.data || [];
|
|
|
+ const points = pointRes.data || [];
|
|
|
+ networkOptions.value = networks;
|
|
|
+ const details = await Promise.all(points.map((item) => getMonitorPointDetail(item.pointId)));
|
|
|
+ const detailMap = {};
|
|
|
+ details.forEach((detail, index) => {
|
|
|
+ detailMap[points[index].pointId] = detail.data || {};
|
|
|
+ });
|
|
|
+ regions.value = buildRegions(networks, points, detailMap);
|
|
|
+ selectedRegionId.value = regions.value[0]?.id;
|
|
|
+
|
|
|
+ const allDeviceIds = regions.value.flatMap((item) => item.devices.map((device) => device.equipmentId));
|
|
|
+ operationSchemes.value = operationSchemes.value.map((item, index) => ({
|
|
|
+ ...item,
|
|
|
+ deviceIds: allDeviceIds.slice(0, Math.max(1, index + 1))
|
|
|
+ }));
|
|
|
+ incidentPlans.value = incidentPlans.value.map((item, index) => ({
|
|
|
+ ...item,
|
|
|
+ devices: regions.value.flatMap((region) => region.devices).slice(index, index + 2)
|
|
|
+ }));
|
|
|
+ addLog("刷新远程测控页面", `共加载 ${regions.value.length} 个区域`);
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ loading.value = false;
|
|
|
+ });
|
|
|
}
|
|
|
-const closeSchemeModal = () => { schemeModalVisible.value = false }
|
|
|
-
|
|
|
-// 区域相关
|
|
|
-const openRegionModal = () => { isEditRegion.value = false; regionForm.value = { name: '', location: '', controlMode: 'auto', unit: '' }; regionModalVisible.value = true }
|
|
|
-const saveRegion = () => {
|
|
|
- if (isEditRegion.value) {
|
|
|
- const index = regions.value.findIndex(r => r.id === regionForm.value.id)
|
|
|
- if (index !== -1) regions.value[index] = { ...regionForm.value, devices: regions.value[index].devices }
|
|
|
- } else {
|
|
|
- const newId = Math.max(...regions.value.map(r => r.id), 0) + 1
|
|
|
- regions.value.push({ ...regionForm.value, id: newId, devices: [], status: regionForm.value.controlMode === 'auto' ? 'auto' : 'manual' })
|
|
|
- }
|
|
|
- closeRegionModal()
|
|
|
- alert('区域已保存')
|
|
|
-}
|
|
|
-const closeRegionModal = () => { regionModalVisible.value = false }
|
|
|
-
|
|
|
-// 事故预案相关
|
|
|
-const openIncidentModal = () => { isEditIncident.value = false; incidentForm.value = { name: '', type: 'leak', description: '', deviceIds: [], action: 'all_stop' }; incidentModalVisible.value = true }
|
|
|
-const editIncident = (incident) => { isEditIncident.value = true; incidentForm.value = { ...incident, deviceIds: [...incident.deviceIds] }; incidentModalVisible.value = true }
|
|
|
-const saveIncident = () => {
|
|
|
- const devices = incidentForm.value.deviceIds.map(id => allDevices.value.find(d => d.id === id)).filter(d => d)
|
|
|
- const incidentData = {
|
|
|
- ...incidentForm.value,
|
|
|
- devices: devices.map(d => ({ id: d.id, name: d.name }))
|
|
|
- }
|
|
|
- if (isEditIncident.value) {
|
|
|
- const index = incidentPlans.value.findIndex(i => i.id === incidentForm.value.id)
|
|
|
- if (index !== -1) incidentPlans.value[index] = incidentData
|
|
|
- } else {
|
|
|
- const newId = Math.max(...incidentPlans.value.map(i => i.id), 0) + 1
|
|
|
- incidentPlans.value.push({ ...incidentData, id: newId })
|
|
|
- }
|
|
|
- closeIncidentModal()
|
|
|
- alert('预案已保存')
|
|
|
+
|
|
|
+function selectRegion(id) {
|
|
|
+ selectedRegionId.value = id;
|
|
|
+ manualMode.value = currentRegion.value?.controlMode === "manual";
|
|
|
+ addLog("切换区域控制器", currentRegion.value?.name || "-");
|
|
|
}
|
|
|
-const deleteIncident = (incident) => {
|
|
|
- if (confirm(`删除预案"${incident.name}"?`)) {
|
|
|
- incidentPlans.value = incidentPlans.value.filter(i => i.id !== incident.id)
|
|
|
- }
|
|
|
+
|
|
|
+function saveAutoParams() {
|
|
|
+ addLog("保存自动控制参数", `${currentRegion.value?.name || "-"} 阈值已更新`);
|
|
|
}
|
|
|
-const closeIncidentModal = () => { incidentModalVisible.value = false }
|
|
|
-
|
|
|
-// 触发事故预案
|
|
|
-const triggerIncident = (incident) => {
|
|
|
- const devices = incident.devices
|
|
|
- devices.forEach(device => {
|
|
|
- const dev = allDevices.value.find(d => d.id === device.id)
|
|
|
- if (dev) {
|
|
|
- if (incident.action === 'all_stop' || incident.action === 'emergency_stop') {
|
|
|
- dev.status = 'stopped'
|
|
|
- } else if (incident.action === 'all_start') {
|
|
|
- dev.status = 'running'
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
- executionRecords.value.unshift({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleString(),
|
|
|
- incidentName: incident.name,
|
|
|
- status: 'success'
|
|
|
- })
|
|
|
- addControlLog('预案系统', `触发预案:${incident.name}`, `${devices.length}台设备联动`, 'success')
|
|
|
- alert(`预案"${incident.name}"已启动,${devices.length}台设备已联动响应`)
|
|
|
-}
|
|
|
-
|
|
|
-// 单设备控制
|
|
|
-const sendControlCommand = (device, command) => {
|
|
|
- if (!isManualMode.value && controlMode.value === 'auto') {
|
|
|
- alert('当前为自动控制模式,请切换到手动模式后操作')
|
|
|
- return
|
|
|
- }
|
|
|
- if (command === 'start') {
|
|
|
- device.status = 'running'
|
|
|
- addControlLog('中控', '启动设备', device.name, 'success')
|
|
|
- alert(`已启动设备: ${device.name}`)
|
|
|
- } else if (command === 'stop') {
|
|
|
- device.status = 'stopped'
|
|
|
- addControlLog('中控', '停止设备', device.name, 'success')
|
|
|
- alert(`已停止设备: ${device.name}`)
|
|
|
+
|
|
|
+function toggleControlMode() {
|
|
|
+ if (!currentRegion.value) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ currentRegion.value.controlMode = manualMode.value ? "manual" : "auto";
|
|
|
+ addLog("切换控制模式", `${currentRegion.value.name} -> ${manualMode.value ? "手动模式" : "自动模式"}`);
|
|
|
}
|
|
|
|
|
|
-// 切换控制模式
|
|
|
-const toggleControlMode = () => {
|
|
|
- if (currentRegion.value) {
|
|
|
- currentRegion.value.controlMode = isManualMode.value ? 'manual' : 'auto'
|
|
|
+function sendControlCommand(device, command) {
|
|
|
+ if (!manualMode.value) {
|
|
|
+ addLog("控制拦截", "当前为自动模式,无法手动控制");
|
|
|
+ return;
|
|
|
}
|
|
|
- addControlLog('系统', `切换控制模式`, isManualMode.value ? '手动模式' : '自动模式', 'success')
|
|
|
+ device.status = command === "start" ? "running" : "stopped";
|
|
|
+ addLog(command === "start" ? "启动设备" : "停止设备", device.name);
|
|
|
}
|
|
|
|
|
|
-// 设置设备参数
|
|
|
-const openSetValueModal = (device) => {
|
|
|
- currentSetDevice.value = device
|
|
|
- setValue.value = device.currentValue || 0
|
|
|
- setValueModalVisible.value = true
|
|
|
+function openSetValueDialog(device) {
|
|
|
+ currentSetDevice.value = device;
|
|
|
+ setValue.value = device.currentValue || 0;
|
|
|
+ setValueOpen.value = true;
|
|
|
}
|
|
|
-const submitSetValue = () => {
|
|
|
+
|
|
|
+function submitSetValue() {
|
|
|
if (currentSetDevice.value) {
|
|
|
- currentSetDevice.value.currentValue = setValue.value
|
|
|
- addControlLog('中控', `设置参数`, `${currentSetDevice.value.name} = ${setValue.value}`, 'success')
|
|
|
- alert(`已设置 ${currentSetDevice.value.name} 目标值为 ${setValue.value}`)
|
|
|
+ currentSetDevice.value.currentValue = setValue.value;
|
|
|
+ addLog("下发参数设置", `${currentSetDevice.value.name} -> ${setValue.value}`);
|
|
|
}
|
|
|
- closeSetValueModal()
|
|
|
-}
|
|
|
-const closeSetValueModal = () => { setValueModalVisible.value = false }
|
|
|
-
|
|
|
-// 快捷指令
|
|
|
-const quickCommand = (cmd) => {
|
|
|
- if (cmd === '全部启动') {
|
|
|
- currentDevices.value.forEach(d => d.status = 'running')
|
|
|
- addControlLog('中控', '快捷指令', '全部启动', 'success')
|
|
|
- alert('已发送全部启动指令')
|
|
|
- } else if (cmd === '全部停止') {
|
|
|
- currentDevices.value.forEach(d => d.status = 'stopped')
|
|
|
- addControlLog('中控', '快捷指令', '全部停止', 'success')
|
|
|
- alert('已发送全部停止指令')
|
|
|
- } else if (cmd === '紧急停止') {
|
|
|
- currentDevices.value.forEach(d => d.status = 'stopped')
|
|
|
- addControlLog('中控', '紧急停止', '全部设备已停止', 'success')
|
|
|
- alert('紧急停止指令已执行')
|
|
|
+ setValueOpen.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+function quickCommand(command) {
|
|
|
+ if (!manualMode.value) {
|
|
|
+ addLog("快捷指令拦截", "请先切换到手动模式");
|
|
|
+ return;
|
|
|
}
|
|
|
+ currentDevices.value.forEach((device) => {
|
|
|
+ device.status = command === "全部启动" ? "running" : "stopped";
|
|
|
+ });
|
|
|
+ addLog("执行快捷指令", command);
|
|
|
}
|
|
|
|
|
|
-// 添加控制日志
|
|
|
-const addControlLog = (operator, action, target, result) => {
|
|
|
- controlLogs.value.unshift({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleTimeString(),
|
|
|
- action: action,
|
|
|
- target: target,
|
|
|
- result: result
|
|
|
- })
|
|
|
- if (controlLogs.value.length > 20) controlLogs.value.pop()
|
|
|
+function applyScheme(scheme) {
|
|
|
+ const targetIds = new Set(scheme.deviceIds);
|
|
|
+ regions.value.forEach((region) => {
|
|
|
+ region.devices.forEach((device) => {
|
|
|
+ if (targetIds.has(device.equipmentId)) {
|
|
|
+ device.status = scheme.name.includes("节能") ? "stopped" : "running";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ addLog("应用设备运行方案", scheme.name);
|
|
|
}
|
|
|
|
|
|
-// 辅助函数
|
|
|
-const getIncidentTypeText = (type) => {
|
|
|
- const map = { leak: '泄漏事故', fire: '火灾事故', pressure: '压力异常', maintenance: '养护维护' }
|
|
|
- return map[type] || type
|
|
|
+function triggerIncident(plan) {
|
|
|
+ plan.devices.forEach((device) => {
|
|
|
+ const found = regions.value.flatMap((item) => item.devices).find((item) => item.id === device.id);
|
|
|
+ if (found) {
|
|
|
+ found.status = "stopped";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ addLog("启动事故联动预案", plan.name);
|
|
|
}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadDashboard();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
-
|
|
|
-.prototype-container {
|
|
|
- max-width: 1600px;
|
|
|
- margin: 0 auto;
|
|
|
- background: #f0f4f8;
|
|
|
- border-radius: 28px;
|
|
|
- box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
|
|
|
- overflow: hidden;
|
|
|
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
|
-}
|
|
|
-
|
|
|
-/* 头部 */
|
|
|
-.header {
|
|
|
- background: #0a2b3c;
|
|
|
- padding: 16px 28px;
|
|
|
+.remote-page {
|
|
|
+ background: linear-gradient(180deg, #f3f7fd 0%, #f8fafc 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.hero-panel {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
gap: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 20px 24px;
|
|
|
+ border-radius: 18px;
|
|
|
+ background: linear-gradient(135deg, #0f2742, #1d4f7d);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-title {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-subtitle {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.84);
|
|
|
+}
|
|
|
+
|
|
|
+.panel-card {
|
|
|
+ border-radius: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
|
|
|
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
|
|
|
-.control-mode-badge { padding: 6px 18px; border-radius: 30px; font-weight: 500; font-size: 0.8rem; }
|
|
|
-.control-mode-badge.auto { background: #27ae60; color: white; }
|
|
|
-.control-mode-badge.manual { background: #3498db; color: white; }
|
|
|
-
|
|
|
-/* 布局 */
|
|
|
-.main-layout { display: flex; flex-wrap: wrap; }
|
|
|
-.left-panel { width: 320px; background: #f9fbfd; padding: 20px; }
|
|
|
-.middle-panel { flex: 1; padding: 20px; min-width: 400px; }
|
|
|
-.right-panel { width: 360px; background: #f9fbfd; padding: 20px; }
|
|
|
-
|
|
|
-/* 卡片 */
|
|
|
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
|
|
|
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
|
-.card-body { padding: 16px; }
|
|
|
-
|
|
|
-/* 区域列表 */
|
|
|
-.region-list { max-height: 250px; overflow-y: auto; }
|
|
|
-.region-item { background: #fef9f2; border-radius: 14px; padding: 12px; margin-bottom: 10px; cursor: pointer; border-left: 3px solid #3498db; }
|
|
|
-.region-item.active { background: #e3f0fd; border-left-color: #2980b9; }
|
|
|
-.region-name { font-weight: 700; font-size: 0.8rem; }
|
|
|
-.region-status { font-size: 0.65rem; margin-top: 4px; }
|
|
|
-
|
|
|
-/* 参数设置 */
|
|
|
-.param-group { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
|
-.param-label { font-size: 0.7rem; font-weight: 600; width: 70px; color: #2c5a72; }
|
|
|
-.param-input { flex: 1; padding: 6px 10px; border-radius: 10px; border: 1px solid #cbdde6; font-size: 0.7rem; }
|
|
|
-.param-unit { font-size: 0.7rem; color: #6f8ea8; width: 40px; }
|
|
|
-
|
|
|
-/* 方案列表 */
|
|
|
-.scheme-list { max-height: 200px; overflow-y: auto; }
|
|
|
-.scheme-item { background: #f8fafc; border-radius: 14px; padding: 12px; margin-bottom: 10px; border: 1px solid #eef2f6; }
|
|
|
-.scheme-name { font-weight: 700; font-size: 0.8rem; }
|
|
|
-.scheme-desc { font-size: 0.65rem; color: #6f8ea8; margin: 4px 0; }
|
|
|
-.scheme-actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
|
-
|
|
|
-/* 设备控制列表 */
|
|
|
-.device-control-list { max-height: 320px; overflow-y: auto; }
|
|
|
-.device-control-item { background: #f8fafc; border-radius: 14px; padding: 12px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
|
|
|
-.device-basic { flex: 1; }
|
|
|
-.device-name { font-weight: 700; font-size: 0.8rem; }
|
|
|
-.device-meta { font-size: 0.65rem; color: #6f8ea8; }
|
|
|
-.device-status { font-size: 0.65rem; margin-top: 4px; }
|
|
|
-.device-value { font-size: 0.7rem; font-weight: 600; color: #2c5a72; margin-top: 4px; }
|
|
|
-.device-control { display: flex; gap: 8px; }
|
|
|
-
|
|
|
-/* 状态点 */
|
|
|
-.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
|
|
-.dot-auto { background: #27ae60; }
|
|
|
-.dot-manual { background: #3498db; }
|
|
|
-.dot-running { background: #27ae60; box-shadow: 0 0 4px #27ae60; }
|
|
|
-.dot-stopped { background: #95a5a6; }
|
|
|
-
|
|
|
-/* 手动控制面板 */
|
|
|
-.manual-control-panel { min-height: 180px; }
|
|
|
-.control-log { background: #1a2b3c; border-radius: 16px; padding: 12px; color: #a8d4e6; font-family: monospace; font-size: 0.7rem; max-height: 150px; overflow-y: auto; margin-bottom: 16px; }
|
|
|
-.log-item { padding: 4px 0; border-bottom: 1px solid #2a4a5a; display: flex; gap: 12px; }
|
|
|
-.log-time { color: #6f9eb0; width: 70px; }
|
|
|
-.log-action { color: #f1c40f; }
|
|
|
-.log-target { color: #a8d4e6; flex: 1; }
|
|
|
-.log-result.success { color: #27ae60; }
|
|
|
-.log-result.failed { color: #e74c3c; }
|
|
|
-.quick-title { font-size: 0.7rem; font-weight: 600; margin-bottom: 8px; }
|
|
|
-.quick-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
|
-.auto-mode-hint { text-align: center; padding: 20px; color: #6f8ea8; }
|
|
|
-.hint-icon { font-size: 2rem; display: block; margin-bottom: 8px; }
|
|
|
-.hint-sub { font-size: 0.65rem; display: block; margin-top: 8px; color: #a0b8cc; }
|
|
|
-
|
|
|
-/* 事故预案列表 */
|
|
|
-.incident-list { max-height: 300px; overflow-y: auto; }
|
|
|
-.incident-item { background: white; border-radius: 14px; padding: 12px; margin-bottom: 12px; border-left: 4px solid; }
|
|
|
-.incident-item.leak { border-left-color: #e74c3c; }
|
|
|
-.incident-item.fire { border-left-color: #f39c12; }
|
|
|
-.incident-item.pressure { border-left-color: #3498db; }
|
|
|
-.incident-item.maintenance { border-left-color: #27ae60; }
|
|
|
-.incident-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
|
|
|
-.incident-name { font-weight: 700; font-size: 0.8rem; }
|
|
|
-.incident-type { font-size: 0.6rem; padding: 2px 8px; border-radius: 20px; background: #eef2fa; }
|
|
|
-.incident-desc { font-size: 0.65rem; color: #6f8ea8; margin-bottom: 8px; }
|
|
|
-.incident-devices { font-size: 0.65rem; margin-bottom: 8px; }
|
|
|
-.devices-label { color: #2c5a72; }
|
|
|
-.incident-actions { display: flex; gap: 8px; }
|
|
|
-
|
|
|
-/* 执行记录 */
|
|
|
-.execution-list { max-height: 150px; overflow-y: auto; }
|
|
|
-.execution-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eef2f6; font-size: 0.7rem; }
|
|
|
-.exec-time { color: #6f8ea8; }
|
|
|
-.exec-status.success { color: #27ae60; }
|
|
|
-
|
|
|
-/* 按钮样式 */
|
|
|
-.btn-primary { background: #3498db; border: none; color: white; padding: 6px 14px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
|
|
|
-.btn-primary:hover { background: #2980b9; }
|
|
|
-.btn-outline { background: white; border: 1px solid #3498db; color: #3498db; padding: 5px 12px; border-radius: 30px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-success { background: #27ae60; border: none; color: white; padding: 5px 12px; border-radius: 30px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-danger { background: #e74c3c; border: none; color: white; padding: 5px 12px; border-radius: 30px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-danger-outline { background: white; border: 1px solid #e74c3c; color: #e74c3c; padding: 4px 10px; border-radius: 30px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-warning { background: #f39c12; border: none; color: white; padding: 5px 12px; border-radius: 30px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-sm { padding: 4px 10px; font-size: 0.65rem; }
|
|
|
-
|
|
|
-/* 复选框列表 */
|
|
|
-.device-check-list { max-height: 150px; overflow-y: auto; border: 1px solid #eef2f6; border-radius: 12px; padding: 8px; }
|
|
|
-.device-check { display: block; padding: 4px 0; font-size: 0.7rem; }
|
|
|
-.mode-switch { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
|
-
|
|
|
-/* 模态框 */
|
|
|
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
|
-.modal-content { background: white; border-radius: 24px; width: 450px; max-width: 90%; padding: 24px; max-height: 85vh; overflow-y: auto; }
|
|
|
-.form-group { margin-bottom: 16px; }
|
|
|
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 6px; display: block; color: #2c5a72; }
|
|
|
-.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
|
|
|
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; }
|
|
|
-.region-hint { font-size: 0.65rem; font-weight: normal; background: #eef2fa; padding: 2px 10px; border-radius: 20px; }
|
|
|
-</style>
|
|
|
+.card-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.card-subtitle {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.region-list,
|
|
|
+.scheme-list,
|
|
|
+.plan-list,
|
|
|
+.log-list,
|
|
|
+.device-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.region-item,
|
|
|
+.scheme-item,
|
|
|
+.plan-item,
|
|
|
+.log-item,
|
|
|
+.device-item {
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1px solid #edf2f7;
|
|
|
+}
|
|
|
+
|
|
|
+.region-item {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.region-item.active {
|
|
|
+ border-color: #60a5fa;
|
|
|
+ background: #eef6ff;
|
|
|
+}
|
|
|
+
|
|
|
+.region-name,
|
|
|
+.device-name,
|
|
|
+.scheme-name,
|
|
|
+.plan-name,
|
|
|
+.log-action {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #111827;
|
|
|
+}
|
|
|
+
|
|
|
+.region-meta,
|
|
|
+.region-status,
|
|
|
+.device-meta,
|
|
|
+.device-value,
|
|
|
+.scheme-desc,
|
|
|
+.scheme-footer,
|
|
|
+.plan-desc,
|
|
|
+.plan-devices,
|
|
|
+.log-time,
|
|
|
+.log-target {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: inline-block;
|
|
|
+ margin-right: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-auto {
|
|
|
+ background: #16a34a;
|
|
|
+}
|
|
|
+
|
|
|
+.dot-manual {
|
|
|
+ background: #2563eb;
|
|
|
+}
|
|
|
+
|
|
|
+.device-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.scheme-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .hero-panel,
|
|
|
+ .card-header-row {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|