394 lines
12 KiB
Vue
394 lines
12 KiB
Vue
<template>
|
||
<div class="h-screen flex flex-col overflow-hidden">
|
||
<div class="flex flex-1 overflow-hidden relative">
|
||
<SplitterGroup
|
||
id="splitter-group-v"
|
||
direction="vertical"
|
||
class="w-full h-full"
|
||
@layout="handleVerticalSplitterResize"
|
||
>
|
||
<!-- 使用 v-show 替代 v-if -->
|
||
<SplitterPanel
|
||
v-show="!isBottomBarFullscreen"
|
||
id="splitter-group-v-panel-project"
|
||
:default-size="verticalSplitterSize"
|
||
>
|
||
<SplitterGroup
|
||
id="splitter-group-h"
|
||
direction="horizontal"
|
||
class="w-full h-full"
|
||
@layout="handleHorizontalSplitterResize"
|
||
>
|
||
<!-- 左侧图形化区域 -->
|
||
<SplitterPanel
|
||
id="splitter-group-h-panel-canvas"
|
||
:default-size="horizontalSplitterSize"
|
||
:min-size="30"
|
||
class="relative bg-base-200 overflow-hidden h-full"
|
||
>
|
||
<DiagramCanvas
|
||
ref="diagramCanvas"
|
||
:showDocPanel="showDocPanel"
|
||
:exam-id="(route.query.examId as string) || ''"
|
||
@open-components="openComponentsMenu"
|
||
@toggle-doc-panel="toggleDocPanel"
|
||
/>
|
||
</SplitterPanel>
|
||
<!-- 拖拽分割线 -->
|
||
<SplitterResizeHandle
|
||
id="splitter-group-h-resize-handle"
|
||
class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors"
|
||
/>
|
||
<!-- 右侧编辑区域 -->
|
||
<SplitterPanel
|
||
id="splitter-group-h-panel-properties"
|
||
:min-size="20"
|
||
class="bg-base-200 h-full overflow-hidden flex flex-col"
|
||
>
|
||
<div class="overflow-y-auto flex-1">
|
||
<!-- 使用条件渲染显示不同的面板 -->
|
||
<PropertyPanel
|
||
v-show="!showDocPanel"
|
||
:componentData="componentManager.selectedComponentData.value"
|
||
:componentConfig="
|
||
componentManager.selectedComponentConfig.value
|
||
"
|
||
@updateProp="updateComponentProp"
|
||
@updateDirectProp="updateComponentDirectProp"
|
||
/>
|
||
<div
|
||
v-show="showDocPanel"
|
||
class="doc-panel overflow-y-auto h-full"
|
||
>
|
||
<MarkdownRenderer
|
||
:content="documentContent"
|
||
:examId="(route.query.examId as string) || ''"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</SplitterPanel>
|
||
</SplitterGroup>
|
||
</SplitterPanel>
|
||
|
||
<!-- 分割线也使用 v-show -->
|
||
<SplitterResizeHandle
|
||
v-show="!isBottomBarFullscreen"
|
||
id="splitter-group-v-resize-handle"
|
||
class="h-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors"
|
||
/>
|
||
|
||
<!-- 功能底栏 -->
|
||
<SplitterPanel
|
||
id="splitter-group-v-panel-bar"
|
||
:default-size="isBottomBarFullscreen ? 100 : (100 - verticalSplitterSize)"
|
||
:min-size="isBottomBarFullscreen ? 100 : 15"
|
||
class="w-full overflow-hidden pt-3"
|
||
>
|
||
<BottomBar
|
||
:isFullscreen="isBottomBarFullscreen"
|
||
@toggle-fullscreen="handleToggleBottomBarFullscreen"
|
||
/>
|
||
</SplitterPanel>
|
||
</SplitterGroup>
|
||
</div>
|
||
<!-- 元器件选择组件 -->
|
||
<ComponentSelector
|
||
:open="showComponentsMenu"
|
||
@update:open="showComponentsMenu = $event"
|
||
@close="showComponentsMenu = false"
|
||
/>
|
||
|
||
<!-- 实验板申请对话框 -->
|
||
<RequestBoardDialog
|
||
:open="showRequestBoardDialog"
|
||
@close="handleRequestBoardClose"
|
||
@success="handleRequestBoardSuccess"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, watch } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
|
||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
|
||
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
|
||
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
|
||
import PropertyPanel from "@/components/PropertyPanel.vue";
|
||
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
||
import BottomBar from "@/views/Project/BottomBar.vue";
|
||
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
|
||
import { useProvideComponentManager } from "@/components/LabCanvas";
|
||
import { useAlertStore } from "@/components/Alert";
|
||
import { AuthManager } from "@/utils/AuthManager";
|
||
import { useEquipments } from "@/stores/equipments";
|
||
import type { Board } from "@/APIClient";
|
||
|
||
import { useRoute } from "vue-router";
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
// 提供组件管理服务
|
||
const componentManager = useProvideComponentManager();
|
||
|
||
// 设备管理store
|
||
const equipments = useEquipments();
|
||
|
||
const alert = useAlertStore();
|
||
|
||
// --- 使用VueUse保存分栏状态 ---
|
||
// 左右分栏比例(默认60%)
|
||
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
|
||
// 上下分栏比例(默认80%)
|
||
const verticalSplitterSize = useLocalStorage('project-vertical-splitter-size', 80);
|
||
// 底栏全屏状态
|
||
const isBottomBarFullscreen = useLocalStorage('project-bottom-bar-fullscreen', false);
|
||
// 文档面板显示状态
|
||
const showDocPanel = useLocalStorage('project-show-doc-panel', false);
|
||
|
||
function handleToggleBottomBarFullscreen() {
|
||
isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
|
||
}
|
||
|
||
// --- 处理分栏大小变化 ---
|
||
function handleHorizontalSplitterResize(sizes: number[]) {
|
||
if (sizes && sizes.length > 0) {
|
||
horizontalSplitterSize.value = sizes[0];
|
||
}
|
||
}
|
||
|
||
function handleVerticalSplitterResize(sizes: number[]) {
|
||
if (sizes && sizes.length > 0) {
|
||
// 只在非全屏状态下保存分栏大小,避免全屏时的100%被保存
|
||
if (!isBottomBarFullscreen.value) {
|
||
verticalSplitterSize.value = sizes[0];
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 实验板申请对话框 ---
|
||
const showRequestBoardDialog = ref(false);
|
||
|
||
// --- 文档面板控制 ---
|
||
const documentContent = ref("");
|
||
|
||
// 切换文档面板和属性面板
|
||
async function toggleDocPanel() {
|
||
showDocPanel.value = !showDocPanel.value;
|
||
|
||
// 如果切换到文档面板,则获取文档内容
|
||
if (showDocPanel.value) {
|
||
await loadDocumentContent();
|
||
}
|
||
}
|
||
|
||
// 加载文档内容
|
||
async function loadDocumentContent() {
|
||
try {
|
||
// 检查是否有实验ID参数
|
||
const examId = route.query.examId as string;
|
||
if (examId) {
|
||
// 如果有实验ID,从API加载实验文档
|
||
console.log('加载实验文档:', examId);
|
||
const client = AuthManager.createAuthenticatedExamClient();
|
||
|
||
// 获取markdown类型的资源列表
|
||
const resources = await client.getExamResourceList(examId, 'doc');
|
||
|
||
if (resources && resources.length > 0) {
|
||
// 获取第一个markdown资源
|
||
const markdownResource = resources[0];
|
||
|
||
// 使用动态API获取资源文件内容
|
||
const response = await client.getExamResourceById(markdownResource.id);
|
||
|
||
if (!response || !response.data) {
|
||
throw new Error('获取markdown文件失败');
|
||
}
|
||
|
||
const content = await response.data.text();
|
||
|
||
// 更新文档内容,暂时不处理图片路径,由MarkdownRenderer处理
|
||
documentContent.value = content;
|
||
} else {
|
||
documentContent.value = "# 暂无实验文档\n\n该实验尚未提供文档内容。";
|
||
}
|
||
} else {
|
||
documentContent.value = "# 无文档";
|
||
}
|
||
} catch (error) {
|
||
console.error("加载文档失败:", error);
|
||
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
|
||
}
|
||
}
|
||
|
||
// --- UI 状态管理 ---
|
||
const showComponentsMenu = ref(false);
|
||
const diagramCanvas = ref(null);
|
||
|
||
function openComponentsMenu() {
|
||
showComponentsMenu.value = true;
|
||
}
|
||
|
||
// 更新组件属性的方法 - 委托给componentManager
|
||
function updateComponentProp(
|
||
componentId: string,
|
||
propName: string,
|
||
value: any,
|
||
) {
|
||
componentManager.updateComponentProp(componentId, propName, value);
|
||
}
|
||
|
||
// 更新组件的直接属性 - 委托给componentManager
|
||
function updateComponentDirectProp(
|
||
componentId: string,
|
||
propName: string,
|
||
value: any,
|
||
) {
|
||
componentManager.updateComponentDirectProp(componentId, propName, value);
|
||
}
|
||
|
||
// --- 实验板管理 ---
|
||
// 检查并初始化用户实验板
|
||
async function checkAndInitializeBoard() {
|
||
try {
|
||
const client = AuthManager.createAuthenticatedDataClient();
|
||
const userInfo = await client.getUserInfo();
|
||
|
||
if (userInfo.boardID && userInfo.boardID.trim() !== '') {
|
||
// 用户已绑定实验板,获取实验板信息并更新到equipment
|
||
try {
|
||
const board = await client.getBoardByID(userInfo.boardID);
|
||
updateEquipmentFromBoard(board);
|
||
alert?.show(`实验板 ${board.boardName} 已连接`, "success");
|
||
} catch (boardError) {
|
||
console.error('获取实验板信息失败:', boardError);
|
||
alert?.show("获取实验板信息失败", "error");
|
||
showRequestBoardDialog.value = true;
|
||
}
|
||
} else {
|
||
// 用户未绑定实验板,显示申请对话框
|
||
showRequestBoardDialog.value = true;
|
||
}
|
||
} catch (error) {
|
||
console.error('检查用户实验板失败:', error);
|
||
alert?.show("检查用户信息失败", "error");
|
||
showRequestBoardDialog.value = true;
|
||
}
|
||
}
|
||
|
||
// 根据实验板信息更新equipment store
|
||
function updateEquipmentFromBoard(board: Board) {
|
||
equipments.setAddr(board.ipAddr);
|
||
equipments.setPort(board.port);
|
||
|
||
console.log(`实验板信息已更新到equipment store:`, {
|
||
address: board.ipAddr,
|
||
port: board.port,
|
||
boardName: board.boardName,
|
||
boardId: board.id
|
||
});
|
||
}
|
||
|
||
// 处理申请实验板对话框关闭
|
||
function handleRequestBoardClose() {
|
||
showRequestBoardDialog.value = false;
|
||
// 如果用户取消申请,可以选择返回上一页或显示警告
|
||
router.push('/');
|
||
}
|
||
|
||
// 处理申请实验板成功
|
||
function handleRequestBoardSuccess(board: Board) {
|
||
showRequestBoardDialog.value = false;
|
||
updateEquipmentFromBoard(board);
|
||
alert?.show(`实验板 ${board.boardName} 申请成功!`, "success");
|
||
}
|
||
|
||
// --- 生命周期钩子 ---
|
||
onMounted(async () => {
|
||
// 验证用户身份
|
||
try {
|
||
const isAuthenticated = await AuthManager.isAuthenticated();
|
||
if (!isAuthenticated) {
|
||
// 验证失败,跳转到登录页面
|
||
router.push('/login');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
console.error('身份验证失败:', error);
|
||
router.push('/login');
|
||
return;
|
||
}
|
||
|
||
// 检查并初始化用户实验板
|
||
await checkAndInitializeBoard();
|
||
|
||
// 检查是否有例程参数或实验ID参数,如果有则自动打开文档面板
|
||
if (route.query.tutorial || route.query.examId) {
|
||
showDocPanel.value = true;
|
||
await loadDocumentContent();
|
||
}
|
||
|
||
// 设置画布引用并初始化组件管理器
|
||
componentManager.setCanvasRef(diagramCanvas.value);
|
||
await componentManager.initialize();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="postcss">
|
||
/* 样式保持不变 */
|
||
@import "@/assets/main.css";
|
||
|
||
.animate-slideRight {
|
||
animation: slideRight 0.3s ease-out forwards;
|
||
}
|
||
|
||
@keyframes slideRight {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(30px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
|
||
/* 确保滚动行为仅在需要时出现 */
|
||
html,
|
||
body {
|
||
overflow: hidden;
|
||
height: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
/* 文档面板样式 */
|
||
.doc-panel {
|
||
padding: 1.5rem;
|
||
max-width: 100%;
|
||
margin: 0;
|
||
background-color: transparent;
|
||
/* 使用透明背景 */
|
||
border: none;
|
||
/* 确保没有边框 */
|
||
}
|
||
|
||
/* 文档切换按钮样式 */
|
||
.doc-toggle-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
z-index: 50;
|
||
}
|
||
|
||
/* Markdown渲染样式调整 */
|
||
:deep(.markdown-content) {
|
||
padding: 1rem;
|
||
background-color: hsl(var(--b1));
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
</style>
|