DisRoute.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. <!--路线规划辅助-->
  2. <template>
  3. <div class="app-container">
  4. <div class="route-layout">
  5. <!-- 左侧信息面板 -->
  6. <div class="left-panel">
  7. <!-- 突发事件定位 -->
  8. <el-card shadow="never" class="panel-card">
  9. <template #header>
  10. <div class="card-title">
  11. <el-icon><Location /></el-icon>
  12. <span>突发事件定位</span>
  13. </div>
  14. </template>
  15. <div class="event-location">
  16. <div class="loc-row">
  17. <span class="loc-label">事件地点</span>
  18. <el-tag type="danger" effect="dark">{{ selectedEventLocation }}</el-tag>
  19. </div>
  20. <div class="loc-row">
  21. <span class="loc-label">经度</span>
  22. <span class="loc-value">{{ eventLocations[selectedEventLocation].lng }}</span>
  23. </div>
  24. <div class="loc-row">
  25. <span class="loc-label">纬度</span>
  26. <span class="loc-value">{{ eventLocations[selectedEventLocation].lat }}</span>
  27. </div>
  28. </div>
  29. <el-select v-model="selectedEventLocation" placeholder="选择事件地点" style="width:100%;margin-bottom:12px" @change="refreshAll">
  30. <el-option v-for="(loc, key) in eventLocations" :key="key" :label="key" :value="key" />
  31. </el-select>
  32. <el-button type="primary" style="width:100%" @click="refreshAll">
  33. <el-icon><Position /></el-icon> 分析路线 &amp; 跳转GIS
  34. </el-button>
  35. </el-card>
  36. <!-- 值守/指挥人员 -->
  37. <el-card shadow="never" class="panel-card">
  38. <template #header>
  39. <div class="card-title">
  40. <el-icon><User /></el-icon>
  41. <span>值守人员 &amp; 指挥人员</span>
  42. </div>
  43. </template>
  44. <div class="person-list">
  45. <div v-for="person in personnel" :key="person.id" class="person-item">
  46. <div class="person-info">
  47. <span class="person-name">{{ person.name }}</span>
  48. <el-tag :type="person.role === '指挥人员' ? 'warning' : 'primary'" size="small">{{ person.role }}</el-tag>
  49. </div>
  50. <el-button size="small" text type="primary">{{ person.position }}</el-button>
  51. </div>
  52. </div>
  53. <div class="person-tip">系统根据人员当前位置自动分析最优路线</div>
  54. </el-card>
  55. <!-- 智能绕行路线推荐 -->
  56. <el-card shadow="never" class="panel-card">
  57. <template #header>
  58. <div class="card-title">
  59. <el-icon><Guide /></el-icon>
  60. <span>智能绕行路线推荐</span>
  61. </div>
  62. </template>
  63. <div class="route-list">
  64. <!-- 最短路径 -->
  65. <div
  66. class="route-item"
  67. :class="{ active: activeRouteType === 'shortest' }"
  68. @click="selectRoute('shortest')"
  69. >
  70. <div class="route-head">
  71. <span class="route-name">最短路径(距离优先)</span>
  72. <el-tag size="small" type="success">{{ displayRoutes.shortest?.distance || '--' }} km</el-tag>
  73. </div>
  74. <div class="route-path">{{ displayRoutes.shortest?.pathDisplay || '--' }}</div>
  75. <div class="route-meta">
  76. <span v-if="displayRoutes.shortest">预估耗时 {{ displayRoutes.shortest.estimateTime }}分钟,途经{{ displayRoutes.shortest.viaArea }}</span>
  77. </div>
  78. </div>
  79. <!-- 最快路径 -->
  80. <div
  81. class="route-item"
  82. :class="{ active: activeRouteType === 'fastest' }"
  83. @click="selectRoute('fastest')"
  84. >
  85. <div class="route-head">
  86. <span class="route-name">最快路径(时间优先)</span>
  87. <el-tag size="small" type="warning">{{ displayRoutes.fastest?.time || '--' }} min</el-tag>
  88. </div>
  89. <div class="route-path">{{ displayRoutes.fastest?.pathDisplay || '--' }}</div>
  90. <div class="route-meta">
  91. <span v-if="displayRoutes.fastest">避开拥堵,节省约{{ displayRoutes.fastest.savePercent }}%时间</span>
  92. </div>
  93. </div>
  94. </div>
  95. </el-card>
  96. </div>
  97. <!-- 右侧地图区域 -->
  98. <div class="right-panel">
  99. <div class="map-container">
  100. <!-- 地名标签 -->
  101. <div v-for="(pos, key) in mapLabels" :key="key" class="map-label" :style="{ left: pos.x + '%', top: pos.y + '%' }">
  102. {{ key }}
  103. </div>
  104. <!-- 路线连接线 -->
  105. <div v-for="(line, idx) in mapLines" :key="'line-'+idx" class="map-line" :style="line.style" :class="line.type" />
  106. <!-- 模拟标记点 -->
  107. <div
  108. v-for="marker in mapMarkers"
  109. :key="marker.id"
  110. class="map-marker"
  111. :class="marker.type"
  112. :style="{ left: marker.x + '%', top: marker.y + '%' }"
  113. >
  114. <div class="marker-dot"></div>
  115. <div class="marker-pulse" v-if="marker.type === 'event'"></div>
  116. <div class="marker-label">{{ marker.name }}</div>
  117. </div>
  118. <!-- 图例 -->
  119. <div class="map-legend">
  120. <div class="legend-item"><span class="legend-dot event"></span> 突发事件点</div>
  121. <div class="legend-item"><span class="legend-dot person"></span> 值守/指挥人员站点</div>
  122. <div class="legend-item"><span class="legend-dot route"></span> 推荐路线/备选站</div>
  123. <div class="legend-item"><span class="legend-line"></span> 绕行通过路段</div>
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. </template>
  130. <script setup>
  131. import { ref, reactive, computed, onMounted } from 'vue'
  132. import { Location, Position, User, Guide } from '@element-plus/icons-vue'
  133. // 事件地点坐标
  134. const eventLocations = {
  135. '化工园区': { lng: '118.50', lat: '31.20' },
  136. '城南枢纽': { lng: '118.45', lat: '31.15' },
  137. '城西隧道': { lng: '118.38', lat: '31.22' },
  138. '滨海大道': { lng: '118.62', lat: '31.08' }
  139. }
  140. const selectedEventLocation = ref('化工园区')
  141. const activeRouteType = ref('shortest')
  142. // 人员数据
  143. const personnel = ref([
  144. { id: 1, name: '张建国', role: '指挥人员', position: '应急指挥中心' },
  145. { id: 2, name: '李卫东', role: '值守人员', position: '消防站A' },
  146. { id: 3, name: '王敏', role: '值守人员', position: '交警驻点' },
  147. { id: 4, name: '陈方', role: '指挥人员', position: '医疗点B' }
  148. ])
  149. // 地图标签
  150. const mapLabels = {
  151. '北环快速': { x: 42, y: 12 },
  152. '城西隧道': { x: 10, y: 40 },
  153. '滨海大道': { x: 75, y: 28 },
  154. '化工园区': { x: 22, y: 62 },
  155. '城南枢纽': { x: 30, y: 78 },
  156. '东侧入口': { x: 88, y: 52 }
  157. }
  158. // 地图标记点
  159. const mapMarkers = computed(() => {
  160. const markers = [
  161. { id: 'e1', name: '化工园区', x: 22, y: 62, type: 'event' },
  162. { id: 'e2', name: '城南枢纽', x: 30, y: 78, type: 'event' },
  163. { id: 'e3', name: '城西隧道', x: 10, y: 40, type: 'event' },
  164. { id: 'e4', name: '滨海大道', x: 75, y: 28, type: 'event' },
  165. { id: 'p1', name: '张建国', x: 52, y: 48, type: 'person' },
  166. { id: 'p2', name: '李卫东', x: 40, y: 34, type: 'person' },
  167. { id: 'p3', name: '王敏', x: 35, y: 58, type: 'person' },
  168. { id: 'p4', name: '陈方', x: 62, y: 68, type: 'person' },
  169. { id: 'r1', name: '西侧绕行点', x: 6, y: 68, type: 'route' },
  170. { id: 'r2', name: '东侧入口', x: 88, y: 52, type: 'route' },
  171. { id: 'r3', name: '北环快速', x: 42, y: 12, type: 'route' }
  172. ]
  173. // 高亮当前事件点
  174. return markers.map(m => {
  175. if (m.type === 'event' && m.name === selectedEventLocation.value) {
  176. return { ...m, active: true }
  177. }
  178. return { ...m, active: false }
  179. })
  180. })
  181. // 地图路线(模拟)
  182. const mapLines = computed(() => {
  183. const lines = []
  184. // 基础道路
  185. lines.push({ style: { left: '22%', top: '62%', width: '14%', transform: 'rotate(30deg)' }, type: 'road' })
  186. lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'road' })
  187. lines.push({ style: { left: '40%', top: '34%', width: '14%', transform: 'rotate(40deg)' }, type: 'road' })
  188. lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'road' })
  189. lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'road' })
  190. // 根据选中路线类型高亮
  191. if (activeRouteType.value === 'shortest') {
  192. lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'active-route' })
  193. lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'active-route' })
  194. } else {
  195. lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'active-route' })
  196. lines.push({ style: { left: '10%', top: '40%', width: '28%', transform: 'rotate(5deg)' }, type: 'active-route' })
  197. }
  198. return lines
  199. })
  200. // 路线数据
  201. const routeData = reactive({
  202. '化工园区': {
  203. shortest: { distance: '3.2', pathDisplay: '交警驻点 → 化工园区', estimateTime: '5', viaArea: '城东主干道' },
  204. fastest: { time: '4.8', pathDisplay: '消防站A → 应急指挥中心 → 化工园区', savePercent: '15' }
  205. },
  206. '城南枢纽': {
  207. shortest: { distance: '2.8', pathDisplay: '医疗点B → 城南枢纽', estimateTime: '4', viaArea: '城南大道' },
  208. fastest: { time: '3.7', pathDisplay: '应急指挥中心 → 交警驻点 → 城南枢纽', savePercent: '12' }
  209. },
  210. '城西隧道': {
  211. shortest: { distance: '4.2', pathDisplay: '消防站A → 城西隧道', estimateTime: '7', viaArea: '城西快速路' },
  212. fastest: { time: '5.5', pathDisplay: '应急指挥中心 → 北环快速 → 城西隧道', savePercent: '20' }
  213. },
  214. '滨海大道': {
  215. shortest: { distance: '5.8', pathDisplay: '医疗点B → 滨海大道', estimateTime: '6', viaArea: '滨海路' },
  216. fastest: { time: '4.2', pathDisplay: '北环快速 → 东侧入口 → 滨海大道', savePercent: '18' }
  217. }
  218. })
  219. const displayRoutes = computed(() => {
  220. const data = routeData[selectedEventLocation.value]
  221. return data || { shortest: null, fastest: null }
  222. })
  223. const selectRoute = (type) => {
  224. activeRouteType.value = type
  225. }
  226. const refreshAll = () => {
  227. activeRouteType.value = 'shortest'
  228. }
  229. onMounted(() => {
  230. refreshAll()
  231. })
  232. </script>
  233. <style scoped>
  234. .app-container {
  235. padding: 16px;
  236. background: #f0f2f5;
  237. min-height: calc(100vh - 84px);
  238. }
  239. .route-layout {
  240. display: flex;
  241. gap: 16px;
  242. height: calc(100vh - 120px);
  243. }
  244. .left-panel {
  245. width: 30%;
  246. min-width: 320px;
  247. display: flex;
  248. flex-direction: column;
  249. gap: 12px;
  250. overflow-y: auto;
  251. }
  252. .right-panel {
  253. flex: 1;
  254. min-width: 0;
  255. }
  256. .panel-card {
  257. border-radius: 8px;
  258. }
  259. .card-title {
  260. display: flex;
  261. align-items: center;
  262. gap: 6px;
  263. font-weight: 600;
  264. font-size: 14px;
  265. color: #303133;
  266. }
  267. .card-title .el-icon {
  268. color: #409EFF;
  269. font-size: 16px;
  270. }
  271. /* 事件定位 */
  272. .event-location {
  273. margin-bottom: 12px;
  274. }
  275. .loc-row {
  276. display: flex;
  277. align-items: center;
  278. justify-content: space-between;
  279. padding: 6px 0;
  280. font-size: 13px;
  281. }
  282. .loc-label {
  283. color: #909399;
  284. font-size: 12px;
  285. }
  286. .loc-value {
  287. font-family: monospace;
  288. color: #303133;
  289. font-weight: 600;
  290. }
  291. /* 人员列表 */
  292. .person-list {
  293. max-height: 180px;
  294. overflow-y: auto;
  295. }
  296. .person-item {
  297. display: flex;
  298. justify-content: space-between;
  299. align-items: center;
  300. padding: 8px 0;
  301. border-bottom: 1px solid #f0f0f0;
  302. }
  303. .person-item:last-child {
  304. border-bottom: none;
  305. }
  306. .person-info {
  307. display: flex;
  308. align-items: center;
  309. gap: 8px;
  310. }
  311. .person-name {
  312. font-weight: 600;
  313. font-size: 13px;
  314. color: #303133;
  315. }
  316. .person-tip {
  317. margin-top: 8px;
  318. font-size: 12px;
  319. color: #909399;
  320. text-align: center;
  321. }
  322. /* 路线推荐 */
  323. .route-list {
  324. display: flex;
  325. flex-direction: column;
  326. gap: 10px;
  327. }
  328. .route-item {
  329. padding: 12px;
  330. border: 1px solid #ebeef5;
  331. border-radius: 6px;
  332. cursor: pointer;
  333. transition: all 0.2s;
  334. background: #fafafa;
  335. }
  336. .route-item:hover {
  337. border-color: #409EFF;
  338. }
  339. .route-item.active {
  340. border-color: #409EFF;
  341. border-left: 3px solid #409EFF;
  342. background: #ecf5ff;
  343. }
  344. .route-head {
  345. display: flex;
  346. justify-content: space-between;
  347. align-items: center;
  348. margin-bottom: 6px;
  349. }
  350. .route-name {
  351. font-weight: 600;
  352. font-size: 14px;
  353. color: #303133;
  354. }
  355. .route-path {
  356. font-size: 12px;
  357. color: #606266;
  358. margin-bottom: 4px;
  359. }
  360. .route-meta {
  361. font-size: 12px;
  362. color: #909399;
  363. }
  364. /* 地图区域 */
  365. .map-container {
  366. position: relative;
  367. width: 100%;
  368. height: 100%;
  369. background: #1a2a3a;
  370. border-radius: 8px;
  371. overflow: hidden;
  372. min-height: 500px;
  373. }
  374. /* 地名标签 */
  375. .map-label {
  376. position: absolute;
  377. color: rgba(255, 255, 255, 0.5);
  378. font-size: 11px;
  379. pointer-events: none;
  380. transform: translate(-50%, -50%);
  381. white-space: nowrap;
  382. letter-spacing: 1px;
  383. }
  384. /* 标记点 */
  385. .map-marker {
  386. position: absolute;
  387. transform: translate(-50%, -50%);
  388. z-index: 10;
  389. }
  390. .marker-dot {
  391. width: 14px;
  392. height: 14px;
  393. border-radius: 50%;
  394. border: 2px solid #fff;
  395. position: relative;
  396. z-index: 2;
  397. }
  398. .map-marker.event .marker-dot {
  399. background: #e74c3c;
  400. width: 18px;
  401. height: 18px;
  402. box-shadow: 0 0 12px rgba(231, 76, 60, 0.6);
  403. }
  404. .map-marker.person .marker-dot {
  405. background: #3498db;
  406. box-shadow: 0 0 8px rgba(52, 152, 219, 0.5);
  407. }
  408. .map-marker.route .marker-dot {
  409. background: #e67e22;
  410. box-shadow: 0 0 8px rgba(230, 126, 34, 0.5);
  411. }
  412. .marker-pulse {
  413. position: absolute;
  414. top: 50%;
  415. left: 50%;
  416. width: 36px;
  417. height: 36px;
  418. margin-left: -18px;
  419. margin-top: -18px;
  420. border-radius: 50%;
  421. background: rgba(231, 76, 60, 0.2);
  422. animation: pulse 2s ease-out infinite;
  423. z-index: 1;
  424. }
  425. @keyframes pulse {
  426. 0% { transform: scale(0.5); opacity: 1; }
  427. 100% { transform: scale(1.5); opacity: 0; }
  428. }
  429. .marker-label {
  430. position: absolute;
  431. left: 50%;
  432. top: 100%;
  433. transform: translateX(-50%);
  434. margin-top: 4px;
  435. font-size: 10px;
  436. color: rgba(255, 255, 255, 0.85);
  437. white-space: nowrap;
  438. background: rgba(0, 0, 0, 0.5);
  439. padding: 1px 6px;
  440. border-radius: 3px;
  441. }
  442. /* 路线连接 */
  443. .map-line {
  444. position: absolute;
  445. height: 2px;
  446. transform-origin: left center;
  447. pointer-events: none;
  448. }
  449. .map-line.road {
  450. background: rgba(255, 255, 255, 0.15);
  451. border: none;
  452. }
  453. .map-line.active-route {
  454. height: 3px;
  455. background: #e67e22;
  456. box-shadow: 0 0 6px rgba(230, 126, 34, 0.6);
  457. z-index: 5;
  458. }
  459. /* 图例 */
  460. .map-legend {
  461. position: absolute;
  462. bottom: 16px;
  463. right: 16px;
  464. background: rgba(0, 0, 0, 0.75);
  465. padding: 10px 16px;
  466. border-radius: 8px;
  467. color: #fff;
  468. font-size: 12px;
  469. z-index: 20;
  470. display: flex;
  471. flex-direction: column;
  472. gap: 6px;
  473. backdrop-filter: blur(4px);
  474. }
  475. .legend-item {
  476. display: flex;
  477. align-items: center;
  478. gap: 8px;
  479. }
  480. .legend-dot {
  481. width: 10px;
  482. height: 10px;
  483. border-radius: 50%;
  484. flex-shrink: 0;
  485. }
  486. .legend-dot.event {
  487. background: #e74c3c;
  488. }
  489. .legend-dot.person {
  490. background: #3498db;
  491. }
  492. .legend-dot.route {
  493. background: #e67e22;
  494. }
  495. .legend-line {
  496. width: 18px;
  497. height: 3px;
  498. background: #e67e22;
  499. border-radius: 2px;
  500. flex-shrink: 0;
  501. }
  502. </style>