From 8f6087cea468898da496aa535763acf0d93b02ff Mon Sep 17 00:00:00 2001 From: xudan Date: Wed, 10 Sep 2025 15:56:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E6=97=A7=E7=9A=84?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E8=8F=9C=E5=8D=95=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E6=9C=BA=E5=99=A8=E4=BA=BA=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=9B=BE=E7=89=87=E8=AE=BE=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E6=8C=89=E9=92=AE=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/context-menu.vue | 45 -- src/components/context-menu/context-menu.vue | 75 +++- src/components/context-menu/robot-menu.vue | 88 +++- src/components/context-menu/storage-menu.vue | 29 -- .../modal/robot-image-settings-modal.vue | 401 ++++++++++++++++++ src/pages/movement-supervision.vue | 15 +- src/services/color/color-config.service.ts | 166 +++++++- src/services/context-menu/event-parser.ts | 26 +- .../context-menu/robot-menu.service.ts | 3 +- src/services/editor.service.ts | 70 ++- 10 files changed, 811 insertions(+), 107 deletions(-) delete mode 100644 src/components/context-menu.vue create mode 100644 src/components/modal/robot-image-settings-modal.vue 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 @@
+
+ × +
@@ -33,6 +36,7 @@ v-else-if="menuType === 'robot' && robotInfo" :robot-info="robotInfo" @action-complete="handleActionComplete" + @custom-image="handleCustomImage" /> @@ -48,8 +52,8 @@ @@ -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; +} diff --git a/src/components/context-menu/robot-menu.vue b/src/components/context-menu/robot-menu.vue index cb10702..c608115 100644 --- a/src/components/context-menu/robot-menu.vue +++ b/src/components/context-menu/robot-menu.vue @@ -93,6 +93,10 @@
系统管理
+
+ 🖼️ + 自定义图片 +
🔄 重置 @@ -109,27 +113,67 @@
+ + + 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(); }