From b4ac71a7175ebb522cfb6789b500183b42eb243b Mon Sep 17 00:00:00 2001 From: xudan Date: Mon, 8 Sep 2025 15:46:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E9=A2=9C=E8=89=B2?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A2=9C=E8=89=B2=E9=85=8D=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=92=8C=E5=B8=A6=E9=80=8F=E6=98=8E=E5=BA=A6=E7=9A=84?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E9=80=89=E6=8B=A9=E5=99=A8=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=A2=9C=E8=89=B2=E7=AE=A1=E7=90=86=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 | 2 +- src/components/color/README.md | 108 ++++++++ .../{ => color}/color-config-panel.vue | 249 +++++++++++++++--- .../{ => color}/color-picker-with-alpha.vue | 0 src/components/map-toolbar.vue | 2 +- src/pages/movement-supervision.vue | 2 +- .../{ => color}/color-config.service.ts | 228 +++++++++++++++- src/services/draw/storage-location-drawer.ts | 2 +- src/services/editor.service.ts | 26 +- 9 files changed, 570 insertions(+), 49 deletions(-) create mode 100644 src/components/color/README.md rename src/components/{ => color}/color-config-panel.vue (64%) rename src/components/{ => color}/color-picker-with-alpha.vue (100%) rename src/services/{ => color}/color-config.service.ts (62%) diff --git a/src/apis/scene/type.ts b/src/apis/scene/type.ts index 5f5cd8d..2681f06 100644 --- a/src/apis/scene/type.ts +++ b/src/apis/scene/type.ts @@ -1,7 +1,7 @@ import type { RobotGroup, RobotInfo } from '@api/robot'; import type { Meta2dData } from '@meta2d/core'; -import type { EditorColorConfig } from '../../services/color-config.service'; +import type { EditorColorConfig } from '../../services/color/color-config.service'; export interface SceneInfo { id: string; // 场景id diff --git a/src/components/color/README.md b/src/components/color/README.md new file mode 100644 index 0000000..b5eda5b --- /dev/null +++ b/src/components/color/README.md @@ -0,0 +1,108 @@ +# 颜色配置组件 + +## 概述 + +这个文件夹包含了所有与颜色配置相关的组件和服务,包括颜色配置面板、颜色选择器和颜色配置服务。 + +## 文件结构 + +``` +src/components/color/ +├── color-config-panel.vue # 颜色配置面板组件 +├── color-picker-with-alpha.vue # 带透明度的颜色选择器 +└── README.md # 说明文档 + +src/services/color/ +└── color-config.service.ts # 颜色配置服务 +``` + +## 新增功能:区域边框颜色配置 + +### 功能特性 + +1. **全局边框设置**: + - 默认边框宽度(1-10px) + - 默认透明度(15%) + - 边框样式固定为实线 + +2. **区域类型边框颜色**: + - 库区(类型1) + - 互斥区(类型11) + - 非互斥区(类型12) + - 约束区(类型13) + - 描述区(类型14) + +### 使用方法 + +#### 1. 在颜色配置面板中设置 + +打开颜色配置面板,在"区域颜色"部分找到"边框配置": + +- 调整全局边框设置(宽度、透明度) +- 为每种区域类型设置独立的边框颜色 + +#### 2. 通过代码设置 + +```typescript +import colorConfig from '@/services/color/color-config.service'; + +// 设置区域边框颜色 +colorConfig.setAreaBorderColor(1, '#FF0000'); // 库区红色边框 + +// 设置边框宽度 +colorConfig.setAreaBorderWidth(1, 3); // 3px宽度 + +// 设置边框透明度 +colorConfig.setAreaBorderOpacity(1, 0.15); // 15%透明度 + +// 批量设置 +colorConfig.setAreaBorderConfig(1, { + color: '#FF0000', + width: 3, + opacity: 0.15, +}); +``` + +#### 3. 获取当前配置 + +```typescript +// 获取边框颜色 +const borderColor = colorConfig.getAreaBorderColor(1); + +// 获取边框宽度 +const borderWidth = colorConfig.getAreaBorderWidth(1); + +// 获取边框透明度 +const borderOpacity = colorConfig.getAreaBorderOpacity(1); +``` + +### 测试功能 + +在颜色配置面板中点击"测试边框颜色"按钮,系统会: + +1. 在控制台输出当前所有区域类型的边框配置 +2. 自动设置一些测试颜色(库区红色、互斥区绿色、非互斥区蓝色) +3. 触发编辑器重新渲染以显示新的边框颜色 + +### 技术实现 + +- **实时渲染**:配置更改后立即触发编辑器重新渲染 +- **本地存储**:所有配置自动保存到localStorage +- **类型安全**:完整的TypeScript类型定义 +- **向后兼容**:保持现有API不变 + +### 注意事项 + +1. 边框颜色优先级:`area.border.colors[type]` > `area.types[type].borderColor` > 默认值 +2. 边框样式固定为实线 +3. 透明度范围:0-1(0%到100%) +4. 边框宽度范围:1-10像素 + +### 故障排除 + +如果边框颜色不生效,请检查: + +1. 确保区域类型正确(1, 11, 12, 13, 14) +2. 检查控制台是否有调试信息输出 +3. 确认颜色值格式正确(如:#FF0000) +4. 使用"测试边框颜色"功能验证配置是否正确 diff --git a/src/components/color-config-panel.vue b/src/components/color/color-config-panel.vue similarity index 64% rename from src/components/color-config-panel.vue rename to src/components/color/color-config-panel.vue index 7b619f5..fdd5c83 100644 --- a/src/components/color-config-panel.vue +++ b/src/components/color/color-config-panel.vue @@ -47,40 +47,119 @@

区域颜色

-
- - + + +
+
填充颜色
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- - -
-
- - + + +
+
边框配置
+ + +
+
+ + + px +
+
+ + + {{ Math.round(config.area.border.opacity * 100) }}% +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -218,9 +297,9 @@ @@ -417,6 +539,7 @@ const resetToDefault = () => { transition: all 0.2s; } + .btn-reset { color: #ff4d4f; border-color: #ff4d4f; @@ -485,6 +608,60 @@ const resetToDefault = () => { cursor: not-allowed; } +/* 边框配置样式 */ +.border-global-settings { + margin-bottom: 16px; + padding: 12px; + background: #f8f9fa; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.border-colors { + margin-top: 12px; +} + + +.opacity-slider { + width: 120px; + height: 6px; + border-radius: 3px; + background: #d9d9d9; + outline: none; + cursor: pointer; + -webkit-appearance: none; + appearance: none; +} + +.opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: #1890ff; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.opacity-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: #1890ff; + cursor: pointer; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.opacity-value { + font-size: 14px; + color: #666; + margin-left: 8px; + min-width: 35px; + text-align: right; +} + /* 滚动条样式 */ .color-config-panel::-webkit-scrollbar { width: 6px; diff --git a/src/components/color-picker-with-alpha.vue b/src/components/color/color-picker-with-alpha.vue similarity index 100% rename from src/components/color-picker-with-alpha.vue rename to src/components/color/color-picker-with-alpha.vue diff --git a/src/components/map-toolbar.vue b/src/components/map-toolbar.vue index 1b664b6..20e0e1a 100644 --- a/src/components/map-toolbar.vue +++ b/src/components/map-toolbar.vue @@ -4,7 +4,7 @@ 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'; +import ColorConfigPanel from './color/color-config-panel.vue'; // 通用地图工具栏(右下角),临时中文按钮 // 功能:放大、缩小、适配视图、全屏、截图、网格(占位) diff --git a/src/pages/movement-supervision.vue b/src/pages/movement-supervision.vue index c3d3e3e..305438f 100644 --- a/src/pages/movement-supervision.vue +++ b/src/pages/movement-supervision.vue @@ -10,7 +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 colorConfig from '../services/color/color-config.service'; import { type ContextMenuState, createContextMenuManager, diff --git a/src/services/color-config.service.ts b/src/services/color/color-config.service.ts similarity index 62% rename from src/services/color-config.service.ts rename to src/services/color/color-config.service.ts index 8709c52..ff77bd6 100644 --- a/src/services/color-config.service.ts +++ b/src/services/color/color-config.service.ts @@ -46,12 +46,22 @@ export interface EditorColorConfig { strokeActive: string; stroke: Record; // 按区域类型索引 fill: Record; // 按区域类型索引 + // 边框颜色配置 + border: { + width: number; // 边框宽度 + opacity: number; // 边框透明度 (0-1) + // 各类型区域边框颜色 + colors: Record; // 按区域类型索引的边框颜色 + }; // 各类型区域专用颜色 types: { [key: number]: { stroke: string; strokeActive: string; fill: string; + borderColor: string; // 边框颜色 + borderWidth: number; // 边框宽度 + borderOpacity: number; // 边框透明度 }; }; }; @@ -165,13 +175,60 @@ const DEFAULT_COLORS: EditorColorConfig = { 13: '#e61e4a33', 14: '#FFD70033' }, + // 边框配置 + border: { + width: 1, // 默认边框宽度 + opacity: 0.15, // 默认边框透明度 15% + colors: { + 1: '#1890FF', // 库区边框颜色 + 11: '#FF4D4F', // 互斥区边框颜色 + 12: '#52C41A', // 非互斥区边框颜色 + 13: '#FA8C16', // 约束区边框颜色 + 14: '#722ED1' // 描述区边框颜色 + } + }, 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' }, // 描述区 + 1: { + stroke: '#9ACDFF99', + strokeActive: '#EBB214', + fill: '#9ACDFF33', + borderColor: '#1890FF', + borderWidth: 1, + borderOpacity: 0.15 + }, // 库区 + 11: { + stroke: '#FF535399', + strokeActive: '#EBB214', + fill: '#FF9A9A33', + borderColor: '#FF4D4F', + borderWidth: 1, + borderOpacity: 0.15 + }, // 互斥区 + 12: { + stroke: '#0DBB8A99', + strokeActive: '#EBB214', + fill: '#0DBB8A33', + borderColor: '#52C41A', + borderWidth: 1, + borderOpacity: 0.15 + }, // 非互斥区 + 13: { + stroke: '#e61e4aad', + strokeActive: '#EBB214', + fill: '#e61e4a33', + borderColor: '#FA8C16', + borderWidth: 1, + borderOpacity: 0.15 + }, // 约束区 + 14: { + stroke: '#FFD70099', + strokeActive: '#EBB214', + fill: '#FFD70033', + borderColor: '#722ED1', + borderWidth: 1, + borderOpacity: 0.15 + }, // 描述区 } }, robot: { @@ -217,6 +274,7 @@ class ColorConfigService { private config = ref({ ...DEFAULT_COLORS }); private editorService: any = null; private readonly STORAGE_KEY = 'editor-color-config'; + private isResetting = false; // 标记是否正在重置 constructor() { // 从本地存储加载配置 @@ -234,7 +292,10 @@ class ColorConfigService { watch( () => this.config.value, (newConfig) => { - this.saveToLocalStorage(newConfig); + // 如果正在重置,不保存到本地存储 + if (!this.isResetting) { + this.saveToLocalStorage(newConfig); + } }, { deep: true } ); @@ -276,6 +337,11 @@ class ColorConfigService { * 从本地存储加载配置 */ private loadFromLocalStorage(): void { + // 如果正在重置,不加载本地存储 + if (this.isResetting) { + return; + } + try { const stored = localStorage.getItem(this.STORAGE_KEY); if (stored) { @@ -336,7 +402,18 @@ class ColorConfigService { * 重置为默认配置 */ public resetToDefault(): void { - this.config.value = { ...DEFAULT_COLORS }; + // 设置重置标记,防止自动保存 + this.isResetting = true; + + // 清除本地存储 + localStorage.removeItem(this.STORAGE_KEY); + + // 重置为默认配置,使用深度复制确保完全重置 + this.config.value = JSON.parse(JSON.stringify(DEFAULT_COLORS)); + + // 重置标记,允许后续自动保存 + this.isResetting = false; + // 触发编辑器重新渲染 this.triggerRender(); } @@ -366,10 +443,94 @@ class ColorConfigService { this.triggerRender(); } + /** + * 设置区域边框颜色 + * @param areaType 区域类型 + * @param color 边框颜色 + */ + public setAreaBorderColor(areaType: number, color: string): void { + this.setColor(`area.border.colors.${areaType}`, color); + this.setColor(`area.types.${areaType}.borderColor`, color); + } + + /** + * 设置区域边框宽度 + * @param areaType 区域类型 + * @param width 边框宽度 + */ + public setAreaBorderWidth(areaType: number, width: number): void { + this.setNestedValue(this.config.value, `area.types.${areaType}.borderWidth`, width); + this.triggerRender(); + } + + + /** + * 设置区域边框透明度 + * @param areaType 区域类型 + * @param opacity 透明度 (0-1) + */ + public setAreaBorderOpacity(areaType: number, opacity: number): void { + this.setNestedValue(this.config.value, `area.types.${areaType}.borderOpacity`, opacity); + this.triggerRender(); + } + + /** + * 获取区域边框颜色 + * @param areaType 区域类型 + */ + public getAreaBorderColor(areaType: number): string { + return this.getNestedValue(this.config.value, `area.border.colors.${areaType}`) || + this.getNestedValue(this.config.value, `area.types.${areaType}.borderColor`) || + this.config.value.area.border.colors[areaType] || ''; + } + + /** + * 获取区域边框宽度 + * @param areaType 区域类型 + */ + public getAreaBorderWidth(areaType: number): number { + return this.getNestedValue(this.config.value, `area.types.${areaType}.borderWidth`) || this.config.value.area.border.width; + } + + + /** + * 获取区域边框透明度 + * @param areaType 区域类型 + */ + public getAreaBorderOpacity(areaType: number): number { + return this.getNestedValue(this.config.value, `area.types.${areaType}.borderOpacity`) || this.config.value.area.border.opacity; + } + + /** + * 批量设置区域边框配置 + * @param areaType 区域类型 + * @param config 边框配置 + */ + public setAreaBorderConfig(areaType: number, config: { + color?: string; + width?: number; + opacity?: number; + }): void { + if (config.color !== undefined) { + this.setAreaBorderColor(areaType, config.color); + } + if (config.width !== undefined) { + this.setAreaBorderWidth(areaType, config.width); + } + if (config.opacity !== undefined) { + this.setAreaBorderOpacity(areaType, config.opacity); + } + } + /** * 从主题配置加载默认颜色 */ private loadConfig(): void { + // 如果正在重置,不加载主题配置 + if (this.isResetting) { + return; + } + const theme = sTheme.editor as any; if (theme) { // 从主题配置中加载颜色,如果没有则使用默认值 @@ -419,6 +580,59 @@ class ColorConfigService { 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], + }, + border: { + width: theme.area?.border?.width || DEFAULT_COLORS.area.border.width, + opacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.border.opacity, + colors: { + 1: theme.area?.border?.['color-1'] || DEFAULT_COLORS.area.border.colors[1], + 11: theme.area?.border?.['color-11'] || DEFAULT_COLORS.area.border.colors[11], + 12: theme.area?.border?.['color-12'] || DEFAULT_COLORS.area.border.colors[12], + 13: theme.area?.border?.['color-13'] || DEFAULT_COLORS.area.border.colors[13], + 14: theme.area?.border?.['color-14'] || DEFAULT_COLORS.area.border.colors[14], + } + }, + types: { + 1: { + stroke: theme.area?.['stroke-1'] || DEFAULT_COLORS.area.types[1].stroke, + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.types[1].strokeActive, + fill: theme.area?.['fill-1'] || DEFAULT_COLORS.area.types[1].fill, + borderColor: theme.area?.border?.['color-1'] || DEFAULT_COLORS.area.types[1].borderColor, + borderWidth: theme.area?.border?.width || DEFAULT_COLORS.area.types[1].borderWidth, + borderOpacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.types[1].borderOpacity, + }, + 11: { + stroke: theme.area?.['stroke-11'] || DEFAULT_COLORS.area.types[11].stroke, + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.types[11].strokeActive, + fill: theme.area?.['fill-11'] || DEFAULT_COLORS.area.types[11].fill, + borderColor: theme.area?.border?.['color-11'] || DEFAULT_COLORS.area.types[11].borderColor, + borderWidth: theme.area?.border?.width || DEFAULT_COLORS.area.types[11].borderWidth, + borderOpacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.types[11].borderOpacity, + }, + 12: { + stroke: theme.area?.['stroke-12'] || DEFAULT_COLORS.area.types[12].stroke, + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.types[12].strokeActive, + fill: theme.area?.['fill-12'] || DEFAULT_COLORS.area.types[12].fill, + borderColor: theme.area?.border?.['color-12'] || DEFAULT_COLORS.area.types[12].borderColor, + borderWidth: theme.area?.border?.width || DEFAULT_COLORS.area.types[12].borderWidth, + borderOpacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.types[12].borderOpacity, + }, + 13: { + stroke: theme.area?.['stroke-13'] || DEFAULT_COLORS.area.types[13].stroke, + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.types[13].strokeActive, + fill: theme.area?.['fill-13'] || DEFAULT_COLORS.area.types[13].fill, + borderColor: theme.area?.border?.['color-13'] || DEFAULT_COLORS.area.types[13].borderColor, + borderWidth: theme.area?.border?.width || DEFAULT_COLORS.area.types[13].borderWidth, + borderOpacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.types[13].borderOpacity, + }, + 14: { + stroke: theme.area?.['stroke-14'] || DEFAULT_COLORS.area.types[14].stroke, + strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.types[14].strokeActive, + fill: theme.area?.['fill-14'] || DEFAULT_COLORS.area.types[14].fill, + borderColor: theme.area?.border?.['color-14'] || DEFAULT_COLORS.area.types[14].borderColor, + borderWidth: theme.area?.border?.width || DEFAULT_COLORS.area.types[14].borderWidth, + borderOpacity: theme.area?.border?.opacity || DEFAULT_COLORS.area.types[14].borderOpacity, + } } }, robot: { diff --git a/src/services/draw/storage-location-drawer.ts b/src/services/draw/storage-location-drawer.ts index 5767e65..d65f9d1 100644 --- a/src/services/draw/storage-location-drawer.ts +++ b/src/services/draw/storage-location-drawer.ts @@ -1,7 +1,7 @@ import type { MapPen } from '@api/map'; import { LockState } from '@meta2d/core'; -import colorConfig from '../color-config.service'; +import colorConfig from '../color/color-config.service'; // 预加载锁定图标(仅一次) const lockedIcon = new Image(); diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 358a22e..130f9f9 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -30,7 +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 colorConfig from './color/color-config.service'; import { drawStorageBackground, drawStorageLocation, @@ -1895,10 +1895,32 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.fillStyle = finalFillColor; ctx.fill(); + // 获取边框颜色 - 优先使用新的边框颜色配置 + const borderColor = type ? colorConfig.getAreaBorderColor(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 || ''; + + // 获取边框宽度和透明度 + const borderWidth = type ? colorConfig.getAreaBorderWidth(type) : 1; + const borderOpacity = type ? colorConfig.getAreaBorderOpacity(type) : 0.15; + + + // 设置边框宽度和样式 + ctx.lineWidth = borderWidth; + ctx.setLineDash([]); // 固定为实线 + + // 优先使用边框颜色,然后是通用颜色,最后是主题颜色 + let finalStrokeColor = borderColor || generalStrokeColor || typeStrokeColor || themeStrokeColor || ''; + + // 应用透明度 + if (borderOpacity < 1 && finalStrokeColor.startsWith('#')) { + const alpha = Math.round(borderOpacity * 255).toString(16).padStart(2, '0'); + finalStrokeColor = finalStrokeColor + alpha; + } + + ctx.strokeStyle = finalStrokeColor; + ctx.stroke(); // 如果是描述区且有描述内容,渲染描述文字