307 lines
8.6 KiB
Vue
307 lines
8.6 KiB
Vue
<template>
|
||
<div class="button-container" :style="{
|
||
width: width + 'px',
|
||
height: height + 'px',
|
||
position: 'relative',
|
||
}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="400 400 800 800"
|
||
class="mechanical-button">
|
||
<!-- defs 和按钮底座保持不变 -->
|
||
<defs>
|
||
<filter id="btn-shadow">
|
||
<feGaussianBlur in="SourceAlpha" stdDeviation="20" result="blur" />
|
||
<feColorMatrix result="bluralpha" type="matrix" :values="colorMatrix" />
|
||
<feOffset in="bluralpha" dx="20" dy="20" result="offsetBlur" />
|
||
<feMerge>
|
||
<feMergeNode in="offsetBlur" />
|
||
<feMergeNode in="SourceGraphic" />
|
||
</feMerge>
|
||
</filter>
|
||
<linearGradient id="normal" gradientTransform="rotate(45 0 0)">
|
||
<stop stop-color="#4b4b4b" offset="0" />
|
||
<stop stop-color="#171717" offset="1" />
|
||
</linearGradient>
|
||
<linearGradient id="pressed" gradientTransform="rotate(45 0 0)">
|
||
<stop stop-color="#171717" offset="0" />
|
||
<stop stop-color="#4b4b4b" offset="1" />
|
||
</linearGradient>
|
||
</defs>
|
||
<!-- 按钮底座 -->
|
||
<rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" />
|
||
<rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
|
||
|
||
<!-- 装饰螺丝 -->
|
||
<circle r="20" cx="1075" cy="1075" fill="#171717" />
|
||
<circle r="20" cx="1075" cy="525" fill="#171717" />
|
||
<circle r="20" cx="525" cy="525" fill="#171717" />
|
||
<circle r="20" cx="525" cy="1075" fill="#171717" />
|
||
|
||
<!-- 按钮主体 -->
|
||
<circle r="220" cx="800" cy="800" fill="black" filter="url(#btn-shadow)" />
|
||
<circle :r="btnHeight" cx="800" cy="800" :fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
|
||
fill-opacity="0.9" @mousedown="toggleButtonState(true)" @mouseup="toggleButtonState(false)"
|
||
@mouseleave="toggleButtonState(false)" style="
|
||
pointer-events: auto;
|
||
transition: all 20ms ease-in-out;
|
||
cursor: pointer;
|
||
" />
|
||
<!-- 按键文字 - 仅显示绑定的按键 -->
|
||
<text v-if="bindKeyDisplay" x="800" y="800" font-size="310" text-anchor="middle" dominant-baseline="central"
|
||
fill="#ccc" style="
|
||
font-family: Arial;
|
||
filter: url(#btn-shadow);
|
||
user-select: none;
|
||
pointer-events: none;
|
||
mix-blend-mode: overlay;
|
||
">
|
||
{{ bindKeyDisplay }}
|
||
</text>
|
||
</svg>
|
||
|
||
<!-- 渲染自定义引脚数组 -->
|
||
<div v-for="pin in props.pins" :key="pin.pinId" :style="{
|
||
position: 'absolute',
|
||
left: `${pin.x * props.size}px`,
|
||
top: `${pin.y * props.size}px`,
|
||
transform: 'translate(-50%, -50%)',
|
||
zIndex: 3,
|
||
pointerEvents: 'auto',
|
||
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
|
||
:data-pin-y="`${pin.y * props.size}`">
|
||
<Pin :ref="(el) => {
|
||
if (el) pinRefs[pin.pinId] = el;
|
||
}
|
||
" direction="output" type="digital" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId"
|
||
:size="0.8" :componentId="props.componentId" @value-change="handlePinValueChange" @pin-click="handlePinClick" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||
import Pin from "./Pin.vue";
|
||
import { useEquipments } from "@/stores/equipments";
|
||
import { useDialogStore } from "@/stores/dialog";
|
||
import { useConstraintsStore } from "../../stores/constraints";
|
||
import { isNull, isUndefined } from "mathjs";
|
||
import z from "zod";
|
||
import { toNumber } from "lodash";
|
||
|
||
// 按钮特有属性
|
||
export interface ButtonProps {
|
||
size: number;
|
||
componentId?: string;
|
||
pins?: {
|
||
pinId: string;
|
||
constraint: string;
|
||
x: number;
|
||
y: number;
|
||
}[];
|
||
|
||
bindKey?: string;
|
||
bindMatrixKey?: string;
|
||
}
|
||
|
||
const props = defineProps<ButtonProps>();
|
||
|
||
// Global Stores
|
||
const constrainsts = useConstraintsStore();
|
||
const dialog = useDialogStore();
|
||
const eqps = useEquipments();
|
||
|
||
// 存储多个Pin引用
|
||
const pinRefs = ref<Record<string, any>>({});
|
||
|
||
// 计算实际宽高
|
||
const width = computed(() => 160 * props.size);
|
||
const height = computed(() => 160 * props.size);
|
||
|
||
// 显示绑定的按键
|
||
const bindKeyDisplay = computed(() =>
|
||
props.bindKey ? props.bindKey.toUpperCase() : "",
|
||
);
|
||
|
||
// 定义组件发出的事件
|
||
const emit = defineEmits([
|
||
"update:bindKey",
|
||
"update:constraint",
|
||
"press",
|
||
"release",
|
||
"click",
|
||
"value-change",
|
||
"pin-click",
|
||
]);
|
||
|
||
// 内部状态
|
||
const isKeyPressed = ref(false);
|
||
const btnHeight = ref(200);
|
||
const colorMatrix = ref("1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0");
|
||
|
||
// 处理Pin值变化
|
||
function handlePinValueChange(value: any) {
|
||
emit("value-change", value);
|
||
}
|
||
|
||
// 处理Pin点击事件
|
||
function handlePinClick(info: any) {
|
||
emit("pin-click", info);
|
||
}
|
||
|
||
// --- 按键状态逻辑 ---
|
||
function toggleButtonState(isPressed: boolean) {
|
||
isKeyPressed.value = isPressed;
|
||
btnHeight.value = isPressed ? 180 : 200;
|
||
|
||
// 矩阵键盘
|
||
if (eqps.enableMatrixKey) {
|
||
const ret = eqps.setMatrixKey(props.bindMatrixKey, isPressed);
|
||
|
||
if (ret) eqps.matrixKeypadSetKeyStates(eqps.matrixKeyStates);
|
||
else
|
||
dialog.error(
|
||
`绑定的矩阵键盘值只能是0 ~ 15,而不是: ${props.bindMatrixKey}`,
|
||
);
|
||
}
|
||
|
||
// 发出事件通知父组件
|
||
if (isPressed) {
|
||
emit("press");
|
||
|
||
if (props.pins) {
|
||
// 如果有约束,通知约束状态变化为高电平
|
||
// 对所有引脚应用相同的状态
|
||
props.pins.forEach((pin) => {
|
||
if (pin.constraint) {
|
||
constrainsts.notifyConstraintChange(pin.constraint, "high");
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
emit("release");
|
||
emit("click");
|
||
// 如果有约束,通知约束状态变化为低电平
|
||
if (props.pins) {
|
||
props.pins.forEach((pin) => {
|
||
if (pin.constraint) {
|
||
constrainsts.notifyConstraintChange(pin.constraint, "low");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理键盘事件
|
||
function handleKeyDown(event: KeyboardEvent) {
|
||
if (event.key === props.bindKey) {
|
||
toggleButtonState(true);
|
||
setTimeout(() => toggleButtonState(false), 150);
|
||
}
|
||
}
|
||
|
||
// --- 生命周期钩子 ---
|
||
onMounted(() => {
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
document.removeEventListener("keydown", handleKeyDown);
|
||
});
|
||
|
||
// 向外暴露方法
|
||
defineExpose({
|
||
toggleButtonState,
|
||
getInfo: () => ({
|
||
bindKey: props.bindKey,
|
||
componentId: props.componentId,
|
||
pins: props.pins,
|
||
}),
|
||
// 获取引脚位置
|
||
getPinPosition: (pinId: string) => {
|
||
console.debug(`[MechanicalButton] 调用getPinPosition,寻找pinId: ${pinId}`);
|
||
console.debug(
|
||
`[MechanicalButton] 组件ID: ${props.componentId}, 当前尺寸: ${props.size}, 组件宽高: ${width.value}x${height.value}`,
|
||
);
|
||
console.debug(`[MechanicalButton] 当前存在的pins:`, props.pins);
|
||
|
||
// 如果是自定义的引脚ID
|
||
if (props.pins && props.pins.length > 0) {
|
||
const customPin = props.pins.find((p) => p.pinId === pinId);
|
||
|
||
if (customPin) {
|
||
console.debug(
|
||
`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`,
|
||
{
|
||
x: customPin.x,
|
||
y: customPin.y,
|
||
},
|
||
);
|
||
|
||
// 考虑组件尺寸的缩放
|
||
// 这里的x和y是针对标准尺寸(size=1)的坐标,需要根据实际size调整
|
||
const scaledX = customPin.x * props.size;
|
||
const scaledY = customPin.y * props.size;
|
||
|
||
console.debug(`[MechanicalButton] 返回缩放后的坐标:`, {
|
||
x: scaledX,
|
||
y: scaledY,
|
||
});
|
||
return {
|
||
x: scaledX,
|
||
y: scaledY,
|
||
};
|
||
} else {
|
||
console.debug(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
|
||
}
|
||
} else {
|
||
console.debug(`[MechanicalButton] 没有配置任何引脚`);
|
||
}
|
||
console.debug(`[MechanicalButton] 返回null,未找到引脚`);
|
||
return null;
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
// 添加一个静态方法来获取默认props
|
||
export function getDefaultProps() {
|
||
return {
|
||
size: 1,
|
||
pins: [
|
||
{
|
||
pinId: "BTN",
|
||
constraint: "",
|
||
x: 80,
|
||
y: 140,
|
||
},
|
||
],
|
||
bindKey: "",
|
||
bindMatrixKey: "",
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.button-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
position: relative;
|
||
}
|
||
|
||
.mechanical-button {
|
||
display: block;
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: 0;
|
||
font-size: 0;
|
||
box-sizing: content-box;
|
||
overflow: visible;
|
||
}
|
||
|
||
.pin-wrapper {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
</style>
|