feat: 添加视角跟随状态管理功能,集成 Pinia 以优化视角跟随逻辑,新增视角跟随通知组件,提升用户交互体验

This commit is contained in:
xudan 2025-09-12 14:04:20 +08:00
parent 02654b29e0
commit 5cdbcebcd3
10 changed files with 444 additions and 131 deletions

View File

@ -16,6 +16,7 @@
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"lodash-es": "^4.17.21",
"pinia": "^3.0.3",
"rxjs": "^7.8.2",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",

94
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.7.3)(vue@3.5.20(typescript@5.7.3))
rxjs:
specifier: ^7.8.2
version: 7.8.2
@ -759,6 +762,15 @@ packages:
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.7':
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
'@vue/devtools-kit@7.7.7':
resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/language-core@2.2.12':
resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==}
peerDependencies:
@ -879,6 +891,9 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
birpc@2.5.0:
resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -963,6 +978,10 @@ packages:
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
copy-anything@3.0.5:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
core-js@3.45.1:
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
@ -1318,6 +1337,9 @@ packages:
help-me@3.0.0:
resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
hookified@1.12.0:
resolution: {integrity: sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==}
@ -1391,6 +1413,10 @@ packages:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -1526,6 +1552,9 @@ packages:
mitt@2.1.0:
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@ -1612,6 +1641,9 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -1623,6 +1655,15 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pinia@3.0.3:
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -1913,6 +1954,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
speakingurl@14.0.1:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
split2@3.2.2:
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
@ -2010,6 +2055,10 @@ packages:
stylis@4.3.6:
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
superjson@2.2.2:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -2782,6 +2831,24 @@ snapshots:
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.7':
dependencies:
'@vue/devtools-kit': 7.7.7
'@vue/devtools-kit@7.7.7':
dependencies:
'@vue/devtools-shared': 7.7.7
birpc: 2.5.0
hookable: 5.5.3
mitt: 3.0.1
perfect-debounce: 1.0.0
speakingurl: 14.0.1
superjson: 2.2.2
'@vue/devtools-shared@7.7.7':
dependencies:
rfdc: 1.4.1
'@vue/language-core@2.2.12(typescript@5.7.3)':
dependencies:
'@volar/language-core': 2.4.15
@ -2921,6 +2988,8 @@ snapshots:
binary-extensions@2.3.0: {}
birpc@2.5.0: {}
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -3019,6 +3088,10 @@ snapshots:
confbox@0.2.2: {}
copy-anything@3.0.5:
dependencies:
is-what: 4.1.16
core-js@3.45.1: {}
cosmiconfig@9.0.0(typescript@5.7.3):
@ -3409,6 +3482,8 @@ snapshots:
glob: 7.2.3
readable-stream: 3.6.2
hookable@5.5.3: {}
hookified@1.12.0: {}
html-tags@3.3.1: {}
@ -3464,6 +3539,8 @@ snapshots:
is-plain-object@5.0.0: {}
is-what@4.1.16: {}
isexe@2.0.0: {}
js-sdsl@4.3.0: {}
@ -3574,6 +3651,8 @@ snapshots:
mitt@2.1.0: {}
mitt@3.0.1: {}
mlly@1.8.0:
dependencies:
acorn: 8.15.0
@ -3683,12 +3762,21 @@ snapshots:
pathe@2.0.3: {}
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {}
pinia@3.0.3(typescript@5.7.3)(vue@3.5.20(typescript@5.7.3)):
dependencies:
'@vue/devtools-api': 7.7.7
vue: 3.5.20(typescript@5.7.3)
optionalDependencies:
typescript: 5.7.3
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
@ -3951,6 +4039,8 @@ snapshots:
source-map-js@1.2.1: {}
speakingurl@14.0.1: {}
split2@3.2.2:
dependencies:
readable-stream: 3.6.2
@ -4085,6 +4175,10 @@ snapshots:
stylis@4.3.6: {}
superjson@2.2.2:
dependencies:
copy-anything: 3.0.5
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0

