447 lines
14 KiB
Vue
447 lines
14 KiB
Vue
<template>
|
||
<div class="seven-segment-display" :style="{
|
||
width: width + 'px',
|
||
height: height + 'px',
|
||
position: 'relative',
|
||
}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
|
||
<!-- 数码管基座 -->
|
||
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
|
||
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||
<!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
|
||
<!-- a段 (顶部横线) -->
|
||
<polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
|
||
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- b段 (右上竖线) -->
|
||
<polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
|
||
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- c段 (右下竖线) -->
|
||
<polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
|
||
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- d段 (底部横线) -->
|
||
<polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
|
||
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- e段 (左下竖线) -->
|
||
<polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
|
||
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- f段 (左上竖线) -->
|
||
<polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
|
||
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
|
||
|
||
<!-- g段 (中间横线) -->
|
||
<polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
|
||
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
|
||
<!-- dp段 (小数点) -->
|
||
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
|
||
</svg>
|
||
|
||
<!-- 引脚 -->
|
||
<div v-for="pin in pins" :key="pin.pinId" :style="{
|
||
position: 'absolute',
|
||
left: `${pin.x * props.size}px`,
|
||
top: `${pin.y * props.size}px`,
|
||
transform: 'translate(-50%, -50%)',
|
||
}" :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;
|
||
}
|
||
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||
import { useConstraintsStore } from "../../stores/constraints";
|
||
import Pin from "./Pin.vue";
|
||
|
||
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
|
||
|
||
// 存储Pin引用
|
||
const pinRefs = ref<Record<string, any>>({});
|
||
|
||
// 数码管属性
|
||
interface SevenSegmentDisplayProps {
|
||
size?: number;
|
||
color?: string;
|
||
AFTERGLOW_BUFFER_SIZE?: number; // 余晖存储槽大小
|
||
AFTERGLOW_DURATION?: number; // 余晖持续时间(毫秒)
|
||
pins?: {
|
||
pinId: string;
|
||
constraint: string;
|
||
x: number;
|
||
y: number;
|
||
}[];
|
||
cathodeType?: "common" | "anode"; // 共阴极或共阳极
|
||
}
|
||
|
||
const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
|
||
size: 1,
|
||
color: "red",
|
||
AFTERGLOW_BUFFER_SIZE: 1, // 默认存储槽大小为100
|
||
AFTERGLOW_DURATION: 2000, // 默认余晖持续时间500毫秒
|
||
cathodeType: "common", // 默认为共阴极
|
||
pins: () => [
|
||
{ pinId: "a", constraint: "", x: 10, y: 170 }, // a段
|
||
{ pinId: "b", constraint: "", x: 25 - 1, y: 170 }, // b段
|
||
{ pinId: "c", constraint: "", x: 40 - 2, y: 170 }, // c段
|
||
{ pinId: "d", constraint: "", x: 55 - 3, y: 170 }, // d段
|
||
{ pinId: "e", constraint: "", x: 70 - 4, y: 170 }, // e段
|
||
{ pinId: "f", constraint: "", x: 85 - 5, y: 170 }, // f段
|
||
{ pinId: "g", constraint: "", x: 100 - 6, y: 170 }, // g段
|
||
{ pinId: "dp", constraint: "", x: 115 - 7, y: 170 }, // 小数点
|
||
{ pinId: "COM", constraint: "", x: 60, y: 10 }, // 公共端,稍微低一点
|
||
],
|
||
});
|
||
|
||
const width = computed(() => 120 * props.size);
|
||
const height = computed(() => 220 * props.size);
|
||
|
||
// 计算段颜色和非激活状态颜色
|
||
const segmentColor = computed(() => props.color || "red");
|
||
const inactiveColor = computed(() => "#FFFFFF");
|
||
|
||
// 监听props变化
|
||
watch(
|
||
() => props,
|
||
(newProps) => {
|
||
console.log("SevenSegmentDisplay props changed:", newProps);
|
||
updateSegmentStates();
|
||
},
|
||
{ deep: true },
|
||
);
|
||
|
||
// 段引脚的当前状态
|
||
const segmentStates = ref({
|
||
a: false,
|
||
b: false,
|
||
c: false,
|
||
d: false,
|
||
e: false,
|
||
f: false,
|
||
g: false,
|
||
dp: false,
|
||
});
|
||
|
||
// 余晖存储槽 - 每个段有一个存储槽数组,存储历史状态
|
||
const afterglowBuffers = ref<Record<string, boolean[]>>({
|
||
a: [],
|
||
b: [],
|
||
c: [],
|
||
d: [],
|
||
e: [],
|
||
f: [],
|
||
g: [],
|
||
dp: [],
|
||
});
|
||
|
||
// 余晖判定阈值(比如持续10帧才算稳定,可根据刷新间隔和人眼余晖调节)
|
||
const STABLE_THRESHOLD = 3;
|
||
|
||
// 实际显示的段状态(只有稳定后才改变)
|
||
const stableSegmentStates = ref({
|
||
a: false,
|
||
b: false,
|
||
c: false,
|
||
d: false,
|
||
e: false,
|
||
f: false,
|
||
g: false,
|
||
dp: false,
|
||
});
|
||
|
||
// 每段的稳定计数器
|
||
const segmentStableCounters = ref<Record<string, number>>({
|
||
a: 0,
|
||
b: 0,
|
||
c: 0,
|
||
d: 0,
|
||
e: 0,
|
||
f: 0,
|
||
g: 0,
|
||
dp: 0,
|
||
});
|
||
|
||
// 段选关闭时的余晖状态保持
|
||
const afterglowStates = ref({
|
||
a: false,
|
||
b: false,
|
||
c: false,
|
||
d: false,
|
||
e: false,
|
||
f: false,
|
||
g: false,
|
||
dp: false,
|
||
});
|
||
|
||
// 余晖计时器
|
||
const afterglowTimers = ref<Record<string, number | null>>({
|
||
a: null,
|
||
b: null,
|
||
c: null,
|
||
d: null,
|
||
e: null,
|
||
f: null,
|
||
g: null,
|
||
dp: null,
|
||
});
|
||
|
||
// 余晖持续时间(毫秒)
|
||
const AFTERGLOW_DURATION = computed(() => props.AFTERGLOW_DURATION || 500);
|
||
|
||
// 当前COM口状态
|
||
const currentComActive = ref(false); // 初始化为false,等待第一次状态检查
|
||
|
||
// 是否处于余晖模式
|
||
const isInAfterglowMode = ref(false);
|
||
|
||
// 判断段是否激活(用稳定状态或余晖状态)
|
||
function isSegmentActive(
|
||
segment: "a" | "b" | "c" | "d" | "e" | "f" | "g" | "dp",
|
||
): boolean {
|
||
// 如果处于余晖模式,使用余晖状态
|
||
if (isInAfterglowMode.value) {
|
||
return afterglowStates.value[segment];
|
||
}
|
||
|
||
// 如果COM口未激活,所有段都不显示
|
||
if (!currentComActive.value) {
|
||
return false;
|
||
}
|
||
|
||
// 否则使用稳定状态
|
||
return stableSegmentStates.value[segment];
|
||
}
|
||
|
||
// 更新引脚状态的函数
|
||
function updateSegmentStates() {
|
||
// 先获取COM口状态
|
||
const comPin = props.pins.find((p) => p.pinId === "COM");
|
||
let comActive = false; // 默认未激活
|
||
|
||
if (comPin && comPin.constraint) {
|
||
const comState = getConstraintState(comPin.constraint);
|
||
if (props.cathodeType === "anode") {
|
||
// 共阳极模式下,COM为低电平才激活
|
||
comActive = comState === "low";
|
||
} else {
|
||
// 共阴极模式下,COM为低电平才激活
|
||
comActive = comState === "low";
|
||
}
|
||
} else if (!comPin || !comPin.constraint) {
|
||
// 如果没有COM引脚或者COM引脚没有约束,则认为始终激活
|
||
comActive = true;
|
||
}
|
||
|
||
// 检查COM口状态是否发生变化
|
||
const comStateChanged = currentComActive.value !== comActive;
|
||
currentComActive.value = comActive;
|
||
|
||
// 如果COM从激活变为非激活,进入余晖模式
|
||
if (comStateChanged && !comActive) {
|
||
enterAfterglowMode();
|
||
return; // 在余晖模式下,不处理其他引脚变化
|
||
}
|
||
|
||
// 如果COM从非激活变为激活,退出余晖模式
|
||
if (comStateChanged && comActive) {
|
||
exitAfterglowMode();
|
||
}
|
||
|
||
// 关键修复:如果COM口未激活或处于余晖模式,不处理任何引脚状态变化
|
||
if (!comActive || isInAfterglowMode.value) {
|
||
return;
|
||
}
|
||
|
||
// 只有当COM口激活时,才更新段的状态
|
||
updateAfterglowBuffers();
|
||
|
||
// 先更新 segmentStates
|
||
for (const pin of props.pins) {
|
||
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
|
||
if (!pin.constraint) {
|
||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
|
||
continue;
|
||
}
|
||
const pinState = getConstraintState(pin.constraint);
|
||
let newState: boolean;
|
||
if (props.cathodeType === "common") {
|
||
newState = pinState === "high";
|
||
} else {
|
||
newState = pinState === "low";
|
||
}
|
||
// 段状态只有在COM激活时才有效
|
||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
|
||
}
|
||
}
|
||
|
||
// 余晖判定:只有新状态持续 STABLE_THRESHOLD 次才更新 stableSegmentStates
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
const typedSegmentId = segmentId as keyof typeof segmentStates.value;
|
||
const current = segmentStates.value[typedSegmentId];
|
||
const stable = stableSegmentStates.value[typedSegmentId];
|
||
|
||
if (current === stable) {
|
||
segmentStableCounters.value[segmentId] = 0; // 状态一致,计数器清零
|
||
} else {
|
||
segmentStableCounters.value[segmentId]++;
|
||
if (segmentStableCounters.value[segmentId] >= STABLE_THRESHOLD) {
|
||
stableSegmentStates.value[typedSegmentId] = current;
|
||
segmentStableCounters.value[segmentId] = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新余晖存储槽 - 将当前段状态添加到存储槽中
|
||
function updateAfterglowBuffers() {
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
const typedSegmentId = segmentId as keyof typeof segmentStates.value;
|
||
const currentState = segmentStates.value[typedSegmentId];
|
||
|
||
// 将当前状态添加到存储槽的开头
|
||
afterglowBuffers.value[segmentId].unshift(currentState);
|
||
|
||
// 如果存储槽超过了最大容量,移除最旧的状态
|
||
if (
|
||
afterglowBuffers.value[segmentId].length > props.AFTERGLOW_BUFFER_SIZE
|
||
) {
|
||
afterglowBuffers.value[segmentId].pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 进入余晖模式
|
||
function enterAfterglowMode() {
|
||
isInAfterglowMode.value = true;
|
||
|
||
// 保存当前稳定状态作为余晖状态
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
|
||
afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
|
||
|
||
// 设置定时器,在余晖持续时间后退出余晖模式
|
||
if (afterglowTimers.value[segmentId]) {
|
||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||
}
|
||
|
||
afterglowTimers.value[segmentId] = setTimeout(() => {
|
||
afterglowStates.value[typedSegmentId] = false;
|
||
|
||
// 检查是否所有段都已经关闭
|
||
const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
|
||
if (allSegmentsOff) {
|
||
exitAfterglowMode();
|
||
}
|
||
}, AFTERGLOW_DURATION.value);
|
||
}
|
||
}
|
||
|
||
// 退出余晖模式
|
||
function exitAfterglowMode() {
|
||
isInAfterglowMode.value = false;
|
||
|
||
// 清除所有定时器
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
if (afterglowTimers.value[segmentId]) {
|
||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||
afterglowTimers.value[segmentId] = null;
|
||
}
|
||
|
||
// 重置余晖状态
|
||
const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
|
||
afterglowStates.value[typedSegmentId] = false;
|
||
}
|
||
}
|
||
|
||
// 监听约束状态变化
|
||
function onConstraintChange(constraint: string, level: string) {
|
||
const affectedPin = props.pins.find((pin) => pin.constraint === constraint);
|
||
if (affectedPin) {
|
||
updateSegmentStates();
|
||
}
|
||
}
|
||
|
||
// 生命周期钩子
|
||
onMounted(() => {
|
||
// 初始化余晖存储槽
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
afterglowBuffers.value[segmentId] = Array(props.AFTERGLOW_BUFFER_SIZE).fill(
|
||
false,
|
||
);
|
||
}
|
||
|
||
updateSegmentStates();
|
||
onConstraintStateChange(onConstraintChange);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
// 清理所有余晖定时器
|
||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||
if (afterglowTimers.value[segmentId]) {
|
||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 暴露属性和方法
|
||
defineExpose({
|
||
updateSegmentStates,
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.seven-segment-display {
|
||
display: inline-block;
|
||
position: relative;
|
||
}
|
||
|
||
.segment {
|
||
transition:
|
||
opacity 0.2s,
|
||
fill 0.2s;
|
||
}
|
||
|
||
/* 数码管发光效果 */
|
||
.segment[style*="opacity: 1"] {
|
||
filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
|
||
}
|
||
</style>
|
||
|
||
<!-- 导出默认属性函数供外部使用 -->
|
||
<script lang="ts">
|
||
export function getDefaultProps() {
|
||
return {
|
||
size: 1,
|
||
color: "red",
|
||
cathodeType: "common",
|
||
AFTERGLOW_DURATION: 500, // 默认余晖持续时间500毫秒
|
||
pins: [
|
||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||
{ pinId: "b", constraint: "", x: 25 - 1, y: 170 },
|
||
{ pinId: "c", constraint: "", x: 40 - 2, y: 170 },
|
||
{ pinId: "d", constraint: "", x: 55 - 3, y: 170 },
|
||
{ pinId: "e", constraint: "", x: 70 - 4, y: 170 },
|
||
{ pinId: "f", constraint: "", x: 85 - 5, y: 170 },
|
||
{ pinId: "g", constraint: "", x: 100 - 6, y: 170 },
|
||
{ pinId: "dp", constraint: "", x: 115 - 7, y: 170 },
|
||
{ pinId: "COM", constraint: "", x: 60, y: 10 },
|
||
],
|
||
};
|
||
}
|
||
</script>
|