This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FPGA_WebLab/src/components/equipments/SevenSegmentDisplay.vue
2025-07-20 10:33:57 +08:00

447 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>