- 搜索
+
+
+
+
+
+
+
+
+ {{ item.value }}
+ {{ item.lon?.toFixed(4) }}, {{ item.lat?.toFixed(4) }}
+
+
-
+
+
+
diff --git a/src/hooks/rain-earthquake/useAnalysisButton.ts b/src/hooks/rain-earthquake/useAnalysisButton.ts
new file mode 100644
index 0000000..7cc5a10
--- /dev/null
+++ b/src/hooks/rain-earthquake/useAnalysisButton.ts
@@ -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
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 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(-1);
+ const showAreaDialog = ref(false);
+ const radius = ref(10);
+ const dialogPosition = reactive({ 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) => {
+ 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();
+ 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,
+ };
+};
diff --git a/src/hooks/rain-earthquake/useAroundAnalysis.ts b/src/hooks/rain-earthquake/useAroundAnalysis.ts
new file mode 100644
index 0000000..8e2cab6
--- /dev/null
+++ b/src/hooks/rain-earthquake/useAroundAnalysis.ts
@@ -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([]);
+
+ /**
+ * 计算属性:判断是否允许搜索
+ */
+ 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/useCircleDrawer.ts b/src/hooks/rain-earthquake/useCircleDrawer.ts
new file mode 100644
index 0000000..ccc33ab
--- /dev/null
+++ b/src/hooks/rain-earthquake/useCircleDrawer.ts
@@ -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
+ ): 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,
+ };
+};
diff --git a/src/hooks/rain-earthquake/useMarkerManager.ts b/src/hooks/rain-earthquake/useMarkerManager.ts
new file mode 100644
index 0000000..cd87581
--- /dev/null
+++ b/src/hooks/rain-earthquake/useMarkerManager.ts
@@ -0,0 +1,72 @@
+import { Cartesian3, Entity, VerticalOrigin, HorizontalOrigin, HeightReference } from 'cesium';
+import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
+
+// 十字准心标记 SVG
+const MARKER_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,
+ };
+};
diff --git a/src/hooks/rain-earthquake/usePulseEffect.ts b/src/hooks/rain-earthquake/usePulseEffect.ts
new file mode 100644
index 0000000..cc47c00
--- /dev/null
+++ b/src/hooks/rain-earthquake/usePulseEffect.ts
@@ -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,
+ };
+};
diff --git a/src/stores/useStatusStore.ts b/src/stores/useStatusStore.ts
index 9650e0d..445ee4c 100644
--- a/src/stores/useStatusStore.ts
+++ b/src/stores/useStatusStore.ts
@@ -195,6 +195,11 @@ export const useStatusStore = defineStore('status', () => {
},
});
+ /**
+ * 鼠标样式状态
+ */
+ const cursorStyle = ref('default');
+
/**
* 恢复默认值
*/
@@ -338,6 +343,7 @@ export const useStatusStore = defineStore('status', () => {
infrastructureLayers,
weatherLayers,
functionStatus,
+ cursorStyle,
reset,
resetScene,
};
diff --git a/src/types/cesium/EntityOptions.ts b/src/types/cesium/EntityOptions.ts
index e818c3c..ef31567 100644
--- a/src/types/cesium/EntityOptions.ts
+++ b/src/types/cesium/EntityOptions.ts
@@ -75,3 +75,70 @@ export interface EntityOptions {
/** 自定义属性(用于存储额外信息) */
attributes?: Record;
}
+/**
+ * 椭圆配置选项
+ * 用于绘制椭圆形区域(圆形是椭圆的特例: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;
+ }
+}
\ No newline at end of file
diff --git a/src/types/common/useAroundAnalysisType.ts b/src/types/common/useAroundAnalysisType.ts
new file mode 100644
index 0000000..f08e0ab
--- /dev/null
+++ b/src/types/common/useAroundAnalysisType.ts
@@ -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;
+ /** 是否显示区域选择弹窗 */
+ showAreaDialog: Ref;
+ /** 区域半径(公里) */
+ radius: Ref;
+ /** 弹窗位置 */
+ dialogPosition: DialogPosition;
+ /** 分析按钮配置列表 */
+ analysisButtons: AnalysisButtonConfig[];
+ /** 按钮点击处理函数 */
+ handleButtonClick: (index: number, callback: (status: boolean) => void) => void;
+ /** 确认添加区域分析 */
+ handleConfirm: () => void;
+ /** 取消区域分析 */
+ handleCancel: () => void;
+ /** 刷新脉冲效果 */
+ refreshPulseEffect: () => void;
+}