feat: 添加视角跟随状态管理功能,集成 Pinia 以优化视角跟随逻辑,新增视角跟随通知组件,提升用户交互体验
This commit is contained in:
parent
02654b29e0
commit
5cdbcebcd3
@ -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
94
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
// 处理机器人操作
|
||||
|
178
src/components/follow-view-notification.vue
Normal file
178
src/components/follow-view-notification.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
95
src/stores/follow-view.store.ts
Normal file
95
src/stores/follow-view.store.ts
Normal 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,
|
||||
};
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user