feat: 新增自动生成库位功能,包含对话框组件和编辑器服务集成,优化库位创建逻辑
This commit is contained in:
parent
616992e855
commit
87ef9b7212
159
src/components/modal/auto-create-storage-modal.vue
Normal file
159
src/components/modal/auto-create-storage-modal.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="自动生成库位"
|
||||
width="500px"
|
||||
@ok="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="auto-create-storage-content">
|
||||
<a-alert
|
||||
message="检测到库区中包含动作点"
|
||||
:description="`库区 ${areaName} 包含 ${actionPoints.length} 个动作点,是否自动为这些动作点生成库位?`"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<div class="storage-preview">
|
||||
<h4>预览生成的库位:</h4>
|
||||
<div class="preview-list">
|
||||
<div
|
||||
v-for="point in actionPoints"
|
||||
:key="point.id"
|
||||
class="preview-item"
|
||||
>
|
||||
<div class="point-info">
|
||||
<span class="point-name">{{ point.label || point.id }}</span>
|
||||
<span class="point-type">动作点</span>
|
||||
</div>
|
||||
<div class="storage-locations">
|
||||
<a-tag
|
||||
v-for="locationName in getGeneratedLocationNames(point)"
|
||||
:key="locationName"
|
||||
color="blue"
|
||||
class="location-tag"
|
||||
>
|
||||
{{ locationName }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
message="库位命名规则"
|
||||
description="库位名称格式:库区名_动作点名称_序号(如:A001_P001_1)"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
areaName: string;
|
||||
actionPoints: any[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'confirm', actionPoints: any[]): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
// 生成库位名称
|
||||
const getGeneratedLocationNames = (point: any): string[] => {
|
||||
const pointName = point.label || point.id || '';
|
||||
const areaName = props.areaName;
|
||||
|
||||
// 为每个动作点生成一个库位,名称格式:库区名_动作点名称_1
|
||||
return [`${areaName}_${pointName}_1`];
|
||||
};
|
||||
|
||||
// 确认生成库位
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', props.actionPoints);
|
||||
modalVisible.value = false;
|
||||
};
|
||||
|
||||
// 取消生成
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
modalVisible.value = false;
|
||||
};
|
||||
|
||||
defineOptions({
|
||||
name: 'AutoCreateStorageModal'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auto-create-storage-content {
|
||||
.storage-preview {
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.point-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.point-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.point-type {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.storage-locations {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.location-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { getSceneById, pushSceneById, saveSceneById } from '@api/scene';
|
||||
import { EditorService } from '@core/editor.service';
|
||||
import { useViewState } from '@core/useViewState';
|
||||
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
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';
|
||||
|
||||
import type { MapPen } from '../apis/map';
|
||||
import { getSceneById, pushSceneById, saveSceneById } from '../apis/scene';
|
||||
import BatchEditToolbar from '../components/batch-edit-toolbar.vue';
|
||||
import AutoCreateStorageModal from '../components/modal/auto-create-storage-modal.vue';
|
||||
import { EditorService } from '../services/editor.service';
|
||||
import { useViewState } from '../services/useViewState';
|
||||
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '../services/utils';
|
||||
|
||||
const EDITOR_KEY = Symbol('editor-key');
|
||||
|
||||
@ -71,8 +74,25 @@ watch(
|
||||
const container = shallowRef<HTMLDivElement>();
|
||||
const editor = shallowRef<EditorService>();
|
||||
provide(EDITOR_KEY, editor);
|
||||
|
||||
// 自动生成库位对话框相关状态
|
||||
const autoCreateStorageVisible = ref(false);
|
||||
const autoCreateStorageData = ref<{
|
||||
areaName: string;
|
||||
actionPoints: any[];
|
||||
}>({
|
||||
areaName: '',
|
||||
actionPoints: []
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
editor.value = new EditorService(container.value!);
|
||||
|
||||
// 监听自动生成库位对话框事件
|
||||
editor.value?.on('autoCreateStorageDialog', (data: any) => {
|
||||
autoCreateStorageData.value = data;
|
||||
autoCreateStorageVisible.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
const editable = ref<boolean>(false);
|
||||
@ -162,6 +182,19 @@ const handleAutoSaveAndRestoreViewState = async () => {
|
||||
const backToCards = () => {
|
||||
window.parent?.postMessage({ type: 'scene_return_to_cards' }, '*');
|
||||
};
|
||||
|
||||
// 处理自动生成库位确认
|
||||
const handleAutoCreateStorageConfirm = (actionPoints: any[]) => {
|
||||
if (!editor.value) return;
|
||||
|
||||
editor.value.autoCreateStorageLocations(autoCreateStorageData.value.areaName, actionPoints);
|
||||
message.success(`已为 ${actionPoints.length} 个动作点自动生成库位`);
|
||||
};
|
||||
|
||||
// 处理自动生成库位取消
|
||||
const handleAutoCreateStorageCancel = () => {
|
||||
autoCreateStorageVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -222,6 +255,15 @@ const backToCards = () => {
|
||||
<!-- 批量编辑工具栏 - 只在编辑模式下显示 -->
|
||||
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
||||
|
||||
<!-- 自动生成库位对话框 -->
|
||||
<AutoCreateStorageModal
|
||||
v-model:visible="autoCreateStorageVisible"
|
||||
:area-name="autoCreateStorageData.areaName"
|
||||
:action-points="autoCreateStorageData.actionPoints"
|
||||
@confirm="handleAutoCreateStorageConfirm"
|
||||
@cancel="handleAutoCreateStorageCancel"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
} from './draw/storage-location-drawer';
|
||||
import { LayerManagerService } from './layer-manager.service';
|
||||
import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service';
|
||||
import { AutoStorageGenerator } from './utils/auto-storage-generator';
|
||||
|
||||
/**
|
||||
* 场景编辑器服务类
|
||||
@ -59,6 +60,9 @@ export class EditorService extends Meta2d {
|
||||
/** 区域操作服务实例 */
|
||||
private readonly areaOperationService!: AreaOperationService;
|
||||
|
||||
/** 自动生成库位工具实例 */
|
||||
private readonly autoStorageGenerator!: AutoStorageGenerator;
|
||||
|
||||
//#region 场景文件管理
|
||||
/**
|
||||
* 加载场景文件到编辑器
|
||||
@ -947,9 +951,56 @@ export class EditorService extends Meta2d {
|
||||
/**
|
||||
* 为所有动作点创建库位pen对象
|
||||
* 用于初始化或刷新所有动作点的库位显示
|
||||
* 优化性能:使用批量操作减少DOM操作次数
|
||||
*/
|
||||
public createAllStorageLocationPens(): void {
|
||||
this.storageLocationService?.createAll();
|
||||
if (!this.storageLocationService) return;
|
||||
|
||||
// 使用 requestIdleCallback 在浏览器空闲时执行,避免阻塞UI
|
||||
if (typeof requestIdleCallback !== 'undefined') {
|
||||
requestIdleCallback(() => {
|
||||
this.storageLocationService?.createAll();
|
||||
}, { timeout: 200 });
|
||||
} else {
|
||||
// 降级到 requestAnimationFrame
|
||||
requestAnimationFrame(() => {
|
||||
this.storageLocationService?.createAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动为动作点生成库位
|
||||
* @param areaName 库区名称
|
||||
* @param actionPoints 动作点列表
|
||||
*/
|
||||
public autoCreateStorageLocations(areaName: string, actionPoints: MapPen[]): void {
|
||||
this.autoStorageGenerator.autoCreateStorageLocations(areaName, actionPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定动作点生成库位
|
||||
* @param pointId 动作点ID
|
||||
* @param areaName 库区名称
|
||||
* @returns 生成的库位名称,如果失败返回null
|
||||
*/
|
||||
public generateStorageForPoint(pointId: string, areaName: string): string | null {
|
||||
return this.autoStorageGenerator.generateStorageForPoint(pointId, areaName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成库位
|
||||
* @param pointIds 动作点ID列表
|
||||
* @param areaName 库区名称
|
||||
* @returns 生成结果统计
|
||||
*/
|
||||
public batchGenerateStorageForPoints(pointIds: string[], areaName: string): {
|
||||
success: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
generatedNames: string[];
|
||||
} {
|
||||
return this.autoStorageGenerator.batchGenerateStorageForPoints(pointIds, areaName);
|
||||
}
|
||||
|
||||
|
||||
@ -1197,14 +1248,14 @@ export class EditorService extends Meta2d {
|
||||
}
|
||||
}
|
||||
|
||||
public updatePoint(id: string, info: Partial<MapPointInfo>): void {
|
||||
public updatePoint(id: string, info: Partial<MapPointInfo>, autoCreateStorage = true): void {
|
||||
const { point } = this.getPenById(id) ?? {};
|
||||
if (!point?.type) return;
|
||||
const o = { ...point, ...info };
|
||||
this.setValue({ id, point: o }, { render: true, history: true, doEvent: true });
|
||||
|
||||
// 如果是动作点且库位信息发生变化,重新创建库位pen对象
|
||||
if (point.type === MapPointType.动作点 && info.associatedStorageLocations) {
|
||||
if (point.type === MapPointType.动作点 && info.associatedStorageLocations && autoCreateStorage) {
|
||||
this.createStorageLocationPens(id, info.associatedStorageLocations);
|
||||
}
|
||||
}
|
||||
@ -1399,6 +1450,11 @@ export class EditorService extends Meta2d {
|
||||
};
|
||||
const area = await this.addPen(pen, true, true, true);
|
||||
this.bottom(area);
|
||||
|
||||
// 如果是库区且包含动作点,触发自动生成库位的确认对话框
|
||||
if (type === MapAreaType.库区 && points.length > 0) {
|
||||
this.autoStorageGenerator.triggerAutoCreateStorageDialog(pen, points);
|
||||
}
|
||||
}
|
||||
|
||||
public updateArea(id: string, info: Partial<MapAreaInfo>): void {
|
||||
@ -1425,6 +1481,9 @@ export class EditorService extends Meta2d {
|
||||
// 初始化库位服务
|
||||
this.storageLocationService = new StorageLocationService(this, '');
|
||||
|
||||
// 初始化自动生成库位工具
|
||||
this.autoStorageGenerator = new AutoStorageGenerator(this);
|
||||
|
||||
// 设置颜色配置服务的编辑器实例
|
||||
colorConfig.setEditorService(this);
|
||||
|
||||
|
||||
@ -518,16 +518,10 @@ export class StorageLocationService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在库位pen对象
|
||||
const existingPens = this.getStorageLocationPens(pointId);
|
||||
// 先删除已存在的库位pen对象,避免重复
|
||||
this.delete(pointId);
|
||||
|
||||
// 如果已存在pen对象,只更新状态,不重新创建
|
||||
if (existingPens.length > 0) {
|
||||
this.update(pointId, storageLocations);
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有在不存在pen对象时才创建新的
|
||||
// 创建新的库位pen对象
|
||||
const pointRect = this.editor.getPointRect(pointPen) ?? { x: 0, y: 0, width: 0, height: 0 };
|
||||
const pens = createStorageLocationPens(pointId, storageLocations, pointRect, storageStateMap);
|
||||
|
||||
@ -605,14 +599,24 @@ export class StorageLocationService {
|
||||
if (!this.editor) return;
|
||||
|
||||
const { penTypes, tags } = this.config;
|
||||
const storagePens = this.editor.find(
|
||||
`${penTypes.storageLocation},${penTypes.storageMore},${penTypes.storageBackground}`
|
||||
).filter(
|
||||
|
||||
// 分别查找每种类型的库位pen对象
|
||||
const storageLocationPens = this.editor.find(penTypes.storageLocation).filter(
|
||||
(pen) => pen.tags?.includes(`${tags.pointPrefix}${pointId}`)
|
||||
);
|
||||
|
||||
if (storagePens.length > 0) {
|
||||
this.editor.delete(storagePens, true, true);
|
||||
const storageMorePens = this.editor.find(penTypes.storageMore).filter(
|
||||
(pen) => pen.tags?.includes(`${tags.pointPrefix}${pointId}`)
|
||||
);
|
||||
|
||||
const storageBackgroundPens = this.editor.find(penTypes.storageBackground).filter(
|
||||
(pen) => pen.tags?.includes(`${tags.pointPrefix}${pointId}`)
|
||||
);
|
||||
|
||||
const allStoragePens = [...storageLocationPens, ...storageMorePens, ...storageBackgroundPens];
|
||||
|
||||
if (allStoragePens.length > 0) {
|
||||
this.editor.delete(allStoragePens, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
184
src/services/utils/auto-storage-generator.ts
Normal file
184
src/services/utils/auto-storage-generator.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import type { MapPen } from '../../apis/map';
|
||||
import { MapPointType } from '../../apis/map';
|
||||
import type { EditorService } from '../editor.service';
|
||||
|
||||
/**
|
||||
* 自动生成库位工具类
|
||||
* 负责处理库区创建时的库位自动生成逻辑
|
||||
*/
|
||||
export class AutoStorageGenerator {
|
||||
private editor: EditorService;
|
||||
|
||||
constructor(editor: EditorService) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发自动生成库位的确认对话框
|
||||
* @param area 库区对象
|
||||
* @param pointIds 动作点ID列表
|
||||
*/
|
||||
public triggerAutoCreateStorageDialog(area: MapPen, pointIds: string[]): void {
|
||||
const actionPoints = pointIds
|
||||
.map(id => this.editor.getPenById(id))
|
||||
.filter((pen): pen is MapPen => !!pen && pen.point?.type === MapPointType.动作点);
|
||||
|
||||
if (actionPoints.length === 0) return;
|
||||
|
||||
// 触发自定义事件,让外部组件处理对话框显示
|
||||
this.editor.emit('autoCreateStorageDialog', {
|
||||
areaName: area.label || area.id || '',
|
||||
actionPoints
|
||||
} as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动为动作点生成库位
|
||||
* @param areaName 库区名称
|
||||
* @param actionPoints 动作点列表
|
||||
*/
|
||||
public autoCreateStorageLocations(areaName: string, actionPoints: MapPen[]): void {
|
||||
actionPoints.forEach(point => {
|
||||
if (point.point?.type !== MapPointType.动作点) return;
|
||||
|
||||
const pointName = point.label || point.id || '';
|
||||
const locationName = this.generateStorageLocationName(areaName, pointName);
|
||||
|
||||
// 获取现有的库位列表,避免覆盖
|
||||
const existingLocations = point.point.associatedStorageLocations || [];
|
||||
|
||||
// 检查是否已存在相同的库位名称
|
||||
if (existingLocations.includes(locationName)) {
|
||||
return; // 如果已存在,跳过
|
||||
}
|
||||
|
||||
// 添加新库位到现有列表
|
||||
const newLocations = [...existingLocations, locationName];
|
||||
|
||||
// 更新动作点的库位信息(禁用自动创建库位pen对象,避免重复)
|
||||
this.editor.updatePoint(point.id!, {
|
||||
associatedStorageLocations: newLocations
|
||||
}, false);
|
||||
|
||||
// 重新创建库位pen对象(create方法内部会先删除再创建)
|
||||
this.editor.createStorageLocationPens(point.id!, newLocations);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成库位名称
|
||||
* @param areaName 库区名称
|
||||
* @param pointName 动作点名称
|
||||
* @returns 生成的库位名称
|
||||
*/
|
||||
private generateStorageLocationName(areaName: string, pointName: string): string {
|
||||
const baseName = `${areaName}_${pointName}_1`;
|
||||
|
||||
// 检查是否已存在同名库位
|
||||
if (!this.checkStorageLocationExists(baseName)) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
// 如果存在,则递增序号
|
||||
let counter = 2;
|
||||
let newName = `${areaName}_${pointName}_${counter}`;
|
||||
|
||||
while (this.checkStorageLocationExists(newName)) {
|
||||
counter++;
|
||||
newName = `${areaName}_${pointName}_${counter}`;
|
||||
}
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查库位名称是否已存在
|
||||
* @param locationName 库位名称
|
||||
* @returns 如果存在则返回true
|
||||
*/
|
||||
private checkStorageLocationExists(locationName: string): boolean {
|
||||
// 检查所有动作点的库位信息
|
||||
const allPoints = this.editor.find('point').filter(
|
||||
pen => pen.point?.type === MapPointType.动作点
|
||||
);
|
||||
|
||||
return allPoints.some(point =>
|
||||
point.point?.associatedStorageLocations?.includes(locationName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定动作点生成库位(单个)
|
||||
* @param pointId 动作点ID
|
||||
* @param areaName 库区名称
|
||||
* @returns 生成的库位名称,如果失败返回null
|
||||
*/
|
||||
public generateStorageForPoint(pointId: string, areaName: string): string | null {
|
||||
const point = this.editor.getPenById(pointId);
|
||||
if (!point || point.point?.type !== MapPointType.动作点) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointName = point.label || point.id || '';
|
||||
const locationName = this.generateStorageLocationName(areaName, pointName);
|
||||
|
||||
// 获取现有的库位列表
|
||||
const existingLocations = point.point.associatedStorageLocations || [];
|
||||
|
||||
// 检查是否已存在相同的库位名称
|
||||
if (existingLocations.includes(locationName)) {
|
||||
return null; // 如果已存在,返回null
|
||||
}
|
||||
|
||||
// 添加新库位到现有列表
|
||||
const newLocations = [...existingLocations, locationName];
|
||||
|
||||
// 先删除原有的库位pen对象,避免重复
|
||||
this.editor.deleteStorageLocation(pointId);
|
||||
|
||||
// 更新动作点的库位信息
|
||||
this.editor.updatePoint(pointId, {
|
||||
associatedStorageLocations: newLocations
|
||||
});
|
||||
|
||||
// 重新创建库位pen对象
|
||||
this.editor.createStorageLocationPens(pointId, newLocations);
|
||||
|
||||
return locationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成库位(为多个动作点)
|
||||
* @param pointIds 动作点ID列表
|
||||
* @param areaName 库区名称
|
||||
* @returns 生成结果统计
|
||||
*/
|
||||
public batchGenerateStorageForPoints(pointIds: string[], areaName: string): {
|
||||
success: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
generatedNames: string[];
|
||||
} {
|
||||
let success = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
const generatedNames: string[] = [];
|
||||
|
||||
pointIds.forEach(pointId => {
|
||||
const result = this.generateStorageForPoint(pointId, areaName);
|
||||
if (result) {
|
||||
success++;
|
||||
generatedNames.push(result);
|
||||
} else {
|
||||
const point = this.editor.getPenById(pointId);
|
||||
if (point && point.point?.type === MapPointType.动作点) {
|
||||
skipped++; // 已存在相同名称
|
||||
} else {
|
||||
failed++; // 不是动作点或不存在
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { success, skipped, failed, generatedNames };
|
||||
}
|
||||
}
|
||||
1
src/services/utils/index.ts
Normal file
1
src/services/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AutoStorageGenerator } from './auto-storage-generator';
|
||||
Loading…
x
Reference in New Issue
Block a user