feat: 在场景编辑器中新增自定义保存功能,优化场景数据处理逻辑,支持多楼层场景的导入与保存,提升用户体验

This commit is contained in:
xudan 2025-10-14 18:03:14 +08:00
parent 5967f0b45c
commit 3295c02122
6 changed files with 293 additions and 23 deletions

75
precise_multi_floor.scene Normal file
View File

@ -0,0 +1,75 @@
[
{
"points": [
{
"id": "p_charge_01",
"name": "充电桩-01",
"desc": "靠近东门的充电桩",
"x": -150.123,
"y": 80.456,
"type": 11,
"robots": [],
"enabled": 1,
"properties": {}
}
],
"routes": [],
"areas": [],
"robotGroups": [],
"robots": [],
"robotLabels": [],
"scale": 1,
"origin": { "x": 0, "y": 0 },
"width": 1920,
"height": 1080,
"ratio": 1,
"mapInfo": {
"name": "一楼"
},
"colorConfig": {}
},
{
"points": [
{
"id": "f2_p_charge_01",
"name": "二楼充电桩",
"x": 88.000,
"y": -120.000,
"type": 11,
"enabled": 1,
"properties": {}
},
{
"id": "f2_p_action_01",
"name": "二楼动作点",
"x": 88.000,
"y": -50.000,
"type": 12,
"associatedStorageLocations": ["F2-A-01"],
"properties": {}
}
],
"routes": [
{
"id": "f2_route_1",
"from": "f2_p_charge_01",
"to": "f2_p_action_01",
"type": 0,
"pass": 0
}
],
"areas": [],
"robotGroups": [],
"robots": [],
"robotLabels": [],
"scale": 1,
"origin": { "x": 0, "y": 0 },
"width": 1920,
"height": 1080,
"ratio": 1,
"mapInfo": {
"name": "二楼"
},
"colorConfig": {}
}
]

View File

@ -0,0 +1,91 @@
{
"points": [
{
"id": "p_charge_01",
"name": "充电桩-01",
"desc": "靠近东门的充电桩",
"x": -150.123,
"y": 80.456,
"type": 11,
"robots": [],
"enabled": 1,
"properties": {}
},
{
"id": "p_station_01",
"name": "工作站-A",
"x": 50.789,
"y": 80.456,
"type": 10,
"properties": {}
},
{
"id": "p_action_01",
"name": "动作点-货架1",
"x": 50.789,
"y": -20.000,
"type": 12,
"associatedStorageLocations": [
"A-01-01",
"A-01-02"
],
"properties": {}
}
],
"routes": [
{
"id": "route_1",
"from": "p_charge_01",
"to": "p_station_01",
"type": 0,
"pass": 0,
"maxSpeed": 1.2,
"properties": {}
},
{
"id": "route_2",
"from": "p_station_01",
"to": "p_action_01",
"type": 1,
"c1": {
"x": 60.000,
"y": 30.000
},
"pass": 1,
"properties": {}
}
],
"areas": [
{
"id": "area_storage_A",
"name": "A库区",
"x": 20.111,
"y": -40.222,
"w": 100.000,
"h": 80.000,
"type": 0,
"points": [
"动作点-货架1"
],
"storageLocations": [
{
"动作点-货架1": [
"A-01-01",
"A-01-02"
]
}
],
"inoutflag": 2,
"properties": {}
}
],
"robotGroups": [],
"robots": [],
"robotLabels": [],
"scale": 1,
"origin": { "x": 0, "y": 0 },
"width": 1920,
"height": 1080,
"ratio": 1,
"colorConfig": {}
}

View File

