635 lines
13 KiB
Vue
635 lines
13 KiB
Vue
|
|
<template>
|
||
|
|
<div class="map-page">
|
||
|
|
<!-- 顶部导航栏 -->
|
||
|
|
<div class="nav-bar">
|
||
|
|
<van-icon name="arrow-left" class="nav-back" @click="goBack" />
|
||
|
|
<h1 class="nav-title">预警地图</h1>
|
||
|
|
<van-icon name="filter" class="nav-filter" @click="showFilter = !showFilter" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 预警类型切换按钮 -->
|
||
|
|
<div class="type-tabs">
|
||
|
|
<button
|
||
|
|
class="type-tab"
|
||
|
|
:class="{ active: activeAlertType === 'road' }"
|
||
|
|
@click="activeAlertType = 'road'"
|
||
|
|
>
|
||
|
|
路况预警
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
class="type-tab"
|
||
|
|
:class="{ active: activeAlertType === 'violation' }"
|
||
|
|
@click="activeAlertType = 'violation'"
|
||
|
|
>
|
||
|
|
违章预警
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 地图区域 -->
|
||
|
|
<div class="map-container">
|
||
|
|
<!-- 网格背景 -->
|
||
|
|
<div class="map-grid"></div>
|
||
|
|
|
||
|
|
<!-- 地图标记点 -->
|
||
|
|
<div
|
||
|
|
v-for="marker in filteredMarkers"
|
||
|
|
:key="marker.id"
|
||
|
|
class="marker-point"
|
||
|
|
:class="marker.color"
|
||
|
|
:style="{ top: marker.position.top, left: marker.position.left }"
|
||
|
|
@click="handleMarkerClick(marker)"
|
||
|
|
>
|
||
|
|
<van-icon name="location" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 我的定位标记 -->
|
||
|
|
<div class="my-location" :style="{ top: myLocation.top, left: myLocation.left }">
|
||
|
|
<div class="pulse-ring"></div>
|
||
|
|
<div class="location-inner">
|
||
|
|
<div class="location-dot"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 右下角浮动按钮 -->
|
||
|
|
<div class="float-buttons">
|
||
|
|
<div class="float-btn" @click="handleLocationClick">
|
||
|
|
<van-icon name="location" />
|
||
|
|
</div>
|
||
|
|
<div class="float-btn">
|
||
|
|
<van-icon name="layer" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 标记详情卡片 -->
|
||
|
|
<div v-if="selectedMarker" class="marker-card">
|
||
|
|
<div class="card-header">
|
||
|
|
<div class="card-info">
|
||
|
|
<h3 class="card-title">{{ selectedMarker.title }}</h3>
|
||
|
|
<p class="card-location">{{ selectedMarker.location }}</p>
|
||
|
|
</div>
|
||
|
|
<van-icon name="cross" class="close-icon" @click="selectedMarker = null" />
|
||
|
|
</div>
|
||
|
|
<div class="card-footer">
|
||
|
|
<span class="card-time">{{ selectedMarker.time }}</span>
|
||
|
|
<van-button size="small" round type="primary" class="detail-btn" @click="goToDetail">
|
||
|
|
查看详情
|
||
|
|
</van-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 筛选面板 -->
|
||
|
|
<van-popup v-model:show="showFilter" position="right" class="filter-panel">
|
||
|
|
<div class="filter-content">
|
||
|
|
<div class="filter-header">
|
||
|
|
<span class="filter-title">筛选条件</span>
|
||
|
|
<van-icon name="cross" class="close-icon" @click="showFilter = false" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 预警类型 -->
|
||
|
|
<div class="filter-section">
|
||
|
|
<h3 class="filter-section-title">预警类型</h3>
|
||
|
|
<div class="filter-options">
|
||
|
|
<label
|
||
|
|
v-for="item in alertTypeOptions"
|
||
|
|
:key="item.value"
|
||
|
|
class="filter-option"
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
:checked="filters.alertTypes.includes(item.value)"
|
||
|
|
@change="toggleFilter('alertTypes', item.value)"
|
||
|
|
/>
|
||
|
|
<span class="checkbox-custom"></span>
|
||
|
|
<span class="option-label">{{ item.label }}</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 底部按钮 -->
|
||
|
|
<div class="filter-actions">
|
||
|
|
<van-button block round class="reset-btn" @click="resetFilter">重置</van-button>
|
||
|
|
<van-button block round type="primary" class="confirm-btn" @click="showFilter = false">确定</van-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</van-popup>
|
||
|
|
|
||
|
|
<!-- 底部导航 -->
|
||
|
|
<BottomTabs :active-tab="'map'" />
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed } from "vue";
|
||
|
|
import { useRouter } from "vue-router";
|
||
|
|
import BottomTabs from "@/components/bottomTabs.vue";
|
||
|
|
|
||
|
|
const router = useRouter();
|
||
|
|
|
||
|
|
// 状态
|
||
|
|
const showFilter = ref(false);
|
||
|
|
const activeAlertType = ref("road");
|
||
|
|
const selectedMarker = ref(null);
|
||
|
|
const filters = ref({
|
||
|
|
alertTypes: ["road", "violation", "emergency"],
|
||
|
|
violationTypes: ["reverse", "parking", "other"],
|
||
|
|
statuses: ["pending", "processing", "completed"]
|
||
|
|
});
|
||
|
|
|
||
|
|
const alertTypeOptions = [
|
||
|
|
{ value: "road", label: "路况预警" },
|
||
|
|
{ value: "violation", label: "违章预警" },
|
||
|
|
{ value: "emergency", label: "紧急任务" }
|
||
|
|
];
|
||
|
|
|
||
|
|
// 我的位置
|
||
|
|
const myLocation = ref({
|
||
|
|
top: "60%",
|
||
|
|
left: "40%"
|
||
|
|
});
|
||
|
|
|
||
|
|
// 标记点数据
|
||
|
|
const markers = ref([
|
||
|
|
{
|
||
|
|
id: 1,
|
||
|
|
type: "road",
|
||
|
|
title: "中山路拥堵",
|
||
|
|
location: "中山路与发展大道",
|
||
|
|
time: "10:30",
|
||
|
|
color: "marker-blue",
|
||
|
|
position: { top: "15%", left: "15%" }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 2,
|
||
|
|
type: "emergency",
|
||
|
|
title: "交通事故",
|
||
|
|
location: "解放大道光谷广场",
|
||
|
|
time: "09:15",
|
||
|
|
color: "marker-red",
|
||
|
|
position: { top: "15%", left: "25%" }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 3,
|
||
|
|
type: "road",
|
||
|
|
title: "道路施工",
|
||
|
|
location: "上新河桥",
|
||
|
|
time: "11:00",
|
||
|
|
color: "marker-blue",
|
||
|
|
position: { top: "35%", left: "35%" }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 4,
|
||
|
|
type: "violation",
|
||
|
|
title: "违停车辆",
|
||
|
|
location: "武汉大道CBD",
|
||
|
|
time: "08:45",
|
||
|
|
color: "marker-purple",
|
||
|
|
position: { top: "50%", left: "55%" }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 5,
|
||
|
|
type: "road",
|
||
|
|
title: "路面积水",
|
||
|
|
location: "汉口江滩大道",
|
||
|
|
time: "12:20",
|
||
|
|
color: "marker-blue",
|
||
|
|
position: { top: "50%", left: "45%" }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 6,
|
||
|
|
type: "emergency",
|
||
|
|
title: "紧急疏导",
|
||
|
|
location: "光谷转盘",
|
||
|
|
time: "13:30",
|
||
|
|
color: "marker-red",
|
||
|
|
position: { top: "70%", left: "75%" }
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 过滤后的标记点
|
||
|
|
const filteredMarkers = computed(() => {
|
||
|
|
return markers.value.filter(marker => {
|
||
|
|
if (marker.type !== activeAlertType.value) return false;
|
||
|
|
if (marker.type === "road") return filters.value.alertTypes.includes("road");
|
||
|
|
if (marker.type === "violation") return filters.value.alertTypes.includes("violation");
|
||
|
|
if (marker.type === "emergency") return filters.value.alertTypes.includes("emergency");
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// 返回
|
||
|
|
function goBack() {
|
||
|
|
router.back();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 标记点点击
|
||
|
|
function handleMarkerClick(marker) {
|
||
|
|
selectedMarker.value = marker;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 定位点击
|
||
|
|
function handleLocationClick() {
|
||
|
|
console.log("定位到当前位置");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 切换筛选
|
||
|
|
function toggleFilter(category, value) {
|
||
|
|
const arr = filters.value[category];
|
||
|
|
if (arr.includes(value)) {
|
||
|
|
filters.value[category] = arr.filter(v => v !== value);
|
||
|
|
} else {
|
||
|
|
filters.value[category] = [...arr, value];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重置筛选
|
||
|
|
function resetFilter() {
|
||
|
|
filters.value = {
|
||
|
|
alertTypes: [],
|
||
|
|
violationTypes: [],
|
||
|
|
statuses: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 前往详情
|
||
|
|
function goToDetail() {
|
||
|
|
if (selectedMarker.value) {
|
||
|
|
if (selectedMarker.value.type === "road") {
|
||
|
|
router.push("/traffic-alerts");
|
||
|
|
} else if (selectedMarker.value.type === "violation") {
|
||
|
|
router.push("/violation-alerts");
|
||
|
|
} else {
|
||
|
|
router.push("/emergency-tasks");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.map-page {
|
||
|
|
height: 100vh;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
background: #f3f4f6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-bar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
background: white;
|
||
|
|
padding: 12px 16px;
|
||
|
|
border-bottom: 1px solid #e5e7eb;
|
||
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
|
|
|
||
|
|
.nav-back {
|
||
|
|
font-size: 24px;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-title {
|
||
|
|
font-size: 17px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-filter {
|
||
|
|
font-size: 24px;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.type-tabs {
|
||
|
|
display: flex;
|
||
|
|
gap: 12px;
|
||
|
|
background: white;
|
||
|
|
padding: 12px 16px;
|
||
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
|
|
|
||
|
|
.type-tab {
|
||
|
|
flex: 1;
|
||
|
|
height: 40px;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
border: none;
|
||
|
|
background: #f3f4f6;
|
||
|
|
color: #6b7280;
|
||
|
|
transition: all 0.2s;
|
||
|
|
|
||
|
|
&.active {
|
||
|
|
background: #2563eb;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.map-container {
|
||
|
|
flex: 1;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
background: #e5e7eb;
|
||
|
|
}
|
||
|
|
|
||
|
|
.map-grid {
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
background-image:
|
||
|
|
linear-gradient(to right, #d1d5db 1px, transparent 1px),
|
||
|
|
linear-gradient(to bottom, #d1d5db 1px, transparent 1px);
|
||
|
|
background-size: 50px 50px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.marker-point {
|
||
|
|
position: absolute;
|
||
|
|
width: 48px;
|
||
|
|
height: 48px;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
color: white;
|
||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
z-index: 10;
|
||
|
|
transition: transform 0.2s;
|
||
|
|
|
||
|
|
&:active {
|
||
|
|
transform: translate(-50%, -50%) scale(1.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.van-icon {
|
||
|
|
font-size: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.marker-blue {
|
||
|
|
background: #2563eb;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.marker-red {
|
||
|
|
background: #ef4444;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.marker-purple {
|
||
|
|
background: #8b5cf6;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.my-location {
|
||
|
|
position: absolute;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
z-index: 10;
|
||
|
|
|
||
|
|
.pulse-ring {
|
||
|
|
position: absolute;
|
||
|
|
width: 64px;
|
||
|
|
height: 64px;
|
||
|
|
background: rgba(59, 130, 246, 0.2);
|
||
|
|
border-radius: 50%;
|
||
|
|
top: 50%;
|
||
|
|
left: 50%;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
animation: ping 1.5s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
.location-inner {
|
||
|
|
width: 40px;
|
||
|
|
height: 40px;
|
||
|
|
background: white;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
|
|
|
||
|
|
.location-dot {
|
||
|
|
width: 24px;
|
||
|
|
height: 24px;
|
||
|
|
background: #2563eb;
|
||
|
|
border-radius: 50%;
|
||
|
|
border: 3px solid white;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes ping {
|
||
|
|
0% {
|
||
|
|
transform: translate(-50%, -50%) scale(1);
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
100% {
|
||
|
|
transform: translate(-50%, -50%) scale(1.5);
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.float-buttons {
|
||
|
|
position: absolute;
|
||
|
|
bottom: 100px;
|
||
|
|
right: 16px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 12px;
|
||
|
|
z-index: 10;
|
||
|
|
|
||
|
|
.float-btn {
|
||
|
|
width: 48px;
|
||
|
|
height: 48px;
|
||
|
|
background: white;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
|
|
|
||
|
|
.van-icon {
|
||
|
|
font-size: 20px;
|
||
|
|
color: #374151;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:first-child .van-icon {
|
||
|
|
color: #2563eb;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.marker-card {
|
||
|
|
position: absolute;
|
||
|
|
bottom: 79px;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
background: white;
|
||
|
|
border-radius: 20px 20px 0 0;
|
||
|
|
padding: 16px;
|
||
|
|
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1);
|
||
|
|
z-index: 20;
|
||
|
|
animation: slideUp 0.3s ease;
|
||
|
|
|
||
|
|
.card-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-start;
|
||
|
|
justify-content: space-between;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
|
||
|
|
.card-info {
|
||
|
|
flex: 1;
|
||
|
|
|
||
|
|
.card-title {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1f2937;
|
||
|
|
margin-bottom: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-location {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #6b7280;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.close-icon {
|
||
|
|
font-size: 20px;
|
||
|
|
color: #9ca3af;
|
||
|
|
padding: 4px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.card-footer {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
|
||
|
|
.card-time {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #9ca3af;
|
||
|
|
}
|
||
|
|
|
||
|
|
.detail-btn {
|
||
|
|
background: #2563eb;
|
||
|
|
border: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes slideUp {
|
||
|
|
from {
|
||
|
|
transform: translateY(100%);
|
||
|
|
}
|
||
|
|
to {
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-panel {
|
||
|
|
width: 80%;
|
||
|
|
max-width: 320px;
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-content {
|
||
|
|
padding: 16px;
|
||
|
|
height: 100%;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
|
||
|
|
.filter-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
margin-bottom: 24px;
|
||
|
|
|
||
|
|
.filter-title {
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1f2937;
|
||
|
|
}
|
||
|
|
|
||
|
|
.close-icon {
|
||
|
|
font-size: 24px;
|
||
|
|
color: #6b7280;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-section {
|
||
|
|
flex: 1;
|
||
|
|
|
||
|
|
.filter-section-title {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #374151;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-options {
|
||
|
|
.filter-option {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px 0;
|
||
|
|
cursor: pointer;
|
||
|
|
|
||
|
|
input[type="checkbox"] {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.checkbox-custom {
|
||
|
|
width: 18px;
|
||
|
|
height: 18px;
|
||
|
|
border: 2px solid #d1d5db;
|
||
|
|
border-radius: 4px;
|
||
|
|
position: relative;
|
||
|
|
|
||
|
|
&::after {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
display: none;
|
||
|
|
left: 5px;
|
||
|
|
top: 1px;
|
||
|
|
width: 5px;
|
||
|
|
height: 9px;
|
||
|
|
border: solid white;
|
||
|
|
border-width: 0 2px 2px 0;
|
||
|
|
transform: rotate(45deg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
input:checked + .checkbox-custom {
|
||
|
|
background: #2563eb;
|
||
|
|
border-color: #2563eb;
|
||
|
|
|
||
|
|
&::after {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.option-label {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #374151;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 24px;
|
||
|
|
|
||
|
|
.reset-btn {
|
||
|
|
flex: 1;
|
||
|
|
height: 44px;
|
||
|
|
border: 1px solid #d1d5db;
|
||
|
|
border-radius: 12px;
|
||
|
|
color: #374151;
|
||
|
|
}
|
||
|
|
|
||
|
|
.confirm-btn {
|
||
|
|
flex: 1;
|
||
|
|
height: 44px;
|
||
|
|
background: #2563eb;
|
||
|
|
border: none;
|
||
|
|
border-radius: 12px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|