Files
jg_app/src/pages/map/index.vue
2026-04-21 18:00:09 +08:00

639 lines
14 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, onMounted, watch } from "vue";
import { useRouter } from "vue-router";
import BottomTabs from "@/components/bottomTabs.vue";
import { getEventUnfinished } from "@/api/traffic";
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([]);
const initLoading = ref(false);
// 获取未完成的预警事件
async function fetchUnfinishedEvents() {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
const areaCode = userInfo.areaCode;
const eventCategory = activeAlertType.value === 'road' ? 1 : 2;
initLoading.value = true;
try {
const res = await getEventUnfinished({ eventCategory, areaCode });
console.log('未完成预警事件:', res);
if (res) {
markers.value = (Array.isArray(res) ? res : res.data || []).map(item => ({
id: item.id,
type: activeAlertType.value,
title: item.eventType || item.title || '预警事件',
location: item.siteName || item.location || '',
time: item.eventTime || item.time || '',
color: getMarkerColor(item.eventLevel),
position: {
top: item.latitude ? `${Math.min(Math.max(item.latitude, 5), 95)}%` : `${Math.random() * 80 + 10}%`,
left: item.longitude ? `${Math.min(Math.max(item.longitude, 5), 95)}%` : `${Math.random() * 80 + 10}%`
}
}));
}
} catch (error) {
console.error('获取预警事件失败:', error);
markers.value = [];
} finally {
initLoading.value = false;
}
}
// 根据事件等级获取标记颜色
function getMarkerColor(eventLevel) {
const colorMap = {
1: 'marker-red',
2: 'marker-orange',
3: 'marker-blue',
4: 'marker-purple'
};
return colorMap[eventLevel] || 'marker-blue';
}
// 过滤后的标记点
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;
});
});
// 切换预警类型时重新加载数据
watch(activeAlertType, () => {
fetchUnfinishedEvents();
});
// 页面加载时获取数据
onMounted(() => {
fetchUnfinishedEvents();
});
// 返回
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-orange {
background: #f97316;
}
&.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>