feat(context-menu): 优化站点右键菜单样式并改用Ant Design Menu组件,同时为机器人选择弹窗添加搜索功能和视觉优化

This commit is contained in:
xudan 2025-12-08 13:57:04 +08:00
parent 284fce1873
commit 322a4fa8a7
3 changed files with 207 additions and 127 deletions

View File

@ -4,5 +4,5 @@ ENV_WEBSOCKET_BASE=/ws
ENV_STORAGE_WEBSOCKET_BASE=/vwedWs
# 开发环境token配置 - 可以手动设置或从另一个项目获取后填入
ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjQ3MDExNDcsInVzZXJuYW1lIjoiYWRtaW4ifQ.NHLOjXm9JblCXXaDmc8PRvqB-uIpjwltrFh2EH4usIc
ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjUzMDIxMjcsInVzZXJuYW1lIjoiYWRtaW4ifQ.Bu5YpD-lg4YG2_H6kkbauZK7WpjqOxoBtwD4AiZXAWI
ENV_DEV_TENANT_ID=1000

View File

@ -3,7 +3,6 @@
<!-- 菜单头部显示站点信息 -->
<div class="menu-header">
<div class="point-info">
<div class="point-icon">📍</div>
<div class="point-details">
<div class="point-name">{{ pointInfo?.name || '未知站点' }}</div>
<div class="point-id">ID: {{ pointInfo?.id }}</div>
@ -12,25 +11,21 @@
</div>
<!-- 菜单分割线 -->
<a-divider style="margin: 12px 0;" />
<a-divider style="margin: 6px 0;" />
<!-- 菜单选项 -->
<div class="menu-options">
<!-- 导航至此站点 -->
<div
class="menu-item"
:class="{ disabled: !canNavigate }"
<a-menu
:selectable="false"
class="menu-options"
>
<a-menu-item
key="navigate"
:disabled="!canNavigate"
@click="handleNavigateToPoint"
>
<div class="menu-item-content">
<span class="menu-icon">🧭</span>
<span class="menu-text">导航至此站点</span>
</div>
<div class="menu-description">
选择机器人并下发移动任务到当前站点
</div>
</div>
</div>
导航至此站点
</a-menu-item>
</a-menu>
<!-- 机器人选择弹窗 -->
<RobotSelectorModal
@ -157,25 +152,24 @@ const handleRobotSelectorCancel = () => {
<style scoped>
.point-menu {
width: 100%;
min-width: 320px;
max-width: 400px;
padding: 8px;
min-width: 280px;
max-width: 320px;
padding: 4px;
}
/* 菜单头部 */
.menu-header {
padding: 8px 12px;
padding: 4px 8px;
}
.point-info {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
}
.point-icon {
font-size: 20px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
@ -183,35 +177,42 @@ const handleRobotSelectorCancel = () => {
.point-details {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.point-name {
font-size: 14px;
font-size: 13px;
font-weight: 500;
color: #262626;
margin-bottom: 4px;
}
.point-id {
font-size: 12px;
color: #8c8c8c;
font-size: 11px;
color: var(--text-color-secondary, #8c8c8c);
}
/* 菜单选项 */
.menu-options {
padding: 0 4px;
padding: 0 2px;
}
.menu-item {
padding: 12px;
margin: 4px 0;
border-radius: 6px;
padding: 6px 8px;
margin: 2px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
}
.menu-item:hover:not(.disabled) {
background-color: #f5f5f5;
background-color: var(--hover-bg, #f5f5f5);
}
/* 确保菜单项不超出容器宽度 */
.menu-item-content {
box-sizing: border-box;
width: 100%;
}
.menu-item.disabled {
@ -222,30 +223,71 @@ const handleRobotSelectorCancel = () => {
.menu-item-content {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
gap: 8px;
}
.menu-icon {
font-size: 16px;
width: 20px;
font-size: 14px;
width: 16px;
text-align: center;
}
.menu-text {
font-size: 14px;
font-size: 13px;
font-weight: 500;
color: #262626;
color: var(--text-color, #262626);
}
.menu-description {
font-size: 12px;
color: #8c8c8c;
margin-left: 32px;
/* 深色主题样式 */
:root([data-theme='dark']) .point-menu {
background-color: #1f1f1f;
}
/* 分割线样式 */
:deep(.ant-divider) {
border-color: #f0f0f0;
:root([data-theme='dark']) .point-name {
color: #ffffff;
}
:root([data-theme='dark']) .point-id {
color: #a0a0a0;
}
:root([data-theme='dark']) .menu-item:hover:not(.disabled) {
background-color: #333333;
}
:root([data-theme='dark']) .menu-text {
color: #ffffff;
}
/* 兼容其他可能的深色主题类名 */
.dark .point-menu,
[data-theme="dark"] .point-menu,
.theme-dark .point-menu {
background-color: #1f1f1f;
}
.dark .point-name,
[data-theme="dark"] .point-name,
.theme-dark .point-name {
color: #ffffff;
}
.dark .point-id,
[data-theme="dark"] .point-id,
.theme-dark .point-id {
color: #a0a0a0;
}
.dark .menu-item:hover:not(.disabled),
[data-theme="dark"] .menu-item:hover:not(.disabled),
.theme-dark .menu-item:hover:not(.disabled) {
background-color: #333333;
}
.dark .menu-text,
[data-theme="dark"] .menu-text,
.theme-dark .menu-text {
color: #ffffff;
}
</style>

View File

@ -7,30 +7,37 @@
@cancel="handleCancel"
>
<div class="robot-selector">
<!-- 搜索框 -->
<div class="search-section">
<a-input
v-model:value="searchKeyword"
placeholder="搜索机器人名称或ID"
:allow-clear="true"
class="search-input"
>
</a-input>
</div>
<!-- 机器人列表 -->
<div class="robot-list">
<a-radio-group v-model:value="selectedRobotId" class="radio-group">
<div
v-for="robot in robotList"
v-for="robot in filteredRobotList"
:key="robot.id"
class="robot-item"
:class="{ selected: selectedRobotId === robot.id }"
@click="handleSelectRobot(robot.id)"
>
<a-radio :value="robot.id">
<div class="robot-info">
<div class="robot-name">{{ robot.label }}</div>
<div class="robot-details">
<a-tag :color="getRobotStatusColor(robot.state)">
{{ getRobotStatusText(robot.state) }}
</a-tag>
<span class="robot-id">ID: {{ robot.id }}</span>
<span v-if="robot.targetPoint" class="current-station">
目标位置: {{ robot.targetPoint }}
</span>
<span class="robot-name">{{ robot.label }}</span>
<span class="robot-status">- {{ getRobotStatusText(robot.state) }}</span>
</div>
<div class="check-indicator" v-if="selectedRobotId === robot.id">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
</div>
</div>
</a-radio>
</div>
</a-radio-group>
</div>
<!-- 操作按钮 -->
@ -55,7 +62,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import type { RobotInfo } from '../../apis/robot';
import { RobotState } from '../../apis/robot';
import { getAllRobots } from '../../apis/robot';
import { editorStore } from '../../stores/editor.store';
interface Props {
open?: boolean;
@ -80,18 +87,32 @@ const visible = ref(false);
const robotList = ref<RobotInfo[]>([]);
const selectedRobotId = ref<string>('');
const loading = ref(false);
const searchKeyword = ref<string>('');
//
const selectedRobot = computed(() =>
robotList.value.find(robot => robot.id === selectedRobotId.value)
);
const filteredRobotList = computed(() => {
if (!searchKeyword.value.trim()) {
return robotList.value;
}
const keyword = searchKeyword.value.toLowerCase().trim();
return robotList.value.filter(robot =>
robot.label.toLowerCase().includes(keyword) ||
robot.id.toLowerCase().includes(keyword)
);
});
// props.open
watch(() => props.open, (newVal) => {
visible.value = newVal;
if (newVal) {
loadRobotList();
selectedRobotId.value = '';
searchKeyword.value = '';
}
});
@ -101,14 +122,17 @@ watch(visible, (newVal) => {
});
//
const loadRobotList = async () => {
const loadRobotList = () => {
try {
const robots = await getAllRobots();
robotList.value = robots || [];
const editor = editorStore.getEditorValue();
if (editor) {
robotList.value = editor.robots || [];
} else {
robotList.value = [];
}
} catch (error) {
console.error('加载机器人列表失败:', error);
robotList.value = [];
message.error('加载机器人列表失败');
}
};
@ -129,17 +153,7 @@ const getRobotStatusText = (state?: RobotState): string => {
return stateMap[state || RobotState.空闲中] || '未知';
};
//
const getRobotStatusColor = (state?: RobotState): string => {
const colorMap: Record<RobotState, string> = {
[RobotState.未知]: '#d9d9d9', // -
[RobotState.任务执行中]: '#1890ff', // -
[RobotState.充电中]: '#faad14', // -
[RobotState.停靠中]: '#52c41a', // 绿 -
[RobotState.空闲中]: '#52c41a', // 绿 -
};
return colorMap[state || RobotState.空闲中] || '#d9d9d9';
};
//
const handleConfirm = async () => {
@ -172,6 +186,11 @@ const handleConfirm = async () => {
}
};
//
const handleSelectRobot = (robotId: string) => {
selectedRobotId.value = robotId;
};
//
const handleCancel = () => {
visible.value = false;
@ -188,76 +207,95 @@ onMounted(() => {
<style scoped>
.robot-selector {
padding: 16px 0;
padding: 12px 0;
}
.search-section {
margin-bottom: 12px;
padding: 0 8px;
}
.search-input {
width: 100%;
}
.search-icon {
color: var(--text-color-secondary, #8c8c8c);
}
.robot-list {
max-height: 400px;
max-height: 320px;
overflow-y: auto;
}
.radio-group {
width: 100%;
}
.robot-item {
display: block;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px;
margin-bottom: 8px;
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.3s;
height: 48px;
padding: 0 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.robot-item:hover {
border-color: #1890ff;
background-color: #f5f5f5;
.robot-item.selected {
background-color: #0dbb8a;
}
.robot-item.selected .robot-name {
color: #fff;
}
.robot-item.selected .robot-status {
color: rgba(255, 255, 255, 0.8);
}
.check-indicator {
color: #fff;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px;
}
.robot-info {
width: 100%;
}
.robot-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #262626;
}
.robot-details {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
white-space: nowrap;
}
.robot-id {
color: #595959;
.robot-name {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.current-station {
color: #595959;
.robot-status {
font-size: 14px;
flex-shrink: 0;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 16px;
padding-top: 12px;
}
/* 自定义单选框样式 */
:deep(.ant-radio-wrapper) {
width: 100%;
margin-right: 0;
}
:deep(.ant-radio-wrapper span:last-child) {
width: 100%;
}
</style>