feat: add project view

This commit is contained in:
alivender
2025-04-25 16:11:53 +08:00
parent 4465091db3
commit cfd8769e9b
6 changed files with 589 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ const items = [
{ id: 1, icon: iconMenu, text: "用户界面", page: "/user" },
{ id: 2, icon: iconMenu, text: "ComponentTest", page: "/test" },
{ id: 3, icon: iconMenu, text: "JtagTest", page: "/test/jtag" },
{ id: 4, icon: iconMenu, text: "工程界面", page: "/project" }, // 新增工程界面入口
];
</script>

View 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>

View File

@@ -3,13 +3,15 @@ import LoginView from "../views/LoginView.vue";
import UserView from "../views/UserView.vue";
import TestView from "../views/TestView.vue";
import JtagTest from "../views/JtagTest.vue";
import ProjectView from "../views/ProjectView.vue";
const routes = [
{ path: "/", redirect: "/user" },
{ path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView },
{ path: "/test/jtag", name:"JtagTest", component: JtagTest}
{ path: "/test/jtag", name:"JtagTest", component: JtagTest},
{ path: "/project", name: "Project", component: ProjectView } // 新增工程界面
];
const router = createRouter({

137
src/views/ProjectView.vue Normal file
View File

@@ -0,0 +1,137 @@
<template>
<div class="h-screen w-screen flex flex-col">
<!-- 顶部工具栏 -->
<div class="flex items-center p-2 border-b border-base-300 bg-base-100">
<h2 class="text-xl font-bold mr-auto">FPGA 工程界面</h2>
<button class="btn btn-circle btn-primary" @click="openComponentsMenu">
<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="flex flex-1 overflow-hidden">
<!-- 左侧图形化区域 -->
<DiagramCanvas
ref="diagramCanvas"
:initialComponents="components"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved"
/>
<!-- 右侧编辑区域 -->
<div class="w-[40%] bg-base-100 border-l flex flex-col p-4">
<h3 class="text-lg font-bold mb-4">属性编辑器</h3>
<div v-if="!selectedComponent" class="text-gray-400">选择元器件以编辑属性</div>
<div v-else>
<div class="mb-2">编辑元器件: {{ getComponentName(selectedComponent) }}</div>
<!-- 这里可以添加元器件的属性编辑表单 -->
</div>
</div>
</div>
<!-- 元器件选择菜单 -->
<div v-if="showComponentsMenu" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click.self="showComponentsMenu = false">
<div class="bg-base-100 p-4 rounded-lg shadow-xl max-w-3xl max-h-[80vh] overflow-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">选择元器件</h3>
<button class="btn btn-ghost btn-sm" @click="showComponentsMenu = false">
<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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div v-for="(component, index) in availableComponents" :key="index"
class="border p-4 rounded-lg flex flex-col items-center hover:bg-base-200 cursor-pointer"
@click="addComponent(component)">
<div class="flex-1 flex items-center justify-center p-2">
<component :is="component.type" style="transform: scale(0.5);"></component>
</div>
<div class="mt-2 text-center">{{ component.name }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 引入wokwi-elements和组件
import "@wokwi/elements";
import { ref, reactive } from 'vue';
import DiagramCanvas from '@/components/DiagramCanvas.vue';
// 元器件管理
const showComponentsMenu = ref(false);
interface ComponentItem {
id: string;
type: string;
name: string;
x: number;
y: number;
}
const components = ref<ComponentItem[]>([]);
const selectedComponent = ref<string | null>(null);
const selectedComponentData = ref<ComponentItem | null>(null);
const diagramCanvas = ref(null);
// 可用元器件列表
const availableComponents = [
{ type: 'wokwi-led', name: 'LED灯' },
{ type: 'wokwi-resistor', name: '电阻' },
{ type: 'wokwi-pushbutton', name: '按钮' },
{ type: 'wokwi-7segment', name: '7段数码管' },
{ type: 'wokwi-arduino-uno', name: 'Arduino Uno' },
{ type: 'wokwi-servo', name: '舵机' },
{ type: 'wokwi-lcd1602', name: 'LCD显示屏' },
{ type: 'wokwi-dht22', name: '温湿度传感器' },
{ type: 'wokwi-buzzer', name: '蜂鸣器' }
];
// 打开元器件选择菜单
function openComponentsMenu() {
showComponentsMenu.value = true;
}
// 添加新元器件
function addComponent(componentTemplate) {
const newComponent = {
id: `component-${Date.now()}`,
type: componentTemplate.type,
name: componentTemplate.name,
x: 100,
y: 100
};
components.value.push(newComponent);
// 由于我们使用的是响应式数据绑定,不需要再次调用 diagramCanvas 的 addComponent
// DiagramCanvas 组件通过 :initialComponents="components" 已经接收到更新
showComponentsMenu.value = false;
}
// 处理组件选中事件
function handleComponentSelected(component) {
selectedComponent.value = component ? component.id : null;
selectedComponentData.value = component;
}
// 处理组件移动事件
function handleComponentMoved(moveData) {
const component = components.value.find(c => c.id === moveData.id);
if (component) {
component.x = moveData.x;
component.y = moveData.y;
}
}
// 获取组件名称
function getComponentName(componentId) {
const component = components.value.find(c => c.id === componentId);
return component ? component.name : '';
}
</script>