feat: Pin移动连线也跟着移动
This commit is contained in:
parent
b3a5342d6b
commit
10db7c67bf
|
@ -9,7 +9,7 @@
|
||||||
: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">
|
<svg class="wires-layer" width="4000" height="4000">
|
||||||
<!-- 已完成的连线 -->
|
<!-- 已完成的连线 -->
|
||||||
<Wire
|
<WireComponent
|
||||||
v-for="wire in wires"
|
v-for="wire in wires"
|
||||||
:key="wire.id"
|
:key="wire.id"
|
||||||
:id="wire.id"
|
:id="wire.id"
|
||||||
|
@ -19,16 +19,17 @@
|
||||||
:end-y="wire.endY"
|
:end-y="wire.endY"
|
||||||
:stroke-color="wire.color || '#4a5568'"
|
:stroke-color="wire.color || '#4a5568'"
|
||||||
:stroke-width="2"
|
:stroke-width="2"
|
||||||
:is-active="wire.isActive"
|
:is-active="wireSelectedId === wire.id"
|
||||||
:start-component-id="wire.startComponentId"
|
:start-component-id="wire.startComponentId"
|
||||||
:start-pin-label="wire.startPinLabel"
|
:start-pin-label="wire.startPinLabel"
|
||||||
:end-component-id="wire.endComponentId"
|
:end-component-id="wire.endComponentId"
|
||||||
:end-pin-label="wire.endPinLabel"
|
:end-pin-label="wire.endPinLabel"
|
||||||
|
:constraint="wire.constraint"
|
||||||
@click="handleWireClick(wire)"
|
@click="handleWireClick(wire)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 正在创建的连线 -->
|
<!-- 正在创建的连线 -->
|
||||||
<Wire
|
<WireComponent
|
||||||
v-if="isCreatingWire"
|
v-if="isCreatingWire"
|
||||||
id="temp-wire"
|
id="temp-wire"
|
||||||
:start-x="creatingWireStart.x"
|
:start-x="creatingWireStart.x"
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
@mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <component
|
@mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <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 || {}, component.id)"
|
||||||
@update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
|
@update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
|
||||||
@pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
|
@pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
|
||||||
:ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
|
:ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
|
||||||
|
@ -80,7 +81,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';
|
import WireComponent from '@/components/equipments/Wire.vue';
|
||||||
|
|
||||||
// 定义组件接受的属性
|
// 定义组件接受的属性
|
||||||
interface ComponentItem {
|
interface ComponentItem {
|
||||||
|
@ -112,6 +113,7 @@ const selectedComponentId = ref<string | null>(null);
|
||||||
const hoveredComponent = ref<string | null>(null);
|
const hoveredComponent = ref<string | null>(null);
|
||||||
const draggingComponentId = ref<string | null>(null);
|
const draggingComponentId = ref<string | null>(null);
|
||||||
const componentDragOffset = reactive({ x: 0, y: 0 });
|
const componentDragOffset = reactive({ x: 0, y: 0 });
|
||||||
|
const wireSelectedId = ref<string | null>(null);
|
||||||
|
|
||||||
// 组件引用跟踪
|
// 组件引用跟踪
|
||||||
const componentRefs = ref<Record<string, any>>({});
|
const componentRefs = ref<Record<string, any>>({});
|
||||||
|
@ -170,20 +172,27 @@ const getComponentDefinition = (type: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 准备组件属性,确保类型正确
|
// 准备组件属性,确保类型正确
|
||||||
function prepareComponentProps(props: Record<string, any>): Record<string, any> {
|
function prepareComponentProps(props: Record<string, any>, componentId?: string): Record<string, any> {
|
||||||
const result: Record<string, any> = {};
|
const result: Record<string, any> = { ...props };
|
||||||
for (const key in props) {
|
|
||||||
let value = props[key];
|
// 添加组件ID属性,用于唯一标识针脚
|
||||||
|
if (componentId) {
|
||||||
|
result.componentId = componentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保某些属性的类型正确
|
||||||
|
for (const key in result) {
|
||||||
|
let value = result[key];
|
||||||
// 只要不是 null/undefined 且不是 string,就强制转字符串
|
// 只要不是 null/undefined 且不是 string,就强制转字符串
|
||||||
if (
|
if (
|
||||||
(key === 'style' || key === 'direction' || key === 'type') &&
|
(key === 'style' || key === 'direction' || key === 'type') &&
|
||||||
value != null &&
|
value != null &&
|
||||||
typeof value !== 'string'
|
typeof value !== 'string'
|
||||||
) {
|
) {
|
||||||
value = String(value);
|
result[key] = String(value);
|
||||||
}
|
}
|
||||||
result[key] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +372,17 @@ function getComponentRef(componentId: string) {
|
||||||
return componentRefs.value[component.id] || null;
|
return componentRefs.value[component.id] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取Wire组件引用
|
||||||
|
function getWireRef(wireId: string) {
|
||||||
|
// 在SVG中查找Wire组件
|
||||||
|
const wire = document.querySelector(`[data-wire-id="${wireId}"]`);
|
||||||
|
if (wire && '__vueParentInstance' in wire) {
|
||||||
|
// @ts-ignore - 访问Vue内部属性
|
||||||
|
return wire.__vueParentInstance?.component?.exposed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
// 暴露给父组件的方法
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getComponentRef,
|
getComponentRef,
|
||||||
|
@ -384,6 +404,9 @@ interface WireItem {
|
||||||
color?: string;
|
color?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
constraint?: string;
|
constraint?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
routingMode?: 'auto' | 'orthogonal' | 'direct';
|
||||||
|
showLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wires = ref<WireItem[]>([]);
|
const wires = ref<WireItem[]>([]);
|
||||||
|
@ -449,39 +472,62 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||||
|
|
||||||
// 添加鼠标移动监听
|
// 添加鼠标移动监听
|
||||||
document.addEventListener('mousemove', onCreatingWireMouseMove);
|
document.addEventListener('mousemove', onCreatingWireMouseMove);
|
||||||
|
} else { // 完成连线
|
||||||
} else {
|
|
||||||
// 完成连线
|
|
||||||
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
|
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
|
||||||
// 如果点了同一个Pin,取消连线
|
// 如果点了同一个Pin,取消连线
|
||||||
cancelWireCreation();
|
cancelWireCreation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查约束条件
|
// 获取起点和终点的约束
|
||||||
const startConstraint = creatingWireStartInfo.constraint;
|
const startConstraint = creatingWireStartInfo.constraint || '';
|
||||||
const endConstraint = pinInfo.constraint;
|
const endConstraint = pinInfo.constraint || '';
|
||||||
|
|
||||||
if (startConstraint && endConstraint && startConstraint !== endConstraint) {
|
|
||||||
// 两个Pin都有约束,但约束不同,需要询问用户选择哪个约束
|
|
||||||
promptForConstraintSelection(
|
|
||||||
componentId,
|
|
||||||
pinInfo,
|
|
||||||
startConstraint,
|
|
||||||
endConstraint
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 确定最终约束
|
|
||||||
let finalConstraint = '';
|
let finalConstraint = '';
|
||||||
|
// 如果两端约束完全一致,直接用该约束,不弹窗
|
||||||
if (startConstraint) {
|
if (startConstraint && endConstraint && startConstraint === endConstraint) {
|
||||||
|
finalConstraint = startConstraint;
|
||||||
|
} else {
|
||||||
|
// 确定最终要使用的约束
|
||||||
|
let replacedConstraint: string | null = null;
|
||||||
|
let newConstraint: string | null = null;
|
||||||
|
if (startConstraint && endConstraint) {
|
||||||
|
const isStartSystemConstraint = startConstraint.startsWith('$');
|
||||||
|
const isEndSystemConstraint = endConstraint.startsWith('$');
|
||||||
|
if (!isStartSystemConstraint && isEndSystemConstraint) {
|
||||||
|
finalConstraint = startConstraint;
|
||||||
|
replacedConstraint = endConstraint;
|
||||||
|
newConstraint = startConstraint;
|
||||||
|
} else if (isStartSystemConstraint && !isEndSystemConstraint) {
|
||||||
|
finalConstraint = endConstraint;
|
||||||
|
replacedConstraint = startConstraint;
|
||||||
|
newConstraint = endConstraint;
|
||||||
|
} else if (isStartSystemConstraint && isEndSystemConstraint) {
|
||||||
|
finalConstraint = Math.random() < 0.5 ? startConstraint : endConstraint;
|
||||||
|
replacedConstraint = (finalConstraint === startConstraint) ? endConstraint : startConstraint;
|
||||||
|
newConstraint = finalConstraint;
|
||||||
|
} else {
|
||||||
|
const userChoice = confirm(`针脚约束冲突:${startConstraint} 和 ${endConstraint}。点击"确定"保留${startConstraint},点击"取消"保留${endConstraint}`);
|
||||||
|
finalConstraint = userChoice ? startConstraint : endConstraint;
|
||||||
|
replacedConstraint = userChoice ? endConstraint : startConstraint;
|
||||||
|
newConstraint = finalConstraint;
|
||||||
|
}
|
||||||
|
} else if (startConstraint) {
|
||||||
finalConstraint = startConstraint;
|
finalConstraint = startConstraint;
|
||||||
} else if (endConstraint) {
|
} else if (endConstraint) {
|
||||||
finalConstraint = endConstraint;
|
finalConstraint = endConstraint;
|
||||||
} else {
|
} else {
|
||||||
// 两个Pin都没有约束,生成随机约束
|
|
||||||
finalConstraint = generateRandomConstraint();
|
finalConstraint = generateRandomConstraint();
|
||||||
}
|
}
|
||||||
|
// 如果发生了批量替换需求,遍历所有组件的所有Pin
|
||||||
|
if (replacedConstraint && newConstraint && replacedConstraint !== newConstraint) {
|
||||||
|
props.components.forEach(comp => {
|
||||||
|
if (comp.props && comp.props.constraint === replacedConstraint) {
|
||||||
|
emit('update-component-prop', { id: comp.id, propName: 'constraint', value: newConstraint });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 完成连线
|
// 完成连线
|
||||||
completeWireCreation(
|
completeWireCreation(
|
||||||
componentId,
|
componentId,
|
||||||
|
@ -495,7 +541,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||||
updatePinConstraint(componentId, pinInfo.label, finalConstraint);
|
updatePinConstraint(componentId, pinInfo.label, finalConstraint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 生成随机约束名
|
// 生成随机约束名
|
||||||
function generateRandomConstraint() {
|
function generateRandomConstraint() {
|
||||||
|
@ -503,36 +548,6 @@ function generateRandomConstraint() {
|
||||||
return `$auto_constraint_${randomId}`;
|
return `$auto_constraint_${randomId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 询问用户选择约束
|
|
||||||
function promptForConstraintSelection(
|
|
||||||
endComponentId: string,
|
|
||||||
endPinInfo: any,
|
|
||||||
startConstraint: string,
|
|
||||||
endConstraint: string
|
|
||||||
) {
|
|
||||||
// 在实际应用中,这里可以使用Modal对话框等UI组件
|
|
||||||
// 这里简化为使用浏览器的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的约束
|
// 更新Pin的约束
|
||||||
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
|
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
|
||||||
// 通过组件ID获取组件实例
|
// 通过组件ID获取组件实例
|
||||||
|
@ -575,26 +590,15 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
|
||||||
endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
||||||
endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
||||||
console.log(`通过 getPinPosition 获取终点针脚 ${endPinLabel} 的画布坐标: (${endX}, ${endY})`);
|
console.log(`通过 getPinPosition 获取终点针脚 ${endPinLabel} 的画布坐标: (${endX}, ${endY})`);
|
||||||
} else {
|
} else { console.warn(`getPinPosition 返回 null,将使用备选方法`);
|
||||||
console.warn(`getPinPosition 返回 null,将使用备选方法`);
|
|
||||||
}
|
}
|
||||||
} else if (endPinInfo && endPinInfo.position) {
|
} else if (endPinInfo && endPinInfo.position) {
|
||||||
// 如果 getPinPosition 不可用,使用传入的针脚位置信息
|
// 如果 getPinPosition 不可用,使用传入的针脚位置信息
|
||||||
const pinPagePosition = endPinInfo.position;
|
const pinPagePosition = endPinInfo.position;
|
||||||
endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
|
endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
|
||||||
endY = (pinPagePosition.y - containerRect.top - position.y) / 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 = {
|
const newWire: WireItem = {
|
||||||
id: `wire-${Date.now()}`,
|
id: `wire-${Date.now()}`,
|
||||||
|
@ -607,18 +611,12 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
|
||||||
endComponentId: endComponentId,
|
endComponentId: endComponentId,
|
||||||
endPinLabel: endPinLabel,
|
endPinLabel: endPinLabel,
|
||||||
color: '#4a5568',
|
color: '#4a5568',
|
||||||
constraint: constraint
|
constraint: constraint,
|
||||||
|
routingMode: 'orthogonal',
|
||||||
|
strokeWidth: 2,
|
||||||
|
showLabel: false
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
wires.value.push(newWire);
|
||||||
|
|
||||||
// 通知父组件连线已创建
|
// 通知父组件连线已创建
|
||||||
|
@ -642,28 +640,7 @@ function onCreatingWireMouseMove(e: MouseEvent) {
|
||||||
|
|
||||||
// 处理连线点击
|
// 处理连线点击
|
||||||
function handleWireClick(wire: WireItem) {
|
function handleWireClick(wire: WireItem) {
|
||||||
// 这里可以添加连线选中、删除等功能
|
wireSelectedId.value = wire.id;
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据针脚位置更新连线
|
// 根据针脚位置更新连线
|
||||||
|
@ -671,32 +648,15 @@ function updateWireWithPinPositions(wire: WireItem) {
|
||||||
if (!canvasContainer.value) return;
|
if (!canvasContainer.value) return;
|
||||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
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) {
|
if (wire.startComponentId && wire.startPinLabel) {
|
||||||
const startComponentRef = componentRefs.value[wire.startComponentId];
|
const startComponentRef = componentRefs.value[wire.startComponentId];
|
||||||
if (startComponentRef && startComponentRef.getPinPosition) {
|
if (startComponentRef && startComponentRef.getPinPosition) {
|
||||||
const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
|
const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
|
||||||
if (pinPosition) {
|
if (pinPosition) {
|
||||||
const newStartX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
wire.startX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
||||||
const newStartY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
wire.startY = (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 方法`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,31 +666,10 @@ function updateWireWithPinPositions(wire: WireItem) {
|
||||||
if (endComponentRef && endComponentRef.getPinPosition) {
|
if (endComponentRef && endComponentRef.getPinPosition) {
|
||||||
const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
|
const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
|
||||||
if (pinPosition) {
|
if (pinPosition) {
|
||||||
const newEndX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
wire.endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
||||||
const newEndY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
wire.endY = (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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -738,14 +677,9 @@ function updateWireWithPinPositions(wire: WireItem) {
|
||||||
function updateWiresForComponent(componentId: string) {
|
function updateWiresForComponent(componentId: string) {
|
||||||
if (!canvasContainer.value || !componentId) return;
|
if (!canvasContainer.value || !componentId) return;
|
||||||
|
|
||||||
console.log(`更新组件 ${componentId} 相关的连线位置`);
|
|
||||||
|
|
||||||
// 检查组件是否存在
|
// 检查组件是否存在
|
||||||
const component = props.components.find(c => c.id === componentId);
|
const component = props.components.find(c => c.id === componentId);
|
||||||
if (!component) {
|
if (!component) return;
|
||||||
console.warn(`找不到组件 ${componentId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找与该组件关联的所有连线
|
// 查找与该组件关联的所有连线
|
||||||
const relatedWires = wires.value.filter(wire =>
|
const relatedWires = wires.value.filter(wire =>
|
||||||
|
@ -753,49 +687,14 @@ function updateWiresForComponent(componentId: string) {
|
||||||
wire.endComponentId === componentId
|
wire.endComponentId === componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`找到 ${relatedWires.length} 条相关连线`);
|
if (relatedWires.length === 0) return;
|
||||||
|
|
||||||
if (relatedWires.length === 0) {
|
|
||||||
// 没有找到直接关联的连线,检查所有连线
|
|
||||||
console.log('没有找到直接关联的连线,检查所有连线');
|
|
||||||
|
|
||||||
// 打印所有连线信息,帮助调试
|
|
||||||
wires.value.forEach((wire, index) => {
|
|
||||||
console.log(`连线 ${index}: startComponentId=${wire.startComponentId}, endComponentId=${wire.endComponentId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新所有相关连线的位置
|
// 更新所有相关连线的位置
|
||||||
relatedWires.forEach(wire => {
|
relatedWires.forEach(wire => {
|
||||||
console.log(`更新连线 ${wire.id} (${wire.startComponentId}/${wire.startPinLabel} -> ${wire.endComponentId}/${wire.endPinLabel})`);
|
|
||||||
updateWireWithPinPositions(wire);
|
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(() => {
|
||||||
// 初始化中心位置
|
// 初始化中心位置
|
||||||
|
@ -809,18 +708,6 @@ onMounted(() => {
|
||||||
|
|
||||||
// 添加键盘事件监听器
|
// 添加键盘事件监听器
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
// 添加定期更新连线位置的定时器
|
|
||||||
const wireUpdateInterval = setInterval(() => {
|
|
||||||
if (wires.value.length > 0) {
|
|
||||||
updateAllWires();
|
|
||||||
}
|
|
||||||
}, 1000); // 每秒更新一次所有连线位置
|
|
||||||
|
|
||||||
// 在组件卸载时清除定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(wireUpdateInterval);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理键盘事件
|
// 处理键盘事件
|
||||||
|
@ -838,6 +725,16 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||||
// 取消连线创建
|
// 取消连线创建
|
||||||
cancelWireCreation();
|
cancelWireCreation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除选中连线
|
||||||
|
if (wireSelectedId.value && (e.key === 'Delete' || e.key === 'Backspace')) {
|
||||||
|
const idx = wires.value.findIndex(w => w.id === wireSelectedId.value);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const deletedWire = wires.value.splice(idx, 1)[0];
|
||||||
|
emit('wire-deleted', deletedWire.id);
|
||||||
|
wireSelectedId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
@ -891,7 +788,7 @@ onUnmounted(() => {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: auto; /* 修复:允许线被点击 */
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,4 +858,9 @@ onUnmounted(() => {
|
||||||
.component-wrapper :deep(input) {
|
.component-wrapper :deep(input) {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wire-active {
|
||||||
|
stroke: #ff9800 !important;
|
||||||
|
filter: drop-shadow(0 0 4px #ff9800cc);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
<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>
|
|
|
@ -5,71 +5,63 @@
|
||||||
: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"
|
:data-component-id="componentId"
|
||||||
>
|
:data-pin-label="props.label"
|
||||||
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g v-if="props.appearance === 'None'">
|
> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
|
||||||
<g transform="translate(-12.5, -12.5)" class="interactive"> <circle
|
<g v-if="props.appearance === 'None'">
|
||||||
|
<g transform="translate(-12.5, -12.5)">
|
||||||
|
<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"
|
class="interactive"
|
||||||
@mouseleave="hidePinTooltip"
|
|
||||||
@click.stop="handlePinClick"
|
@click.stop="handlePinClick"
|
||||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
:data-pin-element="`${props.componentId}-${props.label}`" />
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g v-else-if="props.appearance === 'Dip'">
|
<g v-else-if="props.appearance === 'Dip'">
|
||||||
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
|
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
|
||||||
<g transform="translate(-12.5, -12.5)" class="interactive">
|
<g transform="translate(-12.5, -12.5)">
|
||||||
<rect
|
<rect
|
||||||
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
|
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
|
||||||
width="25"
|
width="25"
|
||||||
height="25"
|
height="25"
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
rx="2.5" /> <circle
|
rx="2.5" />
|
||||||
|
<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"
|
class="interactive"
|
||||||
@mouseleave="hidePinTooltip"
|
|
||||||
@click.stop="handlePinClick"
|
@click.stop="handlePinClick"
|
||||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
:data-pin-element="`${props.componentId}-${props.label}`" />
|
||||||
<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 v-else-if="props.appearance === 'SMT'">
|
</g>
|
||||||
|
<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>
|
||||||
<!-- SMT样式的针脚 --> <circle
|
<circle
|
||||||
fill="#ecececc5"
|
fill="#ecececc5"
|
||||||
cx="10"
|
cx="10"
|
||||||
cy="0"
|
cy="0"
|
||||||
r="3.75"
|
r="3.75"
|
||||||
class="interactive"
|
class="interactive"
|
||||||
@mouseenter="showPinTooltip"
|
|
||||||
@mouseleave="hidePinTooltip"
|
|
||||||
@click.stop="handlePinClick"
|
@click.stop="handlePinClick"
|
||||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
:data-pin-element="`${props.componentId}-${props.label}`"
|
||||||
</g> </g>
|
/>
|
||||||
|
</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">
|
||||||
|
@ -82,6 +74,7 @@ interface Props {
|
||||||
direction?: 'input' | 'output' | 'inout';
|
direction?: 'input' | 'output' | 'inout';
|
||||||
type?: 'digital' | 'analog';
|
type?: 'digital' | 'analog';
|
||||||
appearance?: 'None' | 'Dip' | 'SMT';
|
appearance?: 'None' | 'Dip' | 'SMT';
|
||||||
|
componentId?: string; // 添加组件ID属性,用于唯一标识
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -90,7 +83,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
constraint: '',
|
constraint: '',
|
||||||
direction: 'input',
|
direction: 'input',
|
||||||
type: 'digital',
|
type: 'digital',
|
||||||
appearance: 'Dip'
|
appearance: 'Dip',
|
||||||
|
componentId: 'pin-default' // 默认ID
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
|
@ -100,58 +94,6 @@ const emit = defineEmits([
|
||||||
|
|
||||||
// 内部状态
|
// 内部状态
|
||||||
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) {
|
function handlePinClick(event: MouseEvent) {
|
||||||
|
@ -222,10 +164,10 @@ defineExpose({
|
||||||
// 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
|
// 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
|
||||||
if (pinLabel !== props.label) return null;
|
if (pinLabel !== props.label) return null;
|
||||||
|
|
||||||
// 使用自身作为一个唯一标识符,确保针脚位置是基于实际的DOM位置
|
// 使用组件ID和针脚标签的组合作为唯一标识符
|
||||||
// 这样可以避免document.querySelector获取到非预期的元素
|
const selector = `[data-pin-element="${props.componentId}-${props.label}"]`;
|
||||||
const pinElement = document.querySelector(`[data-pin-id="${props.label}-${props.constraint}"]`) as SVGElement;
|
|
||||||
|
|
||||||
|
const pinElement = document.querySelector(selector) as SVGElement;
|
||||||
if (pinElement) {
|
if (pinElement) {
|
||||||
// 获取针脚元素的位置
|
// 获取针脚元素的位置
|
||||||
const rect = pinElement.getBoundingClientRect();
|
const rect = pinElement.getBoundingClientRect();
|
||||||
|
@ -235,13 +177,11 @@ defineExpose({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果找不到特定元素,使用计算出的位置
|
// 如果找不到特定元素,使用SVG元素位置
|
||||||
// 这种情况下我们需要找到父SVG元素位置
|
const svgSelector = `svg.pin-component[data-component-id="${props.componentId}"][data-pin-label="${props.label}"]`;
|
||||||
const svgElement = document.querySelector(`.pin-component[data-pin-id="${props.label}"]`) as SVGElement;
|
|
||||||
if (!svgElement) {
|
const svgElement = document.querySelector(svgSelector) as SVGElement;
|
||||||
console.error(`找不到针脚 ${props.label} 的SVG元素`);
|
if (!svgElement) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgRect = svgElement.getBoundingClientRect();
|
const svgRect = svgElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
@ -249,12 +189,6 @@ defineExpose({
|
||||||
let pinX = svgRect.left + svgRect.width / 2;
|
let pinX = svgRect.left + svgRect.width / 2;
|
||||||
let pinY = svgRect.top + svgRect.height / 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 };
|
return { x: pinX, y: pinY };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -273,15 +207,4 @@ 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>
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<g :data-wire-id="props.id">
|
||||||
|
<path
|
||||||
|
:d="pathData"
|
||||||
|
fill="none"
|
||||||
|
:stroke="computedStroke"
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
:class="{ 'wire-active': props.isActive }"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
<!-- 可选:添加连线标签或状态指示器 -->
|
||||||
|
<text
|
||||||
|
v-if="showLabel"
|
||||||
|
:x="labelPosition.x"
|
||||||
|
:y="labelPosition.y"
|
||||||
|
class="wire-label"
|
||||||
|
>{{ props.constraint || '' }}</text>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, defineEmits, reactive } 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;
|
||||||
|
// 添加约束属性
|
||||||
|
constraint?: string;
|
||||||
|
// 显示标签
|
||||||
|
showLabel?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
strokeColor: '#4a5568',
|
||||||
|
strokeWidth: 2,
|
||||||
|
isActive: false,
|
||||||
|
routingMode: 'orthogonal',
|
||||||
|
showLabel: false,
|
||||||
|
constraint: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedStroke = computed(() => props.isActive ? '#ff9800' : props.strokeColor);
|
||||||
|
|
||||||
|
const emit = defineEmits(['click', 'update:active', 'update:position']);
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
emit('click', { id: props.id, event });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算标签位置 - 放在连线中间
|
||||||
|
const labelPosition = computed(() => {
|
||||||
|
return {
|
||||||
|
x: (props.startX + props.endX) / 2,
|
||||||
|
y: (props.startY + props.endY) / 2 - 5
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathData = computed(() => {
|
||||||
|
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 (dx === 0 || dy === 0) {
|
||||||
|
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absDx = Math.abs(dx);
|
||||||
|
const absDy = Math.abs(dy);
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 暴露方法,用于获取这条连线的信息
|
||||||
|
defineExpose({ id: props.id,
|
||||||
|
getInfo: () => ({
|
||||||
|
id: props.id,
|
||||||
|
startComponentId: props.startComponentId,
|
||||||
|
startPinLabel: props.startPinLabel,
|
||||||
|
endComponentId: props.endComponentId,
|
||||||
|
endPinLabel: props.endPinLabel,
|
||||||
|
constraint: props.constraint
|
||||||
|
}),
|
||||||
|
// 更新连线位置
|
||||||
|
updatePosition: (newStartX: number, newStartY: number, newEndX: number, newEndY: number) => {
|
||||||
|
// 由于 props 是只读的,我们只能通过事件通知父组件更新
|
||||||
|
emit('update:position', {
|
||||||
|
id: props.id,
|
||||||
|
startX: newStartX,
|
||||||
|
startY: newStartY,
|
||||||
|
endX: newEndX,
|
||||||
|
endY: newEndY
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 获取连线的针脚情况
|
||||||
|
getPinPosition: () => null, // 为了与其他组件接口一致,但Wire不是针脚组件
|
||||||
|
// 获取连线的路由模式
|
||||||
|
getRoutingMode: () => props.routingMode,
|
||||||
|
// 设置连线状态(如高亮等)
|
||||||
|
setActive: (active: boolean) => {
|
||||||
|
emit('update:active', active);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wire-active {
|
||||||
|
stroke-dasharray: 5;
|
||||||
|
animation: dash 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wire-label {
|
||||||
|
font-size: 10px;
|
||||||
|
fill: #666;
|
||||||
|
text-anchor: middle;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -318,7 +318,55 @@ const componentConfigs: Record<string, ComponentConfig> = {
|
||||||
description: '相同约束字符串的组件将被视为有电气连接'
|
description: '相同约束字符串的组件将被视为有电气连接'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
// 线缆配置
|
||||||
|
Wire: {
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'routingMode',
|
||||||
|
type: 'select',
|
||||||
|
label: '路由方式',
|
||||||
|
default: 'orthogonal',
|
||||||
|
options: [
|
||||||
|
{ value: 'orthogonal', label: '直角' },
|
||||||
|
{ value: 'direct', label: '直线' },
|
||||||
|
{ value: 'auto', label: '自动' }
|
||||||
|
],
|
||||||
|
description: '线路连接方式'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'strokeColor',
|
||||||
|
type: 'string',
|
||||||
|
label: '线条颜色',
|
||||||
|
default: '#4a5568',
|
||||||
|
description: '线条颜色,使用CSS颜色值'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'strokeWidth',
|
||||||
|
type: 'number',
|
||||||
|
label: '线条宽度',
|
||||||
|
default: 2,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 0.5,
|
||||||
|
description: '线条宽度'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'constraint',
|
||||||
|
type: 'string',
|
||||||
|
label: '约束名称',
|
||||||
|
default: '',
|
||||||
|
description: '线路约束名称,用于标识连接关系'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showLabel',
|
||||||
|
type: 'boolean',
|
||||||
|
label: '显示标签',
|
||||||
|
default: false,
|
||||||
|
description: '是否显示连线上的约束标签'
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取组件配置的函数
|
// 获取组件配置的函数
|
||||||
|
|
|
@ -344,7 +344,7 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
|
||||||
// 处理连线创建事件
|
// 处理连线创建事件
|
||||||
function handleWireCreated(wireData: any) {
|
function handleWireCreated(wireData: any) {
|
||||||
console.log('Wire created:', wireData);
|
console.log('Wire created:', wireData);
|
||||||
// 可以在这里添加连线创建的相关逻辑
|
// 连线已在DiagramCanvas.vue中完成约束处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理连线删除事件
|
// 处理连线删除事件
|
||||||
|
|
Loading…
Reference in New Issue