feat: enhance DiagramCanvas and Pin components with wire creation and tooltip functionality

- Added wire creation logic in DiagramCanvas.vue with mouse tracking and event handling.
- Implemented tooltip display for pins in Pin.vue with detailed information on hover.
- Updated ProjectView.vue to handle wire creation and deletion events.
- Refactored Wire.vue to support dynamic path rendering based on routing mode.
This commit is contained in:
alivender 2025-04-26 21:53:33 +08:00
parent b6839af5d2
commit b3a5342d6b
4 changed files with 896 additions and 33 deletions

View File

@ -2,11 +2,46 @@
<div class="flex-1 min-w-[60%] bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer" <div class="flex-1 min-w-[60%] bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer"
@mousedown="handleCanvasMouseDown" @mousedown="handleCanvasMouseDown"
@mousedown.middle.prevent="startMiddleDrag" @mousedown.middle.prevent="startMiddleDrag"
@wheel.prevent="onZoom"> @wheel.prevent="onZoom"
<div @contextmenu.prevent="handleContextMenu"><div
ref="canvas" ref="canvas"
class="diagram-canvas" class="diagram-canvas"
:style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }"> <!-- 渲染画布上的组件 --> :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }"> <!-- 渲染连线 -->
<svg class="wires-layer" width="4000" height="4000">
<!-- 已完成的连线 -->
<Wire
v-for="wire in wires"
:key="wire.id"
:id="wire.id"
:start-x="wire.startX"
:start-y="wire.startY"
:end-x="wire.endX"
:end-y="wire.endY"
:stroke-color="wire.color || '#4a5568'"
:stroke-width="2"
:is-active="wire.isActive"
:start-component-id="wire.startComponentId"
:start-pin-label="wire.startPinLabel"
:end-component-id="wire.endComponentId"
:end-pin-label="wire.endPinLabel"
@click="handleWireClick(wire)"
/>
<!-- 正在创建的连线 -->
<Wire
v-if="isCreatingWire"
id="temp-wire"
:start-x="creatingWireStart.x"
:start-y="creatingWireStart.y"
:end-x="mousePosition.x"
:end-y="mousePosition.y"
stroke-color="#3182ce"
:stroke-width="2"
:is-active="true"
/>
</svg>
<!-- 渲染画布上的组件 -->
<div v-for="component in props.components" :key="component.id" <div v-for="component in props.components" :key="component.id"
class="component-wrapper" class="component-wrapper"
:class="{ :class="{
@ -20,13 +55,13 @@
}" }"
@mousedown.left.stop="startComponentDrag($event, component)" @mousedown.left.stop="startComponentDrag($event, component)"
@mouseover="hoveredComponent = component.id" @mouseover="hoveredComponent = component.id"
@mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <component
<component
:is="getComponentDefinition(component.type)" :is="getComponentDefinition(component.type)"
v-if="props.componentModules[component.type]" v-if="props.componentModules[component.type]"
v-bind="prepareComponentProps(component.props || {})" v-bind="prepareComponentProps(component.props || {})"
@update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)" @update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
:ref="el => { if (el) componentRefs[component.id] = el; }" @pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
:ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
/> />
<!-- Fallback if component module not loaded yet --> <!-- Fallback if component module not loaded yet -->
@ -45,6 +80,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'; import { ref, reactive, onMounted, onUnmounted } from 'vue';
import Wire from '@/components/Wire.vue';
// //
interface ComponentItem { interface ComponentItem {
@ -62,7 +98,7 @@ const props = defineProps<{
}>(); }>();
// //
const emit = defineEmits(['component-selected', 'component-moved', 'update-component-prop', 'component-delete']); const emit = defineEmits(['component-selected', 'component-moved', 'update-component-prop', 'component-delete', 'wire-created', 'wire-deleted']);
// --- --- // --- ---
const canvasContainer = ref<HTMLElement | null>(null); const canvasContainer = ref<HTMLElement | null>(null);
@ -292,8 +328,23 @@ function onComponentDrag(e: MouseEvent) {
// //
function stopComponentDrag() { function stopComponentDrag() {
// 线
if (draggingComponentId.value) {
console.log(`组件拖拽结束: ${draggingComponentId.value},开始更新连线位置`);
// ID
const currentId = draggingComponentId.value;
//
draggingComponentId.value = null; draggingComponentId.value = null;
// DOM线
setTimeout(() => {
updateWiresForComponent(currentId);
}, 50);
} else {
draggingComponentId.value = null;
}
document.removeEventListener('mousemove', onComponentDrag); document.removeEventListener('mousemove', onComponentDrag);
document.removeEventListener('mouseup', stopComponentDrag); document.removeEventListener('mouseup', stopComponentDrag);
} }
@ -314,21 +365,462 @@ function getComponentRef(componentId: string) {
// //
defineExpose({ defineExpose({
getComponentRef getComponentRef,
getCanvasPosition: () => ({ x: position.x, y: position.y }),
getScale: () => scale.value
}); });
// --- 线 ---
interface WireItem {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
startComponentId: string;
startPinLabel: string;
endComponentId?: string;
endPinLabel?: string;
color?: string;
isActive?: boolean;
constraint?: string;
}
const wires = ref<WireItem[]>([]);
const isCreatingWire = ref(false);
const creatingWireStart = reactive({ x: 0, y: 0 });
const creatingWireStartInfo = reactive({
componentId: '',
pinLabel: '',
constraint: ''
});
const mousePosition = reactive({ x: 0, y: 0 });
//
function updateMousePosition(e: MouseEvent) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
}
// Pin
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
if (!canvasContainer.value) return;
//
const containerRect = canvasContainer.value.getBoundingClientRect();
// (线)
updateMousePosition(event);
// pinInfo
if (!pinInfo || !pinInfo.label) {
console.error('无效的针脚信息:', pinInfo);
return;
}
//
if (!pinInfo.position) {
console.error('针脚信息中缺少位置数据:', pinInfo);
return;
}
const pinPagePosition = pinInfo.position;
console.log(`针脚 ${pinInfo.label} 的页面坐标:`, pinPagePosition);
//
const pinCanvasX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
const pinCanvasY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
console.log(`针脚 ${pinInfo.label} 的画布坐标:`, { x: pinCanvasX, y: pinCanvasY });
if (!isCreatingWire.value) {
// 线
isCreatingWire.value = true;
// 使线
creatingWireStart.x = pinCanvasX;
creatingWireStart.y = pinCanvasY;
creatingWireStartInfo.componentId = componentId;
creatingWireStartInfo.pinLabel = pinInfo.label;
creatingWireStartInfo.constraint = pinInfo.constraint;
console.log(`开始创建连线,起点针脚: ${componentId}/${pinInfo.label}, 位置: (${pinCanvasX}, ${pinCanvasY})`);
//
document.addEventListener('mousemove', onCreatingWireMouseMove);
} else {
// 线
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
// Pin线
cancelWireCreation();
return;
}
//
const startConstraint = creatingWireStartInfo.constraint;
const endConstraint = pinInfo.constraint;
if (startConstraint && endConstraint && startConstraint !== endConstraint) {
// Pin
promptForConstraintSelection(
componentId,
pinInfo,
startConstraint,
endConstraint
);
} else {
//
let finalConstraint = '';
if (startConstraint) {
finalConstraint = startConstraint;
} else if (endConstraint) {
finalConstraint = endConstraint;
} else {
// Pin
finalConstraint = generateRandomConstraint();
}
// 线
completeWireCreation(
componentId,
pinInfo.label,
finalConstraint,
pinInfo
);
// Pin
updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
updatePinConstraint(componentId, pinInfo.label, finalConstraint);
}
}
}
//
function generateRandomConstraint() {
const randomId = Math.floor(Math.random() * 1000000);
return `$auto_constraint_${randomId}`;
}
//
function promptForConstraintSelection(
endComponentId: string,
endPinInfo: any,
startConstraint: string,
endConstraint: string
) {
// 使ModalUI
// 使confirm
const useStartConstraint = confirm(
`连接两个不同约束的Pin:\n` +
`- 起点约束: ${startConstraint}\n` +
`- 终点约束: ${endConstraint}\n\n` +
`点击"确定"使用起点约束,点击"取消"使用终点约束。`
);
const finalConstraint = useStartConstraint ? startConstraint : endConstraint;
// 线
completeWireCreation(
endComponentId,
endPinInfo.label,
finalConstraint,
endPinInfo
);
// Pin
updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
updatePinConstraint(endComponentId, endPinInfo.label, finalConstraint);
}
// Pin
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
// ID
const component = props.components.find(c => c.id === componentId);
if (!component) return;
//
const componentRef = componentRefs.value[componentId];
if (!componentRef) return;
//
if (component.props && componentRef.getInfo) {
const pinInfo = componentRef.getInfo();
if (pinInfo && pinInfo.label === pinLabel) {
emit('update-component-prop', {
id: componentId,
propName: 'constraint',
value: constraint
});
}
}
}
// 线
function completeWireCreation(endComponentId: string, endPinLabel: string, constraint: string, endPinInfo?: any) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
//
let endX = mousePosition.x;
let endY = mousePosition.y;
console.log(`开始创建连线,起点:(${creatingWireStart.x}, ${creatingWireStart.y})`);
// 使 getPinPosition
const endComponentRef = componentRefs.value[endComponentId];
if (endComponentRef && endComponentRef.getPinPosition) {
const pinPosition = endComponentRef.getPinPosition(endPinLabel);
if (pinPosition) {
endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`通过 getPinPosition 获取终点针脚 ${endPinLabel} 的画布坐标: (${endX}, ${endY})`);
} else {
console.warn(`getPinPosition 返回 null将使用备选方法`);
}
} else if (endPinInfo && endPinInfo.position) {
// getPinPosition 使
const pinPagePosition = endPinInfo.position;
endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
endY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
console.log(`通过 pinInfo.position 获取终点针脚位置: (${endX}, ${endY})`);
} else {
console.warn(`无法获取针脚 ${endPinLabel} 的精确位置,使用鼠标位置代替`);
}
//
const distanceSquared = Math.pow(endX - creatingWireStart.x, 2) + Math.pow(endY - creatingWireStart.y, 2);
if (distanceSquared < 1) { // 1
console.warn(`起点和终点太接近 (${distanceSquared}像素²),调整终点位置`);
//
endX += 10 + Math.random() * 5;
endY += 10 + Math.random() * 5;
}
// 线
const newWire: WireItem = {
id: `wire-${Date.now()}`,
startX: creatingWireStart.x,
startY: creatingWireStart.y,
endX: endX,
endY: endY,
startComponentId: creatingWireStartInfo.componentId,
startPinLabel: creatingWireStartInfo.pinLabel,
endComponentId: endComponentId,
endPinLabel: endPinLabel,
color: '#4a5568',
constraint: constraint
};
console.log(`新连线创建完成:`, newWire);
//
if (Math.abs(newWire.startX - newWire.endX) < 1 && Math.abs(newWire.startY - newWire.endY) < 1) {
console.warn(`连线的起点和终点重合,调整终点位置`);
newWire.endX += 20;
newWire.endY += 20;
}
wires.value.push(newWire);
// 线
emit('wire-created', newWire);
//
isCreatingWire.value = false;
document.removeEventListener('mousemove', onCreatingWireMouseMove);
}
// 线
function cancelWireCreation() {
isCreatingWire.value = false;
document.removeEventListener('mousemove', onCreatingWireMouseMove);
}
// 线
function onCreatingWireMouseMove(e: MouseEvent) {
updateMousePosition(e);
}
// 线
function handleWireClick(wire: WireItem) {
// 线
const deleteWire = confirm('是否删除此连线?');
if (deleteWire) {
// 线
const index = wires.value.findIndex(w => w.id === wire.id);
if (index !== -1) {
const deletedWire = wires.value.splice(index, 1)[0];
// 线
emit('wire-deleted', deletedWire.id);
}
}
}
// 线
function updateAllWires() {
if (!canvasContainer.value) return;
// 线
wires.value.forEach(wire => {
updateWireWithPinPositions(wire);
});
}
// 线
function updateWireWithPinPositions(wire: WireItem) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
console.log(`更新连线 ${wire.id},当前位置: 起点(${wire.startX}, ${wire.startY}), 终点(${wire.endX}, ${wire.endY})`);
//
const originalStartX = wire.startX;
const originalStartY = wire.startY;
const originalEndX = wire.endX;
const originalEndY = wire.endY;
//
if (wire.startComponentId && wire.startPinLabel) {
const startComponentRef = componentRefs.value[wire.startComponentId];
if (startComponentRef && startComponentRef.getPinPosition) {
const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
if (pinPosition) {
const newStartX = (pinPosition.x - containerRect.left - position.x) / scale.value;
const newStartY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`更新连线起点 ${wire.startComponentId}/${wire.startPinLabel}: (${wire.startX}, ${wire.startY}) => (${newStartX}, ${newStartY})`);
wire.startX = newStartX;
wire.startY = newStartY;
} else {
console.warn(`无法获取针脚 ${wire.startComponentId}/${wire.startPinLabel} 的位置`);
}
} else {
console.warn(`组件 ${wire.startComponentId} 没有实现 getPinPosition 方法`);
}
}
//
if (wire.endComponentId && wire.endPinLabel) {
const endComponentRef = componentRefs.value[wire.endComponentId];
if (endComponentRef && endComponentRef.getPinPosition) {
const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
if (pinPosition) {
const newEndX = (pinPosition.x - containerRect.left - position.x) / scale.value;
const newEndY = (pinPosition.y - containerRect.top - position.y) / scale.value;
console.log(`更新连线终点 ${wire.endComponentId}/${wire.endPinLabel}: (${wire.endX}, ${wire.endY}) => (${newEndX}, ${newEndY})`);
wire.endX = newEndX;
wire.endY = newEndY;
} else {
console.warn(`无法获取针脚 ${wire.endComponentId}/${wire.endPinLabel} 的位置`);
}
} else {
console.warn(`组件 ${wire.endComponentId} 没有实现 getPinPosition 方法`);
}
}
//
const positionChanged =
originalStartX !== wire.startX ||
originalStartY !== wire.startY ||
originalEndX !== wire.endX ||
originalEndY !== wire.endY;
if (positionChanged) {
//
ensureUniquePinPositions(wire);
}
}
// 线
function updateWiresForComponent(componentId: string) {
if (!canvasContainer.value || !componentId) return;
console.log(`更新组件 ${componentId} 相关的连线位置`);
//
const component = props.components.find(c => c.id === componentId);
if (!component) {
console.warn(`找不到组件 ${componentId}`);
return;
}
// 线
const relatedWires = wires.value.filter(wire =>
wire.startComponentId === componentId ||
wire.endComponentId === componentId
);
console.log(`找到 ${relatedWires.length} 条相关连线`);
if (relatedWires.length === 0) {
// 线线
console.log('没有找到直接关联的连线,检查所有连线');
// 线
wires.value.forEach((wire, index) => {
console.log(`连线 ${index}: startComponentId=${wire.startComponentId}, endComponentId=${wire.endComponentId}`);
});
return;
}
// 线
relatedWires.forEach(wire => {
console.log(`更新连线 ${wire.id} (${wire.startComponentId}/${wire.startPinLabel} -> ${wire.endComponentId}/${wire.endPinLabel})`);
updateWireWithPinPositions(wire);
});
}
//
function ensureUniquePinPositions(wire: WireItem) {
if (Math.abs(wire.startX - wire.endX) < 5 && Math.abs(wire.startY - wire.endY) < 5) {
console.warn('检测到连线起点和终点非常接近,添加随机偏移');
// ID
const idSum = (wire.startComponentId?.charCodeAt(0) || 0) +
(wire.endComponentId?.charCodeAt(0) || 0) +
(wire.startPinLabel?.charCodeAt(0) || 0) +
(wire.endPinLabel?.charCodeAt(0) || 0);
// 使ID
const offsetX = 20 * Math.cos(idSum * 0.1);
const offsetY = 20 * Math.sin(idSum * 0.1);
wire.endX += offsetX;
wire.endY += offsetY;
console.log(`应用偏移 (${offsetX.toFixed(2)}, ${offsetY.toFixed(2)}) 到连线终点`);
}
}
// --- --- // --- ---
onMounted(() => { onMounted(() => {
// //
if (canvasContainer.value) { if (canvasContainer.value) {
position.x = canvasContainer.value.clientWidth / 2; //
position.y = canvasContainer.value.clientHeight / 2; position.x = canvasContainer.value.clientWidth / 2 - 2000; //
} position.y = canvasContainer.value.clientHeight / 2 - 2000; //
if (canvasContainer.value) {
canvasContainer.value.addEventListener('wheel', onZoom); canvasContainer.value.addEventListener('wheel', onZoom);
} }
// //
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
// 线
const wireUpdateInterval = setInterval(() => {
if (wires.value.length > 0) {
updateAllWires();
}
}, 1000); // 线
//
onUnmounted(() => {
clearInterval(wireUpdateInterval);
});
}); });
// //
@ -340,6 +832,12 @@ function handleKeyDown(e: KeyboardEvent) {
// //
selectedComponentId.value = null; selectedComponentId.value = null;
} }
// 线ESC
if (isCreatingWire.value && e.key === 'Escape') {
// 线
cancelWireCreation();
}
} }
onUnmounted(() => { onUnmounted(() => {
@ -386,6 +884,22 @@ onUnmounted(() => {
-ms-user-select: none; -ms-user-select: none;
} }
/* 连线层样式 */
.wires-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 50;
}
.wires-layer path {
pointer-events: stroke;
cursor: pointer;
}
/* 元器件容器样式 */ /* 元器件容器样式 */
.component-wrapper { .component-wrapper {
position: absolute; position: absolute;

141
src/components/Wire.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<path
:d="pathData"
fill="none"
:stroke="strokeColor"
:stroke-width="strokeWidth"
stroke-linecap="round"
stroke-linejoin="round"
:class="{ 'wire-active': isActive }"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, defineEmits } from 'vue';
interface Props {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
strokeColor?: string;
strokeWidth?: number;
isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct';
//
startComponentId?: string;
startPinLabel?: string;
endComponentId?: string;
endPinLabel?: string;
}
const props = withDefaults(defineProps<Props>(), {
strokeColor: '#4a5568',
strokeWidth: 2,
isActive: false,
routingMode: 'orthogonal'
});
const emit = defineEmits(['click']);
function handleClick(event: MouseEvent) {
emit('click', { id: props.id, event });
}
const pathData = computed(() => {
//
const dx = Math.abs(props.endX - props.startX);
const dy = Math.abs(props.endY - props.startY);
//
if (dx < 0.5 && dy < 0.5) {
console.warn('连线的起点和终点几乎重合,强制绘制可见路径');
//
const r = 5; // 5
return `M ${props.startX} ${props.startY}
m -${r}, 0
a ${r},${r} 0 1,0 ${r*2},0
a ${r},${r} 0 1,0 -${r*2},0`;
}
if (props.routingMode === 'direct') {
return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
} else if (props.routingMode === 'orthogonal') {
// 线
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
} else {
// 线
if (dx < 10 || dy < 10) {
// 使线
return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
} else {
// 使线
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
}
}
});
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
//
const dx = endX - startX;
const dy = endY - startY;
//
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
console.warn('连线的起点和终点几乎重合,调整路径显示');
//
const offset = 5; // 5
return `M ${startX} ${startY}
L ${startX + offset} ${startY}
L ${startX + offset} ${startY + offset}
L ${startX} ${startY + offset}
L ${startX} ${startY}`;
}
// 线线
if (dx === 0 || dy === 0) {
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// 45线
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx === absDy) {
// 45线
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// - 使L
//
if (absDx > absDy) {
const middleX = startX + dx * 0.5;
return `M ${startX} ${startY}
L ${middleX} ${startY}
L ${middleX} ${endY}
L ${endX} ${endY}`;
} else {
//
const middleY = startY + dy * 0.5;
return `M ${startX} ${startY}
L ${startX} ${middleY}
L ${endX} ${middleY}
L ${endX} ${endY}`;
}
}
</script>
<style scoped>
.wire-active {
stroke-dasharray: 5;
animation: dash 0.5s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: 10;
}
}
</style>

View File

@ -5,15 +5,18 @@
:height="height" :height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight" :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component" class="pin-component"
:data-pin-id="props.label"
> >
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g v-if="props.appearance === 'None'">
<g v-if="props.appearance === 'None'"> <g transform="translate(-12.5, -12.5)" class="interactive"> <circle
<g transform="translate(-12.5, -12.5)" class="interactive">
<circle
style="fill:#909090" style="fill:#909090"
cx="12.5" cx="12.5"
cy="12.5" cy="12.5"
r="3.75" /> r="3.75"
@mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick"
:data-pin-id="`${props.label}-${props.constraint}`" />
</g> </g>
</g> </g>
<g v-else-if="props.appearance === 'Dip'"> <g v-else-if="props.appearance === 'Dip'">
@ -25,31 +28,52 @@
height="25" height="25"
x="0" x="0"
y="0" y="0"
rx="2.5" /> rx="2.5" /> <circle
<circle
style="fill:#ecececc5;fill-opacity:0.772973" style="fill:#ecececc5;fill-opacity:0.772973"
cx="12.5" cx="12.5"
cy="12.5" cy="12.5"
r="3.75" /> r="3.75"
@mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick"
:data-pin-id="`${props.label}-${props.constraint}`" />
<text <text
style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973" style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
x="7.3" x="7.3"
y="7" y="7"
xml:space="preserve">{{ props.label }}</text> xml:space="preserve">{{ props.label }}</text>
</g> </g>
</g> </g> <g v-else-if="props.appearance === 'SMT'">
<g v-else-if="props.appearance === 'SMT'">
<rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" /> <rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" />
<rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" /> <rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" />
<rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" /> <rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" />
<text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text> <text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
</g> <!-- SMT样式的针脚 --> <circle
</g> fill="#ecececc5"
cx="10"
cy="0"
r="3.75"
class="interactive"
@mouseenter="showPinTooltip"
@mouseleave="hidePinTooltip"
@click.stop="handlePinClick"
:data-pin-id="`${props.label}-${props.constraint}`" />
</g> </g>
</svg> </svg>
<!-- 提示框 - 在SVG外部 -->
<div v-if="showTooltip" class="pin-tooltip" :style="{
position: 'absolute',
top: tooltipPosition.top + 'px',
left: tooltipPosition.left + 'px',
transform: 'translate(-50%, -100%)',
marginTop: '-5px'
}">
{{ tooltipText }}
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, reactive } from 'vue';
interface Props { interface Props {
size?: number; size?: number;
@ -70,11 +94,92 @@ const props = withDefaults(defineProps<Props>(), {
}); });
const emit = defineEmits([ const emit = defineEmits([
'value-change' 'value-change',
'pin-click' // Pin
]); ]);
// //
const analogValue = ref(0); const analogValue = ref(0);
const showTooltip = ref(false);
const tooltipText = ref('');
const tooltipPosition = reactive({
top: 0,
left: 0
});
//
function showPinTooltip(event: MouseEvent) {
showTooltip.value = true;
const target = event.target as SVGElement;
const rect = target.getBoundingClientRect();
//
tooltipPosition.top = rect.top;
tooltipPosition.left = rect.left + rect.width / 2;
//
tooltipText.value = generateTooltipText();
}
//
function hidePinTooltip() {
showTooltip.value = false;
}
//
function generateTooltipText() {
const parts = [];
parts.push(`标签: ${props.label}`);
if (props.constraint) {
parts.push(`约束: ${props.constraint}`);
} else {
parts.push('约束: 未定义');
}
parts.push(`方向: ${getDirectionText()}`);
parts.push(`类型: ${props.type === 'digital' ? '数字' : '模拟'}`);
return parts.join(' | ');
}
//
function getDirectionText() {
switch (props.direction) {
case 'input': return '输入';
case 'output': return '输出';
case 'inout': return '双向';
default: return '未知';
}
}
//
function handlePinClick(event: MouseEvent) {
// SVG
const target = event.target as SVGElement;
const rect = target.getBoundingClientRect();
const pinCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
console.log(`针脚 ${props.label} 被点击,位置:`, pinCenter);
//
emit('pin-click', {
label: props.label,
constraint: props.constraint,
type: props.type,
direction: props.direction,
//
position: {
x: pinCenter.x,
y: pinCenter.y
},
//
originalEvent: event
});
}
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size); const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
const height = computed(() => { const height = computed(() => {
@ -112,7 +217,46 @@ defineExpose({
direction: props.direction, direction: props.direction,
type: props.type, type: props.type,
appearance: props.appearance appearance: props.appearance
}) }), //
getPinPosition: (pinLabel: string) => {
// Pin
if (pinLabel !== props.label) return null;
// 使DOM
// document.querySelector
const pinElement = document.querySelector(`[data-pin-id="${props.label}-${props.constraint}"]`) as SVGElement;
if (pinElement) {
//
const rect = pinElement.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
// 使
// SVG
const svgElement = document.querySelector(`.pin-component[data-pin-id="${props.label}"]`) as SVGElement;
if (!svgElement) {
console.error(`找不到针脚 ${props.label} 的SVG元素`);
return null;
}
const svgRect = svgElement.getBoundingClientRect();
//
let pinX = svgRect.left + svgRect.width / 2;
let pinY = svgRect.top + svgRect.height / 2;
//
// 使
const randomOffset = 0.1;
pinX += Math.random() * randomOffset;
pinY += Math.random() * randomOffset;
return { x: pinX, y: pinY };
}
}); });
</script> </script>
@ -120,6 +264,7 @@ defineExpose({
.pin-component { .pin-component {
display: block; display: block;
user-select: none; user-select: none;
position: relative;
} }
.interactive { .interactive {
cursor: pointer; cursor: pointer;
@ -128,4 +273,15 @@ defineExpose({
.interactive:hover { .interactive:hover {
filter: brightness(1.2); filter: brightness(1.2);
} }
.pin-tooltip {
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style> </style>

View File

@ -4,11 +4,12 @@
<!-- 左侧图形化区域 --> <!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas
ref="diagramCanvas" :components="components" ref="diagramCanvas" :components="components"
:componentModules="componentModules" :componentModules="componentModules" @component-selected="handleComponentSelected"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved" @component-moved="handleComponentMoved"
@update-component-prop="updateComponentProp" @update-component-prop="updateComponentProp"
@component-delete="handleComponentDelete" @component-delete="handleComponentDelete"
@wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted"
/> />
<!-- 添加元器件按钮 --> <!-- 添加元器件按钮 -->
<button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu"> <button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
@ -219,12 +220,50 @@ async function handleAddComponent(componentData: { type: string; name: string; p
// 便使 // 便使
await loadComponentModule(componentData.type); await loadComponentModule(componentData.type);
//
const canvasInstance = diagramCanvas.value as any;
// 使
let posX = 100;
let posY = 100;
try {
if (canvasInstance) {
//
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
//
const canvasPosition = canvasInstance.getCanvasPosition ?
canvasInstance.getCanvasPosition() :
{ x: 0, y: 0 };
const scale = canvasInstance.getScale ?
canvasInstance.getScale() :
1;
//
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
//
posX = (viewportWidth / 2 - canvasPosition.x) / scale;
posY = (viewportHeight / 2 - canvasPosition.y) / scale;
}
}
} catch (error) {
console.error('Error getting canvas position:', error);
// 使
}
//
const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50;
const newComponent: ComponentItem = { const newComponent: ComponentItem = {
id: `component-${Date.now()}`, id: `component-${Date.now()}`,
type: componentData.type, type: componentData.type,
name: componentData.name, name: componentData.name,
x: 100, // x: Math.round(posX + offsetX),
y: 100, y: Math.round(posY + offsetY),
props: componentData.props, // 使 ComponentSelector props: componentData.props, // 使 ComponentSelector
}; };
@ -302,9 +341,22 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
} }
} }
// 线
function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData);
// 线
}
// 线
function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId);
// 线
}
// --- --- // --- ---
onMounted(() => { onMounted(() => {
// ComponentSelector //
console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
}); });
onUnmounted(() => { onUnmounted(() => {