338 lines
10 KiB
Vue
338 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { message, Modal } from 'ant-design-vue';
|
|
import { computed, onMounted, provide, ref, type ShallowRef, shallowRef, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import { getSceneById, importBinTaskExcel, 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';
|
|
import { editorStore } from '../stores/editor.store';
|
|
|
|
const EDITOR_KEY = Symbol('editor-key');
|
|
|
|
type Props = {
|
|
id: string;
|
|
};
|
|
const props = defineProps<Props>();
|
|
|
|
const { t } = useI18n();
|
|
|
|
//#region 接口
|
|
const readScene = async () => {
|
|
const res = await getSceneById(props.id);
|
|
title.value = res?.label ?? '';
|
|
editor.value?.load(res?.json, editable.value);
|
|
};
|
|
|
|
const saveScene = async () => {
|
|
const json = editor.value?.save();
|
|
if (!json) return Promise.reject('无法获取场景数据');
|
|
const res = await saveSceneById(props.id, json);
|
|
if (!res) return Promise.reject('保存失败');
|
|
|
|
// 保存成功后重置历史记录状态,表示当前场景已保存
|
|
if (editor.value?.store) {
|
|
editor.value.store.historyIndex = undefined;
|
|
editor.value.store.histories = [];
|
|
}
|
|
|
|
message.success(t('场景保存成功'));
|
|
return Promise.resolve();
|
|
};
|
|
|
|
const pushScene = async () => {
|
|
const res = await pushSceneById(props.id);
|
|
if (!res) return Promise.reject();
|
|
message.success(t('场景推送成功'));
|
|
return Promise.resolve();
|
|
};
|
|
|
|
//#endregion
|
|
|
|
const title = ref<string>('');
|
|
|
|
// 监听标题变化,动态更新页面标题
|
|
onMounted(() => {
|
|
document.title = '场景编辑器';
|
|
});
|
|
|
|
watch(
|
|
() => props.id,
|
|
async () => {
|
|
await readScene();
|
|
// 在场景加载完成后自动保存和恢复视图状态
|
|
await handleAutoSaveAndRestoreViewState();
|
|
},
|
|
{ immediate: true, flush: 'post' },
|
|
);
|
|
|
|
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 存储到 store 中
|
|
editorStore.setEditor(editor as ShallowRef<EditorService>);
|
|
|
|
// 监听自动生成库位对话框事件
|
|
editor.value?.on('autoCreateStorageDialog', (data: any) => {
|
|
autoCreateStorageData.value = data;
|
|
autoCreateStorageVisible.value = true;
|
|
});
|
|
});
|
|
|
|
const editable = ref<boolean>(false);
|
|
watch(editable, (v) => editor.value?.setState(v));
|
|
|
|
const toPush = () => {
|
|
Modal.confirm({
|
|
class: 'confirm',
|
|
title: t('是否保存并推送该场景文件?'),
|
|
content: t('推送前会先保存当前场景的所有修改。'),
|
|
centered: true,
|
|
cancelText: t('取消'),
|
|
okText: t('保存并推送'),
|
|
onOk: async () => {
|
|
try {
|
|
await saveScene();
|
|
await pushScene();
|
|
} catch (error) {
|
|
console.error('保存或推送失败:', error);
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const importScene = async () => {
|
|
const file = await selectFile('.scene');
|
|
if (!file || !file.size) return;
|
|
const json = await decodeTextFile(file);
|
|
editor.value?.load(json, editable.value, undefined, true); // 第四个参数isImport=true
|
|
};
|
|
const exportScene = () => {
|
|
const json = editor.value?.save();
|
|
if (!json) return;
|
|
const blob = textToBlob(json);
|
|
if (!blob?.size) return;
|
|
const url = URL.createObjectURL(blob);
|
|
downloadFile(url, `${title.value || 'unknown'}.scene`);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
// 导入Bintask Excel文件
|
|
const importBinTask = async () => {
|
|
try {
|
|
const file = await selectFile('.xlsx,.xls');
|
|
if (!file || !file.size) return;
|
|
|
|
const success = await importBinTaskExcel(props.id, file);
|
|
if (success) {
|
|
message.success('Bintask导入成功');
|
|
} else {
|
|
message.error('Bintask导入失败');
|
|
}
|
|
} catch (error: any) {
|
|
message.error(error.message || 'Bintask导入失败');
|
|
}
|
|
};
|
|
|
|
const show = ref<boolean>(true);
|
|
const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
|
|
watch(
|
|
() => editor.value?.selected.value[0],
|
|
(v) => {
|
|
const pen = editor.value?.getPenById(v);
|
|
if (pen?.id) {
|
|
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
|
|
return;
|
|
}
|
|
if (current.value?.type === 'robot') return;
|
|
current.value = undefined;
|
|
},
|
|
);
|
|
const isRobot = computed(() => current.value?.type === 'robot');
|
|
const isPoint = computed(() => current.value?.type === 'point');
|
|
const isRoute = computed(() => current.value?.type === 'line');
|
|
const isArea = computed(() => current.value?.type === 'area');
|
|
|
|
const selectRobot = (id: string) => {
|
|
current.value = { type: 'robot', id };
|
|
editor.value?.inactive();
|
|
};
|
|
|
|
// 视图状态管理
|
|
const { saveViewState, autoSaveAndRestoreViewState, isSaving } = useViewState();
|
|
|
|
// 保存当前视图状态
|
|
const handleSaveViewState = async () => {
|
|
if (!editor.value) return;
|
|
|
|
try {
|
|
await saveViewState(editor.value, props.id);
|
|
message.success('视图状态保存成功');
|
|
} catch {
|
|
message.error('视图状态保存失败');
|
|
}
|
|
};
|
|
|
|
// 自动保存和恢复视图状态
|
|
const handleAutoSaveAndRestoreViewState = async () => {
|
|
if (!editor.value) return;
|
|
|
|
await autoSaveAndRestoreViewState(editor.value, props.id);
|
|
};
|
|
// 返回到父级 iframe 的场景卡片
|
|
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>
|
|
<a-layout class="full">
|
|
<a-layout-header class="p-16" style="height: 64px">
|
|
<a-flex justify="space-between" align="center">
|
|
<a-typography-text class="title">{{ title }} --场景编辑</a-typography-text>
|
|
<a-space align="center">
|
|
<a-button @click="backToCards"> 返回 </a-button>
|
|
<a-button type="primary" :loading="isSaving" @click="handleSaveViewState"> 保存比例 </a-button>
|
|
<a-button v-if="editable" class="warning" @click="editable = false">
|
|
<i class="icon exit size-18 mr-8" />
|
|
<span>{{ $t('退出编辑器') }}</span>
|
|
</a-button>
|
|
<a-button v-else type="primary" @click="editable = true">
|
|
<i class="icon edit size-18 mr-8" />
|
|
<span>{{ $t('启用编辑器') }}</span>
|
|
</a-button>
|
|
<a-button @click="toPush">{{ $t('推送') }}</a-button>
|
|
<a-button @click="importScene">{{ $t('导入') }}</a-button>
|
|
<a-button @click="importBinTask">导入Bintask</a-button>
|
|
<a-button @click="exportScene">{{ $t('导出') }}</a-button>
|
|
</a-space>
|
|
</a-flex>
|
|
</a-layout-header>
|
|
|
|
<a-layout class="p-16">
|
|
<a-layout-sider :width="320">
|
|
<a-tabs type="card">
|
|
<a-tab-pane key="1" :tab="$t('机器人')">
|
|
<RobotGroups
|
|
v-if="editor"
|
|
:token="EDITOR_KEY"
|
|
:sid="id"
|
|
:editable="editable"
|
|
:current="current?.id"
|
|
@change="selectRobot"
|
|
show-group-edit
|
|
/>
|
|
</a-tab-pane>
|
|
<a-tab-pane key="2" :tab="$t('库位')">
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-storage />
|
|
</a-tab-pane>
|
|
<a-tab-pane key="3" :tab="$t('高级组')">
|
|
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
</a-layout-sider>
|
|
<a-layout-content>
|
|
<div ref="container" class="editor-container full"></div>
|
|
</a-layout-content>
|
|
</a-layout>
|
|
</a-layout>
|
|
|
|
<div v-if="editable" class="toolbar-container">
|
|
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
|
</div>
|
|
|
|
<!-- 批量编辑工具栏 - 只在编辑模式下显示 -->
|
|
<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>
|
|
</a-float-button>
|
|
<div v-if="show" class="card-container">
|
|
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
|
|
|
|
<template v-if="isPoint">
|
|
<PointEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
|
|
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
|
|
</template>
|
|
<template v-if="isRoute">
|
|
<RouteEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
|
|
<RouteDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
|
|
</template>
|
|
<template v-if="isArea">
|
|
<AreaEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
|
|
<AreaDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.editor-container {
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
.toolbar-container {
|
|
position: fixed;
|
|
bottom: 40px;
|
|
left: 50%;
|
|
z-index: 100;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.card-container {
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 64px;
|
|
z-index: 100;
|
|
width: 320px;
|
|
height: calc(100% - 96px);
|
|
overflow: visible;
|
|
overflow-y: auto;
|
|
pointer-events: none;
|
|
|
|
& > * {
|
|
pointer-events: all;
|
|
}
|
|
}
|
|
</style>
|