Compare commits
1 Commits
sjy
...
32d95f323f
| Author | SHA1 | Date | |
|---|---|---|---|
| 32d95f323f |
@@ -26,3 +26,4 @@ dist-ssr
|
|||||||
auto-imports.d.ts
|
auto-imports.d.ts
|
||||||
components.d.ts
|
components.d.ts
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@
|
|||||||
"sockjs-client": "^1.6.1",
|
"sockjs-client": "^1.6.1",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"vite-plugin-cesium": "^1.2.22",
|
"vite-plugin-cesium": "^1.2.22",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.35",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -49,4 +49,4 @@
|
|||||||
"vite-plugin-vue-devtools": "^8.0.3",
|
"vite-plugin-vue-devtools": "^8.0.3",
|
||||||
"vue-tsc": "^3.2.6"
|
"vue-tsc": "^3.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useStatusStore } from '@/stores/useStatusStore';
|
import { provide } from 'vue';//从Vue导入provide函数,用于依赖注入
|
||||||
import AroundAnalysisDetailComponent from './around-analysis/AroundAnalysisDetailComponent.vue';
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
import ButtonComponent from './around-analysis/ButtonComponent.vue';
|
import { useAnalysisButton } from '@/hooks/rain-earthquake/useAnalysisButton';
|
||||||
import SearchComponent from './around-analysis/SearchComponent.vue';
|
import AroundAnalysisDetailComponent from './around-analysis/AroundAnalysisDetailComponent.vue';
|
||||||
|
import ButtonComponent from './around-analysis/ButtonComponent.vue';
|
||||||
|
import SearchComponent from './around-analysis/SearchComponent.vue';
|
||||||
|
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
|
// 在父组件中创建唯一的 Hook 实例,包含所有周边分析相关的状态和方法
|
||||||
|
const analysisButtonState = useAnalysisButton();
|
||||||
|
|
||||||
|
// 通过 provide 共享给所有子组件(让 TypeScript 自动推断类型)
|
||||||
|
provide('analysisButtonState', analysisButtonState);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
+138
-3
@@ -1,7 +1,142 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div v-if="showAreaDialog" class="area-dialog" :style="{ left: dialogPosition.x + 'px', top: dialogPosition.y + 'px' }">
|
||||||
|
<div class="dialog-header">选择区域</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="radius-input-group">
|
||||||
|
<span class="label">半径:</span>
|
||||||
|
<input
|
||||||
|
v-model.number="radius"
|
||||||
|
type="number"
|
||||||
|
class="radius-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
<span class="unit">公里</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="confirm-btn" @click="handleConfirm">确认添加</button>
|
||||||
|
<button class="cancel-btn" @click="handleCancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue';//从Vue导入inject函数,用于依赖注入
|
||||||
|
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
<style scoped></style>
|
// 从父组件注入共享状态,明确指定类型
|
||||||
|
const analysisButtonState = inject<AnalysisButtonState>('analysisButtonState');
|
||||||
|
|
||||||
|
const {
|
||||||
|
showAreaDialog,
|
||||||
|
radius,
|
||||||
|
dialogPosition,
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel
|
||||||
|
} = analysisButtonState!;//使用非空断言操作符,告诉TypeScript该值一定存在
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 对话框容器样式 */
|
||||||
|
.area-dialog {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100001;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 0;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 60, 120, 0.95), rgba(0, 40, 80, 0.95));
|
||||||
|
border: 2px solid #00b4ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 180, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* 对话框标题栏样式 */
|
||||||
|
.dialog-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(90deg, #00b4ff, #0080cc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 10px 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radius-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
/* 标签和单位文字的样式 */
|
||||||
|
.radius-input-group .label,
|
||||||
|
.radius-input-group .unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radius-input {
|
||||||
|
width: 50px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 100, 180, 0.6);
|
||||||
|
border: 1px solid #00b4ff;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
/* 隐藏数字输入框的上下调节按钮(针对WebKit浏览器) */
|
||||||
|
.radius-input::-webkit-inner-spin-button,
|
||||||
|
.radius-input::-webkit-outer-spin-button {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 6px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
background: linear-gradient(180deg, #2d8a4e, #1e6b3a);
|
||||||
|
border: 1px solid #3da862;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn:hover {
|
||||||
|
background: linear-gradient(180deg, #3da862, #2d8a4e);
|
||||||
|
box-shadow: 0 2px 8px rgba(45, 138, 78, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: linear-gradient(180deg, #c0392b, #96281b);
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: linear-gradient(180deg, #e74c3c, #c0392b);
|
||||||
|
box-shadow: 0 2px 8px rgba(192, 57, 43, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div class="analysis-button-box">
|
||||||
|
<ul class="analysis-button-ul">
|
||||||
|
<li v-for="(buttonItem, index) in analysisButtons" :key="index">
|
||||||
|
<button
|
||||||
|
@click="handleButtonClick(index, buttonItem.callback)"
|
||||||
|
:style="{
|
||||||
|
'background-image': `url(${selectedButtonIndex === index ? rightOrangeButton : rightBlueButton})`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ selectedButtonIndex === index && buttonItem.activeName ? buttonItem.activeName : buttonItem.name }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import { rightBlueButton, rightOrangeButton } from '@/assets';
|
||||||
|
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
<style scoped></style>
|
// 从父组件注入共享状态,明确指定类型
|
||||||
|
const analysisButtonState = inject<AnalysisButtonState>('analysisButtonState');
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedButtonIndex,
|
||||||
|
analysisButtons,
|
||||||
|
handleButtonClick
|
||||||
|
} = analysisButtonState!;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.analysis-button-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 95px;
|
||||||
|
right: 40px;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 180px;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-button-ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-button-ul li {
|
||||||
|
margin: 15px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-button-ul li button {
|
||||||
|
width: 190px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 5px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
background-size: 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 20px center;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,105 @@
|
|||||||
<!-- 搜索组件 -->
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="search-component-box">
|
||||||
搜索
|
<el-autocomplete
|
||||||
|
v-model="state"
|
||||||
|
:fetch-suggestions="querySearch"
|
||||||
|
popper-class="my-autocomplete"
|
||||||
|
placeholder="搜索地点"
|
||||||
|
@select="handleSelect"
|
||||||
|
@focus="handleFocus"
|
||||||
|
clearable
|
||||||
|
:disabled="!canSearch"
|
||||||
|
:teleported="false"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-icon class="el-input__icon">
|
||||||
|
<edit />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="value">{{ item.value }}</div>
|
||||||
|
<span class="link">{{ item.lon?.toFixed(4) }}, {{ item.lat?.toFixed(4) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts"></script>
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { Edit } from '@element-plus/icons-vue';
|
||||||
|
import { useAroundAnalysis } from '@/hooks/rain-earthquake/useAroundAnalysis';
|
||||||
|
|
||||||
<style scoped></style>
|
const {
|
||||||
|
state,
|
||||||
|
canSearch,
|
||||||
|
querySearch,
|
||||||
|
handleSelect,
|
||||||
|
handleFocus,
|
||||||
|
loadAllPointData
|
||||||
|
} = useAroundAnalysis();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAllPointData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-component-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 125px;
|
||||||
|
right: 210px;
|
||||||
|
z-index: 10000;
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(60, 99, 147, 0.9) ;
|
||||||
|
box-shadow: 0 0 0 1px rgba(160, 173, 192, 0.5) ;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.el-input__inner) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.el-input__inner::placeholder) {
|
||||||
|
color: #9ca9b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 2px rgba(79, 131, 194, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.my-autocomplete) {
|
||||||
|
background: rgba(26, 58, 95, 0.95);
|
||||||
|
border: 1px solid #2a3d58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.my-autocomplete li) {
|
||||||
|
padding: 2px 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #e6edf3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.my-autocomplete li .value) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.my-autocomplete li .link) {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-component-box :deep(.my-autocomplete li:hover),
|
||||||
|
.search-component-box :deep(.my-autocomplete li.highlighted) {
|
||||||
|
background: rgba(58, 112, 169, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
import { ref, reactive, onUnmounted, watch, computed } from 'vue';
|
||||||
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
||||||
|
import { LoadingResource } from '@/types/common/LoadingResourceType';
|
||||||
|
import type {
|
||||||
|
PointResource,
|
||||||
|
PointResourceCategory,
|
||||||
|
ButtonResourceConfig,
|
||||||
|
AnalysisButtonConfig,
|
||||||
|
DialogPosition,
|
||||||
|
AnalysisButtonState
|
||||||
|
} from '@/types/common/useAroundAnalysisType';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
import {
|
||||||
|
ScreenSpaceEventHandler,
|
||||||
|
ScreenSpaceEventType,
|
||||||
|
Cartesian2,
|
||||||
|
Cartographic,
|
||||||
|
Cartesian3,
|
||||||
|
} from 'cesium';
|
||||||
|
import { useCircleDrawer } from './useCircleDrawer';
|
||||||
|
import { usePulseEffect } from './usePulseEffect';
|
||||||
|
import { useMarkerManager } from './useMarkerManager';
|
||||||
|
|
||||||
|
// ==================== 常量配置 ====================
|
||||||
|
const DIALOG_WIDTH = 280;
|
||||||
|
const DIALOG_HEIGHT = 150;
|
||||||
|
const DIALOG_PADDING = 10;
|
||||||
|
const DIALOG_OFFSET = 20;
|
||||||
|
const EARTH_RADIUS = 6371000;
|
||||||
|
const MIN_FLY_HEIGHT = 10000;
|
||||||
|
const FLY_HEIGHT_MULTIPLIER = 6000;
|
||||||
|
const FLY_DURATION = 2;
|
||||||
|
|
||||||
|
// ==================== Store 实例 ====================
|
||||||
|
const statusStore = useStatusStore();
|
||||||
|
const loadingResourceStore = useLoadingResourceStore();
|
||||||
|
|
||||||
|
const poi = computed(() => statusStore.poiLayers);
|
||||||
|
const map = computed(() => statusStore.mapLayers);
|
||||||
|
const infra = computed(() => statusStore.infrastructureLayers);
|
||||||
|
|
||||||
|
// ==================== 资源配置 ====================
|
||||||
|
const RESOURCE_CONFIGS: ButtonResourceConfig[] = [
|
||||||
|
{ key: LoadingResource.SCHOOL, category: 'school' },
|
||||||
|
{ key: LoadingResource.HOSPITAL, category: 'hospital' },
|
||||||
|
{ key: LoadingResource.DANGEROUS_SOURCE, category: 'danger' },
|
||||||
|
{ key: LoadingResource.EMERGENCY_SHELTER, category: 'shelter' },
|
||||||
|
{ key: LoadingResource.FIRE_STATION, category: 'fire' },
|
||||||
|
{ key: LoadingResource.STORE_POINTS, category: 'store' },
|
||||||
|
{ key: LoadingResource.SUBWAY_STATION, category: 'subway' },
|
||||||
|
{ key: LoadingResource.LANDSLIDE_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'landslide' },
|
||||||
|
{ key: LoadingResource.DEBRIS_FLOW_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'debris_flow' },
|
||||||
|
{ key: LoadingResource.WATER_LOGGING_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'water_logging' },
|
||||||
|
{ key: LoadingResource.FLASH_FLOOD_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'flash_flood' },
|
||||||
|
{ key: LoadingResource.RISK_POINT, category: 'risk-point' },
|
||||||
|
{ key: LoadingResource.BRIDGE, category: 'bridge' },
|
||||||
|
{ key: LoadingResource.RESERVOIR, category: 'reservoir' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 图层可见性判断 ====================
|
||||||
|
const isCategoryVisible = (category: PointResourceCategory, originalType?: string): boolean => {
|
||||||
|
const visibilityMap: Record<string, () => boolean> = {
|
||||||
|
school: () => poi.value.showSchool.show,
|
||||||
|
hospital: () => poi.value.showHospital.show,
|
||||||
|
danger: () => poi.value.showDangerSource.show,
|
||||||
|
shelter: () => poi.value.showRefugeeShelter.show,
|
||||||
|
fire: () => poi.value.showFireStation.show,
|
||||||
|
store: () => poi.value.showReservePoint.show,
|
||||||
|
subway: () => poi.value.showSubwayStation.show,
|
||||||
|
'risk-point': () => map.value.riskPointShow.show,
|
||||||
|
bridge: () => infra.value.showBridge.show,
|
||||||
|
reservoir: () => infra.value.showReservoir.show,
|
||||||
|
'hidden-danger': () => {
|
||||||
|
const hiddenMap: Record<string, () => boolean> = {
|
||||||
|
landslide: () => poi.value.showLandslideHiddenPoint.show,
|
||||||
|
debris_flow: () => poi.value.showDebrisFlowHiddenPoint.show,
|
||||||
|
water_logging: () => poi.value.showWaterLoggingHiddenPoint.show,
|
||||||
|
flash_flood: () => poi.value.showFlashFloodHiddenPoint.show,
|
||||||
|
};
|
||||||
|
return hiddenMap[originalType || '']?.() ?? false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return visibilityMap[category]?.() ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 响应式状态 ====================
|
||||||
|
export const useAnalysisButton = (): AnalysisButtonState => {
|
||||||
|
const selectedButtonIndex = ref<number>(-1);
|
||||||
|
const showAreaDialog = ref(false);
|
||||||
|
const radius = ref(10);
|
||||||
|
const dialogPosition = reactive<DialogPosition>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
let clickHandler: ScreenSpaceEventHandler | null = null;
|
||||||
|
let currentCenterPosition: Cartesian3 | null = null;
|
||||||
|
|
||||||
|
// ==================== 组合子 Hook ====================
|
||||||
|
const { drawCircle, clearCircle } = useCircleDrawer();
|
||||||
|
const { addPulseEffectToPoints, removePulseEffect } = usePulseEffect();
|
||||||
|
const { addMarker, removeMarker } = useMarkerManager();
|
||||||
|
|
||||||
|
// ==================== 数据加载与计算 ====================
|
||||||
|
|
||||||
|
const loadAllPointData = (): PointResource[] => {
|
||||||
|
const resources: PointResource[] = [];
|
||||||
|
|
||||||
|
RESOURCE_CONFIGS.forEach(config => {
|
||||||
|
const data = loadingResourceStore.getLoadingResource(config.key).info;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const convertedData = data.map((item: Record<string, unknown>) => {
|
||||||
|
const id = item.id || item._id || item.uuid || 'unknown_id';
|
||||||
|
const safeId = typeof id === 'string' ? id : typeof id === 'number' ? id : 'unknown_id';
|
||||||
|
const value = (item.name && String(item.name).trim() !== '')
|
||||||
|
? String(item.name)
|
||||||
|
: String(safeId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: safeId,
|
||||||
|
value,
|
||||||
|
category: config.category,
|
||||||
|
originalType: (config.forcedType || (item.type as string) || (item.disasterType as string))?.toLowerCase()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
resources.push(...convertedData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const seenIds = new Map<string | number, PointResource>();
|
||||||
|
for (const item of resources) {
|
||||||
|
if (!seenIds.has(item.id)) {
|
||||||
|
seenIds.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueResources = Array.from(seenIds.values());
|
||||||
|
console.log('加载的点数据总数:', uniqueResources.length);
|
||||||
|
return uniqueResources;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDistance = (
|
||||||
|
centerLon: number,
|
||||||
|
centerLat: number,
|
||||||
|
pointLon: unknown,
|
||||||
|
pointLat: unknown
|
||||||
|
): number => {
|
||||||
|
const pLon = Number(pointLon);
|
||||||
|
const pLat = Number(pointLat);
|
||||||
|
|
||||||
|
if (isNaN(pLon) || isNaN(pLat)) return Infinity;
|
||||||
|
|
||||||
|
const dLat = (pLat - centerLat) * Math.PI / 180;
|
||||||
|
const dLon = (pLon - centerLon) * Math.PI / 180;
|
||||||
|
const a = Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(centerLat * Math.PI / 180) * Math.cos(pLat * Math.PI / 180) *
|
||||||
|
Math.sin(dLon / 2) ** 2;
|
||||||
|
return EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPointsInCircle = (centerPosition: Cartesian3, radiusKm: number): PointResource[] => {
|
||||||
|
const cartographic = Cartographic.fromCartesian(centerPosition);
|
||||||
|
const centerLon = cartographic.longitude * (180 / Math.PI);
|
||||||
|
const centerLat = cartographic.latitude * (180 / Math.PI);
|
||||||
|
|
||||||
|
const allPoints = loadAllPointData();
|
||||||
|
const radiusMeters = radiusKm * 1000;
|
||||||
|
|
||||||
|
return allPoints.filter(point => {
|
||||||
|
if (point.lon === undefined || point.lat === undefined) return false;
|
||||||
|
|
||||||
|
const distance = calculateDistance(centerLon, centerLat, point.lon, point.lat);
|
||||||
|
return distance <= radiusMeters && isCategoryVisible(point.category as PointResourceCategory, point.originalType);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新脉冲效果
|
||||||
|
*/
|
||||||
|
const refreshPulseEffect = () => {
|
||||||
|
if (!currentCenterPosition) return;
|
||||||
|
|
||||||
|
console.log('刷新脉冲效果...');
|
||||||
|
removePulseEffect();
|
||||||
|
|
||||||
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
||||||
|
addPulseEffectToPoints(pointsInCircle);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 地图事件处理 ====================
|
||||||
|
|
||||||
|
const registerMapClickHandler = () => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
clickHandler = new ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||||
|
clickHandler.setInputAction((clickEvent: { position: Cartesian2 }) => {
|
||||||
|
const cartesian = viewer.camera.pickEllipsoid(clickEvent.position, viewer.scene.globe.ellipsoid);
|
||||||
|
if (cartesian) {
|
||||||
|
currentCenterPosition = cartesian;
|
||||||
|
const cartographic = Cartographic.fromCartesian(cartesian);
|
||||||
|
const longitude = cartographic.longitude * (180 / Math.PI);
|
||||||
|
const latitude = cartographic.latitude * (180 / Math.PI);
|
||||||
|
|
||||||
|
console.log('点击位置:', { longitude, latitude });
|
||||||
|
addMarker(cartesian);
|
||||||
|
showAreaDialog.value = true;
|
||||||
|
calculateDialogPosition(clickEvent.position);
|
||||||
|
}
|
||||||
|
}, ScreenSpaceEventType.LEFT_CLICK);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMapClickHandler = () => {
|
||||||
|
if (clickHandler) {
|
||||||
|
clickHandler.destroy();
|
||||||
|
clickHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDialogPosition = (clickPosition: Cartesian2) => {
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
const screenHeight = window.innerHeight;
|
||||||
|
|
||||||
|
let x = clickPosition.x + DIALOG_OFFSET;
|
||||||
|
let y = clickPosition.y + DIALOG_OFFSET;
|
||||||
|
|
||||||
|
if (x + DIALOG_WIDTH > screenWidth - DIALOG_PADDING) {
|
||||||
|
x = clickPosition.x - DIALOG_WIDTH - DIALOG_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + DIALOG_HEIGHT > screenHeight - DIALOG_PADDING) {
|
||||||
|
y = clickPosition.y - DIALOG_HEIGHT - DIALOG_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogPosition.x = Math.max(DIALOG_PADDING, Math.min(x, screenWidth - DIALOG_WIDTH - DIALOG_PADDING));
|
||||||
|
dialogPosition.y = Math.max(DIALOG_PADDING, Math.min(y, screenHeight - DIALOG_HEIGHT - DIALOG_PADDING));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 资源清理 ====================
|
||||||
|
|
||||||
|
const clearAllAnalysisResources = () => {
|
||||||
|
removeMarker();
|
||||||
|
clearCircle();
|
||||||
|
removePulseEffect();
|
||||||
|
currentCenterPosition = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 事件处理 ====================
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!currentCenterPosition) {
|
||||||
|
console.error('中心点位置不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('确认添加区域分析', {
|
||||||
|
radius: radius.value,
|
||||||
|
center: currentCenterPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
drawCircle(currentCenterPosition, radius.value);
|
||||||
|
|
||||||
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
||||||
|
addPulseEffectToPoints(pointsInCircle);
|
||||||
|
|
||||||
|
const cartographic = Cartographic.fromCartesian(currentCenterPosition);
|
||||||
|
const longitude = cartographic.longitude * (180 / Math.PI);
|
||||||
|
const latitude = cartographic.latitude * (180 / Math.PI);
|
||||||
|
|
||||||
|
const flyHeight = Math.max(radius.value * FLY_HEIGHT_MULTIPLIER, MIN_FLY_HEIGHT);
|
||||||
|
CesiumUtilsSingleton.flyToTarget([longitude, latitude, flyHeight], FLY_DURATION);
|
||||||
|
showAreaDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
showAreaDialog.value = false;
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = (index: number, callback: (status: boolean) => void) => {
|
||||||
|
const isActive = selectedButtonIndex.value === index;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
selectedButtonIndex.value = -1;
|
||||||
|
callback(false);
|
||||||
|
} else {
|
||||||
|
if (selectedButtonIndex.value !== -1) {
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
showAreaDialog.value = false;
|
||||||
|
}
|
||||||
|
selectedButtonIndex.value = index;
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 监听器 ====================
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => loadingResourceStore.loadingResource,
|
||||||
|
() => {
|
||||||
|
console.log('检测到资源数据变化,刷新脉冲效果');
|
||||||
|
refreshPulseEffect();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const layerVisibilityWatchers = [
|
||||||
|
() => poi.value.showSchool.show,
|
||||||
|
() => poi.value.showHospital.show,
|
||||||
|
() => poi.value.showDangerSource.show,
|
||||||
|
() => poi.value.showRefugeeShelter.show,
|
||||||
|
() => poi.value.showFireStation.show,
|
||||||
|
() => poi.value.showReservePoint.show,
|
||||||
|
() => poi.value.showSubwayStation.show,
|
||||||
|
() => poi.value.showLandslideHiddenPoint.show,
|
||||||
|
() => poi.value.showDebrisFlowHiddenPoint.show,
|
||||||
|
() => poi.value.showWaterLoggingHiddenPoint.show,
|
||||||
|
() => poi.value.showFlashFloodHiddenPoint.show,
|
||||||
|
() => map.value.riskPointShow.show,
|
||||||
|
() => infra.value.showBridge.show,
|
||||||
|
() => infra.value.showReservoir.show,
|
||||||
|
];
|
||||||
|
|
||||||
|
watch(layerVisibilityWatchers, () => {
|
||||||
|
console.log('检测到图层可见性变化,刷新脉冲效果');
|
||||||
|
refreshPulseEffect();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
removeMapClickHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 按钮配置 ====================
|
||||||
|
const analysisButtons: AnalysisButtonConfig[] = [
|
||||||
|
{
|
||||||
|
name: '标记区域分析',
|
||||||
|
activeName: '取消区域分析',
|
||||||
|
callback: (status: boolean) => {
|
||||||
|
console.log('标记区域分析', status);
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer?.canvas) return;
|
||||||
|
|
||||||
|
statusStore.cursorStyle = status ? 'crosshair' : 'default';
|
||||||
|
viewer.canvas.style.cursor = status ? 'crosshair' : 'default';
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
registerMapClickHandler();
|
||||||
|
} else {
|
||||||
|
removeMapClickHandler();
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
showAreaDialog.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '隐藏行政区划',
|
||||||
|
callback: (status: boolean) => {
|
||||||
|
console.log('隐藏行政区划', status);
|
||||||
|
useStatusStore().mapLayers.showAdministrativeDivision.show = !status;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedButtonIndex,
|
||||||
|
showAreaDialog,
|
||||||
|
radius,
|
||||||
|
dialogPosition,
|
||||||
|
analysisButtons,
|
||||||
|
handleButtonClick,
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel,
|
||||||
|
refreshPulseEffect,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
||||||
|
import { LoadingResource } from '@/types/common/LoadingResourceType';
|
||||||
|
import type { PointResource, ResourceConfig } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周边分析搜索组件钩子函数
|
||||||
|
* @returns 搜索相关的状态和方法
|
||||||
|
*/
|
||||||
|
export const useAroundAnalysis = () => {
|
||||||
|
const statusStore = useStatusStore();//用于访问图层显示状态
|
||||||
|
const loadingResourceStore = useLoadingResourceStore();//用于访问各类点位数据
|
||||||
|
// 计算属性:获取图层的显示状态
|
||||||
|
const poi = computed(() => statusStore.poiLayers);
|
||||||
|
const map = computed(() => statusStore.mapLayers);
|
||||||
|
const infra = computed(() => statusStore.infrastructureLayers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源配置列表
|
||||||
|
*/
|
||||||
|
const RESOURCE_CONFIGS: ResourceConfig[] = [
|
||||||
|
{ key: LoadingResource.SCHOOL, category: 'school', isVisible: () => poi.value.showSchool.show },
|
||||||
|
{ key: LoadingResource.HOSPITAL, category: 'hospital', isVisible: () => poi.value.showHospital.show },
|
||||||
|
{ key: LoadingResource.DANGEROUS_SOURCE, category: 'danger', isVisible: () => poi.value.showDangerSource.show },
|
||||||
|
{ key: LoadingResource.EMERGENCY_SHELTER, category: 'shelter', isVisible: () => poi.value.showRefugeeShelter.show },
|
||||||
|
{ key: LoadingResource.FIRE_STATION, category: 'fire', isVisible: () => poi.value.showFireStation.show },
|
||||||
|
{ key: LoadingResource.STORE_POINTS, category: 'store', isVisible: () => poi.value.showReservePoint.show },
|
||||||
|
{ key: LoadingResource.SUBWAY_STATION, category: 'subway', isVisible: () => poi.value.showSubwayStation.show },
|
||||||
|
{ key: LoadingResource.LANDSLIDE_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'landslide', isVisible: () => poi.value.showLandslideHiddenPoint.show },
|
||||||
|
{ key: LoadingResource.DEBRIS_FLOW_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'debris_flow', isVisible: () => poi.value.showDebrisFlowHiddenPoint.show },
|
||||||
|
{ key: LoadingResource.WATER_LOGGING_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'water_logging', isVisible: () => poi.value.showWaterLoggingHiddenPoint.show },
|
||||||
|
{ key: LoadingResource.FLASH_FLOOD_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'flash_flood', isVisible: () => poi.value.showFlashFloodHiddenPoint.show },
|
||||||
|
{ key: LoadingResource.RISK_POINT, category: 'risk-point', isVisible: () => map.value.riskPointShow.show },
|
||||||
|
{ key: LoadingResource.BRIDGE, category: 'bridge', isVisible: () => infra.value.showBridge.show },
|
||||||
|
{ key: LoadingResource.RESERVOIR, category: 'reservoir', isVisible: () => infra.value.showReservoir.show },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有资源数据
|
||||||
|
*/
|
||||||
|
const allResources = ref<PointResource[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算属性:判断是否允许搜索
|
||||||
|
*/
|
||||||
|
const canSearch = computed(() => {
|
||||||
|
return RESOURCE_CONFIGS.some(config => config.isVisible());
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询建议回调
|
||||||
|
* @param queryString - 搜索字符串
|
||||||
|
* @param cb - 回调函数
|
||||||
|
*/
|
||||||
|
function querySearch(queryString: string, cb: (results: PointResource[]) => void) {
|
||||||
|
if (!canSearch.value) {
|
||||||
|
cb([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQuery = queryString.toLowerCase();
|
||||||
|
const filteredResults = allResources.value.filter(item => {
|
||||||
|
const config = RESOURCE_CONFIGS.find(c => c.category === item.category);
|
||||||
|
let isVisible = false;
|
||||||
|
if (config) {
|
||||||
|
if (item.category === 'hidden-danger') {
|
||||||
|
const type = (item.originalType as string)?.toLowerCase();
|
||||||
|
isVisible = (type === config.forcedType?.toLowerCase()) && config.isVisible();
|
||||||
|
} else {
|
||||||
|
isVisible = config.isVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVisible) return false;
|
||||||
|
if (!queryString) return true;
|
||||||
|
const matchStr = (item.value || '').toLowerCase();
|
||||||
|
return matchStr.includes(lowerQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
cb(filteredResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择建议回调
|
||||||
|
* @param item - 选中的点资源
|
||||||
|
*/
|
||||||
|
function handleSelect(item: PointResource) {
|
||||||
|
if (item.lon != null && item.lat != null) {
|
||||||
|
CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聚焦事件,重新加载数据
|
||||||
|
*/
|
||||||
|
function handleFocus() {
|
||||||
|
loadAllPointData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据处理:将 Store 数据转换为资源格式
|
||||||
|
* @param infoList - 原始数据列表
|
||||||
|
* @param category - 资源分类
|
||||||
|
* @param forcedType - 强制类型
|
||||||
|
* @returns 转换后的点资源数组
|
||||||
|
*/
|
||||||
|
function convertStoreDataToResources(
|
||||||
|
infoList: Record<string, unknown>[],
|
||||||
|
category: PointResource['category'],
|
||||||
|
forcedType?: string,
|
||||||
|
): PointResource[] {
|
||||||
|
if (!Array.isArray(infoList)) return [];
|
||||||
|
|
||||||
|
return infoList.map((item: Record<string, unknown>) => {
|
||||||
|
const id = item.id || item._id || item.uuid || 'unknown_id';
|
||||||
|
const safeId = typeof id === 'string' ? id : typeof id === 'number' ? id : 'unknown_id';
|
||||||
|
|
||||||
|
const value = (item.name && String(item.name).trim() !== '')
|
||||||
|
? String(item.name)
|
||||||
|
: String(safeId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: safeId,
|
||||||
|
value: value,
|
||||||
|
category: category,
|
||||||
|
originalType: (forcedType || (item.type as string) || (item.disasterType as string))?.toLowerCase(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载所有点类数据
|
||||||
|
*/
|
||||||
|
function loadAllPointData() {
|
||||||
|
const resources: PointResource[] = [];
|
||||||
|
|
||||||
|
RESOURCE_CONFIGS.forEach(config => {
|
||||||
|
const data = loadingResourceStore.getLoadingResource(config.key).info;
|
||||||
|
resources.push(...convertStoreDataToResources(data, config.category, config.forcedType));
|
||||||
|
});
|
||||||
|
|
||||||
|
const seenIds = new Map<string | number, PointResource>();
|
||||||
|
const uniqueResources: PointResource[] = [];
|
||||||
|
for (const item of resources) {
|
||||||
|
if (!seenIds.has(item.id)) {
|
||||||
|
seenIds.set(item.id, item);
|
||||||
|
uniqueResources.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allResources.value = uniqueResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索框的值
|
||||||
|
*/
|
||||||
|
const state = ref('');
|
||||||
|
|
||||||
|
return { state, allResources, canSearch, querySearch, handleSelect, handleFocus, loadAllPointData };
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Cartesian3, Color, HeightReference } from 'cesium';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
import type { CircleAnalysisOptions } from '@/types/cesium/EntityOptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 圆形区域绘制管理 Hook
|
||||||
|
* @returns 圆形绘制相关方法
|
||||||
|
*/
|
||||||
|
export const useCircleDrawer = () => {
|
||||||
|
/**
|
||||||
|
* 绘制圆形区域
|
||||||
|
* @param centerPosition - 中心点位置
|
||||||
|
* @param radiusKm - 半径(公里)
|
||||||
|
* @param options - 可选配置
|
||||||
|
*/
|
||||||
|
const drawCircle = (
|
||||||
|
centerPosition: Cartesian3,
|
||||||
|
radiusKm: number,
|
||||||
|
options?: Partial<CircleAnalysisOptions>
|
||||||
|
): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
const radiusMeters = radiusKm * 1000;
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
const fillColor = options?.fillColor || Color.RED;
|
||||||
|
const fillAlpha = options?.fillAlpha ?? 0.1;
|
||||||
|
const outlineColor = options?.outlineColor || Color.RED;
|
||||||
|
const outlineAlpha = options?.outlineAlpha ?? 0.9;
|
||||||
|
const outlineWidth = options?.outlineWidth ?? 3;
|
||||||
|
const height = options?.height ?? 0;
|
||||||
|
const heightReference = options?.heightReference ?? HeightReference.CLAMP_TO_GROUND;
|
||||||
|
|
||||||
|
const circleConfig = {
|
||||||
|
position: centerPosition,
|
||||||
|
ellipse: {
|
||||||
|
semiMajorAxis: radiusMeters,
|
||||||
|
semiMinorAxis: radiusMeters,
|
||||||
|
height,
|
||||||
|
heightReference,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const circleFillEntity = viewer.entities.add({
|
||||||
|
...circleConfig,
|
||||||
|
ellipse: {
|
||||||
|
...circleConfig.ellipse,
|
||||||
|
material: fillColor.withAlpha(fillAlpha),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const circleOutlineEntity = viewer.entities.add({
|
||||||
|
...circleConfig,
|
||||||
|
ellipse: {
|
||||||
|
...circleConfig.ellipse,
|
||||||
|
material: Color.TRANSPARENT,
|
||||||
|
outline: true,
|
||||||
|
outlineColor: outlineColor.withAlpha(outlineAlpha),
|
||||||
|
outlineWidth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
circleFillEntity ._isAnalysisCircle = true;
|
||||||
|
circleOutlineEntity._isAnalysisCircle = true;
|
||||||
|
|
||||||
|
console.log(`已添加圆形图层, 半径: ${radiusKm}公里`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有圆形区域
|
||||||
|
*/
|
||||||
|
const clearCircle = (): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
viewer.entities.values
|
||||||
|
.filter(entity => entity._isAnalysisCircle)
|
||||||
|
.forEach(entity => viewer.entities.remove(entity));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawCircle,
|
||||||
|
clearCircle,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Cartesian3, Entity, VerticalOrigin, HorizontalOrigin, HeightReference } from 'cesium';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
|
||||||
|
// 十字准心标记 SVG
|
||||||
|
const MARKER_SVG = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
|
||||||
|
<circle cx="40" cy="40" r="6" fill="#FF0000" stroke="#FFFFFF" stroke-width="2"/>
|
||||||
|
<path d="M 15 20 L 15 15 L 20 15" stroke="#00FF00" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 65 20 L 65 15 L 60 15" stroke="#00FF00" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 15 60 L 15 65 L 20 65" stroke="#00FF00" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 65 60 L 65 65 L 60 65" stroke="#00FF00" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记管理 Hook
|
||||||
|
* @returns 标记相关方法
|
||||||
|
*/
|
||||||
|
export const useMarkerManager = () => {
|
||||||
|
let currentMarkerEntity: Entity | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加标记
|
||||||
|
* @param position - 标记位置
|
||||||
|
*/
|
||||||
|
const addMarker = (position: Cartesian3): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
// 移除旧标记
|
||||||
|
if (currentMarkerEntity) {
|
||||||
|
viewer.entities.remove(currentMarkerEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const markerDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(MARKER_SVG)}`;
|
||||||
|
currentMarkerEntity = viewer.entities.add({
|
||||||
|
position,
|
||||||
|
billboard: {
|
||||||
|
image: markerDataUrl,
|
||||||
|
scale: 1.0,
|
||||||
|
verticalOrigin: VerticalOrigin.CENTER,
|
||||||
|
horizontalOrigin: HorizontalOrigin.CENTER,
|
||||||
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
|
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除标记
|
||||||
|
*/
|
||||||
|
const removeMarker = (): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer || !currentMarkerEntity) return;
|
||||||
|
|
||||||
|
viewer.entities.remove(currentMarkerEntity);
|
||||||
|
currentMarkerEntity = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前标记实体
|
||||||
|
*/
|
||||||
|
const getCurrentMarker = (): Entity | null => {
|
||||||
|
return currentMarkerEntity;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addMarker,
|
||||||
|
removeMarker,
|
||||||
|
getCurrentMarker,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
Cartesian3,
|
||||||
|
Color,
|
||||||
|
Entity,
|
||||||
|
VerticalOrigin,
|
||||||
|
HorizontalOrigin,
|
||||||
|
HeightReference,
|
||||||
|
CallbackProperty,
|
||||||
|
JulianDate
|
||||||
|
} from 'cesium';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
import type { PointResource } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脉冲效果管理 Hook
|
||||||
|
* @returns 脉冲效果相关方法
|
||||||
|
*/
|
||||||
|
export const usePulseEffect = () => {
|
||||||
|
let pulseEntities: Entity[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建红色圆形纹理
|
||||||
|
* @param radius - 半径
|
||||||
|
* @param lineWidth - 线宽
|
||||||
|
* @param opacity - 透明度
|
||||||
|
* @returns Base64 图片数据
|
||||||
|
*/
|
||||||
|
const createRedCircleTexture = (radius = 15, lineWidth = 2, opacity = 0.8): string => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const size = radius * 2 + 10;
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) return '';
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(size / 2, size / 2, radius, 0, 2 * Math.PI);
|
||||||
|
ctx.strokeStyle = `rgba(255, 0, 0, ${opacity})`;
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为单个点添加脉冲效果
|
||||||
|
* @param point - 点资源
|
||||||
|
* @param startTime - 开始时间(秒)
|
||||||
|
*/
|
||||||
|
const addPulseEffectToPoint = (point: PointResource, startTime: number): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer || point.lon === undefined || point.lat === undefined) return;
|
||||||
|
|
||||||
|
const lon = Number(point.lon);
|
||||||
|
const lat = Number(point.lat);
|
||||||
|
if (isNaN(lon) || isNaN(lat)) return;
|
||||||
|
|
||||||
|
const position = Cartesian3.fromDegrees(lon, lat, 0);
|
||||||
|
const baseTexture = createRedCircleTexture(10, 3, 1.0);
|
||||||
|
|
||||||
|
const dynamicScale = new CallbackProperty((time: JulianDate) => {
|
||||||
|
const elapsed = ((time.secondsOfDay - startTime) % 1 + 1) % 1;
|
||||||
|
return 1.1 + 0.2 * Math.sin(elapsed * Math.PI * 2);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
const pulseEntity = viewer.entities.add({
|
||||||
|
position,
|
||||||
|
billboard: {
|
||||||
|
image: baseTexture,
|
||||||
|
scale: dynamicScale,
|
||||||
|
color: new CallbackProperty((time: JulianDate) => {
|
||||||
|
const elapsed = ((time.secondsOfDay - startTime) % 1 + 1) % 1;
|
||||||
|
const alpha = 0.6 + 0.4 * Math.sin(elapsed * Math.PI * 2);
|
||||||
|
return Color.fromBytes(255, 0, 0, Math.floor(alpha * 255));
|
||||||
|
}, false),
|
||||||
|
verticalOrigin: VerticalOrigin.CENTER,
|
||||||
|
horizontalOrigin: HorizontalOrigin.CENTER,
|
||||||
|
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||||
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pulseEntities.push(pulseEntity);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为多个点添加脉冲效果
|
||||||
|
* @param points - 点资源数组
|
||||||
|
*/
|
||||||
|
const addPulseEffectToPoints = (points: PointResource[]): void => {
|
||||||
|
const startTime = JulianDate.now().secondsOfDay;
|
||||||
|
points.forEach(point => addPulseEffectToPoint(point, startTime));
|
||||||
|
console.log(`已为 ${points.length} 个点添加脉冲效果`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除所有脉冲效果
|
||||||
|
*/
|
||||||
|
const removePulseEffect = (): void => {
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
pulseEntities.forEach(entity => viewer.entities.remove(entity));
|
||||||
|
pulseEntities = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前脉冲实体数量
|
||||||
|
*/
|
||||||
|
const getPulseCount = (): number => {
|
||||||
|
return pulseEntities.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addPulseEffectToPoint,
|
||||||
|
addPulseEffectToPoints,
|
||||||
|
removePulseEffect,
|
||||||
|
getPulseCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -195,6 +195,11 @@ export const useStatusStore = defineStore('status', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标样式状态
|
||||||
|
*/
|
||||||
|
const cursorStyle = ref<string>('default');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 恢复默认值
|
* 恢复默认值
|
||||||
*/
|
*/
|
||||||
@@ -338,6 +343,7 @@ export const useStatusStore = defineStore('status', () => {
|
|||||||
infrastructureLayers,
|
infrastructureLayers,
|
||||||
weatherLayers,
|
weatherLayers,
|
||||||
functionStatus,
|
functionStatus,
|
||||||
|
cursorStyle,
|
||||||
reset,
|
reset,
|
||||||
resetScene,
|
resetScene,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,3 +75,70 @@ export interface EntityOptions {
|
|||||||
/** 自定义属性(用于存储额外信息) */
|
/** 自定义属性(用于存储额外信息) */
|
||||||
attributes?: Record<string, unknown>;
|
attributes?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 椭圆配置选项
|
||||||
|
* 用于绘制椭圆形区域(圆形是椭圆的特例:semiMajorAxis = semiMinorAxis)
|
||||||
|
*/
|
||||||
|
export interface EllipseOptions {
|
||||||
|
/** 椭圆中心位置 */
|
||||||
|
position: Cartesian3;
|
||||||
|
/** 半长轴(米) */
|
||||||
|
semiMajorAxis: number;
|
||||||
|
/** 半短轴(米) */
|
||||||
|
semiMinorAxis: number;
|
||||||
|
/** 旋转角度(弧度),默认0 */
|
||||||
|
rotation?: number;
|
||||||
|
/** 高度,默认0 */
|
||||||
|
height?: number;
|
||||||
|
/** 拉伸高度,默认0 */
|
||||||
|
extrudedHeight?: number;
|
||||||
|
/** 高度参考,默认CLAMP_TO_GROUND */
|
||||||
|
heightReference?: HeightReference;
|
||||||
|
/** 填充材质,默认白色 */
|
||||||
|
material?: MaterialProperty | Color;
|
||||||
|
/** 是否显示轮廓,默认false */
|
||||||
|
outline?: boolean;
|
||||||
|
/** 轮廓颜色,默认黑色 */
|
||||||
|
outlineColor?: Color;
|
||||||
|
/** 轮廓宽度,默认1 */
|
||||||
|
outlineWidth?: number;
|
||||||
|
/** 分段数(控制平滑度),默认128 */
|
||||||
|
granularity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 圆形区域配置选项(基于椭圆配置)
|
||||||
|
* 用于周边分析功能的圆形绘制
|
||||||
|
*/
|
||||||
|
export interface CircleAnalysisOptions {
|
||||||
|
/** 圆心位置 */
|
||||||
|
position: Cartesian3;
|
||||||
|
/** 半径(公里) */
|
||||||
|
radiusKm: number;
|
||||||
|
/** 填充颜色,默认半透明红色 */
|
||||||
|
fillColor?: Color;
|
||||||
|
/** 填充透明度,默认0.1 */
|
||||||
|
fillAlpha?: number;
|
||||||
|
/** 轮廓颜色,默认红色 */
|
||||||
|
outlineColor?: Color;
|
||||||
|
/** 轮廓透明度,默认0.9 */
|
||||||
|
outlineAlpha?: number;
|
||||||
|
/** 轮廓宽度,默认3 */
|
||||||
|
outlineWidth?: number;
|
||||||
|
/** 高度,默认0 */
|
||||||
|
height?: number;
|
||||||
|
/** 高度参考,默认CLAMP_TO_GROUND */
|
||||||
|
heightReference?: HeightReference;
|
||||||
|
}
|
||||||
|
// ==================== Cesium Entity 类型扩展 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cesium Entity 类型扩展
|
||||||
|
* 为周边分析功能添加自定义属性
|
||||||
|
*/
|
||||||
|
declare module 'cesium' {
|
||||||
|
interface Entity {
|
||||||
|
/** 是否为分析圆形标记 */
|
||||||
|
_isAnalysisCircle?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import type { LoadingResource } from './LoadingResourceType';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
/**
|
||||||
|
* 周边分析组件相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点资源分类类型
|
||||||
|
*/
|
||||||
|
export type PointResourceCategory =
|
||||||
|
| 'school'
|
||||||
|
| 'hospital'
|
||||||
|
| 'danger'
|
||||||
|
| 'shelter'
|
||||||
|
| 'fire'
|
||||||
|
| 'store'
|
||||||
|
| 'hidden-danger'
|
||||||
|
| 'risk-point'
|
||||||
|
| 'bridge'
|
||||||
|
| 'reservoir'
|
||||||
|
| 'subway';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点资源数据结构
|
||||||
|
*/
|
||||||
|
export interface PointResource {
|
||||||
|
/** 点ID */
|
||||||
|
id: string | number;
|
||||||
|
/** 显示值(名称) */
|
||||||
|
value: string;
|
||||||
|
/** 经度 */
|
||||||
|
lon?: number;
|
||||||
|
/** 纬度 */
|
||||||
|
lat?: number;
|
||||||
|
/** 资源分类 */
|
||||||
|
category?: PointResourceCategory;
|
||||||
|
/** 原始类型(用于隐患点子类型区分) */
|
||||||
|
originalType?: string;
|
||||||
|
/** 其他任意属性 */
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源配置接口(用于 SearchComponent)
|
||||||
|
*/
|
||||||
|
export interface ResourceConfig {
|
||||||
|
/** 加载资源键 */
|
||||||
|
key: LoadingResource;
|
||||||
|
/** 资源分类 */
|
||||||
|
category: PointResourceCategory;
|
||||||
|
/** 强制类型(用于隐患点) */
|
||||||
|
forcedType?: string;
|
||||||
|
/** 是否可见的判断函数 */
|
||||||
|
isVisible: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源配置接口(用于 ButtonComponent,不含 isVisible)
|
||||||
|
*/
|
||||||
|
export interface ButtonResourceConfig {
|
||||||
|
/** 加载资源键 */
|
||||||
|
key: LoadingResource;
|
||||||
|
/** 资源分类 */
|
||||||
|
category: PointResourceCategory;
|
||||||
|
/** 强制类型(用于隐患点) */
|
||||||
|
forcedType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析按钮配置接口
|
||||||
|
*/
|
||||||
|
export interface AnalysisButtonConfig {
|
||||||
|
/** 按钮默认名称 */
|
||||||
|
name: string;
|
||||||
|
/** 按钮激活时的名称(可选) */
|
||||||
|
activeName?: string;
|
||||||
|
/** 按钮点击回调函数 */
|
||||||
|
callback: (status: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话框位置接口
|
||||||
|
*/
|
||||||
|
export interface DialogPosition {
|
||||||
|
/** X 坐标 */
|
||||||
|
x: number;
|
||||||
|
/** Y 坐标 */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周边分析按钮状态接口(用于 provide/inject 共享)
|
||||||
|
* 注意:响应式属性使用 Ref 类型
|
||||||
|
*/
|
||||||
|
export interface AnalysisButtonState {
|
||||||
|
/** 当前选中的按钮索引 */
|
||||||
|
selectedButtonIndex: Ref<number>;
|
||||||
|
/** 是否显示区域选择弹窗 */
|
||||||
|
showAreaDialog: Ref<boolean>;
|
||||||
|
/** 区域半径(公里) */
|
||||||
|
radius: Ref<number>;
|
||||||
|
/** 弹窗位置 */
|
||||||
|
dialogPosition: DialogPosition;
|
||||||
|
/** 分析按钮配置列表 */
|
||||||
|
analysisButtons: AnalysisButtonConfig[];
|
||||||
|
/** 按钮点击处理函数 */
|
||||||
|
handleButtonClick: (index: number, callback: (status: boolean) => void) => void;
|
||||||
|
/** 确认添加区域分析 */
|
||||||
|
handleConfirm: () => void;
|
||||||
|
/** 取消区域分析 */
|
||||||
|
handleCancel: () => void;
|
||||||
|
/** 刷新脉冲效果 */
|
||||||
|
refreshPulseEffect: () => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user