feat(mes/eqp): 新增设备监控模拟页面
该页面实现了设备状态统计看板、多条件筛选搜索、自动刷新、设备卡片展示、详情弹窗查看功能,使用mock数据模拟真实设备运行状态和参数变化
This commit is contained in:
529
klp-ui/src/views/mes/eqp/mock/index.vue
Normal file
529
klp-ui/src/views/mes/eqp/mock/index.vue
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container eqp-monitor">
|
||||||
|
|
||||||
|
<div class="stat-panel mb16">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-wrap total-icon"><i class="el-icon-cpu" /></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-num">{{ stats.total }}</div>
|
||||||
|
<div class="stat-label">设备总数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-wrap running-icon"><i class="el-icon-video-play" /></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-num running-num">{{ stats.running }}</div>
|
||||||
|
<div class="stat-label">运行中</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-wrap standby-icon"><i class="el-icon-video-pause" /></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-num standby-num">{{ stats.stopped }}</div>
|
||||||
|
<div class="stat-label">已停机</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-wrap fault-icon"><i class="el-icon-warning-outline" /></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-num fault-num">{{ stats.fault }}</div>
|
||||||
|
<div class="stat-label">故 障</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon-wrap offline-icon"><i class="el-icon-remove-outline" /></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-num offline-num">{{ stats.offline }}</div>
|
||||||
|
<div class="stat-label">离 线</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="68px">
|
||||||
|
<el-form-item label="设备类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" placeholder="全部" clearable style="width:140px" @change="handleQuery">
|
||||||
|
<el-option label="CNC加工中心" value="CNC" />
|
||||||
|
<el-option label="工业机器人" value="Robot" />
|
||||||
|
<el-option label="传送带" value="Conveyor" />
|
||||||
|
<el-option label="液压机" value="Hydraulic" />
|
||||||
|
<el-option label="焊机" value="Welding" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px" @change="handleQuery">
|
||||||
|
<el-option label="运行中" value="running" />
|
||||||
|
<el-option label="已停机" value="stopped" />
|
||||||
|
<el-option label="故障" value="fault" />
|
||||||
|
<el-option label="离线" value="offline" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关键词" prop="keyword">
|
||||||
|
<el-input v-model="queryParams.keyword" placeholder="设备编号/名称" clearable style="width:180px" @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button :type="autoRefresh ? 'danger' : 'primary'" plain :icon="autoRefresh ? 'el-icon-close' : 'el-icon-refresh'" size="mini" @click="toggleAutoRefresh">
|
||||||
|
{{ autoRefresh ? '停止刷新' : '自动刷新' }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col v-if="autoRefresh" :span="1.5">
|
||||||
|
<span class="refresh-tip">数据自动刷新中...</span>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :show-search.sync="showSearch" @queryTable="handleQuery" />
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 设备卡片网格 -->
|
||||||
|
<div v-loading="loading" class="device-grid">
|
||||||
|
<div
|
||||||
|
v-for="dev in filteredDevices"
|
||||||
|
:key="dev.id"
|
||||||
|
class="device-card"
|
||||||
|
:class="['card-' + dev.status]"
|
||||||
|
@click="handleDetail(dev)"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-left">
|
||||||
|
<span class="card-code">{{ dev.code }}</span>
|
||||||
|
<span :class="['type-tag', 'type-' + dev.type]">{{ dev.typeName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-status">
|
||||||
|
<div class="status-dot-wrap" :class="'status-' + dev.status">
|
||||||
|
<span class="status-dot" /><span>{{ dev.status | statusFilter }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-name">{{ dev.name }}</div>
|
||||||
|
<div class="card-workshop">{{ dev.workshop }}</div>
|
||||||
|
|
||||||
|
<!-- 参数区域 -->
|
||||||
|
<div class="card-params">
|
||||||
|
<div v-for="item in dev.params" :key="item.key" class="param-row">
|
||||||
|
<div class="param-label">{{ item.label }}</div>
|
||||||
|
<div class="param-gauge">
|
||||||
|
<div class="param-gauge-track">
|
||||||
|
<div
|
||||||
|
class="param-gauge-fill"
|
||||||
|
:class="{ 'gauge-alarm': item.alarm }"
|
||||||
|
:style="{ width: item.percent + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-value" :class="{ 'param-alarm': item.alarm }">
|
||||||
|
{{ item.value }} <small>{{ item.unit }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部 -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="card-time">{{ dev.runningTime }}</span>
|
||||||
|
<span class="card-update">{{ dev.updateTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<el-empty v-if="!loading && filteredDevices.length === 0" description="暂无匹配设备" />
|
||||||
|
|
||||||
|
<!-- 详情弹窗 -->
|
||||||
|
<el-dialog :title="detailTitle" :visible.sync="detailOpen" width="800px" append-to-body>
|
||||||
|
<div v-if="detailDev" class="detail-body">
|
||||||
|
<el-descriptions :column="3" border size="small">
|
||||||
|
<el-descriptions-item label="设备编号">{{ detailDev.code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备名称">{{ detailDev.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备类型">{{ detailDev.typeName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所属车间">{{ detailDev.workshop }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前状态">
|
||||||
|
<div class="status-dot-wrap" :class="'status-' + detailDev.status">
|
||||||
|
<span class="status-dot" /><span>{{ detailDev.status | statusFilter }}</span>
|
||||||
|
</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="累计运行">{{ detailDev.runningTime }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div class="detail-params mt16">
|
||||||
|
<div class="detail-param-title">实时参数详情</div>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col v-for="item in detailDev.params" :key="item.key" :span="8">
|
||||||
|
<div class="detail-param-card" :class="{ 'detail-alarm': item.alarm }">
|
||||||
|
<div class="d-param-label">{{ item.label }}</div>
|
||||||
|
<div class="d-param-value">{{ item.value }} <small>{{ item.unit }}</small></div>
|
||||||
|
<div class="d-param-range">范围: {{ item.min }} ~ {{ item.max }} {{ item.unit }}</div>
|
||||||
|
<div class="d-param-bar">
|
||||||
|
<div class="d-param-bar-inner" :class="{ 'bar-alarm': item.alarm }" :style="{ width: item.percent + '%' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function randomVal(base, range) {
|
||||||
|
return base + (Math.random() - 0.5) * 2 * range
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEVICES = [
|
||||||
|
{ id: 'EQP-0001', code: 'EQP-0001', name: '立式加工中心', type: 'CNC', typeName: 'CNC加工中心', workshop: '一车间', hours: 3200 },
|
||||||
|
{ id: 'EQP-0002', code: 'EQP-0002', name: '五轴加工中心', type: 'CNC', typeName: 'CNC加工中心', workshop: '一车间', hours: 4800 },
|
||||||
|
{ id: 'EQP-0003', code: 'EQP-0003', name: '卧式加工中心', type: 'CNC', typeName: 'CNC加工中心', workshop: '一车间', hours: 2100 },
|
||||||
|
{ id: 'EQP-0004', code: 'EQP-0004', name: '龙门加工中心', type: 'CNC', typeName: 'CNC加工中心', workshop: '一车间', hours: 5600 },
|
||||||
|
{ id: 'EQP-0005', code: 'EQP-0005', name: '钻攻加工中心', type: 'CNC', typeName: 'CNC加工中心', workshop: '一车间', hours: 1500 },
|
||||||
|
{ id: 'EQP-0006', code: 'EQP-0006', name: '六轴机器人', type: 'Robot', typeName: '工业机器人', workshop: '二车间', hours: 2800 },
|
||||||
|
{ id: 'EQP-0007', code: 'EQP-0007', name: '协作机器人', type: 'Robot', typeName: '工业机器人', workshop: '二车间', hours: 3100 },
|
||||||
|
{ id: 'EQP-0008', code: 'EQP-0008', name: 'SCARA机器人', type: 'Robot', typeName: '工业机器人', workshop: '二车间', hours: 1900 },
|
||||||
|
{ id: 'EQP-0009', code: 'EQP-0009', name: '辊筒传送带', type: 'Conveyor', typeName: '传送带', workshop: '三车间', hours: 4000 },
|
||||||
|
{ id: 'EQP-0010', code: 'EQP-0010', name: '皮带传送带', type: 'Conveyor', typeName: '传送带', workshop: '三车间', hours: 3500 },
|
||||||
|
{ id: 'EQP-0011', code: 'EQP-0011', name: '链板传送带', type: 'Conveyor', typeName: '传送带', workshop: '三车间', hours: 2700 },
|
||||||
|
{ id: 'EQP-0012', code: 'EQP-0012', name: '四柱液压机', type: 'Hydraulic', typeName: '液压机', workshop: '一车间', hours: 5200 },
|
||||||
|
{ id: 'EQP-0013', code: 'EQP-0013', name: '框架式液压机', type: 'Hydraulic', typeName: '液压机', workshop: '一车间', hours: 4400 },
|
||||||
|
{ id: 'EQP-0014', code: 'EQP-0014', name: '弧焊机器人工作站', type: 'Welding', typeName: '焊机', workshop: '二车间', hours: 2300 },
|
||||||
|
{ id: 'EQP-0015', code: 'EQP-0015', name: '激光焊接机', type: 'Welding', typeName: '焊机', workshop: '二车间', hours: 3700 },
|
||||||
|
{ id: 'EQP-0016', code: 'EQP-0016', name: '电阻点焊机', type: 'Welding', typeName: '焊机', workshop: '二车间', hours: 4100 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const TYPE_PARAMS = {
|
||||||
|
CNC: [
|
||||||
|
{ key: 'temp', label: '主轴温度', unit: '°C', min: 20, max: 75, setpoint: 42, range: 8, decimals: 1 },
|
||||||
|
{ key: 'speed', label: '主轴转速', unit: 'rpm', min: 0, max: 12000, setpoint: 6000, range: 1500, decimals: 0 },
|
||||||
|
{ key: 'current', label: '工作电流', unit: 'A', min: 0, max: 80, setpoint: 35, range: 15, decimals: 1 },
|
||||||
|
{ key: 'vibration', label: '振动幅度', unit: 'mm/s', min: 0, max: 10, setpoint: 2.5, range: 2, decimals: 1 },
|
||||||
|
{ key: 'pressure', label: '液压压力', unit: 'MPa', min: 0, max: 25, setpoint: 12, range: 4, decimals: 1 },
|
||||||
|
{ key: 'voltage', label: '输入电压', unit: 'V', min: 340, max: 420, setpoint: 380, range: 5, decimals: 1 }
|
||||||
|
],
|
||||||
|
Robot: [
|
||||||
|
{ key: 'temp', label: '电机温度', unit: '°C', min: 10, max: 90, setpoint: 50, range: 10, decimals: 1 },
|
||||||
|
{ key: 'speed', label: '关节转速', unit: '°/s', min: 0, max: 250, setpoint: 120, range: 40, decimals: 0 },
|
||||||
|
{ key: 'current', label: '驱动电流', unit: 'A', min: 0, max: 30, setpoint: 12, range: 6, decimals: 1 },
|
||||||
|
{ key: 'vibration', label: '末端振动', unit: 'mm/s', min: 0, max: 5, setpoint: 1, range: 0.8, decimals: 1 },
|
||||||
|
{ key: 'pressure', label: '气源压力', unit: 'MPa', min: 0, max: 1, setpoint: 0.6, range: 0.15, decimals: 2 },
|
||||||
|
{ key: 'voltage', label: '供电电压', unit: 'V', min: 200, max: 240, setpoint: 220, range: 3, decimals: 1 }
|
||||||
|
],
|
||||||
|
Conveyor: [
|
||||||
|
{ key: 'temp', label: '电机温度', unit: '°C', min: 10, max: 80, setpoint: 45, range: 8, decimals: 1 },
|
||||||
|
{ key: 'speed', label: '输送速度', unit: 'm/min', min: 0, max: 60, setpoint: 30, range: 8, decimals: 1 },
|
||||||
|
{ key: 'current', label: '电机电流', unit: 'A', min: 0, max: 25, setpoint: 10, range: 4, decimals: 1 },
|
||||||
|
{ key: 'vibration', label: '运行振动', unit: 'mm/s', min: 0, max: 8, setpoint: 2, range: 1.5, decimals: 1 },
|
||||||
|
{ key: 'pressure', label: '张紧压力', unit: 'N', min: 0, max: 500, setpoint: 250, range: 50, decimals: 0 },
|
||||||
|
{ key: 'voltage', label: '供电电压', unit: 'V', min: 360, max: 400, setpoint: 380, range: 3, decimals: 1 }
|
||||||
|
],
|
||||||
|
Hydraulic: [
|
||||||
|
{ key: 'temp', label: '油液温度', unit: '°C', min: 15, max: 65, setpoint: 40, range: 6, decimals: 1 },
|
||||||
|
{ key: 'speed', label: '滑块速度', unit: 'mm/s', min: 0, max: 200, setpoint: 80, range: 25, decimals: 0 },
|
||||||
|
{ key: 'current', label: '电机电流', unit: 'A', min: 0, max: 120, setpoint: 55, range: 20, decimals: 1 },
|
||||||
|
{ key: 'vibration', label: '机身振动', unit: 'mm/s', min: 0, max: 12, setpoint: 4, range: 2.5, decimals: 1 },
|
||||||
|
{ key: 'pressure', label: '工作压力', unit: 'MPa', min: 0, max: 35, setpoint: 20, range: 5, decimals: 1 },
|
||||||
|
{ key: 'voltage', label: '供电电压', unit: 'V', min: 360, max: 400, setpoint: 380, range: 5, decimals: 1 }
|
||||||
|
],
|
||||||
|
Welding: [
|
||||||
|
{ key: 'temp', label: '焊枪温度', unit: '°C', min: 20, max: 120, setpoint: 65, range: 15, decimals: 1 },
|
||||||
|
{ key: 'speed', label: '送丝速度', unit: 'm/min', min: 0, max: 20, setpoint: 10, range: 3, decimals: 1 },
|
||||||
|
{ key: 'current', label: '焊接电流', unit: 'A', min: 0, max: 350, setpoint: 180, range: 50, decimals: 0 },
|
||||||
|
{ key: 'vibration', label: '机头振动', unit: 'mm/s', min: 0, max: 6, setpoint: 1.8, range: 1.2, decimals: 1 },
|
||||||
|
{ key: 'pressure', label: '保护气压', unit: 'MPa', min: 0, max: 1.5, setpoint: 0.8, range: 0.2, decimals: 2 },
|
||||||
|
{ key: 'voltage', label: '焊接电压', unit: 'V', min: 15, max: 40, setpoint: 26, range: 5, decimals: 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParamItem(cfg, val, isStopped) {
|
||||||
|
const alarm = !isStopped && (val > cfg.max * 0.92 || val < cfg.min * 1.08)
|
||||||
|
return {
|
||||||
|
key: cfg.key,
|
||||||
|
label: cfg.label,
|
||||||
|
value: val.toFixed(cfg.decimals),
|
||||||
|
unit: cfg.unit,
|
||||||
|
min: cfg.min,
|
||||||
|
max: cfg.max,
|
||||||
|
alarm: alarm,
|
||||||
|
percent: Math.min(100, Math.max(0, ((val - cfg.min) / (cfg.max - cfg.min)) * 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EqpMonitor',
|
||||||
|
filters: {
|
||||||
|
statusFilter(val) {
|
||||||
|
const map = { running: '运行中', stopped: '已停机', fault: '故障', offline: '离线' }
|
||||||
|
return map[val] || val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showSearch: true,
|
||||||
|
autoRefresh: true,
|
||||||
|
sourceData: [],
|
||||||
|
detailOpen: false,
|
||||||
|
detailDev: null,
|
||||||
|
queryParams: { type: '', status: '', keyword: '' },
|
||||||
|
timer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
stats() {
|
||||||
|
const arr = this.sourceData
|
||||||
|
if (!arr || !arr.length) return { total: 0, running: 0, stopped: 0, fault: 0, offline: 0 }
|
||||||
|
let total = 0; let running = 0; let stopped = 0; let fault = 0; let offline = 0
|
||||||
|
arr.forEach(d => {
|
||||||
|
total++
|
||||||
|
if (d.status === 'running') running++
|
||||||
|
else if (d.status === 'stopped') stopped++
|
||||||
|
else if (d.status === 'fault') fault++
|
||||||
|
else if (d.status === 'offline') offline++
|
||||||
|
})
|
||||||
|
return { total, running, stopped, fault, offline }
|
||||||
|
},
|
||||||
|
filteredDevices() {
|
||||||
|
const q = this.queryParams
|
||||||
|
return this.sourceData.filter(d => {
|
||||||
|
if (q.type && d.type !== q.type) return false
|
||||||
|
if (q.status && d.status !== q.status) return false
|
||||||
|
if (q.keyword) {
|
||||||
|
const kw = q.keyword.toLowerCase()
|
||||||
|
if (d.code.toLowerCase().indexOf(kw) < 0 && d.name.toLowerCase().indexOf(kw) < 0) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
detailTitle() {
|
||||||
|
return this.detailDev ? '设备详情 - ' + this.detailDev.name : '设备详情'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.initSource()
|
||||||
|
this.tickAll()
|
||||||
|
this.startTimer()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTimer()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initSource() {
|
||||||
|
this.sourceData = DEVICES.map(d => {
|
||||||
|
const statusRand = Math.random()
|
||||||
|
let status = 'running'
|
||||||
|
if (statusRand < 0.03) status = 'offline'
|
||||||
|
else if (statusRand < 0.13) status = 'fault'
|
||||||
|
else if (statusRand < 0.28) status = 'stopped'
|
||||||
|
const cfg = TYPE_PARAMS[d.type]
|
||||||
|
const params = cfg.map(c => {
|
||||||
|
const stopped = status === 'stopped' || status === 'offline'
|
||||||
|
const val = stopped ? 0 : randomVal(c.setpoint, c.range)
|
||||||
|
return buildParamItem(c, val, stopped)
|
||||||
|
})
|
||||||
|
const now = new Date()
|
||||||
|
const ts = ('0' + now.getHours()).slice(-2) + ':' + ('0' + now.getMinutes()).slice(-2) + ':' + ('0' + now.getSeconds()).slice(-2)
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
code: d.code,
|
||||||
|
name: d.name,
|
||||||
|
type: d.type,
|
||||||
|
typeName: d.typeName,
|
||||||
|
workshop: d.workshop,
|
||||||
|
status: status,
|
||||||
|
runningTime: d.hours + 'h',
|
||||||
|
updateTime: ts,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tickAll() {
|
||||||
|
const now = new Date()
|
||||||
|
const ts = ('0' + now.getHours()).slice(-2) + ':' + ('0' + now.getMinutes()).slice(-2) + ':' + ('0' + now.getSeconds()).slice(-2)
|
||||||
|
this.sourceData.forEach(dev => {
|
||||||
|
dev.updateTime = ts
|
||||||
|
if (dev.status === 'offline') return
|
||||||
|
const cfg = TYPE_PARAMS[dev.type]
|
||||||
|
if (!cfg) return
|
||||||
|
const stopped = dev.status === 'stopped'
|
||||||
|
let hasAlarm = false
|
||||||
|
const newParams = cfg.map(c => {
|
||||||
|
const val = stopped ? randomVal(0, c.range * 0.3) : randomVal(c.setpoint, c.range)
|
||||||
|
const item = buildParamItem(c, val, stopped)
|
||||||
|
if (item.alarm) hasAlarm = true
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
dev.params = newParams
|
||||||
|
if (stopped && Math.random() < 0.03) dev.status = 'running'
|
||||||
|
else if (!stopped && dev.status === 'running') {
|
||||||
|
if (hasAlarm && Math.random() < 0.3) dev.status = 'fault'
|
||||||
|
else if (Math.random() < 0.015) dev.status = 'stopped'
|
||||||
|
}
|
||||||
|
if (dev.status === 'fault' && Math.random() < 0.08) dev.status = 'running'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
// filtering is computed, no action needed
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
if (this.$refs.queryForm) {
|
||||||
|
this.$refs.queryForm.resetFields()
|
||||||
|
}
|
||||||
|
this.queryParams = { type: '', status: '', keyword: '' }
|
||||||
|
},
|
||||||
|
toggleAutoRefresh() {
|
||||||
|
this.autoRefresh = !this.autoRefresh
|
||||||
|
this.autoRefresh ? this.startTimer() : this.stopTimer()
|
||||||
|
},
|
||||||
|
startTimer() {
|
||||||
|
if (this.timer) return
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.tickAll()
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
stopTimer() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.$modal.msgSuccess('导出功能(演示)')
|
||||||
|
},
|
||||||
|
handleDetail(dev) {
|
||||||
|
this.detailDev = dev
|
||||||
|
this.detailOpen = true
|
||||||
|
},
|
||||||
|
handleStart(dev) {
|
||||||
|
const d = this.sourceData.find(item => item.id === dev.id)
|
||||||
|
if (!d) return
|
||||||
|
d.status = 'running'
|
||||||
|
const cfg = TYPE_PARAMS[d.type]
|
||||||
|
d.params = cfg.map(c => buildParamItem(c, randomVal(c.setpoint, c.range), false))
|
||||||
|
this.$modal.msgSuccess(d.name + ' 已启动')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.eqp-monitor { background: #f4f5f7; min-height: 100vh; }
|
||||||
|
.mb16 { margin-bottom: 16px; }
|
||||||
|
.mt16 { margin-top: 16px; }
|
||||||
|
|
||||||
|
/* ─── 统计看板 ─── */
|
||||||
|
.stat-panel {
|
||||||
|
display: flex; background: #fff; border: 1px solid #dcdee0; border-radius: 4px; padding: 0; overflow: hidden;
|
||||||
|
}
|
||||||
|
.stat-item { flex: 1; display: flex; align-items: center; padding: 18px 20px; gap: 14px; position: relative; }
|
||||||
|
.stat-divider { position: absolute; right: 0; top: 16px; bottom: 16px; width: 1px; background: #e4e6ea; }
|
||||||
|
.stat-icon-wrap { width: 42px; height: 42px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
|
||||||
|
.total-icon { background: #edf0f3; color: #3d4b5c; }
|
||||||
|
.running-icon { background: #e8f5ef; color: #0a7c42; }
|
||||||
|
.standby-icon { background: #fdf3e3; color: #8b5c00; }
|
||||||
|
.fault-icon { background: #fdecea; color: #a61c00; }
|
||||||
|
.offline-icon { background: #f0f1f2; color: #5f6368; }
|
||||||
|
.stat-body { display: flex; flex-direction: column; }
|
||||||
|
.stat-num { font-size: 26px; font-weight: 700; line-height: 1; color: #1f2329; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }
|
||||||
|
.running-num { color: #0a7c42; }
|
||||||
|
.standby-num { color: #8b5c00; }
|
||||||
|
.fault-num { color: #a61c00; }
|
||||||
|
.offline-num { color: #5f6368; }
|
||||||
|
.stat-label { font-size: 12px; color: #8f9099; margin-top: 4px; letter-spacing: 1px; }
|
||||||
|
|
||||||
|
/* ─── 刷新提示 ─── */
|
||||||
|
.refresh-tip { display: inline-block; padding: 6px 0; font-size: 12px; color: #0a7c42; animation: blink 1.2s infinite; }
|
||||||
|
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||||
|
|
||||||
|
/* ─── 设备卡片网格 ─── */
|
||||||
|
.device-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdee0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow .2s, border-color .2s;
|
||||||
|
&:hover { box-shadow: 0 2px 12px rgba(0,0,0,.06); border-color: #bbb; }
|
||||||
|
&.card-fault { border-color: #f5c6c4; background: #fffbfb; }
|
||||||
|
&.card-offline { border-color: #e0e2e4; background: #f7f8f9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.card-left { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.card-code { font-family: 'Consolas','Courier New',monospace; font-weight: 600; font-size: 13px; color: #1f2329; }
|
||||||
|
.card-name { font-size: 14px; font-weight: 500; color: #1f2329; margin-bottom: 2px; }
|
||||||
|
.card-workshop { font-size: 11px; color: #8f9099; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
/* 类型标签 */
|
||||||
|
.type-tag { display: inline-block; padding: 0 6px; border-radius: 2px; font-size: 10px; font-weight: 600; }
|
||||||
|
.type-CNC { background: #e8edf5; color: #2b4c8c; border: 1px solid #c5d0e8; }
|
||||||
|
.type-Robot { background: #e8f5ef; color: #0a7c42; border: 1px solid #b7d9c5; }
|
||||||
|
.type-Conveyor { background: #fdf3e3; color: #8b5c00; border: 1px solid #f0d9a8; }
|
||||||
|
.type-Hydraulic { background: #eef0f3; color: #4a5568; border: 1px solid #cbd5e0; }
|
||||||
|
.type-Welding { background: #fdecea; color: #a61c00; border: 1px solid #f5c6c4; }
|
||||||
|
|
||||||
|
/* 状态指示 */
|
||||||
|
.status-dot-wrap { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500; }
|
||||||
|
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
||||||
|
.status-running { color: #0a7c42; }
|
||||||
|
.status-running .status-dot { background: #0a7c42; box-shadow: 0 0 0 2px #c8ead8; }
|
||||||
|
.status-stopped { color: #8b5c00; }
|
||||||
|
.status-stopped .status-dot { background: #d4860a; box-shadow: 0 0 0 2px #f5e2b8; }
|
||||||
|
.status-fault { color: #a61c00; }
|
||||||
|
.status-fault .status-dot { background: #c5221f; box-shadow: 0 0 0 2px #f5c6c4; animation: pulse 0.8s infinite; }
|
||||||
|
.status-offline { color: #5f6368; }
|
||||||
|
.status-offline .status-dot { background: #9aa0a6; box-shadow: 0 0 0 2px #e0e2e4; }
|
||||||
|
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 2px #f5c6c4; } 50% { box-shadow: 0 0 0 5px rgba(197,34,31,.25); } }
|
||||||
|
|
||||||
|
/* ─── 卡片参数行 ─── */
|
||||||
|
.card-params {
|
||||||
|
border-top: 1px solid #f0f1f2;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.param-row {
|
||||||
|
display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
.param-label { width: 52px; font-size: 10px; color: #8f9099; text-align: right; flex-shrink: 0; }
|
||||||
|
.param-gauge { flex: 1; }
|
||||||
|
.param-gauge-track { height: 4px; background: #e8eaed; border-radius: 2px; overflow: hidden; }
|
||||||
|
.param-gauge-fill { height: 100%; background: #409eff; border-radius: 2px; transition: width .4s ease; min-width: 2px; }
|
||||||
|
.param-gauge-fill.gauge-alarm { background: #c5221f; }
|
||||||
|
.param-value { width: 60px; font-size: 12px; font-weight: 600; color: #1f2329; text-align: right; font-variant-numeric: tabular-nums; flex-shrink: 0; }
|
||||||
|
.param-value small { font-size: 9px; color: #8f9099; font-weight: 400; }
|
||||||
|
.param-value.param-alarm { color: #a61c00; }
|
||||||
|
|
||||||
|
/* ─── 卡片底部 ─── */
|
||||||
|
.card-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; padding-top: 8px; border-top: 1px solid #f0f1f2; }
|
||||||
|
.card-time { font-size: 11px; color: #b0b3bb; }
|
||||||
|
.card-update { font-size: 10px; color: #b0b3bb; }
|
||||||
|
|
||||||
|
/* ─── 详情弹窗 ─── */
|
||||||
|
.detail-param-title { font-size: 14px; font-weight: 600; color: #1f2329; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
|
||||||
|
.detail-param-card { background: #fafbfc; border: 1px solid #e4e6ea; border-radius: 4px; padding: 14px 16px; margin-bottom: 10px; text-align: center; }
|
||||||
|
.detail-param-card.detail-alarm { border-color: #f5c6c4; background: #fffbfb; }
|
||||||
|
.d-param-label { font-size: 12px; color: #8f9099; margin-bottom: 6px; }
|
||||||
|
.d-param-value { font-size: 24px; font-weight: 700; color: #1f2329; font-variant-numeric: tabular-nums; }
|
||||||
|
.d-param-value small { font-size: 12px; color: #8f9099; font-weight: 400; }
|
||||||
|
.detail-alarm .d-param-value { color: #a61c00; }
|
||||||
|
.d-param-range { font-size: 11px; color: #b0b3bb; margin: 4px 0 8px; }
|
||||||
|
.d-param-bar { height: 4px; background: #e8eaed; border-radius: 2px; overflow: hidden; }
|
||||||
|
.d-param-bar-inner { height: 100%; background: #409eff; border-radius: 2px; transition: width .4s ease; }
|
||||||
|
.d-param-bar-inner.bar-alarm { background: #c5221f; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user