| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- <!--路线规划辅助-->
- <template>
- <div class="app-container">
- <div class="route-layout">
- <!-- 左侧信息面板 -->
- <div class="left-panel">
- <!-- 突发事件定位 -->
- <el-card shadow="never" class="panel-card">
- <template #header>
- <div class="card-title">
- <el-icon><Location /></el-icon>
- <span>突发事件定位</span>
- </div>
- </template>
- <div class="event-location">
- <div class="loc-row">
- <span class="loc-label">事件地点</span>
- <el-tag type="danger" effect="dark">{{ selectedEventLocation }}</el-tag>
- </div>
- <div class="loc-row">
- <span class="loc-label">经度</span>
- <span class="loc-value">{{ eventLocations[selectedEventLocation].lng }}</span>
- </div>
- <div class="loc-row">
- <span class="loc-label">纬度</span>
- <span class="loc-value">{{ eventLocations[selectedEventLocation].lat }}</span>
- </div>
- </div>
- <el-select v-model="selectedEventLocation" placeholder="选择事件地点" style="width:100%;margin-bottom:12px" @change="refreshAll">
- <el-option v-for="(loc, key) in eventLocations" :key="key" :label="key" :value="key" />
- </el-select>
- <el-button type="primary" style="width:100%" @click="refreshAll">
- <el-icon><Position /></el-icon> 分析路线 & 跳转GIS
- </el-button>
- </el-card>
- <!-- 值守/指挥人员 -->
- <el-card shadow="never" class="panel-card">
- <template #header>
- <div class="card-title">
- <el-icon><User /></el-icon>
- <span>值守人员 & 指挥人员</span>
- </div>
- </template>
- <div class="person-list">
- <div v-for="person in personnel" :key="person.id" class="person-item">
- <div class="person-info">
- <span class="person-name">{{ person.name }}</span>
- <el-tag :type="person.role === '指挥人员' ? 'warning' : 'primary'" size="small">{{ person.role }}</el-tag>
- </div>
- <el-button size="small" text type="primary">{{ person.position }}</el-button>
- </div>
- </div>
- <div class="person-tip">系统根据人员当前位置自动分析最优路线</div>
- </el-card>
- <!-- 智能绕行路线推荐 -->
- <el-card shadow="never" class="panel-card">
- <template #header>
- <div class="card-title">
- <el-icon><Guide /></el-icon>
- <span>智能绕行路线推荐</span>
- </div>
- </template>
- <div class="route-list">
- <!-- 最短路径 -->
- <div
- class="route-item"
- :class="{ active: activeRouteType === 'shortest' }"
- @click="selectRoute('shortest')"
- >
- <div class="route-head">
- <span class="route-name">最短路径(距离优先)</span>
- <el-tag size="small" type="success">{{ displayRoutes.shortest?.distance || '--' }} km</el-tag>
- </div>
- <div class="route-path">{{ displayRoutes.shortest?.pathDisplay || '--' }}</div>
- <div class="route-meta">
- <span v-if="displayRoutes.shortest">预估耗时 {{ displayRoutes.shortest.estimateTime }}分钟,途经{{ displayRoutes.shortest.viaArea }}</span>
- </div>
- </div>
- <!-- 最快路径 -->
- <div
- class="route-item"
- :class="{ active: activeRouteType === 'fastest' }"
- @click="selectRoute('fastest')"
- >
- <div class="route-head">
- <span class="route-name">最快路径(时间优先)</span>
- <el-tag size="small" type="warning">{{ displayRoutes.fastest?.time || '--' }} min</el-tag>
- </div>
- <div class="route-path">{{ displayRoutes.fastest?.pathDisplay || '--' }}</div>
- <div class="route-meta">
- <span v-if="displayRoutes.fastest">避开拥堵,节省约{{ displayRoutes.fastest.savePercent }}%时间</span>
- </div>
- </div>
- </div>
- </el-card>
- </div>
- <!-- 右侧地图区域 -->
- <div class="right-panel">
- <div class="map-container">
- <!-- 地名标签 -->
- <div v-for="(pos, key) in mapLabels" :key="key" class="map-label" :style="{ left: pos.x + '%', top: pos.y + '%' }">
- {{ key }}
- </div>
- <!-- 路线连接线 -->
- <div v-for="(line, idx) in mapLines" :key="'line-'+idx" class="map-line" :style="line.style" :class="line.type" />
- <!-- 模拟标记点 -->
- <div
- v-for="marker in mapMarkers"
- :key="marker.id"
- class="map-marker"
- :class="marker.type"
- :style="{ left: marker.x + '%', top: marker.y + '%' }"
- >
- <div class="marker-dot"></div>
- <div class="marker-pulse" v-if="marker.type === 'event'"></div>
- <div class="marker-label">{{ marker.name }}</div>
- </div>
- <!-- 图例 -->
- <div class="map-legend">
- <div class="legend-item"><span class="legend-dot event"></span> 突发事件点</div>
- <div class="legend-item"><span class="legend-dot person"></span> 值守/指挥人员站点</div>
- <div class="legend-item"><span class="legend-dot route"></span> 推荐路线/备选站</div>
- <div class="legend-item"><span class="legend-line"></span> 绕行通过路段</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted } from 'vue'
- import { Location, Position, User, Guide } from '@element-plus/icons-vue'
- // 事件地点坐标
- const eventLocations = {
- '化工园区': { lng: '118.50', lat: '31.20' },
- '城南枢纽': { lng: '118.45', lat: '31.15' },
- '城西隧道': { lng: '118.38', lat: '31.22' },
- '滨海大道': { lng: '118.62', lat: '31.08' }
- }
- const selectedEventLocation = ref('化工园区')
- const activeRouteType = ref('shortest')
- // 人员数据
- const personnel = ref([
- { id: 1, name: '张建国', role: '指挥人员', position: '应急指挥中心' },
- { id: 2, name: '李卫东', role: '值守人员', position: '消防站A' },
- { id: 3, name: '王敏', role: '值守人员', position: '交警驻点' },
- { id: 4, name: '陈方', role: '指挥人员', position: '医疗点B' }
- ])
- // 地图标签
- const mapLabels = {
- '北环快速': { x: 42, y: 12 },
- '城西隧道': { x: 10, y: 40 },
- '滨海大道': { x: 75, y: 28 },
- '化工园区': { x: 22, y: 62 },
- '城南枢纽': { x: 30, y: 78 },
- '东侧入口': { x: 88, y: 52 }
- }
- // 地图标记点
- const mapMarkers = computed(() => {
- const markers = [
- { id: 'e1', name: '化工园区', x: 22, y: 62, type: 'event' },
- { id: 'e2', name: '城南枢纽', x: 30, y: 78, type: 'event' },
- { id: 'e3', name: '城西隧道', x: 10, y: 40, type: 'event' },
- { id: 'e4', name: '滨海大道', x: 75, y: 28, type: 'event' },
- { id: 'p1', name: '张建国', x: 52, y: 48, type: 'person' },
- { id: 'p2', name: '李卫东', x: 40, y: 34, type: 'person' },
- { id: 'p3', name: '王敏', x: 35, y: 58, type: 'person' },
- { id: 'p4', name: '陈方', x: 62, y: 68, type: 'person' },
- { id: 'r1', name: '西侧绕行点', x: 6, y: 68, type: 'route' },
- { id: 'r2', name: '东侧入口', x: 88, y: 52, type: 'route' },
- { id: 'r3', name: '北环快速', x: 42, y: 12, type: 'route' }
- ]
- // 高亮当前事件点
- return markers.map(m => {
- if (m.type === 'event' && m.name === selectedEventLocation.value) {
- return { ...m, active: true }
- }
- return { ...m, active: false }
- })
- })
- // 地图路线(模拟)
- const mapLines = computed(() => {
- const lines = []
- // 基础道路
- lines.push({ style: { left: '22%', top: '62%', width: '14%', transform: 'rotate(30deg)' }, type: 'road' })
- lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'road' })
- lines.push({ style: { left: '40%', top: '34%', width: '14%', transform: 'rotate(40deg)' }, type: 'road' })
- lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'road' })
- lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'road' })
- // 根据选中路线类型高亮
- if (activeRouteType.value === 'shortest') {
- lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'active-route' })
- lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'active-route' })
- } else {
- lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'active-route' })
- lines.push({ style: { left: '10%', top: '40%', width: '28%', transform: 'rotate(5deg)' }, type: 'active-route' })
- }
- return lines
- })
- // 路线数据
- const routeData = reactive({
- '化工园区': {
- shortest: { distance: '3.2', pathDisplay: '交警驻点 → 化工园区', estimateTime: '5', viaArea: '城东主干道' },
- fastest: { time: '4.8', pathDisplay: '消防站A → 应急指挥中心 → 化工园区', savePercent: '15' }
- },
- '城南枢纽': {
- shortest: { distance: '2.8', pathDisplay: '医疗点B → 城南枢纽', estimateTime: '4', viaArea: '城南大道' },
- fastest: { time: '3.7', pathDisplay: '应急指挥中心 → 交警驻点 → 城南枢纽', savePercent: '12' }
- },
- '城西隧道': {
- shortest: { distance: '4.2', pathDisplay: '消防站A → 城西隧道', estimateTime: '7', viaArea: '城西快速路' },
- fastest: { time: '5.5', pathDisplay: '应急指挥中心 → 北环快速 → 城西隧道', savePercent: '20' }
- },
- '滨海大道': {
- shortest: { distance: '5.8', pathDisplay: '医疗点B → 滨海大道', estimateTime: '6', viaArea: '滨海路' },
- fastest: { time: '4.2', pathDisplay: '北环快速 → 东侧入口 → 滨海大道', savePercent: '18' }
- }
- })
- const displayRoutes = computed(() => {
- const data = routeData[selectedEventLocation.value]
- return data || { shortest: null, fastest: null }
- })
- const selectRoute = (type) => {
- activeRouteType.value = type
- }
- const refreshAll = () => {
- activeRouteType.value = 'shortest'
- }
- onMounted(() => {
- refreshAll()
- })
- </script>
- <style scoped>
- .app-container {
- padding: 16px;
- background: #f0f2f5;
- min-height: calc(100vh - 84px);
- }
- .route-layout {
- display: flex;
- gap: 16px;
- height: calc(100vh - 120px);
- }
- .left-panel {
- width: 30%;
- min-width: 320px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- overflow-y: auto;
- }
- .right-panel {
- flex: 1;
- min-width: 0;
- }
- .panel-card {
- border-radius: 8px;
- }
- .card-title {
- display: flex;
- align-items: center;
- gap: 6px;
- font-weight: 600;
- font-size: 14px;
- color: #303133;
- }
- .card-title .el-icon {
- color: #409EFF;
- font-size: 16px;
- }
- /* 事件定位 */
- .event-location {
- margin-bottom: 12px;
- }
- .loc-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 0;
- font-size: 13px;
- }
- .loc-label {
- color: #909399;
- font-size: 12px;
- }
- .loc-value {
- font-family: monospace;
- color: #303133;
- font-weight: 600;
- }
- /* 人员列表 */
- .person-list {
- max-height: 180px;
- overflow-y: auto;
- }
- .person-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 0;
- border-bottom: 1px solid #f0f0f0;
- }
- .person-item:last-child {
- border-bottom: none;
- }
- .person-info {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .person-name {
- font-weight: 600;
- font-size: 13px;
- color: #303133;
- }
- .person-tip {
- margin-top: 8px;
- font-size: 12px;
- color: #909399;
- text-align: center;
- }
- /* 路线推荐 */
- .route-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .route-item {
- padding: 12px;
- border: 1px solid #ebeef5;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s;
- background: #fafafa;
- }
- .route-item:hover {
- border-color: #409EFF;
- }
- .route-item.active {
- border-color: #409EFF;
- border-left: 3px solid #409EFF;
- background: #ecf5ff;
- }
- .route-head {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 6px;
- }
- .route-name {
- font-weight: 600;
- font-size: 14px;
- color: #303133;
- }
- .route-path {
- font-size: 12px;
- color: #606266;
- margin-bottom: 4px;
- }
- .route-meta {
- font-size: 12px;
- color: #909399;
- }
- /* 地图区域 */
- .map-container {
- position: relative;
- width: 100%;
- height: 100%;
- background: #1a2a3a;
- border-radius: 8px;
- overflow: hidden;
- min-height: 500px;
- }
- /* 地名标签 */
- .map-label {
- position: absolute;
- color: rgba(255, 255, 255, 0.5);
- font-size: 11px;
- pointer-events: none;
- transform: translate(-50%, -50%);
- white-space: nowrap;
- letter-spacing: 1px;
- }
- /* 标记点 */
- .map-marker {
- position: absolute;
- transform: translate(-50%, -50%);
- z-index: 10;
- }
- .marker-dot {
- width: 14px;
- height: 14px;
- border-radius: 50%;
- border: 2px solid #fff;
- position: relative;
- z-index: 2;
- }
- .map-marker.event .marker-dot {
- background: #e74c3c;
- width: 18px;
- height: 18px;
- box-shadow: 0 0 12px rgba(231, 76, 60, 0.6);
- }
- .map-marker.person .marker-dot {
- background: #3498db;
- box-shadow: 0 0 8px rgba(52, 152, 219, 0.5);
- }
- .map-marker.route .marker-dot {
- background: #e67e22;
- box-shadow: 0 0 8px rgba(230, 126, 34, 0.5);
- }
- .marker-pulse {
- position: absolute;
- top: 50%;
- left: 50%;
- width: 36px;
- height: 36px;
- margin-left: -18px;
- margin-top: -18px;
- border-radius: 50%;
- background: rgba(231, 76, 60, 0.2);
- animation: pulse 2s ease-out infinite;
- z-index: 1;
- }
- @keyframes pulse {
- 0% { transform: scale(0.5); opacity: 1; }
- 100% { transform: scale(1.5); opacity: 0; }
- }
- .marker-label {
- position: absolute;
- left: 50%;
- top: 100%;
- transform: translateX(-50%);
- margin-top: 4px;
- font-size: 10px;
- color: rgba(255, 255, 255, 0.85);
- white-space: nowrap;
- background: rgba(0, 0, 0, 0.5);
- padding: 1px 6px;
- border-radius: 3px;
- }
- /* 路线连接 */
- .map-line {
- position: absolute;
- height: 2px;
- transform-origin: left center;
- pointer-events: none;
- }
- .map-line.road {
- background: rgba(255, 255, 255, 0.15);
- border: none;
- }
- .map-line.active-route {
- height: 3px;
- background: #e67e22;
- box-shadow: 0 0 6px rgba(230, 126, 34, 0.6);
- z-index: 5;
- }
- /* 图例 */
- .map-legend {
- position: absolute;
- bottom: 16px;
- right: 16px;
- background: rgba(0, 0, 0, 0.75);
- padding: 10px 16px;
- border-radius: 8px;
- color: #fff;
- font-size: 12px;
- z-index: 20;
- display: flex;
- flex-direction: column;
- gap: 6px;
- backdrop-filter: blur(4px);
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .legend-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- flex-shrink: 0;
- }
- .legend-dot.event {
- background: #e74c3c;
- }
- .legend-dot.person {
- background: #3498db;
- }
- .legend-dot.route {
- background: #e67e22;
- }
- .legend-line {
- width: 18px;
- height: 3px;
- background: #e67e22;
- border-radius: 2px;
- flex-shrink: 0;
- }
- </style>
|