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 @@
+
+
+
编辑器颜色配置
+
+
+
+
小点位颜色
+
+
+
+ updateColor('point.small.fill.1', { target: { value } })"
+ />
+
+
+
+ updateColor('point.small.fill.2', { target: { value } })"
+ />
+
+
+
+ updateColor('point.small.fill.3', { target: { value } })"
+ />
+
+
+
+ updateColor('point.small.fill.4', { target: { value } })"
+ />
+
+
+
+ updateColor('point.small.fill.5', { target: { value } })"
+ />
+
+
+
+
+
+
路线颜色
+
+
+ updateColor('route.stroke.1', { target: { value } })"
+ />
+
+
+
+ updateColor('route.stroke.2', { target: { value } })"
+ />
+
+
+
+ updateColor('route.stroke.10', { target: { value } })"
+ />
+
+
+
+
+
+
区域颜色
+
+
+ updateColor('area.fill.1', { target: { value } })"
+ />
+
+
+
+ updateColor('area.fill.11', { target: { value } })"
+ />
+
+
+
+ updateColor('area.fill.12', { target: { value } })"
+ />
+
+
+
+ updateColor('area.fill.13', { target: { value } })"
+ />
+
+
+
+ updateColor('area.fill.14', { target: { value } })"
+ />
+
+
+
+
+
+
路线颜色
+
+
+ updateColor('route.strokeEmpty', { target: { value } })"
+ />
+
+
+
+ updateColor('route.strokeLoaded', { target: { value } })"
+ />
+
+
+
+
+
+
机器人颜色
+
+
+ updateColor('robot.fillNormal', { target: { value } })"
+ />
+
+
+
+ updateColor('robot.fillWarning', { target: { value } })"
+ />
+
+
+
+ updateColor('robot.fillFault', { target: { value } })"
+ />
+
+
+
+ updateColor('robot.line', { target: { value } })"
+ />
+
+
+
+
+
+
库位颜色
+
+
+ updateColor('storage.occupied', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.available', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.default', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.locked', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.moreButton.background', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.moreButton.border', { target: { value } })"
+ />
+
+
+
+ updateColor('storage.moreButton.text', { target: { value } })"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ {{ displayColor }}
+
+
+
+
+
+
+
+
+
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]);