feat: 在场景编辑器中新增自定义保存功能,优化场景数据处理逻辑,支持多楼层场景的导入与保存,提升用户体验
This commit is contained in:
parent
5967f0b45c
commit
3295c02122
75
precise_multi_floor.scene
Normal file
75
precise_multi_floor.scene
Normal 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": {}
|
||||
}
|
||||
]
|
91
precise_single_floor.scene
Normal file
91
precise_single_floor.scene
Normal 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": {}
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user