refactor: 重构卫星选择器为独立组件,优化代码结构和性能
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive, watch } from 'vue';
|
||||||
|
|
||||||
const layoutConfig = reactive({
|
const layoutConfig = reactive({
|
||||||
darkTheme: false,
|
darkTheme: false,
|
||||||
@ -8,17 +8,31 @@ const layoutConfig = reactive({
|
|||||||
surface: null,
|
surface: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 从 localStorage 读取初始状态,如果没有则使用默认值 false
|
||||||
|
const getStoredMenuState = (): boolean => {
|
||||||
|
const stored = localStorage.getItem('staticMenuDesktopInactive');
|
||||||
|
return stored ? JSON.parse(stored) : false;
|
||||||
|
};
|
||||||
|
|
||||||
const layoutState = reactive({
|
const layoutState = reactive({
|
||||||
activeMenuItem: null,
|
activeMenuItem: null,
|
||||||
configSidebarVisible: false,
|
configSidebarVisible: false,
|
||||||
menuHoverActive: false,
|
menuHoverActive: false,
|
||||||
overlayMenuActive: false,
|
overlayMenuActive: false,
|
||||||
profileSidebarVisible: false,
|
profileSidebarVisible: false,
|
||||||
staticMenuDesktopInactive: false,
|
staticMenuDesktopInactive: getStoredMenuState(),
|
||||||
staticMenuMobileActive: false,
|
staticMenuMobileActive: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useLayout() {
|
export function useLayout() {
|
||||||
|
// 监听 staticMenuDesktopInactive 的变化并保存到 localStorage
|
||||||
|
watch(
|
||||||
|
() => layoutState.staticMenuDesktopInactive,
|
||||||
|
(newValue) => {
|
||||||
|
localStorage.setItem('staticMenuDesktopInactive', JSON.stringify(newValue));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const setActiveMenuItem = (item: Record<string, never>) => {
|
const setActiveMenuItem = (item: Record<string, never>) => {
|
||||||
layoutState.activeMenuItem = item.value || item;
|
layoutState.activeMenuItem = item.value || item;
|
||||||
};
|
};
|
||||||
|
186
src/pages/Satellite/Cesium/components/SatelliteSelector.vue
Normal file
186
src/pages/Satellite/Cesium/components/SatelliteSelector.vue
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Entity, Viewer } from 'cesium';
|
||||||
|
|
||||||
|
import { FilterOutlined, SearchOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { computed, onBeforeUnmount, reactive, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import SatelliteEntity from '../cesium-helper/SatelliteEntity';
|
||||||
|
|
||||||
|
const { tleList, viewer } = defineProps<{
|
||||||
|
tleList: string[];
|
||||||
|
viewer: null | Viewer;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 存储卫星实体和选中状态
|
||||||
|
interface SatelliteItem {
|
||||||
|
entity: Entity | null; // 存储添加到viewer的实体引用
|
||||||
|
name: string;
|
||||||
|
selected: boolean;
|
||||||
|
tle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化卫星数据
|
||||||
|
const satellites = ref<SatelliteItem[]>(
|
||||||
|
tleList.map((tle) => {
|
||||||
|
const name = tle.split('\n')[0].trim();
|
||||||
|
return {
|
||||||
|
entity: null,
|
||||||
|
name,
|
||||||
|
selected: false,
|
||||||
|
tle,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
checkAll: false,
|
||||||
|
indeterminate: false,
|
||||||
|
searchText: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const selectedCount = computed(() => satellites.value.filter((s) => s.selected).length);
|
||||||
|
const totalCount = computed(() => satellites.value.length);
|
||||||
|
|
||||||
|
// 过滤后的卫星列表 - 添加记忆化以提高性能
|
||||||
|
const filteredSatellites = computed(() => {
|
||||||
|
if (!state.searchText) return satellites.value;
|
||||||
|
|
||||||
|
const searchLower = state.searchText.toLowerCase();
|
||||||
|
return satellites.value.filter((s) => s.name.toLowerCase().includes(searchLower));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听选中状态变化,更新全选和半选状态
|
||||||
|
watchEffect(() => {
|
||||||
|
const count = selectedCount.value;
|
||||||
|
const filteredCount = filteredSatellites.value.length;
|
||||||
|
state.indeterminate = count > 0 && count < filteredCount;
|
||||||
|
state.checkAll = count === filteredCount && filteredCount > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换卫星选中状态
|
||||||
|
const toggleSatellite = (satellite: SatelliteItem) => {
|
||||||
|
satellite.selected = !satellite.selected;
|
||||||
|
updateSatelliteEntity(satellite);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新卫星实体(添加或移除)
|
||||||
|
const updateSatelliteEntity = (satellite: SatelliteItem) => {
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
|
if (satellite.selected) {
|
||||||
|
// 添加卫星到viewer
|
||||||
|
try {
|
||||||
|
const satelliteObj = new SatelliteEntity(satellite.tle);
|
||||||
|
const cesiumSateEntity = satelliteObj.createSatelliteEntity();
|
||||||
|
const result = viewer.entities.add(cesiumSateEntity);
|
||||||
|
satellite.entity = result; // 保存实体引用以便后续移除
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`添加卫星 ${satellite.name} 失败:`, error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 从viewer中移除卫星
|
||||||
|
if (satellite.entity) {
|
||||||
|
viewer.entities.remove(satellite.entity);
|
||||||
|
satellite.entity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选/取消全选
|
||||||
|
const onCheckAllChange = (e: { target: { checked: boolean } }) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
|
||||||
|
filteredSatellites.value.forEach((satellite) => {
|
||||||
|
if (satellite.selected !== checked) {
|
||||||
|
satellite.selected = checked;
|
||||||
|
updateSatelliteEntity(satellite);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理所有卫星实体
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (viewer) {
|
||||||
|
// 移除所有实体
|
||||||
|
satellites.value.forEach((satellite) => {
|
||||||
|
if (satellite.entity) {
|
||||||
|
viewer?.entities.remove(satellite.entity);
|
||||||
|
satellite.entity = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="absolute top-0 left-0 p-4">
|
||||||
|
<APopover placement="bottomLeft" trigger="click" :overlayStyle="{ width: '320px' }">
|
||||||
|
<template #title>
|
||||||
|
选择卫星
|
||||||
|
<span class="font-normal text-xs text-gray-500">{{ `(已选择: ${selectedCount}/${totalCount})` }}</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="satellite-selector">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<AInput v-model:value="state.searchText" placeholder="搜索卫星" allowClear>
|
||||||
|
<template #prefix><SearchOutlined /></template>
|
||||||
|
</AInput>
|
||||||
|
|
||||||
|
<ADivider class="my-3" />
|
||||||
|
|
||||||
|
<!-- 无结果提示 -->
|
||||||
|
<div v-if="filteredSatellites.length === 0" class="text-center text-gray-500 py-4">没有找到匹配的卫星</div>
|
||||||
|
|
||||||
|
<!-- 全选选项 -->
|
||||||
|
<ACheckbox
|
||||||
|
v-if="filteredSatellites.length > 0"
|
||||||
|
:checked="state.checkAll"
|
||||||
|
:indeterminate="state.indeterminate"
|
||||||
|
@change="onCheckAllChange"
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</ACheckbox>
|
||||||
|
|
||||||
|
<!-- 卫星列表 -->
|
||||||
|
<div class="satellite-list max-h-60 overflow-y-auto pr-1">
|
||||||
|
<ACheckbox
|
||||||
|
v-for="satellite in filteredSatellites"
|
||||||
|
:key="satellite.tle"
|
||||||
|
:checked="satellite.selected"
|
||||||
|
@change="() => toggleSatellite(satellite)"
|
||||||
|
class="flex my-2 w-full"
|
||||||
|
>
|
||||||
|
{{ satellite.name }}
|
||||||
|
</ACheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 触发按钮 -->
|
||||||
|
<AButton>
|
||||||
|
<template #icon><FilterOutlined /></template>
|
||||||
|
<span v-if="selectedCount > 0" class="ml-1">({{ selectedCount }})</span>
|
||||||
|
</AButton>
|
||||||
|
</APopover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.satellite-list {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-list::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-selector {
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,61 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Entity, Viewer } from 'cesium';
|
import type { Viewer } from 'cesium';
|
||||||
|
|
||||||
import { cesium_init } from './cesium-helper/00.cesium-init';
|
import { cesium_init } from './cesium-helper/00.cesium-init';
|
||||||
import { TLE_LIST } from './cesium-helper/_TLE_DATA';
|
import { TLE_LIST } from './cesium-helper/_TLE_DATA';
|
||||||
import SatelliteEntity from './cesium-helper/SatelliteEntity';
|
import SatelliteSelector from './components/SatelliteSelector.vue';
|
||||||
|
|
||||||
// 存储卫星实体和选中状态
|
|
||||||
interface SatelliteItem {
|
|
||||||
entity: Entity | null; // 存储添加到viewer的实体引用
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
selected: boolean;
|
|
||||||
tle: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化卫星数据
|
|
||||||
const satellites = ref<SatelliteItem[]>(
|
|
||||||
TLE_LIST.map((tle) => {
|
|
||||||
const name = tle.split('\n')[0].trim();
|
|
||||||
return {
|
|
||||||
entity: null,
|
|
||||||
id: name,
|
|
||||||
name,
|
|
||||||
selected: false,
|
|
||||||
tle,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
checkAll: false,
|
|
||||||
indeterminate: false,
|
|
||||||
searchText: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const selectedCount = computed(() => satellites.value.filter((s) => s.selected).length);
|
|
||||||
const totalCount = computed(() => satellites.value.length);
|
|
||||||
|
|
||||||
// 过滤后的卫星列表 - 添加记忆化以提高性能
|
|
||||||
const filteredSatellites = computed(() => {
|
|
||||||
if (!state.searchText) return satellites.value;
|
|
||||||
|
|
||||||
const searchLower = state.searchText.toLowerCase();
|
|
||||||
return satellites.value.filter((s) => s.name.toLowerCase().includes(searchLower));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听选中状态变化,更新全选和半选状态
|
|
||||||
watchEffect(() => {
|
|
||||||
const count = selectedCount.value;
|
|
||||||
const filteredCount = filteredSatellites.value.length;
|
|
||||||
state.indeterminate = count > 0 && count < filteredCount;
|
|
||||||
state.checkAll = count === filteredCount && filteredCount > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cesium相关
|
// Cesium相关
|
||||||
let viewer: null | Viewer = null;
|
let viewer: null | Viewer = $ref(null);
|
||||||
|
|
||||||
// 初始化Cesium
|
// 初始化Cesium
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -73,114 +24,16 @@ onMounted(() => {
|
|||||||
// 清理资源
|
// 清理资源
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (viewer) {
|
if (viewer) {
|
||||||
// 移除所有实体
|
|
||||||
satellites.value.forEach((satellite) => {
|
|
||||||
if (satellite.entity) {
|
|
||||||
viewer?.entities.remove(satellite.entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 销毁viewer
|
// 销毁viewer
|
||||||
viewer.destroy();
|
viewer.destroy();
|
||||||
viewer = null;
|
viewer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换卫星选中状态
|
|
||||||
const toggleSatellite = (satellite: SatelliteItem) => {
|
|
||||||
satellite.selected = !satellite.selected;
|
|
||||||
updateSatelliteEntity(satellite);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新卫星实体(添加或移除)
|
|
||||||
const updateSatelliteEntity = (satellite: SatelliteItem) => {
|
|
||||||
if (!viewer) return;
|
|
||||||
|
|
||||||
if (satellite.selected) {
|
|
||||||
// 添加卫星到viewer
|
|
||||||
try {
|
|
||||||
const satelliteObj = new SatelliteEntity(satellite.tle);
|
|
||||||
const cesiumSateEntity = satelliteObj.createSatelliteEntity();
|
|
||||||
const result = viewer.entities.add(cesiumSateEntity);
|
|
||||||
satellite.entity = result; // 保存实体引用以便后续移除
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`添加卫星 ${satellite.name} 失败:`, error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 从viewer中移除卫星
|
|
||||||
if (satellite.entity) {
|
|
||||||
viewer.entities.remove(satellite.entity);
|
|
||||||
satellite.entity = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 全选/取消全选
|
|
||||||
const onCheckAllChange = (e: { target: { checked: boolean } }) => {
|
|
||||||
const checked = e.target.checked;
|
|
||||||
|
|
||||||
filteredSatellites.value.forEach((satellite) => {
|
|
||||||
if (satellite.selected !== checked) {
|
|
||||||
satellite.selected = checked;
|
|
||||||
updateSatelliteEntity(satellite);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="cesiumContainer" class="z-0">
|
<div id="cesiumContainer" class="z-0">
|
||||||
<div class="absolute top-0 left-0 p-4 z-10">
|
<SatelliteSelector class="z-1" :viewer="viewer" :tle-list="TLE_LIST" />
|
||||||
<APopover placement="bottomLeft" trigger="click" :overlayStyle="{ width: '320px' }">
|
|
||||||
<template #title>
|
|
||||||
选择卫星
|
|
||||||
<span class="font-normal text-xs text-gray-500">{{ `(已选择: ${selectedCount}/${totalCount})` }}</span>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="satellite-selector">
|
|
||||||
<!-- 搜索框 -->
|
|
||||||
<AInput v-model:value="state.searchText" placeholder="搜索卫星" allowClear>
|
|
||||||
<template #prefix><SearchOutlined /></template>
|
|
||||||
</AInput>
|
|
||||||
|
|
||||||
<ADivider class="my-3" />
|
|
||||||
|
|
||||||
<!-- 无结果提示 -->
|
|
||||||
<div v-if="filteredSatellites.length === 0" class="text-center text-gray-500 py-4">没有找到匹配的卫星</div>
|
|
||||||
|
|
||||||
<!-- 全选选项 -->
|
|
||||||
<ACheckbox
|
|
||||||
v-if="filteredSatellites.length > 0"
|
|
||||||
:checked="state.checkAll"
|
|
||||||
:indeterminate="state.indeterminate"
|
|
||||||
@change="onCheckAllChange"
|
|
||||||
>
|
|
||||||
全选
|
|
||||||
</ACheckbox>
|
|
||||||
|
|
||||||
<!-- 卫星列表 -->
|
|
||||||
<div class="satellite-list max-h-60 overflow-y-auto pr-1">
|
|
||||||
<ACheckbox
|
|
||||||
v-for="satellite in filteredSatellites"
|
|
||||||
:key="satellite.id"
|
|
||||||
:checked="satellite.selected"
|
|
||||||
@change="() => toggleSatellite(satellite)"
|
|
||||||
class="flex my-2 w-full"
|
|
||||||
>
|
|
||||||
{{ satellite.name }}
|
|
||||||
</ACheckbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 触发按钮 -->
|
|
||||||
<AButton>
|
|
||||||
<template #icon><FilterOutlined /></template>
|
|
||||||
<!-- <span>卫星筛选</span> -->
|
|
||||||
<span v-if="selectedCount > 0" class="ml-1">({{ selectedCount }})</span>
|
|
||||||
</AButton>
|
|
||||||
</APopover>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -188,21 +41,4 @@ const onCheckAllChange = (e: { target: { checked: boolean } }) => {
|
|||||||
#cesiumContainer {
|
#cesiumContainer {
|
||||||
@apply absolute top-0 left-0 right-0 bottom-0;
|
@apply absolute top-0 left-0 right-0 bottom-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.satellite-list {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.satellite-list::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.satellite-list::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.satellite-selector {
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -39,10 +39,11 @@ export function Plugins() {
|
|||||||
const plugins: PluginOption[] = [];
|
const plugins: PluginOption[] = [];
|
||||||
|
|
||||||
plugins.push(
|
plugins.push(
|
||||||
|
// https://vue-macros.dev/zh-CN/guide/bundler-integration.html
|
||||||
VueMacros({
|
VueMacros({
|
||||||
plugins: {
|
plugins: {
|
||||||
vue: Vue({ include: [/\.vue$/, /\.md$/] }),
|
vue: Vue({ include: [/\.vue$/, /\.md$/] }),
|
||||||
vueJsx: VueJsx(), // 如有需要
|
vueJsx: VueJsx(),
|
||||||
// https://uvr.esm.is/guide/configuration.html
|
// https://uvr.esm.is/guide/configuration.html
|
||||||
// https://github.com/posva/unplugin-vue-router
|
// https://github.com/posva/unplugin-vue-router
|
||||||
vueRouter: VueRouter({
|
vueRouter: VueRouter({
|
||||||
@ -53,7 +54,7 @@ export function Plugins() {
|
|||||||
routesFolder: 'src/pages',
|
routesFolder: 'src/pages',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}), // https://vue-macros.dev/zh-CN/guide/bundler-integration.html
|
}),
|
||||||
|
|
||||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts?tab=readme-ov-file#configuration
|
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts?tab=readme-ov-file#configuration
|
||||||
// Layouts({ defaultLayout: 'sakai-vue/AppLayout', pagesDirs: [] }),
|
// Layouts({ defaultLayout: 'sakai-vue/AppLayout', pagesDirs: [] }),
|
||||||
|
Reference in New Issue
Block a user