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 { 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 = () => {
|
||||
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
||||
</div>
|
||||
|
||||
<!-- 批量编辑工具栏 - 只在编辑模式下显示 -->
|
||||
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
||||
|
||||
<template v-if="current?.id">
|
||||
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
||||
<template #icon><i class="icon detail" /></template>
|
||||
|
@ -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<number, { stroke: string; strokeActive: string; fill: string }> {
|
||||
const colors: 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 colorSchemes = {
|
||||
@ -138,21 +138,25 @@ function generatePointTypeColors(): Record<number, { stroke: string; strokeActiv
|
||||
// 大点位颜色 (11+)
|
||||
large: {
|
||||
stroke: '#595959',
|
||||
strokeActive: '#EBB214',
|
||||
fills: ['#1890ff', '#52c41a', '#faad14', '#722ed1', '#13c2c2', '#ff4d4f']
|
||||
strokeActive: '#EBB214'
|
||||
}
|
||||
};
|
||||
|
||||
POINT_TYPES.forEach(type => {
|
||||
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<number, any> = {};
|
||||
|
||||
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;
|
||||
|
@ -869,8 +869,8 @@ export class EditorService extends Meta2d {
|
||||
this.delete([pen], true, true);
|
||||
}
|
||||
|
||||
public updatePen(id: string, pen: Partial<MapPen>, record = true): void {
|
||||
this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
|
||||
public updatePen(id: string, pen: Partial<MapPen>, 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<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
|
||||
@ -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<MapPen> }>): 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:
|
||||
|
14
src/vite-env.d.ts
vendored
14
src/vite-env.d.ts
vendored
@ -1,7 +1,15 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user