Compare commits
2 Commits
b3a5342d6b
...
1c75aa621a
Author | SHA1 | Date |
---|---|---|
|
1c75aa621a | |
|
10db7c67bf |
|
@ -8,6 +8,9 @@
|
|||
/* eslint-disable */
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
import { batchSetConstraintStates, notifyConstraintChange } from './stores/constraints';
|
||||
import type { ConstraintLevel } from './stores/constraints';
|
||||
|
||||
export class Client {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
|
@ -866,4 +869,57 @@ function throwException(message: string, status: number, response: string, heade
|
|||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 约束通信相关方法
|
||||
export function receiveConstraintUpdates(constraints: Record<string, ConstraintLevel>) {
|
||||
// 批量更新约束状态
|
||||
batchSetConstraintStates(constraints);
|
||||
}
|
||||
|
||||
export function sendConstraintUpdate(constraint: string, level: ConstraintLevel) {
|
||||
// 向后端发送约束状态变化
|
||||
console.log(`发送约束 ${constraint} 状态变化为 ${level}`);
|
||||
|
||||
// TODO: 实际的WebSocket或HTTP请求发送约束变化
|
||||
// 例如:
|
||||
// socket.emit('constraintUpdate', { constraint, level });
|
||||
// 或
|
||||
// fetch('/api/constraints', {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify({ constraint, level }),
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
}
|
||||
|
||||
// 初始化约束通信
|
||||
export function initConstraintCommunication() {
|
||||
// 监听服务器发来的约束状态变化
|
||||
// 示例:
|
||||
// socket.on('constraintUpdates', (data) => {
|
||||
// receiveConstraintUpdates(data);
|
||||
// });
|
||||
|
||||
// 模拟接收一些初始约束状态
|
||||
setTimeout(() => {
|
||||
receiveConstraintUpdates({
|
||||
'A1': 'high',
|
||||
'A2': 'low',
|
||||
'A3': 'undefined'
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 覆盖全局notifyConstraintChange,加入发送逻辑
|
||||
const originalNotifyConstraintChange = notifyConstraintChange;
|
||||
const wrappedNotifyConstraintChange = (constraint: string, level: ConstraintLevel) => {
|
||||
// 调用原始方法更新本地状态
|
||||
originalNotifyConstraintChange(constraint, level);
|
||||
|
||||
// 向后端发送更新
|
||||
sendConstraintUpdate(constraint, level);
|
||||
};
|
||||
|
||||
// 替换全局方法
|
||||
(window as any).__notifyConstraintChange = notifyConstraintChange;
|
||||
(window as any).notifyConstraintChange = wrappedNotifyConstraintChange;
|
|
@ -1,80 +0,0 @@
|
|||
.diagram-canvas {
|
||||
position: relative;
|
||||
width: 4000px;
|
||||
height: 4000px;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
/* 将网格线应用到容器而不是画布上,确保覆盖整个可视区域 */
|
||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
|
||||
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* 为黑暗模式设置不同的网格线颜色 */
|
||||
:root[data-theme="dark"] .flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
|
||||
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* 禁用滚动条 */
|
||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* 元器件容器样式 */
|
||||
.component-wrapper {
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
display: inline-block; /* 确保元素宽度基于内容 */
|
||||
max-width: fit-content; /* 强制宽度适应内容 */
|
||||
max-height: fit-content; /* 强制高度适应内容 */
|
||||
overflow: visible; /* 允许内容溢出(用于显示边框) */
|
||||
}
|
||||
|
||||
/* 悬停状态 */
|
||||
.component-hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 3px dashed #3498db;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.component-selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 4px dashed #e74c3c;
|
||||
border-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
:style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }"> <!-- 渲染连线 -->
|
||||
<svg class="wires-layer" width="4000" height="4000">
|
||||
<!-- 已完成的连线 -->
|
||||
<Wire
|
||||
<WireComponent
|
||||
v-for="wire in wires"
|
||||
:key="wire.id"
|
||||
:id="wire.id"
|
||||
|
@ -19,16 +19,14 @@
|
|||
:end-y="wire.endY"
|
||||
:stroke-color="wire.color || '#4a5568'"
|
||||
:stroke-width="2"
|
||||
:is-active="wire.isActive"
|
||||
:is-active="false"
|
||||
:start-component-id="wire.startComponentId"
|
||||
:start-pin-label="wire.startPinLabel"
|
||||
:end-component-id="wire.endComponentId"
|
||||
:end-pin-label="wire.endPinLabel"
|
||||
@click="handleWireClick(wire)"
|
||||
:constraint="wire.constraint"
|
||||
/>
|
||||
|
||||
<!-- 正在创建的连线 -->
|
||||
<Wire
|
||||
<WireComponent
|
||||
v-if="isCreatingWire"
|
||||
id="temp-wire"
|
||||
:start-x="creatingWireStart.x"
|
||||
|
@ -58,7 +56,7 @@
|
|||
@mouseleave="hoveredComponent = null"><!-- 动态渲染组件 --> <component
|
||||
:is="getComponentDefinition(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)"
|
||||
@pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
|
||||
:ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
|
||||
|
@ -80,7 +78,16 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||
import Wire from '@/components/Wire.vue';
|
||||
import WireComponent from '@/components/equipments/Wire.vue';
|
||||
import { wires, addWire, deleteWire, updateWiresConstraintByPin, findWiresByPin, isCreatingWire, creatingWireStart, creatingWireStartInfo, mousePosition, resetWireCreation, setWireCreationStart, setMousePosition } from './wireManager';
|
||||
import type { WireItem } from './wireManager';
|
||||
|
||||
// 右键菜单处理函数(如无特殊需求可为空实现)
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
// 可根据需要自定义右键菜单逻辑
|
||||
// 目前只是阻止默认行为
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// 定义组件接受的属性
|
||||
interface ComponentItem {
|
||||
|
@ -170,19 +177,10 @@ const getComponentDefinition = (type: string) => {
|
|||
};
|
||||
|
||||
// 准备组件属性,确保类型正确
|
||||
function prepareComponentProps(props: Record<string, any>): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in props) {
|
||||
let value = props[key];
|
||||
// 只要不是 null/undefined 且不是 string,就强制转字符串
|
||||
if (
|
||||
(key === 'style' || key === 'direction' || key === 'type') &&
|
||||
value != null &&
|
||||
typeof value !== 'string'
|
||||
) {
|
||||
value = String(value);
|
||||
}
|
||||
result[key] = value;
|
||||
function prepareComponentProps(props: Record<string, any>, componentId?: string): Record<string, any> {
|
||||
const result: Record<string, any> = { ...props };
|
||||
if (componentId) {
|
||||
result.componentId = componentId;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -351,6 +349,19 @@ function stopComponentDrag() {
|
|||
|
||||
// 更新组件属性
|
||||
function updateComponentProp(componentId: string, propName: string, value: any) {
|
||||
// 如果是引脚约束变更,自动调用updatePinConstraint
|
||||
if (propName === 'constraint') {
|
||||
// 查找该组件的Pin label(假设label唯一,或可扩展为多Pin)
|
||||
const componentRef = componentRefs.value[componentId];
|
||||
if (componentRef && componentRef.getInfo) {
|
||||
const pinInfo = componentRef.getInfo();
|
||||
if (pinInfo) {
|
||||
updatePinConstraint(componentId, value);
|
||||
return; // 已处理,无需再emit
|
||||
}
|
||||
}
|
||||
}
|
||||
// 其它属性正常emit
|
||||
emit('update-component-prop', { id: componentId, propName, value });
|
||||
}
|
||||
|
||||
|
@ -363,6 +374,17 @@ function getComponentRef(componentId: string) {
|
|||
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({
|
||||
getComponentRef,
|
||||
|
@ -371,170 +393,18 @@ defineExpose({
|
|||
});
|
||||
|
||||
// --- 连线状态 ---
|
||||
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;
|
||||
// 处理连线创建事件
|
||||
function handleWireCreated(wireData: any) {
|
||||
addWire(wireData);
|
||||
emit('wire-created', wireData);
|
||||
}
|
||||
|
||||
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;
|
||||
// 删除连线
|
||||
function handleWireDeleted(wireId: string) {
|
||||
deleteWire(wireId);
|
||||
emit('wire-deleted', wireId);
|
||||
}
|
||||
|
||||
// 处理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
|
||||
) {
|
||||
// 在实际应用中,这里可以使用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的约束
|
||||
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
|
||||
// 更新Pin的约束时同步Wire
|
||||
function updatePinConstraint(componentId: string, constraint: string) {
|
||||
// 通过组件ID获取组件实例
|
||||
const component = props.components.find(c => c.id === componentId);
|
||||
if (!component) return;
|
||||
|
@ -546,7 +416,7 @@ function updatePinConstraint(componentId: string, pinLabel: string, constraint:
|
|||
// 更新组件属性
|
||||
if (component.props && componentRef.getInfo) {
|
||||
const pinInfo = componentRef.getInfo();
|
||||
if (pinInfo && pinInfo.label === pinLabel) {
|
||||
if (pinInfo) {
|
||||
emit('update-component-prop', {
|
||||
id: componentId,
|
||||
propName: 'constraint',
|
||||
|
@ -554,10 +424,114 @@ function updatePinConstraint(componentId: string, pinLabel: string, constraint:
|
|||
});
|
||||
}
|
||||
}
|
||||
// 同步所有相关Wire的constraint
|
||||
updateWiresConstraintByPin(componentId, constraint);
|
||||
}
|
||||
|
||||
// 更新鼠标位置
|
||||
function updateMousePosition(e: MouseEvent) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
setMousePosition(
|
||||
(e.clientX - containerRect.left - position.x) / scale.value,
|
||||
(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);
|
||||
if (!pinInfo || !pinInfo.label) {
|
||||
console.error('无效的针脚信息:', pinInfo);
|
||||
return;
|
||||
}
|
||||
if (!pinInfo.position) {
|
||||
console.error('针脚信息中缺少位置数据:', pinInfo);
|
||||
return;
|
||||
}
|
||||
const pinPagePosition = pinInfo.position;
|
||||
const pinCanvasX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
|
||||
const pinCanvasY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
|
||||
if (!isCreatingWire.value) {
|
||||
setWireCreationStart(pinCanvasX, pinCanvasY, componentId, pinInfo.label, pinInfo.constraint);
|
||||
document.addEventListener('mousemove', onCreatingWireMouseMove);
|
||||
} else {
|
||||
if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
|
||||
cancelWireCreation();
|
||||
return;
|
||||
}
|
||||
// 获取起点和终点的约束
|
||||
const startConstraint = creatingWireStartInfo.constraint || '';
|
||||
const endConstraint = pinInfo.constraint || '';
|
||||
let finalConstraint = '';
|
||||
// 如果两端约束完全一致,直接用该约束,不弹窗
|
||||
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;
|
||||
} else if (endConstraint) {
|
||||
finalConstraint = endConstraint;
|
||||
} else {
|
||||
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(
|
||||
componentId,
|
||||
finalConstraint,
|
||||
pinInfo
|
||||
);
|
||||
|
||||
// 更新两个Pin的约束
|
||||
updatePinConstraint(creatingWireStartInfo.componentId, finalConstraint);
|
||||
updatePinConstraint(componentId, finalConstraint);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机约束名
|
||||
function generateRandomConstraint() {
|
||||
const randomId = Math.floor(Math.random() * 1000000);
|
||||
return `$auto_constraint_${randomId}`;
|
||||
}
|
||||
|
||||
// 完成连线创建
|
||||
function completeWireCreation(endComponentId: string, endPinLabel: string, constraint: string, endPinInfo?: any) {
|
||||
function completeWireCreation(endComponentId: string, constraint: string, endPinInfo?: any) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
|
||||
|
@ -570,32 +544,21 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
|
|||
// 尝试使用 getPinPosition 获取精确的针脚位置
|
||||
const endComponentRef = componentRefs.value[endComponentId];
|
||||
if (endComponentRef && endComponentRef.getPinPosition) {
|
||||
const pinPosition = endComponentRef.getPinPosition(endPinLabel);
|
||||
const pinPosition = endComponentRef.getPinPosition(endComponentId);
|
||||
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,将使用备选方法`);
|
||||
console.log(`通过 getPinPosition 获取终点针脚 ${endComponentId} 的画布坐标: (${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,
|
||||
|
@ -603,22 +566,14 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
|
|||
endX: endX,
|
||||
endY: endY,
|
||||
startComponentId: creatingWireStartInfo.componentId,
|
||||
startPinLabel: creatingWireStartInfo.pinLabel,
|
||||
endComponentId: endComponentId,
|
||||
endPinLabel: endPinLabel,
|
||||
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);
|
||||
|
||||
// 通知父组件连线已创建
|
||||
|
@ -631,7 +586,7 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
|
|||
|
||||
// 取消连线创建
|
||||
function cancelWireCreation() {
|
||||
isCreatingWire.value = false;
|
||||
resetWireCreation();
|
||||
document.removeEventListener('mousemove', onCreatingWireMouseMove);
|
||||
}
|
||||
|
||||
|
@ -640,112 +595,63 @@ 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) {
|
||||
if (wire.startComponentId) {
|
||||
const startComponentRef = componentRefs.value[wire.startComponentId];
|
||||
if (startComponentRef && startComponentRef.getPinPosition) {
|
||||
const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
|
||||
const pinPosition = startComponentRef.getPinPosition(wire.startComponentId);
|
||||
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;
|
||||
wire.startX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
||||
wire.startY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
||||
} else {
|
||||
console.warn(`无法获取针脚 ${wire.startComponentId}/${wire.startPinLabel} 的位置`);
|
||||
console.warn(`无法获取组件${wire.startComponentId}的针脚${wire.startComponentId}位置`);
|
||||
// 尝试多次获取位置(针对初次渲染可能有延迟的情况)
|
||||
setTimeout(() => {
|
||||
const retryPosition = startComponentRef.getPinPosition(wire.startComponentId);
|
||||
if (retryPosition) {
|
||||
wire.startX = (retryPosition.x - containerRect.left - position.x) / scale.value;
|
||||
wire.startY = (retryPosition.y - containerRect.top - position.y) / scale.value;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
console.warn(`组件 ${wire.startComponentId} 没有实现 getPinPosition 方法`);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新终点
|
||||
if (wire.endComponentId && wire.endPinLabel) {
|
||||
if (wire.endComponentId) {
|
||||
const endComponentRef = componentRefs.value[wire.endComponentId];
|
||||
if (endComponentRef && endComponentRef.getPinPosition) {
|
||||
const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
|
||||
const pinPosition = endComponentRef.getPinPosition(wire.endComponentId);
|
||||
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;
|
||||
wire.endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
|
||||
wire.endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
|
||||
} else {
|
||||
console.warn(`无法获取针脚 ${wire.endComponentId}/${wire.endPinLabel} 的位置`);
|
||||
console.warn(`无法获取组件${wire.endComponentId}的针脚${wire.endComponentId}位置`);
|
||||
// 尝试多次获取位置(针对初次渲染可能有延迟的情况)
|
||||
setTimeout(() => {
|
||||
const retryPosition = endComponentRef.getPinPosition(wire.endComponentId);
|
||||
if (retryPosition) {
|
||||
wire.endX = (retryPosition.x - containerRect.left - position.x) / scale.value;
|
||||
wire.endY = (retryPosition.y - containerRect.top - position.y) / scale.value;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
if (!component) return;
|
||||
|
||||
// 查找与该组件关联的所有连线
|
||||
const relatedWires = wires.value.filter(wire =>
|
||||
|
@ -753,49 +659,14 @@ function updateWiresForComponent(componentId: string) {
|
|||
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;
|
||||
}
|
||||
if (relatedWires.length === 0) 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(() => {
|
||||
// 初始化中心位置
|
||||
|
@ -809,18 +680,6 @@ onMounted(() => {
|
|||
|
||||
// 添加键盘事件监听器
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 添加定期更新连线位置的定时器
|
||||
const wireUpdateInterval = setInterval(() => {
|
||||
if (wires.value.length > 0) {
|
||||
updateAllWires();
|
||||
}
|
||||
}, 1000); // 每秒更新一次所有连线位置
|
||||
|
||||
// 在组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
clearInterval(wireUpdateInterval);
|
||||
});
|
||||
});
|
||||
|
||||
// 处理键盘事件
|
||||
|
@ -860,11 +719,11 @@ onUnmounted(() => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-image:
|
||||
/* background-image:
|
||||
linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
|
||||
linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px); */
|
||||
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
|
||||
background-position: 0 0;
|
||||
user-select: none;
|
||||
|
@ -891,7 +750,7 @@ onUnmounted(() => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
pointer-events: auto; /* 修复:允许线被点击 */
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
|
@ -930,13 +789,13 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
/* 为黑暗模式设置不同的网格线颜色 */
|
||||
:root[data-theme="dark"] .diagram-container {
|
||||
/* :root[data-theme="dark"] .diagram-container {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
|
||||
}
|
||||
} */
|
||||
|
||||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
|
||||
.component-wrapper :deep(svg) {
|
||||
|
@ -961,4 +820,9 @@ onUnmounted(() => {
|
|||
.component-wrapper :deep(input) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.wire-active {
|
||||
stroke: #0099ff !important;
|
||||
filter: drop-shadow(0 0 4px #0099ffcc);
|
||||
}
|
||||
</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>
|
|
@ -76,13 +76,15 @@
|
|||
pointerEvents: 'auto'
|
||||
}">
|
||||
<Pin
|
||||
ref="pinRef"
|
||||
direction="output"
|
||||
type="digital"
|
||||
appearance="None"
|
||||
:label="props.label"
|
||||
:constraint="props.constraint"
|
||||
:size="0.8"
|
||||
:componentId="props.componentId"
|
||||
@value-change="handlePinValueChange"
|
||||
@pin-click="handlePinClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -91,15 +93,18 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import Pin from './Pin.vue';
|
||||
import { notifyConstraintChange } from '../../stores/constraints';
|
||||
|
||||
const pinRef = ref<any>(null);
|
||||
|
||||
// 从Pin组件继承属性
|
||||
interface PinProps {
|
||||
label?: string;
|
||||
constraint?: string;
|
||||
componentId?: string; // 添加componentId属性
|
||||
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
|
||||
direction?: 'input' | 'output' | 'inout';
|
||||
type?: 'digital' | 'analog';
|
||||
appearance?: 'None' | 'Dip' | 'SMT';
|
||||
}
|
||||
|
||||
// 按钮特有属性
|
||||
|
@ -118,10 +123,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
buttonText: '',
|
||||
label: 'BTN',
|
||||
constraint: '',
|
||||
componentId: 'button-default', // 添加默认componentId
|
||||
// 这些值会被覆盖,但需要默认值以满足类型要求
|
||||
direction: 'output',
|
||||
type: 'digital',
|
||||
appearance: 'Dip'
|
||||
type: 'digital'
|
||||
});
|
||||
|
||||
// 计算实际宽高
|
||||
|
@ -142,7 +147,8 @@ const emit = defineEmits([
|
|||
'press',
|
||||
'release',
|
||||
'click',
|
||||
'value-change'
|
||||
'value-change',
|
||||
'pin-click'
|
||||
]);
|
||||
|
||||
// 内部状态
|
||||
|
@ -155,6 +161,11 @@ function handlePinValueChange(value: any) {
|
|||
emit('value-change', value);
|
||||
}
|
||||
|
||||
// 处理Pin点击事件
|
||||
function handlePinClick(info: any) {
|
||||
emit('pin-click', info);
|
||||
}
|
||||
|
||||
// --- 按键状态逻辑 ---
|
||||
function toggleButtonState(isPressed: boolean) {
|
||||
isKeyPressed.value = isPressed;
|
||||
|
@ -163,9 +174,17 @@ function toggleButtonState(isPressed: boolean) {
|
|||
// 发出事件通知父组件
|
||||
if (isPressed) {
|
||||
emit('press');
|
||||
// 如果有约束,通知约束状态变化为高电平
|
||||
if (props.constraint) {
|
||||
notifyConstraintChange(props.constraint, 'high');
|
||||
}
|
||||
} else {
|
||||
emit('release');
|
||||
emit('click');
|
||||
// 如果有约束,通知约束状态变化为低电平
|
||||
if (props.constraint) {
|
||||
notifyConstraintChange(props.constraint, 'low');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,11 +215,18 @@ defineExpose({
|
|||
// 继承自Pin的属性
|
||||
label: props.label,
|
||||
constraint: props.constraint,
|
||||
componentId: props.componentId, // 添加componentId
|
||||
// 固定的Pin属性
|
||||
direction: 'output',
|
||||
type: 'digital',
|
||||
appearance: 'None'
|
||||
})
|
||||
type: 'digital'
|
||||
}),
|
||||
// 代理 getPinPosition 到内部 Pin
|
||||
getPinPosition: (pinLabel: string) => {
|
||||
if (pinRef.value && pinRef.value.getPinPosition) {
|
||||
return pinRef.value.getPinPosition(pinLabel);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,79 +1,35 @@
|
|||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
|
||||
class="pin-component"
|
||||
:data-pin-id="props.label"
|
||||
:data-component-id="props.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)" class="interactive"> <circle
|
||||
style="fill:#909090"
|
||||
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
|
||||
<g>
|
||||
<g transform="translate(-12.5, -12.5)">
|
||||
<circle
|
||||
:style="{ fill: pinColor }"
|
||||
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}`" />
|
||||
</g>
|
||||
</g>
|
||||
<g v-else-if="props.appearance === 'Dip'">
|
||||
<!-- 使用inkscape创建的SVG替代原有Dip样式 -->
|
||||
<g transform="translate(-12.5, -12.5)" class="interactive">
|
||||
<rect
|
||||
:style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
|
||||
width="25"
|
||||
height="25"
|
||||
x="0"
|
||||
y="0"
|
||||
rx="2.5" /> <circle
|
||||
style="fill:#ecececc5;fill-opacity:0.772973"
|
||||
cx="12.5"
|
||||
cy="12.5"
|
||||
r="3.75"
|
||||
@mouseenter="showPinTooltip"
|
||||
@mouseleave="hidePinTooltip"
|
||||
@click.stop="handlePinClick"
|
||||
:data-pin-id="`${props.label}-${props.constraint}`" />
|
||||
<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'">
|
||||
<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
|
||||
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>
|
||||
</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">
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
|
||||
|
||||
// 生成唯一ID
|
||||
const uniqueId = `pin-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
|
@ -81,7 +37,7 @@ interface Props {
|
|||
constraint?: string;
|
||||
direction?: 'input' | 'output' | 'inout';
|
||||
type?: 'digital' | 'analog';
|
||||
appearance?: 'None' | 'Dip' | 'SMT';
|
||||
componentId?: string; // 添加组件ID属性,用于唯一标识
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
@ -90,7 +46,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
constraint: '',
|
||||
direction: 'input',
|
||||
type: 'digital',
|
||||
appearance: 'Dip'
|
||||
componentId: 'pin-default' // 默认ID
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
|
@ -100,58 +56,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) {
|
||||
|
@ -162,42 +66,64 @@ function handlePinClick(event: MouseEvent) {
|
|||
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
|
||||
},
|
||||
// 获取原始事件
|
||||
position: pinCenter,
|
||||
originalEvent: event
|
||||
});
|
||||
}
|
||||
|
||||
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
|
||||
const height = computed(() => {
|
||||
if (props.appearance === 'None') return 20 * props.size;
|
||||
if (props.appearance === 'Dip') return 30 * props.size; // 调整Dip样式高度
|
||||
return 60 * props.size;
|
||||
});
|
||||
const viewBoxWidth = computed(() => props.appearance === 'None' ? 40 : 30);
|
||||
const viewBoxHeight = computed(() => {
|
||||
if (props.appearance === 'None') return 20;
|
||||
if (props.appearance === 'Dip') return 30; // 调整Dip样式视图高度
|
||||
return 60;
|
||||
});
|
||||
const width = computed(() => 40 * props.size);
|
||||
const height = computed(() => 20 * props.size);
|
||||
const viewBoxWidth = computed(() => 40);
|
||||
const viewBoxHeight = computed(() => 20);
|
||||
|
||||
const getColorByType = computed(() => {
|
||||
return props.type === 'analog' ? '#2a6099' : '#444';
|
||||
});
|
||||
|
||||
// 根据约束电平状态计算引脚颜色
|
||||
const pinColor = computed(() => {
|
||||
return getConstraintColor(props.constraint) || getColorByType.value;
|
||||
});
|
||||
|
||||
// 监听约束状态变化
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
// 监听约束状态变化
|
||||
if (props.constraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === props.constraint) {
|
||||
emit('value-change', { constraint, level });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.constraint, (newConstraint, oldConstraint) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
if (newConstraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === newConstraint) {
|
||||
emit('value-change', { constraint, level });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateAnalogValue(value: number) {
|
||||
if (props.type !== 'analog') return;
|
||||
analogValue.value = Math.max(0, Math.min(1, value));
|
||||
|
@ -216,46 +142,39 @@ defineExpose({
|
|||
constraint: props.constraint,
|
||||
direction: props.direction,
|
||||
type: props.type,
|
||||
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();
|
||||
componentId: props.componentId
|
||||
}),
|
||||
getPinPosition: (componentId: string) => {
|
||||
if (componentId !== props.componentId) return null;
|
||||
console.log('getPinPosition', componentId, props.componentId);
|
||||
const uniqueSelector = `[data-pin-element="${props.componentId}"]`;
|
||||
console.log('uniqueSelector', uniqueSelector);
|
||||
const pinElements = document.querySelectorAll(uniqueSelector);
|
||||
console.log('pinElements', pinElements);
|
||||
if (pinElements.length === 0) return null;
|
||||
if (pinElements.length === 1) {
|
||||
const rect = pinElements[0].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;
|
||||
for (const pinElement of pinElements) {
|
||||
let parentSvg = pinElement.closest('svg.pin-component');
|
||||
if (!parentSvg) continue;
|
||||
if (parentSvg.getAttribute('data-component-id') === props.componentId) {
|
||||
const rect = pinElement.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
const rect = pinElements[0].getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -273,15 +192,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>
|
||||
|
|
|
@ -14,16 +14,15 @@
|
|||
<rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
|
||||
|
||||
<!-- LED 发光部分 -->
|
||||
<rect
|
||||
<rect
|
||||
width="70"
|
||||
height="30"
|
||||
x="15"
|
||||
y="15"
|
||||
:fill="ledColor"
|
||||
:style="{ opacity: isOn ? brightness/100 : 0.2 }"
|
||||
:style="{ opacity: isOn ? 1 : 0.2 }"
|
||||
rx="15"
|
||||
ry="15"
|
||||
@click="toggleLed"
|
||||
class="interactive"
|
||||
/>
|
||||
|
||||
|
@ -35,46 +34,64 @@
|
|||
x="12"
|
||||
y="12"
|
||||
:fill="ledColor"
|
||||
:style="{ opacity: brightness/100 * 0.3 }"
|
||||
:style="{ opacity: 0.3 }"
|
||||
rx="18"
|
||||
ry="18"
|
||||
filter="blur(5px)"
|
||||
class="glow"
|
||||
/>
|
||||
</svg>
|
||||
<!-- 新增:数字输入引脚Pin,放在LED左侧居中 -->
|
||||
<div style="position:absolute;left:-18px;top:50%;transform:translateY(-50%);">
|
||||
<Pin
|
||||
ref="pinRef"
|
||||
v-bind="props"
|
||||
@pin-click="$emit('pin-click', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
|
||||
import Pin from './Pin.vue';
|
||||
|
||||
// LED特有属性
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
initialOn?: boolean;
|
||||
brightness?: number;
|
||||
// --- 关键:暴露getPinPosition,代理到内部Pin ---
|
||||
const pinRef = ref<any>(null);
|
||||
|
||||
// 从Pin组件继承属性
|
||||
interface PinProps {
|
||||
label?: string;
|
||||
constraint?: string;
|
||||
componentId?: string; // 添加componentId属性
|
||||
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
|
||||
direction?: 'input' | 'output' | 'inout';
|
||||
type?: 'digital' | 'analog';
|
||||
}
|
||||
|
||||
// 组件属性定义
|
||||
// LED特有属性
|
||||
interface LEDProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
// 组合两个接口
|
||||
interface Props extends PinProps, LEDProps {}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 1,
|
||||
color: 'red',
|
||||
initialOn: false,
|
||||
brightness: 80, // 亮度默认为80%
|
||||
constraint: ''
|
||||
constraint: '',
|
||||
label: 'LED',
|
||||
componentId: ''
|
||||
});
|
||||
|
||||
// 计算实际宽高
|
||||
const width = computed(() => 100 * props.size);
|
||||
const height = computed(() => 60 * props.size);
|
||||
|
||||
// 内部状态
|
||||
const isOn = ref(props.initialOn);
|
||||
const brightness = ref(props.brightness);
|
||||
const isOn = ref(false);
|
||||
|
||||
// LED 颜色映射表
|
||||
const colorMap: Record<string, string> = {
|
||||
'red': '#ff3333',
|
||||
'green': '#33ff33',
|
||||
|
@ -85,70 +102,64 @@ const colorMap: Record<string, string> = {
|
|||
'purple': '#9933ff'
|
||||
};
|
||||
|
||||
// 计算实际LED颜色
|
||||
const ledColor = computed(() => {
|
||||
return colorMap[props.color.toLowerCase()] || props.color;
|
||||
});
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits([
|
||||
'toggle',
|
||||
'brightness-change',
|
||||
'value-change'
|
||||
]);
|
||||
// 监听约束状态变化
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
// 手动切换LED状态
|
||||
function toggleLed() {
|
||||
isOn.value = !isOn.value;
|
||||
emit('toggle', isOn.value);
|
||||
emit('value-change', {
|
||||
isOn: isOn.value,
|
||||
brightness: brightness.value
|
||||
});
|
||||
}
|
||||
|
||||
// 设置亮度
|
||||
function setBrightness(value: number) {
|
||||
// 限制亮度值在0-100范围内
|
||||
brightness.value = Math.max(0, Math.min(100, value));
|
||||
emit('brightness-change', brightness.value);
|
||||
emit('value-change', {
|
||||
isOn: isOn.value,
|
||||
brightness: brightness.value
|
||||
});
|
||||
}
|
||||
|
||||
// 手动设置LED开关状态
|
||||
function setLedState(on: boolean) {
|
||||
isOn.value = on;
|
||||
emit('toggle', isOn.value);
|
||||
emit('value-change', {
|
||||
isOn: isOn.value,
|
||||
brightness: brightness.value
|
||||
});
|
||||
}
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.brightness, (newVal) => {
|
||||
brightness.value = newVal;
|
||||
onMounted(() => {
|
||||
if (props.constraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === props.constraint) {
|
||||
isOn.value = (level === 'high');
|
||||
}
|
||||
});
|
||||
// 初始化LED状态
|
||||
const currentState = getConstraintState(props.constraint);
|
||||
isOn.value = (currentState === 'high');
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.initialOn, (newVal) => {
|
||||
isOn.value = newVal;
|
||||
onUnmounted(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.constraint, (newConstraint) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
if (newConstraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === newConstraint) {
|
||||
isOn.value = (level === 'high');
|
||||
}
|
||||
});
|
||||
// 初始化LED状态
|
||||
const currentState = getConstraintState(newConstraint);
|
||||
isOn.value = (currentState === 'high');
|
||||
}
|
||||
});
|
||||
|
||||
// 向外暴露方法
|
||||
defineExpose({
|
||||
toggleLed,
|
||||
setBrightness,
|
||||
setLedState,
|
||||
getInfo: () => ({
|
||||
// LED特有属性
|
||||
color: props.color,
|
||||
isOn: isOn.value,
|
||||
brightness: brightness.value,
|
||||
constraint: props.constraint
|
||||
})
|
||||
constraint: props.constraint,
|
||||
componentId: props.componentId,
|
||||
direction: 'input',
|
||||
type: 'digital'
|
||||
}),
|
||||
getPinPosition: (componentId: string) => {
|
||||
if (pinRef.value && pinRef.value.getPinPosition) {
|
||||
return pinRef.value.getPinPosition(componentId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
<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, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
|
||||
|
||||
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 constraintColor = computed(() => {
|
||||
if (!props.constraint) return props.strokeColor;
|
||||
return getConstraintColor(props.constraint);
|
||||
});
|
||||
|
||||
// 计算实际使用的颜色:isActive优先,其次是constraint电平颜色,最后是默认色
|
||||
const computedStroke = computed(() => {
|
||||
if (props.isActive) return '#ff9800';
|
||||
return constraintColor.value || 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}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听约束状态变化
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
// 监听约束状态变化
|
||||
if (props.constraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === props.constraint) {
|
||||
// 约束状态变化,触发重新渲染
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理监听
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听约束属性变化
|
||||
watch(() => props.constraint, (newConstraint, oldConstraint) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
|
||||
if (newConstraint) {
|
||||
unsubscribe = onConstraintStateChange((constraint, level) => {
|
||||
if (constraint === newConstraint) {
|
||||
// 约束状态变化,触发重新渲染
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法,用于获取这条连线的信息
|
||||
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>
|
|
@ -154,18 +154,6 @@ const componentConfigs: Record<string, ComponentConfig> = {
|
|||
{ value: 'analog', label: 'analog' }
|
||||
],
|
||||
description: '引脚的模数特性,数字或模拟'
|
||||
},
|
||||
{
|
||||
name: 'appearance',
|
||||
type: 'select',
|
||||
label: '引脚样式',
|
||||
default: 'Dip',
|
||||
options: [
|
||||
{ value: 'None', label: 'None' },
|
||||
{ value: 'Dip', label: 'Dip' },
|
||||
{ value: 'SMT', label: 'SMT' }
|
||||
],
|
||||
description: '引脚的外观样式,不影响功能'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -313,12 +301,60 @@ const componentConfigs: Record<string, ComponentConfig> = {
|
|||
{
|
||||
name: 'constraint',
|
||||
type: 'string',
|
||||
label: '连接约束',
|
||||
label: '引脚约束',
|
||||
default: '',
|
||||
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: '是否显示连线上的约束标签'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
// 获取组件配置的函数
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { ref, reactive } from 'vue';
|
||||
|
||||
export interface WireItem {
|
||||
id: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
startComponentId: string;
|
||||
endComponentId?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
constraint?: string;
|
||||
strokeWidth?: number;
|
||||
routingMode?: 'auto' | 'orthogonal' | 'direct';
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
// 全局wires状态
|
||||
export const wires = ref<WireItem[]>([]);
|
||||
|
||||
// 添加新连线
|
||||
export function addWire(wire: WireItem) {
|
||||
wires.value.push(wire);
|
||||
}
|
||||
|
||||
// 删除连线
|
||||
export function deleteWire(wireId: string) {
|
||||
const idx = wires.value.findIndex(w => w.id === wireId);
|
||||
if (idx !== -1) wires.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
// 批量更新与某个引脚相关的所有Wire的约束
|
||||
export function updateWiresConstraintByPin(componentId: string, newConstraint: string) {
|
||||
wires.value.forEach(wire => {
|
||||
if (
|
||||
(wire.startComponentId === componentId) ||
|
||||
(wire.endComponentId === componentId)
|
||||
) {
|
||||
wire.constraint = newConstraint;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 查找与某个引脚相关的所有Wire
|
||||
export function findWiresByPin(componentId: string) {
|
||||
return wires.value.filter(wire =>
|
||||
(wire.startComponentId === componentId) ||
|
||||
(wire.endComponentId === componentId)
|
||||
);
|
||||
}
|
||||
|
||||
// 临时连线相关状态
|
||||
export const isCreatingWire = ref(false);
|
||||
export const creatingWireStart = reactive({ x: 0, y: 0 });
|
||||
export const creatingWireStartInfo = reactive({
|
||||
componentId: '',
|
||||
pinLabel: '',
|
||||
constraint: ''
|
||||
});
|
||||
export const mousePosition = reactive({ x: 0, y: 0 });
|
||||
|
||||
export function resetWireCreation() {
|
||||
isCreatingWire.value = false;
|
||||
creatingWireStart.x = 0;
|
||||
creatingWireStart.y = 0;
|
||||
creatingWireStartInfo.componentId = '';
|
||||
creatingWireStartInfo.pinLabel = '';
|
||||
creatingWireStartInfo.constraint = '';
|
||||
}
|
||||
|
||||
export function setWireCreationStart(x: number, y: number, componentId: string, pinLabel: string, constraint: string) {
|
||||
isCreatingWire.value = true;
|
||||
creatingWireStart.x = x;
|
||||
creatingWireStart.y = y;
|
||||
creatingWireStartInfo.componentId = componentId;
|
||||
creatingWireStartInfo.pinLabel = pinLabel;
|
||||
creatingWireStartInfo.constraint = constraint;
|
||||
}
|
||||
|
||||
export function setMousePosition(x: number, y: number) {
|
||||
mousePosition.x = x;
|
||||
mousePosition.y = y;
|
||||
}
|
||||
|
||||
// 其它Wire相关操作可继续扩展...
|
|
@ -5,6 +5,10 @@ import { createPinia } from 'pinia'
|
|||
|
||||
import App from '@/App.vue'
|
||||
import router from './router'
|
||||
import { initConstraintCommunication } from './APIClient'
|
||||
|
||||
const app = createApp(App).use(router).use(createPinia()).mount('#app')
|
||||
|
||||
// 初始化约束通信
|
||||
initConstraintCommunication();
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { ref, reactive } from 'vue';
|
||||
|
||||
// 约束电平状态类型
|
||||
export type ConstraintLevel = 'high' | 'low' | 'undefined';
|
||||
|
||||
// 约束状态存储
|
||||
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
|
||||
|
||||
// 约束颜色映射
|
||||
export const constraintColors = {
|
||||
high: '#ff3333', // 高电平为红色
|
||||
low: '#3333ff', // 低电平为蓝色
|
||||
undefined: '#999999' // 未定义为灰色
|
||||
};
|
||||
|
||||
// 获取约束状态
|
||||
export function getConstraintState(constraint: string): ConstraintLevel {
|
||||
if (!constraint) return 'undefined';
|
||||
return constraintStates[constraint] || 'undefined';
|
||||
}
|
||||
|
||||
// 设置约束状态
|
||||
export function setConstraintState(constraint: string, level: ConstraintLevel) {
|
||||
if (!constraint) return;
|
||||
constraintStates[constraint] = level;
|
||||
}
|
||||
|
||||
// 批量设置约束状态
|
||||
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) {
|
||||
// 收集发生变化的约束
|
||||
const changedConstraints: [string, ConstraintLevel][] = [];
|
||||
|
||||
// 更新状态并收集变化
|
||||
Object.entries(states).forEach(([constraint, level]) => {
|
||||
if (constraintStates[constraint] !== level) {
|
||||
constraintStates[constraint] = level;
|
||||
changedConstraints.push([constraint, level]);
|
||||
}
|
||||
});
|
||||
|
||||
// 通知所有变化
|
||||
changedConstraints.forEach(([constraint, level]) => {
|
||||
stateChangeCallbacks.forEach(callback => callback(constraint, level));
|
||||
});
|
||||
}
|
||||
|
||||
// 获取约束对应的颜色
|
||||
export function getConstraintColor(constraint: string): string {
|
||||
const state = getConstraintState(constraint);
|
||||
return constraintColors[state];
|
||||
}
|
||||
|
||||
// 清除所有约束状态
|
||||
export function clearAllConstraintStates() {
|
||||
Object.keys(constraintStates).forEach(key => {
|
||||
delete constraintStates[key];
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有约束状态
|
||||
export function getAllConstraintStates(): Record<string, ConstraintLevel> {
|
||||
return { ...constraintStates };
|
||||
}
|
||||
|
||||
// 注册约束状态变化回调
|
||||
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
|
||||
|
||||
export function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
|
||||
stateChangeCallbacks.push(callback);
|
||||
return () => {
|
||||
const index = stateChangeCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
stateChangeCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 触发约束变化
|
||||
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
|
||||
setConstraintState(constraint, level);
|
||||
stateChangeCallbacks.forEach(callback => callback(constraint, level));
|
||||
}
|
|
@ -50,7 +50,7 @@
|
|||
type="text"
|
||||
:placeholder="prop.label || prop.name"
|
||||
class="input input-bordered input-sm w-full"
|
||||
:value="selectedComponentData.props[prop.name]"
|
||||
:value="selectedComponentData.props?.[prop.name]"
|
||||
@input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
|
@ -58,14 +58,14 @@
|
|||
type="number"
|
||||
:placeholder="prop.label || prop.name"
|
||||
class="input input-bordered input-sm w-full"
|
||||
:value="selectedComponentData.props[prop.name]"
|
||||
:value="selectedComponentData.props?.[prop.name]"
|
||||
@input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
|
||||
/> <!-- 可以为 boolean 添加 checkbox,为 color 添加 color picker 等 -->
|
||||
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm mr-2"
|
||||
:checked="selectedComponentData.props[prop.name]"
|
||||
:checked="selectedComponentData.props?.[prop.name]"
|
||||
@change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span>{{ prop.label || prop.name }}</span>
|
||||
|
@ -73,12 +73,12 @@
|
|||
<select
|
||||
v-else-if="prop.type === 'select' && prop.options"
|
||||
class="select select-bordered select-sm w-full"
|
||||
:value="selectedComponentData.props[prop.name]"
|
||||
:value="selectedComponentData.props?.[prop.name]"
|
||||
@change="(event) => {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
const value = selectElement.value;
|
||||
console.log('选择的值:', value, '类型:', typeof value);
|
||||
updateComponentProp(selectedComponentData.id, prop.name, value);
|
||||
if (selectedComponentData) {updateComponentProp(selectedComponentData.id, prop.name, value);}
|
||||
}"
|
||||
>
|
||||
<option v-for="option in prop.options" :key="option.value" :value="option.value">
|
||||
|
@ -143,6 +143,7 @@ interface ComponentModule {
|
|||
type: string;
|
||||
label?: string;
|
||||
default: any;
|
||||
options?: Array<{ value: any; label: string }>; // 添加 options 字段用于 select 类型
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
@ -344,7 +345,7 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
|
|||
// 处理连线创建事件
|
||||
function handleWireCreated(wireData: any) {
|
||||
console.log('Wire created:', wireData);
|
||||
// 可以在这里添加连线创建的相关逻辑
|
||||
// 连线已在DiagramCanvas.vue中完成约束处理
|
||||
}
|
||||
|
||||
// 处理连线删除事件
|
||||
|
|
Loading…
Reference in New Issue