feat: add project view
This commit is contained in:
370
src/components/DiagramCanvas.vue
Normal file
370
src/components/DiagramCanvas.vue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="flex-1 min-w-[60%] bg-base-200 relative overflow-auto" ref="canvasContainer"
|
||||
@mousedown="handleCanvasMouseDown"
|
||||
@mousedown.middle.prevent="startMiddleDrag"
|
||||
@mousemove="onDrag"
|
||||
@mouseup="stopDrag"
|
||||
@mouseleave="stopDrag"
|
||||
@wheel="onZoom">
|
||||
<div
|
||||
ref="canvas"
|
||||
class="diagram-canvas"
|
||||
:style="{transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`}">
|
||||
<!-- wokwi-elements FPGA开发板 -->
|
||||
<wokwi-fpga-board class="absolute top-10 left-10" style="width:600px;height:400px;"></wokwi-fpga-board>
|
||||
<!-- 放置其他元器件的区域 -->
|
||||
<div v-for="component in components" :key="component.id"
|
||||
class="absolute cursor-move component-wrapper"
|
||||
:class="{
|
||||
'component-hover': hoveredComponent === component.id,
|
||||
'component-selected': selectedComponent === component.id
|
||||
}"
|
||||
:style="{top: component.y + 'px', left: component.x + 'px', width: 'auto', height: 'auto'}"
|
||||
@mousedown.left.stop="startComponentDrag($event, component)"
|
||||
@mouseover="hoveredComponent = component.id"
|
||||
@mouseleave="hoveredComponent = null">
|
||||
<component :is="component.type"></component>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缩放指示器 -->
|
||||
<div class="absolute bottom-2 right-2 bg-base-100 px-2 py-1 rounded-md opacity-70">
|
||||
{{ Math.round(scale * 100) }}%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 引入wokwi-elements
|
||||
import "@wokwi/elements";
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
|
||||
// 定义组件接受的属性
|
||||
const props = defineProps<{
|
||||
initialComponents?: Array<ComponentItem>,
|
||||
}>();
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits(['component-selected', 'component-moved']);
|
||||
|
||||
// 定义组件接口
|
||||
interface ComponentItem {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// 画布位置和缩放
|
||||
const position = reactive({ x: 0, y: 0 });
|
||||
const scale = ref(1);
|
||||
const isDragging = ref(false);
|
||||
const isMiddleDragging = ref(false); // 是否在使用中键拖拽
|
||||
const dragStart = reactive({ x: 0, y: 0 });
|
||||
const canvas = ref(null);
|
||||
const canvasContainer = ref(null);
|
||||
|
||||
// 元器件管理
|
||||
const components = ref<ComponentItem[]>([]);
|
||||
const draggingComponent = ref<ComponentItem | null>(null);
|
||||
|
||||
// 监听props变化,更新本地组件数组
|
||||
watch(() => props.initialComponents, (newComponents) => {
|
||||
if (newComponents) {
|
||||
// 通过创建深拷贝来断开引用连接
|
||||
components.value = JSON.parse(JSON.stringify(newComponents));
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
const componentDragOffset = reactive({ x: 0, y: 0 });
|
||||
const hoveredComponent = ref(null); // 鼠标悬停的元器件ID
|
||||
const selectedComponent = ref(null); // 当前选中的元器件ID
|
||||
|
||||
// 画布拖拽
|
||||
function startDrag(e) {
|
||||
// 只处理左键拖拽
|
||||
if (e.button !== 0) return;
|
||||
|
||||
// 确保其他拖拽状态被重置
|
||||
isMiddleDragging.value = false;
|
||||
|
||||
isDragging.value = true;
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// 中键拖拽画布(无论点击到哪里)
|
||||
function startMiddleDrag(e) {
|
||||
// 确保是中键
|
||||
if (e.button !== 1) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 确保其他拖拽状态被重置
|
||||
isDragging.value = false;
|
||||
|
||||
// 设置中键拖拽状态
|
||||
isMiddleDragging.value = true;
|
||||
|
||||
// 记录起始位置
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
}
|
||||
|
||||
// 处理画布鼠标按下事件
|
||||
function handleCanvasMouseDown(e) {
|
||||
// 如果不是左键,则不做处理
|
||||
if (e.button !== 0) return;
|
||||
|
||||
// 如果是直接点击画布(而不是元器件),清除选中状态
|
||||
if (e.target === canvasContainer.value || e.target === canvas.value) {
|
||||
selectedComponent.value = null;
|
||||
emit('component-selected', null);
|
||||
}
|
||||
|
||||
// 继续处理拖拽
|
||||
startDrag(e);
|
||||
}
|
||||
|
||||
function onDrag(e) {
|
||||
// 如果左键或中键拖拽正在进行中
|
||||
if (isDragging.value || isMiddleDragging.value) {
|
||||
// 防止拖拽过程中选中文本
|
||||
e.preventDefault();
|
||||
|
||||
position.x = e.clientX - dragStart.x;
|
||||
position.y = e.clientY - dragStart.y;
|
||||
}
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false;
|
||||
isMiddleDragging.value = false;
|
||||
}
|
||||
|
||||
// 画布缩放
|
||||
function onZoom(e) {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||
const newScale = Math.max(0.3, Math.min(3, scale.value + delta));
|
||||
|
||||
// 保持鼠标位置不变的缩放
|
||||
if (canvas.value && canvasContainer.value) {
|
||||
const rect = canvasContainer.value.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// 计算鼠标在画布中的相对位置
|
||||
const mouseXInCanvas = (mouseX - position.x) / scale.value;
|
||||
const mouseYInCanvas = (mouseY - position.y) / scale.value;
|
||||
|
||||
// 调整位置以保持鼠标位置不变
|
||||
position.x = mouseX - mouseXInCanvas * newScale;
|
||||
position.y = mouseY - mouseYInCanvas * newScale;
|
||||
}
|
||||
|
||||
scale.value = newScale;
|
||||
}
|
||||
|
||||
// 元器件拖拽
|
||||
function startComponentDrag(e, component) {
|
||||
// 确保只处理左键拖拽元器件
|
||||
if (e.button !== 0) return;
|
||||
|
||||
e.stopPropagation();
|
||||
draggingComponent.value = component;
|
||||
|
||||
// 设置选中元器件并通知父组件
|
||||
selectedComponent.value = component.id;
|
||||
emit('component-selected', component);
|
||||
|
||||
// 保存起始位置和鼠标位置
|
||||
const initialX = component.x;
|
||||
const initialY = component.y;
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
|
||||
const mouseMoveHandler = (moveEvent) => {
|
||||
if (!draggingComponent.value) return;
|
||||
|
||||
// 计算鼠标移动的距离(在屏幕坐标系中)
|
||||
const dx = moveEvent.clientX - startX;
|
||||
const dy = moveEvent.clientY - startY;
|
||||
|
||||
// 将移动距离转换为画布坐标系中的距离
|
||||
const canvasDx = dx / scale.value;
|
||||
const canvasDy = dy / scale.value;
|
||||
|
||||
// 更新组件位置(相对于初始位置的增量)
|
||||
draggingComponent.value.x = initialX + canvasDx;
|
||||
draggingComponent.value.y = initialY + canvasDy;
|
||||
|
||||
// 通知父组件元器件位置已变化
|
||||
emit('component-moved', {
|
||||
id: draggingComponent.value.id,
|
||||
x: draggingComponent.value.x,
|
||||
y: draggingComponent.value.y
|
||||
});
|
||||
};
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
draggingComponent.value = null;
|
||||
// 保持选中状态,不清除 selectedComponent
|
||||
window.removeEventListener('mousemove', mouseMoveHandler);
|
||||
window.removeEventListener('mouseup', mouseUpHandler);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', mouseMoveHandler);
|
||||
window.addEventListener('mouseup', mouseUpHandler);
|
||||
}
|
||||
|
||||
// 自定义FPGA开发板组件(未实现,只是示例)
|
||||
customElements.define('wokwi-fpga-board', class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
background-color: #2e7d32;
|
||||
border: 8px solid #1b5e20;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.board-label {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.board-components {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 40px;
|
||||
right: 40px;
|
||||
bottom: 40px;
|
||||
background-color: #388e3c;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.pins {
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.pins.top {
|
||||
top: 0;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
}
|
||||
.pins.bottom {
|
||||
bottom: 0;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
}
|
||||
.pin {
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
background-color: silver;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
<div class="board-label">FPGA开发板</div>
|
||||
<div class="board-components"></div>
|
||||
<div class="pins top">
|
||||
${Array(20).fill().map(() => '<div class="pin"></div>').join('')}
|
||||
</div>
|
||||
<div class="pins bottom">
|
||||
${Array(20).fill().map(() => '<div class="pin"></div>').join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// 公开方法,允许外部添加组件
|
||||
function addComponent(component: ComponentItem) {
|
||||
components.value.push(component);
|
||||
}
|
||||
|
||||
// 公开方法,允许外部重置画布
|
||||
function resetCanvas() {
|
||||
position.x = 0;
|
||||
position.y = 0;
|
||||
scale.value = 1;
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
defineExpose({
|
||||
addComponent,
|
||||
resetCanvas
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diagram-canvas {
|
||||
position: relative;
|
||||
width: 4000px;
|
||||
height: 4000px;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
/* 禁用滚动条 */
|
||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* 元器件容器样式 */
|
||||
.component-wrapper {
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
display: inline-block; /* 确保元素宽度基于内容 */
|
||||
max-width: fit-content; /* 强制宽度适应内容 */
|
||||
max-height: fit-content; /* 强制高度适应内容 */
|
||||
overflow: visible; /* 允许内容溢出(用于显示边框) */
|
||||
}
|
||||
|
||||
/* 悬停状态 */
|
||||
.component-hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 3px dashed #3498db;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.component-selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 4px dashed #e74c3c;
|
||||
border-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user