From 9bd97f724859326e9c88776ef3a82f34a851fa30 Mon Sep 17 00:00:00 2001 From: xudan Date: Fri, 5 Sep 2025 11:50:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A2=9C=E8=89=B2?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=9D=A2=E6=9D=BF=E5=92=8C=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=92=8C=E7=BB=98=E5=88=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/scene/type.ts | 3 + src/components/color-config-panel.vue | 388 +++++++++++++++ src/components/color-picker-with-alpha.vue | 356 ++++++++++++++ src/components/map-toolbar.vue | 113 ++++- src/pages/movement-supervision.vue | 3 + src/services/color-config.service.ts | 483 +++++++++++++++++++ src/services/draw/storage-location-drawer.ts | 16 +- src/services/editor.service.ts | 98 +++- 8 files changed, 1433 insertions(+), 27 deletions(-) create mode 100644 src/components/color-config-panel.vue create mode 100644 src/components/color-picker-with-alpha.vue create mode 100644 src/services/color-config.service.ts diff --git a/src/apis/scene/type.ts b/src/apis/scene/type.ts index 269381f..5f5cd8d 100644 --- a/src/apis/scene/type.ts +++ b/src/apis/scene/type.ts @@ -1,6 +1,8 @@ import type { RobotGroup, RobotInfo } from '@api/robot'; import type { Meta2dData } from '@meta2d/core'; +import type { EditorColorConfig } from '../../services/color-config.service'; + export interface SceneInfo { id: string; // 场景id label: string; // 场景名称 @@ -30,6 +32,7 @@ export interface StandardScene { width?: number; // 场景宽度 height?: number; // 场景高度 ratio?: number; // 坐标缩放比例 + colorConfig?: EditorColorConfig; // 颜色配置 } export interface StandardScenePoint { id: string; diff --git a/src/components/color-config-panel.vue b/src/components/color-config-panel.vue new file mode 100644 index 0000000..f68837d --- /dev/null +++ b/src/components/color-config-panel.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/src/components/color-picker-with-alpha.vue b/src/components/color-picker-with-alpha.vue new file mode 100644 index 0000000..e4feba5 --- /dev/null +++ b/src/components/color-picker-with-alpha.vue @@ -0,0 +1,356 @@ + + + + + diff --git a/src/components/map-toolbar.vue b/src/components/map-toolbar.vue index e56b48b..1b664b6 100644 --- a/src/components/map-toolbar.vue +++ b/src/components/map-toolbar.vue @@ -4,6 +4,8 @@ import type { EditorService } from '@core/editor.service'; import { useToolbar } from '@core/useToolbar'; import { computed, inject, type InjectionKey, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue'; +import ColorConfigPanel from './color-config-panel.vue'; + // 通用地图工具栏(右下角),临时中文按钮 // 功能:放大、缩小、适配视图、全屏、截图、网格(占位) @@ -17,6 +19,9 @@ const editorRef = inject(props.token)!; const isFullscreen = computed(() => !!document.fullscreenElement); +// 颜色配置面板状态 +const showColorConfig = ref(false); + // 使用 useToolbar 的相关能力(内部使用 jumpToPosition、修改 store 等) const { toggleGrid: _toggleGrid, @@ -103,12 +108,26 @@ const toggleMeasure = () => { } }; +// 颜色配置相关方法 +const toggleColorConfig = () => { + showColorConfig.value = !showColorConfig.value; + // 如果打开颜色配置,关闭测量模式 + if (showColorConfig.value && measuring.value) { + measuring.value = false; + start.value = null; + end.value = null; + current.value = null; + } +}; + const onKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') { measuring.value = false; start.value = null; end.value = null; current.value = null; + // 也关闭颜色配置面板 + showColorConfig.value = false; } }; @@ -172,6 +191,16 @@ defineOptions({ 标尺 网格 + + + +
+
+
+

编辑器颜色配置

+ + + +
+
+ +
+
+
+
- + @@ -294,6 +343,64 @@ defineOptions({ background-size: 20px 20px; } +/* 颜色配置面板 */ +.color-config-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.color-config-container { + background: #fff; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + max-width: 90vw; + max-height: 90vh; + width: 800px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.color-config-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #e8e8e8; + background: #fafafa; +} + +.color-config-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; +} + +.close-btn { + color: #999; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; +} + +.close-btn:hover { + color: #666; + background: #f0f0f0; +} + +.color-config-content { + flex: 1; + overflow-y: auto; + padding: 0; +} + /* 测量叠加层 */ .measure-overlay { position: fixed; diff --git a/src/pages/movement-supervision.vue b/src/pages/movement-supervision.vue index a2bd1a9..09cc336 100644 --- a/src/pages/movement-supervision.vue +++ b/src/pages/movement-supervision.vue @@ -10,6 +10,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } fro import { useRoute } from 'vue-router'; import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service'; +import colorConfig from '../services/color-config.service'; import { type ContextMenuState, createContextMenuManager, @@ -231,6 +232,8 @@ onMounted(async () => { // 设置编辑器服务 if (editor.value) { autoDoorSimulationService.setEditorService(editor.value); + // 设置颜色配置服务的编辑器实例 + // colorConfig.setEditorService(editor.value); // 注释掉模拟逻辑,使用真实WebSocket数据 // autoDoorSimulationService.startSimulation({ diff --git a/src/services/color-config.service.ts b/src/services/color-config.service.ts new file mode 100644 index 0000000..f7a45dc --- /dev/null +++ b/src/services/color-config.service.ts @@ -0,0 +1,483 @@ +import sTheme from '@core/theme.service'; +import { ref, watch } from 'vue'; + +/** + * 编辑器颜色配置接口 + */ +export interface EditorColorConfig { + // 点位颜色 + point: { + small: { + stroke: string; + strokeActive: string; + fill: Record; // 按点位类型索引 + }; + large: { + stroke: string; + strokeActive: string; + strokeOccupied: string; + strokeUnoccupied: string; + strokeEmpty: string; + strokeDisabled: string; + strokeEnabled: string; + strokeLocked: string; + strokeUnlocked: string; + }; + // 各类型点位专用颜色 + types: { + [key: number]: { + stroke: string; + strokeActive: string; + fill: string; + }; + }; + }; + + // 路线颜色 + route: { + strokeActive: string; + stroke: Record; // 按路线类型索引 + // 空载和载货路线专用颜色 + strokeEmpty: string; // 空载路线颜色 + strokeLoaded: string; // 载货路线颜色 + }; + + // 区域颜色 + area: { + strokeActive: string; + stroke: Record; // 按区域类型索引 + fill: Record; // 按区域类型索引 + // 各类型区域专用颜色 + types: { + [key: number]: { + stroke: string; + strokeActive: string; + fill: string; + }; + }; + }; + + // 机器人颜色 + robot: { + stroke: string; + fill: string; + line: string; + strokeNormal: string; + fillNormal: string; + strokeWarning: string; + fillWarning: string; + strokeFault: string; + fillFault: string; + }; + + // 库位颜色 + storage: { + occupied: string; + available: string; + default: string; + locked: string; + moreButton: { + background: string; + border: string; + text: string; + }; + }; + + // 自动门颜色 + autoDoor: { + strokeClosed: string; + fillClosed: string; + strokeOpen: string; + fillOpen: string; + }; + + // 通用颜色 + common: { + color: string; + background: string; + }; +} + +/** + * 默认颜色配置 + */ +const DEFAULT_COLORS: EditorColorConfig = { + point: { + small: { + stroke: '#8C8C8C', + strokeActive: '#EBB214', + fill: { + 1: '#14D1A5', + 2: '#69C6F5', + 3: '#E48B1D', + 4: '#E48B1D', + 5: '#a72b69' + } + }, + large: { + stroke: '#595959', + strokeActive: '#EBB214', + strokeOccupied: '#ff4d4f', + strokeUnoccupied: '#52c41a', + strokeEmpty: '#1890ff', + strokeDisabled: '#bfbfbf', + strokeEnabled: '#52c41a', + strokeLocked: '#faad14', + strokeUnlocked: '#52c41a' + }, + types: { + // 小点位类型 (1-9) + 1: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#14D1A5' }, // 普通点 + 2: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#69C6F5' }, // 等待点 + 3: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#E48B1D' }, // 避让点 + 4: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#E48B1D' }, // 临时避让点 + 5: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#a72b69' }, // 库区点 + // 大点位类型 (11+) + 11: { stroke: '#595959', strokeActive: '#EBB214', fill: '#1890ff' }, // 电梯点 + 12: { stroke: '#595959', strokeActive: '#EBB214', fill: '#52c41a' }, // 自动门点 + 13: { stroke: '#595959', strokeActive: '#EBB214', fill: '#faad14' }, // 充电点 + 14: { stroke: '#595959', strokeActive: '#EBB214', fill: '#722ed1' }, // 停靠点 + 15: { stroke: '#595959', strokeActive: '#EBB214', fill: '#13c2c2' }, // 动作点 + 16: { stroke: '#595959', strokeActive: '#EBB214', fill: '#ff4d4f' }, // 禁行点 + } + }, + route: { + strokeActive: '#EBB214', + stroke: { + 0: '#8C8C8C', + 1: '#52C41A', + 2: '#1982F3', + 10: '#E63A3A' + }, + strokeEmpty: '#52C41A', // 空载路线 - 绿色 + strokeLoaded: '#1982F3' // 载货路线 - 蓝色 + }, + area: { + strokeActive: '#EBB214', + stroke: { + 1: '#9ACDFF99', + 11: '#FF535399', + 12: '#0DBB8A99', + 13: '#e61e4aad', + 14: '#FFD70099' + }, + fill: { + 1: '#9ACDFF33', + 11: '#FF9A9A33', + 12: '#0DBB8A33', + 13: '#e61e4a33', + 14: '#FFD70033' + }, + types: { + // 区域类型 + 1: { stroke: '#9ACDFF99', strokeActive: '#EBB214', fill: '#9ACDFF33' }, // 库区 + 11: { stroke: '#FF535399', strokeActive: '#EBB214', fill: '#FF9A9A33' }, // 互斥区 + 12: { stroke: '#0DBB8A99', strokeActive: '#EBB214', fill: '#0DBB8A33' }, // 非互斥区 + 13: { stroke: '#e61e4aad', strokeActive: '#EBB214', fill: '#e61e4a33' }, // 约束区 + 14: { stroke: '#FFD70099', strokeActive: '#EBB214', fill: '#FFD70033' }, // 描述区 + } + }, + robot: { + stroke: '#01FDAF99', + fill: '#01FAAD33', + line: '#01fdaf', + strokeNormal: '#01FDAF99', + fillNormal: '#01FAAD33', + strokeWarning: '#FF851B99', + fillWarning: '#FF851B33', + strokeFault: '#FF4D4F99', + fillFault: '#FF4D4F33' + }, + storage: { + occupied: '#ff4d4f', + available: '#52c41a', + default: '#f5f5f5', + locked: '#faad14', + moreButton: { + background: '#e6f4ff', + border: '#1677ff', + text: '#1677ff' + } + }, + autoDoor: { + strokeClosed: '#FF4D4F99', + fillClosed: '#FF4D4F33', + strokeOpen: '#1890FF99', + fillOpen: '#1890FF33' + }, + common: { + color: '#595959', + background: '#F0F2F5' + } +}; + +/** + * 颜色配置管理服务 + */ +class ColorConfigService { + private config = ref({ ...DEFAULT_COLORS }); + private editorService: any = null; + private readonly STORAGE_KEY = 'editor-color-config'; + + constructor() { + // 从本地存储加载配置 + this.loadFromLocalStorage(); + + // 监听主题变化,重新加载配置 + watch( + () => sTheme.theme, + () => { + this.loadConfig(); + } + ); + + // 监听配置变化,自动保存到本地存储 + watch( + () => this.config.value, + (newConfig) => { + this.saveToLocalStorage(newConfig); + }, + { deep: true } + ); + } + + /** + * 获取当前颜色配置 + */ + public get currentConfig(): EditorColorConfig { + return this.config.value; + } + + /** + * 设置编辑器服务实例 + */ + public setEditorService(editor: any): void { + this.editorService = editor; + } + + /** + * 触发编辑器重新渲染 + */ + private triggerRender(): void { + if (this.editorService && typeof this.editorService.render === 'function') { + this.editorService.render(); + } + } + + /** + * 从本地存储加载配置 + */ + private loadFromLocalStorage(): void { + try { + const stored = localStorage.getItem(this.STORAGE_KEY); + if (stored) { + const parsedConfig = JSON.parse(stored); + this.config.value = this.mergeConfig(DEFAULT_COLORS, parsedConfig); + } else { + this.config.value = { ...DEFAULT_COLORS }; + } + } catch (error) { + console.warn('Failed to load color config from localStorage:', error); + this.config.value = { ...DEFAULT_COLORS }; + } + } + + /** + * 保存配置到本地存储 + */ + private saveToLocalStorage(config: EditorColorConfig): void { + try { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(config)); + } catch (error) { + console.warn('Failed to save color config to localStorage:', error); + } + } + + /** + * 从场景数据加载配置(已废弃,现在使用本地存储) + * @deprecated 使用本地存储替代场景文件存储 + */ + public loadFromScene(): void { + // 不再从场景数据加载,保持向后兼容 + console.warn('loadFromScene is deprecated, color config now uses localStorage'); + } + + /** + * 获取当前配置(已废弃,现在使用本地存储) + * @deprecated 使用本地存储替代场景文件存储 + */ + public getConfigForSave(): EditorColorConfig { + console.warn('getConfigForSave is deprecated, color config now uses localStorage'); + return { ...this.config.value }; + } + + /** + * 更新颜色配置 + * @param updates 部分配置更新 + */ + public updateConfig(updates: Partial): void { + this.config.value = { + ...this.config.value, + ...updates + }; + // 触发编辑器重新渲染 + this.triggerRender(); + } + + /** + * 重置为默认配置 + */ + public resetToDefault(): void { + this.config.value = { ...DEFAULT_COLORS }; + // 触发编辑器重新渲染 + this.triggerRender(); + } + + /** + * 获取特定类型的颜色 + * @param path 颜色路径,如 'point.small.stroke' + */ + public getColor(path: string): string { + return this.getNestedValue(this.config.value, path) || ''; + } + + /** + * 设置特定类型的颜色 + * @param path 颜色路径 + * @param value 颜色值 + */ + public setColor(path: string, value: string): void { + this.setNestedValue(this.config.value, path, value); + // 触发编辑器重新渲染 + this.triggerRender(); + } + + /** + * 从主题配置加载默认颜色 + */ + private loadConfig(): void { + const theme = sTheme.editor as any; + if (theme) { + // 从主题配置中加载颜色,如果没有则使用默认值 + this.config.value = this.mergeConfig(DEFAULT_COLORS, { + point: { + small: { + stroke: theme['point-s']?.stroke || DEFAULT_COLORS.point.small.stroke, + strokeActive: theme['point-s']?.strokeActive || DEFAULT_COLORS.point.small.strokeActive, + fill: { + 1: theme['point-s']?.['fill-1'] || DEFAULT_COLORS.point.small.fill[1], + 2: theme['point-s']?.['fill-2'] || DEFAULT_COLORS.point.small.fill[2], + 3: theme['point-s']?.['fill-3'] || DEFAULT_COLORS.point.small.fill[3], + 4: theme['point-s']?.['fill-4'] || DEFAULT_COLORS.point.small.fill[4], + 5: theme['point-s']?.['fill-5'] || DEFAULT_COLORS.point.small.fill[5], + } + }, + large: { + stroke: theme['point-l']?.stroke || DEFAULT_COLORS.point.large.stroke, + strokeActive: theme['point-l']?.strokeActive || DEFAULT_COLORS.point.large.strokeActive, + strokeOccupied: theme['point-l']?.['stroke-occupied'] || DEFAULT_COLORS.point.large.strokeOccupied, + strokeUnoccupied: theme['point-l']?.['stroke-unoccupied'] || DEFAULT_COLORS.point.large.strokeUnoccupied, + strokeEmpty: theme['point-l']?.['stroke-empty'] || DEFAULT_COLORS.point.large.strokeEmpty, + strokeDisabled: theme['point-l']?.['stroke-disabled'] || DEFAULT_COLORS.point.large.strokeDisabled, + strokeEnabled: theme['point-l']?.['stroke-enabled'] || DEFAULT_COLORS.point.large.strokeEnabled, + strokeLocked: theme['point-l']?.['stroke-locked'] || DEFAULT_COLORS.point.large.strokeLocked, + strokeUnlocked: theme['point-l']?.['stroke-unlocked'] || DEFAULT_COLORS.point.large.strokeUnlocked, + } + }, + route: { + strokeActive: theme.route?.strokeActive || DEFAULT_COLORS.route.strokeActive, + stroke: { + 0: theme.route?.['stroke-0'] || DEFAULT_COLORS.route.stroke[0], + 1: theme.route?.['stroke-1'] || DEFAULT_COLORS.route.stroke[1], + 2: theme.route?.['stroke-2'] || DEFAULT_COLORS.route.stroke[2], + 10: theme.route?.['stroke-10'] || DEFAULT_COLORS.route.stroke[10], + }, + strokeEmpty: theme.route?.['stroke-empty'] || DEFAULT_COLORS.route.strokeEmpty, + strokeLoaded: theme.route?.['stroke-loaded'] || DEFAULT_COLORS.route.strokeLoaded, + }, + area: { + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.strokeActive, + stroke: { + 1: theme.area?.['stroke-1'] || DEFAULT_COLORS.area.stroke[1], + 11: theme.area?.['stroke-11'] || DEFAULT_COLORS.area.stroke[11], + 12: theme.area?.['stroke-12'] || DEFAULT_COLORS.area.stroke[12], + 13: theme.area?.['stroke-13'] || DEFAULT_COLORS.area.stroke[13], + 14: theme.area?.['stroke-14'] || DEFAULT_COLORS.area.stroke[14], + }, + fill: { + 1: theme.area?.['fill-1'] || DEFAULT_COLORS.area.fill[1], + 11: theme.area?.['fill-11'] || DEFAULT_COLORS.area.fill[11], + 12: theme.area?.['fill-12'] || DEFAULT_COLORS.area.fill[12], + 13: theme.area?.['fill-13'] || DEFAULT_COLORS.area.fill[13], + 14: theme.area?.['fill-14'] || DEFAULT_COLORS.area.fill[14], + } + }, + robot: { + stroke: theme.robot?.stroke || DEFAULT_COLORS.robot.stroke, + fill: theme.robot?.fill || DEFAULT_COLORS.robot.fill, + line: theme.robot?.line || DEFAULT_COLORS.robot.line, + strokeNormal: theme.robot?.['stroke-normal'] || DEFAULT_COLORS.robot.strokeNormal, + fillNormal: theme.robot?.['fill-normal'] || DEFAULT_COLORS.robot.fillNormal, + strokeWarning: theme.robot?.['stroke-warning'] || DEFAULT_COLORS.robot.strokeWarning, + fillWarning: theme.robot?.['fill-warning'] || DEFAULT_COLORS.robot.fillWarning, + strokeFault: theme.robot?.['stroke-fault'] || DEFAULT_COLORS.robot.strokeFault, + fillFault: theme.robot?.['fill-fault'] || DEFAULT_COLORS.robot.fillFault, + }, + autoDoor: { + strokeClosed: theme.autoDoor?.['stroke-closed'] || DEFAULT_COLORS.autoDoor.strokeClosed, + fillClosed: theme.autoDoor?.['fill-closed'] || DEFAULT_COLORS.autoDoor.fillClosed, + strokeOpen: theme.autoDoor?.['stroke-open'] || DEFAULT_COLORS.autoDoor.strokeOpen, + fillOpen: theme.autoDoor?.['fill-open'] || DEFAULT_COLORS.autoDoor.fillOpen, + }, + common: { + color: theme.color || DEFAULT_COLORS.common.color, + background: theme.background || DEFAULT_COLORS.common.background, + } + }); + } + } + + /** + * 深度合并配置对象 + */ + private mergeConfig(target: any, source: any): any { + const result = { ...target }; + for (const key in source) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + result[key] = this.mergeConfig(target[key] || {}, source[key]); + } else { + result[key] = source[key]; + } + } + return result; + } + + /** + * 获取嵌套对象的值 + */ + private getNestedValue(obj: any, path: string): any { + return path.split('.').reduce((current, key) => current?.[key], obj); + } + + /** + * 设置嵌套对象的值 + */ + private setNestedValue(obj: any, path: string, value: any): void { + const keys = path.split('.'); + const lastKey = keys.pop()!; + const target = keys.reduce((current, key) => { + if (!current[key]) { + // 如果是数字键,创建数组或对象 + if (!isNaN(Number(key))) { + current[key] = {}; + } else { + current[key] = {}; + } + } + return current[key]; + }, obj); + target[lastKey] = value; + } +} + +export default new ColorConfigService(); diff --git a/src/services/draw/storage-location-drawer.ts b/src/services/draw/storage-location-drawer.ts index f47d8ed..5767e65 100644 --- a/src/services/draw/storage-location-drawer.ts +++ b/src/services/draw/storage-location-drawer.ts @@ -1,6 +1,8 @@ import type { MapPen } from '@api/map'; import { LockState } from '@meta2d/core'; +import colorConfig from '../color-config.service'; + // 预加载锁定图标(仅一次) const lockedIcon = new Image(); lockedIcon.src = new URL('../../assets/icons/png/locked.png', import.meta.url).toString(); @@ -207,8 +209,10 @@ export function drawStorageLocation(ctx: CanvasRenderingContext2D, pen: MapPen): ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); - // 填充颜色:占用为红色,否则默认灰底 - ctx.fillStyle = occupied ? '#ff4d4f' : '#f5f5f5'; + // 使用配置的颜色,如果没有配置则使用默认值 + ctx.fillStyle = occupied + ? colorConfig.getColor('storage.occupied') || '#ff4d4f' + : colorConfig.getColor('storage.default') || '#f5f5f5'; ctx.fill(); ctx.strokeStyle = '#999999'; ctx.stroke(); @@ -269,14 +273,14 @@ export function drawStorageMore(ctx: CanvasRenderingContext2D, pen: MapPen): voi ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); - // 填充颜色和边框 - ctx.fillStyle = '#e6f4ff'; + // 使用配置的颜色,如果没有配置则使用默认值 + ctx.fillStyle = colorConfig.getColor('storage.moreButton.background') || '#e6f4ff'; ctx.fill(); - ctx.strokeStyle = '#1677ff'; + ctx.strokeStyle = colorConfig.getColor('storage.moreButton.border') || '#1677ff'; ctx.stroke(); // 绘制文字 - ctx.fillStyle = '#1677ff'; + ctx.fillStyle = colorConfig.getColor('storage.moreButton.text') || '#1677ff'; ctx.font = `${Math.floor(w * 0.6)}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 945f344..deb5a13 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -30,6 +30,7 @@ import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from ' import { reactive, watch } from 'vue'; import { AreaOperationService } from './area-operation.service'; +import colorConfig from './color-config.service'; import { drawStorageBackground, drawStorageLocation, @@ -80,6 +81,8 @@ export class EditorService extends Meta2d { const { robotGroups, robots, points, routes, areas, ...extraFields } = scene; // 保存所有额外字段(包括width、height等) this.#originalSceneData = extraFields; + + // 颜色配置现在使用本地存储,不再从场景数据加载 this.open(); this.setState(editable); @@ -118,6 +121,7 @@ export class EditorService extends Meta2d { routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)), areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)), blocks: [], + colorConfig: colorConfig.getConfigForSave(), // 添加颜色配置到场景数据 ...this.#originalSceneData, // 统一保留所有额外字段(包括width、height等) }; @@ -1386,6 +1390,9 @@ export class EditorService extends Meta2d { // 初始化库位服务 this.storageLocationService = new StorageLocationService(this, ''); + // 设置颜色配置服务的编辑器实例 + colorConfig.setEditorService(this); + // 禁用第6个子元素的拖放功能 (container.children.item(5)).ondrop = null; // 监听所有画布事件 @@ -1438,7 +1445,7 @@ export class EditorService extends Meta2d { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + #listen(e: unknown, v: any) { switch (e) { case 'opened': @@ -1584,13 +1591,13 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { if (isConnected === false) { // 未连接:深红色实心,不描边 - ctx.fillStyle = '#fe5a5ae0'; + ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0'; } else { // 已连接:根据门状态显示颜色(0=关门-浅红,1=开门-蓝色) if (deviceStatus === 0) { - ctx.fillStyle = '#cddc39'; + ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39'; } else { - ctx.fillStyle = '#1890FF'; + ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF'; } } ctx.fill(); @@ -1603,7 +1610,7 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { case MapPointType.等待点: case MapPointType.避让点: case MapPointType.临时避让点: - case MapPointType.库区点: + case MapPointType.库区点: { ctx.beginPath(); ctx.moveTo(x + w / 2 - r, y + r); ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r); @@ -1611,9 +1618,20 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r); ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r); ctx.closePath(); - ctx.fillStyle = get(theme, `point-s.fill-${type}`) ?? ''; + // 优先使用小点位专用颜色,如果没有则使用类型专用颜色 + const smallGeneralColor = colorConfig.getColor(`point.small.fill.${type}`); + const smallTypeColor = colorConfig.getColor(`point.types.${type}.fill`); + const smallThemeColor = get(theme, `point-s.fill-${type}`); + const finalColor = smallGeneralColor || smallTypeColor || smallThemeColor || ''; + + + ctx.fillStyle = finalColor; ctx.fill(); - ctx.strokeStyle = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke') ?? ''; + + const smallTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`); + const smallGeneralStrokeColor = colorConfig.getColor(active ? 'point.small.strokeActive' : 'point.small.stroke'); + const smallThemeStrokeColor = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke'); + ctx.strokeStyle = smallTypeStrokeColor || smallGeneralStrokeColor || smallThemeStrokeColor || ''; if (type === MapPointType.临时避让点) { ctx.lineCap = 'round'; ctx.beginPath(); @@ -1636,20 +1654,34 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { } ctx.stroke(); break; + } case MapPointType.电梯点: case MapPointType.自动门点: case MapPointType.充电点: case MapPointType.停靠点: case MapPointType.动作点: - case MapPointType.禁行点: + case MapPointType.禁行点: { ctx.roundRect(x, y, w, h, r); - ctx.strokeStyle = statusStyle ?? get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? ''; + + // 优先使用类型专用颜色,如果没有则使用通用颜色 + const largeTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`); + const largeGeneralStrokeColor = colorConfig.getColor(active ? 'point.large.strokeActive' : 'point.large.stroke'); + const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke'); + ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || ''); ctx.stroke(); + + // 填充颜色 + const largeTypeFillColor = colorConfig.getColor(`point.types.${type}.fill`); + if (largeTypeFillColor) { + ctx.fillStyle = largeTypeFillColor; + ctx.fill(); + } break; + } default: break; } - ctx.fillStyle = get(theme, 'color') ?? ''; + ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? ''); ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; @@ -1706,7 +1738,25 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.save(); ctx.beginPath(); - ctx.strokeStyle = get(theme, active ? 'route.strokeActive' : `route.stroke-${pass}`) ?? ''; + // 根据路线通行类型获取颜色 + let routeColor = ''; + if (active) { + routeColor = colorConfig.getColor('route.strokeActive') || get(theme, 'route.strokeActive') || ''; + } else { + // 根据通行类型选择颜色 + switch (pass) { + case MapRoutePassType.仅空载可通行: + routeColor = colorConfig.getColor('route.strokeEmpty') || get(theme, 'route.stroke-empty') || ''; + break; + case MapRoutePassType.仅载货可通行: + routeColor = colorConfig.getColor('route.strokeLoaded') || get(theme, 'route.stroke-loaded') || ''; + break; + default: + routeColor = colorConfig.getColor(`route.stroke.${pass}`) || get(theme, `route.stroke-${pass}`) || ''; + break; + } + } + ctx.strokeStyle = routeColor; ctx.lineWidth = active ? 3 * s : 2 * s; ctx.moveTo(x1, y1); switch (type) { @@ -1802,14 +1852,26 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.save(); ctx.rect(x, y, w, h); - ctx.fillStyle = get(theme, `area.fill-${type}`) ?? ''; + + // 优先使用通用颜色,如果没有则使用类型专用颜色 + const generalFillColor = colorConfig.getColor(`area.fill.${type}`); + const typeFillColor = colorConfig.getColor(`area.types.${type}.fill`); + const themeFillColor = get(theme, `area.fill-${type}`); + const finalFillColor = generalFillColor || typeFillColor || themeFillColor || ''; + + + ctx.fillStyle = finalFillColor; ctx.fill(); - ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? ''; + + const generalStrokeColor = colorConfig.getColor(active ? 'area.strokeActive' : `area.stroke.${type}`); + const typeStrokeColor = colorConfig.getColor(`area.types.${type}.stroke`); + const themeStrokeColor = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`); + ctx.strokeStyle = generalStrokeColor || typeStrokeColor || themeStrokeColor || ''; ctx.stroke(); // 如果是描述区且有描述内容,渲染描述文字 if (type === MapAreaType.描述区 && desc) { - ctx.fillStyle = get(theme, 'color') ?? ''; + ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? ''); // 动态计算字体大小,让文字填充区域 let descFontSize = Math.min(w / 6, h / 4, 200); @@ -1854,7 +1916,7 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { }); } else if (type !== MapAreaType.描述区 && label) { // 非描述区才显示标签 - ctx.fillStyle = get(theme, 'color') ?? ''; + ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? ''); ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; @@ -1905,12 +1967,12 @@ function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void { const oy = y + h / 2; ctx.save(); ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2); - ctx.fillStyle = get(theme, `robot.fill-${status}`) ?? get(theme, 'robot.fill') ?? ''; + ctx.fillStyle = colorConfig.getColor(`robot.fill${status === 'normal' ? '' : `-${status}`}`) || (get(theme, `robot.fill-${status}`) ?? (get(theme, 'robot.fill') ?? '')); ctx.fill(); - ctx.strokeStyle = get(theme, `robot.stroke-${status}`) ?? get(theme, 'robot.stroke') ?? ''; + ctx.strokeStyle = colorConfig.getColor(`robot.stroke${status === 'normal' ? '' : `-${status}`}`) || (get(theme, `robot.stroke-${status}`) ?? (get(theme, 'robot.stroke') ?? '')); ctx.stroke(); if (path?.length) { - ctx.strokeStyle = get(theme, 'robot.line') ?? ''; + ctx.strokeStyle = colorConfig.getColor('robot.line') || (get(theme, 'robot.line') ?? ''); ctx.lineCap = 'round'; ctx.lineWidth = s * 4; ctx.setLineDash([s * 5, s * 10]);