From 51c7352a90da9266aca4c301a07f84172938ab53 Mon Sep 17 00:00:00 2001
From: zxyroyy <1442470094@qq.com>
Date: Mon, 22 Jun 2026 21:45:09 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=96=87=E4=BB=B6=E5=90=8D?=
=?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../function-child/AroundAnalysis.vue | 135 ++++++-
.../around-analysis/SearchComponent.vue | 160 +++++++-
.../rain-earthquake/useAroundAnalysis.ts | 164 --------
...seAnalysisButton.ts => useAroundButton.ts} | 2 +-
src/hooks/rain-earthquake/useAroundSearch.ts | 357 ++++++++++++++++++
src/types/common/useAroundAnalysisType.ts | 26 ++
6 files changed, 661 insertions(+), 183 deletions(-)
delete mode 100644 src/hooks/rain-earthquake/useAroundAnalysis.ts
rename src/hooks/rain-earthquake/{useAnalysisButton.ts => useAroundButton.ts} (99%)
create mode 100644 src/hooks/rain-earthquake/useAroundSearch.ts
diff --git a/src/component/rain-earthquake/function-child/AroundAnalysis.vue b/src/component/rain-earthquake/function-child/AroundAnalysis.vue
index a806d49..00bca4c 100644
--- a/src/component/rain-earthquake/function-child/AroundAnalysis.vue
+++ b/src/component/rain-earthquake/function-child/AroundAnalysis.vue
@@ -15,12 +15,35 @@
+
+
+
-
+
\ No newline at end of file
diff --git a/src/component/rain-earthquake/function-child/around-analysis/SearchComponent.vue b/src/component/rain-earthquake/function-child/around-analysis/SearchComponent.vue
index 0559b96..b7098ab 100644
--- a/src/component/rain-earthquake/function-child/around-analysis/SearchComponent.vue
+++ b/src/component/rain-earthquake/function-child/around-analysis/SearchComponent.vue
@@ -22,24 +22,52 @@
+
+
+
@@ -76,12 +104,12 @@ onMounted(() => {
}
.search-component-box :deep(.my-autocomplete li) {
- padding: 2px 6px;
+ padding: 2px 6px;
display: flex;
justify-content: space-between;
cursor: pointer;
color: #e6edf3;
- font-size: 13px;
+ font-size: 13px;
}
.search-component-box :deep(.my-autocomplete li .value) {
@@ -92,7 +120,7 @@ onMounted(() => {
}
.search-component-box :deep(.my-autocomplete li .link) {
- font-size: 12px;
+ font-size: 12px;
margin-left: 8px;
flex-shrink: 0;
}
@@ -102,4 +130,106 @@ onMounted(() => {
background: rgba(58, 112, 169, 0.7);
color: #fff;
}
-
+
+/* 搜索触发的区域选择对话框样式(fixed定位,相对于视口) */
+.search-area-dialog {
+ position: fixed;
+ 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;
+}
+
+.search-area-dialog .dialog-header {
+ padding: 8px 12px;
+ font-size: 16px;
+ font-weight: bold;
+ text-align: center;
+ color: white;
+ background: linear-gradient(90deg, #00b4ff, #0080cc);
+}
+
+.search-area-dialog .dialog-content {
+ padding: 10px 6px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.search-area-dialog .radius-input-group {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.search-area-dialog .radius-input-group .label,
+.search-area-dialog .radius-input-group .unit {
+ font-size: 14px;
+ color: white;
+}
+
+.search-area-dialog .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;
+}
+
+.search-area-dialog .radius-input::-webkit-inner-spin-button,
+.search-area-dialog .radius-input::-webkit-outer-spin-button {
+ opacity: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ margin: 0;
+}
+
+.search-area-dialog .dialog-footer {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding: 10px 10px 8px;
+}
+
+.search-area-dialog .confirm-btn,
+.search-area-dialog .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;
+}
+
+.search-area-dialog .confirm-btn {
+ background: linear-gradient(180deg, #2d8a4e, #1e6b3a);
+ border: 1px solid #3da862;
+}
+
+.search-area-dialog .confirm-btn:hover {
+ background: linear-gradient(180deg, #3da862, #2d8a4e);
+ box-shadow: 0 2px 8px rgba(45, 138, 78, 0.5);
+}
+
+.search-area-dialog .cancel-btn {
+ background: linear-gradient(180deg, #c0392b, #96281b);
+ border: 1px solid #e74c3c;
+}
+
+.search-area-dialog .cancel-btn:hover {
+ background: linear-gradient(180deg, #e74c3c, #c0392b);
+ box-shadow: 0 2px 8px rgba(192, 57, 43, 0.5);
+}
+
\ No newline at end of file
diff --git a/src/hooks/rain-earthquake/useAroundAnalysis.ts b/src/hooks/rain-earthquake/useAroundAnalysis.ts
deleted file mode 100644
index 83e9906..0000000
--- a/src/hooks/rain-earthquake/useAroundAnalysis.ts
+++ /dev/null
@@ -1,164 +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 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.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([]);
-
- /**
- * 计算属性:判断是否允许搜索
- */
- 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[],
- category: PointResource['category'],
- forcedType?: string,
- ): PointResource[] {
- if (!Array.isArray(infoList)) return [];
-
- return infoList.map((item: Record) => {
- 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();
- 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 };
-};
diff --git a/src/hooks/rain-earthquake/useAnalysisButton.ts b/src/hooks/rain-earthquake/useAroundButton.ts
similarity index 99%
rename from src/hooks/rain-earthquake/useAnalysisButton.ts
rename to src/hooks/rain-earthquake/useAroundButton.ts
index a72299f..af22aa5 100644
--- a/src/hooks/rain-earthquake/useAnalysisButton.ts
+++ b/src/hooks/rain-earthquake/useAroundButton.ts
@@ -86,7 +86,7 @@ const isCategoryVisible = (category: PointResourceCategory, originalType?: strin
};
// ==================== 响应式状态 ====================
-export const useAnalysisButton = (): AnalysisButtonState => {
+export const useAroundButton = (): AnalysisButtonState => {
const selectedButtonIndex = ref(-1);
const showAreaDialog = ref(false);
const radius = ref(10);
diff --git a/src/hooks/rain-earthquake/useAroundSearch.ts b/src/hooks/rain-earthquake/useAroundSearch.ts
new file mode 100644
index 0000000..825b6b5
--- /dev/null
+++ b/src/hooks/rain-earthquake/useAroundSearch.ts
@@ -0,0 +1,357 @@
+import { computed, ref, watch } 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';
+import { Cartesian3, Cartographic } from 'cesium';
+import { useCircleDrawer } from './useCircleDrawer';
+import { usePulseEffect } from './usePulseEffect';
+import { useMarkerManager } from './useMarkerManager';
+
+/**
+ * 周边分析搜索组件钩子函数
+ * @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 showAreaDialog = ref(false);
+ const areaRadius = ref(10);
+ const dialogPosition = ref({ x: 0, y: 0 });
+ const pendingAnalysisPoint = ref(null);
+ const isAreaAnalysisActive = ref(false);
+ const currentAnalysisCenter = ref(null);
+ const showPulsePointListFromSearch = ref(false);
+ const pulsePointsFromSearch = ref([]);
+
+ // 组合子 Hook
+ const { drawCircle, clearCircle } = useCircleDrawer();
+ const { addPulseEffectToPoints, removePulseEffect } = usePulseEffect();
+ const { addMarker, removeMarker } = useMarkerManager();
+
+ /**
+ * 资源配置列表
+ */
+ 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([]);
+
+ /**
+ * 计算属性:判断是否允许搜索
+ */
+ 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 - 选中的点资源
+ */
+ async function handleSelect(item: PointResource) {
+ if (item.lon == null || item.lat == null) return;
+
+ await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
+ startAreaAnalysis(item);
+ }
+
+ /**
+ * 开始区域分析
+ * @param centerPoint - 中心点资源
+ */
+ function startAreaAnalysis(centerPoint: PointResource) {
+ if (centerPoint.lon == null || centerPoint.lat == null) return;
+ // 清除之前的分析
+ clearSearchAreaAnalysis();
+ // 保存待分析的点
+ pendingAnalysisPoint.value = centerPoint;
+ // 设置中心点
+ const centerPosition = Cartesian3.fromDegrees(centerPoint.lon, centerPoint.lat, 0);
+ currentAnalysisCenter.value = centerPosition;
+ isAreaAnalysisActive.value = true;
+ // 添加标记(红点+四个绿色角)
+ addMarker(centerPosition);
+ // 显示区域选择对话框
+ showAreaDialog.value = true;
+ // 计算对话框位置(屏幕中心右下20px)
+ calculateDialogPosition();
+ }
+
+ /**
+ * 计算对话框位置(在屏幕中心点右下20px,确保在界面内)
+ */
+ function calculateDialogPosition() {
+ const W = 280, H = 150, P = 10, O = 20;
+ const cx = window.innerWidth / 2, cy = window.innerHeight / 2;
+ let x = cx + O, y = cy + O;
+ x = x + W > window.innerWidth - P ? cx - W - O : x;
+ y = y + H > window.innerHeight - P ? cy - H - O : y;
+ dialogPosition.value = {
+ x: Math.max(P, Math.min(x, window.innerWidth - W - P)),
+ y: Math.max(P, Math.min(y, window.innerHeight - H - P)),
+ };
+ }
+
+ /**
+ * 确认区域分析
+ */
+ function handleAreaConfirm() {
+ if (!pendingAnalysisPoint.value) return;
+ const { lon, lat } = pendingAnalysisPoint.value;
+ if (lon == null || lat == null) return;
+ // 关闭对话框
+ showAreaDialog.value = false;
+ // 绘制圆形
+ if (currentAnalysisCenter.value) {
+ drawCircle(currentAnalysisCenter.value, areaRadius.value);
+ }
+ // 计算并添加脉冲效果
+ refreshSearchPulseEffect();
+ // 飞行到合适的高度以显示整个圆形区域
+ const flyHeight = Math.max(areaRadius.value * 6000, 10000);
+ CesiumUtilsSingleton.flyToTarget([lon, lat, flyHeight], 2);
+ }
+
+ /**
+ * 取消区域分析
+ */
+ function handleAreaCancel() {
+ showAreaDialog.value = false;
+ clearSearchAreaAnalysis();
+ }
+
+ /**
+ * 清除搜索触发的区域分析
+ */
+ function clearSearchAreaAnalysis() {
+ removeMarker();
+ clearCircle();
+ removePulseEffect();
+ currentAnalysisCenter.value = null;
+ isAreaAnalysisActive.value = false;
+ showPulsePointListFromSearch.value = false;
+ pulsePointsFromSearch.value = [];
+ pendingAnalysisPoint.value = null;
+ }
+
+ /**
+ * 刷新搜索触发的脉冲效果
+ */
+ function refreshSearchPulseEffect() {
+ if (!currentAnalysisCenter.value) {
+ console.warn('refreshSearchPulseEffect: currentAnalysisCenter.value 为空');
+ return;
+ }
+ removePulseEffect();
+ const pointsInCircle = getPointsInCircle(currentAnalysisCenter.value, areaRadius.value);
+ addPulseEffectToPoints(pointsInCircle);
+ pulsePointsFromSearch.value = pointsInCircle;
+ showPulsePointListFromSearch.value = true;
+ }
+
+ /**
+ * 获取圆形范围内的点
+ * @param centerPosition - 中心位置
+ * @param radiusKm - 半径(公里)
+ * @returns 圆形范围内的点资源数组
+ */
+ function 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 radiusMeters = radiusKm * 1000;
+ return allResources.value.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, point.originalType);
+ });
+ }
+
+ /**
+ * 计算两点间距离(米)
+ */
+ function calculateDistance(
+ centerLon: number,
+ centerLat: number,
+ pointLon: unknown,
+ pointLat: unknown
+ ): number {
+ const EARTH_RADIUS = 6371000;
+ 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));
+ }
+
+ /**
+ * 判断分类是否可见(复用 RESOURCE_CONFIGS 配置)
+ */
+ function isCategoryVisible(category?: string, originalType?: string): boolean {
+ if (!category) return false;
+
+ const config = RESOURCE_CONFIGS.find(c => c.category === category);
+ if (!config) return false;
+
+ // 隐患点需要额外判断子类型
+ if (category === 'hidden-danger') {
+ return config.forcedType === originalType?.toLowerCase() && config.isVisible();
+ }
+
+ return config.isVisible();
+ }
+
+ // 监听图层可见性变化,自动刷新脉冲效果(复用 RESOURCE_CONFIGS)
+ const layerVisibilityWatchers = RESOURCE_CONFIGS.map(config => config.isVisible);
+
+ // 监听图层可见性变化
+ watch(layerVisibilityWatchers, () => {
+ if (currentAnalysisCenter.value && showPulsePointListFromSearch.value) {
+ loadAllPointData(); // ① 重新加载所有数据
+ refreshSearchPulseEffect(); // ② 刷新脉冲
+ }
+ });
+
+ // 监听资源数据变化
+ watch(
+ () => loadingResourceStore.loadingResource,
+ () => {
+ if (currentAnalysisCenter.value && showPulsePointListFromSearch.value) {
+ loadAllPointData();
+ refreshSearchPulseEffect();
+ }
+ },
+ { deep: true }
+ );
+
+ /**
+ * 处理聚焦事件,重新加载数据
+ */
+ function handleFocus() {
+ loadAllPointData();
+ }
+
+ /**
+ * 数据处理:将 Store 数据转换为资源格式
+ * @param infoList - 原始数据列表
+ * @param category - 资源分类
+ * @param forcedType - 强制类型
+ * @returns 转换后的点资源数组
+ */
+ function convertStoreDataToResources(
+ infoList: Record[],
+ 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();
+ 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,
+ // 区域分析相关
+ showAreaDialog,
+ areaRadius,
+ dialogPosition,
+ isAreaAnalysisActive,
+ showPulsePointListFromSearch,
+ pulsePointsFromSearch,
+ handleAreaConfirm,
+ handleAreaCancel,
+ refreshSearchPulseEffect,
+ clearSearchAreaAnalysis,
+ };
+};
\ No newline at end of file
diff --git a/src/types/common/useAroundAnalysisType.ts b/src/types/common/useAroundAnalysisType.ts
index a0e554a..f8f3483 100644
--- a/src/types/common/useAroundAnalysisType.ts
+++ b/src/types/common/useAroundAnalysisType.ts
@@ -116,3 +116,29 @@ export interface AnalysisButtonState {
/** 是否显示脉冲点列表 */
showPulsePointList: Ref;
}
+
+/**
+ * 搜索区域分析状态接口(用于 provide/inject 共享)
+ */
+export interface SearchAreaAnalysisState {
+ /** 是否显示区域选择对话框 */
+ showAreaDialog: Ref;
+ /** 区域半径(公里) */
+ areaRadius: Ref;
+ /** 对话框位置 */
+ dialogPosition: Ref;
+ /** 确认区域分析 */
+ handleAreaConfirm: () => void;
+ /** 取消区域分析 */
+ handleAreaCancel: () => void;
+ /** 搜索框状态 */
+ state: Ref;
+ /** 是否允许搜索 */
+ canSearch: Ref;
+ /** 查询建议 */
+ querySearch: (queryString: string, cb: (results: PointResource[]) => void) => void;
+ /** 选择建议回调 */
+ handleSelect: (item: PointResource) => void;
+ /** 聚焦事件处理 */
+ handleFocus: () => void;
+}
\ No newline at end of file