feat: remake most of forntend

This commit is contained in:
alivender
2025-04-26 19:59:35 +08:00
parent 4e741f9ef8
commit bc4f44ecaa
41 changed files with 84095 additions and 672 deletions

View File

@@ -1,26 +1,79 @@
<template>
<div class="bg-base-200 min-h-screen">
<template> <div class="bg-base-200 min-h-screen">
<main class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col lg:flex-row-reverse">
<img src="https://placehold.co/600x400" class="max-w-sm rounded-lg shadow-2xl" />
<div>
<h1 class="text-5xl font-bold">Welcome to FPGA Web Lab!</h1>
<p class="py-6">
Prototype and simulate electronic circuits in your browser.
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
<!-- 图片容器 -->
<div class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
<img src="https://placehold.co/600x400" class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
<!-- 这里使用relative定位限制覆盖层只在图片容器内 -->
<div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
</div>
<!-- 内容容器 -->
<div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
<h1 class="text-4xl md:text-5xl font-bold mb-3 relative group">
<span class="relative z-10 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Welcome to
</span>
<span class="text-base-content">FPGA Web Lab!</span>
<span class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"></span>
</h1>
<p class="py-6 text-lg opacity-80 leading-relaxed">
Prototype and simulate electronic circuits in your browser with our modern, intuitive interface. Create, test, and share your FPGA designs seamlessly.
</p>
<button class="btn btn-primary">Get Started</button>
<div class="flex flex-wrap gap-4 actions-container">
<router-link to="/project" class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
进入工程界面
</router-link>
<router-link to="/login" class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
登录
</router-link>
<router-link to="/user" class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
用户中心
</router-link>
<router-link to="/test" class="btn btn-info text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
测试功能
</router-link>
<router-link to="/test/jtag" class="btn btn-warning text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
JTAG测试
</router-link>
</div>
<div class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md">
<p class="text-sm">
<span class="font-semibold text-primary">提示</span> 您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计
</p>
</div>
</div>
</div>
</main>
<div class="fixed bottom-10 right-10 btn btn-circle">
<ThemeControlButton class=""></ThemeControlButton>
</div>
</div>
</template>
<script lang="ts" setup>
import ThemeControlButton from "@/components/ThemeControlButton.vue";
import "@/router";
</script>

View File

@@ -1,9 +1,12 @@
<template>
<div class="h-screen w-screen flex justify-center">
<div class="h-full w-32"></div>
<div class="h-screen flex flex-col overflow-hidden">
<!-- 主要内容 -->
<div class="flex-1 flex justify-center">
<div class="h-full w-32"></div>
<div class="h-full w-[70%] shadow-2xl flex">
<button class="btn btn-primary h-10 w-30">获取ID Code</button>
<div class="h-full w-[70%] shadow-2xl flex items-start p-4">
<button class="btn btn-primary h-10 w-30">获取ID Code</button>
</div>
</div>
</div>
</template>

View File

@@ -1,7 +1,9 @@
<template>
<main>
<div class="flex items-center justify-center min-h-screen">
<LoginCard />
<div class="relative w-full max-w-md">
<LoginCard />
</div>
</div>
</main>
</template>

View File

@@ -1,127 +1,258 @@
<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">
<div class="h-screen flex flex-col overflow-hidden">
<div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 -->
<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 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"
/>
<!-- 添加元器件按钮 -->
<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>
<!-- 元器件选择菜单 -->
<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
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);
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>
</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";
import { ref, reactive } from 'vue';
// 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;
type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton'
name: string;
x: number;
y: number;
props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性
}
const components = ref<ComponentItem[]>([]);
const selectedComponent = ref<string | null>(null);
const selectedComponentData = ref<ComponentItem | null>(null);
const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId
const selectedComponentData = computed(() => { // 改为计算属性
return components.value.find(c => c.id === selectedComponentId.value) || 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: '蜂鸣器' }
];
// 存储动态导入的组件模块
interface ComponentModule {
default: any;
config?: {
props?: Array<{
name: string;
type: string;
label?: string;
default: any;
}>;
};
}
// 打开元器件选择菜单
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;
}
// 添加元器件
function addComponent(componentTemplate) {
const newComponent = {
// 处理 ComponentSelector 组件添加元器件事件
async function handleAddComponent(componentData: { type: string; name: string; props: Record<string, any> }) {
// 加载组件模块以便后续使用
await loadComponentModule(componentData.type);
const newComponent: ComponentItem = {
id: `component-${Date.now()}`,
type: componentTemplate.type,
name: componentTemplate.name,
x: 100,
y: 100
type: componentData.type,
name: componentData.name,
x: 100, // 或者计算画布中心位置
y: 100,
props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
};
components.value.push(newComponent);
// 由于我们使用的是响应式数据绑定,不需要再次调用 diagramCanvas 的 addComponent
// DiagramCanvas 组件通过 :initialComponents="components" 已经接收到更新
showComponentsMenu.value = false;
}
// 处理组件选中事件
function handleComponentSelected(component) {
selectedComponent.value = component ? component.id : null;
selectedComponentData.value = component;
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) {
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;
@@ -129,9 +260,95 @@ function handleComponentMoved(moveData) {
}
}
// 获取组件名称
function getComponentName(componentId) {
const component = components.value.find(c => c.id === componentId);
return component ? component.name : '';
// 处理组件删除事件
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);
}
}
// --- 生命周期钩子 ---
onMounted(() => {
// 无需在这里预加载组件ComponentSelector 组件会处理这部分逻辑
});
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>

View File

@@ -1,5 +1,5 @@
<template>
<div class="w-screen h-screen">
<div class="h-screen overflow-hidden">
<Switch width="1400" height="360" />
<MechanicalButton width="1400" height="360" />
<PopButton></PopButton>

View File

@@ -9,7 +9,6 @@
<script setup lang="ts">
import UploadCard from "@/components/UploadCard.vue";
import Sidebar from "../components/Sidebar.vue";
</script>
<style scoped>