12 changed files with 671 additions and 80 deletions
@ -0,0 +1,168 @@
@@ -0,0 +1,168 @@
|
||||
<template> |
||||
<div id="map" style="height: 100%; width: 100%; overflow: hidden"></div> |
||||
</template> |
||||
|
||||
<script> |
||||
import AMapLoader from '@amap/amap-jsapi-loader'; |
||||
|
||||
/** |
||||
* props.rectangles: Array<{ id, name, status, geomWkt }> |
||||
* - geomWkt: "POLYGON((lng lat,lng lat,...))" |
||||
* - status: 施工中 | 已施工 | 未施工 | 故障 | 运行中 | 停机 |
||||
*/ |
||||
const STATUS_STYLE = { |
||||
施工中: { stroke: 'rgba(41,169,253,1)', fill: 'rgba(41,169,253,0.45)' }, |
||||
已施工: { stroke: 'rgba(82,196,26,1)', fill: 'rgba(82,196,26,0.45)' }, |
||||
未施工: { stroke: 'rgba(250,173,20,1)', fill: 'rgba(250,173,20,0.45)' }, |
||||
故障: { stroke: 'rgba(219,77,77,1)', fill: 'rgba(219,77,77,0.6)' }, |
||||
运行中: { stroke: 'rgba(19,194,194,1)', fill: 'rgba(19,194,194,0.45)' }, |
||||
停机: { stroke: 'rgba(99,99,99,1)', fill: 'rgba(0,0,0,0.45)' } |
||||
}; |
||||
|
||||
export default { |
||||
name: 'TaskMapPolygon', |
||||
props: { |
||||
rectangles: { |
||||
type: Array, |
||||
default: () => [] |
||||
} |
||||
}, |
||||
data() { |
||||
return { |
||||
map: null, |
||||
AMapIns: null, |
||||
polygons: [], |
||||
texts: [], |
||||
pointAll: { lng: [], lat: [] } |
||||
}; |
||||
}, |
||||
watch: { |
||||
rectangles: { |
||||
immediate: true, |
||||
deep: true, |
||||
handler(val) { |
||||
if (this.map && val?.length) { |
||||
this.clearLayers(); |
||||
this.formatData(val); |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
mounted() { |
||||
this.initAMap(); |
||||
}, |
||||
beforeUnmount() { |
||||
this.map?.destroy(); |
||||
}, |
||||
methods: { |
||||
initAMap() { |
||||
const map_center = JSON.parse(this.$storage.get('map_center') || 'null'); |
||||
let defaultCenterPoint = [121.5563, 32.107]; |
||||
if (map_center) { |
||||
defaultCenterPoint = map_center.find((el) => el.key == 'default')?.value || defaultCenterPoint; |
||||
} |
||||
|
||||
window._AMapSecurityConfig = { securityJsCode: '5869fe06ab3640b49006f4ec5e595e9f' }; |
||||
AMapLoader.load({ |
||||
key: '3758e4530242ba6282c77f96ea69262d', |
||||
version: '2.0', |
||||
plugins: ['AMap.MouseTool', 'AMap.Scale', 'AMap.Text'] |
||||
}) |
||||
.then((AMap) => { |
||||
this.AMapIns = AMap; |
||||
this.map = new AMap.Map('map', { |
||||
viewMode: '2D', |
||||
zoom: 12, |
||||
center: defaultCenterPoint |
||||
}); |
||||
if (this.rectangles?.length) { |
||||
this.formatData(this.rectangles); |
||||
} |
||||
}) |
||||
.catch(console.error); |
||||
}, |
||||
clearLayers() { |
||||
if (!this.map) return; |
||||
this.map.remove(this.polygons); |
||||
this.map.remove(this.texts); |
||||
this.polygons = []; |
||||
this.texts = []; |
||||
this.pointAll = { lng: [], lat: [] }; |
||||
}, |
||||
formatData(data = []) { |
||||
// 解析 WKT -> AMap path,并着色绘制(使用原始坐标,不做偏移转换) |
||||
const list = (data || []).map((item, index) => { |
||||
const raw = (item.geomWkt || '') |
||||
.replace(/POLYGON\(\(/i, '') |
||||
.replace(/\)\)$/i, '') |
||||
.trim(); |
||||
const points = raw.split(','); |
||||
const path = []; |
||||
for (let i = 0; i < points.length; i++) { |
||||
const parts = points[i].trim().split(/\s+/); |
||||
const lng = Number(parts[0]); |
||||
const lat = Number(parts[1]); |
||||
if (isNaN(lng) || isNaN(lat)) continue; |
||||
this.pointAll.lng.push(lng); |
||||
this.pointAll.lat.push(lat); |
||||
path.push([lng, lat]); |
||||
} |
||||
return { ...item, path, index }; |
||||
}); |
||||
|
||||
list.forEach((it) => this.createPolygon(it)); |
||||
|
||||
// 视野自适配:基于多边形覆盖物 |
||||
if (this.polygons.length) { |
||||
// padding: [top, right, bottom, left] |
||||
this.map.setFitView(this.polygons, false, [60, 60, 60, 60]); |
||||
} |
||||
}, |
||||
createPolygon(item) { |
||||
const { path, status, name } = item; |
||||
const style = STATUS_STYLE[status] || STATUS_STYLE['未施工']; |
||||
const polygon = new this.AMapIns.Polygon({ |
||||
path, |
||||
strokeColor: style.stroke, |
||||
strokeWeight: 2, |
||||
strokeOpacity: 1, |
||||
strokeStyle: 'solid', |
||||
fillColor: style.fill, |
||||
fillOpacity: 0.45, |
||||
cursor: 'pointer', |
||||
zIndex: 50 |
||||
}); |
||||
console.log(polygon, 'polygon'); |
||||
this.map.add(polygon); |
||||
this.polygons.push(polygon); |
||||
|
||||
// 居中名称标签 |
||||
const text = new this.AMapIns.Text({ |
||||
text: name || '', |
||||
anchor: 'center', |
||||
style: { 'background-color': 'transparent', border: 'none', color: '#ffffff', 'font-size': '12px' } |
||||
}); |
||||
// 计算简单中心点 |
||||
const mid = path[Math.floor(path.length / 2)] || path[0]; |
||||
if (mid) text.setPosition(mid); |
||||
text.setMap(this.map); |
||||
this.texts.push(text); |
||||
|
||||
// hover 高亮 |
||||
polygon.on('mouseover', () => { |
||||
polygon.setOptions({ fillOpacity: 0.55 }); |
||||
}); |
||||
polygon.on('mouseout', () => { |
||||
polygon.setOptions({ fillOpacity: 0.35 }); |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
#map { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,348 @@
@@ -0,0 +1,348 @@
|
||||
<template> |
||||
<div class="task-screen"> |
||||
<div class="map-wrap"> |
||||
<MapComponent :rectangles="rectangles" /> |
||||
<div class="legend"> |
||||
<div class="legend-item" v-for="(item, i) in legends" :key="i"> |
||||
<span class="dot" :style="{ background: item.color }"></span> |
||||
<span class="text">{{ item.label }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="side left"> |
||||
<div class="panel"> |
||||
<div class="panel-title"> |
||||
<img class="titleIcon" :src="titleIcon" /> |
||||
<span>工区统计</span> |
||||
<span class="more">更多</span> |
||||
</div> |
||||
|
||||
<div class="progress-group"> |
||||
<div class="progress-row"> |
||||
<div class="label">工区总进度</div> |
||||
<div class="value">{{ areaProgress }}%</div> |
||||
</div> |
||||
<fks-progress :percentage="areaProgress" :stroke-width="8" color="#4dabf7" /> |
||||
<div class="progress-sub"> |
||||
<div class="sub-row" v-for="(p, pi) in subProgress" :key="pi"> |
||||
<div class="name">{{ p.name }}</div> |
||||
<div class="bar"> |
||||
<fks-progress :percentage="p.value" :stroke-width="6" :show-text="false" color="#91c9ff" /> |
||||
</div> |
||||
<div class="num">{{ p.value }}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<fks-table :data="areaTable" border height="280"> |
||||
<fks-table-column type="index" label="#" width="50" /> |
||||
<fks-table-column prop="name" label="工区名称" min-width="90" /> |
||||
<fks-table-column prop="pile" label="桩条安装进度(%)" width="140" /> |
||||
<fks-table-column prop="module" label="组件安装进度(%)" width="140" /> |
||||
<fks-table-column prop="owner" label="负责安环" width="90" /> |
||||
<fks-table-column prop="year" label="10号车" width="80" /> |
||||
</fks-table> |
||||
</div> |
||||
|
||||
<div class="panel"> |
||||
<div class="panel-title"> |
||||
<img class="titleIcon" :src="titleIcon" /> |
||||
<span>材料统计</span> |
||||
<span class="more">更多</span> |
||||
</div> |
||||
<fks-table :data="materialTable" border height="300"> |
||||
<fks-table-column prop="name" label="材料名称" min-width="100" /> |
||||
<fks-table-column prop="used" label="当日消耗量" width="120" /> |
||||
<fks-table-column prop="stock" label="库存量" width="100" /> |
||||
</fks-table> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="side right"> |
||||
<div class="panel"> |
||||
<div class="panel-title"> |
||||
<img class="titleIcon" :src="titleIcon" /> |
||||
<span>设备信息</span> |
||||
<span class="more">更多</span> |
||||
</div> |
||||
|
||||
<div class="kpibox"> |
||||
<div class="kpi"> |
||||
<div class="kpi-label">当前设备在线数量</div> |
||||
<div class="kpi-value">{{ deviceStats.online }}</div> |
||||
</div> |
||||
<div class="kpi"> |
||||
<div class="kpi-label">组件设备</div> |
||||
<div class="kpi-value">{{ deviceStats.component }}</div> |
||||
</div> |
||||
<div class="kpi"> |
||||
<div class="kpi-label">当前设备安装数量</div> |
||||
<div class="kpi-value">{{ deviceStats.installing }}</div> |
||||
</div> |
||||
<div class="kpi"> |
||||
<div class="kpi-label">组件安装数</div> |
||||
<div class="kpi-value">{{ deviceStats.moduleInstalled }}</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<fks-table :data="deviceTable" border height="280"> |
||||
<fks-table-column prop="id" label="设备ID" width="80" /> |
||||
<fks-table-column prop="area" label="所属工区" width="90" /> |
||||
<fks-table-column prop="team" label="工作小组" width="100" /> |
||||
<fks-table-column prop="dayWork" label="当日工作量" width="110" /> |
||||
<fks-table-column prop="runtime" label="运行时长(h)" width="110" /> |
||||
</fks-table> |
||||
</div> |
||||
|
||||
<div class="panel"> |
||||
<div class="panel-title"> |
||||
<img class="titleIcon" :src="titleIcon" /> |
||||
<span>报警信息</span> |
||||
<span class="more">更多</span> |
||||
</div> |
||||
<fks-table :data="alarmTable" border height="300"> |
||||
<fks-table-column prop="code" label="报警编号" width="100" /> |
||||
<fks-table-column prop="device" label="设备编号" width="100" /> |
||||
<fks-table-column prop="desc" label="报警内容" min-width="140" /> |
||||
<fks-table-column prop="time" label="报警时间" width="160" /> |
||||
</fks-table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import Mix from '@/mixins/module'; |
||||
import MapComponent from './MapComponent.vue'; |
||||
|
||||
export default { |
||||
name: 'TaskScreenView', |
||||
mixins: [Mix], |
||||
components: { MapComponent }, |
||||
data() { |
||||
return { |
||||
titleIcon: require('@/assets/img/Automated/titleIcon1.png'), |
||||
areaProgress: 30, |
||||
subProgress: [ |
||||
{ name: '桩条安装进度(%)', value: 10 }, |
||||
{ name: '组件安装进度(%)', value: 10 } |
||||
], |
||||
areaTable: [ |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' }, |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' }, |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' }, |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' }, |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' }, |
||||
{ name: 'xxx', pile: 10, module: 10, owner: '10号车', year: '10号车' } |
||||
], |
||||
materialTable: [ |
||||
{ name: 'xxx', used: 128, stock: 3 }, |
||||
{ name: 'xxx', used: 64, stock: 4 }, |
||||
{ name: 'xxx', used: 325, stock: 5 }, |
||||
{ name: 'xxx', used: 1900, stock: 5 }, |
||||
{ name: 'xxx', used: 1900, stock: 5 }, |
||||
{ name: 'xxx', used: 1900, stock: 5 } |
||||
], |
||||
deviceStats: { |
||||
online: 2234, |
||||
component: 2234, |
||||
installing: 2234, |
||||
moduleInstalled: 2234 |
||||
}, |
||||
deviceTable: [ |
||||
{ id: '001', area: 'xxx', team: 'xxx', dayWork: 128, runtime: 18 }, |
||||
{ id: '002', area: 'xxx', team: 'xxx', dayWork: 325, runtime: 18 }, |
||||
{ id: '003', area: 'xxx', team: 'xxx', dayWork: 1900, runtime: 18 }, |
||||
{ id: '004', area: 'xxx', team: 'xxx', dayWork: 1900, runtime: 18 }, |
||||
{ id: '005', area: 'xxx', team: 'xxx', dayWork: 325, runtime: 18 } |
||||
], |
||||
alarmTable: [ |
||||
{ code: 'A001', device: 'xxx', desc: '报警内容', time: '2024-01-01 12:01' }, |
||||
{ code: 'A002', device: 'xxx', desc: '报警内容', time: '2024-01-01 12:10' }, |
||||
{ code: 'A003', device: 'xxx', desc: '报警内容', time: '2024-01-01 12:35' }, |
||||
{ code: 'A004', device: 'xxx', desc: '报警内容', time: '2024-01-01 12:58' } |
||||
], |
||||
rectangles: [ |
||||
// 西湖区(放大范围,近似行政区外包) |
||||
{ id: '1', name: '西湖区', status: '施工中', geomWkt: 'POLYGON((120.0000 30.1500,120.2500 30.1500,120.2500 30.3700,120.0000 30.3700,120.0000 30.1500))' }, |
||||
// 拱墅区(放大范围,近似行政区外包) |
||||
{ id: '2', name: '拱墅区', status: '已施工', geomWkt: 'POLYGON((120.0900 30.3000,120.2300 30.3000,120.2300 30.4200,120.0900 30.4200,120.0900 30.3000))' }, |
||||
// 上城区(放大范围,近似行政区外包) |
||||
{ id: '3', name: '上城区', status: '未施工', geomWkt: 'POLYGON((120.1400 30.2000,120.2600 30.2000,120.2600 30.3100,120.1400 30.3100,120.1400 30.2000))' }, |
||||
// 滨江区(放大范围,近似行政区外包) |
||||
{ id: '4', name: '滨江区', status: '故障', geomWkt: 'POLYGON((120.0900 30.1400,120.2700 30.1400,120.2700 30.2400,120.0900 30.2400,120.0900 30.1400))' }, |
||||
// 萧山区(放大范围,近似行政区外包) |
||||
{ id: '5', name: '萧山区', status: '运行中', geomWkt: 'POLYGON((120.1800 30.0200,120.5300 30.0200,120.5300 30.3300,120.1800 30.3300,120.1800 30.0200))' }, |
||||
// 余杭区(放大范围,近似行政区外包) |
||||
{ id: '6', name: '余杭区', status: '停机', geomWkt: 'POLYGON((119.8000 30.1800,120.1000 30.1800,120.1000 30.5000,119.8000 30.5000,119.8000 30.1800))' } |
||||
], |
||||
legends: [ |
||||
{ label: '施工中', color: '#3fa7ff' }, |
||||
{ label: '已施工', color: '#52c41a' }, |
||||
{ label: '未施工', color: '#faad14' }, |
||||
{ label: '故障', color: '#ff4d4f' }, |
||||
{ label: '运行中', color: '#13c2c2' }, |
||||
{ label: '停机', color: '#bfbfbf' } |
||||
] |
||||
}; |
||||
} |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
.task-screen { |
||||
position: relative; |
||||
display: flex; |
||||
height: 100%; |
||||
width: 100%; |
||||
background: #0b1535; |
||||
} |
||||
.map-wrap { |
||||
position: relative; |
||||
flex: 1; |
||||
min-height: 600px; |
||||
} |
||||
.legend { |
||||
position: absolute; |
||||
left: 50%; |
||||
transform: translateX(-50%); |
||||
bottom: 12px; |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 8px 12px; |
||||
background: rgba(0, 12, 32, 0.7); |
||||
border: 1px solid rgba(91, 166, 255, 0.35); |
||||
border-radius: 6px; |
||||
.legend-item { |
||||
display: flex; |
||||
align-items: center; |
||||
margin: 0 10px; |
||||
.dot { |
||||
width: 12px; |
||||
height: 12px; |
||||
border-radius: 50%; |
||||
margin-right: 6px; |
||||
} |
||||
.text { |
||||
color: #cfe6ff; |
||||
font-size: 12px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.side { |
||||
width: 360px; |
||||
padding: 12px; |
||||
box-sizing: border-box; |
||||
.panel + .panel { |
||||
margin-top: 12px; |
||||
} |
||||
} |
||||
.left { |
||||
position: absolute; |
||||
left: 0; |
||||
top: 0; |
||||
bottom: 0; |
||||
overflow: auto; |
||||
} |
||||
.right { |
||||
position: absolute; |
||||
right: 0; |
||||
top: 0; |
||||
bottom: 0; |
||||
overflow: auto; |
||||
} |
||||
|
||||
.panel { |
||||
background: rgba(0, 12, 32, 0.7); |
||||
border: 1px solid rgba(91, 166, 255, 0.35); |
||||
border-radius: 8px; |
||||
padding: 12px; |
||||
.panel-title { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
color: #cfe6ff; |
||||
font-size: 14px; |
||||
margin-bottom: 10px; |
||||
.titleIcon { |
||||
width: 18px; |
||||
height: 18px; |
||||
margin-right: 6px; |
||||
} |
||||
span { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
} |
||||
.more { |
||||
color: #6bb1ff; |
||||
cursor: pointer; |
||||
font-size: 12px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.progress-group { |
||||
margin-bottom: 12px; |
||||
.progress-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
color: #cfe6ff; |
||||
margin-bottom: 6px; |
||||
.label { |
||||
font-size: 13px; |
||||
} |
||||
.value { |
||||
font-size: 13px; |
||||
} |
||||
} |
||||
.progress-sub { |
||||
margin-top: 8px; |
||||
.sub-row { |
||||
display: grid; |
||||
grid-template-columns: 120px 1fr 36px; |
||||
align-items: center; |
||||
color: #cfe6ff; |
||||
font-size: 12px; |
||||
margin-bottom: 6px; |
||||
.name { |
||||
opacity: 0.85; |
||||
} |
||||
.bar { |
||||
padding: 0 8px; |
||||
} |
||||
.num { |
||||
text-align: right; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.kpibox { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
grid-gap: 8px; |
||||
margin-bottom: 12px; |
||||
.kpi { |
||||
background: rgba(8, 26, 58, 0.7); |
||||
border: 1px solid rgba(91, 166, 255, 0.25); |
||||
border-radius: 6px; |
||||
padding: 10px 8px; |
||||
.kpi-label { |
||||
color: #94b8ff; |
||||
font-size: 12px; |
||||
margin-bottom: 4px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.kpi-value { |
||||
color: #e8f3ff; |
||||
font-weight: 600; |
||||
font-size: 18px; |
||||
line-height: 1.2; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
const route = |
||||
{ |
||||
path: '/ScreenViewManage', |
||||
name: 'ScreenViewManage', |
||||
component: () => import('./index.vue'), |
||||
title: 'SECOND_MENU_ROBOT_TASK_SCREEN_VIEW', // 使用翻译键
|
||||
meta: { |
||||
title: 'SECOND_MENU_ROBOT_TASK_SCREEN_VIEW' // 也在meta中使用翻译键
|
||||
} |
||||
} |
||||
|
||||
|
||||
export default route |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
<template> |
||||
<router-view class="main-wrapper" /> |
||||
</template> |
||||
|
||||
<script> |
||||
import Mix from '@/mixins/module' |
||||
export default { |
||||
name: 'RobotSystemManage', |
||||
mixins: [Mix], |
||||
beforeDestroy() {} |
||||
} |
||||
</script> |
||||
<style scoped> |
||||
</style> |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
const route = |
||||
{ |
||||
path: '/ScreenViewManage', |
||||
name: 'ScreenViewManage', |
||||
component: () => import('./index.vue'), |
||||
title: 'ScreenViewManage', |
||||
} |
||||
|
||||
|
||||
export default route |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
<template> |
||||
<router-view class="main-wrapper" /> |
||||
</template> |
||||
|
||||
<script> |
||||
import Mix from '@/mixins/module'; |
||||
export default { |
||||
name: 'RobotSystemManage', |
||||
mixins: [Mix], |
||||
beforeDestroy() {} |
||||
}; |
||||
</script> |
||||
<style scoped></style> |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
/* |
||||
* @Author: 你的名字 |
||||
* @Date: 2025-01-XX |
||||
* @Description: 运输安装管理系统国际化 |
||||
*/ |
||||
export default { |
||||
// 主系统
|
||||
PUTMS: '运输安装管理系统', |
||||
|
||||
// 一级菜单
|
||||
FIRST_MENU_ROBOT_SCREEN_VIEW_MANAGE: '大屏管理', |
||||
// 二级菜单
|
||||
SECOND_MENU_ROBOT_TASK_SCREEN_VIEW: '任务大屏' |
||||
}; |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
const route = |
||||
{ |
||||
path: '/RobotSystemManage', |
||||
name: 'RobotSystemManage', |
||||
component: () => import('./index.vue'), |
||||
title: 'RobotSystemManage', |
||||
} |
||||
|
||||
|
||||
export default route |
||||
Loading…
Reference in new issue