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 pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
|
||||||
const point = computed<MapPointInfo | null>(() => {
|
const point = computed<MapPointInfo | null>(() => {
|
||||||
const v = pen.value?.point;
|
const v = pen.value?.point;
|
||||||
if (!v?.type) return null;
|
if (isNil(v?.type)) return null;
|
||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
const rect = computed<Partial<Rect>>(() => {
|
const rect = computed<Partial<Rect>>(() => {
|
||||||
@ -105,7 +105,9 @@ const binTaskData = computed(() => {
|
|||||||
<a-flex align="center" :gap="8">
|
<a-flex align="center" :gap="8">
|
||||||
<i class="icon point" />
|
<i class="icon point" />
|
||||||
<a-typography-text class="card-title" style="flex: auto" :content="pen.label" ellipsis />
|
<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-flex>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
|
@ -6,11 +6,17 @@ import { isEmpty } from 'lodash-es';
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
|
import { inject, type InjectionKey, ref, type ShallowRef, watch } from 'vue';
|
||||||
|
|
||||||
|
type SavePayload = {
|
||||||
|
json: string;
|
||||||
|
png?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
token: InjectionKey<ShallowRef<EditorService>>;
|
token: InjectionKey<ShallowRef<EditorService>>;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
sid?: string;
|
sid?: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
customSave?: (payload: SavePayload) => Promise<void>;
|
||||||
};
|
};
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const editor = inject(props.token)!;
|
const editor = inject(props.token)!;
|
||||||
@ -19,12 +25,17 @@ const editor = inject(props.token)!;
|
|||||||
const updateScene = async () => {
|
const updateScene = async () => {
|
||||||
const json = editor.value.save();
|
const json = editor.value.save();
|
||||||
if (!json) return;
|
if (!json) return;
|
||||||
|
if (props.customSave) {
|
||||||
|
const png = editor.value.toPng(8, undefined, true);
|
||||||
|
await props.customSave({ json, png });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (props.sid) {
|
if (props.sid) {
|
||||||
await saveSceneByGroupId(props.id, props.sid, json);
|
await saveSceneByGroupId(props.id, props.sid, json);
|
||||||
} else {
|
return;
|
||||||
const png = editor.value.toPng(8, undefined, true);
|
|
||||||
await saveSceneById(props.id, json, png);
|
|
||||||
}
|
}
|
||||||
|
const png = editor.value.toPng(8, undefined, true);
|
||||||
|
await saveSceneById(props.id, json, png);
|
||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -396,6 +396,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
//#region 生命周期管理
|
//#region 生命周期管理
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
console.log('Current route in movement-supervision.vue:', window.location.href);
|
||||||
if (mode.value === 'live') {
|
if (mode.value === 'live') {
|
||||||
await readScene();
|
await readScene();
|
||||||
await monitorScene();
|
await monitorScene();
|
||||||
|
@ -32,45 +32,71 @@ const readScene = async () => {
|
|||||||
title.value = res?.label ?? '';
|
title.value = res?.label ?? '';
|
||||||
let sceneJson = res?.json;
|
let sceneJson = res?.json;
|
||||||
|
|
||||||
|
requireImportTransform.value = false;
|
||||||
// 适配不同的场景数据格式
|
// 适配不同的场景数据格式
|
||||||
if (Array.isArray(sceneJson)) {
|
if (Array.isArray(sceneJson)) {
|
||||||
if (sceneJson.length > 0) {
|
if (sceneJson.length > 0) {
|
||||||
// 多楼层
|
// 多楼层
|
||||||
floorScenes.value = sceneJson;
|
floorScenes.value = sceneJson;
|
||||||
currentFloorIndex.value = 0;
|
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 {
|
} else {
|
||||||
// 空数组
|
// 空数组
|
||||||
message.warn('场景文件为空数组');
|
message.warn('场景文件为空数组');
|
||||||
floorScenes.value = [{}]; // 创建一个默认的空楼层
|
floorScenes.value = [{}]; // 创建一个默认的空楼层
|
||||||
currentFloorIndex.value = 0;
|
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) {
|
} else if (sceneJson && Object.keys(sceneJson).length > 0) {
|
||||||
// 单楼层,统一为多楼层数组格式
|
// 单楼层,统一为多楼层数组格式
|
||||||
floorScenes.value = [sceneJson];
|
floorScenes.value = [sceneJson];
|
||||||
currentFloorIndex.value = 0;
|
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 {
|
} else {
|
||||||
message.warn('场景文件为空或格式不正确');
|
message.warn('场景文件为空或格式不正确');
|
||||||
floorScenes.value = [{}]; // 创建一个默认的空楼层
|
floorScenes.value = [{}]; // 创建一个默认的空楼层
|
||||||
currentFloorIndex.value = 0;
|
currentFloorIndex.value = 0;
|
||||||
editor.value?.load('{}', editable.value);
|
previousFloorIndex.value = 0;
|
||||||
|
editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveScene = async () => {
|
type SaveScenePayload = {
|
||||||
// 保存前,先将当前编辑器的内容更新到 floorScenes 中
|
json?: string;
|
||||||
const currentJson = editor.value?.save();
|
png?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveScene = async (payload?: SaveScenePayload) => {
|
||||||
|
const currentJson = payload?.json ?? editor.value?.save();
|
||||||
if (currentJson) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据楼层数量决定保存单层对象还是多层数组
|
let dataToSave: string | undefined;
|
||||||
const dataToSave = floorScenes.value.length > 1 ? floorScenes.value : floorScenes.value[0];
|
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('无法获取场景数据');
|
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 (!res) return Promise.reject('保存失败');
|
||||||
|
|
||||||
if (editor.value?.store) {
|
if (editor.value?.store) {
|
||||||
@ -89,6 +115,10 @@ const pushScene = async () => {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToolbarSave = async (payload: SaveScenePayload) => {
|
||||||
|
await saveScene(payload);
|
||||||
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const title = ref<string>('');
|
const title = ref<string>('');
|
||||||
@ -96,9 +126,11 @@ const title = ref<string>('');
|
|||||||
const floorScenes = ref<any[]>([]);
|
const floorScenes = ref<any[]>([]);
|
||||||
// 新增:当前楼层的索引
|
// 新增:当前楼层的索引
|
||||||
const currentFloorIndex = ref(0);
|
const currentFloorIndex = ref(0);
|
||||||
|
const previousFloorIndex = ref(0);
|
||||||
|
|
||||||
// 新增:判断是否为多楼层模式
|
// 新增:判断是否为多楼层模式
|
||||||
const isMultiFloor = computed(() => floorScenes.value.length > 1);
|
const isMultiFloor = computed(() => floorScenes.value.length > 1);
|
||||||
|
const requireImportTransform = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.title = '场景编辑器';
|
document.title = '场景编辑器';
|
||||||
@ -199,8 +231,54 @@ const toPush = () => {
|
|||||||
const importScene = async () => {
|
const importScene = async () => {
|
||||||
const file = await selectFile('.scene');
|
const file = await selectFile('.scene');
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const json = await decodeTextFile(file);
|
const jsonString = await decodeTextFile(file);
|
||||||
editor.value?.load(json, editable.value, undefined, true);
|
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);
|
const importSmapModalVisible = ref(false);
|
||||||
@ -225,7 +303,8 @@ const handleImportSmapConfirm = async ({ smapFile, keepProperties }: { smapFile:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sceneJson) {
|
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 handleFloorChange = async (value: any) => {
|
||||||
const newFloorIndex = value as number;
|
const newFloorIndex = value as number;
|
||||||
|
const prevFloorIndex = previousFloorIndex.value;
|
||||||
if (editor.value && floorScenes.value[newFloorIndex]) {
|
if (editor.value && floorScenes.value[newFloorIndex]) {
|
||||||
// 切换前,先保存当前楼层的状态
|
// 切换前,先保存当前楼层的状态
|
||||||
const currentJson = editor.value.save();
|
const currentJson = editor.value.save();
|
||||||
floorScenes.value[currentFloorIndex.value] = JSON.parse(currentJson);
|
if (currentJson) {
|
||||||
|
floorScenes.value[prevFloorIndex] = JSON.parse(currentJson);
|
||||||
|
}
|
||||||
|
|
||||||
// 加载新楼层
|
// 加载新楼层
|
||||||
currentFloorIndex.value = newFloorIndex;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -450,7 +540,7 @@ const handleFloorChange = async (value: any) => {
|
|||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<div v-if="editable" class="toolbar-container">
|
<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>
|
</div>
|
||||||
|
|
||||||
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user