存档代码
This commit is contained in:
@@ -11,19 +11,16 @@
|
|||||||
|
|
||||||
<!-- 具体功能组件 -->
|
<!-- 具体功能组件 -->
|
||||||
<AroundAnalysisDetailComponent />
|
<AroundAnalysisDetailComponent />
|
||||||
|
|
||||||
<!-- 脉冲点列表组件 -->
|
|
||||||
<PulsePointListComponent />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索触发的区域选择对话框(fixed定位,相对于视口) -->
|
<!-- 区域选择对话框(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 v-if="analysisButtonState.showAreaDialog.value" class="search-area-dialog" :style="{ left: analysisButtonState.dialogPosition.x + 'px', top: analysisButtonState.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="searchAreaState.areaRadius.value"
|
v-model.number="analysisButtonState.radius.value"
|
||||||
type="number"
|
type="number"
|
||||||
class="radius-input"
|
class="radius-input"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -33,8 +30,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button class="confirm-btn" @click="searchAreaState.handleAreaConfirm">确认添加</button>
|
<button class="confirm-btn" @click="analysisButtonState.handleConfirm">确认添加</button>
|
||||||
<button class="cancel-btn" @click="searchAreaState.handleAreaCancel">取消</button>
|
<button class="cancel-btn" @click="analysisButtonState.handleCancel">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,23 +41,19 @@ import { provide } from 'vue';//从Vue导入provide函数,用于依赖注入
|
|||||||
import { useStatusStore } from '@/stores/useStatusStore';
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
import { useAroundButton } from '@/hooks/rain-earthquake/useAroundButton.ts';
|
import { useAroundButton } from '@/hooks/rain-earthquake/useAroundButton.ts';
|
||||||
import { useAroundSearch } from '@/hooks/rain-earthquake/useAroundSearch.ts';
|
import { useAroundSearch } from '@/hooks/rain-earthquake/useAroundSearch.ts';
|
||||||
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';
|
||||||
import PulsePointListComponent from './around-analysis/PulsePointListComponent.vue';
|
|
||||||
|
|
||||||
|
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
// 在父组件中创建唯一的 Hook 实例,包含所有周边分析相关的状态和方法
|
// 在父组件中创建唯一的 Hook 实例
|
||||||
const analysisButtonState = useAroundButton();
|
const analysisButtonState = useAroundButton();
|
||||||
|
const searchState = useAroundSearch();
|
||||||
|
|
||||||
// 搜索触发的区域分析 Hook(在父组件中创建,确保 watch 不会被销毁)
|
// 通过 provide 共享给所有子组件
|
||||||
const searchAreaState = useAroundSearch();
|
|
||||||
|
|
||||||
// 通过 provide 共享给所有子组件(让 TypeScript 自动推断类型)
|
|
||||||
provide('analysisButtonState', analysisButtonState);
|
provide('analysisButtonState', analysisButtonState);
|
||||||
provide('searchAreaState', searchAreaState);
|
provide('searchState', searchState);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="showAreaDialog" class="area-dialog" :style="{ left: dialogPosition.x + 'px', top: dialogPosition.y + 'px' }">
|
|
||||||
<div class="dialog-header">选择区域</div>
|
|
||||||
<div class="dialog-content">
|
|
||||||
<div class="radius-input-group">
|
|
||||||
<span class="label">半径:</span>
|
|
||||||
<input
|
|
||||||
v-model.number="radius"
|
|
||||||
type="number"
|
|
||||||
class="radius-input"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
/>
|
|
||||||
<span class="unit">公里</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<button class="confirm-btn" @click="handleConfirm">确认添加</button>
|
|
||||||
<button class="cancel-btn" @click="handleCancel">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { inject } from 'vue';//从Vue导入inject函数,用于依赖注入
|
|
||||||
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
|
||||||
|
|
||||||
// 从父组件注入共享状态,明确指定类型
|
|
||||||
const analysisButtonState = inject<AnalysisButtonState>('analysisButtonState');
|
|
||||||
|
|
||||||
const {
|
|
||||||
showAreaDialog,
|
|
||||||
radius,
|
|
||||||
dialogPosition,
|
|
||||||
handleConfirm,
|
|
||||||
handleCancel
|
|
||||||
} = analysisButtonState!;//使用非空断言操作符,告诉TypeScript该值一定存在
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 对话框容器样式 */
|
|
||||||
.area-dialog {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100001;
|
|
||||||
min-width: 200px;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(180deg, rgba(0, 60, 120, 0.95), rgba(0, 40, 80, 0.95));
|
|
||||||
border: 2px solid #00b4ff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 180, 255, 0.3);
|
|
||||||
color: white;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
/* 对话框标题栏样式 */
|
|
||||||
.dialog-header {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(90deg, #00b4ff, #0080cc);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
padding: 10px 6px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radius-input-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
/* 标签和单位文字的样式 */
|
|
||||||
.radius-input-group .label,
|
|
||||||
.radius-input-group .unit {
|
|
||||||
font-size: 14px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radius-input {
|
|
||||||
width: 50px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
background: rgba(0, 100, 180, 0.6);
|
|
||||||
border: 1px solid #00b4ff;
|
|
||||||
border-radius: 3px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
/* 隐藏数字输入框的上下调节按钮(针对WebKit浏览器) */
|
|
||||||
.radius-input::-webkit-inner-spin-button,
|
|
||||||
.radius-input::-webkit-outer-spin-button {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 10px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn,
|
|
||||||
.cancel-btn {
|
|
||||||
padding: 6px 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
background: linear-gradient(180deg, #2d8a4e, #1e6b3a);
|
|
||||||
border: 1px solid #3da862;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:hover {
|
|
||||||
background: linear-gradient(180deg, #3da862, #2d8a4e);
|
|
||||||
box-shadow: 0 2px 8px rgba(45, 138, 78, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background: linear-gradient(180deg, #c0392b, #96281b);
|
|
||||||
border: 1px solid #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn:hover {
|
|
||||||
background: linear-gradient(180deg, #e74c3c, #c0392b);
|
|
||||||
box-shadow: 0 2px 8px rgba(192, 57, 43, 0.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
-595
@@ -1,595 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="showPulsePointList"
|
|
||||||
ref="listRef"
|
|
||||||
class="pulse-point-list"
|
|
||||||
:style="{ right: position.right + 'px', bottom: position.bottom + 'px' }"
|
|
||||||
>
|
|
||||||
<div class="list-header" @mousedown="startDrag">
|
|
||||||
<span>脉冲点列表 ({{ pulsePoints.length }})</span>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="exportToWord"
|
|
||||||
class="export-btn"
|
|
||||||
:icon="Download"
|
|
||||||
>
|
|
||||||
导出
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-box">
|
|
||||||
<el-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索点位..."
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
prefix-icon="Search"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="point-list-content">
|
|
||||||
<el-table
|
|
||||||
:data="filteredPoints"
|
|
||||||
border
|
|
||||||
stripe
|
|
||||||
max-height="300"
|
|
||||||
style="width: 100%"
|
|
||||||
empty-text="暂无数据"
|
|
||||||
>
|
|
||||||
<!-- 名称列 - 悬浮时文字换行展开 -->
|
|
||||||
<el-table-column
|
|
||||||
prop="value"
|
|
||||||
label="名称"
|
|
||||||
width="150"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="name-cell">
|
|
||||||
{{ (row as any).value || '未命名' }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column prop="category" label="类型" width="80" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag size="small" :type="getCategoryType(row.category)">
|
|
||||||
{{ getCategoryName((row as any).category) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column label="坐标" width="135" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="coordinate">
|
|
||||||
{{ (row as any).lon?.toFixed(4) || '-' }}, {{ (row as any).lat?.toFixed(4) || '-' }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed, inject, reactive, watch, nextTick } from 'vue';
|
|
||||||
import { Download } from '@element-plus/icons-vue';
|
|
||||||
import { Document, Packer, Paragraph, Table, TableRow, TableCell, WidthType, AlignmentType, TextRun, HeadingLevel } from 'docx';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
|
||||||
|
|
||||||
const analysisButtonState = inject<AnalysisButtonState>('analysisButtonState');
|
|
||||||
|
|
||||||
const pulsePoints = computed(() => analysisButtonState?.pulsePoints.value || []);
|
|
||||||
const showPulsePointList = computed(() => analysisButtonState?.showPulsePointList.value || false);
|
|
||||||
|
|
||||||
const searchKeyword = ref('');
|
|
||||||
const listRef = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
// 使用 right 和 bottom 定位(距离右下角的偏移量)
|
|
||||||
const position = reactive({ right: 20, bottom: 20 });
|
|
||||||
const isDragging = ref(false);
|
|
||||||
const dragStart = reactive({ x: 0, y: 0 });
|
|
||||||
const initialPosition = reactive({ right: 0, bottom: 0 });
|
|
||||||
const listSize = reactive({ width: 375, height: 420 });
|
|
||||||
|
|
||||||
// 获取列表的实际尺寸
|
|
||||||
const updateListSize = () => {
|
|
||||||
if (listRef.value) {
|
|
||||||
const rect = listRef.value.getBoundingClientRect();
|
|
||||||
listSize.width = rect.width;
|
|
||||||
listSize.height = rect.height;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 边界限制函数
|
|
||||||
const constrainPosition = (right: number, bottom: number) => {
|
|
||||||
updateListSize();
|
|
||||||
|
|
||||||
const actualLeft = window.innerWidth - listSize.width - right;
|
|
||||||
const actualTop = window.innerHeight - listSize.height - bottom;
|
|
||||||
|
|
||||||
let constrainedRight = right;
|
|
||||||
let constrainedBottom = bottom;
|
|
||||||
|
|
||||||
if (actualLeft < 0) {
|
|
||||||
constrainedRight = window.innerWidth - listSize.width;
|
|
||||||
}
|
|
||||||
if (actualLeft > window.innerWidth - listSize.width) {
|
|
||||||
constrainedRight = 0;
|
|
||||||
}
|
|
||||||
if (actualTop < 100) {
|
|
||||||
constrainedBottom = window.innerHeight - listSize.height - 100;
|
|
||||||
}
|
|
||||||
if (actualTop > window.innerHeight - listSize.height) {
|
|
||||||
constrainedBottom = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
constrainedRight = Math.max(0, Math.min(constrainedRight, window.innerWidth - listSize.width));
|
|
||||||
constrainedBottom = Math.max(0, Math.min(constrainedBottom, window.innerHeight - listSize.height));
|
|
||||||
|
|
||||||
return {
|
|
||||||
right: constrainedRight,
|
|
||||||
bottom: constrainedBottom
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置位置到右下角
|
|
||||||
const resetPosition = () => {
|
|
||||||
nextTick(() => {
|
|
||||||
updateListSize();
|
|
||||||
position.right = 20;
|
|
||||||
position.bottom = 20;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听列表显示状态
|
|
||||||
watch(showPulsePointList, async (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
await nextTick();
|
|
||||||
resetPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
watch(() => window.innerWidth, () => {
|
|
||||||
if (showPulsePointList.value) {
|
|
||||||
resetPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => window.innerHeight, () => {
|
|
||||||
if (showPulsePointList.value) {
|
|
||||||
resetPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredPoints = computed(() => {
|
|
||||||
if (!searchKeyword.value) return pulsePoints.value;
|
|
||||||
|
|
||||||
const keyword = searchKeyword.value.toLowerCase();
|
|
||||||
return pulsePoints.value.filter(point =>
|
|
||||||
point.value?.toLowerCase().includes(keyword) ||
|
|
||||||
point.category?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// 导出为 Word 文档
|
|
||||||
const exportToWord = async () => {
|
|
||||||
try {
|
|
||||||
const points = filteredPoints.value;
|
|
||||||
|
|
||||||
// 统计各类别数量
|
|
||||||
const categoryCount: Record<string, number> = {};
|
|
||||||
points.forEach(point => {
|
|
||||||
const category = getCategoryName(point.category);
|
|
||||||
categoryCount[category] = (categoryCount[category] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建表格行数据
|
|
||||||
const tableRows = [
|
|
||||||
new TableRow({
|
|
||||||
children: [
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: '序号', alignment: AlignmentType.CENTER })],
|
|
||||||
width: { size: 8, type: WidthType.PERCENTAGE },
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: '名称', alignment: AlignmentType.CENTER })],
|
|
||||||
width: { size: 35, type: WidthType.PERCENTAGE },
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: '类型', alignment: AlignmentType.CENTER })],
|
|
||||||
width: { size: 15, type: WidthType.PERCENTAGE },
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: '坐标', alignment: AlignmentType.CENTER })],
|
|
||||||
width: { size: 42, type: WidthType.PERCENTAGE },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 添加数据行
|
|
||||||
points.forEach((point, index) => {
|
|
||||||
tableRows.push(
|
|
||||||
new TableRow({
|
|
||||||
children: [
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: String(index + 1), alignment: AlignmentType.CENTER })],
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: point.value || '未命名' })],
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({ text: getCategoryName(point.category), alignment: AlignmentType.CENTER })],
|
|
||||||
}),
|
|
||||||
new TableCell({
|
|
||||||
children: [new Paragraph({
|
|
||||||
text: `${point.lon?.toFixed(4) || '-'}, ${point.lat?.toFixed(4) || '-'}`,
|
|
||||||
alignment: AlignmentType.CENTER
|
|
||||||
})],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建统计数据段落
|
|
||||||
const statsParagraphs = Object.entries(categoryCount).map(([category, count]) => {
|
|
||||||
return new Paragraph({
|
|
||||||
children: [
|
|
||||||
new TextRun({ text: `${category}: `, bold: true }),
|
|
||||||
new TextRun({ text: `${count} 个` }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建文档
|
|
||||||
const doc = new Document({
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
properties: {},
|
|
||||||
children: [
|
|
||||||
// 标题
|
|
||||||
new Paragraph({
|
|
||||||
text: '脉冲点列表导出报告',
|
|
||||||
heading: HeadingLevel.HEADING_1,
|
|
||||||
alignment: AlignmentType.CENTER,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 空行
|
|
||||||
new Paragraph({}),
|
|
||||||
|
|
||||||
// 总数统计
|
|
||||||
new Paragraph({
|
|
||||||
children: [
|
|
||||||
new TextRun({ text: '总点数: ', bold: true }),
|
|
||||||
new TextRun({ text: `${points.length} 个` }),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 空行
|
|
||||||
new Paragraph({}),
|
|
||||||
|
|
||||||
// 分类统计标题
|
|
||||||
new Paragraph({
|
|
||||||
text: '分类统计:',
|
|
||||||
heading: HeadingLevel.HEADING_2,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 分类统计数据
|
|
||||||
...statsParagraphs,
|
|
||||||
|
|
||||||
// 空行
|
|
||||||
new Paragraph({}),
|
|
||||||
|
|
||||||
// 详细数据标题
|
|
||||||
new Paragraph({
|
|
||||||
text: '详细数据:',
|
|
||||||
heading: HeadingLevel.HEADING_2,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 空行
|
|
||||||
new Paragraph({}),
|
|
||||||
|
|
||||||
// 表格
|
|
||||||
new Table({
|
|
||||||
rows: tableRows,
|
|
||||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成并下载文档
|
|
||||||
const blob = await Packer.toBlob(doc);
|
|
||||||
const fileName = `脉冲点列表_${new Date().toISOString().slice(0, 10)}.docx`;
|
|
||||||
saveAs(blob, fileName);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('导出失败:', error);
|
|
||||||
alert('导出失败,请重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 开始拖动
|
|
||||||
const startDrag = (e: MouseEvent) => {
|
|
||||||
// Deleted:if ((e.target as HTMLElement).closest('.close-btn')) return;
|
|
||||||
|
|
||||||
isDragging.value = true;
|
|
||||||
dragStart.x = e.clientX;
|
|
||||||
dragStart.y = e.clientY;
|
|
||||||
initialPosition.right = position.right;
|
|
||||||
initialPosition.bottom = position.bottom;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onDrag);
|
|
||||||
document.addEventListener('mouseup', stopDrag);
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 拖动中
|
|
||||||
const onDrag = (e: MouseEvent) => {
|
|
||||||
if (!isDragging.value) return;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - dragStart.x;
|
|
||||||
const deltaY = e.clientY - dragStart.y;
|
|
||||||
|
|
||||||
updateListSize();
|
|
||||||
|
|
||||||
const currentLeft = window.innerWidth - listSize.width - position.right;
|
|
||||||
const currentTop = window.innerHeight - listSize.height - position.bottom;
|
|
||||||
|
|
||||||
let newLeft = currentLeft + deltaX;
|
|
||||||
let newTop = currentTop + deltaY;
|
|
||||||
|
|
||||||
newLeft = Math.max(0, newLeft);
|
|
||||||
newLeft = Math.min(newLeft, window.innerWidth - listSize.width);
|
|
||||||
newTop = Math.max(100, newTop);
|
|
||||||
newTop = Math.min(newTop, window.innerHeight - listSize.height);
|
|
||||||
|
|
||||||
const newRight = window.innerWidth - listSize.width - newLeft;
|
|
||||||
const newBottom = window.innerHeight - listSize.height - newTop;
|
|
||||||
|
|
||||||
const constrained = constrainPosition(newRight, newBottom);
|
|
||||||
position.right = constrained.right;
|
|
||||||
position.bottom = constrained.bottom;
|
|
||||||
|
|
||||||
dragStart.x = e.clientX;
|
|
||||||
dragStart.y = e.clientY;
|
|
||||||
initialPosition.right = position.right;
|
|
||||||
initialPosition.bottom = position.bottom;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 停止拖动
|
|
||||||
const stopDrag = () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
document.removeEventListener('mousemove', onDrag);
|
|
||||||
document.removeEventListener('mouseup', stopDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCategoryName = (category?: string): string => {
|
|
||||||
const nameMap: Record<string, string> = {
|
|
||||||
'school': '学校',
|
|
||||||
'hospital': '医院',
|
|
||||||
'danger': '危险源',
|
|
||||||
'shelter': '避难所',
|
|
||||||
'fire': '消防站',
|
|
||||||
'store': '储备点',
|
|
||||||
'subway': '地铁站',
|
|
||||||
'hidden-danger': '隐患点',
|
|
||||||
'risk-point': '风险点',
|
|
||||||
'bridge': '桥梁',
|
|
||||||
'reservoir': '水库'
|
|
||||||
};
|
|
||||||
return nameMap[category || ''] || category || '未知';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCategoryType = (category?: string): string => {
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
'school': 'primary',
|
|
||||||
'hospital': 'success',
|
|
||||||
'danger': 'danger',
|
|
||||||
'shelter': 'warning',
|
|
||||||
'fire': 'danger',
|
|
||||||
'store': 'info',
|
|
||||||
'subway': '',
|
|
||||||
'hidden-danger': 'warning',
|
|
||||||
'risk-point': 'danger',
|
|
||||||
'bridge': 'info',
|
|
||||||
'reservoir': 'success'
|
|
||||||
};
|
|
||||||
return typeMap[category || ''] || '';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pulse-point-list {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 100000;
|
|
||||||
background: rgba(14, 52, 98, 0.95);
|
|
||||||
border: 1px solid rgba(0, 225, 255, 0.5);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
width: auto;
|
|
||||||
min-width: 350px;
|
|
||||||
max-width: 450px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: calc(100vh - 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid rgba(0, 225, 255, 0.3);
|
|
||||||
cursor: move;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header span {
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.export-btn {
|
|
||||||
background-color: rgba(0, 225, 255, 0.3);
|
|
||||||
border-color: rgba(0, 225, 255, 0.5);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-btn:hover {
|
|
||||||
background-color: rgba(0, 225, 255, 0.5);
|
|
||||||
border-color: rgba(0, 225, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-list-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coordinate {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
white-space: nowrap;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格样式 */
|
|
||||||
:deep(.el-table) {
|
|
||||||
background-color: transparent;
|
|
||||||
--el-table-tr-bg-color: transparent;
|
|
||||||
--el-table-header-bg-color: rgba(86, 204, 242, 0.3);
|
|
||||||
--el-table-row-hover-bg-color: rgba(0, 225, 255, 0.2);
|
|
||||||
--el-table-border-color: rgba(255, 255, 255, 0.2);
|
|
||||||
--el-table-text-color: white;
|
|
||||||
--el-table-header-text-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table th.el-table__cell) {
|
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgb(86, 204, 242) 0%,
|
|
||||||
rgb(47, 128, 237) 100%
|
|
||||||
);
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 8px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table td.el-table__cell) {
|
|
||||||
background-color: rgba(15, 61, 118, 0.4);
|
|
||||||
color: white;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
padding: 6px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
|
||||||
background-color: rgba(15, 61, 118, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__body tr:hover) > td {
|
|
||||||
background-color: rgba(0, 225, 255, 0.15) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* 单元格内容样式 */
|
|
||||||
:deep(.el-table .cell) {
|
|
||||||
padding-left: 8px !important;
|
|
||||||
padding-right: 8px !important;
|
|
||||||
overflow: visible;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
background-color: rgba(15, 61, 118, 0.6);
|
|
||||||
border: 1px solid rgba(0, 225, 255, 0.3);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__wrapper.is-focus) {
|
|
||||||
box-shadow: 0 0 0 1px rgba(0, 225, 255, 0.8) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动条样式 */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(0, 225, 255, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(0, 225, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: rgba(15, 61, 118, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏外层容器的滚动条 */
|
|
||||||
.pulse-point-list::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格内部滚动条样式 */
|
|
||||||
.point-list-content::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-list-content::-webkit-scrollbar-thumb {
|
|
||||||
background: #888;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-list-content::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-list-content::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 名称单元格样式 - 默认截断,悬浮时换行展开 */
|
|
||||||
.name-cell {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 悬浮时展开文字,换行显示完整内容 */
|
|
||||||
.name-cell:hover {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 强制表格头部文字居中 */
|
|
||||||
:deep(.el-table th.el-table__cell .cell) {
|
|
||||||
justify-content: center !important;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -22,50 +22,36 @@
|
|||||||
</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 { inject, onMounted } from 'vue';
|
import { inject, onMounted } from 'vue';
|
||||||
import { Edit } from '@element-plus/icons-vue';
|
import { Edit } from '@element-plus/icons-vue';
|
||||||
import type { SearchAreaAnalysisState } from '@/types/common/useAroundAnalysisType';
|
import type { PointResource } from '@/types/common/useAroundAnalysisType';
|
||||||
|
import type { AnalysisButtonState } from '@/types/common/useAroundAnalysisType';
|
||||||
|
|
||||||
// 从父组件注入搜索区域分析状态
|
// 从父组件注入搜索状态和按钮状态
|
||||||
const searchAreaState = inject<SearchAreaAnalysisState>('searchAreaState');
|
const searchState = inject<ReturnType<typeof import('@/hooks/rain-earthquake/useAroundSearch').useAroundSearch>>('searchState');
|
||||||
|
const buttonState = inject<AnalysisButtonState>('analysisButtonState');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
canSearch,
|
canSearch,
|
||||||
querySearch,
|
querySearch,
|
||||||
handleSelect,
|
handleSelect: baseHandleSelect,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
} = searchAreaState!;
|
} = searchState!;
|
||||||
|
|
||||||
|
// 包装 handleSelect,在搜索选择后触发区域分析
|
||||||
|
const handleSelect = async (item: PointResource) => {
|
||||||
|
await baseHandleSelect(item);
|
||||||
|
// 飞行完成后,触发区域分析
|
||||||
|
buttonState?.startAreaAnalysisFromSearch(item);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 加载数据
|
// 加载数据
|
||||||
if (searchAreaState) {
|
if (searchState) {
|
||||||
// 通过调用 handleFocus 来加载数据
|
|
||||||
handleFocus();
|
handleFocus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -130,106 +116,4 @@ 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>
|
||||||
@@ -327,6 +327,43 @@ export const useAroundButton = (): AnalysisButtonState => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从搜索结果启动区域分析
|
||||||
|
* 自动激活按钮(变为"取消区域分析"),显示区域选择对话框
|
||||||
|
* @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(
|
watch(
|
||||||
@@ -411,5 +448,6 @@ export const useAroundButton = (): AnalysisButtonState => {
|
|||||||
refreshPulseEffect,
|
refreshPulseEffect,
|
||||||
pulsePoints,
|
pulsePoints,
|
||||||
showPulsePointList,
|
showPulsePointList,
|
||||||
|
startAreaAnalysisFromSearch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
||||||
import { useStatusStore } from '@/stores/useStatusStore';
|
import { useStatusStore } from '@/stores/useStatusStore';
|
||||||
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
||||||
import { LoadingResource } from '@/types/common/LoadingResourceType';
|
import { LoadingResource } from '@/types/common/LoadingResourceType';
|
||||||
import type { PointResource, ResourceConfig } from '@/types/common/useAroundAnalysisType';
|
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 搜索相关的状态和方法
|
* @returns 搜索相关的状态和方法
|
||||||
*/
|
*/
|
||||||
export const useAroundSearch = () => {
|
export const useAroundSearch = () => {
|
||||||
@@ -21,21 +17,6 @@ export const useAroundSearch = () => {
|
|||||||
const map = computed(() => statusStore.mapLayers);
|
const map = computed(() => statusStore.mapLayers);
|
||||||
const infra = computed(() => statusStore.infrastructureLayers);
|
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();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源配置列表
|
* 资源配置列表
|
||||||
*/
|
*/
|
||||||
@@ -102,187 +83,19 @@ export const useAroundSearch = () => {
|
|||||||
/**
|
/**
|
||||||
* 选择建议回调
|
* 选择建议回调
|
||||||
* @param item - 选中的点资源
|
* @param item - 选中的点资源
|
||||||
|
* @param onAnalysisStart - 搜索后触发区域分析的回调(由父组件传入)
|
||||||
*/
|
*/
|
||||||
async function handleSelect(item: PointResource) {
|
async function handleSelect(item: PointResource, onAnalysisStart?: (point: PointResource) => void) {
|
||||||
if (item.lon == null || item.lat == null) return;
|
if (item.lon == null || item.lat == null) return;
|
||||||
|
|
||||||
await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
||||||
startAreaAnalysis(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 飞行完成后,触发区域分析回调
|
||||||
* 开始区域分析
|
if (onAnalysisStart) {
|
||||||
* @param centerPoint - 中心点资源
|
onAnalysisStart(item);
|
||||||
*/
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理聚焦事件,重新加载数据
|
* 处理聚焦事件,重新加载数据
|
||||||
*/
|
*/
|
||||||
@@ -342,16 +155,5 @@ export const useAroundSearch = () => {
|
|||||||
handleSelect,
|
handleSelect,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
loadAllPointData,
|
loadAllPointData,
|
||||||
// 区域分析相关
|
|
||||||
showAreaDialog,
|
|
||||||
areaRadius,
|
|
||||||
dialogPosition,
|
|
||||||
isAreaAnalysisActive,
|
|
||||||
showPulsePointListFromSearch,
|
|
||||||
pulsePointsFromSearch,
|
|
||||||
handleAreaConfirm,
|
|
||||||
handleAreaCancel,
|
|
||||||
refreshSearchPulseEffect,
|
|
||||||
clearSearchAreaAnalysis,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,8 @@ export interface AnalysisButtonState {
|
|||||||
pulsePoints: Ref<PointResource[]>;
|
pulsePoints: Ref<PointResource[]>;
|
||||||
/** 是否显示脉冲点列表 */
|
/** 是否显示脉冲点列表 */
|
||||||
showPulsePointList: Ref<boolean>;
|
showPulsePointList: Ref<boolean>;
|
||||||
|
/** 从搜索结果启动区域分析 */
|
||||||
|
startAreaAnalysisFromSearch: (point: PointResource) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user