2025-10-13 11:40:34 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="app-container ems-overview">
|
|
|
|
|
|
<div class="toolbar">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
|
<el-button-group>
|
|
|
|
|
|
<el-button size="mini" :type="viewLevel===1?'primary':'default'" @click="setView(1)">1</el-button>
|
|
|
|
|
|
<el-button size="mini" :type="viewLevel===2?'primary':'default'" @click="setView(2)">2</el-button>
|
|
|
|
|
|
<el-button size="mini" :type="viewLevel===3?'primary':'default'" @click="setView(3)">3</el-button>
|
|
|
|
|
|
</el-button-group>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
v-model="query.keyword"
|
|
|
|
|
|
placeholder="据位/设备关键词"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="q-input"
|
|
|
|
|
|
@keyup.enter.native="handleQuery"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-button size="mini" type="primary" @click="handleQuery">查询</el-button>
|
|
|
|
|
|
<el-button size="mini" @click="resetQuery">重置查询</el-button>
|
|
|
|
|
|
</div>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<div class="toolbar-right">
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-zoom-in" @click="zoom(1.1)"/>
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-zoom-out" @click="zoom(0.9)"/>
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<el-button size="mini" icon="el-icon-refresh" @click="resetView">重置视图</el-button>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<div class="canvas-wrapper" ref="wrapper">
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<svg :viewBox="viewBox" class="overview-svg">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<!-- 区域:由矩形拼接组成 -->
|
|
|
|
|
|
<g
|
|
|
|
|
|
v-for="area in areas"
|
|
|
|
|
|
:key="area.id"
|
|
|
|
|
|
style="cursor:pointer;"
|
|
|
|
|
|
@click="selectArea(area, $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 拼接矩形 -->
|
|
|
|
|
|
<g v-for="(shape, idx) in area.shapes" :key="area.id + '_s_' + idx">
|
|
|
|
|
|
<rect
|
|
|
|
|
|
:x="shape.x"
|
|
|
|
|
|
:y="shape.y"
|
|
|
|
|
|
:width="shape.w"
|
|
|
|
|
|
:height="shape.h"
|
|
|
|
|
|
:fill="area.id===selectedAreaId ? area.fillActive : area.fill"
|
|
|
|
|
|
:stroke="area.stroke"
|
|
|
|
|
|
:stroke-width="2"
|
|
|
|
|
|
:opacity="viewLevel===3 ? 0.8 : 1"
|
|
|
|
|
|
rx="4"
|
|
|
|
|
|
ry="4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<!-- 区域标题 -->
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<text :x="area.labelPos.x" :y="area.labelPos.y" class="area-label">
|
|
|
|
|
|
{{ area.name }}
|
|
|
|
|
|
</text>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<!-- 点位格子(方/长方形) -->
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<g v-if="viewLevel!==1 || area.id===selectedAreaId">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<g
|
|
|
|
|
|
v-for="pt in area.pointsInfo"
|
|
|
|
|
|
:key="pt.id"
|
|
|
|
|
|
@click.stop="openPoint(pt, area, $event)"
|
|
|
|
|
|
style="cursor:pointer;"
|
|
|
|
|
|
>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<rect
|
|
|
|
|
|
:x="pt.x-pt.w/2"
|
|
|
|
|
|
:y="pt.y-pt.h/2"
|
|
|
|
|
|
:width="pt.w"
|
|
|
|
|
|
:height="pt.h"
|
2025-10-14 09:40:21 +08:00
|
|
|
|
rx="4"
|
|
|
|
|
|
ry="4"
|
2025-10-13 11:40:34 +08:00
|
|
|
|
:fill="ptFill(pt)"
|
|
|
|
|
|
stroke="#444"
|
2025-10-14 09:40:21 +08:00
|
|
|
|
:stroke-width="selectedPoint && selectedPoint.id===pt.id ? 2 : 1"
|
|
|
|
|
|
:opacity="viewLevel===2 ? 0.9 : 0.8"
|
|
|
|
|
|
:class="{'pt-highlight': highlightedIds.has(pt.id)}"
|
2025-10-13 11:40:34 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<text :x="pt.x" :y="pt.y-6" class="pt-title">{{ pt.name }}</text>
|
|
|
|
|
|
<text :x="pt.x" :y="pt.y+10" class="pt-sub">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
{{ safeMetric(pt, 'metric1', 'label', '指标1') + ':' + safeMetric(pt, 'metric1', 'value', '--') }}
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</text>
|
|
|
|
|
|
<text :x="pt.x" :y="pt.y+24" class="pt-sub">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
{{ safeMetric(pt, 'metric2', 'label', '指标2') + ':' + safeMetric(pt, 'metric2', 'value', '--') }}
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</text>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
</svg>
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 悬浮信息卡(点击块/点位时显示) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="infoBox.show"
|
|
|
|
|
|
class="info-box"
|
|
|
|
|
|
:style="{ left: infoBox.x + 'px', top: infoBox.y + 'px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="info-title">{{ infoBox.title }}</div>
|
|
|
|
|
|
<div class="info-line" v-for="(line, i) in infoBox.lines" :key="i">{{ line }}</div>
|
|
|
|
|
|
</div>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 点位详情 -->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
title="点位详情"
|
|
|
|
|
|
:visible.sync="pointDialogOpen"
|
|
|
|
|
|
width="420px"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="selectedPoint">
|
|
|
|
|
|
<p>点位名称:{{ selectedPoint.name }}</p>
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<p>所属区域:{{ selectedArea ? selectedArea.name : '-' }}</p>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
<p>坐标:({{ selectedPoint.x }}, {{ selectedPoint.y }})</p>
|
|
|
|
|
|
<el-divider></el-divider>
|
|
|
|
|
|
<p>设备数据:</p>
|
|
|
|
|
|
<ul class="point-ul">
|
2025-10-14 09:40:21 +08:00
|
|
|
|
<li>
|
|
|
|
|
|
{{ safeMetric(selectedPoint, 'metric1', 'label', '指标1') }}:
|
|
|
|
|
|
{{ safeMetric(selectedPoint, 'metric1', 'value', '--') }}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
{{ safeMetric(selectedPoint, 'metric2', 'label', '指标2') }}:
|
|
|
|
|
|
{{ safeMetric(selectedPoint, 'metric2', 'value', '--') }}
|
|
|
|
|
|
</li>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button size="mini" type="primary" @click="pointDialogOpen=false">关 闭</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-10-14 09:40:21 +08:00
|
|
|
|
// @ts-nocheck
|
2025-10-13 11:40:34 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
name: 'EmsOverview',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
viewLevel: 1, // 1/2/3 切换视图
|
|
|
|
|
|
scale: 1,
|
|
|
|
|
|
viewBox: '0 0 1200 700',
|
|
|
|
|
|
selectedAreaId: null,
|
|
|
|
|
|
selectedPoint: null,
|
|
|
|
|
|
selectedArea: null,
|
|
|
|
|
|
pointDialogOpen: false,
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 查询
|
|
|
|
|
|
query: {
|
|
|
|
|
|
keyword: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
highlightedIds: new Set(),
|
|
|
|
|
|
|
|
|
|
|
|
infoBox: {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
lines: []
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 示例数据:使用由正方形/长方形拼接的区域形状
|
2025-10-13 11:40:34 +08:00
|
|
|
|
areas: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'A',
|
|
|
|
|
|
name: '冷藏区',
|
2025-10-14 09:40:21 +08:00
|
|
|
|
// 用多个长方形拼接
|
|
|
|
|
|
shapes: [
|
|
|
|
|
|
{ x: 40, y: 60, w: 560, h: 560 }, // 大矩形
|
|
|
|
|
|
{ x: 40, y: 620, w: 560, h: 10 } // 底部细条,强调边界(可选)
|
|
|
|
|
|
],
|
|
|
|
|
|
labelPos: { x: 60, y: 110 },
|
2025-10-13 11:40:34 +08:00
|
|
|
|
fill: '#4c6ef5',
|
|
|
|
|
|
fillActive: '#3b5bdb',
|
|
|
|
|
|
stroke: '#2b2d42',
|
2025-10-14 09:40:21 +08:00
|
|
|
|
// 点位
|
2025-10-13 11:40:34 +08:00
|
|
|
|
pointsInfo: [
|
2025-10-14 09:40:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: 'A1', name: '点位-01', x: 250, y: 220, w: 110, h: 70,
|
|
|
|
|
|
deviceData: { metric1: { label: '温度', value: '4.2℃' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'A2', name: '点位-02', x: 520, y: 260, w: 110, h: 70,
|
|
|
|
|
|
deviceData: { metric1: { label: '温度', value: '5.1℃' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'A3', name: '点位-03', x: 420, y: 420, w: 110, h: 70,
|
|
|
|
|
|
deviceData: { metric1: { label: '温度', value: '7.8℃' }, metric2: { label: '状态', value: '告警' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
// 库存示例(点击区域显示)
|
|
|
|
|
|
inventory: { title: '库存', count: 31890 }
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'B',
|
|
|
|
|
|
name: '冷冻区',
|
2025-10-14 09:40:21 +08:00
|
|
|
|
shapes: [
|
|
|
|
|
|
{ x: 620, y: 60, w: 520, h: 280 }, // 上部
|
|
|
|
|
|
{ x: 620, y: 350, w: 520, h: 270 } // 下部
|
|
|
|
|
|
],
|
|
|
|
|
|
labelPos: { x: 640, y: 110 },
|
2025-10-13 11:40:34 +08:00
|
|
|
|
fill: '#4ea8de',
|
|
|
|
|
|
fillActive: '#3a86ff',
|
|
|
|
|
|
stroke: '#1b263b',
|
|
|
|
|
|
pointsInfo: [
|
2025-10-14 09:40:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: 'B1', name: '点位-11', x: 780, y: 220, w: 110, h: 70,
|
|
|
|
|
|
deviceData: { metric1: { label: '温度', value: '-12.3℃' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'B2', name: '点位-12', x: 980, y: 460, w: 110, h: 70,
|
|
|
|
|
|
deviceData: { metric1: { label: '温度', value: '-10.0℃' }, metric2: { label: '状态', value: '维护' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
inventory: { title: '库存', count: 21890 }
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-14 09:40:21 +08:00
|
|
|
|
// 分拣区:原来是不规则多边形,现改为由多个矩形拼接成“侧边栏”效果
|
2025-10-13 11:40:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: 'C',
|
|
|
|
|
|
name: '分拣区',
|
2025-10-14 09:40:21 +08:00
|
|
|
|
shapes: [
|
|
|
|
|
|
{ x: 1160, y: 60, w: 20, h: 560 }, // 竖条边框
|
|
|
|
|
|
{ x: 1080, y: 60, w: 70, h: 130 },
|
|
|
|
|
|
{ x: 1080, y: 200, w: 70, h: 130 },
|
|
|
|
|
|
{ x: 1080, y: 340, w: 70, h: 130 },
|
|
|
|
|
|
{ x: 1080, y: 480, w: 70, h: 140 }
|
|
|
|
|
|
],
|
|
|
|
|
|
labelPos: { x: 1090, y: 90 },
|
2025-10-13 11:40:34 +08:00
|
|
|
|
fill: '#95d5b2',
|
|
|
|
|
|
fillActive: '#74c69d',
|
|
|
|
|
|
stroke: '#2d6a4f',
|
|
|
|
|
|
pointsInfo: [
|
2025-10-14 09:40:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: 'C1', name: '能量棒', x: 1115, y: 130, w: 60, h: 60,
|
|
|
|
|
|
deviceData: { metric1: { label: '重量', value: '1000g' }, metric2: { label: '状态', value: '良好' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'C2', name: '显卡', x: 1115, y: 270, w: 60, h: 60,
|
|
|
|
|
|
deviceData: { metric1: { label: '型号', value: '4001' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'C3', name: '特种钢', x: 1115, y: 410, w: 60, h: 60,
|
|
|
|
|
|
deviceData: { metric1: { label: '数量', value: '999个' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'C4', name: '能量棒', x: 1115, y: 560, w: 60, h: 60,
|
|
|
|
|
|
deviceData: { metric1: { label: '重量', value: '500g' }, metric2: { label: '状态', value: '正常' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
inventory: { title: '库存', count: 4080 }
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
setView(n) {
|
|
|
|
|
|
this.viewLevel = n;
|
|
|
|
|
|
},
|
|
|
|
|
|
zoom(factor) {
|
|
|
|
|
|
this.scale = Math.max(0.5, Math.min(2.5, this.scale * factor));
|
2025-10-14 09:40:21 +08:00
|
|
|
|
const base = { w: 1200, h: 700 };
|
2025-10-13 11:40:34 +08:00
|
|
|
|
const w = base.w / this.scale;
|
|
|
|
|
|
const h = base.h / this.scale;
|
|
|
|
|
|
this.viewBox = `0 0 ${w} ${h}`;
|
|
|
|
|
|
},
|
|
|
|
|
|
resetView() {
|
|
|
|
|
|
this.scale = 1;
|
|
|
|
|
|
this.viewBox = '0 0 1200 700';
|
|
|
|
|
|
this.selectedAreaId = null;
|
|
|
|
|
|
this.selectedPoint = null;
|
|
|
|
|
|
this.selectedArea = null;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
this.hideInfo();
|
|
|
|
|
|
this.highlightedIds = new Set();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 安全读取设备指标
|
|
|
|
|
|
safeMetric(obj, mKey, fKey, defVal) {
|
|
|
|
|
|
if (!obj || !obj.deviceData) return defVal;
|
|
|
|
|
|
const m = obj.deviceData[mKey];
|
|
|
|
|
|
return m && m[fKey] != null ? m[fKey] : defVal;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 点位填充色
|
|
|
|
|
|
ptFill(pt) {
|
|
|
|
|
|
const val = (this.safeMetric(pt, 'metric2', 'value', '') || '').toString();
|
|
|
|
|
|
if (val.includes('告警') || val.includes('异常')) return '#ff6b6b';
|
|
|
|
|
|
if (val.includes('维护')) return '#ffd166';
|
|
|
|
|
|
return '#8ecae6';
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 区域选择 + 信息卡显示(库存)
|
|
|
|
|
|
selectArea(area, evt) {
|
2025-10-13 11:40:34 +08:00
|
|
|
|
this.selectedAreaId = area.id;
|
|
|
|
|
|
this.selectedArea = area;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
this.showInfo(evt, area.name, [
|
|
|
|
|
|
area.inventory?.title || '库存',
|
|
|
|
|
|
`库存数量:${this.formatNumber(area.inventory?.count ?? 0)}`
|
|
|
|
|
|
]);
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 打开点位(弹框 + 信息卡)
|
|
|
|
|
|
openPoint(pt, area, evt) {
|
2025-10-13 11:40:34 +08:00
|
|
|
|
this.selectedPoint = pt;
|
|
|
|
|
|
this.selectedArea = area;
|
|
|
|
|
|
this.pointDialogOpen = true;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
const l1 = `${this.safeMetric(pt, 'metric1', 'label', '指标1')}:${this.safeMetric(pt, 'metric1', 'value', '--')}`;
|
|
|
|
|
|
const l2 = `${this.safeMetric(pt, 'metric2', 'label', '指标2')}:${this.safeMetric(pt, 'metric2', 'value', '--')}`;
|
|
|
|
|
|
this.showInfo(evt, pt.name, [l1, l2]);
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 悬浮信息卡
|
|
|
|
|
|
showInfo(evt, title, lines) {
|
|
|
|
|
|
const wrap = this.$refs.wrapper;
|
|
|
|
|
|
if (!wrap) return;
|
|
|
|
|
|
const rect = wrap.getBoundingClientRect();
|
|
|
|
|
|
const x = evt.clientX - rect.left + 12;
|
|
|
|
|
|
const y = evt.clientY - rect.top + 12;
|
|
|
|
|
|
this.infoBox = { show: true, x, y, title, lines };
|
|
|
|
|
|
},
|
|
|
|
|
|
hideInfo() {
|
|
|
|
|
|
this.infoBox.show = false;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 查询(据位查询):优先走接口,失败回退本地过滤
|
|
|
|
|
|
async handleQuery() {
|
|
|
|
|
|
const kw = (this.query.keyword || '').trim();
|
|
|
|
|
|
this.highlightedIds = new Set();
|
|
|
|
|
|
if (!kw) {
|
|
|
|
|
|
this.hideInfo();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let usedApi = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 存在封装的 API 则调用
|
|
|
|
|
|
if (this.$api && this.$api.ems && this.$api.ems.queryPoints) {
|
|
|
|
|
|
await this.queryPoints({ keyword: kw });
|
|
|
|
|
|
usedApi = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 忽略接口错误,回退本地
|
|
|
|
|
|
usedApi = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 高亮与信息卡(对当前 this.areas 数据集进行)
|
|
|
|
|
|
let firstHit = null;
|
|
|
|
|
|
this.areas.forEach(area => {
|
|
|
|
|
|
(area.pointsInfo || []).forEach(pt => {
|
|
|
|
|
|
const text = [
|
|
|
|
|
|
pt.name,
|
|
|
|
|
|
this.safeMetric(pt, 'metric1', 'label', ''),
|
|
|
|
|
|
this.safeMetric(pt, 'metric1', 'value', ''),
|
|
|
|
|
|
this.safeMetric(pt, 'metric2', 'label', ''),
|
|
|
|
|
|
this.safeMetric(pt, 'metric2', 'value', '')
|
|
|
|
|
|
].join(' ');
|
|
|
|
|
|
if (text.includes(kw)) {
|
|
|
|
|
|
this.highlightedIds.add(pt.id);
|
|
|
|
|
|
if (!firstHit) firstHit = { pt, area };
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (firstHit) {
|
|
|
|
|
|
this.selectedAreaId = firstHit.area.id;
|
|
|
|
|
|
this.selectedArea = firstHit.area;
|
|
|
|
|
|
this.selectedPoint = firstHit.pt;
|
|
|
|
|
|
const wrap = this.$refs.wrapper;
|
|
|
|
|
|
if (wrap) {
|
|
|
|
|
|
const rect = wrap.getBoundingClientRect();
|
|
|
|
|
|
const x = (firstHit.pt.x / (1200 / rect.width)) + 12;
|
|
|
|
|
|
const y = (firstHit.pt.y / (700 / rect.height)) + 12;
|
|
|
|
|
|
this.infoBox = {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
x: Math.min(rect.width - 220, Math.max(8, x)),
|
|
|
|
|
|
y: Math.min(rect.height - 140, Math.max(8, y)),
|
|
|
|
|
|
title: firstHit.pt.name,
|
|
|
|
|
|
lines: [
|
|
|
|
|
|
`${this.safeMetric(firstHit.pt, 'metric1', 'label', '指标1')}:${this.safeMetric(firstHit.pt, 'metric1', 'value', '--')}`,
|
|
|
|
|
|
`${this.safeMetric(firstHit.pt, 'metric2', 'label', '指标2')}:${this.safeMetric(firstHit.pt, 'metric2', 'value', '--')}`
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.info(usedApi ? '接口无匹配结果' : '未查询到匹配点位/设备数据');
|
|
|
|
|
|
this.hideInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
resetQuery() {
|
|
|
|
|
|
this.query.keyword = '';
|
|
|
|
|
|
this.highlightedIds = new Set();
|
|
|
|
|
|
this.hideInfo();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 对接接口:据位查询或区域点位拉取
|
2025-10-13 11:40:34 +08:00
|
|
|
|
queryPoints(params) {
|
2025-10-14 09:40:21 +08:00
|
|
|
|
if (!(this.$api && this.$api.ems && this.$api.ems.queryPoints)) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.$api.ems.queryPoints(params).then(res => {
|
|
|
|
|
|
const data = (res && (res.data ?? res)) || {};
|
|
|
|
|
|
// 结构 A:包含 areas
|
|
|
|
|
|
if (Array.isArray(data.areas)) {
|
|
|
|
|
|
// 将后端 areas 适配为前端结构,缺省字段从现有同 id 区域继承
|
|
|
|
|
|
const existMap = new Map(this.areas.map(a => [a.id, a]));
|
|
|
|
|
|
this.areas = data.areas.map(a => {
|
|
|
|
|
|
const old = existMap.get(a.id) || {};
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: a.id,
|
|
|
|
|
|
name: a.name ?? old.name ?? '',
|
|
|
|
|
|
shapes: Array.isArray(a.shapes) ? a.shapes : (old.shapes || []),
|
|
|
|
|
|
labelPos: a.labelPos ?? old.labelPos ?? { x: 60, y: 110 },
|
|
|
|
|
|
fill: old.fill || '#4c6ef5',
|
|
|
|
|
|
fillActive: old.fillActive || '#3b5bdb',
|
|
|
|
|
|
stroke: old.stroke || '#2b2d42',
|
|
|
|
|
|
inventory: a.inventory ?? old.inventory ?? null,
|
|
|
|
|
|
pointsInfo: Array.isArray(a.points)
|
|
|
|
|
|
? a.points.map(p => ({
|
|
|
|
|
|
id: p.id, name: p.name,
|
|
|
|
|
|
x: p.x, y: p.y, w: p.w, h: p.h,
|
|
|
|
|
|
deviceData: p.deviceData || {}
|
|
|
|
|
|
}))
|
|
|
|
|
|
: (old.pointsInfo || [])
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 结构 B:仅返回 points(需含 areaId)
|
|
|
|
|
|
if (Array.isArray(data.points)) {
|
|
|
|
|
|
const byArea = {};
|
|
|
|
|
|
data.points.forEach(p => {
|
|
|
|
|
|
const aid = p.areaId;
|
|
|
|
|
|
if (!aid) return;
|
|
|
|
|
|
(byArea[aid] = byArea[aid] || []).push({
|
|
|
|
|
|
id: p.id, name: p.name,
|
|
|
|
|
|
x: p.x, y: p.y, w: p.w, h: p.h,
|
|
|
|
|
|
deviceData: p.deviceData || {}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
this.areas = this.areas.map(a => ({
|
|
|
|
|
|
...a,
|
|
|
|
|
|
pointsInfo: byArea[a.id] ? byArea[a.id] : (a.pointsInfo || [])
|
|
|
|
|
|
}));
|
|
|
|
|
|
// 可选:data.areasInfo 覆盖库存
|
|
|
|
|
|
if (Array.isArray(data.areasInfo)) {
|
|
|
|
|
|
const infoMap = new Map(data.areasInfo.map(i => [i.id || i.areaId, i]));
|
|
|
|
|
|
this.areas = this.areas.map(a => ({
|
|
|
|
|
|
...a,
|
|
|
|
|
|
inventory: infoMap.get(a.id)?.inventory ?? a.inventory
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 未识别结构,保持不变
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
this.$message.error('查询接口失败');
|
|
|
|
|
|
// 继续走本地回退
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatNumber(n) {
|
|
|
|
|
|
const str = (n ?? 0).toString();
|
|
|
|
|
|
return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.ems-overview {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
2025-10-14 09:40:21 +08:00
|
|
|
|
.toolbar-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.q-input {
|
|
|
|
|
|
width: 220px;
|
|
|
|
|
|
}
|
2025-10-13 11:40:34 +08:00
|
|
|
|
.toolbar-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-13 11:40:34 +08:00
|
|
|
|
.canvas-wrapper {
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
height: 720px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.overview-svg {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.area-label {
|
2025-10-14 09:40:21 +08:00
|
|
|
|
fill: #ffffff;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
font-size: 16px;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
font-weight: 700;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.pt-title, .pt-sub {
|
2025-10-14 09:40:21 +08:00
|
|
|
|
fill: #ffffff;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
text-anchor: middle;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
user-select: none;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.pt-highlight {
|
|
|
|
|
|
stroke: #ffb703 !important;
|
|
|
|
|
|
stroke-width: 3 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-box {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
|
max-width: 240px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
box-shadow: 0 6px 20px rgba(0,0,0,0.18);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 6px;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-14 09:40:21 +08:00
|
|
|
|
.info-line {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 11:40:34 +08:00
|
|
|
|
.point-ul {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|