This commit is contained in:
Zzc 2025-11-20 12:53:27 +08:00
commit 27ff5b5764
18 changed files with 541 additions and 270 deletions

View File

@ -1,14 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" /> <meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>H5移动端</title> <title>H5移动端</title>
<script>
window._AMapSecurityConfig = {
securityJsCode: "08c037da44c78afd7338203268c2d2a5"
};
</script>
<script src="https://webapi.amap.com/loader.js"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View File

@ -211,6 +211,7 @@ import { ref, onMounted, reactive, toRaw, watch } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { showToast, showLoadingToast } from "vant"; import { showToast, showLoadingToast } from "vant";
import { request } from "../../../../shared/utils/request"; import { request } from "../../../../shared/utils/request";
import { loadAMap } from "../../../../shared/utils/aMap";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -218,7 +219,7 @@ const searchValue = ref(""); // 搜索框输入值
const showPopup = ref(false); // const showPopup = ref(false); //
const yhzDetail = ref({}); // const yhzDetail = ref({}); //
const materialList = ref([]); // const materialList = ref([]); //
const INIT_FORM = { const getInitForm = () => ({
material: { material: {
jd: "", // jd: "", //
wd: "", // wd: "", //
@ -238,8 +239,8 @@ const INIT_FORM = {
remark: "", // remark: "", //
}, },
photos: [], photos: [],
}; });
const form = reactive({ ...INIT_FORM }); // const form = reactive(getInitForm());
// rid // rid
const getMaterialList = async (wzmc) => { const getMaterialList = async (wzmc) => {
@ -281,15 +282,6 @@ onMounted(() => {
// //
const showTimePicker = ref(false); const showTimePicker = ref(false);
const currentDate = ref([
new Date().getFullYear(),
new Date().getMonth() + 1,
new Date().getDate(),
]);
const onDateConfirm = ({ selectedValues }) => {
form.rkrq = selectedValues.join("-");
showTimePicker.value = false;
};
// //
const dwField = ref(null); const dwField = ref(null);
@ -338,7 +330,6 @@ const handleSubmit = async () => {
message: "新增成功", message: "新增成功",
}); });
onPopupClose(); onPopupClose();
Object.assign(form, { ...INIT_FORM });
getMaterialList(searchValue.value); getMaterialList(searchValue.value);
} else { } else {
throw new Error(res.message); throw new Error(res.message);
@ -423,37 +414,38 @@ const afterRead = async (file) => {
}; };
// //
const handleGetLocation = () => { const handleGetLocation = async () => {
if (!navigator.geolocation) { // AMap
showToast("您的浏览器不支持地理位置获取"); if (!window.AMap) {
return; await loadAMap();
} }
const loadingToast = showLoadingToast({
showLoadingToast({ message: "正在获取位置",
message: "定位中...",
forbidClick: true, forbidClick: true,
duration: 0, // 0
zIndex: 9999,
}); });
navigator.geolocation.getCurrentPosition( AMap.plugin("AMap.Geolocation", () => {
(position) => { const geolocation = new AMap.Geolocation({
form.material.jd = position.coords.longitude.toFixed(6); enableHighAccuracy: true,
form.material.wd = position.coords.latitude.toFixed(6); timeout: 5000,
}, showMarker: false,
(error) => { zoomToAccuracy: true,
const errorMessage = });
{
1: "位置服务被拒绝", geolocation.getCurrentPosition((status, result) => {
2: "暂时无法获取位置", if (status === "complete") {
3: "定位超时", form.material.jd = result.position.lng.toFixed(6);
}[error.code] || "定位失败"; form.material.wd = result.position.lat.toFixed(6);
showToast(errorMessage); loadingToast.close();
}, } else {
{ loadingToast.close();
enableHighAccuracy: true, // showToast(result);
timeout: 5000, // console.log("result", result);
maximumAge: 0, //
} }
); });
});
}; };
watch( watch(
@ -505,6 +497,9 @@ const handleAdd = async () => {
}; };
const onPopupClose = () => { const onPopupClose = () => {
Object.assign(form, getInitForm());
fileList.value = [];
[showDwPicker, showFzrPicker].forEach((v) => (v.value = false));
showPopup.value = false; showPopup.value = false;
}; };
</script> </script>

View File

@ -9,17 +9,18 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.5.18",
"vue-router": "^4.6.3",
"pinia": "^3.0.3",
"element-plus": "^2.11.5",
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"echarts": "^6.0.0", "@h5/shared": "workspace:*",
"vue-echarts": "^8.0.1", "@turf/turf": "^7.3.0",
"cesium": "^1.135.0",
"axios": "^1.13.2",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"@h5/shared": "workspace:*" "axios": "^1.13.2",
"cesium": "^1.135.0",
"echarts": "^6.0.0",
"element-plus": "^2.11.5",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-echarts": "^8.0.1",
"vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",

View File

@ -10,6 +10,9 @@ const routes = [
{ {
path: '/cockpit', path: '/cockpit',
name: 'Cockpit', name: 'Cockpit',
meta: {
screen: true
},
component: () => import('../views/cockpit/index.vue') component: () => import('../views/cockpit/index.vue')
}, },
{ {

View File

@ -2,7 +2,6 @@ import { request } from '@shared/utils/request'
// 获取业务底图 // 获取业务底图
export function getBaseMap() { export function getBaseMap() {
// return [...ddt]
return request({ return request({
url: '/snow-ops-platform/dataDirectory/queryCatalog', url: '/snow-ops-platform/dataDirectory/queryCatalog',
method: 'GET', method: 'GET',
@ -14,7 +13,6 @@ export function getBaseMap() {
// 获取业务图 // 获取业务图
export function getBusinessMap() { export function getBusinessMap() {
// return [...si]
return request({ return request({
url: '/snow-ops-platform/dataDirectory/queryCatalog', url: '/snow-ops-platform/dataDirectory/queryCatalog',
method: 'GET', method: 'GET',
@ -24,6 +22,48 @@ export function getBusinessMap() {
}) })
} }
// 获取所有的养护站
export function getAllYHZList() {
return request({
url: '/snow-ops-platform/yhz/listByDistrict',
method: 'get',
params: {
qxmc: ''
}
})
}
export function getYHZDetail(params) {
return request({
url: '/snow-ops-platform/yhz/getById',
method: 'get',
params
})
}
// 获得高海拔路段图 high-altitude road
export function getHighAltitudeRoadMap () {
return request({
url: '/snow-ops-platform/dataDirectory/queryCatalog',
method: 'GET',
params: {
pcatalog: 'GHBMAP'
}
})
}
// 获取天气预警统计
export function getWeatherWarningStatistics (params) {
return request({
url: '/snow-ops-platform/weatherWarning/statistics',
method: 'GET',
params
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 183 KiB

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="cockpit-layout"> <div class="cockpit-layout">
<!-- <PageHeader /> --> <!-- <PageHeader /> -->
<PageHeader />
<div class="cockpit-main"> <div class="cockpit-main">
<!-- 地图底层 --> <!-- 地图底层 -->
<div class="map-layer"> <div class="map-layer">
@ -13,8 +14,8 @@
<!-- 浮动面板层 --> <!-- 浮动面板层 -->
<div class="panels-layer"> <div class="panels-layer">
<div class="panel-column left-panel"> <div class="panel-column left-panel">
<WeatherWarning /> <WeatherWarning ref="weatherWarningRef" />
<EmergencyResources /> <EmergencyResources @clickEmergencyResource="handleEmergencyResourceClick" />
</div> </div>
<div class="center-spacer" aria-hidden="true"></div> <div class="center-spacer" aria-hidden="true"></div>
@ -26,17 +27,14 @@
</div> </div>
<!-- 图例工具栏 --> <!-- 图例工具栏 -->
<LegendToolbar class="legend-toolbar" :legendKeys="legendKeys" @marker-toggle="handleLegendMarkerToggle" /> <LegendToolbar class="legend-toolbar" v-model="legendToolActiveItem" :legendKeys="legendKeys"
@marker-toggle="handleLegendMarkerToggle" />
<!-- 应急力量详情提示框 --> <!-- 应急力量详情提示框 -->
<EmergencyForceTooltip <EmergencyForceTooltip :visible="emergencyForceInteraction.tooltipVisible.value"
:visible="emergencyForceInteraction.tooltipVisible.value" :position="emergencyForceInteraction.tooltipPosition.value" :data="emergencyForceInteraction.tooltipData.value"
:position="emergencyForceInteraction.tooltipPosition.value" :loading="emergencyForceInteraction.loading.value" :error="emergencyForceInteraction.error.value"
:data="emergencyForceInteraction.tooltipData.value" @close="emergencyForceInteraction.hideTooltip" />
:loading="emergencyForceInteraction.loading.value"
:error="emergencyForceInteraction.error.value"
@close="emergencyForceInteraction.hideTooltip"
/>
</div> </div>
</div> </div>
</template> </template>
@ -63,6 +61,8 @@ import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定
onMounted(() => { onMounted(() => {
// //
mapBase.loadBaseData() mapBase.loadBaseData()
weatherWarningRef.value.loadData()
}) })
// ==================== ==================== // ==================== ====================
@ -79,6 +79,9 @@ const EMERGENCY_FORCE_LAYER_ID = 'legend:emergencyForce'
*/ */
const EMERGENCY_FORCE_CACHE_TTL = 60 * 1000 const EMERGENCY_FORCE_CACHE_TTL = 60 * 1000
// ==================== ====================
const weatherWarningRef = ref(null)
// ==================== ==================== // ==================== ====================
const mapStore = useMapStore() const mapStore = useMapStore()
@ -101,7 +104,7 @@ const mapBase = useMapBase(mapStore)
/** /**
* 标记图hook搭配lengendToolbar使用, 作用是点击某个图例项时在地图上显示所有该图例的图 * 标记图hook搭配lengendToolbar使用, 作用是点击某个图例项时在地图上显示所有该图例的图
*/ */
const mapImageMark = useMapImageMark(mapStore) const mapImageMarkHook = useMapImageMark(mapStore)
/** /**
* 应急力量数据加载状态 * 应急力量数据加载状态
@ -131,9 +134,22 @@ const emergencyForceAbortController = ref(null)
/** /**
* 工具图标列表通过key关联 * 工具图标列表通过key关联
* key具体有哪些请根据LegendToolbar.vue中定义的defaultLegendItems * key具体有哪些请根据LegendToolbar.vue中定义的defaultLegendItems
* 后续的ImageMarkData中也需要与这里的key值对应
*/ */
const legendKeys = ref(['serviceFacility', 'riskRoad', 'blockEvent', 'weatherAlert', 'emergencyForce']) const legendKeys = ref(['serviceFacility', 'riskRoad', 'blockEvent', 'weatherAlert', 'emergencyForce'])
/**
* 过滤标记对象存放当前需要过滤的关键字
*/
const filterMark = ref({
serviceFacility: []
})
/**
* 图例项
*/
const legendToolActiveItem = ref([])
// ==================== ==================== // ==================== ====================
/** /**
@ -413,6 +429,48 @@ const renderEmergencyForcePoints = async (entityService, points, markerIcon) =>
// ==================== ==================== // ==================== ====================
/**
* 应急资源表格行被点击时触发
*/
const handleEmergencyResourceClick = (resource) => {
let focusKey = null
//
const filterList = filterMark.value.serviceFacility
const index = filterList.findIndex((item) => item == resource.qxmc)
//
if (index > -1) {
filterList.splice(index, 1)
}
else {
filterList.push(resource.qxmc)
focusKey = resource.qxmc
}
// legend
const legendIndex = legendToolActiveItem.value.indexOf('serviceFacility')
// legend
if (filterList.length && legendIndex == -1) {
legendToolActiveItem.value.push('serviceFacility')
}
// legend
if (!filterList.length && legendIndex > -1) {
legendToolActiveItem.value.splice(legendIndex, 1)
}
// hook
mapImageMarkHook.filterYHZMark(
"serviceFacility",
(item) => {
return filterList.find((filterItem) => filterItem == item.qxmc)
},
(item) => {
return item.qxmc == focusKey
}
)
}
/** /**
* 处理图例工具栏的标记切换事件 * 处理图例工具栏的标记切换事件
* *
@ -422,10 +480,19 @@ const renderEmergencyForcePoints = async (entityService, points, markerIcon) =>
* @param {string} [payload.markerIcon] - 标记图标 * @param {string} [payload.markerIcon] - 标记图标
*/ */
const handleLegendMarkerToggle = async ({ key, active, markerIcon }) => { const handleLegendMarkerToggle = async ({ key, active, markerIcon }) => {
if (key === 'riskRoad') {
mapBase.toggleHighAltitudeRoadMap(active)
return
}
// //
if (key !== 'emergencyForce') { if (key !== 'emergencyForce') {
if (active == false) {
filterMark.value[key] = []
}
mapImageMark.toggleMark({ key, active, markerIcon }) mapImageMarkHook.toggleMark({ key, active, markerIcon })
return return
} }
@ -552,6 +619,7 @@ onBeforeUnmount(() => {
@supports (width: 1cqw) { @supports (width: 1cqw) {
--cq-inline-100: 100cqw; --cq-inline-100: 100cqw;
} }
@supports (height: 1cqh) { @supports (height: 1cqh) {
--cq-block-100: 100cqh; --cq-block-100: 100cqh;
} }
@ -568,13 +636,15 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: var(--cockpit-min-height); min-height: var(--cockpit-min-height);
min-width: 0; /* 允许 flex 子元素收缩 */ min-width: 0;
/* 允许 flex 子元素收缩 */
background: url(../assets/img/cockpit-main-bg.png) no-repeat; background: url(../assets/img/cockpit-main-bg.png) no-repeat;
background-size: cover; background-size: cover;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: auto; /* 当宿主高度 < 最小高度时允许滚动 */ overflow: auto;
/* 当宿主高度 < 最小高度时允许滚动 */
} }
/* 窄容器嵌入的紧凑布局(<1100px 宽度)*/ /* 窄容器嵌入的紧凑布局(<1100px 宽度)*/
@ -586,8 +656,10 @@ onBeforeUnmount(() => {
.cockpit-main { .cockpit-main {
position: relative; position: relative;
flex: 1; flex: 1;
min-height: 0; /* 允许网格在 flex 上下文中收缩 */ min-height: 0;
overflow: hidden; /* 防止内容溢出 */ /* 允许网格在 flex 上下文中收缩 */
overflow: hidden;
/* 防止内容溢出 */
} }
/* 地图底层 - 填满整个容器 */ /* 地图底层 - 填满整个容器 */
@ -603,7 +675,8 @@ onBeforeUnmount(() => {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 1; z-index: 1;
pointer-events: none; /* 不阻挡交互 */ pointer-events: none;
/* 不阻挡交互 */
background: url(../assets/img/遮罩层.png) no-repeat center/cover; background: url(../assets/img/遮罩层.png) no-repeat center/cover;
} }
@ -617,7 +690,8 @@ onBeforeUnmount(() => {
gap: var(--cockpit-gap); gap: var(--cockpit-gap);
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
pointer-events: none; /* 容器不拦截事件,让中间区域透明 */ pointer-events: none;
/* 容器不拦截事件,让中间区域透明 */
} }
/* 中间占位区域 - 透明且不可交互,点击穿透到地图 */ /* 中间占位区域 - 透明且不可交互,点击穿透到地图 */
@ -630,10 +704,13 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--cockpit-gap); gap: var(--cockpit-gap);
min-width: 0; /* 防止在窄容器中溢出 */ min-width: 0;
min-height: 0; /* 允许 flex 子元素收缩并启用滚动 */ /* 防止在窄容器中溢出 */
min-height: 0;
/* 允许 flex 子元素收缩并启用滚动 */
padding: 1rem; padding: 1rem;
pointer-events: auto; /* 恢复面板的交互能力 */ pointer-events: auto;
/* 恢复面板的交互能力 */
} }
/* 图例工具栏 - 居中显示在底部 */ /* 图例工具栏 - 居中显示在底部 */
@ -643,6 +720,7 @@ onBeforeUnmount(() => {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 3; z-index: 3;
pointer-events: auto; /* 确保图例可交互 */ pointer-events: auto;
/* 确保图例可交互 */
} }
</style> </style>

View File

@ -21,7 +21,7 @@
:class="{ 'row-alt': index % 2 === 0 }" :class="{ 'row-alt': index % 2 === 0 }"
> >
<div class="row-number">{{ index + 1 }}</div> <div class="row-number">{{ index + 1 }}</div>
<span class="district-name">{{ resource.qxmc }}</span> <span class="district-name" @click="emit('clickEmergencyResource' , resource)">{{ resource.qxmc }}</span>
<span class="count green">{{ resource.yhzCount }}</span> <span class="count green">{{ resource.yhzCount }}</span>
<span class="count orange">{{ resource.wzCount }}</span> <span class="count orange">{{ resource.wzCount }}</span>
<div class="equipment-cell"> <div class="equipment-cell">
@ -43,7 +43,9 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { request } from '@shared/utils/request' import { request } from '@shared/utils/request'
const resources = ref([ const emit = defineEmits(['clickEmergencyResource'])
const testData = [
{ {
id: 1, id: 1,
name: '万州区', name: '万州区',
@ -98,7 +100,9 @@ const resources = ref([
equipmentClass: 'red', equipmentClass: 'red',
hasAlert: false hasAlert: false
} }
]) ]
const resources = ref([])
// /district/statistics // /district/statistics
const getDistrictStatistics = async () => { const getDistrictStatistics = async () => {
@ -106,7 +110,6 @@ const getDistrictStatistics = async () => {
url: '/snow-ops-platform/district/statistics', url: '/snow-ops-platform/district/statistics',
method: 'GET' method: 'GET'
}) })
console.log(res)
if(res.code === '00000') { if(res.code === '00000') {
resources.value = res.data resources.value = res.data
} else { } else {
@ -240,6 +243,7 @@ onMounted(() => {
.district-name { .district-name {
text-align: left; text-align: left;
cursor: pointer;
} }
.count { .count {

View File

@ -11,8 +11,32 @@
</button> </button>
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="tooltip-content"> <div class="tooltip-content" v-if="!loading && detail">
<component v-if="hasData" :is="contentMap[data.mapData.layerId]" :data="data" />
<div class="info-item">
<span class="label">名称:</span>
<span class="value">{{ detail.mc }}</span>
</div>
<div class="info-item">
<span class="label">所属区县</span>
<span class="value">{{ detail.qxmc }}</span>
</div>
<div class="info-item">
<span class="label">应急设备</span>
<span class="value">{{ detail.sbsl }}</span>
</div>
<div class="info-item">
<span class="label">应急物资</span>
<span class="value">{{ detail.wzsl }}</span>
</div>
<div class="info-item">
<span class="label">应急人员</span>
<span class="value">{{ detail.rysl }}</span>
</div>
<!-- 如果没有数据 --> <!-- 如果没有数据 -->
<div v-if="!hasData" class="no-data"> <div v-if="!hasData" class="no-data">
@ -36,11 +60,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import serviceFacility from './serviceFacility.vue' import { getYHZDetail } from '@/views/cockpit/api/commonHttp.js'
import riskRoad from './riskRoad.vue'
import weatherAlert from './weatherAlert.vue'
import blockEvent from './blockEvent.vue'
/** /**
* 应急力量详情提示框组件 * 应急力量详情提示框组件
* 使用 HTML Overlay 方式显示在地图标记点上方 * 使用 HTML Overlay 方式显示在地图标记点上方
@ -75,14 +95,6 @@ const props = defineProps({
default: () => ({}) default: () => ({})
}, },
/**
* 加载状态
*/
loading: {
type: Boolean,
default: false
},
/** /**
* 错误信息 * 错误信息
*/ */
@ -93,16 +105,10 @@ const props = defineProps({
}) })
// ==================== State ==================== // ==================== State ====================
const contentMap = ref({
// const title = ref('养护站')
serviceFacility, const detail = ref(null)
// const loading = ref(true)
riskRoad,
//
blockEvent,
//
weatherAlert,
})
// ==================== Emits ==================== // ==================== Emits ====================
@ -117,22 +123,22 @@ const hasData = computed(() => {
return !!props.data return !!props.data
}) })
const title = computed(() => {
if (props.data.mapData.layerId === 'serviceFacility') {
return '养护站'
}
if (props.data.mapData.layerId === 'riskRoad') {
return '高海拔路段'
}
if (props.data.mapData.layerId === 'blockEvent') {
return '阻断事件'
}
if (props.data.mapData.layerId === 'weatherAlert') {
return '气象预警'
}
})
// ==================== Methods ==================== // ==================== Methods ====================
// index.js
const init = () => {
getDetail()
}
const getDetail = async () => {
loading.value = true
const res = await getYHZDetail({
id: props.data.id
})
if (res.success) {
detail.value = res.data
loading.value = false
}
}
/** /**
* 处理关闭按钮点击 * 处理关闭按钮点击
@ -140,11 +146,22 @@ const title = computed(() => {
const handleClose = () => { const handleClose = () => {
emit('close') emit('close')
} }
defineExpose({
init
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@use '@/styles/mixins.scss' as *; @use '@/styles/mixins.scss' as *;
.flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.common-tooltip { .common-tooltip {
// CSS // CSS
// PNG // PNG
@ -161,7 +178,7 @@ const handleClose = () => {
transform: translate(-50%, calc(-100% - 20px)); transform: translate(-50%, calc(-100% - 20px));
min-width: 200px; min-width: 200px;
max-width: 300px; width: 300px;
// padding + // padding +
// padding // padding
@ -262,25 +279,43 @@ const handleClose = () => {
// //
.tooltip-content { .tooltip-content {
padding-right: 1.5rem; // display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 5px;
.info-item:first-child {
grid-column: 1 / -1;
}
// padding-right: 1.5rem; //
}
.mb {
margin-bottom: 0.5rem
} }
// //
.info-item { .info-item {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
.label { .label {
white-space: nowrap;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
margin-right: 0.5rem; margin-right: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
} }
.value { .value {
color: #fff; font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: 500; font-weight: 500;
font-size: 14px;
color: #14FFF6;
line-height: 21px;
text-align: left;
font-style: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
} }

View File

@ -1,8 +1,13 @@
import { createVNode, render } from 'vue' import { createVNode, render } from 'vue'
import ImageMarkTooltip from './ImageMarkTooltip.vue' import serviceFacility from './YHZTooltip.vue'
class ImageMarkTooltipUI { const UIMap = {
constructor() { serviceFacility
}
class ImageMarkTooltip {
constructor({ key }) {
this.key = key
this.instance = null this.instance = null
this.container = null this.container = null
this.entity = null this.entity = null
@ -22,8 +27,10 @@ class ImageMarkTooltipUI {
this.entity = options.entity this.entity = options.entity
const UIInstance = UIMap[this.key]
// 创建 VNode // 创建 VNode
const vnode = createVNode(ImageMarkTooltip, { const vnode = createVNode(UIInstance, {
visible: true, visible: true,
position: options.position || { x: 0, y: 0 }, position: options.position || { x: 0, y: 0 },
data: options.data || {}, data: options.data || {},
@ -37,6 +44,9 @@ class ImageMarkTooltipUI {
// 渲染到容器 // 渲染到容器
render(vnode, this.container) render(vnode, this.container)
this.instance = vnode.component this.instance = vnode.component
if(this.instance.exposed?.init) {
this.instance.exposed.init()
}
return this.instance return this.instance
} }
@ -68,11 +78,7 @@ class ImageMarkTooltipUI {
} }
} }
// 创建单例实例
const instance = new ImageMarkTooltipUI()
export const CommonTooltip = instance export const newImageMarkTooltip = ({ key }) => {
return new ImageMarkTooltip({ key })
export const newImageMarkTooltip = () => {
return new ImageMarkTooltipUI()
} }

View File

@ -1,5 +1,23 @@
<template> <template>
<div class="tool-tip-content"> <div class="tool-tip-content">
<div class="info-item">
<span class="label">名称:</span>
<span class="value">{{ data.mc }}</span>
</div>
<div class="flex">
<div class="info-item">
<span class="label">所属区县</span>
<span class="value">{{ data.yjllpz }}</span>
</div>
<div class="info-item">
<span class="label">应急设备</span>
<span class="value">{{ data.wz }}</span>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>

View File

@ -1,51 +0,0 @@
<template>
<div class="content" v-if="data">
<div class="info-item">
<span class="label">名称:</span>
<span class="value">{{ data.mc }}</span>
</div>
<div class="flex">
<div class="info-item">
<span class="label">所属区县</span>
<span class="value">{{ data.yjllpz }}</span>
</div>
<div class="info-item">
<span class="label">应急物资</span>
<span class="value">{{ data.wz }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
});
</script>
<style scoped lang="scss">
@use '@/styles/mixins.scss' as *;
//
.info-item {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
.label {
color: rgba(255, 255, 255, 0.7);
margin-right: 0.5rem;
}
.value {
color: #fff;
font-weight: 500;
}
}
</style>

View File

@ -1,8 +1,6 @@
<template> <template>
<header class="page-header"> <header class="page-header">
<div class="header-bg"> <div class="header-bg">
<h1 class="title">安全保通服务</h1>
<button class="app-button">应用</button>
</div> </div>
</header> </header>
</template> </template>
@ -14,7 +12,7 @@
@use '@/styles/mixins.scss' as *; @use '@/styles/mixins.scss' as *;
.page-header { .page-header {
height: vh(137); height: vw(111);
background: url(../assets/img/header-bg.png) no-repeat; background: url(../assets/img/header-bg.png) no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
display: flex; display: flex;
@ -23,15 +21,6 @@
position: relative; position: relative;
} }
.header-bg {
background-image: url(../assets/img/header-title-bg.png);
width: 100%;
height: vh(107);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.title { .title {
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
@ -43,24 +32,4 @@
text-align: center; text-align: center;
} }
.app-button {
position: absolute;
right: vw(40);
top: 50%;
transform: translateY(-50%);
width: vw(165);
height: vh(44);
background: url(../assets/img/header-btn-app-bg.png) no-repeat;
background-size: 100% 100%;
color: rgba(255, 255, 255, 1);
font-size: fs(16);
font-family: SourceHanSansCN-Regular, sans-serif;
border: none;
cursor: pointer;
transition: opacity 0.3s;
&:hover {
opacity: 0.8;
}
}
</style> </style>

View File

@ -66,6 +66,7 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { getWeatherWarningStatistics } from '@/views/cockpit/api/commonHttp.js'
// //
import glowTopIcon from '../assets/img/weather-badge-glow-top.png' import glowTopIcon from '../assets/img/weather-badge-glow-top.png'
@ -79,7 +80,8 @@ const warningLevels = ref([
{ {
id: 1, id: 1,
name: '蓝色预警', name: '蓝色预警',
count: 320, key: 'blueWarningCount',
count: 0,
color: 'rgba(132, 199, 255, 1)', color: 'rgba(132, 199, 255, 1)',
glowTopIcon: glowTopIcon, glowTopIcon: glowTopIcon,
glowBottomIcon: glowBottomIcon, glowBottomIcon: glowBottomIcon,
@ -88,7 +90,8 @@ const warningLevels = ref([
{ {
id: 2, id: 2,
name: '黄色预警', name: '黄色预警',
count: 36, key: 'yellowWarningCount',
count: 0,
color: 'rgba(215, 209, 38, 1)', color: 'rgba(215, 209, 38, 1)',
glowTopIcon: glowTopIcon, glowTopIcon: glowTopIcon,
glowBottomIcon: glowBottomIcon, glowBottomIcon: glowBottomIcon,
@ -97,7 +100,8 @@ const warningLevels = ref([
{ {
id: 3, id: 3,
name: '橙色预警', name: '橙色预警',
count: 2, key: 'orangeWarningCount',
count: 0,
color: 'rgba(255, 114, 0, 1)', color: 'rgba(255, 114, 0, 1)',
glowTopIcon: glowTopIcon, glowTopIcon: glowTopIcon,
glowBottomIcon: glowBottomIcon, glowBottomIcon: glowBottomIcon,
@ -106,7 +110,8 @@ const warningLevels = ref([
{ {
id: 4, id: 4,
name: '红色预警', name: '红色预警',
count: 1, key: 'redWarningCount',
count: 0,
color: 'rgba(255, 32, 0, 1)', color: 'rgba(255, 32, 0, 1)',
glowTopIcon: glowTopIcon, glowTopIcon: glowTopIcon,
glowBottomIcon: glowBottomIcon, glowBottomIcon: glowBottomIcon,
@ -121,6 +126,24 @@ const districts = ref([
{ id: 4, name: '涪陵区', km: 35, warning: '蓝色预警', warningColor: 'rgba(47, 156, 246, 1)' }, { id: 4, name: '涪陵区', km: 35, warning: '蓝色预警', warningColor: 'rgba(47, 156, 246, 1)' },
{ id: 5, name: '合川区', km: 46, warning: '蓝色预警', warningColor: 'rgba(47, 156, 246, 1)' } { id: 5, name: '合川区', km: 46, warning: '蓝色预警', warningColor: 'rgba(47, 156, 246, 1)' }
]) ])
const loadData = async (areaCode) => {
const param = {
areaCode: areaCode || ''
}
const res = await getWeatherWarningStatistics(param)
if(res.success) {
for(const item of warningLevels.value) {
item.count = res.data[item.key]
}
}
}
defineExpose({
loadData
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -11,16 +11,19 @@ export default class ImageMarkData {
cacheData = null cacheData = null
expiresAt = null expiresAt = null
abortController = null abortController = null
loading = false
/** /**
* 60秒内重复点击使用缓存数据减少服务器压力 * 60秒内重复点击使用缓存数据减少服务器压力
*/ */
cacheTime = 60 * 1000 cacheTime = 60 * 1000
constructor({ api, tooltip, onResponse }) { constructor({ api, tooltip, markerIcon, onResponse, key }) {
this.api = api this.api = api
this.key = key
this.response = onResponse this.response = onResponse
this.markerIcon = markerIcon
// 初始化提示框 // 初始化提示框
this.tooltip = tooltip || newImageMarkTooltip() this.tooltip = tooltip || newImageMarkTooltip({key})
} }
getCache = () => { getCache = () => {
@ -37,7 +40,7 @@ export default class ImageMarkData {
// 首先查看缓存是否可用 // 首先查看缓存是否可用
const cacheData = this.getCache() const cacheData = this.getCache()
if (cacheData) return cacheData if (cacheData) return cacheData
this.loading = true
const controller = this.createAbortController() const controller = this.createAbortController()
const config = {} const config = {}
if (controller) config.signal = controller.signal if (controller) config.signal = controller.signal
@ -47,6 +50,7 @@ export default class ImageMarkData {
} catch (error) { } catch (error) {
console.error('请求失败: ', error) console.error('请求失败: ', error)
} finally { } finally {
this.loading = false
let data = null let data = null
if (this.response) data = this.response(res) if (this.response) data = this.response(res)
else data = this.commonOnResponse(res) else data = this.commonOnResponse(res)
@ -81,7 +85,6 @@ export default class ImageMarkData {
// 通用的响应处理 // 通用的响应处理
commonOnResponse = (res) => { commonOnResponse = (res) => {
if (res?.success) { if (res?.success) {
res.data = res.data.slice(0, 2)
const dataList = res.data.map((item) => { const dataList = res.data.map((item) => {
item.mapData = { item.mapData = {
id: this.key + '-' + item.rid, id: this.key + '-' + item.rid,
@ -91,7 +94,6 @@ export default class ImageMarkData {
} }
return item return item
}) })
this.data = dataList
return dataList return dataList
} }
return [] return []

View File

@ -1,4 +1,4 @@
import { getBaseMap, getBusinessMap } from '@/views/cockpit/api/commonHttp.js' import { getBaseMap, getBusinessMap, getHighAltitudeRoadMap } from '@/views/cockpit/api/commonHttp.js'
import * as Cesium from 'cesium' import * as Cesium from 'cesium'
@ -6,6 +6,8 @@ import * as Cesium from 'cesium'
// 主要是加载地图底图 // 主要是加载地图底图
export const useMapBase = (mapStore) => { export const useMapBase = (mapStore) => {
let hightAltitudeRoadMapId
// 加载当前业务的底图, 类似于天地图,但是没有使用天地图作为底图,有大的地块的地形纹理,但是缩小范围很小,属于比较粗的图 // 加载当前业务的底图, 类似于天地图,但是没有使用天地图作为底图,有大的地块的地形纹理,但是缩小范围很小,属于比较粗的图
const loadBaseMap = async () => { const loadBaseMap = async () => {
const layerService = mapStore.services().layer const layerService = mapStore.services().layer
@ -53,7 +55,7 @@ export const useMapBase = (mapStore) => {
return layers; return layers;
}; };
// 加载业务地图,业务地图主要是高亮当前业务下的地区的区县,边界都会有高亮线条 // 加载业务地图,业务地图主要是高亮当前业务下的地区的区县与管线图,边界都会有高亮线条
const loadBusinessMap = async () => { const loadBusinessMap = async () => {
const layerService = mapStore.services().layer const layerService = mapStore.services().layer
const res = await getBusinessMap() const res = await getBusinessMap()
@ -75,6 +77,34 @@ export const useMapBase = (mapStore) => {
} }
} }
// 加载高海拔底图
const toggleHighAltitudeRoadMap = async (flag) => {
const layerService = mapStore.services().layer
if(!flag && hightAltitudeRoadMapId) {
layerService.removeLayer(hightAltitudeRoadMapId)
return
}
const res = await getHighAltitudeRoadMap()
const resData = res.data
resData[0].Children = resData
mapStore.baseMapGroups = resData
for (const item of resData) {
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
for (const layerConfig of layers) {
const layer = {
id: layerConfig.id,
type: layerConfig.type,
url: layerConfig.url,
meta: layerConfig.meta,
}
hightAltitudeRoadMapId = await layerService.addLayer(layer)
}
}
}
const loadBaseData = () => { const loadBaseData = () => {
setTimeout(() => { setTimeout(() => {
loadBaseMap() loadBaseMap()
@ -83,6 +113,7 @@ export const useMapBase = (mapStore) => {
} }
return { return {
loadBaseData loadBaseData,
toggleHighAltitudeRoadMap
} }
} }

View File

@ -1,7 +1,18 @@
import { fetchEmergencyForceList } from '@/views/cockpit/api/emergencyForce' import { fetchEmergencyForceList } from '@/views/cockpit/api/emergencyForce'
import { getAllYHZList } from '@/views/cockpit/api/commonHttp'
import ImageMarkData from './ImageMarkData' import ImageMarkData from './ImageMarkData'
import * as Cesium from 'cesium' import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
import serviceFacilityMarkerIcon from '../assets/legendTool/服务设施icon定位.png'
import riskRoadMarkerIcon from '../assets/legendTool/风险路段icon定位.png'
import blockEventMarkerIcon from '../assets/legendTool/阻断事件icon定位.png'
import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定位.png'
import weatherAlertMarkerIcon from '../assets/legendTool/气象预警icon定位.png'
import { ElMessage } from 'element-plus'
/** /**
* 当前业务下的地图服务 * 当前业务下的地图服务
@ -15,16 +26,41 @@ export const useMapImageMark = (mapStore) => {
// 接口服务映射 // 接口服务映射
const imageMarkMap = { const imageMarkMap = {
// 养护站 legend
serviceFacility: new ImageMarkData({ serviceFacility: new ImageMarkData({
api: fetchEmergencyForceList, key: 'serviceFacility',
}), api: getAllYHZList,
riskRoad: new ImageMarkData({ markerIcon: serviceFacilityMarkerIcon,
api: fetchEmergencyForceList, onResponse: function (res) {
if (res?.success) {
res.data = res.data.filter((item) => {
if (isNaN(item.jd) || isNaN(item.wd)) {
return false
}
return true
})
const dataList = res.data.map((item) => {
item.mapData = {
id: this.key + '-' + item.id,
layerId: this.key,
position: [Number(item.jd), Number(item.wd), 0],
image: this.markerIcon
}
return item
})
return dataList
}
return []
}
}), }),
blockEvent: new ImageMarkData({ blockEvent: new ImageMarkData({
key: 'blockEvent',
markerIcon: blockEventMarkerIcon,
api: fetchEmergencyForceList, api: fetchEmergencyForceList,
}), }),
weatherAlert: new ImageMarkData({ weatherAlert: new ImageMarkData({
key: 'weatherAlert',
markerIcon: weatherAlertMarkerIcon,
api: fetchEmergencyForceList, api: fetchEmergencyForceList,
}) })
} }
@ -99,23 +135,45 @@ export const useMapImageMark = (mapStore) => {
imageMarkData.tooltip.close() imageMarkData.tooltip.close()
} }
/**
* 聚焦到图片实例的中心点
*/
const focusOnMarkers = (dataList) => {
if (!dataList || dataList.length === 0) return
const positions = dataList.map((item) => {
return [item.mapData.position[0], item.mapData.position[1]]
})
const features = turf.points(positions)
const center = turf.center(features)
const position = center.geometry.coordinates
mapStore.viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(position[0], position[1], 160000),
orientation: {
heading: Cesium.Math.toRadians(0), // 方向角
pitch: Cesium.Math.toRadians(-90), // 俯仰角(-90 为垂直向下看)
roll: 0 // 翻滚角
}
})
}
/** /**
* 根据图标名称请求后台服务动态加载图标列表 * 根据图标名称请求后台服务动态加载图标列表
*/ */
const loadDynamicMark = async ({ key, markerIcon, params }) => { const loadDynamicMark = async ({ key, params }) => {
const imageMarkData = imageMarkMap[key] const imageMarkData = imageMarkMap[key]
if (!imageMarkData) return if (!imageMarkData) return
imageMarkData.markerIcon = markerIcon // 取消上一次请求
imageMarkData.key = key imageMarkData.cancelRequest()
const dataList = await imageMarkData.request(params) const dataList = await imageMarkData.request(params)
clearLayerEntity(key)
drawImageEntities(dataList) drawImageEntities(dataList)
} }
/** /**
* 显示/隐藏地图标 * 显示/隐藏地图标
*/ */
const toggleMark = async ({ key, active, markerIcon, params }) => { const toggleMark = async ({ key, active, params }) => {
const imageMarkData = imageMarkMap[key] const imageMarkData = imageMarkMap[key]
if (!active) { if (!active) {
@ -124,7 +182,31 @@ export const useMapImageMark = (mapStore) => {
return return
} }
loadDynamicMark({ key, markerIcon, params }) loadDynamicMark({ key, params })
}
/**
* 过滤养护站图标
*/
const filterYHZMark = async (key, filterFn, focusFn) => {
const imageMarkData = imageMarkMap[key]
if (!imageMarkData) return
// 取消上一次请求
imageMarkData.cancelRequest()
let dataList = await imageMarkData.request()
dataList = dataList.filter((item) => {
return filterFn(item)
})
clearLayerEntity(key)
// 绘制过滤出来的点
drawImageEntities(dataList)
// 聚焦聚焦函数过滤出来的点
const focusDataList = dataList.filter((item)=>{
return focusFn(item)
})
if(focusDataList) {
focusOnMarkers(focusDataList)
}
} }
/** /**
@ -171,7 +253,8 @@ export const useMapImageMark = (mapStore) => {
} }
return { return {
toggleMark toggleMark,
filterYHZMark
} }
} }

View File

@ -0,0 +1,24 @@
export function loadAMap() {
return new Promise((resolve, reject) => {
if (window.AMap) return resolve();
// 确保AMapLoader存在
if (!window.AMapLoader) {
return reject(new Error("AMapLoader未正确加载"));
}
window.AMapLoader.load({
key: "848ab05db2a57a7782c153119f50dcef",
version: "2.0",
})
.then((AMap) => {
window.AMap = AMap;
console.log("AMap初始化完成", AMap);
resolve();
})
.catch((err) => {
console.error("AMap加载失败:", err);
reject(new Error("地图加载失败,请检查网络连接"));
});
});
};