FPGA_WebLab/src/views/ProjectView.vue

408 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="h-screen flex flex-col overflow-hidden">
<div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas
ref="diagramCanvas" :components="components"
:componentModules="componentModules" @component-selected="handleComponentSelected"
@component-moved="handleComponentMoved"
@update-component-prop="updateComponentProp"
@component-delete="handleComponentDelete"
@wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted"
/>
<!-- 添加元器件按钮 -->
<button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
<!-- SVG icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<!-- 拖拽分割线 -->
<div
class="resizer cursor-col-resize bg-base-300 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize"
></div>
<!-- 右侧编辑区域 -->
<div class="bg-base-100 flex flex-col p-4 overflow-auto" :style="{ width: (100 - leftPanelWidth) + '%' }">
<h3 class="text-lg font-bold mb-4">属性编辑器</h3>
<div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div>
<div v-else>
<div class="mb-4 pb-4 border-b border-base-300">
<h4 class="font-semibold text-lg mb-1">{{ selectedComponentData.name }}</h4>
<p class="text-xs text-gray-500">ID: {{ selectedComponentData.id }}</p>
<p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p>
</div>
<!-- 动态属性表单 -->
<div v-if="selectedComponentConfig && selectedComponentConfig.props" class="space-y-4">
<div v-for="prop in selectedComponentConfig.props" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/>
<input
v-else-if="prop.type === 'number'"
type="number"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/> <!-- 可以为 boolean 添加 checkbox为 color 添加 color picker 等 -->
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="selectedComponentData.props?.[prop.name]"
@change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ prop.label || prop.name }}</span>
</div> <!-- 下拉选择框 -->
<select
v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@change="(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
console.log('选择的值:', value, '类型:', typeof value);
if (selectedComponentData) {updateComponentProp(selectedComponentData.id, prop.name, value);}
}"
>
<option v-for="option in prop.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
</div>
</div>
<div v-else-if="selectedComponentData && !selectedComponentConfig" class="text-gray-500 text-sm">
正在加载组件配置...
</div>
<div v-else-if="selectedComponentData && selectedComponentConfig && (!selectedComponentConfig.props || selectedComponentConfig.props.length === 0)" class="text-gray-500 text-sm">
此组件没有可配置的属性。
</div>
</div>
</div> </div>
<!-- 元器件选择组件 -->
<ComponentSelector
:open="showComponentsMenu"
@update:open="showComponentsMenu = $event"
@add-component="handleAddComponent"
@close="showComponentsMenu = false"
/>
</div>
</template>
<script setup lang="ts">
// 引入wokwi-elements和组件
// import "@wokwi/elements"; // 不再需要全局引入 wokwi
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // 引入 defineAsyncComponent 和 shallowRef
import DiagramCanvas from '@/components/DiagramCanvas.vue';
import ComponentSelector from '@/components/ComponentSelector.vue';
import { getComponentConfig } from '@/components/equipments/componentConfig';
import type { ComponentConfig } from '@/components/equipments/componentConfig';
// --- 元器件管理 ---
const showComponentsMenu = ref(false);
interface ComponentItem {
id: string;
type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton'
name: string;
x: number;
y: number;
props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性
}
const components = ref<ComponentItem[]>([]);
const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId
const selectedComponentData = computed(() => { // 改为计算属性
return components.value.find(c => c.id === selectedComponentId.value) || null;
});
const diagramCanvas = ref(null);
// 存储动态导入的组件模块
interface ComponentModule {
default: any;
config?: {
props?: Array<{
name: string;
type: string;
label?: string;
default: any;
options?: Array<{ value: any; label: string }>; // 添加 options 字段用于 select 类型
}>;
};
}
const componentModules = shallowRef<Record<string, ComponentModule>>({});
const selectedComponentConfig = shallowRef<ComponentModule['config'] | null>(null); // 存储选中组件的配置
// 动态加载组件定义
async function loadComponentModule(type: string) {
if (!componentModules.value[type]) {
try {
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
const module = await import(`../components/equipments/${type}.vue`);
// 使用 markRaw 包装模块,避免不必要的响应式处理
componentModules.value = {
...componentModules.value,
[type]: module
};
console.log(`Loaded module for ${type}:`, module);
} catch (error) {
console.error(`Failed to load component module ${type}:`, error);
return null;
}
}
return componentModules.value[type];
}
// --- 分割面板 ---
const leftPanelWidth = ref(60);
const isResizing = ref(false);
// 分割面板拖拽相关函数
function startResize(e: MouseEvent) {
isResizing.value = true;
document.addEventListener('mousemove', onResize);
document.addEventListener('mouseup', stopResize);
e.preventDefault(); // 防止文本选择
}
function onResize(e: MouseEvent) {
if (!isResizing.value) return;
// 获取容器宽度和鼠标位置
const container = document.querySelector('.flex-1.overflow-hidden') as HTMLElement;
if (!container) return;
const containerWidth = container.clientWidth;
const mouseX = e.clientX;
// 计算左侧面板应占的百分比
let newWidth = (mouseX / containerWidth) * 100;
// 限制最小宽度和最大宽度
newWidth = Math.max(20, Math.min(newWidth, 80));
// 更新宽度
leftPanelWidth.value = newWidth;
}
function stopResize() {
isResizing.value = false;
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', stopResize);
}
// --- 元器件操作 ---
function openComponentsMenu() {
showComponentsMenu.value = true;
}
// 处理 ComponentSelector 组件添加元器件事件
async function handleAddComponent(componentData: { type: string; name: string; props: Record<string, any> }) {
// 加载组件模块以便后续使用
await loadComponentModule(componentData.type);
// 获取画布容器和位置信息
const canvasInstance = diagramCanvas.value as any;
// 默认位置(当无法获取画布信息时使用)
let posX = 100;
let posY = 100;
try {
if (canvasInstance) {
// 获取画布容器
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
// 获取当前画布的位置和缩放信息
const canvasPosition = canvasInstance.getCanvasPosition ?
canvasInstance.getCanvasPosition() :
{ x: 0, y: 0 };
const scale = canvasInstance.getScale ?
canvasInstance.getScale() :
1;
// 计算可视区域中心点在画布坐标系中的位置
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
// 计算画布中心点的坐标
posX = (viewportWidth / 2 - canvasPosition.x) / scale;
posY = (viewportHeight / 2 - canvasPosition.y) / scale;
}
}
} catch (error) {
console.error('Error getting canvas position:', error);
// 使用默认位置
}
// 添加一些随机偏移,避免元器件重叠
const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50;
const newComponent: ComponentItem = {
id: `component-${Date.now()}`,
type: componentData.type,
name: componentData.name,
x: Math.round(posX + offsetX),
y: Math.round(posY + offsetY),
props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
};
components.value.push(newComponent);
}
// 处理组件选中事件
async function handleComponentSelected(componentData: ComponentItem | null) {
selectedComponentId.value = componentData ? componentData.id : null;
selectedComponentConfig.value = null; // 重置配置
if (componentData) {
// 从配置文件中获取组件配置
const config = getComponentConfig(componentData.type);
if (config) {
selectedComponentConfig.value = config;
console.log(`Config for ${componentData.type}:`, config);
} else {
console.warn(`No config found for component type ${componentData.type}`);
}
// 同时加载组件模块以备用
await loadComponentModule(componentData.type);
}
}
// 处理组件移动事件
function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
const component = components.value.find(c => c.id === moveData.id);
if (component) {
component.x = moveData.x;
component.y = moveData.y;
}
}
// 处理组件删除事件
function handleComponentDelete(componentId: string) {
// 查找要删除的组件索引
const index = components.value.findIndex(c => c.id === componentId);
if (index !== -1) {
// 从数组中移除该组件
components.value.splice(index, 1);
// 如果删除的是当前选中的组件,清除选中状态
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
selectedComponentConfig.value = null;
}
}
}
// 更新组件属性的方法,处理字符串类型的初始值特殊格式
function updateComponentProp(componentId: string | { id: string; propName: string; value: any }, propName?: string, value?: any) {
// 处理来自 DiagramCanvas 的事件
if (typeof componentId === 'object') {
const { id, propName: name, value: val } = componentId;
componentId = id;
propName = name;
value = val;
}
const component = components.value.find(c => c.id === componentId);
if (component && propName !== undefined) {
if (!component.props) {
component.props = {};
}
// 检查值是否为对象如果是对象并有value属性则使用该属性值
if (value !== null && typeof value === 'object' && 'value' in value) {
value = value.value;
}
// 直接更新属性值
component.props[propName] = value;
console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value);
}
}
// 处理连线创建事件
function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData);
// 连线已在DiagramCanvas.vue中完成约束处理
}
// 处理连线删除事件
function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId);
// 可以在这里添加连线删除的相关逻辑
}
// --- 生命周期钩子 ---
onMounted(() => {
// 初始化画布设置
console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
});
onUnmounted(() => {
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', stopResize);
});
</script>
<style scoped lang="postcss">
/* 样式保持不变 */
@import "../assets/main.css";
/* 分割线样式 */
.resizer {
width: 6px;
height: 100%;
background-color: var(--b3);
cursor: col-resize;
transition: background-color 0.3s;
z-index: 10;
}
.resizer:hover, .resizer:active {
width: 6px;
}
/* 调整大小时应用全局样式 */
:global(body.resizing) {
cursor: col-resize;
user-select: none;
}
.animate-slideRight {
animation: slideRight 0.3s ease-out forwards;
}
@keyframes slideRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>