diff --git a/docs/批量编辑功能使用说明.md b/docs/批量编辑功能使用说明.md
new file mode 100644
index 0000000..e7851d3
--- /dev/null
+++ b/docs/批量编辑功能使用说明.md
@@ -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. 所有更改都会记录在编辑历史中,支持撤销操作
+
+## 🐛 故障排除
+
+### 问题:批量编辑按钮不可用
+
+- **原因**:没有选中任何元素
+- **解决**:先通过鼠标拖动选中需要编辑的点位或路线
+
+### 问题:编辑面板中没有显示选项
+
+- **原因**:选中的元素类型不支持该编辑选项
+- **解决**:确保选中的是点位或路线类型的元素
+
+### 问题:更改没有生效
+
+- **原因**:可能没有点击"确定"按钮
+- **解决**:在编辑面板中点击"确定"按钮应用更改
diff --git a/src/components/batch-edit-toolbar.vue b/src/components/batch-edit-toolbar.vue
new file mode 100644
index 0000000..da444cc
--- /dev/null
+++ b/src/components/batch-edit-toolbar.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/src/components/modal/batch-edit-modal.vue b/src/components/modal/batch-edit-modal.vue
new file mode 100644
index 0000000..2dda271
--- /dev/null
+++ b/src/components/modal/batch-edit-modal.vue
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
+
+
点位编辑 ({{ pointItems.length }} 个)
+
+
+
+
+ {{ key }}
+
+
+
+
+
+
+
+
+
路线编辑 ({{ lineItems.length }} 个)
+
+
+
+
+ {{ key }}
+
+
+
+
+
+
+
+
+
预览更改
+
+
+
{{ getItemName(item) }}
+
{{ getItemType(item) }}
+
+
+ 点位: {{ getPointChanges(item).join(', ') }}
+
+
+ 路线: {{ getRouteChanges(item).join(', ') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/scene-editor.vue b/src/pages/scene-editor.vue
index 3cf3be4..c5b37b7 100644
--- a/src/pages/scene-editor.vue
+++ b/src/pages/scene-editor.vue
@@ -8,6 +8,7 @@ import { computed, watch } from 'vue';
import { ref } from 'vue';
import { onMounted, provide, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
+import BatchEditToolbar from '@common/batch-edit-toolbar.vue';
const EDITOR_KEY = Symbol('editor-key');
@@ -98,7 +99,7 @@ const toPush = () => {
const importScene = async () => {
const file = await selectFile('.scene');
- if (!file?.size) return;
+ if (!file || !file.size) return;
const json = await decodeTextFile(file);
editor.value?.load(json, editable.value, undefined, true); // 第四个参数isImport=true
};
@@ -218,6 +219,9 @@ const backToCards = () => {
+
+
+
diff --git a/src/services/color/color-config.service.ts b/src/services/color/color-config.service.ts
index 4de8cec..2f5e495 100644
--- a/src/services/color/color-config.service.ts
+++ b/src/services/color/color-config.service.ts
@@ -39,7 +39,7 @@ export interface EditorColorConfig {
[key: number]: {
stroke: string;
strokeActive: string;
- fill: string;
+ fill?: string; // 大点位不需要填充颜色
};
};
};
@@ -124,8 +124,8 @@ export interface EditorColorConfig {
/**
* 生成点位类型的默认颜色配置
*/
-function generatePointTypeColors(): Record {
- const colors: Record = {};
+function generatePointTypeColors(): Record {
+ const colors: Record = {};
// 预定义的颜色方案
const colorSchemes = {
@@ -138,21 +138,25 @@ function generatePointTypeColors(): Record {
const isSmallPoint = type >= 1 && type <= 9;
const scheme = isSmallPoint ? colorSchemes.small : colorSchemes.large;
- const fillIndex = isSmallPoint ? (type - 1) : (type - 11);
colors[type] = {
stroke: scheme.stroke,
- strokeActive: scheme.strokeActive,
- fill: scheme.fills[fillIndex] || scheme.fills[0]
+ strokeActive: scheme.strokeActive
};
+
+ // 只有小点位才设置填充颜色
+ 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;
@@ -712,18 +716,24 @@ class ColorConfigService {
const types: Record = {};
POINT_TYPES.forEach(type => {
+ const isSmallPoint = type >= 1 && type <= 9;
+
types[type] = {
stroke: theme['point-s']?.stroke ||
DEFAULT_COLORS.point.types[type]?.stroke ||
DEFAULT_COLORS.point.small.stroke,
strokeActive: theme['point-s']?.strokeActive ||
DEFAULT_COLORS.point.types[type]?.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]
+ DEFAULT_COLORS.point.small.strokeActive
};
+
+ // 只有小点位才设置填充颜色
+ 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;
diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts
index 130f9f9..7478c06 100644
--- a/src/services/editor.service.ts
+++ b/src/services/editor.service.ts
@@ -869,8 +869,8 @@ export class EditorService extends Meta2d {
this.delete([pen], true, true);
}
- public updatePen(id: string, pen: Partial, record = true): void {
- this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
+ public updatePen(id: string, pen: Partial, record = true, render = true): void {
+ this.setValue({ ...pen, id }, { render, history: record, doEvent: true });
}
/**
@@ -1231,6 +1231,13 @@ export class EditorService extends Meta2d {
},
{ render: true, history: true, doEvent: true },
);
+
+ // 如果是大点位类型(需要图片),异步重新加载主题确保图片正确显示
+ if (type >= 10) {
+ requestAnimationFrame(() => {
+ this.reloadTheme();
+ });
+ }
}
#mapPoint(type: MapPointType): Required> {
@@ -1461,6 +1468,19 @@ export class EditorService extends Meta2d {
this.render();
}
+ /**
+ * 重新加载主题和图片配置
+ * 用于批量编辑后确保大点位图片正确显示
+ * 异步执行,避免阻塞UI
+ */
+ public reloadTheme(): void {
+ const currentTheme = this.data().theme || 'light';
+ // 使用 requestAnimationFrame 确保在下一个渲染帧执行,避免阻塞UI
+ requestAnimationFrame(() => {
+ this.#load(currentTheme);
+ });
+ }
+
#onDelete(pens?: MapPen[]): void {
pens?.forEach((pen) => {
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 }>): void {
+ // 批量更新所有点位,避免多次渲染
+ updates.forEach(({ id, updates }) => {
+ this.updatePen(id, updates, true, false); // 记录历史但不立即渲染
+ });
+ // 统一渲染一次
+ this.render();
+ }
+
#register() {
this.register({ line: () => new Path2D() });
this.registerCanvasDraw({
@@ -1697,13 +1806,6 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
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:
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 323c78a..238bebc 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,7 +1,15 @@
///
declare module '*.vue' {
- import type { DefineComponent } from 'vue'
- const component: DefineComponent<{}, {}, any>
- export default component
+ import type { DefineComponent } from 'vue';
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
}
+
+interface ImportMetaEnv {
+ readonly BASE_URL: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
\ No newline at end of file