feat: Pin移动连线也跟着移动
This commit is contained in:
@@ -5,71 +5,63 @@
|
||||
:height="height"
|
||||
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
|
||||
class="pin-component"
|
||||
:data-pin-id="props.label"
|
||||
>
|
||||
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`"> <g v-if="props.appearance === 'None'">
|
||||
<g transform="translate(-12.5, -12.5)" class="interactive"> <circle
|
||||
: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(-12.5, -12.5)">
|
||||
<circle
|
||||
style="fill:#909090"
|
||||
cx="12.5"
|
||||
cy="12.5"
|
||||
r="3.75"
|
||||
@mouseenter="showPinTooltip"
|
||||
@mouseleave="hidePinTooltip"
|
||||
class="interactive"
|
||||
@click.stop="handlePinClick"
|
||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
||||
:data-pin-element="`${props.componentId}-${props.label}`" />
|
||||
</g>
|
||||
</g>
|
||||
<g v-else-if="props.appearance === 'Dip'">
|
||||
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
|
||||
<g transform="translate(-12.5, -12.5)" class="interactive">
|
||||
<g transform="translate(-12.5, -12.5)">
|
||||
<rect
|
||||
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
|
||||
width="25"
|
||||
height="25"
|
||||
x="0"
|
||||
y="0"
|
||||
rx="2.5" /> <circle
|
||||
rx="2.5" />
|
||||
<circle
|
||||
style="fill:#ecececc5;fill-opacity:0.772973"
|
||||
cx="12.5"
|
||||
cy="12.5"
|
||||
r="3.75"
|
||||
@mouseenter="showPinTooltip"
|
||||
@mouseleave="hidePinTooltip"
|
||||
class="interactive"
|
||||
@click.stop="handlePinClick"
|
||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
||||
:data-pin-element="`${props.componentId}-${props.label}`" />
|
||||
<text
|
||||
style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
|
||||
x="7.3"
|
||||
y="7"
|
||||
xml:space="preserve">{{ props.label }}</text>
|
||||
</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="-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" />
|
||||
<text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
|
||||
<!-- SMT样式的针脚 --> <circle
|
||||
<circle
|
||||
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>
|
||||
:data-pin-element="`${props.componentId}-${props.label}`"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -82,6 +74,7 @@ interface Props {
|
||||
direction?: 'input' | 'output' | 'inout';
|
||||
type?: 'digital' | 'analog';
|
||||
appearance?: 'None' | 'Dip' | 'SMT';
|
||||
componentId?: string; // 添加组件ID属性,用于唯一标识
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -90,7 +83,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
constraint: '',
|
||||
direction: 'input',
|
||||
type: 'digital',
|
||||
appearance: 'Dip'
|
||||
appearance: 'Dip',
|
||||
componentId: 'pin-default' // 默认ID
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -100,58 +94,6 @@ const emit = defineEmits([
|
||||
|
||||
// 内部状态
|
||||
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) {
|
||||
@@ -222,10 +164,10 @@ defineExpose({
|
||||
// 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
|
||||
if (pinLabel !== props.label) return null;
|
||||
|
||||
// 使用自身作为一个唯一标识符,确保针脚位置是基于实际的DOM位置
|
||||
// 这样可以避免document.querySelector获取到非预期的元素
|
||||
const pinElement = document.querySelector(`[data-pin-id="${props.label}-${props.constraint}"]`) as SVGElement;
|
||||
// 使用组件ID和针脚标签的组合作为唯一标识符
|
||||
const selector = `[data-pin-element="${props.componentId}-${props.label}"]`;
|
||||
|
||||
const pinElement = document.querySelector(selector) as SVGElement;
|
||||
if (pinElement) {
|
||||
// 获取针脚元素的位置
|
||||
const rect = pinElement.getBoundingClientRect();
|
||||
@@ -235,13 +177,11 @@ defineExpose({
|
||||
};
|
||||
}
|
||||
|
||||
// 如果找不到特定元素,使用计算出的位置
|
||||
// 这种情况下我们需要找到父SVG元素位置
|
||||
const svgElement = document.querySelector(`.pin-component[data-pin-id="${props.label}"]`) as SVGElement;
|
||||
if (!svgElement) {
|
||||
console.error(`找不到针脚 ${props.label} 的SVG元素`);
|
||||
return null;
|
||||
}
|
||||
// 如果找不到特定元素,使用SVG元素位置
|
||||
const svgSelector = `svg.pin-component[data-component-id="${props.componentId}"][data-pin-label="${props.label}"]`;
|
||||
|
||||
const svgElement = document.querySelector(svgSelector) as SVGElement;
|
||||
if (!svgElement) return null;
|
||||
|
||||
const svgRect = svgElement.getBoundingClientRect();
|
||||
|
||||
@@ -249,12 +189,6 @@ defineExpose({
|
||||
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 };
|
||||
}
|
||||
});
|
||||
@@ -273,15 +207,4 @@ defineExpose({
|
||||
.interactive:hover {
|
||||
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>
|
||||
|
||||
149
src/components/equipments/Wire.vue
Normal file
149
src/components/equipments/Wire.vue
Normal file
@@ -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: '相同约束字符串的组件将被视为有电气连接'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// 线缆配置
|
||||
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: '是否显示连线上的约束标签'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
// 获取组件配置的函数
|
||||
|
||||
Reference in New Issue
Block a user