更改文件名并修改代码
This commit is contained in:
@@ -15,12 +15,35 @@
|
|||||||
<!-- 脉冲点列表组件 -->
|
<!-- 脉冲点列表组件 -->
|
||||||
<PulsePointListComponent />
|
<PulsePointListComponent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索触发的区域选择对话框(fixed定位,相对于视口) -->
|
||||||
|
<div v-if="searchAreaState.showAreaDialog.value" class="search-area-dialog" :style="{ left: searchAreaState.dialogPosition.value.x + 'px', top: searchAreaState.dialogPosition.value.y + 'px' }">
|
||||||
|
<div class="dialog-header">选择区域</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="radius-input-group">
|
||||||
|
<span class="label">半径:</span>
|
||||||
|
<input
|
||||||
|
v-model.number="searchAreaState.areaRadius.value"
|
||||||
|
type="number"
|
||||||
|
class="radius-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
<span class="unit">公里</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="confirm-btn" @click="searchAreaState.handleAreaConfirm">确认添加</button>
|
||||||
|
<button class="cancel-btn" @click="searchAreaState.handleAreaCancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide } from 'vue';//从Vue导入provide函数,用于依赖注入
|
import { provide } from 'vue';//从Vue导入provide函数,用于依赖注入
|
||||||
import { useStatusStore } from '@/stores/useStatusStore';
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
import { useAnalysisButton } from '@/hooks/rain-earthquake/useAnalysisButton';
|
import { useAroundButton } from '@/hooks/rain-earthquake/useAroundButton.ts';
|
||||||
|
import { useAroundSearch } from '@/hooks/rain-earthquake/useAroundSearch.ts';
|
||||||
import AroundAnalysisDetailComponent from './around-analysis/AroundAnalysisDetailComponent.vue';
|
import AroundAnalysisDetailComponent from './around-analysis/AroundAnalysisDetailComponent.vue';
|
||||||
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';
|
||||||
@@ -30,10 +53,116 @@ import PulsePointListComponent from './around-analysis/PulsePointListComponent.v
|
|||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
// 在父组件中创建唯一的 Hook 实例,包含所有周边分析相关的状态和方法
|
// 在父组件中创建唯一的 Hook 实例,包含所有周边分析相关的状态和方法
|
||||||
const analysisButtonState = useAnalysisButton();
|
const analysisButtonState = useAroundButton();
|
||||||
|
|
||||||
|
// 搜索触发的区域分析 Hook(在父组件中创建,确保 watch 不会被销毁)
|
||||||
|
const searchAreaState = useAroundSearch();
|
||||||
|
|
||||||
// 通过 provide 共享给所有子组件(让 TypeScript 自动推断类型)
|
// 通过 provide 共享给所有子组件(让 TypeScript 自动推断类型)
|
||||||
provide('analysisButtonState', analysisButtonState);
|
provide('analysisButtonState', analysisButtonState);
|
||||||
|
provide('searchAreaState', searchAreaState);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
/* 搜索触发的区域选择对话框样式(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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,12 +22,37 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索触发的区域选择对话框(fixed定位,相对于视口) -->
|
||||||
|
<div v-if="searchAreaState?.showAreaDialog.value" class="search-area-dialog" :style="{ left: searchAreaState.dialogPosition.value.x + 'px', top: searchAreaState.dialogPosition.value.y + 'px' }">
|
||||||
|
<div class="dialog-header">选择区域</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="radius-input-group">
|
||||||
|
<span class="label">半径:</span>
|
||||||
|
<input
|
||||||
|
v-model.number="searchAreaState.areaRadius.value"
|
||||||
|
type="number"
|
||||||
|
class="radius-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
<span class="unit">公里</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="confirm-btn" @click="searchAreaState.handleAreaConfirm">确认添加</button>
|
||||||
|
<button class="cancel-btn" @click="searchAreaState.handleAreaCancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { inject, onMounted } from 'vue';
|
||||||
import { Edit } from '@element-plus/icons-vue';
|
import { Edit } from '@element-plus/icons-vue';
|
||||||
import { useAroundAnalysis } from '@/hooks/rain-earthquake/useAroundAnalysis';
|
import type { SearchAreaAnalysisState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
|
// 从父组件注入搜索区域分析状态
|
||||||
|
const searchAreaState = inject<SearchAreaAnalysisState>('searchAreaState');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
@@ -35,11 +60,14 @@ const {
|
|||||||
querySearch,
|
querySearch,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
loadAllPointData
|
} = searchAreaState!;
|
||||||
} = useAroundAnalysis();
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAllPointData();
|
// 加载数据
|
||||||
|
if (searchAreaState) {
|
||||||
|
// 通过调用 handleFocus 来加载数据
|
||||||
|
handleFocus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,4 +130,106 @@ onMounted(() => {
|
|||||||
background: rgba(58, 112, 169, 0.7);
|
background: rgba(58, 112, 169, 0.7);
|
||||||
color: #fff;
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -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<PointResource[]>([]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算属性:判断是否允许搜索
|
|
||||||
*/
|
|
||||||
const canSearch = computed(() => {
|
|
||||||
return RESOURCE_CONFIGS.some(config => config.isVisible());
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询建议回调
|
|
||||||
* @param queryString - 搜索字符串
|
|
||||||
* @param cb - 回调函数
|
|
||||||
*/
|
|
||||||
function querySearch(queryString: string, cb: (results: PointResource[]) => void) {
|
|
||||||
if (!canSearch.value) {
|
|
||||||
cb([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerQuery = queryString.toLowerCase();
|
|
||||||
const filteredResults = allResources.value.filter(item => {
|
|
||||||
const config = RESOURCE_CONFIGS.find(c => c.category === item.category);
|
|
||||||
let isVisible = false;
|
|
||||||
if (config) {
|
|
||||||
if (item.category === 'hidden-danger') {
|
|
||||||
const type = (item.originalType as string)?.toLowerCase();
|
|
||||||
isVisible = (type === config.forcedType?.toLowerCase()) && config.isVisible();
|
|
||||||
} else {
|
|
||||||
isVisible = config.isVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isVisible) return false;
|
|
||||||
if (!queryString) return true;
|
|
||||||
const matchStr = (item.value || '').toLowerCase();
|
|
||||||
return matchStr.includes(lowerQuery);
|
|
||||||
});
|
|
||||||
|
|
||||||
cb(filteredResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择建议回调
|
|
||||||
* @param item - 选中的点资源
|
|
||||||
*/
|
|
||||||
function handleSelect(item: PointResource) {
|
|
||||||
if (item.lon != null && item.lat != null) {
|
|
||||||
CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理聚焦事件,重新加载数据
|
|
||||||
*/
|
|
||||||
function handleFocus() {
|
|
||||||
loadAllPointData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据处理:将 Store 数据转换为资源格式
|
|
||||||
* @param infoList - 原始数据列表
|
|
||||||
* @param category - 资源分类
|
|
||||||
* @param forcedType - 强制类型
|
|
||||||
* @returns 转换后的点资源数组
|
|
||||||
*/
|
|
||||||
function convertStoreDataToResources(
|
|
||||||
infoList: Record<string, unknown>[],
|
|
||||||
category: PointResource['category'],
|
|
||||||
forcedType?: string,
|
|
||||||
): PointResource[] {
|
|
||||||
if (!Array.isArray(infoList)) return [];
|
|
||||||
|
|
||||||
return infoList.map((item: Record<string, unknown>) => {
|
|
||||||
const id = item.id || item._id || item.uuid || 'unknown_id';
|
|
||||||
const safeId = typeof id === 'string' ? id : typeof id === 'number' ? id : 'unknown_id';
|
|
||||||
|
|
||||||
const value = (item.name && String(item.name).trim() !== '')
|
|
||||||
? String(item.name)
|
|
||||||
: String(safeId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
id: safeId,
|
|
||||||
value: value,
|
|
||||||
category: category,
|
|
||||||
originalType: (forcedType || (item.type as string) || (item.disasterType as string))?.toLowerCase(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载所有点类数据
|
|
||||||
*/
|
|
||||||
function loadAllPointData() {
|
|
||||||
const resources: PointResource[] = [];
|
|
||||||
|
|
||||||
RESOURCE_CONFIGS.forEach(config => {
|
|
||||||
const data = loadingResourceStore.getLoadingResource(config.key).info;
|
|
||||||
resources.push(...convertStoreDataToResources(data, config.category, config.forcedType));
|
|
||||||
});
|
|
||||||
|
|
||||||
const seenIds = new Map<string | number, PointResource>();
|
|
||||||
const uniqueResources: PointResource[] = [];
|
|
||||||
for (const item of resources) {
|
|
||||||
if (!seenIds.has(item.id)) {
|
|
||||||
seenIds.set(item.id, item);
|
|
||||||
uniqueResources.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allResources.value = uniqueResources;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索框的值
|
|
||||||
*/
|
|
||||||
const state = ref('');
|
|
||||||
|
|
||||||
return { state, allResources, canSearch, querySearch, handleSelect, handleFocus, loadAllPointData };
|
|
||||||
};
|
|
||||||
+1
-1
@@ -86,7 +86,7 @@ const isCategoryVisible = (category: PointResourceCategory, originalType?: strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 响应式状态 ====================
|
// ==================== 响应式状态 ====================
|
||||||
export const useAnalysisButton = (): AnalysisButtonState => {
|
export const useAroundButton = (): AnalysisButtonState => {
|
||||||
const selectedButtonIndex = ref<number>(-1);
|
const selectedButtonIndex = ref<number>(-1);
|
||||||
const showAreaDialog = ref(false);
|
const showAreaDialog = ref(false);
|
||||||
const radius = ref(10);
|
const radius = ref(10);
|
||||||
@@ -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<PointResource | null>(null);
|
||||||
|
const isAreaAnalysisActive = ref(false);
|
||||||
|
const currentAnalysisCenter = ref<Cartesian3 | null>(null);
|
||||||
|
const showPulsePointListFromSearch = ref(false);
|
||||||
|
const pulsePointsFromSearch = ref<PointResource[]>([]);
|
||||||
|
|
||||||
|
// 组合子 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<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 - 选中的点资源
|
||||||
|
*/
|
||||||
|
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<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,
|
||||||
|
// 区域分析相关
|
||||||
|
showAreaDialog,
|
||||||
|
areaRadius,
|
||||||
|
dialogPosition,
|
||||||
|
isAreaAnalysisActive,
|
||||||
|
showPulsePointListFromSearch,
|
||||||
|
pulsePointsFromSearch,
|
||||||
|
handleAreaConfirm,
|
||||||
|
handleAreaCancel,
|
||||||
|
refreshSearchPulseEffect,
|
||||||
|
clearSearchAreaAnalysis,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -116,3 +116,29 @@ export interface AnalysisButtonState {
|
|||||||
/** 是否显示脉冲点列表 */
|
/** 是否显示脉冲点列表 */
|
||||||
showPulsePointList: Ref<boolean>;
|
showPulsePointList: Ref<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索区域分析状态接口(用于 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;
|
||||||
|
/** 选择建议回调 */
|
||||||
|
handleSelect: (item: PointResource) => void;
|
||||||
|
/** 聚焦事件处理 */
|
||||||
|
handleFocus: () => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user