存档代码
This commit is contained in:
@@ -1,59 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="around-analysis-box"
|
class="around-analysis-box"
|
||||||
v-show="statusStore.functionStatus.aroundAnalysis.show"
|
v-show="statusStore.functionStatus.aroundAnalysis.show"
|
||||||
>
|
>
|
||||||
<!-- 搜索组件 -->
|
<!-- 搜索组件 -->
|
||||||
<SearchComponent />
|
<SearchComponent />
|
||||||
|
|
||||||
<!-- 按钮组件 -->
|
<!-- 按钮组件 -->
|
||||||
<ButtonComponent />
|
<ButtonComponent />
|
||||||
|
|
||||||
<!-- 具体功能组件 -->
|
|
||||||
<AroundAnalysisDetailComponent />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 区域选择对话框(fixed定位,相对于视口) -->
|
<!-- 区域选择对话框(fixed定位,相对于视口) -->
|
||||||
<div v-if="analysisButtonState.showAreaDialog.value" class="search-area-dialog" :style="{ left: analysisButtonState.dialogPosition.x + 'px', top: analysisButtonState.dialogPosition.y + 'px' }">
|
<div v-if="aroundAnalysisState.showAreaDialog.value" class="search-area-dialog" :style="{ left: aroundAnalysisState.dialogPosition.x + 'px', top: aroundAnalysisState.dialogPosition.y + 'px' }">
|
||||||
<div class="dialog-header">选择区域</div>
|
<div class="dialog-header">选择区域</div>
|
||||||
<div class="dialog-content">
|
<div class="dialog-content">
|
||||||
<div class="radius-input-group">
|
<div class="radius-input-group">
|
||||||
<span class="label">半径:</span>
|
<span class="label">半径:</span>
|
||||||
<input
|
<input
|
||||||
v-model.number="analysisButtonState.radius.value"
|
v-model.number="aroundAnalysisState.radius.value"
|
||||||
type="number"
|
type="number"
|
||||||
class="radius-input"
|
class="radius-input"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
/>
|
/>
|
||||||
<span class="unit">公里</span>
|
<span class="unit">公里</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button class="confirm-btn" @click="analysisButtonState.handleConfirm">确认添加</button>
|
<button class="confirm-btn" @click="aroundAnalysisState.handleConfirm">确认添加</button>
|
||||||
<button class="cancel-btn" @click="analysisButtonState.handleCancel">取消</button>
|
<button class="cancel-btn" @click="aroundAnalysisState.handleCancel">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide } from 'vue';//从Vue导入provide函数,用于依赖注入
|
import { provide } from 'vue';
|
||||||
import { useStatusStore } from '@/stores/useStatusStore';
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
import { useAroundButton } from '@/hooks/rain-earthquake/useAroundButton.ts';
|
import { useAroundAnalysis } from '@/hooks/rain-earthquake/useAroundAnalysis';
|
||||||
import { useAroundSearch } from '@/hooks/rain-earthquake/useAroundSearch.ts';
|
import type { AroundAnalysisState } from '@/types/common/useAroundAnalysisType';
|
||||||
import ButtonComponent from './around-analysis/ButtonComponent.vue';
|
import ButtonComponent from './around-analysis/ButtonComponent.vue';
|
||||||
import SearchComponent from './around-analysis/SearchComponent.vue';
|
import SearchComponent from './around-analysis/SearchComponent.vue';
|
||||||
|
|
||||||
|
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
// 在父组件中创建唯一的 Hook 实例
|
// 在父组件中创建唯一的 Hook 实例
|
||||||
const analysisButtonState = useAroundButton();
|
const aroundAnalysisState = useAroundAnalysis();
|
||||||
const searchState = useAroundSearch();
|
|
||||||
|
|
||||||
// 通过 provide 共享给所有子组件
|
// 通过 provide 共享给所有子组件
|
||||||
provide('analysisButtonState', analysisButtonState);
|
provide<AroundAnalysisState>('aroundAnalysisState', aroundAnalysisState);
|
||||||
provide('searchState', searchState);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -61,7 +55,7 @@ provide('searchState', searchState);
|
|||||||
.search-area-dialog {
|
.search-area-dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100001;
|
z-index: 100001;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: linear-gradient(180deg, rgba(0, 60, 120, 0.95), rgba(0, 40, 80, 0.95));
|
background: linear-gradient(180deg, rgba(0, 60, 120, 0.95), rgba(0, 40, 80, 0.95));
|
||||||
border: 2px solid #00b4ff;
|
border: 2px solid #00b4ff;
|
||||||
@@ -72,8 +66,8 @@ provide('searchState', searchState);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-area-dialog .dialog-header {
|
.search-area-dialog .dialog-header {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -81,7 +75,7 @@ provide('searchState', searchState);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-area-dialog .dialog-content {
|
.search-area-dialog .dialog-content {
|
||||||
padding: 10px 6px;
|
padding: 10px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -95,7 +89,7 @@ provide('searchState', searchState);
|
|||||||
|
|
||||||
.search-area-dialog .radius-input-group .label,
|
.search-area-dialog .radius-input-group .label,
|
||||||
.search-area-dialog .radius-input-group .unit {
|
.search-area-dialog .radius-input-group .unit {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +117,14 @@ provide('searchState', searchState);
|
|||||||
.search-area-dialog .dialog-footer {
|
.search-area-dialog .dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 10px 8px;
|
padding: 10px 10px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-area-dialog .confirm-btn,
|
.search-area-dialog .confirm-btn,
|
||||||
.search-area-dialog .cancel-btn {
|
.search-area-dialog .cancel-btn {
|
||||||
padding: 6px 15px;
|
padding: 6px 15px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
<!-- 按钮组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="analysis-button-box">
|
<div class="analysis-button-box">
|
||||||
<ul class="analysis-button-ul">
|
<ul class="analysis-button-ul">
|
||||||
<li v-for="(buttonItem, index) in analysisButtons" :key="index">
|
<li v-for="(buttonItem, index) in aroundAnalysisState.analysisButtons" :key="index">
|
||||||
<button
|
<button
|
||||||
@click="handleButtonClick(index, buttonItem.callback)"
|
@click="aroundAnalysisState.handleButtonClick(index, buttonItem.callback)"
|
||||||
:style="{
|
:style="{
|
||||||
'background-image': `url(${selectedButtonIndex === index ? rightOrangeButton : rightBlueButton})`,
|
'background-image': `url(${aroundAnalysisState.selectedButtonIndex.value === index ? rightOrangeButton : rightBlueButton})`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ selectedButtonIndex === index && buttonItem.activeName ? buttonItem.activeName : buttonItem.name }}
|
{{ aroundAnalysisState.selectedButtonIndex.value === index && buttonItem.activeName ? buttonItem.activeName : buttonItem.name }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -18,16 +19,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
import { rightBlueButton, rightOrangeButton } from '@/assets';
|
import { rightBlueButton, rightOrangeButton } from '@/assets';
|
||||||
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
import type { AroundAnalysisState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
// 从父组件注入共享状态,明确指定类型
|
// 从父组件注入共享的 Hook 实例
|
||||||
const analysisButtonState = inject<AnalysisButtonState>('analysisButtonState');
|
const aroundAnalysisState = inject<AroundAnalysisState>('aroundAnalysisState')!;
|
||||||
|
|
||||||
const {
|
|
||||||
selectedButtonIndex,
|
|
||||||
analysisButtons,
|
|
||||||
handleButtonClick
|
|
||||||
} = analysisButtonState!;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -73,4 +68,4 @@ const {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
|
<!-- 搜索组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="search-component-box">
|
<div class="search-component-box">
|
||||||
<el-autocomplete
|
<el-autocomplete
|
||||||
v-model="state"
|
v-model="aroundAnalysisState.searchState.value"
|
||||||
:fetch-suggestions="querySearch"
|
:fetch-suggestions="aroundAnalysisState.querySearch"
|
||||||
popper-class="my-autocomplete"
|
popper-class="my-autocomplete"
|
||||||
placeholder="搜索地点"
|
placeholder="搜索地点"
|
||||||
@select="handleSelect"
|
@select="aroundAnalysisState.handleSelect"
|
||||||
@focus="handleFocus"
|
@focus="aroundAnalysisState.handleFocus"
|
||||||
clearable
|
clearable
|
||||||
:disabled="!canSearch"
|
:disabled="!aroundAnalysisState.canSearch.value"
|
||||||
:teleported="false"
|
:teleported="false"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@@ -25,36 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, onMounted } from 'vue';
|
import { inject } from 'vue';
|
||||||
import { Edit } from '@element-plus/icons-vue';
|
import { Edit } from '@element-plus/icons-vue';
|
||||||
import type { PointResource } from '@/types/common/useAroundAnalysisType';
|
import type { AroundAnalysisState } from '@/types/common/useAroundAnalysisType';
|
||||||
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
|
||||||
|
|
||||||
// 从父组件注入搜索状态和按钮状态
|
// 从父组件注入共享的 Hook 实例
|
||||||
const searchState = inject<ReturnType<typeof import('@/hooks/rain-earthquake/useAroundSearch').useAroundSearch>>('searchState');
|
const aroundAnalysisState = inject<AroundAnalysisState>('aroundAnalysisState')!;
|
||||||
const buttonState = inject<AnalysisButtonState>('analysisButtonState');
|
|
||||||
|
|
||||||
const {
|
|
||||||
state,
|
|
||||||
canSearch,
|
|
||||||
querySearch,
|
|
||||||
handleSelect: baseHandleSelect,
|
|
||||||
handleFocus,
|
|
||||||
} = searchState!;
|
|
||||||
|
|
||||||
// 包装 handleSelect,在搜索选择后触发区域分析
|
|
||||||
const handleSelect = async (item: PointResource) => {
|
|
||||||
await baseHandleSelect(item);
|
|
||||||
// 飞行完成后,触发区域分析
|
|
||||||
buttonState?.startAreaAnalysisFromSearch(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 加载数据
|
|
||||||
if (searchState) {
|
|
||||||
handleFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { LoadingResource } from '@/types/common/LoadingResourceType';
|
||||||
|
import type { ResourceConfig } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周边分析资源配置(统一管理)
|
||||||
|
*/
|
||||||
|
export const RESOURCE_CONFIGS: ResourceConfig[] = [
|
||||||
|
{ 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.COLLAPSE_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'collapse' },
|
||||||
|
{ key: LoadingResource.RISK_POINT, category: 'risk-point' },
|
||||||
|
{ key: LoadingResource.BRIDGE, category: 'bridge' },
|
||||||
|
{ key: LoadingResource.RESERVOIR, category: 'reservoir' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常量配置
|
||||||
|
*/
|
||||||
|
export const AROUND_ANALYSIS_CONSTANTS = {
|
||||||
|
DIALOG_WIDTH: 280,
|
||||||
|
DIALOG_HEIGHT: 150,
|
||||||
|
DIALOG_PADDING: 10,
|
||||||
|
DIALOG_OFFSET: 20,
|
||||||
|
EARTH_RADIUS: 6371000,
|
||||||
|
MIN_FLY_HEIGHT: 10000,
|
||||||
|
FLY_HEIGHT_MULTIPLIER: 6000,
|
||||||
|
FLY_DURATION: 2,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
import { ref, reactive, onUnmounted, watch, computed } from 'vue';
|
||||||
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
||||||
|
import { RESOURCE_CONFIGS, AROUND_ANALYSIS_CONSTANTS } from '@/config/aroundAnalysisConfig';
|
||||||
|
import type { PointResource, PointResourceCategory, AnalysisButtonConfig, AroundAnalysisState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
|
import { isCategoryVisible, loadAllPointData, calculateDistance } from '@/utils/aroundAnalysisUtils';
|
||||||
|
import {
|
||||||
|
ScreenSpaceEventHandler,
|
||||||
|
ScreenSpaceEventType,
|
||||||
|
Cartesian2,
|
||||||
|
Cartographic,
|
||||||
|
Cartesian3,
|
||||||
|
} from 'cesium';
|
||||||
|
import { useCircleDrawer } from './useCircleDrawer';
|
||||||
|
import { usePulseEffect } from './usePulseEffect';
|
||||||
|
import { useMarkerManager } from './useMarkerManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周边分析统一 Hook(合并按钮和搜索逻辑)
|
||||||
|
*/
|
||||||
|
export const useAroundAnalysis = (): AroundAnalysisState => {
|
||||||
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
|
// ==================== 响应式状态 ====================
|
||||||
|
const selectedButtonIndex = ref<number>(-1);
|
||||||
|
const showAreaDialog = ref(false);
|
||||||
|
const radius = ref(10);
|
||||||
|
const dialogPosition = reactive({ x: 0, y: 0 });
|
||||||
|
const pulsePoints = ref<PointResource[]>([]);
|
||||||
|
const showPulsePointList = ref(false);
|
||||||
|
const searchState = ref('');
|
||||||
|
const canSearch = computed(() => {
|
||||||
|
return RESOURCE_CONFIGS.some(config => isCategoryVisible(config.category, config.forcedType));
|
||||||
|
});
|
||||||
|
|
||||||
|
let clickHandler: ScreenSpaceEventHandler | null = null;
|
||||||
|
let currentCenterPosition: Cartesian3 | null = null;
|
||||||
|
|
||||||
|
// ==================== 组合子 Hook ====================
|
||||||
|
const { drawCircle, clearCircle } = useCircleDrawer();
|
||||||
|
const { addPulseEffectToPoints, removePulseEffect } = usePulseEffect();
|
||||||
|
const { addMarker, removeMarker } = useMarkerManager();
|
||||||
|
|
||||||
|
// ==================== 核心功能 ====================
|
||||||
|
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(RESOURCE_CONFIGS);
|
||||||
|
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;
|
||||||
|
removePulseEffect();
|
||||||
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
||||||
|
addPulseEffectToPoints(pointsInCircle);
|
||||||
|
pulsePoints.value = pointsInCircle;
|
||||||
|
showPulsePointList.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllAnalysisResources = () => {
|
||||||
|
removeMarker();
|
||||||
|
clearCircle();
|
||||||
|
removePulseEffect();
|
||||||
|
currentCenterPosition = null;
|
||||||
|
pulsePoints.value = [];
|
||||||
|
showPulsePointList.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearVisualEffectsOnly = () => {
|
||||||
|
clearCircle();
|
||||||
|
removePulseEffect();
|
||||||
|
pulsePoints.value = [];
|
||||||
|
showPulsePointList.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 地图事件 ====================
|
||||||
|
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);
|
||||||
|
console.log('点击位置:', {
|
||||||
|
longitude: cartographic.longitude * (180 / Math.PI),
|
||||||
|
latitude: cartographic.latitude * (180 / Math.PI)
|
||||||
|
});
|
||||||
|
addMarker(cartesian);
|
||||||
|
showAreaDialog.value = true;
|
||||||
|
calculateDialogPosition(clickEvent.position);
|
||||||
|
}
|
||||||
|
}, ScreenSpaceEventType.LEFT_CLICK);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMapClickHandler = () => {
|
||||||
|
if (clickHandler) {
|
||||||
|
clickHandler.destroy();
|
||||||
|
clickHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDialogPosition = (clickPosition: Cartesian2) => {
|
||||||
|
const { innerWidth: screenWidth, innerHeight: screenHeight } = window;
|
||||||
|
const { DIALOG_WIDTH, DIALOG_HEIGHT, DIALOG_PADDING, DIALOG_OFFSET } = AROUND_ANALYSIS_CONSTANTS;
|
||||||
|
|
||||||
|
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 handleConfirm = () => {
|
||||||
|
if (!currentCenterPosition) {
|
||||||
|
console.error('中心点位置不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearVisualEffectsOnly();
|
||||||
|
drawCircle(currentCenterPosition, radius.value);
|
||||||
|
|
||||||
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
||||||
|
addPulseEffectToPoints(pointsInCircle);
|
||||||
|
pulsePoints.value = pointsInCircle;
|
||||||
|
showPulsePointList.value = true;
|
||||||
|
|
||||||
|
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 * AROUND_ANALYSIS_CONSTANTS.FLY_HEIGHT_MULTIPLIER, AROUND_ANALYSIS_CONSTANTS.MIN_FLY_HEIGHT);
|
||||||
|
CesiumUtilsSingleton.flyToTarget([longitude, latitude, flyHeight], AROUND_ANALYSIS_CONSTANTS.FLY_DURATION);
|
||||||
|
|
||||||
|
showAreaDialog.value = false;
|
||||||
|
removeMapClickHandler();
|
||||||
|
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (viewer?.canvas) {
|
||||||
|
statusStore.cursorStyle = 'default';
|
||||||
|
viewer.canvas.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAreaAnalysisFromSearch = (point: PointResource) => {
|
||||||
|
if (point.lon == null || point.lat == null) return;
|
||||||
|
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
currentCenterPosition = Cartesian3.fromDegrees(point.lon, point.lat, 0);
|
||||||
|
selectedButtonIndex.value = 0;
|
||||||
|
|
||||||
|
const viewer = CesiumUtilsSingleton.getViewer();
|
||||||
|
if (viewer?.canvas) {
|
||||||
|
statusStore.cursorStyle = 'crosshair';
|
||||||
|
viewer.canvas.style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarker(currentCenterPosition);
|
||||||
|
showAreaDialog.value = true;
|
||||||
|
|
||||||
|
const centerX = window.innerWidth / 2;
|
||||||
|
const centerY = window.innerHeight / 2;
|
||||||
|
const { DIALOG_WIDTH, DIALOG_HEIGHT, DIALOG_PADDING, DIALOG_OFFSET } = AROUND_ANALYSIS_CONSTANTS;
|
||||||
|
|
||||||
|
dialogPosition.x = Math.max(DIALOG_PADDING, Math.min(centerX + DIALOG_OFFSET, window.innerWidth - DIALOG_WIDTH - DIALOG_PADDING));
|
||||||
|
dialogPosition.y = Math.max(DIALOG_PADDING, Math.min(centerY + DIALOG_OFFSET, window.innerHeight - DIALOG_HEIGHT - DIALOG_PADDING));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 搜索功能 ====================
|
||||||
|
const querySearch = (queryString: string, cb: (results: PointResource[]) => void) => {
|
||||||
|
if (!canSearch.value) {
|
||||||
|
cb([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQuery = queryString.toLowerCase();
|
||||||
|
const allResources = loadAllPointData(RESOURCE_CONFIGS);
|
||||||
|
|
||||||
|
const filteredResults = allResources.filter(item => {
|
||||||
|
const config = RESOURCE_CONFIGS.find(c => c.category === item.category);
|
||||||
|
let isVisible = false;
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
isVisible = isCategoryVisible(config.category, config.forcedType || item.originalType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVisible) return false;
|
||||||
|
if (!queryString) return true;
|
||||||
|
|
||||||
|
return (item.value || '').toLowerCase().includes(lowerQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
cb(filteredResults);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = async (item: PointResource) => {
|
||||||
|
if (item.lon == null || item.lat == null) return;
|
||||||
|
await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
||||||
|
startAreaAnalysisFromSearch(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
// 触发数据刷新(如果需要)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 监听器 ====================
|
||||||
|
watch(
|
||||||
|
() => useLoadingResourceStore().loadingResource,
|
||||||
|
() => {
|
||||||
|
if (currentCenterPosition && showPulsePointList.value) {
|
||||||
|
refreshPulseEffect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const poi = computed(() => statusStore.poiLayers);
|
||||||
|
const map = computed(() => statusStore.mapLayers);
|
||||||
|
const infra = computed(() => statusStore.infrastructureLayers);
|
||||||
|
|
||||||
|
watch([
|
||||||
|
() => 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,
|
||||||
|
], () => {
|
||||||
|
if (currentCenterPosition && showPulsePointList.value) {
|
||||||
|
refreshPulseEffect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearAllAnalysisResources();
|
||||||
|
removeMapClickHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 按钮配置 ====================
|
||||||
|
const analysisButtons: AnalysisButtonConfig[] = [
|
||||||
|
{
|
||||||
|
name: '标记区域分析',
|
||||||
|
activeName: '取消区域分析',
|
||||||
|
callback: (status: boolean) => {
|
||||||
|
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) => {
|
||||||
|
statusStore.mapLayers.showAdministrativeDivision.show = !status;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedButtonIndex,
|
||||||
|
showAreaDialog,
|
||||||
|
radius,
|
||||||
|
dialogPosition,
|
||||||
|
analysisButtons,
|
||||||
|
searchState,
|
||||||
|
canSearch,
|
||||||
|
pulsePoints,
|
||||||
|
showPulsePointList,
|
||||||
|
handleButtonClick,
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel,
|
||||||
|
refreshPulseEffect,
|
||||||
|
startAreaAnalysisFromSearch,
|
||||||
|
querySearch,
|
||||||
|
handleSelect,
|
||||||
|
handleFocus,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
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.COLLAPSE_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'collapse' },
|
|
||||||
{ 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,
|
|
||||||
collapse: () => poi.value.showCollapseHiddenPoint.show,
|
|
||||||
};
|
|
||||||
return hiddenMap[originalType || '']?.() ?? false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return visibilityMap[category]?.() ?? false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 响应式状态 ====================
|
|
||||||
export const useAroundButton = (): AnalysisButtonState => {
|
|
||||||
const selectedButtonIndex = ref<number>(-1);
|
|
||||||
const showAreaDialog = ref(false);
|
|
||||||
const radius = ref(10);
|
|
||||||
const dialogPosition = reactive<DialogPosition>({ x: 0, y: 0 });
|
|
||||||
const pulsePoints = ref<PointResource[]>([]);
|
|
||||||
const showPulsePointList = ref(false);
|
|
||||||
|
|
||||||
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);
|
|
||||||
pulsePoints.value = pointsInCircle;
|
|
||||||
showPulsePointList.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 地图事件处理 ====================
|
|
||||||
|
|
||||||
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;
|
|
||||||
pulsePoints.value = [];
|
|
||||||
showPulsePointList.value = false;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 仅清除视觉效果,保留标记点和中心点位置
|
|
||||||
*/
|
|
||||||
const clearVisualEffectsOnly = () => {
|
|
||||||
clearCircle();
|
|
||||||
removePulseEffect();
|
|
||||||
pulsePoints.value = [];
|
|
||||||
showPulsePointList.value = false;
|
|
||||||
};
|
|
||||||
// ==================== 事件处理 ====================
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
if (!currentCenterPosition) {
|
|
||||||
console.error('中心点位置不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('确认添加区域分析', {
|
|
||||||
radius: radius.value,
|
|
||||||
center: currentCenterPosition
|
|
||||||
});
|
|
||||||
// 先清除上一次的视觉效果(保留标记点)
|
|
||||||
clearVisualEffectsOnly();
|
|
||||||
|
|
||||||
// 重新绘制当前选择的效果
|
|
||||||
drawCircle(currentCenterPosition, radius.value);
|
|
||||||
|
|
||||||
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
|
||||||
addPulseEffectToPoints(pointsInCircle);
|
|
||||||
pulsePoints.value = pointsInCircle;
|
|
||||||
showPulsePointList.value = true;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 移除地图点击事件监听器,恢复鼠标默认样式
|
|
||||||
if (clickHandler) {
|
|
||||||
clickHandler.destroy();
|
|
||||||
clickHandler = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复鼠标默认样式
|
|
||||||
const viewer = CesiumUtilsSingleton.getViewer();
|
|
||||||
if (viewer?.canvas) {
|
|
||||||
statusStore.cursorStyle = 'default';
|
|
||||||
viewer.canvas.style.cursor = 'default';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleCancel = () => {
|
|
||||||
showAreaDialog.value = false;
|
|
||||||
clearAllAnalysisResources();
|
|
||||||
pulsePoints.value = [];
|
|
||||||
showPulsePointList.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从搜索结果启动区域分析
|
|
||||||
* 自动激活按钮(变为"取消区域分析"),显示区域选择对话框
|
|
||||||
* @param point - 搜索选中的点资源
|
|
||||||
*/
|
|
||||||
const startAreaAnalysisFromSearch = (point: PointResource) => {
|
|
||||||
if (point.lon == null || point.lat == null) return;
|
|
||||||
|
|
||||||
// 清除之前的分析
|
|
||||||
clearAllAnalysisResources();
|
|
||||||
|
|
||||||
// 设置中心点
|
|
||||||
currentCenterPosition = Cartesian3.fromDegrees(point.lon, point.lat, 0);
|
|
||||||
|
|
||||||
// 激活按钮(索引0是"标记区域分析"按钮)
|
|
||||||
selectedButtonIndex.value = 0;
|
|
||||||
|
|
||||||
// 设置鼠标样式为十字准星
|
|
||||||
const viewer = CesiumUtilsSingleton.getViewer();
|
|
||||||
if (viewer?.canvas) {
|
|
||||||
statusStore.cursorStyle = 'crosshair';
|
|
||||||
viewer.canvas.style.cursor = 'crosshair';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加标记
|
|
||||||
addMarker(currentCenterPosition);
|
|
||||||
|
|
||||||
// 显示对话框
|
|
||||||
showAreaDialog.value = true;
|
|
||||||
|
|
||||||
// 计算对话框位置(屏幕中心)
|
|
||||||
const centerX = window.innerWidth / 2;
|
|
||||||
const centerY = window.innerHeight / 2;
|
|
||||||
dialogPosition.x = Math.max(DIALOG_PADDING, Math.min(centerX + DIALOG_OFFSET, window.innerWidth - DIALOG_WIDTH - DIALOG_PADDING));
|
|
||||||
dialogPosition.y = Math.max(DIALOG_PADDING, Math.min(centerY + DIALOG_OFFSET, window.innerHeight - DIALOG_HEIGHT - DIALOG_PADDING));
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 监听器 ====================
|
|
||||||
|
|
||||||
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, () => {
|
|
||||||
// 只有当有中心点位置且脉冲点列表正在显示时,才刷新脉冲效果
|
|
||||||
if (currentCenterPosition && showPulsePointList.value) {
|
|
||||||
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,
|
|
||||||
pulsePoints,
|
|
||||||
showPulsePointList,
|
|
||||||
startAreaAnalysisFromSearch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
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 useAroundSearch = () => {
|
|
||||||
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.COLLAPSE_HIDDEN_POINT, category: 'hidden-danger', forcedType: 'collapse', isVisible: () => poi.value.showCollapseHiddenPoint.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 - 选中的点资源
|
|
||||||
* @param onAnalysisStart - 搜索后触发区域分析的回调(由父组件传入)
|
|
||||||
*/
|
|
||||||
async function handleSelect(item: PointResource, onAnalysisStart?: (point: PointResource) => void) {
|
|
||||||
if (item.lon == null || item.lat == null) return;
|
|
||||||
|
|
||||||
await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
|
||||||
|
|
||||||
// 飞行完成后,触发区域分析回调
|
|
||||||
if (onAnalysisStart) {
|
|
||||||
onAnalysisStart(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理聚焦事件,重新加载数据
|
|
||||||
*/
|
|
||||||
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 => {
|
|
||||||
const id = item.id || item._id || item.uuid || 'unknown_id';
|
|
||||||
const safeId = typeof id === 'string' || typeof id === 'number' ? id : 'unknown_id';
|
|
||||||
const name = item.name && String(item.name).trim() !== '' ? String(item.name) : String(safeId);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
id: safeId,
|
|
||||||
value: name,
|
|
||||||
category,
|
|
||||||
originalType: (forcedType || item.type || item.disasterType)?.toString().toLowerCase(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载所有点类数据
|
|
||||||
*/
|
|
||||||
function loadAllPointData() {
|
|
||||||
const resources = RESOURCE_CONFIGS.flatMap(config =>
|
|
||||||
convertStoreDataToResources(loadingResourceStore.getLoadingResource(config.key).info, config.category, config.forcedType)
|
|
||||||
);
|
|
||||||
const uniqueMap = new Map<string | number, PointResource>();
|
|
||||||
resources.forEach(item => uniqueMap.set(item.id, item));
|
|
||||||
allResources.value = Array.from(uniqueMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索框的值
|
|
||||||
*/
|
|
||||||
const state = ref('');
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
allResources,
|
|
||||||
canSearch,
|
|
||||||
querySearch,
|
|
||||||
handleSelect,
|
|
||||||
handleFocus,
|
|
||||||
loadAllPointData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Cartesian3,
|
Cartesian3,
|
||||||
Color,
|
Color,
|
||||||
Entity,
|
Entity,
|
||||||
VerticalOrigin,
|
VerticalOrigin,
|
||||||
HorizontalOrigin,
|
HorizontalOrigin,
|
||||||
HeightReference,
|
HeightReference,
|
||||||
CallbackProperty,
|
CallbackProperty,
|
||||||
JulianDate
|
JulianDate,
|
||||||
|
LabelStyle,
|
||||||
|
NearFarScalar
|
||||||
} from 'cesium';
|
} from 'cesium';
|
||||||
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
import type { PointResource } from '@/types/common/useAroundAnalysisType';
|
import type { PointResource } from '@/types/common/useAroundAnalysisType';
|
||||||
@@ -80,6 +82,23 @@ export const usePulseEffect = () => {
|
|||||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
text: point.value || String(point.id),
|
||||||
|
font: 'bold 16px Microsoft YaHei, sans-serif',
|
||||||
|
fillColor: Color.WHITE,
|
||||||
|
outlineColor: Color.BLACK,
|
||||||
|
outlineWidth: 5,
|
||||||
|
style: LabelStyle.FILL_AND_OUTLINE,
|
||||||
|
verticalOrigin: VerticalOrigin.BOTTOM,
|
||||||
|
horizontalOrigin: HorizontalOrigin.CENTER,
|
||||||
|
pixelOffset: new Cartesian3(0, -20),
|
||||||
|
eyeOffset: new Cartesian3(0, 0, -100),
|
||||||
|
heightReference: HeightReference.NONE,
|
||||||
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
|
showBackground: false,
|
||||||
|
scaleByDistance: new NearFarScalar(1000, 1.2, 50000, 0.8),
|
||||||
|
translucencyByDistance: new NearFarScalar(1000, 1.0, 30000, 0.9),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
pulseEntities.push(pulseEntity);
|
pulseEntities.push(pulseEntity);
|
||||||
@@ -119,4 +138,4 @@ export const usePulseEffect = () => {
|
|||||||
removePulseEffect,
|
removePulseEffect,
|
||||||
getPulseCount,
|
getPulseCount,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -8,17 +8,17 @@ import type { Ref } from 'vue';
|
|||||||
* 点资源分类类型
|
* 点资源分类类型
|
||||||
*/
|
*/
|
||||||
export type PointResourceCategory =
|
export type PointResourceCategory =
|
||||||
| 'school'
|
| 'school'
|
||||||
| 'hospital'
|
| 'hospital'
|
||||||
| 'danger'
|
| 'danger'
|
||||||
| 'shelter'
|
| 'shelter'
|
||||||
| 'fire'
|
| 'fire'
|
||||||
| 'store'
|
| 'store'
|
||||||
| 'hidden-danger'
|
| 'hidden-danger'
|
||||||
| 'risk-point'
|
| 'risk-point'
|
||||||
| 'bridge'
|
| 'bridge'
|
||||||
| 'reservoir'
|
| 'reservoir'
|
||||||
| 'subway';
|
| 'subway';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点资源数据结构
|
* 点资源数据结构
|
||||||
@@ -41,7 +41,7 @@ export interface PointResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源配置接口(用于 SearchComponent)
|
* 资源配置接口(统一版本)
|
||||||
*/
|
*/
|
||||||
export interface ResourceConfig {
|
export interface ResourceConfig {
|
||||||
/** 加载资源键 */
|
/** 加载资源键 */
|
||||||
@@ -51,19 +51,7 @@ export interface ResourceConfig {
|
|||||||
/** 强制类型(用于隐患点) */
|
/** 强制类型(用于隐患点) */
|
||||||
forcedType?: string;
|
forcedType?: string;
|
||||||
/** 是否可见的判断函数 */
|
/** 是否可见的判断函数 */
|
||||||
isVisible: () => boolean;
|
isVisible?: () => boolean;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源配置接口(用于 ButtonComponent,不含 isVisible)
|
|
||||||
*/
|
|
||||||
export interface ButtonResourceConfig {
|
|
||||||
/** 加载资源键 */
|
|
||||||
key: LoadingResource;
|
|
||||||
/** 资源分类 */
|
|
||||||
category: PointResourceCategory;
|
|
||||||
/** 强制类型(用于隐患点) */
|
|
||||||
forcedType?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,10 +77,9 @@ export interface DialogPosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 周边分析按钮状态接口(用于 provide/inject 共享)
|
* 周边分析统一状态接口
|
||||||
* 注意:响应式属性使用 Ref 类型
|
|
||||||
*/
|
*/
|
||||||
export interface AnalysisButtonState {
|
export interface AroundAnalysisState {
|
||||||
/** 当前选中的按钮索引 */
|
/** 当前选中的按钮索引 */
|
||||||
selectedButtonIndex: Ref<number>;
|
selectedButtonIndex: Ref<number>;
|
||||||
/** 是否显示区域选择弹窗 */
|
/** 是否显示区域选择弹窗 */
|
||||||
@@ -103,6 +90,15 @@ export interface AnalysisButtonState {
|
|||||||
dialogPosition: DialogPosition;
|
dialogPosition: DialogPosition;
|
||||||
/** 分析按钮配置列表 */
|
/** 分析按钮配置列表 */
|
||||||
analysisButtons: AnalysisButtonConfig[];
|
analysisButtons: AnalysisButtonConfig[];
|
||||||
|
/** 搜索框状态 */
|
||||||
|
searchState: Ref<string>;
|
||||||
|
/** 是否允许搜索 */
|
||||||
|
canSearch: Ref<boolean>;
|
||||||
|
/** 脉冲点列表 */
|
||||||
|
pulsePoints: Ref<PointResource[]>;
|
||||||
|
/** 是否显示脉冲点列表 */
|
||||||
|
showPulsePointList: Ref<boolean>;
|
||||||
|
|
||||||
/** 按钮点击处理函数 */
|
/** 按钮点击处理函数 */
|
||||||
handleButtonClick: (index: number, callback: (status: boolean) => void) => void;
|
handleButtonClick: (index: number, callback: (status: boolean) => void) => void;
|
||||||
/** 确认添加区域分析 */
|
/** 确认添加区域分析 */
|
||||||
@@ -111,32 +107,8 @@ export interface AnalysisButtonState {
|
|||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
/** 刷新脉冲效果 */
|
/** 刷新脉冲效果 */
|
||||||
refreshPulseEffect: () => void;
|
refreshPulseEffect: () => void;
|
||||||
/** 脉冲点列表 */
|
|
||||||
pulsePoints: Ref<PointResource[]>;
|
|
||||||
/** 是否显示脉冲点列表 */
|
|
||||||
showPulsePointList: Ref<boolean>;
|
|
||||||
/** 从搜索结果启动区域分析 */
|
/** 从搜索结果启动区域分析 */
|
||||||
startAreaAnalysisFromSearch: (point: PointResource) => void;
|
startAreaAnalysisFromSearch: (point: PointResource) => void;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索区域分析状态接口(用于 provide/inject 共享)
|
|
||||||
*/
|
|
||||||
export interface SearchAreaAnalysisState {
|
|
||||||
/** 是否显示区域选择对话框 */
|
|
||||||
showAreaDialog: Ref<boolean>;
|
|
||||||
/** 区域半径(公里) */
|
|
||||||
areaRadius: Ref<number>;
|
|
||||||
/** 对话框位置 */
|
|
||||||
dialogPosition: Ref<DialogPosition>;
|
|
||||||
/** 确认区域分析 */
|
|
||||||
handleAreaConfirm: () => void;
|
|
||||||
/** 取消区域分析 */
|
|
||||||
handleAreaCancel: () => void;
|
|
||||||
/** 搜索框状态 */
|
|
||||||
state: Ref<string>;
|
|
||||||
/** 是否允许搜索 */
|
|
||||||
canSearch: Ref<boolean>;
|
|
||||||
/** 查询建议 */
|
/** 查询建议 */
|
||||||
querySearch: (queryString: string, cb: (results: PointResource[]) => void) => void;
|
querySearch: (queryString: string, cb: (results: PointResource[]) => void) => void;
|
||||||
/** 选择建议回调 */
|
/** 选择建议回调 */
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
||||||
|
import type { PointResource, PointResourceCategory, ResourceConfig } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断资源分类是否可见
|
||||||
|
*/
|
||||||
|
export const isCategoryVisible = (
|
||||||
|
category: PointResourceCategory,
|
||||||
|
originalType?: string
|
||||||
|
): boolean => {
|
||||||
|
const statusStore = useStatusStore();
|
||||||
|
const poi = statusStore.poiLayers;
|
||||||
|
const map = statusStore.mapLayers;
|
||||||
|
const infra = statusStore.infrastructureLayers;
|
||||||
|
|
||||||
|
const visibilityMap: Record<string, () => boolean> = {
|
||||||
|
school: () => poi.showSchool.show,
|
||||||
|
hospital: () => poi.showHospital.show,
|
||||||
|
danger: () => poi.showDangerSource.show,
|
||||||
|
shelter: () => poi.showRefugeeShelter.show,
|
||||||
|
fire: () => poi.showFireStation.show,
|
||||||
|
store: () => poi.showReservePoint.show,
|
||||||
|
subway: () => poi.showSubwayStation.show,
|
||||||
|
'risk-point': () => map.riskPointShow.show,
|
||||||
|
bridge: () => infra.showBridge.show,
|
||||||
|
reservoir: () => infra.showReservoir.show,
|
||||||
|
'hidden-danger': () => {
|
||||||
|
const hiddenMap: Record<string, () => boolean> = {
|
||||||
|
landslide: () => poi.showLandslideHiddenPoint.show,
|
||||||
|
debris_flow: () => poi.showDebrisFlowHiddenPoint.show,
|
||||||
|
water_logging: () => poi.showWaterLoggingHiddenPoint.show,
|
||||||
|
flash_flood: () => poi.showFlashFloodHiddenPoint.show,
|
||||||
|
collapse: () => poi.showCollapseHiddenPoint.show,
|
||||||
|
};
|
||||||
|
return hiddenMap[originalType || '']?.() ?? false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return visibilityMap[category]?.() ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换 Store 数据为资源格式
|
||||||
|
*/
|
||||||
|
export const convertStoreDataToResources = (
|
||||||
|
infoList: Record<string, unknown>[],
|
||||||
|
category: PointResource['category'],
|
||||||
|
forcedType?: string
|
||||||
|
): PointResource[] => {
|
||||||
|
if (!Array.isArray(infoList)) return [];
|
||||||
|
return infoList.map(item => {
|
||||||
|
const id = item.id || item._id || item.uuid || 'unknown_id';
|
||||||
|
const safeId = typeof id === 'string' || typeof id === 'number' ? id : 'unknown_id';
|
||||||
|
const name = item.name && String(item.name).trim() !== '' ? String(item.name) : String(safeId);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: safeId,
|
||||||
|
value: name,
|
||||||
|
category,
|
||||||
|
originalType: (forcedType || item.type || item.disasterType)?.toString().toLowerCase(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载所有点数据
|
||||||
|
*/
|
||||||
|
export const loadAllPointData = (configs: ResourceConfig[]): PointResource[] => {
|
||||||
|
const loadingResourceStore = useLoadingResourceStore();
|
||||||
|
const resources = configs.flatMap(config =>
|
||||||
|
convertStoreDataToResources(
|
||||||
|
loadingResourceStore.getLoadingResource(config.key).info,
|
||||||
|
config.category,
|
||||||
|
config.forcedType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueMap = new Map<string | number, PointResource>();
|
||||||
|
resources.forEach(item => uniqueMap.set(item.id, item));
|
||||||
|
return Array.from(uniqueMap.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两点间距离(Haversine 公式)
|
||||||
|
*/
|
||||||
|
export 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 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user