feat: 新增自动生成库位功能,包含对话框组件和编辑器服务集成,优化库位创建逻辑

This commit is contained in:
xudan 2025-09-09 09:58:06 +08:00
parent 616992e855
commit 87ef9b7212
6 changed files with 471 additions and 22 deletions

View 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>

View File

@ -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>

View File

@ -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);

View File

@ -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);
}
}

View 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 };
}
}

View File

@ -0,0 +1 @@
export { AutoStorageGenerator } from './auto-storage-generator';