You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
683 lines
16 KiB
Vue
683 lines
16 KiB
Vue
<template>
|
|
<main class="amap-page">
|
|
<div ref="mapContainerRef" class="amap-container"></div>
|
|
|
|
<header class="top-menu">
|
|
<h1>机场道面外来物检测系统</h1>
|
|
<nav class="menu-actions" aria-label="页面菜单">
|
|
<el-button
|
|
v-for="item in menuItems"
|
|
:key="item.key"
|
|
:type="activeDialog === item.key ? 'primary' : 'default'"
|
|
@click="handleMenuClick(item.key)"
|
|
>
|
|
{{ item.label }}
|
|
</el-button>
|
|
</nav>
|
|
</header>
|
|
|
|
<section class="bottom-card-row" aria-label="底部监控信息">
|
|
<section class="video-card" aria-label="视频监控">
|
|
<VideoView />
|
|
</section>
|
|
|
|
<el-card class="fod-detail-card" shadow="never">
|
|
<section class="fod-detail-panel" aria-label="异物检测详情">
|
|
<div class="fod-header">
|
|
<span>异物检测信息</span>
|
|
<el-tag size="small" :type="statusTagType" effect="dark">
|
|
{{ detectionInfo.status }}
|
|
</el-tag>
|
|
</div>
|
|
|
|
<dl class="fod-grid">
|
|
<div v-for="item in detectionFields" :key="item.label" class="fod-item">
|
|
<dt>{{ item.label }}</dt>
|
|
<dd>{{ item.value }}</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
<div class="fod-images">
|
|
<el-image
|
|
v-for="(image, index) in detectionImages"
|
|
:key="image"
|
|
class="fod-image"
|
|
:src="image"
|
|
:preview-src-list="detectionImages"
|
|
:initial-index="index"
|
|
fit="cover"
|
|
preview-teleported
|
|
/>
|
|
</div>
|
|
</section>
|
|
</el-card>
|
|
|
|
<el-card class="fod-table-card" shadow="never">
|
|
<section class="fod-table-panel" aria-label="异物检测列表">
|
|
<el-table :data="detectionRows" height="100%" size="small" class="fod-table">
|
|
<el-table-column prop="index" label="序号" width="52" align="center" />
|
|
<el-table-column prop="location" label="经纬度" min-width="132" show-overflow-tooltip />
|
|
<el-table-column prop="discoveredAt" label="发现时间" min-width="132" show-overflow-tooltip />
|
|
<el-table-column label="操作" width="60" align="center">
|
|
<template #default="{ row }">
|
|
<el-button link type="primary" size="small" @click="selectDetection(row)">
|
|
查看
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</section>
|
|
</el-card>
|
|
</section>
|
|
|
|
<el-dialog
|
|
v-model="dialogVisible"
|
|
:title="currentDialogTitle"
|
|
width="76vw"
|
|
class="system-dialog"
|
|
append-to-body
|
|
destroy-on-close
|
|
@closed="activeDialog = ''"
|
|
>
|
|
<component :is="currentDialogComponent" v-if="currentDialogComponent" />
|
|
</el-dialog>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import BlockedDataView from '../blocked-data/index.vue'
|
|
import ConfigView from '../config/index.vue'
|
|
import HistoryDataView from '../history-data/index.vue'
|
|
import VideoView from '../video/index.vue'
|
|
|
|
const AMAP_KEY = 'f570ff5caa2f5c956fc6bf66cacabb93'
|
|
const AMAP_SECURITY_CODE = '114b8c8d2356490283ea98293a9c4ae3'
|
|
const AMAP_SCRIPT_ID = 'amap-jsapi'
|
|
const AMAP_CALLBACK_NAME = '__initAmapMapView'
|
|
const MAP_CENTER = [120.09671, 36.365238]
|
|
const MAP_ZOOM = 15
|
|
const MAP_ROTATION = -72.6
|
|
|
|
const mapContainerRef = ref(null)
|
|
const activeDialog = ref('')
|
|
const detectionInfo = {
|
|
discoveredAt: '2026-06-12 14:35:20',
|
|
location: '120.096710E, 36.365238N',
|
|
intensity: '82%',
|
|
count: 3,
|
|
category: '疑似异物',
|
|
status: '待处理',
|
|
}
|
|
const detectionImages = [
|
|
createDetectionImage('#0f766e', 'FOD-01'),
|
|
createDetectionImage('#1d4ed8', 'FOD-02'),
|
|
createDetectionImage('#b45309', 'FOD-03'),
|
|
]
|
|
const detectionRows = [
|
|
{
|
|
index: 1,
|
|
location: '120.096710, 36.365238',
|
|
discoveredAt: '2026-06-12 14:35:20',
|
|
},
|
|
{
|
|
index: 2,
|
|
location: '120.096842, 36.365104',
|
|
discoveredAt: '2026-06-12 14:36:08',
|
|
},
|
|
{
|
|
index: 3,
|
|
location: '120.096514, 36.365387',
|
|
discoveredAt: '2026-06-12 14:37:42',
|
|
},
|
|
{
|
|
index: 4,
|
|
location: '120.096933, 36.365291',
|
|
discoveredAt: '2026-06-12 14:39:16',
|
|
},
|
|
]
|
|
const detectionFields = computed(() => [
|
|
{ label: '发现时间', value: detectionInfo.discoveredAt },
|
|
{ label: '位置信息', value: detectionInfo.location },
|
|
{ label: '探测强度', value: detectionInfo.intensity },
|
|
{ label: '检测次数', value: detectionInfo.count },
|
|
{ label: '异物类别', value: detectionInfo.category },
|
|
{ label: '处理状态', value: detectionInfo.status },
|
|
])
|
|
const statusTagType = computed(() => {
|
|
if (detectionInfo.status === '已处理') {
|
|
return 'success'
|
|
}
|
|
|
|
if (detectionInfo.status === '处理中') {
|
|
return 'warning'
|
|
}
|
|
|
|
return 'danger'
|
|
})
|
|
const menuItems = [
|
|
{ key: 'home', label: '首页' },
|
|
{ key: 'history', label: '历史数据' },
|
|
{ key: 'config', label: '配置' },
|
|
{ key: 'blocked', label: '屏蔽数据' },
|
|
]
|
|
const dialogMap = {
|
|
history: {
|
|
title: '历史数据',
|
|
component: HistoryDataView,
|
|
},
|
|
config: {
|
|
title: '配置',
|
|
component: ConfigView,
|
|
},
|
|
blocked: {
|
|
title: '屏蔽数据',
|
|
component: BlockedDataView,
|
|
},
|
|
}
|
|
const dialogVisible = computed({
|
|
get: () => Boolean(activeDialog.value),
|
|
set: (value) => {
|
|
if (!value) {
|
|
activeDialog.value = ''
|
|
}
|
|
},
|
|
})
|
|
const currentDialogTitle = computed(() => dialogMap[activeDialog.value]?.title || '')
|
|
const currentDialogComponent = computed(() => dialogMap[activeDialog.value]?.component || null)
|
|
|
|
let mapInstance = null
|
|
let amapScriptPromise = null
|
|
|
|
function createDetectionImage(color, label) {
|
|
const svg = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200" viewBox="0 0 320 200">
|
|
<defs>
|
|
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0" stop-color="${color}" stop-opacity="0.95"/>
|
|
<stop offset="1" stop-color="#020617" stop-opacity="1"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="320" height="200" fill="url(#bg)"/>
|
|
<path d="M0 132 C62 108 94 154 150 130 C206 106 244 82 320 112 L320 200 L0 200 Z" fill="#111827" opacity="0.72"/>
|
|
<rect x="18" y="18" width="284" height="164" rx="10" fill="none" stroke="#e2e8f0" stroke-opacity="0.36"/>
|
|
<circle cx="160" cy="100" r="34" fill="none" stroke="#fef3c7" stroke-width="2.5" stroke-dasharray="7 6"/>
|
|
<path d="M141 105 L158 74 L180 121 Z" fill="#fbbf24" opacity="0.86"/>
|
|
<text x="24" y="44" fill="#f8fafc" font-size="18" font-family="Arial, sans-serif" font-weight="700">${label}</text>
|
|
<text x="24" y="168" fill="#cbd5e1" font-size="12" font-family="Arial, sans-serif">Foreign Object Detection Snapshot</text>
|
|
</svg>`
|
|
|
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
|
}
|
|
|
|
function waitForAmapMapConstructor(timeout = 10000) {
|
|
return new Promise((resolve, reject) => {
|
|
const startedAt = Date.now()
|
|
|
|
const checkAmap = () => {
|
|
if (window.AMap?.Map) {
|
|
resolve(window.AMap)
|
|
return
|
|
}
|
|
|
|
if (Date.now() - startedAt >= timeout) {
|
|
reject(new Error('AMap Map constructor is unavailable'))
|
|
return
|
|
}
|
|
|
|
window.setTimeout(checkAmap, 50)
|
|
}
|
|
|
|
checkAmap()
|
|
})
|
|
}
|
|
|
|
function loadAmapScript() {
|
|
window._AMapSecurityConfig = {
|
|
securityJsCode: AMAP_SECURITY_CODE,
|
|
}
|
|
|
|
if (window.AMap?.Map) {
|
|
return Promise.resolve(window.AMap)
|
|
}
|
|
|
|
if (amapScriptPromise) {
|
|
return amapScriptPromise
|
|
}
|
|
|
|
if (document.getElementById(AMAP_SCRIPT_ID)) {
|
|
amapScriptPromise = waitForAmapMapConstructor()
|
|
return amapScriptPromise
|
|
}
|
|
|
|
amapScriptPromise = new Promise((resolve, reject) => {
|
|
window[AMAP_CALLBACK_NAME] = () => {
|
|
waitForAmapMapConstructor().then(resolve).catch(reject)
|
|
}
|
|
|
|
const script = document.createElement('script')
|
|
script.id = AMAP_SCRIPT_ID
|
|
script.async = true
|
|
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}&callback=${AMAP_CALLBACK_NAME}`
|
|
script.onload = () => {
|
|
waitForAmapMapConstructor().then(resolve).catch(reject)
|
|
}
|
|
script.onerror = (error) => {
|
|
amapScriptPromise = null
|
|
reject(error)
|
|
}
|
|
document.head.appendChild(script)
|
|
})
|
|
|
|
amapScriptPromise = amapScriptPromise.catch((error) => {
|
|
amapScriptPromise = null
|
|
throw error
|
|
})
|
|
|
|
return amapScriptPromise
|
|
}
|
|
|
|
function selectDetection(row) {
|
|
console.info('查看异物检测记录:', row)
|
|
}
|
|
|
|
function handleMenuClick(key) {
|
|
if (key === 'home') {
|
|
activeDialog.value = ''
|
|
return
|
|
}
|
|
|
|
activeDialog.value = key
|
|
}
|
|
|
|
async function initMap() {
|
|
try {
|
|
const AMap = await loadAmapScript()
|
|
|
|
if (!AMap?.Map) {
|
|
throw new Error('AMap Map constructor is unavailable')
|
|
}
|
|
|
|
mapInstance = new AMap.Map(mapContainerRef.value, {
|
|
viewMode: '3D',
|
|
zoom: MAP_ZOOM,
|
|
center: MAP_CENTER,
|
|
pitch: 0,
|
|
rotation: MAP_ROTATION,
|
|
showLabel: false,
|
|
layers: [
|
|
new AMap.TileLayer.Satellite(),
|
|
],
|
|
})
|
|
} catch (error) {
|
|
console.error('Load AMap failed:', error)
|
|
}
|
|
}
|
|
|
|
onMounted(initMap)
|
|
|
|
onBeforeUnmount(() => {
|
|
mapInstance?.destroy()
|
|
mapInstance = null
|
|
delete window[AMAP_CALLBACK_NAME]
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.amap-page {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background: #030712;
|
|
}
|
|
|
|
.amap-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.top-menu {
|
|
position: absolute;
|
|
top: 18px;
|
|
left: 24px;
|
|
right: 24px;
|
|
z-index: 4;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
min-height: 56px;
|
|
padding: 0 18px;
|
|
border: 1px solid rgba(226, 232, 240, 0.26);
|
|
border-radius: 14px;
|
|
color: #f8fafc;
|
|
background: rgba(2, 6, 23, 0.5);
|
|
box-shadow: 0 20px 70px rgba(0, 0, 0, 0.28);
|
|
backdrop-filter: blur(14px);
|
|
}
|
|
|
|
.top-menu h1 {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-size: 20px;
|
|
font-weight: 800;
|
|
letter-spacing: 0;
|
|
line-height: 1.2;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.menu-actions {
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.menu-actions :deep(.el-button) {
|
|
min-width: 86px;
|
|
margin: 0;
|
|
border-color: rgba(226, 232, 240, 0.22);
|
|
border-radius: 8px;
|
|
color: #e5eefb;
|
|
background: rgba(15, 23, 42, 0.66);
|
|
}
|
|
|
|
.menu-actions :deep(.el-button:hover) {
|
|
border-color: rgba(56, 189, 248, 0.72);
|
|
color: #ffffff;
|
|
background: rgba(14, 116, 144, 0.72);
|
|
}
|
|
|
|
.menu-actions :deep(.el-button--primary) {
|
|
border-color: rgba(34, 211, 238, 0.85);
|
|
background: rgba(8, 145, 178, 0.86);
|
|
}
|
|
|
|
.bottom-card-row {
|
|
position: absolute;
|
|
left: 24px;
|
|
right: 24px;
|
|
bottom: 24px;
|
|
z-index: 2;
|
|
display: grid;
|
|
grid-template-columns: 2fr 2fr 1fr;
|
|
gap: 16px;
|
|
height: 60%;
|
|
min-height: 0;
|
|
}
|
|
|
|
.video-card,
|
|
.fod-detail-card,
|
|
.fod-table-card {
|
|
min-width: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(226, 232, 240, 0.28);
|
|
border-radius: 16px;
|
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.video-card {
|
|
background: rgba(2, 6, 23, 0.28);
|
|
}
|
|
|
|
.video-card :deep(.monitor-page) {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.video-card :deep(.control-panel) {
|
|
top: 12px;
|
|
right: 12px;
|
|
width: 212px;
|
|
padding: 12px;
|
|
transform: scale(0.5);
|
|
transform-origin: top right;
|
|
}
|
|
|
|
.video-card :deep(.stream-status) {
|
|
left: 12px;
|
|
right: 12px;
|
|
bottom: 12px;
|
|
max-width: none;
|
|
}
|
|
|
|
.video-card :deep(.direction-pad) {
|
|
grid-template-columns: repeat(3, 42px);
|
|
grid-template-rows: repeat(3, 38px);
|
|
gap: 7px;
|
|
}
|
|
|
|
.fod-detail-card,
|
|
.fod-table-card {
|
|
color: #e5eefb;
|
|
background: rgba(2, 6, 23, 0.48);
|
|
}
|
|
|
|
.fod-detail-card :deep(.el-card__body),
|
|
.fod-table-card :deep(.el-card__body) {
|
|
height: 100%;
|
|
padding: 14px;
|
|
}
|
|
|
|
.fod-detail-panel,
|
|
.fod-table-panel {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
|
|
.fod-detail-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.fod-header {
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.16);
|
|
color: #f8fafc;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.fod-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px 14px;
|
|
flex: 0 0 auto;
|
|
margin: 12px 0 0;
|
|
}
|
|
|
|
.fod-item {
|
|
min-width: 0;
|
|
}
|
|
|
|
.fod-item dt {
|
|
margin-bottom: 4px;
|
|
color: rgba(203, 213, 225, 0.74);
|
|
font-size: 12px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.fod-item dd {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
color: #f8fafc;
|
|
font-size: 13px;
|
|
line-height: 1.3;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.fod-images {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
align-content: end;
|
|
gap: 8px;
|
|
margin-top: auto;
|
|
padding-top: 10px;
|
|
border-top: 1px solid rgba(226, 232, 240, 0.16);
|
|
}
|
|
|
|
.fod-image {
|
|
width: 100%;
|
|
height: 74px;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(226, 232, 240, 0.18);
|
|
border-radius: 8px;
|
|
background: rgba(15, 23, 42, 0.62);
|
|
cursor: zoom-in;
|
|
}
|
|
|
|
.fod-table-panel {
|
|
overflow: hidden;
|
|
border: 1px solid rgba(226, 232, 240, 0.14);
|
|
border-radius: 10px;
|
|
background: rgba(15, 23, 42, 0.28);
|
|
}
|
|
|
|
.fod-table {
|
|
width: 100%;
|
|
height: 100%;
|
|
color: #e5eefb;
|
|
background: transparent;
|
|
}
|
|
|
|
.fod-table :deep(.el-table__inner-wrapper::before) {
|
|
display: none;
|
|
}
|
|
|
|
.fod-table :deep(.el-table__header-wrapper th) {
|
|
border-bottom-color: rgba(226, 232, 240, 0.16);
|
|
color: rgba(226, 232, 240, 0.82);
|
|
background: rgba(15, 23, 42, 0.72);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.fod-table :deep(.el-table__row) {
|
|
background: transparent;
|
|
}
|
|
|
|
.fod-table :deep(.el-table__row:hover > td.el-table__cell) {
|
|
background: rgba(14, 165, 233, 0.16);
|
|
}
|
|
|
|
.fod-table :deep(td.el-table__cell) {
|
|
border-bottom-color: rgba(226, 232, 240, 0.1);
|
|
color: #e5eefb;
|
|
background: transparent;
|
|
}
|
|
|
|
.fod-table :deep(.el-table__body-wrapper) {
|
|
background: transparent;
|
|
}
|
|
|
|
:global(.system-dialog) {
|
|
border: 1px solid rgba(226, 232, 240, 0.22);
|
|
border-radius: 14px;
|
|
background: rgba(2, 6, 23, 0.92);
|
|
box-shadow: 0 32px 120px rgba(0, 0, 0, 0.56);
|
|
backdrop-filter: blur(18px);
|
|
}
|
|
|
|
:global(.system-dialog .el-dialog__header) {
|
|
padding: 18px 20px 12px;
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.14);
|
|
}
|
|
|
|
:global(.system-dialog .el-dialog__title) {
|
|
color: #f8fafc;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
:global(.system-dialog .el-dialog__body) {
|
|
padding: 18px 20px 20px;
|
|
color: #e5eefb;
|
|
}
|
|
|
|
:global(.el-select__popper.el-popper) {
|
|
border-color: rgba(226, 232, 240, 0.18);
|
|
background: rgba(2, 6, 23, 0.96);
|
|
}
|
|
|
|
:global(.el-select-dropdown) {
|
|
background: transparent;
|
|
}
|
|
|
|
:global(.el-select-dropdown__item) {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
:global(.el-select-dropdown__item.is-hovering),
|
|
:global(.el-select-dropdown__item:hover) {
|
|
color: #ffffff;
|
|
background: rgba(14, 165, 233, 0.18);
|
|
}
|
|
|
|
:global(.el-select-dropdown__item.is-selected) {
|
|
color: #67e8f9;
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
.top-menu {
|
|
top: 12px;
|
|
left: 12px;
|
|
right: 12px;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.top-menu h1 {
|
|
font-size: 17px;
|
|
}
|
|
|
|
.menu-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.menu-actions :deep(.el-button) {
|
|
min-width: 0;
|
|
}
|
|
|
|
.bottom-card-row {
|
|
left: 12px;
|
|
right: 12px;
|
|
bottom: 12px;
|
|
grid-template-columns: 1fr;
|
|
gap: 10px;
|
|
height: 72%;
|
|
overflow: auto;
|
|
}
|
|
|
|
.video-card {
|
|
min-height: 220px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.fod-detail-card,
|
|
.fod-table-card {
|
|
min-height: 220px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.fod-table-panel {
|
|
min-height: 180px;
|
|
}
|
|
|
|
.fod-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|