feat: 移除旧的上下文菜单组件,增强机器人菜单功能,新增自定义图片设置和关闭按钮,优化用户交互体验
This commit is contained in:
parent
a88876697f
commit
8f6087cea4
@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<!-- 使用新的模块化组件 -->
|
||||
<ContextMenu
|
||||
:visible="visible"
|
||||
:x="x"
|
||||
:y="y"
|
||||
:menu-type="menuType"
|
||||
:storage-locations="storageLocations"
|
||||
:robot-info="robotInfo"
|
||||
:api-base-url="apiBaseUrl"
|
||||
@close="$emit('close')"
|
||||
@action-complete="$emit('actionComplete', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ContextMenu } from './context-menu';
|
||||
import type { StorageLocationInfo } from '../services/context-menu';
|
||||
import type { RobotInfo } from '../services/context-menu';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
x?: number;
|
||||
y?: number;
|
||||
menuType?: 'default' | 'storage' | 'storage-background' | 'robot' | 'point' | 'area';
|
||||
storageLocations?: StorageLocationInfo[];
|
||||
robotInfo?: RobotInfo;
|
||||
apiBaseUrl?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void;
|
||||
(e: 'actionComplete', data: any): void;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
defineEmits<Emits>();
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'ContextMenu',
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 样式已移至各个子组件中 -->
|
@ -16,6 +16,9 @@
|
||||
<div class="context-menu-overlay">
|
||||
<div class="context-menu-header">
|
||||
<div class="menu-title">{{ headerTitle }}</div>
|
||||
<div class="close-button" @click="handleCloseMenu" title="关闭菜单">
|
||||
<span class="close-icon">×</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库位菜单 -->
|
||||
@ -33,6 +36,7 @@
|
||||
v-else-if="menuType === 'robot' && robotInfo"
|
||||
:robot-info="robotInfo"
|
||||
@action-complete="handleActionComplete"
|
||||
@custom-image="handleCustomImage"
|
||||
/>
|
||||
|
||||
<!-- 默认菜单 -->
|
||||
@ -48,8 +52,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
|
||||
import type { RobotInfo } from '../../services/context-menu';
|
||||
import type { StorageLocationInfo } from '../../services/context-menu';
|
||||
import type { RobotInfo, StorageLocationInfo } from '../../services/context-menu';
|
||||
|
||||
// 使用动态导入避免 TypeScript 错误
|
||||
const DefaultMenu = defineAsyncComponent(() => import('./default-menu.vue'));
|
||||
const RobotMenu = defineAsyncComponent(() => import('./robot-menu.vue'));
|
||||
@ -68,6 +72,7 @@ interface Props {
|
||||
interface Emits {
|
||||
(e: 'close'): void;
|
||||
(e: 'actionComplete', data: any): void;
|
||||
(e: 'customImage', data: { robotInfo: RobotInfo }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@ -126,11 +131,20 @@ const handleOpenChange = (open: boolean) => {
|
||||
const handleActionComplete = (data: any) => {
|
||||
console.log('菜单操作完成:', data);
|
||||
emit('actionComplete', data);
|
||||
|
||||
// 某些操作完成后关闭菜单
|
||||
if (data.action && ['occupy', 'release', 'lock', 'unlock', 'enable', 'disable'].includes(data.action)) {
|
||||
emit('close');
|
||||
}
|
||||
// 所有操作都不关闭菜单,只有关闭按钮才能关闭
|
||||
};
|
||||
|
||||
// 处理自定义图片事件
|
||||
const handleCustomImage = (data: { robotInfo: RobotInfo }) => {
|
||||
console.log('打开自定义图片设置:', data.robotInfo);
|
||||
emit('customImage', data);
|
||||
// 不关闭菜单,只有关闭按钮才能关闭
|
||||
};
|
||||
|
||||
// 处理关闭按钮点击
|
||||
const handleCloseMenu = () => {
|
||||
console.log('用户点击关闭按钮');
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -169,6 +183,9 @@ const handleActionComplete = (data: any) => {
|
||||
background-color: #fafafa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 黑色主题菜单头部 */
|
||||
@ -186,4 +203,48 @@ const handleActionComplete = (data: any) => {
|
||||
:root[theme='dark'] .menu-title {
|
||||
color: #ffffffd9 !important;
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 */
|
||||
.close-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: #666;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover .close-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 黑色主题关闭按钮 */
|
||||
:root[theme='dark'] .close-button {
|
||||
background-color: #424242;
|
||||
}
|
||||
|
||||
:root[theme='dark'] .close-button:hover {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
:root[theme='dark'] .close-icon {
|
||||
color: #ffffffd9;
|
||||
}
|
||||
</style>
|
||||
|
@ -93,6 +93,10 @@
|
||||
<!-- 系统管理 -->
|
||||
<div class="action-group">
|
||||
<div class="action-group-title">系统管理</div>
|
||||
<div class="action-item" @click="handleRobotAction('custom_image', '自定义图片')">
|
||||
<span class="action-icon">🖼️</span>
|
||||
<span>自定义图片</span>
|
||||
</div>
|
||||
<div class="action-item" @click="handleRobotAction('reset', '重置')">
|
||||
<span class="action-icon">🔄</span>
|
||||
<span>重置</span>
|
||||
@ -109,27 +113,67 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器人图片设置模态框 -->
|
||||
<RobotImageSettingsModal
|
||||
v-model:open="imageSettingsVisible"
|
||||
:robots="availableRobots"
|
||||
:selected-robot-name="selectedRobotName"
|
||||
@save="handleImageSettingsSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RobotAction,RobotInfo } from '../../services/context-menu/robot-menu.service';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { RobotAction, RobotInfo } from '../../services/context-menu/robot-menu.service';
|
||||
import {
|
||||
executeRobotAction,
|
||||
getRobotStatusColor,
|
||||
getRobotStatusText} from '../../services/context-menu/robot-menu.service';
|
||||
getRobotStatusText
|
||||
} from '../../services/context-menu/robot-menu.service';
|
||||
|
||||
interface Props {
|
||||
robotInfo?: RobotInfo;
|
||||
availableRobots?: Array<{ name: string; type: string; id: string }>;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'actionComplete', data: { action: RobotAction; robot: RobotInfo; success: boolean }): void;
|
||||
(e: 'customImage', data: { robotInfo: RobotInfo }): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
availableRobots: () => []
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 图片设置模态框状态
|
||||
const imageSettingsVisible = ref(false);
|
||||
const selectedRobotName = ref('');
|
||||
|
||||
// 生成可用的机器人列表
|
||||
const availableRobots = computed(() => {
|
||||
// 如果父组件传递了机器人列表,使用父组件的
|
||||
if (props.availableRobots && props.availableRobots.length > 0) {
|
||||
return props.availableRobots;
|
||||
}
|
||||
|
||||
// 否则,基于当前机器人信息生成列表
|
||||
if (props.robotInfo) {
|
||||
return [{
|
||||
name: props.robotInfo.name,
|
||||
type: 'robot', // 默认类型
|
||||
id: props.robotInfo.id
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'RobotMenu',
|
||||
@ -152,10 +196,36 @@ const getBatteryClass = (batteryLevel: number) => {
|
||||
return 'battery-low';
|
||||
};
|
||||
|
||||
// 处理自定义图片操作
|
||||
const handleCustomImage = () => {
|
||||
if (!props.robotInfo?.name) {
|
||||
message.error('未找到机器人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('打开机器人图片设置:', props.robotInfo);
|
||||
|
||||
// 打开模态框,不关闭右键菜单
|
||||
selectedRobotName.value = props.robotInfo.name;
|
||||
imageSettingsVisible.value = true;
|
||||
console.log('设置模态框可见性:', imageSettingsVisible.value);
|
||||
|
||||
// 只触发自定义图片事件,不触发操作完成事件
|
||||
emit('customImage', {
|
||||
robotInfo: props.robotInfo
|
||||
});
|
||||
};
|
||||
|
||||
// 处理机器人操作
|
||||
const handleRobotAction = async (action: RobotAction, actionName: string) => {
|
||||
const handleRobotAction = async (action: RobotAction | 'custom_image', actionName: string) => {
|
||||
if (!props.robotInfo) return;
|
||||
|
||||
// 处理自定义图片操作
|
||||
if (action === 'custom_image') {
|
||||
handleCustomImage();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`执行机器人操作: ${action} (${actionName})`, props.robotInfo);
|
||||
|
||||
@ -173,7 +243,6 @@ const handleRobotAction = async (action: RobotAction, actionName: string) => {
|
||||
} catch (error) {
|
||||
console.error(`机器人${actionName}操作失败:`, error);
|
||||
|
||||
// 发送操作失败事件
|
||||
emit('actionComplete', {
|
||||
action,
|
||||
robot: props.robotInfo,
|
||||
@ -181,6 +250,15 @@ const handleRobotAction = async (action: RobotAction, actionName: string) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片设置保存
|
||||
const handleImageSettingsSave = (data: any) => {
|
||||
console.log('机器人图片设置保存:', data);
|
||||
message.success('机器人图片设置保存成功');
|
||||
|
||||
// 可以在这里添加额外的保存后处理逻辑
|
||||
// 比如通知父组件更新机器人显示等
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -218,35 +218,6 @@ const hideSubMenu = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟隐藏子菜单(已移除,由 Ant Design 处理)
|
||||
// const hideSubMenuDelayed = () => {
|
||||
// if (hideTimer.value) {
|
||||
// clearTimeout(hideTimer.value);
|
||||
// }
|
||||
// hideTimer.value = window.setTimeout(() => {
|
||||
// showSubMenu.value = null;
|
||||
// hideTimer.value = null;
|
||||
// }, 150); // 150ms延迟,给用户足够时间移动到子菜单
|
||||
// };
|
||||
|
||||
// 处理库位项鼠标离开(已移除,由 Ant Design 处理)
|
||||
// const handleStorageMouseLeave = () => {
|
||||
// hideSubMenuDelayed();
|
||||
// };
|
||||
|
||||
// 处理子菜单鼠标进入(已移除,由 Ant Design 处理)
|
||||
// const handleSubMenuMouseEnter = () => {
|
||||
// // 清除隐藏定时器
|
||||
// if (hideTimer.value) {
|
||||
// clearTimeout(hideTimer.value);
|
||||
// hideTimer.value = null;
|
||||
// }
|
||||
// };
|
||||
|
||||
// 处理子菜单鼠标离开(已移除,由 Ant Design 处理)
|
||||
// const handleSubMenuMouseLeave = () => {
|
||||
// hideSubMenu();
|
||||
// };
|
||||
|
||||
// 处理子菜单开关变化
|
||||
const handleSubMenuOpenChange = (open: boolean) => {
|
||||
|
401
src/components/modal/robot-image-settings-modal.vue
Normal file
401
src/components/modal/robot-image-settings-modal.vue
Normal file
@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="props.open"
|
||||
title="机器人图片设置"
|
||||
width="800px"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleSave"
|
||||
@cancel="handleCancel"
|
||||
@update:open="(value) => emit('update:open', value)"
|
||||
>
|
||||
<div class="robot-image-settings">
|
||||
<a-form :model="formData" layout="vertical">
|
||||
<!-- 机器人选择 -->
|
||||
<a-form-item label="选择机器人">
|
||||
<a-select
|
||||
v-model:value="selectedRobot"
|
||||
placeholder="请选择要设置图片的机器人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="robot in availableRobots"
|
||||
:key="robot.name"
|
||||
:value="robot.name"
|
||||
>
|
||||
{{ robot.name }} ({{ robot.type }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 图片设置区域 -->
|
||||
<div v-if="selectedRobot" class="image-settings-section">
|
||||
<a-divider>图片设置</a-divider>
|
||||
|
||||
<!-- 普通状态图片 -->
|
||||
<a-form-item label="普通状态图片">
|
||||
<div class="image-upload-container">
|
||||
<a-upload
|
||||
:file-list="[]"
|
||||
:before-upload="(file) => handleImageUpload(file, 'normal')"
|
||||
accept="image/*"
|
||||
:show-upload-list="false"
|
||||
>
|
||||
<a-button :loading="uploading.normal">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
选择普通状态图片
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<div v-if="formData.images.normal" class="image-preview">
|
||||
<img :src="formData.images.normal" alt="普通状态" />
|
||||
<div class="image-actions">
|
||||
<a-button size="small" @click="removeImage('normal')">删除</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 激活状态图片 -->
|
||||
<a-form-item label="激活状态图片">
|
||||
<div class="image-upload-container">
|
||||
<a-upload
|
||||
:file-list="[]"
|
||||
:before-upload="(file) => handleImageUpload(file, 'active')"
|
||||
accept="image/*"
|
||||
:show-upload-list="false"
|
||||
>
|
||||
<a-button :loading="uploading.active">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
选择激活状态图片
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<div v-if="formData.images.active" class="image-preview">
|
||||
<img :src="formData.images.active" alt="激活状态" />
|
||||
<div class="image-actions">
|
||||
<a-button size="small" @click="removeImage('active')">删除</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 图片尺寸设置 -->
|
||||
<a-form-item label="图片尺寸">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-input-number
|
||||
v-model:value="formData.imageWidth"
|
||||
:min="20"
|
||||
:max="200"
|
||||
placeholder="宽度"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span class="size-label">宽度 (px)</span>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input-number
|
||||
v-model:value="formData.imageHeight"
|
||||
:min="20"
|
||||
:max="200"
|
||||
placeholder="高度"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span class="size-label">高度 (px)</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button @click="resetImages">重置图片</a-button>
|
||||
<a-button danger @click="clearAllImages">清除所有图片</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import colorConfig from '../../services/color/color-config.service';
|
||||
|
||||
interface RobotInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
robots: RobotInfo[];
|
||||
selectedRobotName?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', open: boolean): void;
|
||||
(e: 'update:selectedRobotName', name: string): void;
|
||||
(e: 'save', data: any): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
robots: () => [],
|
||||
selectedRobotName: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 响应式数据
|
||||
const selectedRobot = ref<string>(props.selectedRobotName);
|
||||
const loading = ref(false);
|
||||
const uploading = ref({
|
||||
normal: false,
|
||||
active: false
|
||||
});
|
||||
|
||||
const formData = ref({
|
||||
images: {
|
||||
normal: '',
|
||||
active: ''
|
||||
},
|
||||
imageWidth: 42,
|
||||
imageHeight: 76
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const availableRobots = computed(() => props.robots);
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.open && props.selectedRobotName) {
|
||||
selectedRobot.value = props.selectedRobotName;
|
||||
loadRobotImages(props.selectedRobotName);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听选中机器人变化
|
||||
watch(selectedRobot, (newRobot) => {
|
||||
if (newRobot) {
|
||||
loadRobotImages(newRobot);
|
||||
emit('update:selectedRobotName', newRobot);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.selectedRobotName, (newName) => {
|
||||
console.log('模态框接收到selectedRobotName变化:', newName);
|
||||
if (newName && newName !== selectedRobot.value) {
|
||||
selectedRobot.value = newName;
|
||||
console.log('设置selectedRobot为:', newName);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听open状态变化
|
||||
watch(() => props.open, (isOpen) => {
|
||||
console.log('模态框open状态变化:', isOpen);
|
||||
if (isOpen) {
|
||||
// 如果有选中的机器人名称,则设置并加载数据
|
||||
if (props.selectedRobotName) {
|
||||
selectedRobot.value = props.selectedRobotName;
|
||||
loadRobotImages(props.selectedRobotName);
|
||||
console.log('模态框打开,设置selectedRobot为:', props.selectedRobotName);
|
||||
}
|
||||
} else {
|
||||
// 关闭时重置所有状态
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
|
||||
// 加载机器人图片
|
||||
const loadRobotImages = (robotName: string) => {
|
||||
formData.value.images.normal = colorConfig.getRobotCustomImage(robotName, 'normal') || '';
|
||||
formData.value.images.active = colorConfig.getRobotCustomImage(robotName, 'active') || '';
|
||||
|
||||
// 加载图片尺寸配置
|
||||
formData.value.imageWidth = Number(colorConfig.getColor('robot.imageWidth')) || 42;
|
||||
formData.value.imageHeight = Number(colorConfig.getColor('robot.imageHeight')) || 76;
|
||||
};
|
||||
|
||||
// 处理图片上传
|
||||
const handleImageUpload = async (file: File, state: 'normal' | 'active') => {
|
||||
if (!selectedRobot.value) {
|
||||
message.error('请先选择机器人');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('请选择图片文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
message.error('图片文件大小不能超过2MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
uploading.value[state] = true;
|
||||
|
||||
try {
|
||||
await colorConfig.saveRobotCustomImage(selectedRobot.value, state, file);
|
||||
formData.value.images[state] = colorConfig.getRobotCustomImage(selectedRobot.value, state) || '';
|
||||
message.success(`${state === 'normal' ? '普通状态' : '激活状态'}图片上传成功`);
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
message.error('图片上传失败,请重试');
|
||||
} finally {
|
||||
uploading.value[state] = false;
|
||||
}
|
||||
|
||||
return false; // 阻止默认上传行为
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const removeImage = (state: 'normal' | 'active') => {
|
||||
if (!selectedRobot.value) return;
|
||||
|
||||
colorConfig.removeRobotCustomImage(selectedRobot.value, state);
|
||||
formData.value.images[state] = '';
|
||||
message.success(`${state === 'normal' ? '普通状态' : '激活状态'}图片已删除`);
|
||||
};
|
||||
|
||||
// 重置图片
|
||||
const resetImages = () => {
|
||||
if (!selectedRobot.value) return;
|
||||
|
||||
colorConfig.removeRobotCustomImage(selectedRobot.value);
|
||||
formData.value.images.normal = '';
|
||||
formData.value.images.active = '';
|
||||
message.success('图片已重置');
|
||||
};
|
||||
|
||||
// 清除所有图片
|
||||
const clearAllImages = () => {
|
||||
if (!selectedRobot.value) return;
|
||||
|
||||
colorConfig.removeRobotCustomImage(selectedRobot.value);
|
||||
formData.value.images.normal = '';
|
||||
formData.value.images.active = '';
|
||||
message.success('所有图片已清除');
|
||||
};
|
||||
|
||||
// 保存设置
|
||||
const handleSave = async () => {
|
||||
if (!selectedRobot.value) {
|
||||
message.error('请选择机器人');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 保存图片尺寸配置
|
||||
if (formData.value.imageWidth !== Number(colorConfig.getColor('robot.imageWidth'))) {
|
||||
colorConfig.setColor('robot.imageWidth', formData.value.imageWidth.toString());
|
||||
}
|
||||
if (formData.value.imageHeight !== Number(colorConfig.getColor('robot.imageHeight'))) {
|
||||
colorConfig.setColor('robot.imageHeight', formData.value.imageHeight.toString());
|
||||
}
|
||||
|
||||
emit('save', {
|
||||
robotName: selectedRobot.value,
|
||||
images: formData.value.images,
|
||||
imageWidth: formData.value.imageWidth,
|
||||
imageHeight: formData.value.imageHeight
|
||||
});
|
||||
|
||||
message.success('设置保存成功');
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error);
|
||||
message.error('保存设置失败,请重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:open', false);
|
||||
// 重置表单
|
||||
selectedRobot.value = '';
|
||||
formData.value = {
|
||||
images: {
|
||||
normal: '',
|
||||
active: ''
|
||||
},
|
||||
imageWidth: 42,
|
||||
imageHeight: 76
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.robot-image-settings {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.image-settings-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.image-upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 120px;
|
||||
max-height: 120px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.image-preview:hover .image-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.size-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
@ -407,13 +407,16 @@ const handleActionComplete = (data: any) => {
|
||||
* @param event 点击事件
|
||||
*/
|
||||
const handleGlobalClick = (event: MouseEvent) => {
|
||||
// 如果右键菜单可见,检查点击是否在菜单内
|
||||
if (contextMenuState.value.visible) {
|
||||
// 如果点击不在菜单内,则关闭菜单
|
||||
if (!isClickInsideMenu(event, contextMenuState.value.visible)) {
|
||||
contextMenuManager.close();
|
||||
}
|
||||
|
||||
// 检查是否点击了关闭按钮
|
||||
const closeButton = (event.target as Element)?.closest('.close-button');
|
||||
if (closeButton) {
|
||||
// 如果点击了关闭按钮,让按钮自己的点击事件处理关闭
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -98,6 +98,14 @@ export interface EditorColorConfig {
|
||||
// 机器人图片尺寸
|
||||
imageWidth: number; // 机器人图片宽度
|
||||
imageHeight: number; // 机器人图片高度
|
||||
// 单个机器人自定义图片配置
|
||||
customImages: {
|
||||
[robotName: string]: {
|
||||
normal: string; // Base64编码的普通状态图片
|
||||
active: string; // Base64编码的激活状态图片
|
||||
};
|
||||
};
|
||||
useCustomImages: boolean; // 是否启用自定义图片
|
||||
};
|
||||
|
||||
// 库位颜色
|
||||
@ -318,7 +326,9 @@ const DEFAULT_COLORS: EditorColorConfig = {
|
||||
strokeFault: '#FF4D4F99',
|
||||
fillFault: '#FF4D4F33',
|
||||
imageWidth: 42, // 机器人图片宽度 - 42像素
|
||||
imageHeight: 76 // 机器人图片高度 - 76像素
|
||||
imageHeight: 76, // 机器人图片高度 - 76像素
|
||||
customImages: {}, // 单个机器人自定义图片配置
|
||||
useCustomImages: false // 默认不启用自定义图片
|
||||
},
|
||||
storage: {
|
||||
occupied: '#ff4d4f',
|
||||
@ -465,7 +475,9 @@ const DARK_THEME_COLORS: EditorColorConfig = {
|
||||
strokeFault: '#FF4D4F99',
|
||||
fillFault: '#FF4D4F33',
|
||||
imageWidth: 42, // 机器人图片宽度 - 42像素
|
||||
imageHeight: 76 // 机器人图片高度 - 76像素
|
||||
imageHeight: 76, // 机器人图片高度 - 76像素
|
||||
customImages: {}, // 单个机器人自定义图片配置
|
||||
useCustomImages: false // 默认不启用自定义图片
|
||||
},
|
||||
storage: {
|
||||
occupied: '#ff4d4f',
|
||||
@ -879,7 +891,9 @@ class ColorConfigService {
|
||||
strokeFault: theme.robot['stroke-fault'] || DEFAULT_COLORS.robot.strokeFault,
|
||||
fillFault: theme.robot['fill-fault'] || DEFAULT_COLORS.robot.fillFault,
|
||||
imageWidth: DEFAULT_COLORS.robot.imageWidth,
|
||||
imageHeight: DEFAULT_COLORS.robot.imageHeight
|
||||
imageHeight: DEFAULT_COLORS.robot.imageHeight,
|
||||
customImages: DEFAULT_COLORS.robot.customImages,
|
||||
useCustomImages: DEFAULT_COLORS.robot.useCustomImages
|
||||
};
|
||||
}
|
||||
|
||||
@ -1009,6 +1023,152 @@ class ColorConfigService {
|
||||
}, obj);
|
||||
target[lastKey] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换为Base64字符串
|
||||
* @param file 图片文件
|
||||
* @param maxWidth 最大宽度,用于压缩
|
||||
* @param quality 压缩质量 (0-1)
|
||||
*/
|
||||
public async fileToBase64(file: File, maxWidth: number = 200, quality: number = 0.8): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// 创建canvas进行图片压缩
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// 计算压缩后的尺寸
|
||||
const ratio = Math.min(maxWidth / img.width, maxWidth / img.height);
|
||||
canvas.width = img.width * ratio;
|
||||
canvas.height = img.height * ratio;
|
||||
|
||||
// 绘制压缩后的图片
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 转换为Base64
|
||||
const base64 = canvas.toDataURL('image/png', quality);
|
||||
resolve(base64);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = reader.result as string;
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存单个机器人的自定义图片
|
||||
* @param robotName 机器人名称
|
||||
* @param state 图片状态 ('normal' | 'active')
|
||||
* @param file 图片文件
|
||||
*/
|
||||
public async saveRobotCustomImage(robotName: string, state: 'normal' | 'active', file: File): Promise<void> {
|
||||
try {
|
||||
// 将图片转换为Base64
|
||||
const base64 = await this.fileToBase64(file);
|
||||
|
||||
// 确保customImages对象存在
|
||||
if (!this.config.value.robot.customImages) {
|
||||
this.setNestedValue(this.config.value, 'robot.customImages', {});
|
||||
}
|
||||
|
||||
// 确保机器人对象存在
|
||||
if (!this.config.value.robot.customImages[robotName]) {
|
||||
this.setNestedValue(this.config.value, `robot.customImages.${robotName}`, {});
|
||||
}
|
||||
|
||||
// 保存图片数据
|
||||
this.setNestedValue(this.config.value, `robot.customImages.${robotName}.${state}`, base64);
|
||||
|
||||
// 启用自定义图片
|
||||
this.setColor('robot.useCustomImages', 'true');
|
||||
|
||||
// 更新单个机器人的图片
|
||||
if (this.editorService && typeof this.editorService.updateRobotImage === 'function') {
|
||||
this.editorService.updateRobotImage(robotName);
|
||||
}
|
||||
|
||||
// 触发重新渲染
|
||||
this.triggerRender();
|
||||
} catch (error) {
|
||||
console.error('保存机器人自定义图片失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个机器人的自定义图片
|
||||
* @param robotName 机器人名称
|
||||
* @param state 图片状态 ('normal' | 'active')
|
||||
*/
|
||||
public getRobotCustomImage(robotName: string, state: 'normal' | 'active'): string | null {
|
||||
const customImages = this.config.value.robot.customImages;
|
||||
if (!customImages || !customImages[robotName]) {
|
||||
return null;
|
||||
}
|
||||
return customImages[robotName][state] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个机器人的自定义图片
|
||||
* @param robotName 机器人名称
|
||||
* @param state 图片状态 ('normal' | 'active'),不传则删除所有状态
|
||||
*/
|
||||
public removeRobotCustomImage(robotName: string, state?: 'normal' | 'active'): void {
|
||||
const customImages = this.config.value.robot.customImages;
|
||||
if (!customImages || !customImages[robotName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
// 删除特定状态的图片
|
||||
this.setNestedValue(this.config.value, `robot.customImages.${robotName}.${state}`, '');
|
||||
} else {
|
||||
// 删除整个机器人的图片配置
|
||||
delete customImages[robotName];
|
||||
}
|
||||
|
||||
// 检查是否还有任何自定义图片
|
||||
const hasAnyCustomImages = Object.values(customImages).some(robot =>
|
||||
robot.normal || robot.active
|
||||
);
|
||||
|
||||
if (!hasAnyCustomImages) {
|
||||
this.setColor('robot.useCustomImages', 'false');
|
||||
}
|
||||
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查机器人是否有自定义图片
|
||||
* @param robotName 机器人名称
|
||||
*/
|
||||
public hasRobotCustomImage(robotName: string): boolean {
|
||||
const customImages = this.config.value.robot.customImages;
|
||||
if (!customImages || !customImages[robotName]) {
|
||||
return false;
|
||||
}
|
||||
return !!(customImages[robotName].normal || customImages[robotName].active);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有有自定义图片的机器人名称列表
|
||||
*/
|
||||
public getRobotsWithCustomImages(): string[] {
|
||||
const customImages = this.config.value.robot.customImages;
|
||||
if (!customImages) return [];
|
||||
|
||||
return Object.keys(customImages).filter(robotName =>
|
||||
customImages[robotName].normal || customImages[robotName].active
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ColorConfigService();
|
||||
|
@ -85,10 +85,18 @@ export function parsePenData(penData: Record<string, unknown>): ParsedEventData
|
||||
|
||||
if (tags.includes('robot')) {
|
||||
// 机器人区域
|
||||
// 机器人的真实名称存储在text字段中,而不是name字段
|
||||
const robotName = (pen?.text as string) || name || `机器人-${id}`;
|
||||
console.log('解析pen数据中的机器人信息:', {
|
||||
id,
|
||||
originalName: name,
|
||||
penText: pen?.text,
|
||||
finalName: robotName
|
||||
});
|
||||
return {
|
||||
type: 'robot',
|
||||
id,
|
||||
name,
|
||||
name: robotName,
|
||||
tags,
|
||||
target: document.elementFromPoint(position.x, position.y) as HTMLElement,
|
||||
position,
|
||||
@ -198,10 +206,18 @@ export function parseEventData(event: MouseEvent | PointerEvent): ParsedEventDat
|
||||
|
||||
if (tags.includes('robot')) {
|
||||
// 机器人区域
|
||||
// 机器人的真实名称存储在text字段中,而不是name字段
|
||||
const robotName = (pen?.text as string) || name || `机器人-${id}`;
|
||||
console.log('解析机器人信息:', {
|
||||
id,
|
||||
originalName: name,
|
||||
penText: pen?.text,
|
||||
finalName: robotName
|
||||
});
|
||||
return {
|
||||
type: 'robot',
|
||||
id,
|
||||
name,
|
||||
name: robotName,
|
||||
tags,
|
||||
target,
|
||||
position,
|
||||
@ -282,7 +298,11 @@ export function parseEventData(event: MouseEvent | PointerEvent): ParsedEventDat
|
||||
export function isClickInsideMenu(event: MouseEvent, isMenuVisible: boolean): boolean {
|
||||
if (!isMenuVisible) return false;
|
||||
|
||||
const menuElement = document.querySelector('.context-menu');
|
||||
// 查找右键菜单元素,支持多种可能的类名
|
||||
const menuElement = document.querySelector('.context-menu-overlay') ||
|
||||
document.querySelector('.context-menu') ||
|
||||
document.querySelector('[class*="context-menu"]');
|
||||
|
||||
if (!menuElement) return false;
|
||||
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
|
@ -36,7 +36,8 @@ export type RobotAction =
|
||||
| 'stop' // 停止
|
||||
| 'reset' // 重置
|
||||
| 'diagnose' // 诊断
|
||||
| 'update'; // 更新
|
||||
| 'update' // 更新
|
||||
| 'custom_image'; // 自定义图片
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
type Point,
|
||||
type Rect,
|
||||
} from '@api/map';
|
||||
import type { RobotGroup, RobotInfo, RobotRealtimeInfo, RobotType } from '@api/robot';
|
||||
import type { RobotGroup, RobotInfo, RobotType } from '@api/robot';
|
||||
import type {
|
||||
BinLocationGroup,
|
||||
BinLocationItem,
|
||||
@ -25,7 +25,7 @@ import type {
|
||||
import sTheme from '@core/theme.service';
|
||||
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
|
||||
import { useObservable } from '@vueuse/rxjs';
|
||||
import { clone, get, isEmpty, isNil, isString, nth, omitBy, pick, remove, some } from 'lodash-es';
|
||||
import { clone, get, isEmpty, isNil, isString, nth, pick, remove, some } from 'lodash-es';
|
||||
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
@ -1105,7 +1105,7 @@ export class EditorService extends Meta2d {
|
||||
await Promise.all(
|
||||
this.robots.map(async ({ id, label, type }) => {
|
||||
const pen: MapPen = {
|
||||
...this.#mapRobotImage(type, true),
|
||||
...this.#mapRobotImage(type, true, label), // 传递机器人名称(label)
|
||||
id,
|
||||
name: 'robot',
|
||||
tags: ['robot'],
|
||||
@ -1171,10 +1171,10 @@ export class EditorService extends Meta2d {
|
||||
* 当机器人图片尺寸配置发生变化时调用
|
||||
*/
|
||||
public updateAllRobotImageSizes(): void {
|
||||
this.robots.forEach(({ id, type }) => {
|
||||
this.robots.forEach(({ id, type, label }) => {
|
||||
const pen = this.getPenById(id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.#mapRobotImage(type, pen.robot.active);
|
||||
const imageConfig = this.#mapRobotImage(type, pen.robot.active, label); // 传递机器人名称
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
@ -1188,14 +1188,56 @@ export class EditorService extends Meta2d {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个机器人的图片
|
||||
* 当机器人自定义图片配置发生变化时调用
|
||||
* @param robotName 机器人名称
|
||||
*/
|
||||
public updateRobotImage(robotName: string): void {
|
||||
const robot = this.robots.find(r => r.label === robotName);
|
||||
if (!robot) return;
|
||||
|
||||
const pen = this.getPenById(robot.id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.#mapRobotImage(robot.type, pen.robot.active, robotName);
|
||||
this.setValue(
|
||||
{
|
||||
id: robot.id,
|
||||
image: imageConfig.image,
|
||||
iconWidth: imageConfig.iconWidth,
|
||||
iconHeight: imageConfig.iconHeight,
|
||||
iconTop: imageConfig.iconTop,
|
||||
},
|
||||
{ render: true, history: false, doEvent: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#mapRobotImage(
|
||||
type: RobotType,
|
||||
active?: boolean,
|
||||
robotName?: string,
|
||||
): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop' | 'canvasLayer'>> {
|
||||
const theme = this.data().theme;
|
||||
const image =
|
||||
import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
|
||||
// 检查是否启用自定义图片且有机器人名称
|
||||
const useCustomImages = colorConfig.getColor('robot.useCustomImages') === 'true';
|
||||
let image: string;
|
||||
|
||||
if (useCustomImages && robotName) {
|
||||
// 尝试获取自定义图片
|
||||
const customImage = colorConfig.getRobotCustomImage(robotName, active ? 'active' : 'normal');
|
||||
if (customImage) {
|
||||
image = customImage;
|
||||
} else {
|
||||
// 如果没有自定义图片,回退到默认图片
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
} else {
|
||||
// 使用默认图片
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
|
||||
// 使用配置的机器人图片尺寸,如果没有配置则使用默认值
|
||||
const iconWidth = colorConfig.getColor('robot.imageWidth') ? Number(colorConfig.getColor('robot.imageWidth')) : 42;
|
||||
@ -1634,6 +1676,16 @@ export class EditorService extends Meta2d {
|
||||
(<HTMLDivElement>container.children.item(5)).ondrop = null;
|
||||
// 监听所有画布事件
|
||||
this.on('*', (e, v) => this.#listen(e, v));
|
||||
|
||||
// 添加额外的右键事件监听器,确保阻止默认行为
|
||||
const canvasElement = this.canvas as unknown as HTMLCanvasElement;
|
||||
if (canvasElement && canvasElement.addEventListener) {
|
||||
canvasElement.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
}
|
||||
|
||||
// 注册自定义绘制函数和锚点
|
||||
this.#register();
|
||||
|
||||
@ -1665,7 +1717,9 @@ export class EditorService extends Meta2d {
|
||||
});
|
||||
this.find('robot').forEach((pen) => {
|
||||
if (!pen.robot?.type) return;
|
||||
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active));
|
||||
// 从pen的text属性获取机器人名称
|
||||
const robotName = pen.text || '';
|
||||
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active, robotName));
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user