@ -17,7 +17,7 @@ const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
const point = computed<MapPointInfo | null>(() => {
const v = pen.value?.point;
if (!v?.type) return null;
if (isNil(v?.type)) return null;
return v;
});
const rect = computed<Partial<Rect>>(() => {
@ -105,7 +105,9 @@ const binTaskData = computed(() => {
<a-flex align="center" :gap="8">
<i class="icon point" />
<a-typography-text class="card-title" style="flex: auto" :content="pen.label" ellipsis />
<a-tag :bordered="false">{{ $t(MapPointType[point.type]) }}</a-tag>
<a-tag v-if="typeof point.type === 'number' && MapPointType[point.type]" :bordered="false">{{
$t(MapPointType[point.type])
}}</a-tag>
</a-flex>
</a-col>

View File

@ -6,11 +6,17 @@ import { isEmpty } from 'lodash-es';
import { computed } from 'vue';
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
type SavePayload = {
json: string;
png?: string;
};
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
editable?: boolean;
sid?: string;
id: string;
customSave?: (payload: SavePayload) => Promise<void>;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
@ -19,12 +25,17 @@ const editor = inject(props.token)!;
const updateScene = async () => {
const json = editor.value.save();
if (!json) return;
if (props.customSave) {
const png = editor.value.toPng(8, undefined, true);
await props.customSave({ json, png });
return;
}
if (props.sid) {
await saveSceneByGroupId(props.id, props.sid, json);
} else {
const png = editor.value.toPng(8, undefined, true);
await saveSceneById(props.id, json, png);
return;
}
const png = editor.value.toPng(8, undefined, true);
await saveSceneById(props.id, json, png);
};
//#endregion

View File

@ -396,6 +396,7 @@ onMounted(() => {
//#region
onMounted(async () => {
console.log('Current route in movement-supervision.vue:', window.location.href);
if (mode.value === 'live') {
await readScene();
await monitorScene();

View File

@ -32,45 +32,71 @@ const readScene = async () => {
title.value = res?.label ?? '';
let sceneJson = res?.json;
requireImportTransform.value = false;
//
if (Array.isArray(sceneJson)) {
if (sceneJson.length > 0) {
//
floorScenes.value = sceneJson;
currentFloorIndex.value = 0;
editor.value?.load(floorScenes.value[0], editable.value);
previousFloorIndex.value = 0;
editor.value?.load(floorScenes.value[0], editable.value, undefined, requireImportTransform.value);
} else {
//
message.warn('场景文件为空数组');
floorScenes.value = [{}]; //
currentFloorIndex.value = 0;
editor.value?.load('{}', editable.value);
previousFloorIndex.value = 0;
editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
}
} else if (sceneJson && Object.keys(sceneJson).length > 0) {
//
floorScenes.value = [sceneJson];
currentFloorIndex.value = 0;
editor.value?.load(floorScenes.value[0], editable.value);
previousFloorIndex.value = 0;
editor.value?.load(floorScenes.value[0], editable.value, undefined, requireImportTransform.value);
} else {
message.warn('场景文件为空或格式不正确');
floorScenes.value = [{}]; //
currentFloorIndex.value = 0;
editor.value?.load('{}', editable.value);
previousFloorIndex.value = 0;
editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
}
};
const saveScene = async () => {
// floorScenes
const currentJson = editor.value?.save();
type SaveScenePayload = {
json?: string;
png?: string;
};
const saveScene = async (payload?: SaveScenePayload) => {
const currentJson = payload?.json ?? editor.value?.save();
if (currentJson) {
floorScenes.value[currentFloorIndex.value] = JSON.parse(currentJson);
try {
const parsed = typeof currentJson === 'string' ? JSON.parse(currentJson) : currentJson;
floorScenes.value[currentFloorIndex.value] = parsed;
} catch (error) {
console.error('解析楼层数据失败:', error);
floorScenes.value[currentFloorIndex.value] = currentJson as any;
}
}
//
const dataToSave = floorScenes.value.length > 1 ? floorScenes.value : floorScenes.value[0];
let dataToSave: string | undefined;
if (floorScenes.value.length > 1) {
dataToSave = JSON.stringify(floorScenes.value);
} else {
const singleScene = floorScenes.value[0];
if (typeof singleScene === 'string') {
dataToSave = singleScene;
} else if (singleScene) {
dataToSave = JSON.stringify(singleScene);
} else {
dataToSave = '{}';
}
}
if (!dataToSave) return Promise.reject('无法获取场景数据');
const res = await saveSceneById(props.id, dataToSave);
const res = await saveSceneById(props.id, dataToSave, payload?.png);
if (!res) return Promise.reject('保存失败');
if (editor.value?.store) {
@ -89,6 +115,10 @@ const pushScene = async () => {
return Promise.resolve();
};
const handleToolbarSave = async (payload: SaveScenePayload) => {
await saveScene(payload);
};
//#endregion
const title = ref<string>('');
@ -96,9 +126,11 @@ const title = ref<string>('');
const floorScenes = ref<any[]>([]);
//
const currentFloorIndex = ref(0);
const previousFloorIndex = ref(0);
//
const isMultiFloor = computed(() => floorScenes.value.length > 1);
const requireImportTransform = ref(false);
onMounted(() => {
document.title = '场景编辑器';
@ -199,8 +231,54 @@ const toPush = () => {
const importScene = async () => {
const file = await selectFile('.scene');
if (!file) return;
const json = await decodeTextFile(file);
editor.value?.load(json, editable.value, undefined, true);
const jsonString = await decodeTextFile(file);
if (!jsonString) {
message.error('文件内容为空或无法读取');
return;
}
try {
const sceneData = JSON.parse(jsonString);
//
if (Array.isArray(sceneData)) {
requireImportTransform.value = true;
//
if (sceneData.length > 0) {
floorScenes.value = sceneData;
currentFloorIndex.value = 0;
previousFloorIndex.value = 0;
//
await editor.value?.load(floorScenes.value[0], editable.value, undefined, requireImportTransform.value);
message.success(`成功导入 ${sceneData.length} 个楼层,当前显示第一层。`);
} else {
message.warn('导入的场景文件是一个空数组,已加载空场景。');
floorScenes.value = [{}];
currentFloorIndex.value = 0;
previousFloorIndex.value = 0;
await editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
}
} else if (sceneData && typeof sceneData === 'object' && Object.keys(sceneData).length > 0) {
//
requireImportTransform.value = true;
floorScenes.value = [sceneData];
currentFloorIndex.value = 0;
previousFloorIndex.value = 0;
// editor.load
await editor.value?.load(sceneData, editable.value, undefined, requireImportTransform.value);
message.success('成功导入单楼层场景。');
} else {
message.error('导入失败,场景文件为空或格式不正确。');
// 退
floorScenes.value = [{}];
currentFloorIndex.value = 0;
previousFloorIndex.value = 0;
await editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
}
} catch (error) {
console.error('场景文件解析失败:', error);
message.error('导入失败文件不是有效的JSON格式。');
}
};
const importSmapModalVisible = ref(false);
@ -225,7 +303,8 @@ const handleImportSmapConfirm = async ({ smapFile, keepProperties }: { smapFile:
}
if (sceneJson) {
editor.value?.load(sceneJson, editable.value, undefined, true);
requireImportTransform.value = true;
editor.value?.load(sceneJson, editable.value, undefined, requireImportTransform.value);
}
};
@ -332,17 +411,28 @@ const handleAutoCreateStorageCancel = () => {
const handleFloorChange = async (value: any) => {
const newFloorIndex = value as number;
const prevFloorIndex = previousFloorIndex.value;
if (editor.value && floorScenes.value[newFloorIndex]) {
//
const currentJson = editor.value.save();
floorScenes.value[currentFloorIndex.value] = JSON.parse(currentJson);
if (currentJson) {
floorScenes.value[prevFloorIndex] = JSON.parse(currentJson);
}
//
currentFloorIndex.value = newFloorIndex;
await editor.value.load(floorScenes.value[newFloorIndex], editable.value);
previousFloorIndex.value = newFloorIndex;
await editor.value.load(
floorScenes.value[newFloorIndex],
editable.value,
undefined,
requireImportTransform.value,
);
} else {
currentFloorIndex.value = newFloorIndex;
previousFloorIndex.value = newFloorIndex;
}
};
</script>
<template>
@ -450,7 +540,7 @@ const handleFloorChange = async (value: any) => {
</a-layout>
<div v-if="editable" class="toolbar-container">
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" :custom-save="handleToolbarSave" />
</div>
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />