refactor: 重构canvas
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user