diff --git a/src/components/context-menu.vue b/src/components/context-menu.vue
deleted file mode 100644
index 6654ecd..0000000
--- a/src/components/context-menu.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/components/context-menu/context-menu.vue b/src/components/context-menu/context-menu.vue
index b20be4d..7532021 100644
--- a/src/components/context-menu/context-menu.vue
+++ b/src/components/context-menu/context-menu.vue
@@ -16,6 +16,9 @@
+
+
+
diff --git a/src/pages/movement-supervision.vue b/src/pages/movement-supervision.vue
index a48b3a0..d1aad14 100644
--- a/src/pages/movement-supervision.vue
+++ b/src/pages/movement-supervision.vue
@@ -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;
}
+
+
+
};
/**
diff --git a/src/services/color/color-config.service.ts b/src/services/color/color-config.service.ts
index 460086d..2613028 100644
--- a/src/services/color/color-config.service.ts
+++ b/src/services/color/color-config.service.ts
@@ -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 {
+ 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 {
+ 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();
diff --git a/src/services/context-menu/event-parser.ts b/src/services/context-menu/event-parser.ts
index 0b6be05..79bca33 100644
--- a/src/services/context-menu/event-parser.ts
+++ b/src/services/context-menu/event-parser.ts
@@ -85,10 +85,18 @@ export function parsePenData(penData: Record): 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();
diff --git a/src/services/context-menu/robot-menu.service.ts b/src/services/context-menu/robot-menu.service.ts
index 79324c9..8a2ed27 100644
--- a/src/services/context-menu/robot-menu.service.ts
+++ b/src/services/context-menu/robot-menu.service.ts
@@ -36,7 +36,8 @@ export type RobotAction =
| 'stop' // 停止
| 'reset' // 重置
| 'diagnose' // 诊断
- | 'update'; // 更新
+ | 'update' // 更新
+ | 'custom_image'; // 自定义图片
/**
* 获取机器人信息
diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts
index b2395ef..0f624f4 100644
--- a/src/services/editor.service.ts
+++ b/src/services/editor.service.ts
@@ -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> {
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 {
(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();
}