feat: 移除旧的上下文菜单组件,增强机器人菜单功能,新增自定义图片设置和关闭按钮,优化用户交互体验

This commit is contained in:
xudan 2025-09-10 15:56:50 +08:00
parent a88876697f
commit 8f6087cea4
10 changed files with 811 additions and 107 deletions

View File

@ -1,45 +0,0 @@
<template>
<!-- 使用新的模块化组件 -->
<ContextMenu
:visible="visible"
:x="x"
:y="y"
:menu-type="menuType"
:storage-locations="storageLocations"
:robot-info="robotInfo"
:api-base-url="apiBaseUrl"
@close="$emit('close')"
@action-complete="$emit('actionComplete', $event)"
/>
</template>
<script setup lang="ts">
import { ContextMenu } from './context-menu';
import type { StorageLocationInfo } from '../services/context-menu';
import type { RobotInfo } from '../services/context-menu';
interface Props {
visible: boolean;
x?: number;
y?: number;
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
storageLocations?: StorageLocationInfo[];
robotInfo?: RobotInfo;
apiBaseUrl?: string;
}
interface Emits {
(e: 'close'): void;
(e: 'actionComplete', data: any): void;
}
defineProps<Props>();
defineEmits<Emits>();
//
defineOptions({
name: 'ContextMenu',
});
</script>
<!-- 样式已移至各个子组件中 -->

View File

