feat: 增强编辑器服务,新增批量更新点位和路线类型功能,优化主题重载逻辑,更新颜色配置以支持大点位类型
This commit is contained in:
parent
9fef00729e
commit
b088f7236e
117
docs/批量编辑功能使用说明.md
Normal file
117
docs/批量编辑功能使用说明.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# 批量编辑功能使用说明
|
||||||
|
|
||||||
|
## 🎯 功能概述
|
||||||
|
|
||||||
|
批量编辑功能允许用户在场景编辑器中同时选中多个点位和路线,并批量修改它们的属性,大大提升了编辑效率。
|
||||||
|
|
||||||
|
## ✨ 主要功能
|
||||||
|
|
||||||
|
### 1. 批量选中
|
||||||
|
|
||||||
|
- **鼠标拖动选中**:在编辑模式下,可以通过鼠标拖动框选多个点位和路线
|
||||||
|
- **实时计数**:工具栏会显示当前选中的元素数量
|
||||||
|
- **清除选择**:一键清除所有选中状态
|
||||||
|
|
||||||
|
### 2. 批量编辑点位
|
||||||
|
|
||||||
|
- **点位类型**:支持修改点位类型
|
||||||
|
- 普通点
|
||||||
|
- 等待点
|
||||||
|
- 避让点
|
||||||
|
- 临时避让点
|
||||||
|
- 库区点
|
||||||
|
- 电梯点
|
||||||
|
- 自动门点
|
||||||
|
- 充电点
|
||||||
|
- 停靠点
|
||||||
|
- 动作点
|
||||||
|
- 禁行点
|
||||||
|
|
||||||
|
### 3. 批量编辑路线
|
||||||
|
|
||||||
|
- **通行类型**:
|
||||||
|
- 无限制
|
||||||
|
- 仅空载可通行
|
||||||
|
- 仅载货可通行
|
||||||
|
- 禁行
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 步骤 1:启用编辑器
|
||||||
|
|
||||||
|
1. 打开场景编辑器页面
|
||||||
|
2. 点击右上角的"启用编辑器"按钮
|
||||||
|
3. 编辑器工具栏和批量编辑工具栏将出现在页面上
|
||||||
|
|
||||||
|
### 步骤 2:批量选中元素
|
||||||
|
|
||||||
|
1. 在画布上按住鼠标左键
|
||||||
|
2. 拖动鼠标框选需要编辑的点位和路线
|
||||||
|
3. 选中的元素会高亮显示
|
||||||
|
4. 批量编辑工具栏会显示选中元素的数量
|
||||||
|
|
||||||
|
### 步骤 3:批量编辑
|
||||||
|
|
||||||
|
1. 点击"批量编辑"按钮打开编辑面板
|
||||||
|
2. 根据需要选择要修改的属性:
|
||||||
|
- 如果选中了点位,可以修改点位类型
|
||||||
|
- 如果选中了路线,可以修改路线类型、通行类型和方向
|
||||||
|
3. 在预览区域查看即将应用的更改
|
||||||
|
4. 点击"确定"应用更改,或点击"取消"放弃更改
|
||||||
|
|
||||||
|
### 步骤 4:清除选择
|
||||||
|
|
||||||
|
- 点击"清除选择"按钮可以取消所有选中状态
|
||||||
|
- 或者直接点击画布空白区域也可以清除选择
|
||||||
|
|
||||||
|
## 🎨 界面说明
|
||||||
|
|
||||||
|
### 批量编辑工具栏
|
||||||
|
|
||||||
|
- 位置:页面顶部中央(仅在选中元素时显示)
|
||||||
|
- 功能:显示选中数量、打开批量编辑面板、清除选择
|
||||||
|
|
||||||
|
### 批量编辑面板
|
||||||
|
|
||||||
|
- **选中统计**:显示选中的点位和路线数量
|
||||||
|
- **点位编辑区**:当选中点位时显示,用于修改点位类型
|
||||||
|
- **路线编辑区**:当选中路线时显示,用于修改路线通行类型
|
||||||
|
- **预览区域**:显示即将应用的更改,方便确认
|
||||||
|
|
||||||
|
## 💡 使用技巧
|
||||||
|
|
||||||
|
1. **混合选择**:可以同时选中点位和路线进行批量编辑
|
||||||
|
2. **部分更新**:只修改需要更改的属性,其他属性保持不变
|
||||||
|
3. **预览确认**:在应用更改前,预览区域会显示所有即将修改的内容
|
||||||
|
4. **撤销支持**:所有批量编辑操作都支持撤销(Ctrl+Z)
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
- **响应式设计**:基于 Vue 3 Composition API
|
||||||
|
- **类型安全**:完整的 TypeScript 类型定义
|
||||||
|
- **性能优化**:批量更新减少渲染次数,自动触发画布重绘
|
||||||
|
- **用户体验**:实时预览和撤销支持,紧凑的弹框设计
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
1. 批量编辑功能仅在编辑模式下可用
|
||||||
|
2. 选中的元素必须是点位(point)或路线(line)类型
|
||||||
|
3. 区域(area)和机器人(robot)元素不支持批量编辑
|
||||||
|
4. 所有更改都会记录在编辑历史中,支持撤销操作
|
||||||
|
|
||||||
|
## 🐛 故障排除
|
||||||
|
|
||||||
|
### 问题:批量编辑按钮不可用
|
||||||
|
|
||||||
|
- **原因**:没有选中任何元素
|
||||||
|
- **解决**:先通过鼠标拖动选中需要编辑的点位或路线
|
||||||
|
|
||||||
|
### 问题:编辑面板中没有显示选项
|
||||||
|
|
||||||
|
- **原因**:选中的元素类型不支持该编辑选项
|
||||||
|
- **解决**:确保选中的是点位或路线类型的元素
|
||||||
|
|
||||||
|
### 问题:更改没有生效
|
||||||
|
|
||||||
|
- **原因**:可能没有点击"确定"按钮
|
||||||
|
- **解决**:在编辑面板中点击"确定"按钮应用更改
|
||||||
86
src/components/batch-edit-toolbar.vue
Normal file
86
src/components/batch-edit-toolbar.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="hasSelection" class="batch-edit-toolbar">
|
||||||
|
<a-space size="small">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="showBatchEditModal = true"
|
||||||
|
>
|
||||||
|
批量编辑 ({{ selectedCount }})
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
|
清除选择
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
|
||||||
|
<!-- 批量编辑模态框 -->
|
||||||
|
<BatchEditModal
|
||||||
|
v-model:visible="showBatchEditModal"
|
||||||
|
:selected-items="selectedItems"
|
||||||
|
:editor="editor"
|
||||||
|
@update="handleBatchUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MapPen } from '@api/map';
|
||||||
|
import BatchEditModal from '@common/modal/batch-edit-modal.vue';
|
||||||
|
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
|
||||||
|
|
||||||
|
import type { EditorService } from '../services/editor.service';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
token: InjectionKey<ShallowRef<EditorService>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const editor = inject(props.token)!;
|
||||||
|
|
||||||
|
const showBatchEditModal = ref(false);
|
||||||
|
|
||||||
|
// 计算选中的元素
|
||||||
|
const selectedItems = computed(() => {
|
||||||
|
if (!editor.value) return [];
|
||||||
|
|
||||||
|
const selectedIds = editor.value.selected.value;
|
||||||
|
return selectedIds
|
||||||
|
.map(id => editor.value!.getPenById(id))
|
||||||
|
.filter((pen): pen is MapPen => !!pen && (pen.name === 'point' || pen.name === 'line'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = computed(() => selectedItems.value.length);
|
||||||
|
|
||||||
|
const hasSelection = computed(() => selectedCount.value > 0);
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
editor.value?.inactive();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchUpdate = (updates: Array<{ id: string; updates: Partial<MapPen> }>) => {
|
||||||
|
// 使用批量更新方法,确保所有点位同时更新
|
||||||
|
editor.value?.batchUpdate(updates);
|
||||||
|
showBatchEditModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'BatchEditToolbar'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.batch-edit-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
366
src/components/modal/batch-edit-modal.vue
Normal file
366
src/components/modal/batch-edit-modal.vue
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
title="批量编辑"
|
||||||
|
width="500px"
|
||||||
|
:ok-button-props="{ disabled: !hasChanges }"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div class="batch-edit-content">
|
||||||
|
<!-- 选中元素统计 -->
|
||||||
|
<div class="selection-info">
|
||||||
|
<a-alert
|
||||||
|
:message="`已选择 ${selectedItems.length} 个元素`"
|
||||||
|
:description="selectionDescription"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 点位批量编辑 -->
|
||||||
|
<div v-if="pointItems.length > 0" class="edit-section">
|
||||||
|
<h4>点位编辑 ({{ pointItems.length }} 个)</h4>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="点位类型">
|
||||||
|
<a-select
|
||||||
|
v-model:value="pointType"
|
||||||
|
placeholder="选择点位类型"
|
||||||
|
allow-clear
|
||||||
|
@change="markChanged"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="[key, value] in MAP_POINT_TYPES"
|
||||||
|
:key="value"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 路线批量编辑 -->
|
||||||
|
<div v-if="lineItems.length > 0" class="edit-section">
|
||||||
|
<h4>路线编辑 ({{ lineItems.length }} 个)</h4>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="通行类型">
|
||||||
|
<a-select
|
||||||
|
v-model:value="routePassType"
|
||||||
|
placeholder="选择通行类型"
|
||||||
|
allow-clear
|
||||||
|
@change="markChanged"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="[key, value] in MAP_ROUTE_PASS_TYPES"
|
||||||
|
:key="value"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<div v-if="hasChanges" class="preview-section">
|
||||||
|
<h4>预览更改</h4>
|
||||||
|
<div class="preview-list">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="preview-item"
|
||||||
|
>
|
||||||
|
<span class="item-name">{{ getItemName(item) }}</span>
|
||||||
|
<span class="item-type">{{ getItemType(item) }}</span>
|
||||||
|
<div class="changes">
|
||||||
|
<a-tag v-if="getPointChanges(item).length" color="blue">
|
||||||
|
点位: {{ getPointChanges(item).join(', ') }}
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="getRouteChanges(item).length" color="green">
|
||||||
|
路线: {{ getRouteChanges(item).join(', ') }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import type { MapPen } from '../../apis/map';
|
||||||
|
import {
|
||||||
|
MAP_POINT_TYPES,
|
||||||
|
MAP_ROUTE_PASS_TYPES,
|
||||||
|
MapPointType,
|
||||||
|
MapRoutePassType
|
||||||
|
} from '../../apis/map';
|
||||||
|
import type { EditorService } from '../../services/editor.service';
|
||||||
|
|
||||||
|
// 点位类型对应的尺寸和图像配置
|
||||||
|
const getPointTypeConfig = (type: MapPointType, editor: EditorService) => {
|
||||||
|
const width = type < 10 ? 24 : 48;
|
||||||
|
const height = type < 10 ? 24 : 60;
|
||||||
|
const lineWidth = type < 10 ? 2 : 3;
|
||||||
|
const iconSize = type < 10 ? 4 : 10;
|
||||||
|
|
||||||
|
// 使用与 editor.service.ts 中 #mapPointImage 相同的逻辑
|
||||||
|
const theme = editor.data().theme;
|
||||||
|
// 使用相对路径,图片文件在 public/point/ 目录下
|
||||||
|
const image = type < 10 ? '' : `./point/${type}-${theme}.png`;
|
||||||
|
|
||||||
|
return { width, height, lineWidth, iconSize, image };
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
selectedItems: MapPen[];
|
||||||
|
editor: EditorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'update', updates: Array<{ id: string; updates: Partial<MapPen> }>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const modalVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value) => emit('update:visible', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分离点位和路线
|
||||||
|
const pointItems = computed(() =>
|
||||||
|
props.selectedItems.filter(item => item.name === 'point')
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineItems = computed(() =>
|
||||||
|
props.selectedItems.filter(item => item.name === 'line')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const pointType = ref<MapPointType>();
|
||||||
|
const routePassType = ref<MapRoutePassType>();
|
||||||
|
|
||||||
|
// 变更标记
|
||||||
|
const hasChanges = ref(false);
|
||||||
|
|
||||||
|
const markChanged = () => {
|
||||||
|
hasChanges.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
pointType.value = undefined;
|
||||||
|
routePassType.value = undefined;
|
||||||
|
hasChanges.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听模态框显示状态
|
||||||
|
watch(() => props.visible, (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中元素描述
|
||||||
|
const selectionDescription = computed(() => {
|
||||||
|
const points = pointItems.value.length;
|
||||||
|
const lines = lineItems.value.length;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (points > 0) parts.push(`${points} 个点位`);
|
||||||
|
if (lines > 0) parts.push(`${lines} 条路线`);
|
||||||
|
return parts.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取元素名称
|
||||||
|
const getItemName = (item: MapPen) => {
|
||||||
|
return item.label || item.id || '未命名';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取元素类型
|
||||||
|
const getItemType = (item: MapPen) => {
|
||||||
|
if (item.name === 'point') {
|
||||||
|
const pointTypeName = Object.keys(MapPointType).find(
|
||||||
|
key => MapPointType[key as keyof typeof MapPointType] === item.point?.type
|
||||||
|
);
|
||||||
|
return `点位-${pointTypeName || '未知'}`;
|
||||||
|
} else if (item.name === 'line') {
|
||||||
|
return '路线';
|
||||||
|
}
|
||||||
|
return '未知';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取点位变更描述
|
||||||
|
const getPointChanges = (item: MapPen) => {
|
||||||
|
if (item.name !== 'point' || !pointType.value) return [];
|
||||||
|
|
||||||
|
const currentType = item.point?.type;
|
||||||
|
if (currentType === pointType.value) return [];
|
||||||
|
|
||||||
|
const newTypeName = Object.keys(MapPointType).find(
|
||||||
|
key => MapPointType[key as keyof typeof MapPointType] === pointType.value
|
||||||
|
);
|
||||||
|
return [`类型: ${newTypeName}`];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取路线变更描述
|
||||||
|
const getRouteChanges = (item: MapPen) => {
|
||||||
|
if (item.name !== 'line') return [];
|
||||||
|
|
||||||
|
const changes: string[] = [];
|
||||||
|
|
||||||
|
if (routePassType.value !== undefined && item.route?.pass !== routePassType.value) {
|
||||||
|
const newPassName = Object.keys(MapRoutePassType).find(
|
||||||
|
key => MapRoutePassType[key as keyof typeof MapRoutePassType] === routePassType.value
|
||||||
|
);
|
||||||
|
changes.push(`通行: ${newPassName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认更改
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const updates: Array<{ id: string; updates: Partial<MapPen> }> = [];
|
||||||
|
|
||||||
|
// 处理点位更新
|
||||||
|
pointItems.value.forEach(item => {
|
||||||
|
if (pointType.value) {
|
||||||
|
const pointConfig = getPointTypeConfig(pointType.value, props.editor);
|
||||||
|
const rect = props.editor.getPointRect(item);
|
||||||
|
|
||||||
|
updates.push({
|
||||||
|
id: item.id!,
|
||||||
|
updates: {
|
||||||
|
// 更新点位类型
|
||||||
|
point: {
|
||||||
|
...item.point,
|
||||||
|
type: pointType.value
|
||||||
|
},
|
||||||
|
// 更新点位尺寸和图像
|
||||||
|
...pointConfig,
|
||||||
|
// 调整位置以保持中心点不变
|
||||||
|
x: rect ? rect.x - pointConfig.width / 2 : item.x,
|
||||||
|
y: rect ? rect.y - pointConfig.height / 2 : item.y,
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理路线更新
|
||||||
|
lineItems.value.forEach(item => {
|
||||||
|
if (item.route && routePassType.value !== undefined) {
|
||||||
|
const routeUpdates: Partial<MapPen> = {
|
||||||
|
route: {
|
||||||
|
...item.route,
|
||||||
|
pass: routePassType.value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updates.push({
|
||||||
|
id: item.id!,
|
||||||
|
updates: routeUpdates
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('update', updates);
|
||||||
|
|
||||||
|
// 立即关闭弹窗,提供更好的用户体验
|
||||||
|
modalVisible.value = false;
|
||||||
|
|
||||||
|
// 在后台异步重新加载主题,避免界面卡顿
|
||||||
|
nextTick(() => {
|
||||||
|
// 使用 setTimeout 确保点位更新完成后再重新加载主题
|
||||||
|
setTimeout(() => {
|
||||||
|
props.editor.reloadTheme();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消更改
|
||||||
|
const handleCancel = () => {
|
||||||
|
resetForm();
|
||||||
|
modalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'BatchEditModal'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.batch-edit-content {
|
||||||
|
.selection-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-list {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 10px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 10px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-tag {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -8,6 +8,7 @@ import { computed, watch } from 'vue';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { onMounted, provide, shallowRef } from 'vue';
|
import { onMounted, provide, shallowRef } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import BatchEditToolbar from '@common/batch-edit-toolbar.vue';
|
||||||
|
|
||||||
const EDITOR_KEY = Symbol('editor-key');
|
const EDITOR_KEY = Symbol('editor-key');
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ const toPush = () => {
|
|||||||
|
|
||||||
const importScene = async () => {
|
const importScene = async () => {
|
||||||
const file = await selectFile('.scene');
|
const file = await selectFile('.scene');
|
||||||
if (!file?.size) return;
|
if (!file || !file.size) return;
|
||||||
const json = await decodeTextFile(file);
|
const json = await decodeTextFile(file);
|
||||||
editor.value?.load(json, editable.value, undefined, true); // 第四个参数isImport=true
|
editor.value?.load(json, editable.value, undefined, true); // 第四个参数isImport=true
|
||||||
};
|
};
|
||||||
@ -218,6 +219,9 @@ const backToCards = () => {
|
|||||||
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量编辑工具栏 - 只在编辑模式下显示 -->
|
||||||
|
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
||||||
|
|
||||||
<template v-if="current?.id">
|
<template v-if="current?.id">
|
||||||
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
||||||
<template #icon><i class="icon detail" /></template>
|
<template #icon><i class="icon detail" /></template>
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export interface EditorColorConfig {
|
|||||||
[key: number]: {
|
[key: number]: {
|
||||||
stroke: string;
|
stroke: string;
|
||||||
strokeActive: string;
|
strokeActive: string;
|
||||||
fill: string;
|
fill?: string; // 大点位不需要填充颜色
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -124,8 +124,8 @@ export interface EditorColorConfig {
|
|||||||
/**
|
/**
|
||||||
* 生成点位类型的默认颜色配置
|
* 生成点位类型的默认颜色配置
|
||||||
*/
|
*/
|
||||||
function generatePointTypeColors(): Record<number, { stroke: string; strokeActive: string; fill: string }> {
|
function generatePointTypeColors(): Record<number, { stroke: string; strokeActive: string; fill?: string }> {
|
||||||
const colors: Record<number, { stroke: string; strokeActive: string; fill: string }> = {};
|
const colors: Record<number, { stroke: string; strokeActive: string; fill?: string }> = {};
|
||||||
|
|
||||||
// 预定义的颜色方案
|
// 预定义的颜色方案
|
||||||
const colorSchemes = {
|
const colorSchemes = {
|
||||||
@ -138,21 +138,25 @@ function generatePointTypeColors(): Record<number, { stroke: string; strokeActiv
|
|||||||
// 大点位颜色 (11+)
|
// 大点位颜色 (11+)
|
||||||
large: {
|
large: {
|
||||||
stroke: '#595959',
|
stroke: '#595959',
|
||||||
strokeActive: '#EBB214',
|
strokeActive: '#EBB214'
|
||||||
fills: ['#1890ff', '#52c41a', '#faad14', '#722ed1', '#13c2c2', '#ff4d4f']
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
POINT_TYPES.forEach(type => {
|
POINT_TYPES.forEach(type => {
|
||||||
const isSmallPoint = type >= 1 && type <= 9;
|
const isSmallPoint = type >= 1 && type <= 9;
|
||||||
const scheme = isSmallPoint ? colorSchemes.small : colorSchemes.large;
|
const scheme = isSmallPoint ? colorSchemes.small : colorSchemes.large;
|
||||||
const fillIndex = isSmallPoint ? (type - 1) : (type - 11);
|
|
||||||
|
|
||||||
colors[type] = {
|
colors[type] = {
|
||||||
stroke: scheme.stroke,
|
stroke: scheme.stroke,
|
||||||
strokeActive: scheme.strokeActive,
|
strokeActive: scheme.strokeActive
|
||||||
fill: scheme.fills[fillIndex] || scheme.fills[0]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 只有小点位才设置填充颜色
|
||||||
|
if (isSmallPoint && 'fills' in scheme) {
|
||||||
|
const fillIndex = type - 1;
|
||||||
|
const fills = (scheme as any).fills as string[];
|
||||||
|
colors[type].fill = fills[fillIndex] || fills[0];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
@ -712,18 +716,24 @@ class ColorConfigService {
|
|||||||
const types: Record<number, any> = {};
|
const types: Record<number, any> = {};
|
||||||
|
|
||||||
POINT_TYPES.forEach(type => {
|
POINT_TYPES.forEach(type => {
|
||||||
|
const isSmallPoint = type >= 1 && type <= 9;
|
||||||
|
|
||||||
types[type] = {
|
types[type] = {
|
||||||
stroke: theme['point-s']?.stroke ||
|
stroke: theme['point-s']?.stroke ||
|
||||||
DEFAULT_COLORS.point.types[type]?.stroke ||
|
DEFAULT_COLORS.point.types[type]?.stroke ||
|
||||||
DEFAULT_COLORS.point.small.stroke,
|
DEFAULT_COLORS.point.small.stroke,
|
||||||
strokeActive: theme['point-s']?.strokeActive ||
|
strokeActive: theme['point-s']?.strokeActive ||
|
||||||
DEFAULT_COLORS.point.types[type]?.strokeActive ||
|
DEFAULT_COLORS.point.types[type]?.strokeActive ||
|
||||||
DEFAULT_COLORS.point.small.strokeActive,
|
DEFAULT_COLORS.point.small.strokeActive
|
||||||
fill: theme['point-s']?.[`fill-${type}`] ||
|
|
||||||
DEFAULT_COLORS.point.types[type]?.fill ||
|
|
||||||
DEFAULT_COLORS.point.small.fill[type] ||
|
|
||||||
DEFAULT_COLORS.point.small.fill[1]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 只有小点位才设置填充颜色
|
||||||
|
if (isSmallPoint) {
|
||||||
|
types[type].fill = theme['point-s']?.[`fill-${type}`] ||
|
||||||
|
DEFAULT_COLORS.point.types[type]?.fill ||
|
||||||
|
DEFAULT_COLORS.point.small.fill[type] ||
|
||||||
|
DEFAULT_COLORS.point.small.fill[1];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return types;
|
return types;
|
||||||
|
|||||||
@ -869,8 +869,8 @@ export class EditorService extends Meta2d {
|
|||||||
this.delete([pen], true, true);
|
this.delete([pen], true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePen(id: string, pen: Partial<MapPen>, record = true): void {
|
public updatePen(id: string, pen: Partial<MapPen>, record = true, render = true): void {
|
||||||
this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
|
this.setValue({ ...pen, id }, { render, history: record, doEvent: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1231,6 +1231,13 @@ export class EditorService extends Meta2d {
|
|||||||
},
|
},
|
||||||
{ render: true, history: true, doEvent: true },
|
{ render: true, history: true, doEvent: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果是大点位类型(需要图片),异步重新加载主题确保图片正确显示
|
||||||
|
if (type >= 10) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.reloadTheme();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#mapPoint(type: MapPointType): Required<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
|
#mapPoint(type: MapPointType): Required<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
|
||||||
@ -1461,6 +1468,19 @@ export class EditorService extends Meta2d {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载主题和图片配置
|
||||||
|
* 用于批量编辑后确保大点位图片正确显示
|
||||||
|
* 异步执行,避免阻塞UI
|
||||||
|
*/
|
||||||
|
public reloadTheme(): void {
|
||||||
|
const currentTheme = this.data().theme || 'light';
|
||||||
|
// 使用 requestAnimationFrame 确保在下一个渲染帧执行,避免阻塞UI
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.#load(currentTheme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#onDelete(pens?: MapPen[]): void {
|
#onDelete(pens?: MapPen[]): void {
|
||||||
pens?.forEach((pen) => {
|
pens?.forEach((pen) => {
|
||||||
switch (pen.name) {
|
switch (pen.name) {
|
||||||
@ -1571,6 +1591,95 @@ export class EditorService extends Meta2d {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新点位类型
|
||||||
|
* @param pointIds 点位ID数组
|
||||||
|
* @param pointType 新的点位类型
|
||||||
|
*/
|
||||||
|
public batchUpdatePointType(pointIds: string[], pointType: MapPointType): void {
|
||||||
|
pointIds.forEach(id => {
|
||||||
|
const pen = this.getPenById(id);
|
||||||
|
if (pen?.name === 'point') {
|
||||||
|
this.updatePen(id, {
|
||||||
|
point: {
|
||||||
|
...pen.point,
|
||||||
|
type: pointType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新路线类型
|
||||||
|
* @param routeIds 路线ID数组
|
||||||
|
* @param routeType 新的路线类型
|
||||||
|
*/
|
||||||
|
public batchUpdateRouteType(routeIds: string[], routeType: MapRouteType): void {
|
||||||
|
routeIds.forEach(id => {
|
||||||
|
const pen = this.getPenById(id);
|
||||||
|
if (pen?.name === 'line' && pen.route) {
|
||||||
|
this.updatePen(id, {
|
||||||
|
route: {
|
||||||
|
...pen.route,
|
||||||
|
type: routeType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新路线通行类型
|
||||||
|
* @param routeIds 路线ID数组
|
||||||
|
* @param passType 新的通行类型
|
||||||
|
*/
|
||||||
|
public batchUpdateRoutePassType(routeIds: string[], passType: MapRoutePassType): void {
|
||||||
|
routeIds.forEach(id => {
|
||||||
|
const pen = this.getPenById(id);
|
||||||
|
if (pen?.name === 'line' && pen.route) {
|
||||||
|
this.updatePen(id, {
|
||||||
|
route: {
|
||||||
|
...pen.route,
|
||||||
|
pass: passType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新路线方向
|
||||||
|
* @param routeIds 路线ID数组
|
||||||
|
* @param direction 新的方向
|
||||||
|
*/
|
||||||
|
public batchUpdateRouteDirection(routeIds: string[], direction: 1 | -1): void {
|
||||||
|
routeIds.forEach(id => {
|
||||||
|
const pen = this.getPenById(id);
|
||||||
|
if (pen?.name === 'line' && pen.route) {
|
||||||
|
this.updatePen(id, {
|
||||||
|
route: {
|
||||||
|
...pen.route,
|
||||||
|
direction
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新多个属性
|
||||||
|
* @param updates 更新配置数组
|
||||||
|
*/
|
||||||
|
public batchUpdate(updates: Array<{ id: string; updates: Partial<MapPen> }>): void {
|
||||||
|
// 批量更新所有点位,避免多次渲染
|
||||||
|
updates.forEach(({ id, updates }) => {
|
||||||
|
this.updatePen(id, updates, true, false); // 记录历史但不立即渲染
|
||||||
|
});
|
||||||
|
// 统一渲染一次
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
#register() {
|
#register() {
|
||||||
this.register({ line: () => new Path2D() });
|
this.register({ line: () => new Path2D() });
|
||||||
this.registerCanvasDraw({
|
this.registerCanvasDraw({
|
||||||
@ -1697,13 +1806,6 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
|||||||
const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke');
|
const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke');
|
||||||
ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '');
|
ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '');
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 填充颜色
|
|
||||||
const largeTypeFillColor = colorConfig.getColor(`point.types.${type}.fill`);
|
|
||||||
if (largeTypeFillColor) {
|
|
||||||
ctx.fillStyle = largeTypeFillColor;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
14
src/vite-env.d.ts
vendored
14
src/vite-env.d.ts
vendored
@ -1,7 +1,15 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from 'vue';
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>;
|
||||||
export default component
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly BASE_URL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user