cquavui/src/views/cesiums/ModelControlPanel.vue
2025-10-11 16:10:41 +08:00

386 lines
8.5 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- src/components/ModelControlPanel.vue -->
<template>
<div class="model-control-panel">
<div class="panel-header">
<h3>🏗 3D 模型管理</h3>
</div>
<div class="panel-section">
<h4>快速添加</h4>
<div class="quick-add-buttons">
<button
v-for="model in predefinedModels"
:key="model.type"
class="model-btn"
@click="addModelAtCenter(model.type)"
:disabled="isModelLoading"
>
{{ model.name }}
</button>
<button
class="landmark-btn"
@click="loadLandmarks"
:disabled="isModelLoading"
>
🏛 加载地标
</button>
</div>
</div>
<div class="panel-section" v-if="models.size > 0">
<h4>模型列表 ({{ models.size }})</h4>
<div class="model-list">
<div
v-for="[id, model] in models"
:key="id"
:class="['model-item', { active: selectedModel === id }]"
@click="selectModel(id)"
>
<div class="model-info">
<span class="model-name">{{ id }}</span>
<span class="model-position">
{{ model.position.longitude.toFixed(4) }},
{{ model.position.latitude.toFixed(4) }}
</span>
</div>
<div class="model-actions">
<button @click.stop="flyToModel(id)" title="飞向模型"></button>
<button @click.stop="removeModel(id)" title="删除模型">🗑</button>
</div>
</div>
</div>
</div>
<div class="panel-section" v-if="selectedModel">
<h4>模型控制</h4>
<div class="model-controls">
<div class="control-group">
<label>缩放:</label>
<input
type="range"
min="0.1"
max="5"
step="0.1"
:value="getSelectedModelScale()"
@input="updateSelectedModelScale(parseFloat(($event.target as HTMLInputElement).value))"
/>
<span>{{ getSelectedModelScale() }}x</span>
</div>
<div class="control-group">
<label>经度:</label>
<input
type="number"
step="0.0001"
:value="getSelectedModelPosition().longitude"
@input="updateSelectedModelPosition('longitude', parseFloat(($event.target as HTMLInputElement).value))"
/>
</div>
<div class="control-group">
<label>纬度:</label>
<input
type="number"
step="0.0001"
:value="getSelectedModelPosition().latitude"
@input="updateSelectedModelPosition('latitude', parseFloat(($event.target as HTMLInputElement).value))"
/>
</div>
<div class="control-group">
<label>高度:</label>
<input
type="number"
step="1"
:value="getSelectedModelPosition().height"
@input="updateSelectedModelPosition('height', parseFloat(($event.target as HTMLInputElement).value))"
/>
</div>
<button class="clear-btn" @click="clearAllModels">
🗑 清除所有模型
</button>
</div>
</div>
<div v-if="isModelLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>模型加载中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
models: Map<string, any>;
selectedModel: string | null;
isModelLoading: boolean;
predefinedModels: any;
}>();
const emit = defineEmits<{
'load-landmarks': [];
'add-model': [type: string, position: any];
'select-model': [id: string];
'deselect-model': [];
'update-scale': [id: string, scale: number];
'update-position': [id: string, position: any];
'remove-model': [id: string];
'fly-to-model': [id: string];
'clear-models': [];
}>();
const loadLandmarks = () => {
emit('load-landmarks');
};
const addModelAtCenter = (modelType: string) => {
// 在重庆中心位置添加模型
const position = {
longitude: 106.5516,
latitude: 29.5630,
height: 50
};
emit('add-model', modelType, position);
};
const selectModel = (id: string) => {
if (props.selectedModel === id) {
emit('deselect-model');
} else {
emit('select-model', id);
}
};
const flyToModel = (id: string) => {
emit('fly-to-model', id);
};
const removeModel = (id: string) => {
emit('remove-model', id);
};
const clearAllModels = () => {
emit('clear-models');
};
const getSelectedModelScale = (): number => {
if (!props.selectedModel) return 1;
const model = props.models.get(props.selectedModel);
return model?.options?.scale || 1;
};
const getSelectedModelPosition = () => {
if (!props.selectedModel) return { longitude: 0, latitude: 0, height: 0 };
const model = props.models.get(props.selectedModel);
return model?.position || { longitude: 0, latitude: 0, height: 0 };
};
const updateSelectedModelScale = (scale: number) => {
if (props.selectedModel) {
emit('update-scale', props.selectedModel, scale);
}
};
const updateSelectedModelPosition = (field: string, value: number) => {
if (props.selectedModel) {
const currentPosition = getSelectedModelPosition();
const newPosition = { ...currentPosition, [field]: value };
emit('update-position', props.selectedModel, newPosition);
}
};
</script>
<style scoped>
.model-control-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(42, 42, 42, 0.95);
color: white;
padding: 15px;
border-radius: 10px;
min-width: 300px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-header h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: #fff;
}
.panel-section {
margin-bottom: 20px;
}
.panel-section h4 {
margin: 0 0 10px 0;
font-size: 14px;
color: #ccc;
}
.quick-add-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.model-btn, .landmark-btn {
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.model-btn:hover, .landmark-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.model-btn:disabled, .landmark-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.model-list {
max-height: 200px;
overflow-y: auto;
}
.model-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
margin-bottom: 5px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.model-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.model-item.active {
background: rgba(24, 144, 255, 0.3);
border: 1px solid #1890ff;
}
.model-info {
flex: 1;
}
.model-name {
display: block;
font-size: 12px;
font-weight: bold;
}
.model-position {
display: block;
font-size: 10px;
color: #ccc;
}
.model-actions {
display: flex;
gap: 5px;
}
.model-actions button {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 4px;
border-radius: 3px;
font-size: 10px;
}
.model-actions button:hover {
background: rgba(255, 255, 255, 0.2);
}
.control-group {
display: flex;
align-items: center;
margin-bottom: 8px;
gap: 8px;
}
.control-group label {
font-size: 12px;
min-width: 50px;
color: #ccc;
}
.control-group input {
flex: 1;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: white;
font-size: 12px;
}
.control-group span {
font-size: 12px;
min-width: 30px;
}
.clear-btn {
width: 100%;
padding: 8px;
background: rgba(255, 77, 79, 0.2);
border: 1px solid rgba(255, 77, 79, 0.4);
border-radius: 6px;
color: white;
cursor: pointer;
margin-top: 10px;
}
.clear-btn:hover {
background: rgba(255, 77, 79, 0.3);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 10px;
}
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>