@ -16,6 +16,9 @@
<div class="context-menu-overlay">
<div class="context-menu-header">
<div class="menu-title">{{ headerTitle }}</div>
<div class="close-button" @click="handleCloseMenu" title="关闭菜单">
<span class="close-icon">×</span>
</div>
</div>
<!-- 库位菜单 -->
@ -33,6 +36,7 @@
v-else-if="menuType === 'robot' && robotInfo"
:robot-info="robotInfo"
@action-complete="handleActionComplete"
@custom-image="handleCustomImage"
/>
<!-- 默认菜单 -->
@ -48,8 +52,8 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, ref } from 'vue';
import type { RobotInfo } from '../../services/context-menu';
import type { StorageLocationInfo } from '../../services/context-menu';
import type { RobotInfo, StorageLocationInfo } from '../../services/context-menu';
// 使 TypeScript
const DefaultMenu = defineAsyncComponent(() => import('./default-menu.vue'));
const RobotMenu = defineAsyncComponent(() => import('./robot-menu.vue'));
@ -68,6 +72,7 @@ interface Props {
interface Emits {
(e: 'close'): void;
(e: 'actionComplete', data: any): void;
(e: 'customImage', data: { robotInfo: RobotInfo }): void;
}
const props = withDefaults(defineProps<Props>(), {
@ -126,11 +131,20 @@ const handleOpenChange = (open: boolean) => {
const handleActionComplete = (data: any) => {
console.log('菜单操作完成:', data);
emit('actionComplete', data);
//
if (data.action && ['occupy', 'release', 'lock', 'unlock', 'enable', 'disable'].includes(data.action)) {
emit('close');
}
//
};
//
const handleCustomImage = (data: { robotInfo: RobotInfo }) => {
console.log('打开自定义图片设置:', data.robotInfo);
emit('customImage', data);
//
};
//
const handleCloseMenu = () => {
console.log('用户点击关闭按钮');
emit('close');
};
</script>
@ -169,6 +183,9 @@ const handleActionComplete = (data: any) => {
background-color: #fafafa;
padding: 8px 12px;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 黑色主题菜单头部 */
@ -186,4 +203,48 @@ const handleActionComplete = (data: any) => {
:root[theme='dark'] .menu-title {
color: #ffffffd9 !important;
}
/* 关闭按钮样式 */
.close-button {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.close-button:hover {
background-color: #ff4d4f;
color: white;
}
.close-icon {
font-size: 14px;
font-weight: bold;
line-height: 1;
color: #666;
transition: color 0.2s ease;
}
.close-button:hover .close-icon {
color: white;
}
/* 黑色主题关闭按钮 */
:root[theme='dark'] .close-button {
background-color: #424242;
}
:root[theme='dark'] .close-button:hover {
background-color: #ff4d4f;
}
:root[theme='dark'] .close-icon {
color: #ffffffd9;
}
</style>

View File

@ -93,6 +93,10 @@
<!-- 系统管理 -->
<div class="action-group">
<div class="action-group-title">系统管理</div>
<div class="action-item" @click="handleRobotAction('custom_image', '自定义图片')">
<span class="action-icon">🖼</span>
<span>自定义图片</span>
</div>
<div class="action-item" @click="handleRobotAction('reset', '重置')">
<span class="action-icon">🔄</span>
<span>重置</span>
@ -109,27 +113,67 @@
</div>
</div>
</div>
<!-- 机器人图片设置模态框 -->
<RobotImageSettingsModal
v-model:open="imageSettingsVisible"
:robots="availableRobots"
:selected-robot-name="selectedRobotName"
@save="handleImageSettingsSave"
/>
</div>
</template>
<script setup lang="ts">
import type { RobotAction,RobotInfo } from '../../services/context-menu/robot-menu.service';
import { message } from 'ant-design-vue';
import { computed, ref } from 'vue';
import type { RobotAction, RobotInfo } from '../../services/context-menu/robot-menu.service';
import {
executeRobotAction,
getRobotStatusColor,
getRobotStatusText} from '../../services/context-menu/robot-menu.service';
getRobotStatusText
} from '../../services/context-menu/robot-menu.service';
interface Props {
robotInfo?: RobotInfo;
availableRobots?: Array<{ name: string; type: string; id: string }>;
}
interface Emits {
(e: 'actionComplete', data: { action: RobotAction; robot: RobotInfo; success: boolean }): void;
(e: 'customImage', data: { robotInfo: RobotInfo }): void;
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
availableRobots: () => []
});
const emit = defineEmits<Emits>();
//
const imageSettingsVisible = ref(false);
const selectedRobotName = ref('');
//
const availableRobots = computed(() => {
// 使
if (props.availableRobots && props.availableRobots.length > 0) {
return props.availableRobots;
}
//
if (props.robotInfo) {
return [{
name: props.robotInfo.name,
type: 'robot', //
id: props.robotInfo.id
}];
}
return [];
});
//
defineOptions({
name: 'RobotMenu',
@ -152,10 +196,36 @@ const getBatteryClass = (batteryLevel: number) => {
return 'battery-low';
};
//
const handleCustomImage = () => {
if (!props.robotInfo?.name) {
message.error('未找到机器人信息');
return;
}
console.log('打开机器人图片设置:', props.robotInfo);
//
selectedRobotName.value = props.robotInfo.name;
imageSettingsVisible.value = true;
console.log('设置模态框可见性:', imageSettingsVisible.value);
//
emit('customImage', {
robotInfo: props.robotInfo
});
};
//
const handleRobotAction = async (action: RobotAction, actionName: string) => {
const handleRobotAction = async (action: RobotAction | 'custom_image', actionName: string) => {
if (!props.robotInfo) return;
//
if (action === 'custom_image') {
handleCustomImage();
return;
}
try {
console.log(`执行机器人操作: ${action} (${actionName})`, props.robotInfo);
@ -173,7 +243,6 @@ const handleRobotAction = async (action: RobotAction, actionName: string) => {
} catch (error) {
console.error(`机器人${actionName}操作失败:`, error);
//
emit('actionComplete', {
action,
robot: props.robotInfo,
@ -181,6 +250,15 @@ const handleRobotAction = async (action: RobotAction, actionName: string) => {
});
}
};
//
const handleImageSettingsSave = (data: any) => {
console.log('机器人图片设置保存:', data);
message.success('机器人图片设置保存成功');
//
//
};
</script>
<style scoped>

View File

@ -218,35 +218,6 @@ const hideSubMenu = () => {
}
};
// Ant Design
// const hideSubMenuDelayed = () => {
// if (hideTimer.value) {
// clearTimeout(hideTimer.value);
// }
// hideTimer.value = window.setTimeout(() => {
// showSubMenu.value = null;
// hideTimer.value = null;
// }, 150); // 150ms
// };
// Ant Design
// const handleStorageMouseLeave = () => {
// hideSubMenuDelayed();
// };
// Ant Design
// const handleSubMenuMouseEnter = () => {
// //
// if (hideTimer.value) {
// clearTimeout(hideTimer.value);
// hideTimer.value = null;
// }
// };
// Ant Design
// const handleSubMenuMouseLeave = () => {
// hideSubMenu();
// };
//
const handleSubMenuOpenChange = (open: boolean) => {

View File

@ -0,0 +1,401 @@
<template>
<a-modal
:open="props.open"
title="机器人图片设置"
width="800px"
:confirm-loading="loading"
@ok="handleSave"
@cancel="handleCancel"
@update:open="(value) => emit('update:open', value)"
>
<div class="robot-image-settings">
<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"
>
{{ robot.name }} ({{ robot.type }})
</a-select-option>
</a-select>
</a-form-item>
<!-- 图片设置区域 -->
<div v-if="selectedRobot" class="image-settings-section">
<a-divider>图片设置</a-divider>
<!-- 普通状态图片 -->
<a-form-item label="普通状态图片">
<div class="image-upload-container">
<a-upload
:file-list="[]"
:before-upload="(file) => handleImageUpload(file, 'normal')"
accept="image/*"
:show-upload-list="false"
>
<a-button :loading="uploading.normal">
<template #icon>
<UploadOutlined />
</template>
选择普通状态图片
</a-button>
</a-upload>
<div v-if="formData.images.normal" class="image-preview">
<img :src="formData.images.normal" alt="普通状态" />
<div class="image-actions">
<a-button size="small" @click="removeImage('normal')">删除</a-button>
</div>
</div>
</div>
</a-form-item>
<!-- 激活状态图片 -->
<a-form-item label="激活状态图片">
<div class="image-upload-container">
<a-upload
:file-list="[]"
:before-upload="(file) => handleImageUpload(file, 'active')"
accept="image/*"
:show-upload-list="false"
>
<a-button :loading="uploading.active">
<template #icon>
<UploadOutlined />
</template>
选择激活状态图片
</a-button>
</a-upload>
<div v-if="formData.images.active" class="image-preview">
<img :src="formData.images.active" alt="激活状态" />
<div class="image-actions">
<a-button size="small" @click="removeImage('active')">删除</a-button>
</div>
</div>
</div>
</a-form-item>
<!-- 图片尺寸设置 -->
<a-form-item label="图片尺寸">
<a-row :gutter="16">
<a-col :span="12">
<a-input-number
v-model:value="formData.imageWidth"
:min="20"
:max="200"
placeholder="宽度"
style="width: 100%"
/>
<span class="size-label">宽度 (px)</span>
</a-col>
<a-col :span="12">
<a-input-number
v-model:value="formData.imageHeight"
:min="20"
:max="200"
placeholder="高度"
style="width: 100%"
/>
<span class="size-label">高度 (px)</span>
</a-col>
</a-row>
</a-form-item>
<!-- 操作按钮 -->
<a-form-item>
<a-space>
<a-button @click="resetImages">重置图片</a-button>
<a-button danger @click="clearAllImages">清除所有图片</a-button>
</a-space>
</a-form-item>
</div>
</a-form>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { UploadOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { computed, onMounted, ref, watch } from 'vue';
import colorConfig from '../../services/color/color-config.service';
interface RobotInfo {
name: string;
type: string;
id: string;
}
interface Props {
open: boolean;
robots: RobotInfo[];
selectedRobotName?: string;
}
interface Emits {
(e: 'update:open', open: boolean): void;
(e: 'update:selectedRobotName', name: string): void;
(e: 'save', data: any): void;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
robots: () => [],
selectedRobotName: ''
});
const emit = defineEmits<Emits>();
//
const selectedRobot = ref<string>(props.selectedRobotName);
const loading = ref(false);
const uploading = ref({
normal: false,
active: false
});
const formData = ref({
images: {
normal: '',
active: ''
},
imageWidth: 42,
imageHeight: 76
});
//
const availableRobots = computed(() => props.robots);
//
onMounted(() => {
if (props.open && props.selectedRobotName) {
selectedRobot.value = props.selectedRobotName;
loadRobotImages(props.selectedRobotName);
}
});
//
watch(selectedRobot, (newRobot) => {
if (newRobot) {
loadRobotImages(newRobot);
emit('update:selectedRobotName', newRobot);
}
});
// props
watch(() => props.selectedRobotName, (newName) => {
console.log('模态框接收到selectedRobotName变化:', newName);
if (newName && newName !== selectedRobot.value) {
selectedRobot.value = newName;
console.log('设置selectedRobot为:', newName);
}
});
// open
watch(() => props.open, (isOpen) => {
console.log('模态框open状态变化:', isOpen);
if (isOpen) {
//
if (props.selectedRobotName) {
selectedRobot.value = props.selectedRobotName;
loadRobotImages(props.selectedRobotName);
console.log('模态框打开设置selectedRobot为:', props.selectedRobotName);
}
} else {
//
handleCancel();
}
});
//
const loadRobotImages = (robotName: string) => {
formData.value.images.normal = colorConfig.getRobotCustomImage(robotName, 'normal') || '';
formData.value.images.active = colorConfig.getRobotCustomImage(robotName, 'active') || '';
//
formData.value.imageWidth = Number(colorConfig.getColor('robot.imageWidth')) || 42;
formData.value.imageHeight = Number(colorConfig.getColor('robot.imageHeight')) || 76;
};
//
const handleImageUpload = async (file: File, state: 'normal' | 'active') => {
if (!selectedRobot.value) {
message.error('请先选择机器人');
return false;
}
//
if (!file.type.startsWith('image/')) {
message.error('请选择图片文件');
return false;
}
// (2MB)
if (file.size > 2 * 1024 * 1024) {
message.error('图片文件大小不能超过2MB');
return false;
}
uploading.value[state] = true;
try {
await colorConfig.saveRobotCustomImage(selectedRobot.value, state, file);
formData.value.images[state] = colorConfig.getRobotCustomImage(selectedRobot.value, state) || '';
message.success(`${state === 'normal' ? '普通状态' : '激活状态'}图片上传成功`);
} catch (error) {
console.error('图片上传失败:', error);
message.error('图片上传失败,请重试');
} finally {
uploading.value[state] = false;
}
return false; //
};
//
const removeImage = (state: 'normal' | 'active') => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value, state);
formData.value.images[state] = '';
message.success(`${state === 'normal' ? '普通状态' : '激活状态'}图片已删除`);
};
//
const resetImages = () => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value);
formData.value.images.normal = '';
formData.value.images.active = '';
message.success('图片已重置');
};
//
const clearAllImages = () => {
if (!selectedRobot.value) return;
colorConfig.removeRobotCustomImage(selectedRobot.value);
formData.value.images.normal = '';
formData.value.images.active = '';
message.success('所有图片已清除');
};
//
const handleSave = async () => {
if (!selectedRobot.value) {
message.error('请选择机器人');
return;
}
loading.value = true;
try {
//
if (formData.value.imageWidth !== Number(colorConfig.getColor('robot.imageWidth'))) {
colorConfig.setColor('robot.imageWidth', formData.value.imageWidth.toString());
}
if (formData.value.imageHeight !== Number(colorConfig.getColor('robot.imageHeight'))) {
colorConfig.setColor('robot.imageHeight', formData.value.imageHeight.toString());
}
emit('save', {
robotName: selectedRobot.value,
images: formData.value.images,
imageWidth: formData.value.imageWidth,
imageHeight: formData.value.imageHeight
});
message.success('设置保存成功');
handleCancel();
} catch (error) {
console.error('保存设置失败:', error);
message.error('保存设置失败,请重试');
} finally {
loading.value = false;
}
};
//
const handleCancel = () => {
emit('update:open', false);
//
selectedRobot.value = '';
formData.value = {
images: {
normal: '',
active: ''
},
imageWidth: 42,
imageHeight: 76
};
};
</script>
<style scoped>
.robot-image-settings {
max-height: 600px;
overflow-y: auto;
}
.image-settings-section {
margin-top: 16px;
}
.image-upload-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.image-preview {
position: relative;
display: inline-block;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 8px;
background: #fafafa;
}
.image-preview img {
max-width: 120px;
max-height: 120px;
display: block;
border-radius: 4px;
}
.image-actions {
position: absolute;
top: 4px;
right: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.image-preview:hover .image-actions {
opacity: 1;
}
.size-label {
font-size: 12px;
color: #666;
margin-left: 8px;
}
.ant-form-item {
margin-bottom: 16px;
}
.ant-divider {
margin: 16px 0;
}
</style>

View File

@ -407,13 +407,16 @@ const handleActionComplete = (data: any) => {
* @param event 点击事件
*/
const handleGlobalClick = (event: MouseEvent) => {
//
if (contextMenuState.value.visible) {
//
if (!isClickInsideMenu(event, contextMenuState.value.visible)) {
contextMenuManager.close();
}
//
const closeButton = (event.target as Element)?.closest('.close-button');
if (closeButton) {
//
return;
}
};
/**

View File

@ -98,6 +98,14 @@ export interface EditorColorConfig {
// 机器人图片尺寸
imageWidth: number; // 机器人图片宽度
imageHeight: number; // 机器人图片高度
// 单个机器人自定义图片配置
customImages: {
[robotName: string]: {
normal: string; // Base64编码的普通状态图片
active: string; // Base64编码的激活状态图片
};
};
useCustomImages: boolean; // 是否启用自定义图片
};
// 库位颜色
@ -318,7 +326,9 @@ const DEFAULT_COLORS: EditorColorConfig = {
strokeFault: '#FF4D4F99',
fillFault: '#FF4D4F33',
imageWidth: 42, // 机器人图片宽度 - 42像素
imageHeight: 76 // 机器人图片高度 - 76像素
imageHeight: 76, // 机器人图片高度 - 76像素
customImages: {}, // 单个机器人自定义图片配置
useCustomImages: false // 默认不启用自定义图片
},
storage: {
occupied: '#ff4d4f',
@ -465,7 +475,9 @@ const DARK_THEME_COLORS: EditorColorConfig = {
strokeFault: '#FF4D4F99',
fillFault: '#FF4D4F33',
imageWidth: 42, // 机器人图片宽度 - 42像素
imageHeight: 76 // 机器人图片高度 - 76像素
imageHeight: 76, // 机器人图片高度 - 76像素
customImages: {}, // 单个机器人自定义图片配置
useCustomImages: false // 默认不启用自定义图片
},
storage: {
occupied: '#ff4d4f',
@ -879,7 +891,9 @@ class ColorConfigService {
strokeFault: theme.robot['stroke-fault'] || DEFAULT_COLORS.robot.strokeFault,
fillFault: theme.robot['fill-fault'] || DEFAULT_COLORS.robot.fillFault,
imageWidth: DEFAULT_COLORS.robot.imageWidth,
imageHeight: DEFAULT_COLORS.robot.imageHeight
imageHeight: DEFAULT_COLORS.robot.imageHeight,
customImages: DEFAULT_COLORS.robot.customImages,
useCustomImages: DEFAULT_COLORS.robot.useCustomImages
};
}
@ -1009,6 +1023,152 @@ class ColorConfigService {
}, obj);
target[lastKey] = value;
}
/**
* Base64字符串
* @param file
* @param maxWidth
* @param quality (0-1)
*/
public async fileToBase64(file: File, maxWidth: number = 200, quality: number = 0.8): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
// 创建canvas进行图片压缩
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
// 计算压缩后的尺寸
const ratio = Math.min(maxWidth / img.width, maxWidth / img.height);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
// 绘制压缩后的图片
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 转换为Base64
const base64 = canvas.toDataURL('image/png', quality);
resolve(base64);
};
img.onerror = reject;
img.src = reader.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
/**
*
* @param robotName
* @param state ('normal' | 'active')
* @param file
*/
public async saveRobotCustomImage(robotName: string, state: 'normal' | 'active', file: File): Promise<void> {
try {
// 将图片转换为Base64
const base64 = await this.fileToBase64(file);
// 确保customImages对象存在
if (!this.config.value.robot.customImages) {
this.setNestedValue(this.config.value, 'robot.customImages', {});
}
// 确保机器人对象存在
if (!this.config.value.robot.customImages[robotName]) {
this.setNestedValue(this.config.value, `robot.customImages.${robotName}`, {});
}
// 保存图片数据
this.setNestedValue(this.config.value, `robot.customImages.${robotName}.${state}`, base64);
// 启用自定义图片
this.setColor('robot.useCustomImages', 'true');
// 更新单个机器人的图片
if (this.editorService && typeof this.editorService.updateRobotImage === 'function') {
this.editorService.updateRobotImage(robotName);
}
// 触发重新渲染
this.triggerRender();
} catch (error) {
console.error('保存机器人自定义图片失败:', error);
throw error;
}
}
/**
*
* @param robotName
* @param state ('normal' | 'active')
*/
public getRobotCustomImage(robotName: string, state: 'normal' | 'active'): string | null {
const customImages = this.config.value.robot.customImages;
if (!customImages || !customImages[robotName]) {
return null;
}
return customImages[robotName][state] || null;
}
/**
*
* @param robotName
* @param state ('normal' | 'active')
*/
public removeRobotCustomImage(robotName: string, state?: 'normal' | 'active'): void {
const customImages = this.config.value.robot.customImages;
if (!customImages || !customImages[robotName]) {
return;
}
if (state) {
// 删除特定状态的图片
this.setNestedValue(this.config.value, `robot.customImages.${robotName}.${state}`, '');
} else {
// 删除整个机器人的图片配置
delete customImages[robotName];
}
// 检查是否还有任何自定义图片
const hasAnyCustomImages = Object.values(customImages).some(robot =>
robot.normal || robot.active
);
if (!hasAnyCustomImages) {
this.setColor('robot.useCustomImages', 'false');
}
this.triggerRender();
}
/**
*
* @param robotName
*/
public hasRobotCustomImage(robotName: string): boolean {
const customImages = this.config.value.robot.customImages;
if (!customImages || !customImages[robotName]) {
return false;
}
return !!(customImages[robotName].normal || customImages[robotName].active);
}
/**
*
*/
public getRobotsWithCustomImages(): string[] {
const customImages = this.config.value.robot.customImages;
if (!customImages) return [];
return Object.keys(customImages).filter(robotName =>
customImages[robotName].normal || customImages[robotName].active
);
}
}
export default new ColorConfigService();

View File

@ -85,10 +85,18 @@ export function parsePenData(penData: Record<string, unknown>): ParsedEventData
if (tags.includes('robot')) {
// 机器人区域
// 机器人的真实名称存储在text字段中而不是name字段
const robotName = (pen?.text as string) || name || `机器人-${id}`;
console.log('解析pen数据中的机器人信息:', {
id,
originalName: name,
penText: pen?.text,
finalName: robotName
});
return {
type: 'robot',
id,
name,
name: robotName,
tags,
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
position,
@ -198,10 +206,18 @@ export function parseEventData(event: MouseEvent | PointerEvent): ParsedEventDat
if (tags.includes('robot')) {
// 机器人区域
// 机器人的真实名称存储在text字段中而不是name字段
const robotName = (pen?.text as string) || name || `机器人-${id}`;
console.log('解析机器人信息:', {
id,
originalName: name,
penText: pen?.text,
finalName: robotName
});
return {
type: 'robot',
id,
name,
name: robotName,
tags,
target,
position,
@ -282,7 +298,11 @@ export function parseEventData(event: MouseEvent | PointerEvent): ParsedEventDat
export function isClickInsideMenu(event: MouseEvent, isMenuVisible: boolean): boolean {
if (!isMenuVisible) return false;
const menuElement = document.querySelector('.context-menu');
// 查找右键菜单元素,支持多种可能的类名
const menuElement = document.querySelector('.context-menu-overlay') ||
document.querySelector('.context-menu') ||
document.querySelector('[class*="context-menu"]');
if (!menuElement) return false;
const rect = menuElement.getBoundingClientRect();

View File

@ -36,7 +36,8 @@ export type RobotAction =
| 'stop' // 停止
| 'reset' // 重置
| 'diagnose' // 诊断
| 'update'; // 更新
| 'update' // 更新
| 'custom_image'; // 自定义图片
/**
*

View File

@ -11,7 +11,7 @@ import {
type Point,
type Rect,
} from '@api/map';
import type { RobotGroup, RobotInfo, RobotRealtimeInfo, RobotType } from '@api/robot';
import type { RobotGroup, RobotInfo, RobotType } from '@api/robot';
import type {
BinLocationGroup,
BinLocationItem,
@ -25,7 +25,7 @@ import type {
import sTheme from '@core/theme.service';
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import { clone, get, isEmpty, isNil, isString, nth, omitBy, pick, remove, some } from 'lodash-es';
import { clone, get, isEmpty, isNil, isString, nth, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
@ -1105,7 +1105,7 @@ export class EditorService extends Meta2d {
await Promise.all(
this.robots.map(async ({ id, label, type }) => {
const pen: MapPen = {
...this.#mapRobotImage(type, true),
...this.#mapRobotImage(type, true, label), // 传递机器人名称(label)
id,
name: 'robot',
tags: ['robot'],
@ -1171,10 +1171,10 @@ export class EditorService extends Meta2d {
*
*/
public updateAllRobotImageSizes(): void {
this.robots.forEach(({ id, type }) => {
this.robots.forEach(({ id, type, label }) => {
const pen = this.getPenById(id);
if (pen && pen.robot) {
const imageConfig = this.#mapRobotImage(type, pen.robot.active);
const imageConfig = this.#mapRobotImage(type, pen.robot.active, label); // 传递机器人名称
this.setValue(
{
id,
@ -1188,14 +1188,56 @@ export class EditorService extends Meta2d {
});
}
/**
*
*
* @param robotName
*/
public updateRobotImage(robotName: string): void {
const robot = this.robots.find(r => r.label === robotName);
if (!robot) return;
const pen = this.getPenById(robot.id);
if (pen && pen.robot) {
const imageConfig = this.#mapRobotImage(robot.type, pen.robot.active, robotName);
this.setValue(
{
id: robot.id,
image: imageConfig.image,
iconWidth: imageConfig.iconWidth,
iconHeight: imageConfig.iconHeight,
iconTop: imageConfig.iconTop,
},
{ render: true, history: false, doEvent: false },
);
}
}
#mapRobotImage(
type: RobotType,
active?: boolean,
robotName?: string,
): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop' | 'canvasLayer'>> {
const theme = this.data().theme;
const image =
import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
// 检查是否启用自定义图片且有机器人名称
const useCustomImages = colorConfig.getColor('robot.useCustomImages') === 'true';
let image: string;
if (useCustomImages && robotName) {
// 尝试获取自定义图片
const customImage = colorConfig.getRobotCustomImage(robotName, active ? 'active' : 'normal');
if (customImage) {
image = customImage;
} else {
// 如果没有自定义图片,回退到默认图片
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
}
} else {
// 使用默认图片
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
}
// 使用配置的机器人图片尺寸,如果没有配置则使用默认值
const iconWidth = colorConfig.getColor('robot.imageWidth') ? Number(colorConfig.getColor('robot.imageWidth')) : 42;
@ -1634,6 +1676,16 @@ export class EditorService extends Meta2d {
(<HTMLDivElement>container.children.item(5)).ondrop = null;
// 监听所有画布事件
this.on('*', (e, v) => this.#listen(e, v));
// 添加额外的右键事件监听器,确保阻止默认行为
const canvasElement = this.canvas as unknown as HTMLCanvasElement;
if (canvasElement && canvasElement.addEventListener) {
canvasElement.addEventListener('contextmenu', (event) => {
event.preventDefault();
event.stopPropagation();
}, true);
}
// 注册自定义绘制函数和锚点
this.#register();
@ -1665,7 +1717,9 @@ export class EditorService extends Meta2d {
});
this.find('robot').forEach((pen) => {
if (!pen.robot?.type) return;
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active));
// 从pen的text属性获取机器人名称
const robotName = pen.text || '';
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active, robotName));
});
this.render();
}