refactor: 重构canvas

This commit is contained in:
2025-07-03 21:56:58 +08:00
parent 14d8499f77
commit c3bd61ed51
14 changed files with 186 additions and 4142 deletions

View File

@@ -26,9 +26,11 @@
</template>
<script setup lang="ts">
import LabComponentsDrawer from "@/components/LabCanvas/LabComponentsDrawer.vue";
import { useProvideLabCanvasStore } from "@/components/LabCanvas/composable/LabCanvasManager";
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
import LabCanvas from "@/components/LabCanvas/LabCanvas.vue";
useProvideLabCanvasStore({width:window.innerWidth, height: window.innerHeight});
</script>
<style>

View File

@@ -1,533 +1,5 @@
<template>
<div class="h-screen flex flex-col">
<!-- 头部工具栏 -->
<div class="navbar bg-base-100 shadow-lg">
<div class="navbar-start">
<h1 class="text-xl font-bold">FPGA WebLab - 实验画布</h1>
</div>
<div class="navbar-center">
<div class="join">
<button
class="btn btn-sm join-item"
:class="{ 'btn-active': currentMode === 'design' }"
@click="currentMode = 'design'"
>
设计模式
</button>
<button
class="btn btn-sm join-item"
:class="{ 'btn-active': currentMode === 'simulate' }"
@click="currentMode = 'simulate'"
>
仿真模式
</button>
</div>
</div>
<div class="navbar-end">
<div class="join">
<button class="btn btn-sm join-item" @click="saveProject">
保存项目
</button>
<button class="btn btn-sm join-item" @click="loadProject">
加载项目
</button>
<button class="btn btn-sm join-item" @click="exportProject">
导出
</button>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="flex-1 flex overflow-hidden">
<!-- 左侧属性面板 -->
<div
v-if="showPropertyPanel && selectedComponents.length > 0"
class="w-80 bg-base-100 border-r border-base-300 overflow-y-auto"
>
<div class="p-4">
<h3 class="text-lg font-semibold mb-4">组件属性</h3>
<!-- 单个组件属性 -->
<div v-if="selectedComponents.length === 1" class="space-y-4">
<div>
<label class="label">
<span class="label-text">组件名称</span>
</label>
<input
type="text"
class="input input-bordered input-sm w-full"
v-model="selectedComponents[0].name"
@change="updateComponentName"
/>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="label">
<span class="label-text">X 位置</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
v-model.number="selectedComponents[0].x"
@change="updateComponentPosition"
/>
</div>
<div>
<label class="label">
<span class="label-text">Y 位置</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
v-model.number="selectedComponents[0].y"
@change="updateComponentPosition"
/>
</div>
</div>
<div>
<label class="label">
<span class="label-text">旋转角度</span>
</label>
<input
type="range"
min="0"
max="360"
class="range range-sm"
v-model.number="selectedComponents[0].rotation"
@change="updateComponentRotation"
/>
<div class="text-xs text-center">{{ selectedComponents[0].rotation }}°</div>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">锁定位置</span>
<input
type="checkbox"
class="checkbox checkbox-sm"
v-model="selectedComponents[0].locked"
@change="updateComponentLock"
/>
</label>
</div>
<!-- 组件特有属性 -->
<div v-if="componentSpecificProps.length > 0" class="divider">特有属性</div>
<div v-for="prop in componentSpecificProps" :key="prop.key" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label }}</span>
</label>
<!-- 根据属性类型渲染不同的输入控件 -->
<input
v-if="prop.type === 'text'"
type="text"
class="input input-bordered input-sm"
:value="selectedComponents[0].props[prop.key]"
@input="updateComponentProp(prop.key, ($event.target as HTMLInputElement)?.value)"
/>
<input
v-else-if="prop.type === 'number'"
type="number"
class="input input-bordered input-sm"
:value="selectedComponents[0].props[prop.key]"
@input="updateComponentProp(prop.key, Number(($event.target as HTMLInputElement)?.value))"
/>
<input
v-else-if="prop.type === 'checkbox'"
type="checkbox"
class="checkbox checkbox-sm"
:checked="selectedComponents[0].props[prop.key]"
@change="updateComponentProp(prop.key, ($event.target as HTMLInputElement)?.checked)"
/>
<select
v-else-if="prop.type === 'select'"
class="select select-bordered select-sm"
:value="selectedComponents[0].props[prop.key]"
@change="updateComponentProp(prop.key, ($event.target as HTMLSelectElement)?.value)"
>
<option v-for="option in prop.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
<!-- 多个组件属性 -->
<div v-else class="space-y-4">
<div class="alert alert-info">
<span>已选择 {{ selectedComponents.length }} 个组件</span>
</div>
<button class="btn btn-sm btn-outline w-full" @click="groupSelectedComponents">
组合组件
</button>
<button class="btn btn-sm btn-outline w-full" @click="alignSelectedComponents('left')">
左对齐
</button>
<button class="btn btn-sm btn-outline w-full" @click="alignSelectedComponents('center')">
居中对齐
</button>
<button class="btn btn-sm btn-outline w-full" @click="alignSelectedComponents('right')">
右对齐
</button>
</div>
</div>
</div>
<!-- 中央画布区域 -->
<div class="flex-1 relative">
<LabCanvasNew
v-if="currentMode === 'design'"
@component-selected="handleComponentSelected"
@components-changed="handleComponentsChanged"
/>
<!-- 仿真模式画布 -->
<div v-else class="w-full h-full bg-base-200 flex items-center justify-center">
<div class="text-center">
<h2 class="text-2xl font-bold mb-4">仿真模式</h2>
<p class="text-gray-600 mb-4">在此模式下可以与组件进行交互测试</p>
<button class="btn btn-primary" @click="startSimulation">
开始仿真
</button>
</div>
</div>
</div>
<!-- 右侧图层面板 -->
<div
v-if="showLayerPanel"
class="w-64 bg-base-100 border-l border-base-300 overflow-y-auto"
>
<div class="p-4">
<h3 class="text-lg font-semibold mb-4">图层管理</h3>
<div class="space-y-2">
<div
v-for="component in sortedComponents"
:key="component.id"
class="flex items-center p-2 rounded hover:bg-base-200 cursor-pointer"
:class="{ 'bg-primary text-primary-content': component.isSelected }"
@click="selectComponent(component.id)"
>
<div class="flex-1">
<div class="text-sm font-medium">{{ component.name }}</div>
<div class="text-xs opacity-70">{{ component.type }}</div>
</div>
<div class="flex gap-1">
<button
class="btn btn-xs btn-ghost"
@click.stop="toggleComponentVisibility(component.id)"
:title="component.visible ? '隐藏' : '显示'"
>
{{ component.visible ? '👁️' : '🙈' }}
</button>
<button
class="btn btn-xs btn-ghost"
@click.stop="toggleComponentLock(component.id)"
:title="component.locked ? '解锁' : '锁定'"
>
{{ component.locked ? '🔒' : '🔓' }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="bg-base-100 border-t border-base-300 px-4 py-2 flex justify-between items-center text-sm">
<div class="flex gap-4">
<span>组件总数: {{ totalComponents }}</span>
<span>选中: {{ selectedComponents.length }}</span>
<span>当前模式: {{ currentMode === 'design' ? '设计' : '仿真' }}</span>
</div>
<div class="flex gap-2">
<button
class="btn btn-xs btn-ghost"
@click="showPropertyPanel = !showPropertyPanel"
>
{{ showPropertyPanel ? '隐藏' : '显示' }}属性面板
</button>
<button
class="btn btn-xs btn-ghost"
@click="showLayerPanel = !showLayerPanel"
>
{{ showLayerPanel ? '隐藏' : '显示' }}图层面板
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import LabCanvasNew from '@/components/LabCanvas/LabCanvasNew.vue'
import { useKonvaComponentManager } from '@/components/LabCanvas/composables/useKonvaComponentManager'
import type { KonvaComponent } from '@/components/LabCanvas/types/KonvaComponent'
// 模式状态
const currentMode = ref<'design' | 'simulate'>('design')
// 面板显示状态
const showPropertyPanel = ref(true)
const showLayerPanel = ref(true)
// 组件管理器
const componentManager = useKonvaComponentManager({
enableHistory: true,
autoSave: true,
saveKey: 'fpga-weblab-project'
})
const { components, selectedComponents } = componentManager
// 计算属性
const totalComponents = computed(() => components.value.size)
const sortedComponents = computed(() => {
return Array.from(components.value.values())
.sort((a, b) => b.zIndex - a.zIndex)
})
// 组件特有属性配置
const componentPropertyConfigs: Record<string, Array<{
key: string
label: string
type: 'text' | 'number' | 'checkbox' | 'select'
options?: Array<{ value: any; label: string }>
}>> = {
MechanicalButton: [
{ key: 'color', label: '颜色', type: 'select', options: [
{ value: 'red', label: '红色' },
{ value: 'blue', label: '蓝色' },
{ value: 'green', label: '绿色' }
]},
{ key: 'size', label: '尺寸', type: 'select', options: [
{ value: 'small', label: '小' },
{ value: 'medium', label: '中' },
{ value: 'large', label: '大' }
]},
{ key: 'pressed', label: '按下状态', type: 'checkbox' }
],
Switch: [
{ key: 'state', label: '开关状态', type: 'checkbox' },
{ key: 'style', label: '样式', type: 'select', options: [
{ value: 'toggle', label: '切换' },
{ value: 'rocker', label: '摇杆' }
]}
],
SMT_LED: [
{ key: 'color', label: '颜色', type: 'select', options: [
{ value: 'red', label: '红色' },
{ value: 'green', label: '绿色' },
{ value: 'blue', label: '蓝色' },
{ value: 'yellow', label: '黄色' }
]},
{ key: 'state', label: '点亮状态', type: 'checkbox' },
{ key: 'brightness', label: '亮度', type: 'number' }
]
}
const componentSpecificProps = computed(() => {
if (selectedComponents.value.length !== 1) return []
const component = selectedComponents.value[0]
return componentPropertyConfigs[component.type] || []
})
// 事件处理
function handleComponentSelected(componentIds: string[]) {
// 组件选择已由管理器处理
console.log('Components selected:', componentIds)
}
function handleComponentsChanged() {
// 组件变化处理
console.log('Components changed')
}
// 属性更新方法
function updateComponentName() {
if (selectedComponents.value.length === 1) {
const component = selectedComponents.value[0]
// 名称已通过v-model自动更新
}
}
function updateComponentPosition() {
if (selectedComponents.value.length === 1) {
const component = selectedComponents.value[0]
componentManager.updateComponentPosition(component.id, component.x, component.y)
}
}
function updateComponentRotation() {
if (selectedComponents.value.length === 1) {
const component = selectedComponents.value[0]
// 旋转角度已通过v-model自动更新
}
}
function updateComponentLock() {
if (selectedComponents.value.length === 1) {
const component = selectedComponents.value[0]
// 锁定状态已通过v-model自动更新
}
}
function updateComponentProp(propKey: string, value: any) {
if (selectedComponents.value.length === 1) {
const component = selectedComponents.value[0]
componentManager.updateComponentProps(component.id, { [propKey]: value })
}
}
// 组件操作
function selectComponent(componentId: string) {
componentManager.selectComponent(componentId)
}
function toggleComponentVisibility(componentId: string) {
const component = components.value.get(componentId)
if (component) {
component.visible = !component.visible
}
}
function toggleComponentLock(componentId: string) {
const component = components.value.get(componentId)
if (component) {
component.locked = !component.locked
}
}
// 多选组件操作
function groupSelectedComponents() {
if (selectedComponents.value.length < 2) return
// TODO: 实现组合功能
console.log('Grouping components:', selectedComponents.value.map(c => c.id))
}
function alignSelectedComponents(alignment: 'left' | 'center' | 'right') {
if (selectedComponents.value.length < 2) return
const components = selectedComponents.value
let targetX: number
switch (alignment) {
case 'left':
targetX = Math.min(...components.map(c => c.x))
break
case 'center':
const minX = Math.min(...components.map(c => c.x))
const maxX = Math.max(...components.map(c => c.x + (c.boundingBox?.width || 100)))
targetX = (minX + maxX) / 2
break
case 'right':
targetX = Math.max(...components.map(c => c.x + (c.boundingBox?.width || 100)))
break
}
components.forEach(component => {
if (alignment === 'right') {
componentManager.updateComponentPosition(
component.id,
targetX - (component.boundingBox?.width || 100),
component.y
)
} else if (alignment === 'center') {
componentManager.updateComponentPosition(
component.id,
targetX - (component.boundingBox?.width || 100) / 2,
component.y
)
} else {
componentManager.updateComponentPosition(component.id, targetX, component.y)
}
})
}
// 项目管理
function saveProject() {
const projectData = componentManager.getProjectData()
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `fpga-project-${new Date().toISOString().slice(0, 10)}.json`
a.click()
URL.revokeObjectURL(url)
}
function loadProject() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const projectData = JSON.parse(e.target?.result as string)
componentManager.loadProjectData(projectData)
} catch (error) {
console.error('Failed to load project:', error)
alert('项目文件格式错误')
}
}
reader.readAsText(file)
}
input.click()
}
function exportProject() {
// TODO: 实现导出功能如导出为图片、PDF等
console.log('Exporting project...')
}
function startSimulation() {
// TODO: 实现仿真功能
console.log('Starting simulation...')
}
// 生命周期
onMounted(() => {
console.log('FPGA WebLab Example loaded')
})
</script>
<style scoped>
/* 自定义样式 */
.navbar {
min-height: 4rem;
}
.range {
background: linear-gradient(to right, #ddd 0%, #ddd var(--value), #ccc var(--value), #ccc 100%);
}
</style>
</script>