View File

@ -101,12 +101,16 @@
<!-- 视角控制 -->
<div class="action-group">
<div class="action-group-title">视角控制</div>
<div v-if="!isFollowing" class="action-item" @click="handleRobotAction('follow_view', '视角跟随')">
<div
v-if="!followStore.isFollowing"
class="action-item"
@click="handleRobotAction('follow_view', '视角跟随')"
>
<span class="action-icon">👁</span>
<span>视角跟随</span>
</div>
<div
v-if="isFollowing"
v-if="followStore.isFollowing"
class="action-item follow-active"
@click="handleRobotAction('stop_follow_view', '停止跟随')"
>
@ -149,13 +153,11 @@ import type { RobotInfo } from '../../apis/robot';
import type { RobotAction } from '../../services/context-menu/robot-menu.service';
import {
executeRobotAction,
getGlobalFollowState,
getRobotStatusColor,
getRobotStatusText,
startGlobalFollow,
stopGlobalFollow,
} from '../../services/context-menu/robot-menu.service';
import { editorStore } from '../../stores/editor.store';
import { useFollowViewStore } from '../../stores/follow-view.store';
import RobotImageSettingsModal from '../modal/robot-image-settings-modal.vue';
interface Props {
@ -181,11 +183,8 @@ const robotInfo = computed<RobotInfo | null>(() => {
const imageSettingsVisible = ref(false);
const selectedRobotName = ref('');
// - 使
const isFollowing = computed(() => {
const globalState = getGlobalFollowState();
return globalState.isFollowing;
});
// 使 Pinia store
const followStore = useFollowViewStore();
//
defineOptions({
@ -208,16 +207,17 @@ const startFollowView = () => {
const editor = editorStore.getEditorValue();
if (!editor) return;
// 使
startGlobalFollow(robotInfo.value.id, editor);
message.success('开始视角跟随');
//
const robotName = robotInfo.value.label || `机器人${robotInfo.value.id}`;
// 使 store
followStore.startFollow(robotInfo.value.id, robotName, editor);
};
//
const stopFollowView = () => {
// 使
stopGlobalFollow();
message.success('停止视角跟随');
// 使 store
followStore.stopFollow();
};
//

View File

@ -0,0 +1,178 @@
<template>
<div v-if="followStore.isFollowing" class="follow-view-notification">
<div class="notification-content">
<div class="notification-text">
<div class="notification-title">视角跟随中</div>
<div class="notification-subtitle">正在跟随: {{ followStore.robotName }}</div>
</div>
<div class="notification-actions">
<a-button type="text" size="small" @click="handleStopFollow" class="stop-button">
<template #icon>
<i class="icon exit" />
</template>
停止跟随
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue';
import { useFollowViewStore } from '../stores/follow-view.store';
// 使 Pinia store
const followStore = useFollowViewStore();
//
const handleStopFollow = () => {
followStore.stopFollow();
message.success('已停止视角跟随');
};
//
defineOptions({
name: 'FollowViewNotification',
});
</script>
<style scoped lang="scss">
.follow-view-notification {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
pointer-events: all;
animation: slideDown 0.3s ease-out;
}
.notification-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: linear-gradient(135deg, #0dbb8a 0%, #2ec796 100%);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
color: white;
min-width: 300px;
max-width: 500px;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.notification-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
flex-shrink: 0;
.icon {
font-size: 16px;
color: white;
}
}
.notification-text {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 14px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 2px;
}
.notification-subtitle {
font-size: 12px;
opacity: 0.9;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-actions {
flex-shrink: 0;
}
.stop-button {
color: white !important;
border-color: rgba(255, 255, 255, 0.3) !important;
background: rgba(255, 255, 255, 0.1) !important;
font-size: 12px;
height: 28px;
padding: 0 12px;
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.5) !important;
}
.icon {
font-size: 12px;
margin-right: 4px;
}
}
//
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
//
@media (max-width: 768px) {
.follow-view-notification {
top: 8px;
left: 8px;
right: 8px;
transform: none;
}
.notification-content {
min-width: auto;
max-width: none;
padding: 10px 12px;
}
.notification-icon {
width: 28px;
height: 28px;
.icon {
font-size: 14px;
}
}
.notification-title {
font-size: 13px;
}
.notification-subtitle {
font-size: 11px;
}
.stop-button {
font-size: 11px;
height: 26px;
padding: 0 8px;
}
}
</style>

View File

@ -12,16 +12,8 @@
<a-form :model="formData" layout="vertical">
<!-- 机器人选择 -->
<a-form-item label="选择机器人">
<a-select
v-model:value="selectedRobot"
placeholder="请选择要设置图片的机器人"
style="width: 100%"
>
<a-select-option
v-for="robot in availableRobots"
:key="robot.name"
:value="robot.name"
>
<a-select v-model:value="selectedRobot" placeholder="请选择要设置图片的机器人" style="width: 100%">
<a-select-option v-for="robot in availableRobots" :key="robot.name" :value="robot.name">
{{ robot.name }} ({{ robot.type }})
</a-select-option>
</a-select>
@ -30,7 +22,7 @@
<!-- 图片设置区域 -->
<div v-if="selectedRobot" class="image-settings-section">
<a-divider>图片设置</a-divider>
<!-- 自定义图片 -->
<a-form-item label="自定义图片">
<div class="image-upload-container">
@ -47,7 +39,7 @@
选择自定义图片
</a-button>
</a-upload>
<div v-if="formData.images.normal" class="image-preview">
<img :src="formData.images.normal" alt="自定义图片" />
<div class="image-actions">
@ -99,7 +91,7 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
open: false,
robots: () => [],
selectedRobotName: ''
selectedRobotName: '',
});
const emit = defineEmits<Emits>();
@ -108,13 +100,13 @@ const emit = defineEmits<Emits>();
const selectedRobot = ref<string>(props.selectedRobotName);
const loading = ref(false);
const uploading = ref({
normal: false
normal: false,
});
const formData = ref({
images: {
normal: ''
}
normal: '',
},
});
//
@ -140,25 +132,31 @@ watch(selectedRobot, (newRobot) => {
});
// props
watch(() => props.selectedRobotName, (newName) => {
if (newName && newName !== selectedRobot.value) {
selectedRobot.value = newName;
}
});
watch(
() => props.selectedRobotName,
(newName) => {
if (newName && newName !== selectedRobot.value) {
selectedRobot.value = newName;
}
},
);
// open
watch(() => props.open, (isOpen) => {
if (isOpen) {
//
if (props.selectedRobotName) {
selectedRobot.value = props.selectedRobotName;
loadRobotImages(props.selectedRobotName);
watch(
() => props.open,
(isOpen) => {
if (isOpen) {
//
if (props.selectedRobotName) {
selectedRobot.value = props.selectedRobotName;
loadRobotImages(props.selectedRobotName);
}
} else {
//
handleCancel();
}
} else {
//
handleCancel();
}
});
},
);
//
const loadRobotImages = (robotName: string) => {
@ -196,10 +194,10 @@ const handleImageUpload = async (file: File, state: 'normal') => {
try {
await colorConfig.saveRobotCustomImage(selectedRobot.value, state, file);
formData.value.images[state] = colorConfig.getRobotCustomImage(selectedRobot.value, state) || '';
// pen
updateRobotImage();
message.success('自定义图片上传成功');
} catch (error) {
console.error('图片上传失败:', error);
@ -214,7 +212,7 @@ const handleImageUpload = async (file: File, state: 'normal') => {
//
const removeImage = (state: 'normal') => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value, state);
formData.value.images[state] = '';
updateRobotImage();
@ -224,7 +222,7 @@ const removeImage = (state: 'normal') => {
//
const resetImages = () => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value);
formData.value.images.normal = '';
updateRobotImage();
@ -234,7 +232,7 @@ const resetImages = () => {
//
const clearAllImages = () => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value);
formData.value.images.normal = '';
updateRobotImage();
@ -253,7 +251,7 @@ const handleSave = async () => {
try {
emit('save', {
robotName: selectedRobot.value,
images: formData.value.images
images: formData.value.images,
});
message.success('设置保存成功');
@ -273,8 +271,8 @@ const handleCancel = () => {
selectedRobot.value = '';
formData.value = {
images: {
normal: ''
}
normal: '',
},
};
};
</script>
@ -323,7 +321,6 @@ const handleCancel = () => {
opacity: 1;
}
.ant-form-item {
margin-bottom: 16px;
}
@ -340,6 +337,4 @@ const handleCancel = () => {
:deep(.ant-modal-mask) {
z-index: 999 !important;
}
</style>

View File

@ -2,8 +2,12 @@ import './style.scss';
import { i18n } from '@core/locale.service';
import { router } from '@core/router';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).use(router).use(i18n).mount('#app');
const app = createApp(App);
const pinia = createPinia();
app.use(pinia).use(router).use(i18n).mount('#app');

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { LockState } from '@meta2d/core';
import { LockState } from '@meta2d/core';
import { message } from 'ant-design-vue';
import { isNil } from 'lodash-es';
import { computed, onMounted, onUnmounted, provide, ref, type ShallowRef, shallowRef, watch } from 'vue';
@ -7,11 +7,13 @@ import { useRoute } from 'vue-router';
import type { RobotRealtimeInfo } from '../apis/robot';
import { getSceneByGroupId, getSceneById, monitorRealSceneById, monitorSceneById } from '../apis/scene';
import FollowViewNotification from '../components/follow-view-notification.vue';
import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service';
import {
type ContextMenuState,
createContextMenuManager,
handleContextMenuFromPenData} from '../services/context-menu.service';
import {
type ContextMenuState,
createContextMenuManager,
handleContextMenuFromPenData,
} from '../services/context-menu.service';
import { EditorService } from '../services/editor.service';
import { StorageLocationService } from '../services/storage-location.service';
import { useViewState } from '../services/useViewState';
@ -122,7 +124,7 @@ const monitorScene = async () => {
const newY = y - 60;
// angle
const rotate = angle == null ? undefined : -angle + 180;
return { id, x: newX, y: newY, rotate, visible: true ,locked: LockState.None,};
return { id, x: newX, y: newY, rotate, visible: true, locked: LockState.None };
}
});
@ -216,10 +218,10 @@ const monitorScene = async () => {
//#region
onMounted(() => {
editor.value = new EditorService(container.value!);
// editor store
editorStore.setEditor(editor as ShallowRef<EditorService>);
storageLocationService.value = new StorageLocationService(editor.value, props.sid);
});
//#endregion
@ -285,7 +287,7 @@ watch(
if (contextMenuState.value.isRightClickActive) {
return;
}
const pen = editor.value?.getPenById(v);
if (pen?.id) {
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
@ -363,12 +365,10 @@ const handleEditorContextMenu = (penData: Record<string, unknown>) => {
console.log('EditorService自定义右键菜单事件:', penData);
handleContextMenuFromPenData(penData, contextMenuManager, {
storageLocationService: storageLocationService.value,
robotService: editor.value // EditorService
robotService: editor.value, // EditorService
});
};
/**
* 关闭右键菜单
*/
@ -463,6 +463,9 @@ const handleGlobalKeydown = (event: KeyboardEvent) => {
</div>
</template>
<!-- 视角跟随提示 -->
<FollowViewNotification />
<!-- 右键菜单 -->
<ContextMenu
:visible="contextMenuState.visible"

View File

@ -21,7 +21,8 @@ export {
} from './storage-menu.service';
// 机器人菜单服务
export type { RobotInfo, RobotMenuConfig } from './robot-menu.service';
export type { RobotMenuConfig } from './robot-menu.service';
export type { RobotInfo } from '../../apis/robot';
export {
executeRobotAction,
getRobotInfo,

View File

@ -3,19 +3,10 @@
*
*/
import { reactive } from 'vue';
import * as AmrApi from '../../apis/amr';
import type { RobotInfo } from '../../apis/robot';
import { RobotState } from '../../apis/robot';
// 全局跟随状态管理 - 使用Vue响应式系统
const globalFollowState = reactive({
isFollowing: false,
robotId: '',
timer: null as NodeJS.Timeout | null,
});
export interface RobotMenuConfig {
menuType: 'robot' | 'default';
robotInfo?: RobotInfo;
@ -346,52 +337,3 @@ export function getRobotMenuConfig(robotInfo: RobotInfo): RobotMenuConfig {
robotInfo,
};
}
/**
*
* @param robotId ID
* @param editor
*/
export function startGlobalFollow(robotId: string, editor: any): void {
// 如果已经在跟随其他机器人,先停止
if (globalFollowState.isFollowing && globalFollowState.robotId !== robotId) {
stopGlobalFollow();
}
globalFollowState.isFollowing = true;
globalFollowState.robotId = robotId;
// 立即执行一次聚焦
editor.gotoById(robotId);
// 设置定时器每36毫秒执行一次约27.8fps
globalFollowState.timer = setInterval(() => {
if (globalFollowState.isFollowing) {
editor.gotoById(robotId);
}
}, 10);
console.log('开始全局视角跟随:', robotId);
}
/**
*
*/
export function stopGlobalFollow(): void {
globalFollowState.isFollowing = false;
globalFollowState.robotId = '';
if (globalFollowState.timer) {
clearInterval(globalFollowState.timer);
globalFollowState.timer = null;
}
console.log('停止全局视角跟随');
}
/**
*
*/
export function getGlobalFollowState() {
return globalFollowState;
}

View File

@ -0,0 +1,95 @@
/**
* Store
* 使 Pinia
*/
import { defineStore } from 'pinia';
import { computed,ref } from 'vue';
export interface FollowViewState {
isFollowing: boolean;
robotId: string;
robotName: string;
timer: NodeJS.Timeout | null;
}
export const useFollowViewStore = defineStore('followView', () => {
// 状态
const state = ref<FollowViewState>({
isFollowing: false,
robotId: '',
robotName: '',
timer: null,
});
// 计算属性
const isFollowing = computed(() => state.value.isFollowing);
const robotId = computed(() => state.value.robotId);
const robotName = computed(() => state.value.robotName);
/**
*
* @param robotId ID
* @param robotName
* @param editor
*/
const startFollow = (robotId: string, robotName: string, editor: any) => {
// 如果已经在跟随其他机器人,先停止
if (state.value.isFollowing && state.value.robotId !== robotId) {
stopFollow();
}
// 更新状态
state.value.isFollowing = true;
state.value.robotId = robotId;
state.value.robotName = robotName;
// 立即执行一次聚焦
editor.gotoById(robotId);
// 设置定时器每10毫秒执行一次
state.value.timer = setInterval(() => {
if (state.value.isFollowing) {
editor.gotoById(robotId);
}
}, 10);
console.log('开始视角跟随:', robotId, robotName);
};
/**
*
*/
const stopFollow = () => {
state.value.isFollowing = false;
state.value.robotId = '';
state.value.robotName = '';
if (state.value.timer) {
clearInterval(state.value.timer);
state.value.timer = null;
}
console.log('停止视角跟随');
};
/**
*
*/
const reset = () => {
stopFollow();
};
return {
// 状态
state,
// 计算属性
isFollowing,
robotId,
robotName,
// 方法
startFollow,
stopFollow,
reset,
};
});