feat: 添加颜色配置面板和相关功能,优化场景信息和绘制逻辑
This commit is contained in:
parent
32a8e04635
commit
9bd97f7248
@ -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;
|
||||
|
||||
388
src/components/color-config-panel.vue
Normal file
388
src/components/color-config-panel.vue
Normal file
@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="color-config-panel">
|
||||
<h3>编辑器颜色配置</h3>
|
||||
|
||||
<!-- 点位颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>小点位颜色</h4>
|
||||
|
||||
<div class="color-group">
|
||||
<label>普通点填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.point.small.fill[1]"
|
||||
@update:modelValue="(value) => updateColor('point.small.fill.1', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>等待点填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.point.small.fill[2]"
|
||||
@update:modelValue="(value) => updateColor('point.small.fill.2', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>避让点填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.point.small.fill[3]"
|
||||
@update:modelValue="(value) => updateColor('point.small.fill.3', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>临时避让点填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.point.small.fill[4]"
|
||||
@update:modelValue="(value) => updateColor('point.small.fill.4', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>库区点填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.point.small.fill[5]"
|
||||
@update:modelValue="(value) => updateColor('point.small.fill.5', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路线颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>路线颜色</h4>
|
||||
<div class="color-group">
|
||||
<label>可通行路线:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.route.stroke[1]"
|
||||
@update:modelValue="(value) => updateColor('route.stroke.1', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>双向路线:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.route.stroke[2]"
|
||||
@update:modelValue="(value) => updateColor('route.stroke.2', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>禁行路线:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.route.stroke[10]"
|
||||
@update:modelValue="(value) => updateColor('route.stroke.10', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区域颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>区域颜色</h4>
|
||||
<div class="color-group">
|
||||
<label>库区填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.area.fill[1]"
|
||||
@update:modelValue="(value) => updateColor('area.fill.1', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>互斥区填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.area.fill[11]"
|
||||
@update:modelValue="(value) => updateColor('area.fill.11', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>非互斥区填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.area.fill[12]"
|
||||
@update:modelValue="(value) => updateColor('area.fill.12', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>约束区填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.area.fill[13]"
|
||||
@update:modelValue="(value) => updateColor('area.fill.13', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>描述区填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.area.fill[14]"
|
||||
@update:modelValue="(value) => updateColor('area.fill.14', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 路线颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>路线颜色</h4>
|
||||
<div class="color-group">
|
||||
<label>空载路线:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.route.strokeEmpty"
|
||||
@update:modelValue="(value) => updateColor('route.strokeEmpty', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>载货路线:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.route.strokeLoaded"
|
||||
@update:modelValue="(value) => updateColor('route.strokeLoaded', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器人颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>机器人颜色</h4>
|
||||
<div class="color-group">
|
||||
<label>正常状态填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.robot.fillNormal"
|
||||
@update:modelValue="(value) => updateColor('robot.fillNormal', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>告警状态填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.robot.fillWarning"
|
||||
@update:modelValue="(value) => updateColor('robot.fillWarning', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>故障状态填充:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.robot.fillFault"
|
||||
@update:modelValue="(value) => updateColor('robot.fillFault', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>路径线条:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.robot.line"
|
||||
@update:modelValue="(value) => updateColor('robot.line', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库位颜色配置 -->
|
||||
<div class="color-section">
|
||||
<h4>库位颜色</h4>
|
||||
<div class="color-group">
|
||||
<label>占用状态:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.occupied"
|
||||
@update:modelValue="(value) => updateColor('storage.occupied', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>可用状态:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.available"
|
||||
@update:modelValue="(value) => updateColor('storage.available', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>默认状态:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.default"
|
||||
@update:modelValue="(value) => updateColor('storage.default', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>锁定状态:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.locked"
|
||||
@update:modelValue="(value) => updateColor('storage.locked', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>更多按钮背景:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.moreButton.background"
|
||||
@update:modelValue="(value) => updateColor('storage.moreButton.background', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>更多按钮边框:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.moreButton.border"
|
||||
@update:modelValue="(value) => updateColor('storage.moreButton.border', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label>更多按钮文字:</label>
|
||||
<ColorPickerWithAlpha
|
||||
:modelValue="config.storage.moreButton.text"
|
||||
@update:modelValue="(value) => updateColor('storage.moreButton.text', { target: { value } })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="actions">
|
||||
<button @click="resetToDefault" class="btn-reset">重置为默认</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
|
||||
import colorConfig from '../services/color-config.service';
|
||||
// 动态导入颜色选择器组件
|
||||
const ColorPickerWithAlpha = defineAsyncComponent(() => import('./color-picker-with-alpha.vue'));
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'ColorConfigPanel'
|
||||
});
|
||||
|
||||
const config = computed(() => colorConfig.currentConfig);
|
||||
|
||||
const updateColor = (path: string, event: Event | { target: { value: string } }) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target) {
|
||||
colorConfig.setColor(path, target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const resetToDefault = () => {
|
||||
colorConfig.resetToDefault();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-config-panel {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.color-config-panel h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.color-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.color-section h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #555;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.color-subsection {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.color-subsection h5 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.color-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.color-group label {
|
||||
min-width: 120px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.color-group input[type="color"] {
|
||||
width: 60px;
|
||||
height: 36px;
|
||||
border: 2px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.color-group input[type="color"]:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.color-group input[type="color"]:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: #fff2f0;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
.actions button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.color-config-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.color-config-panel::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.color-config-panel::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.color-config-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
356
src/components/color-picker-with-alpha.vue
Normal file
356
src/components/color-picker-with-alpha.vue
Normal file
@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="color-picker-with-alpha">
|
||||
<div class="color-preview" :style="{ backgroundColor: displayColor }" @click="showPicker = !showPicker">
|
||||
<span class="color-value">{{ displayColor }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showPicker" class="color-picker-popup" @click.stop>
|
||||
<!-- 基础颜色选择器 -->
|
||||
<div class="color-section">
|
||||
<label>基础颜色:</label>
|
||||
<input
|
||||
type="color"
|
||||
:value="baseColor"
|
||||
@input="updateBaseColor"
|
||||
class="base-color-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 透明度滑块 -->
|
||||
<div class="color-section">
|
||||
<label>透明度:</label>
|
||||
<div class="alpha-slider-container">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
:value="alphaValue"
|
||||
@input="updateAlpha"
|
||||
class="alpha-slider"
|
||||
/>
|
||||
<span class="alpha-value">{{ Math.round(alphaValue / 255 * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预设透明度选项 -->
|
||||
<div class="color-section">
|
||||
<label>快速选择:</label>
|
||||
<div class="alpha-presets">
|
||||
<button
|
||||
v-for="preset in alphaPresets"
|
||||
:key="preset.value"
|
||||
@click="setAlpha(preset.value)"
|
||||
:class="{ active: alphaValue === preset.value }"
|
||||
class="alpha-preset-btn"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="color-actions">
|
||||
<button @click="confirmColor" class="btn-confirm">确定</button>
|
||||
<button @click="cancelColor" class="btn-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'ColorPickerWithAlpha'
|
||||
});
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const showPicker = ref(false);
|
||||
const baseColor = ref('#FF0000');
|
||||
const alphaValue = ref(255);
|
||||
|
||||
// 透明度预设选项
|
||||
const alphaPresets = [
|
||||
{ label: '100%', value: 255 },
|
||||
{ label: '80%', value: 204 },
|
||||
{ label: '60%', value: 153 },
|
||||
{ label: '40%', value: 102 },
|
||||
{ label: '20%', value: 51 },
|
||||
{ label: '0%', value: 0 }
|
||||
];
|
||||
|
||||
// 计算显示的颜色值
|
||||
const displayColor = computed(() => {
|
||||
if (props.modelValue) {
|
||||
return props.modelValue;
|
||||
}
|
||||
return baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
|
||||
});
|
||||
|
||||
// 解析输入的颜色值
|
||||
const parseColor = (color: string) => {
|
||||
if (!color) return { base: '#FF0000', alpha: 255 };
|
||||
|
||||
// 如果是8位十六进制颜色
|
||||
if (color.match(/^#[0-9A-Fa-f]{8}$/)) {
|
||||
const base = color.substring(0, 7);
|
||||
const alpha = parseInt(color.substring(7, 9), 16);
|
||||
return { base, alpha };
|
||||
}
|
||||
|
||||
// 如果是6位十六进制颜色
|
||||
if (color.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||||
return { base: color, alpha: 255 };
|
||||
}
|
||||
|
||||
return { base: '#FF0000', alpha: 255 };
|
||||
};
|
||||
|
||||
// 初始化颜色值
|
||||
const initColor = () => {
|
||||
const { base, alpha } = parseColor(props.modelValue);
|
||||
baseColor.value = base;
|
||||
alphaValue.value = alpha;
|
||||
};
|
||||
|
||||
// 更新基础颜色
|
||||
const updateBaseColor = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
baseColor.value = target.value;
|
||||
// 实时更新颜色
|
||||
updateColorValue();
|
||||
};
|
||||
|
||||
// 更新透明度
|
||||
const updateAlpha = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
alphaValue.value = parseInt(target.value);
|
||||
// 实时更新颜色
|
||||
updateColorValue();
|
||||
};
|
||||
|
||||
// 设置透明度
|
||||
const setAlpha = (value: number) => {
|
||||
alphaValue.value = value;
|
||||
// 实时更新颜色
|
||||
updateColorValue();
|
||||
};
|
||||
|
||||
// 实时更新颜色值
|
||||
const updateColorValue = () => {
|
||||
const newColor = baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
|
||||
emit('update:modelValue', newColor);
|
||||
};
|
||||
|
||||
// 确认颜色选择
|
||||
const confirmColor = () => {
|
||||
const newColor = baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
|
||||
emit('update:modelValue', newColor);
|
||||
showPicker.value = false;
|
||||
};
|
||||
|
||||
// 取消颜色选择
|
||||
const cancelColor = () => {
|
||||
initColor();
|
||||
showPicker.value = false;
|
||||
};
|
||||
|
||||
// 监听外部颜色值变化
|
||||
watch(() => props.modelValue, () => {
|
||||
initColor();
|
||||
}, { immediate: true });
|
||||
|
||||
// 点击外部关闭选择器
|
||||
const handleClickOutside = (event: Event) => {
|
||||
if (!(event.target as Element).closest('.color-picker-with-alpha')) {
|
||||
showPicker.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加全局点击监听
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-picker-with-alpha {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 2px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color 0.2s;
|
||||
background-image:
|
||||
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 8px 8px;
|
||||
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
|
||||
}
|
||||
|
||||
.color-preview:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.color-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.color-picker-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 16px;
|
||||
min-width: 280px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.color-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.color-section label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.base-color-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alpha-slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.alpha-slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(to right, transparent, #000);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alpha-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.alpha-value {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alpha-presets {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.alpha-preset-btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.alpha-preset-btn:hover {
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
.alpha-preset-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-confirm,
|
||||
.btn-cancel {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: white;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
</style>
|
||||
@ -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({
|
||||
</a-button>
|
||||
<a-button class="icon-btn tool-btn" size="small" :title="'标尺'" @click="toggleRule">标尺</a-button>
|
||||
<a-button class="icon-btn tool-btn" size="small" :title="'网格'" @click="toggleGrid">网格</a-button>
|
||||
<a-button
|
||||
class="icon-btn tool-btn"
|
||||
size="small"
|
||||
:title="'颜色配置'"
|
||||
:type="showColorConfig ? 'primary' : 'default'"
|
||||
:ghost="!showColorConfig"
|
||||
@click="toggleColorConfig"
|
||||
>
|
||||
<SvgIcon name="palette" :size="16" :forceColor="true" :active="showColorConfig" />
|
||||
</a-button>
|
||||
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'截图'" @click="exportImage">截图</a-button>
|
||||
<a-button
|
||||
class="icon-btn tool-btn"
|
||||
@ -184,6 +213,26 @@ defineOptions({
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 颜色配置面板 -->
|
||||
<div v-if="showColorConfig" class="color-config-overlay" @click.self="showColorConfig = false">
|
||||
<div class="color-config-container" @click.stop>
|
||||
<div class="color-config-header">
|
||||
<h3>编辑器颜色配置</h3>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showColorConfig = false"
|
||||
class="close-btn"
|
||||
>
|
||||
<SvgIcon name="close" :size="16" />
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="color-config-content">
|
||||
<ColorConfigPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测量叠加层(全屏SVG),仅在测量模式下启用点击事件 -->
|
||||
<div
|
||||
class="measure-overlay"
|
||||
@ -201,15 +250,15 @@ defineOptions({
|
||||
<line
|
||||
:x1="start.x"
|
||||
:y1="start.y"
|
||||
:x2="(end || current)!.x"
|
||||
:y2="(end || current)!.y"
|
||||
:x2="(end || current)?.x || 0"
|
||||
:y2="(end || current)?.y || 0"
|
||||
stroke="#1e90ff"
|
||||
stroke-width="2"
|
||||
marker-end="url(#arrow)"
|
||||
/>
|
||||
<!-- 起点/终点标记 -->
|
||||
<circle :cx="start.x" :cy="start.y" r="4" fill="#1e90ff" />
|
||||
<circle :cx="(end || current)!.x" :cy="(end || current)!.y" r="4" fill="#1e90ff" />
|
||||
<circle :cx="(end || current)?.x || 0" :cy="(end || current)?.y || 0" r="4" fill="#1e90ff" />
|
||||
<!-- 文本标签(像素距离) -->
|
||||
<g :transform="`translate(${midpoint.x}, ${midpoint.y})`">
|
||||
<rect x="-40" y="-22" width="80" height="20" rx="4" ry="4" fill="rgba(0,0,0,0.6)" />
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
483
src/services/color-config.service.ts
Normal file
483
src/services/color-config.service.ts
Normal file
@ -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<number, string>; // 按点位类型索引
|
||||
};
|
||||
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<number, string>; // 按路线类型索引
|
||||
// 空载和载货路线专用颜色
|
||||
strokeEmpty: string; // 空载路线颜色
|
||||
strokeLoaded: string; // 载货路线颜色
|
||||
};
|
||||
|
||||
// 区域颜色
|
||||
area: {
|
||||
strokeActive: string;
|
||||
stroke: Record<number, string>; // 按区域类型索引
|
||||
fill: Record<number, string>; // 按区域类型索引
|
||||
// 各类型区域专用颜色
|
||||
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<EditorColorConfig>({ ...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<EditorColorConfig>): 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();
|
||||
@ -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';
|
||||
|
||||
@ -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个子元素的拖放功能
|
||||
(<HTMLDivElement>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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user