feat: add components drawer

This commit is contained in:
SikongJueluo 2025-07-01 21:08:58 +08:00
parent 262c5e4003
commit f1e2dbd9d8
No known key found for this signature in database
4 changed files with 635 additions and 5 deletions

3
components.d.ts vendored
View File

@ -19,7 +19,8 @@ declare module 'vue' {
Dialog: typeof import('./src/components/Dialog.vue')['default']
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
LabCanvas: typeof import('./src/components/LabCanvas.vue')['default']
LabCanvas: typeof import('./src/components/LabCanvas/LabCanvas.vue')['default']
LabComponentsDrawer: typeof import('./src/components/LabCanvas/LabComponentsDrawer.vue')['default']
LoginCard: typeof import('./src/components/LoginCard.vue')['default']
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="w-full h-full relative">
<v-stage
class="h-full w-full"
ref="stageRef"
@ -68,10 +68,15 @@
/>
</v-layer>
</v-stage>
<LabComponentsDrawer
class="absolute top-10 right-20"
v-model:open="isDrawerOpen"
></LabComponentsDrawer>
</div>
</template>
<script setup lang="ts">
import LabComponentsDrawer from "./LabComponentsDrawer.vue";
import Konva from "konva";
import { isNull, isUndefined } from "lodash";
import type {
@ -186,7 +191,8 @@ onMounted(() => {
});
const layerRef = useTemplateRef<VLayer>("layerRef");
const canvasObjectsRef = useTemplateRef<HTMLTemplateElement[]>("canvasObjectsRef");
const canvasObjectsRef =
useTemplateRef<HTMLTemplateElement[]>("canvasObjectsRef");
const transformerRef = useTemplateRef<VTransformer>("transformerRef");
const selectRectRef = useTemplateRef<VNode>("selectRectRef");
const stageRef = useTemplateRef<VStage>("StageRef");
@ -205,6 +211,7 @@ const selectionRectangle = reactive({
});
const stageScale = ref(1);
const isDrawerOpen = ref(false);
onMounted(() => {
if (isNull(transformerRef.value)) return;
@ -261,7 +268,6 @@ function handleDragEnd() {
dragItemId.value = null;
}
// Mouse event handlers
function handleStageClick(e: { target: any; evt: MouseEvent }) {
if (isNull(e.target)) return;
@ -516,3 +522,7 @@ function handleCanvasObjectMouseOut(evt: Event) {
objectConfig.isHoverring = false;
}
</script>
<style>
@import "../../assets/main.css";
</style>

View File

@ -0,0 +1,618 @@
<template>
<div>
<!-- 元器件选择菜单 (Drawer) -->
<div class="drawer drawer-end z-50">
<input
id="Lab-drawer"
type="checkbox"
class="drawer-toggle"
v-model="showComponentsMenu"
/>
<div class="drawer-content">
<!-- Page content here -->
<label
for="Lab-drawer"
class="drawer-button btn btn-primary rounded-2xl"
><Plus></Plus
></label>
</div>
<div class="drawer-side">
<label
for="Lab-drawer"
aria-label="close sidebar"
class="drawer-overlay !bg-opacity-50"
></label>
<div
class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col"
>
<!-- 菜单头部 -->
<div
class="p-6 border-b border-base-300 flex justify-between items-center"
>
<h3 class="text-xl font-bold text-primary flex items-center gap-2">
<Plus class="text-primary w-5 h-5"></Plus>
添加实验元器件
</h3>
<label
for="Lab-drawer"
class="btn btn-ghost btn-sm btn-circle"
@click="closeMenu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
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>
</label>
</div>
<!-- 导航栏 -->
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
<a
class="tab"
:class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'"
>元器件</a
>
<a
class="tab"
:class="{ 'tab-active': activeTab === 'templates' }"
@click="activeTab = 'templates'"
>模板</a
>
<a
class="tab"
:class="{ 'tab-active': activeTab === 'virtual' }"
@click="activeTab = 'virtual'"
>虚拟外设</a
>
</div>
<!-- 搜索框 -->
<div class="px-6 py-4 border-b border-base-300">
<div class="join w-full">
<div class="join-item flex-1 relative">
<input
type="text"
placeholder="搜索..."
class="input input-bordered input-sm w-full pl-10"
v-model="searchQuery"
@keyup.enter="searchComponents"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<button class="btn btn-sm join-item" @click="searchComponents">
搜索
</button>
</div>
</div>
<!-- 元器件列表 (组件选项卡) -->
<div
v-if="activeTab === 'components'"
class="px-6 py-4 overflow-auto flex-1"
>
<div
v-if="filteredComponents.length > 0"
class="grid grid-cols-2 gap-4"
>
<div
v-for="(component, index) in filteredComponents"
:key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(component)"
>
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
>
<!-- 直接使用组件作为预览 -->
<component
v-if="componentModules[component.type]"
:is="componentModules[component.type].default"
class="component-preview"
:size="getPreviewSize(component.type)"
/>
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ component.name }}</h3>
<p class="text-xs opacity-70">{{ component.type }}</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="mx-auto text-base-300 mb-3"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的元器件</p>
<button
class="btn btn-sm btn-ghost mt-3"
@click="searchQuery = ''"
>
清除搜索
</button>
</div>
</div>
<!-- 模板列表 (模板选项卡) -->
<div
v-if="activeTab === 'templates'"
class="px-6 py-4 overflow-auto flex-1"
>
<div
v-if="filteredTemplates.length > 0"
class="grid grid-cols-2 gap-4"
>
<div
v-for="(template, index) in filteredTemplates"
:key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addTemplate(template)"
>
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
>
<img
:src="
template.thumbnailUrl || '/placeholder-template.png'
"
alt="Template thumbnail"
class="max-h-full max-w-full object-contain"
/>
</div>
<h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
<p class="text-xs opacity-70">
{{ template.description || "模板" }}
</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="mx-auto text-base-300 mb-3"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的模板</p>
<button
class="btn btn-sm btn-ghost mt-3"
@click="searchQuery = ''"
>
清除搜索
</button>
</div>
</div>
<!-- 虚拟外设列表 (虚拟外设选项卡) -->
<div
v-if="activeTab === 'virtual'"
class="px-6 py-4 overflow-auto flex-1"
>
<div
v-if="filteredVirtualDevices.length > 0"
class="grid grid-cols-2 gap-4"
>
<div
v-for="(device, index) in filteredVirtualDevices"
:key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(device)"
>
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
>
<!-- 直接使用组件作为预览 -->
<component
v-if="componentModules[device.type]"
:is="componentModules[device.type].default"
class="component-preview"
:size="getPreviewSize(device.type)"
/>
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ device.name }}</h3>
<p class="text-xs opacity-70">{{ device.type }}</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="mx-auto text-base-300 mb-3"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的虚拟外设</p>
<button
class="btn btn-sm btn-ghost mt-3"
@click="searchQuery = ''"
>
清除搜索
</button>
</div>
</div>
<!-- 底部操作区 -->
<div
class="p-4 border-t border-base-300 bg-base-200 flex justify-between"
>
<label
for="Lab-drawer"
class="btn btn-sm btn-ghost"
@click="closeMenu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-1"
>
<path d="M19 12H5M12 19l-7-7 7-7"></path>
</svg>
返回
</label>
<label
for="Lab-drawer"
class="btn btn-sm btn-primary"
@click="closeMenu"
>
完成
</label>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Plus } from "lucide-vue-next";
import { ref, computed, shallowRef, onMounted } from "vue";
import motherboardSvg from "../../components/equipments/svg/motherboard.svg";
import buttonSvg from "../../components/equipments/svg/button.svg";
// /
const isOpen = defineModel<boolean>("open", {
default: false,
});
const showComponentsMenu = computed({
get: () => isOpen.value,
set: (value) => (isOpen.value = value),
});
//
const emit = defineEmits([
"close",
"add-component",
"add-template",
]);
//
const activeTab = ref("components");
// --- ---
const searchQuery = ref("");
// --- ---
const availableComponents = [
{ type: "MechanicalButton", name: "机械按钮" },
{ type: "Switch", name: "开关" },
{ type: "Pin", name: "引脚" },
{ type: "SMT_LED", name: "贴片LED" },
{ type: "SevenSegmentDisplay", name: "数码管" },
{ type: "HDMI", name: "HDMI接口" },
{ type: "DDR", name: "DDR内存" },
{ type: "ETH", name: "以太网接口" },
{ type: "SD", name: "SD卡插槽" },
{ type: "SFP", name: "SFP光纤模块" },
{ type: "SMA", name: "SMA连接器" },
{ type: "MotherBoard", name: "主板" },
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
{ type: "BaseBoard", name: "通用底板" },
];
// --- ---
const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
// --- ---
const availableTemplates = ref([
{
name: "PG2L100H 基础开发板",
id: "PG2L100H_Pango100pro",
description: "包含主板和两个LED的基本设置",
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
thumbnailUrl: motherboardSvg,
},
{
name: "矩阵键盘",
id: "MatrixKey",
description: "包含4x4共16个按键的矩阵键盘",
path: "/EquipmentTemplates/MatrixKey.json",
thumbnailUrl: buttonSvg,
},
]);
//
const componentModules = shallowRef<Record<string, any>>({});
//
async function loadComponentModule(type: string) {
if (!componentModules.value[type]) {
try {
// src/components/equipments/ type
const module = await import(`../../components/equipments/${type}.vue`);
//
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];
}
//
async function preloadComponentModules() {
//
for (const component of availableComponents) {
try {
await loadComponentModule(component.type);
} catch (error) {
console.error(`Failed to preload component ${component.type}:`, error);
}
}
//
for (const device of availableVirtualDevices) {
try {
await loadComponentModule(device.type);
} catch (error) {
console.error(`Failed to preload virtual device ${device.type}:`, error);
}
}
}
//
function getPreviewSize(componentType: string): number {
//
const previewSizes: Record<string, number> = {
MechanicalButton: 0.4, //
Switch: 0.35, //
Pin: 0.8, //
SMT_LED: 0.7, // LED
SevenSegmentDisplay: 0.4, //
HDMI: 0.5, // HDMI
DDR: 0.5, // DDR
ETH: 0.5, //
SD: 0.6, // SD
SFP: 0.4, // SFP
SMA: 0.7, // SMA
MotherBoard: 0.13, //
DDS: 0.3, //
};
// 0.5
return previewSizes[componentType] || 0.5;
}
//
function searchComponents() {
//
// filteredComponents
}
//
function closeMenu() {
showComponentsMenu.value = false;
emit("close");
}
//
async function addComponent(componentTemplate: { type: string; name: string }) {
//
const moduleRef = await loadComponentModule(componentTemplate.type);
let defaultProps: Record<string, any> = {};
// getDefaultProps
if (moduleRef) {
if (typeof moduleRef.getDefaultProps === "function") {
defaultProps = moduleRef.getDefaultProps();
console.log(
`Got default props from ${componentTemplate.type}:`,
defaultProps,
);
} else {
// 退
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
}
} else {
console.log(`Failed to load module for ${componentTemplate.type}`);
}
//
emit("add-component", {
type: componentTemplate.type,
name: componentTemplate.name,
props: defaultProps,
});
//
closeMenu();
}
//
async function addTemplate(template: any) {
try {
// JSON
const response = await fetch(template.path);
if (!response.ok) {
throw new Error(`Failed to load template: ${response.statusText}`);
}
const templateData = await response.json();
console.log("加载模板:", templateData);
//
emit("add-template", {
id: template.id,
name: template.name,
template: templateData,
capsPage: template.capsPage,
});
//
closeMenu();
} catch (error) {
console.error("加载模板出错:", error);
alert("无法加载模板文件,请检查控制台错误信息");
}
}
// ()
const filteredComponents = computed(() => {
if (!searchQuery.value || activeTab.value !== "components") {
return availableComponents;
}
const query = searchQuery.value.toLowerCase();
return availableComponents.filter(
(component) =>
component.name.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query),
);
});
// ()
const filteredTemplates = computed(() => {
if (!searchQuery.value || activeTab.value !== "templates") {
return availableTemplates.value;
}
const query = searchQuery.value.toLowerCase();
return availableTemplates.value.filter(
(template) =>
template.name.toLowerCase().includes(query) ||
(template.description &&
template.description.toLowerCase().includes(query)),
);
});
// ()
const filteredVirtualDevices = computed(() => {
if (!searchQuery.value || activeTab.value !== "virtual") {
return availableVirtualDevices;
}
const query = searchQuery.value.toLowerCase();
return availableVirtualDevices.filter(
(device) =>
device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query),
);
});
//
onMounted(() => {
//
preloadComponentModules();
});
</script>
<style scoped>
/* 组件预览样式 */
.component-preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* 动画效果 */
.animate-slideUp {
animation: slideUp 0.3s ease-out forwards;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -26,8 +26,9 @@
</template>
<script setup lang="ts">
import LabComponentsDrawer from "@/components/LabCanvas/LabComponentsDrawer.vue";
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
import LabCanvas from "@/components/LabCanvas.vue";
import LabCanvas from "@/components/LabCanvas/LabCanvas.vue";
</script>
<style>